diff --git a/.github/workflows/toolchain.yml b/.github/workflows/toolchain.yml index ac1cb47..8e53314 100644 --- a/.github/workflows/toolchain.yml +++ b/.github/workflows/toolchain.yml @@ -32,7 +32,7 @@ jobs: /etc/apt/sources.list.d/google-chrome.sources sudo add-apt-repository --yes ppa:ubuntu-toolchain-r/test sudo apt-get update - sudo apt-get install --yes ninja-build libcrypt-dev libidn2-dev ${{ matrix.packages }} + sudo apt-get install --yes ninja-build libcrypt-dev libidn2-dev libnghttp2-dev ${{ matrix.packages }} - name: Verify C++23 toolchain contract env: CXXFLAGS: ${{ matrix.cxxflags }} @@ -45,22 +45,23 @@ jobs: -DLAGHU_BUILD_PROFILE=FULL cmake --build "$RUNNER_TEMP/laghu-full" --target \ laghu_crypto_provider_test laghu_idna_test laghu_password_auth_test \ - laghu_password_auth_failure_token_test + laghu_password_auth_failure_token_test laghu_http2_test laghu_http2_contract_test ctest --test-dir "$RUNNER_TEMP/laghu-full" --output-on-failure \ - -R '^laghu\.(crypto\.provider|adapters\.(idna|password_auth|password_auth\.failure_token))$' + -R '^laghu\.(crypto\.provider|adapters\.(idna|password_auth|password_auth\.failure_token|http2|http2\.contract))$' - name: Verify system dynamic IDNA and password adapters if: matrix.name == 'gcc-14-libstdc++' run: | cmake -S . -B "$RUNNER_TEMP/laghu-system-adapters" -G Ninja \ -DCMAKE_CXX_COMPILER="${{ matrix.compiler }}" \ -DLAGHU_BUILD_PROFILE=CUSTOM \ - '-DLAGHU_FEATURES=idna;password_auth' \ + '-DLAGHU_FEATURES=idna;password_auth;http2' \ -DLAGHU_DEPENDENCY_SOURCE=SYSTEM \ -DLAGHU_DEPENDENCY_LINK_MODE=DYNAMIC cmake --build "$RUNNER_TEMP/laghu-system-adapters" --target \ - laghu_idna_test laghu_password_auth_test laghu_password_auth_failure_token_test + laghu_idna_test laghu_password_auth_test laghu_password_auth_failure_token_test \ + laghu_http2_test laghu_http2_contract_test ctest --test-dir "$RUNNER_TEMP/laghu-system-adapters" --output-on-failure \ - -R '^laghu\.adapters\.(idna|password_auth|password_auth\.failure_token)$' + -R '^laghu\.adapters\.(idna|password_auth|password_auth\.failure_token|http2|http2\.contract)$' - name: Verify Clang 19 crypto adapter if: matrix.name == 'clang-19-libc++' run: | @@ -80,11 +81,12 @@ jobs: -DCMAKE_CXX_COMPILER="${{ matrix.compiler }}" \ -DCMAKE_CXX_FLAGS="${{ matrix.cxxflags }}" \ -DLAGHU_BUILD_PROFILE=CUSTOM \ - '-DLAGHU_FEATURES=idna;password_auth' + '-DLAGHU_FEATURES=idna;password_auth;http2' cmake --build "$RUNNER_TEMP/laghu-clang-adapters" --target \ - laghu_idna_test laghu_password_auth_test laghu_password_auth_failure_token_test + laghu_idna_test laghu_password_auth_test laghu_password_auth_failure_token_test \ + laghu_http2_test laghu_http2_contract_test ctest --test-dir "$RUNNER_TEMP/laghu-clang-adapters" --output-on-failure \ - -R '^laghu\.adapters\.(idna|password_auth|password_auth\.failure_token)$' + -R '^laghu\.adapters\.(idna|password_auth|password_auth\.failure_token|http2|http2\.contract)$' - name: Verify LibreSSL crypto adapter if: matrix.name == 'gcc-14-libstdc++' run: | @@ -139,11 +141,12 @@ jobs: cmake -S . -B "$RUNNER_TEMP/laghu-adapters" -G Ninja \ -DCMAKE_CXX_COMPILER=clang++ \ -DLAGHU_BUILD_PROFILE=CUSTOM \ - '-DLAGHU_FEATURES=idna;password_auth' + '-DLAGHU_FEATURES=idna;password_auth;http2' cmake --build "$RUNNER_TEMP/laghu-adapters" --target \ - laghu_idna_test laghu_password_auth_test laghu_password_auth_failure_token_test + laghu_idna_test laghu_password_auth_test laghu_password_auth_failure_token_test \ + laghu_http2_test laghu_http2_contract_test ctest --test-dir "$RUNNER_TEMP/laghu-adapters" --output-on-failure \ - -R '^laghu\.adapters\.(idna|password_auth|password_auth\.failure_token)$' + -R '^laghu\.adapters\.(idna|password_auth|password_auth\.failure_token|http2|http2\.contract)$' - uses: actions/upload-artifact@v7 with: name: toolchain-capabilities-macos-appleclang @@ -173,11 +176,12 @@ jobs: cmake -S . -B /tmp/laghu-adapters -G Ninja \ -DCMAKE_CXX_COMPILER=clang++ \ -DLAGHU_BUILD_PROFILE=CUSTOM \ - '-DLAGHU_FEATURES=idna;password_auth' + '-DLAGHU_FEATURES=idna;password_auth;http2' cmake --build /tmp/laghu-adapters --target \ - laghu_idna_test laghu_password_auth_test laghu_password_auth_failure_token_test + laghu_idna_test laghu_password_auth_test laghu_password_auth_failure_token_test \ + laghu_http2_test laghu_http2_contract_test ctest --test-dir /tmp/laghu-adapters --output-on-failure \ - -R '^laghu\.adapters\.(idna|password_auth|password_auth\.failure_token)$' + -R '^laghu\.adapters\.(idna|password_auth|password_auth\.failure_token|http2|http2\.contract)$' linux-aarch64-cross: runs-on: ubuntu-24.04 steps: @@ -193,7 +197,7 @@ jobs: run: | cmake --preset linux-aarch64-gcc \ -DLAGHU_BUILD_PROFILE=CUSTOM \ - -DLAGHU_FEATURES=tls + '-DLAGHU_FEATURES=tls;http2' cmake --build build/linux-aarch64-gcc --target laghu_verify_toolchain - name: Verify ARM64 archive run: | @@ -201,6 +205,8 @@ jobs: aarch64-linux-gnu-ar t build/linux-aarch64-gcc/liblaghu_core.a test -f build/linux-aarch64-gcc/liblaghu_crypto.a aarch64-linux-gnu-ar t build/linux-aarch64-gcc/liblaghu_crypto.a + test -f build/linux-aarch64-gcc/liblaghu_http2.a + aarch64-linux-gnu-ar t build/linux-aarch64-gcc/liblaghu_http2.a linux-clang-asan-ubsan: runs-on: ubuntu-24.04 steps: @@ -236,11 +242,12 @@ jobs: -DCMAKE_CXX_FLAGS=-stdlib=libc++ \ -DLAGHU_SANITIZER_PROFILE=ASAN_UBSAN \ -DLAGHU_BUILD_PROFILE=CUSTOM \ - '-DLAGHU_FEATURES=idna;password_auth' + '-DLAGHU_FEATURES=idna;password_auth;http2' cmake --build "$RUNNER_TEMP/laghu-asan-adapters" --target \ - laghu_idna_test laghu_password_auth_test laghu_password_auth_failure_token_test + laghu_idna_test laghu_password_auth_test laghu_password_auth_failure_token_test \ + laghu_http2_test laghu_http2_contract_test ctest --test-dir "$RUNNER_TEMP/laghu-asan-adapters" --output-on-failure \ - -R '^laghu\.adapters\.(idna|password_auth|password_auth\.failure_token)$' + -R '^laghu\.adapters\.(idna|password_auth|password_auth\.failure_token|http2|http2\.contract)$' - name: Replay binary-envelope fuzz corpus under ASan and UBSan run: | cmake -S . -B "$RUNNER_TEMP/laghu-fuzz" -G Ninja \ diff --git a/CMakeLists.txt b/CMakeLists.txt index 7369d1c..b3b5f67 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -228,6 +228,22 @@ if(NOT laghu_structured_data_feature_index EQUAL -1) endif() endif() +list(FIND LAGHU_EFFECTIVE_FEATURES http2 laghu_http2_feature_index) +if(NOT laghu_http2_feature_index EQUAL -1) + add_library(laghu_http2 STATIC + src/adapters/http2.cpp + $) + laghu_apply_first_party_contract(laghu_http2) + laghu_configure_api_consumer(laghu_http2 adapters) + laghu_register_subsystem_target(laghu_http2 adapters) + laghu_link_subsystems(laghu_http2 laghu_core) + target_link_libraries(laghu_http2 PRIVATE laghu_feature_http2) + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set_source_files_properties(src/adapters/http2.cpp PROPERTIES + COMPILE_OPTIONS "-Wno-old-style-cast;-Wno-unsafe-buffer-usage;-Wno-zero-as-null-pointer-constant") + endif() +endif() + add_executable(laghu_core_smoke src/core/smoke.cpp) laghu_apply_first_party_contract(laghu_core_smoke) laghu_configure_api_consumer(laghu_core_smoke core) @@ -350,6 +366,23 @@ if(TARGET laghu_structured_data) --runs 1) endif() endif() + +if(TARGET laghu_http2) + add_executable(laghu_http2_test tests/adapters/http2.cpp) + laghu_apply_first_party_contract(laghu_http2_test) + laghu_configure_api_consumer(laghu_http2_test adapters) + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set_source_files_properties(tests/adapters/http2.cpp PROPERTIES + COMPILE_OPTIONS -Wno-unsafe-buffer-usage) + endif() + target_link_libraries(laghu_http2_test PRIVATE laghu_http2 laghu_test_support) + laghu_add_native_test(laghu.adapters.http2 laghu_http2_test) + + add_executable(laghu_http2_contract_test tests/adapters/http2_contract.cpp) + laghu_apply_first_party_contract(laghu_http2_contract_test) + laghu_configure_api_consumer(laghu_http2_contract_test adapters) + laghu_add_native_test(laghu.adapters.http2.contract laghu_http2_contract_test) +endif() laghu_write_fuzz_target_registry() laghu_add_fuzz_validation_tests() @@ -532,6 +565,9 @@ if(TARGET laghu_structured_data_test) list(APPEND laghu_verify_targets laghu_structured_data laghu_structured_data_test laghu_structured_data_contract_test) endif() +if(TARGET laghu_http2_test) + list(APPEND laghu_verify_targets laghu_http2 laghu_http2_test laghu_http2_contract_test) +endif() laghu_collect_fuzz_targets(laghu_fuzz_verify_targets) list(APPEND laghu_verify_targets ${laghu_fuzz_verify_targets}) foreach(target IN ITEMS laghu_sanitizer_heap_misuse_fixture diff --git a/cmake/LaghuBuildIdentity.cmake b/cmake/LaghuBuildIdentity.cmake index dfa6652..9b95f37 100644 --- a/cmake/LaghuBuildIdentity.cmake +++ b/cmake/LaghuBuildIdentity.cmake @@ -111,6 +111,12 @@ function(laghu_build_identity_input_hashes output) src/adapters/contract/laghu/adapters/structured_data.hpp src/adapters/structured_data.cpp) endif() + list(FIND LAGHU_EFFECTIVE_FEATURES http2 http2_feature_index) + if(NOT http2_feature_index EQUAL -1) + list(APPEND inputs + src/adapters/contract/laghu/adapters/http2.hpp + src/adapters/http2.cpp) + endif() set(entries) foreach(input IN LISTS inputs) set(path "${CMAKE_SOURCE_DIR}/${input}") diff --git a/cmake/LaghuDependencies.cmake b/cmake/LaghuDependencies.cmake index e5529ac..fddac8b 100644 --- a/cmake/LaghuDependencies.cmake +++ b/cmake/LaghuDependencies.cmake @@ -581,7 +581,13 @@ function(laghu_acquire_vendored_cmake_dependency id private_target) set(CARES_SHARED ON CACHE BOOL "Build c-ares shared library" FORCE) endif() endif() + # Dependency projects must not select Laghu's build configuration through a + # shared cache entry. nghttp2 defaults an empty build type to RelWithDebInfo; + # restore the caller-owned value after its subdirectory is configured. + set(laghu_saved_build_type "${CMAKE_BUILD_TYPE}") FetchContent_MakeAvailable("${content_name}") + set(CMAKE_BUILD_TYPE "${laghu_saved_build_type}" CACHE STRING + "Choose the type of build" FORCE) laghu_dependency_property("${id}" VENDORED_VERSION vendored_version) set_property(GLOBAL PROPERTY "LAGHU_DEPENDENCY_SELECTED_VERSION_${id}" "${vendored_version}") laghu_find_vendored_cmake_target("${id}" vendored_target) diff --git a/src/adapters/contract/laghu/adapters/http2.hpp b/src/adapters/contract/laghu/adapters/http2.hpp new file mode 100644 index 0000000..066b3a9 --- /dev/null +++ b/src/adapters/contract/laghu/adapters/http2.hpp @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +namespace laghu::adapters { + +enum class Http2Role : std::uint8_t { client, server }; + +class Http2StreamId final { + public: + constexpr Http2StreamId() noexcept = default; + + [[nodiscard]] static constexpr core::Result from_wire( + std::int32_t value) noexcept { + if (value <= 0) { + return std::unexpected{core::Error{core::ErrorDomain::core, + core::ErrorCode::invalid_input, 0, + "HTTP/2 stream ID must be positive"}}; + } + return Http2StreamId{value}; + } + + [[nodiscard]] constexpr bool valid() const noexcept { return value_ > 0; } + [[nodiscard]] constexpr std::int32_t wire_value() const noexcept { return value_; } + + private: + friend class Http2Session; + explicit constexpr Http2StreamId(std::int32_t value) noexcept : value_(value) {} + std::int32_t value_{}; +}; + +struct Http2Limits final { + static constexpr std::size_t maximum_submission_headers = 64; + static constexpr std::size_t maximum_settings = 16; + + std::size_t maximum_header_block_bytes{}; + std::size_t maximum_header_fields{}; + std::size_t maximum_debug_data_bytes{}; +}; + +struct Http2Header final { + core::ByteView name{}; + core::ByteView value{}; + bool sensitive{}; +}; + +struct Http2Setting final { + std::uint16_t identifier{}; + std::uint32_t value{}; +}; + +enum class Http2EventKind : std::uint8_t { + headers_begin, + header, + data, + frame_received, + settings, + reset, + goaway, + window_update, + stream_closed, +}; + +struct Http2Event final { + // Byte views are valid only for the duration of the sink callback. + Http2EventKind kind{Http2EventKind::frame_received}; + Http2StreamId stream{}; + core::ByteView name{}; + core::ByteView value{}; + core::ByteView data{}; + std::uint32_t code{}; + std::uint32_t setting_value{}; + std::int32_t last_stream_id{}; + std::int32_t window_increment{}; + core::ByteView debug_data{}; + bool end_stream{}; + bool sensitive{}; + bool settings_ack{}; +}; + +enum class Http2CallbackAction : std::uint8_t { + continue_processing, + // Supported only for header and DATA events. Resume receive() with the + // unconsumed input suffix. + pause, + // Supported for header-begin, header, and DATA events. Other uses fail the + // session rather than silently ignoring the action. + reject_stream, + fail_session, +}; + +enum class Http2DataReadState : std::uint8_t { more, end, deferred }; + +struct Http2DataReadResult final { + std::size_t bytes{}; + Http2DataReadState state{Http2DataReadState::more}; +}; + +using Http2DataRead = core::Result (*)( + void*, core::MutableByteView) noexcept; + +struct Http2DataProvider final { + // The caller owns both fields and must keep them valid until the provider + // reports end, its stream closes, or the session is destroyed. + void* context{}; + Http2DataRead read{}; + + [[nodiscard]] constexpr bool enabled() const noexcept { return read != nullptr; } +}; + +using Http2EventWrite = Http2CallbackAction (*)(void*, const Http2Event&) noexcept; + +struct Http2EventSink final { + // The caller owns both fields and must keep them valid for the session. + void* context{}; + Http2EventWrite write{}; + + [[nodiscard]] constexpr bool enabled() const noexcept { return write != nullptr; } +}; + +struct Http2IoInterest final { + bool read{}; + bool write{}; + // nghttp2 owns no clock or timer. The caller may combine this false value + // with its own stream/session deadlines. + bool timer{}; +}; + +class Http2Session final { + public: + Http2Session(const Http2Session&) = delete; + Http2Session& operator=(const Http2Session&) = delete; + Http2Session(Http2Session&& other) noexcept; + Http2Session& operator=(Http2Session&& other) noexcept; + ~Http2Session(); + + // The arena object must outlive the session. Resetting it invalidates the + // session and all subsequent operations, but the arena object must remain. + [[nodiscard]] static core::Result create( + core::WorkerId worker, Http2Role role, core::BoundedArena& arena, + Http2Limits limits, Http2EventSink event_sink = {}, + DependencyLogSink log_sink = {}) noexcept; + + // A callback may pause parsing. When fewer than input.size() bytes are + // consumed, retain input and pass its unconsumed suffix to receive() again. + [[nodiscard]] core::Result receive(core::ByteView input) noexcept; + // Returned bytes remain valid until the next operation on this session. + [[nodiscard]] core::Result next_output() noexcept; + [[nodiscard]] core::Result interest() const noexcept; + + [[nodiscard]] core::Result submit_headers( + std::span headers, bool end_stream) noexcept; + [[nodiscard]] core::Result submit_headers( + Http2StreamId stream, std::span headers, + bool end_stream) noexcept; + [[nodiscard]] core::Result submit_settings( + std::span settings) noexcept; + [[nodiscard]] core::Result submit_data( + Http2StreamId stream, Http2DataProvider& provider, bool end_stream) noexcept; + // Resumes only an outbound provider that previously reported deferred. + [[nodiscard]] core::Result resume_data(Http2StreamId stream) noexcept; + [[nodiscard]] core::Result submit_reset(Http2StreamId stream, + std::uint32_t error_code) noexcept; + [[nodiscard]] core::Result submit_goaway(std::int32_t last_stream_id, + std::uint32_t error_code, + core::ByteView debug_data) noexcept; + [[nodiscard]] core::Result submit_connection_window_update( + std::int32_t increment) noexcept; + [[nodiscard]] core::Result submit_stream_window_update( + Http2StreamId stream, std::int32_t increment) noexcept; + + private: + constexpr Http2Session(void* native_session, void* native_state, + const core::BoundedArena& arena, + std::uint64_t arena_generation) noexcept + : native_session_(native_session), native_state_(native_state), arena_(&arena), + arena_generation_(arena_generation) {} + + [[nodiscard]] core::Result require_valid() const noexcept; + [[nodiscard]] core::Result submit_headers_impl( + std::int32_t stream_id, std::span headers, + bool end_stream, Http2StreamId* assigned) noexcept; + void release() noexcept; + void move_from(Http2Session&& other) noexcept; + + void* native_session_{}; + void* native_state_{}; + const core::BoundedArena* arena_{}; + std::uint64_t arena_generation_{}; +}; + +static_assert(std::is_trivially_copyable_v); +static_assert(std::is_trivially_copyable_v); +static_assert(std::is_trivially_copyable_v); +static_assert(std::is_trivially_copyable_v); +static_assert(std::is_trivially_copyable_v); +static_assert(std::is_trivially_copyable_v); +static_assert(!std::is_copy_constructible_v); + +} // namespace laghu::adapters diff --git a/src/adapters/dependency.cpp b/src/adapters/dependency.cpp index 91c57dd..864a014 100644 --- a/src/adapters/dependency.cpp +++ b/src/adapters/dependency.cpp @@ -47,6 +47,10 @@ namespace { case core::DependencyOperation::idna_lookup: case core::DependencyOperation::password_verify: case core::DependencyOperation::json_parse: + case core::DependencyOperation::http2_session: + case core::DependencyOperation::http2_receive: + case core::DependencyOperation::http2_send: + case core::DependencyOperation::http2_submit: return value; } return core::DependencyOperation::none; @@ -131,6 +135,14 @@ namespace { return "password_verify"; case core::DependencyOperation::json_parse: return "json_parse"; + case core::DependencyOperation::http2_session: + return "http2_session"; + case core::DependencyOperation::http2_receive: + return "http2_receive"; + case core::DependencyOperation::http2_send: + return "http2_send"; + case core::DependencyOperation::http2_submit: + return "http2_submit"; } return "none"; } diff --git a/src/adapters/http2.cpp b/src/adapters/http2.cpp new file mode 100644 index 0000000..0072af4 --- /dev/null +++ b/src/adapters/http2.cpp @@ -0,0 +1,744 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace laghu::adapters { +namespace { + +constexpr std::size_t maximum_pending_resets = 64; + +struct alignas(std::max_align_t) AllocationHeader final { + std::size_t capacity{}; + std::size_t used_size{}; + AllocationHeader* next{}; +}; + +struct NativeState final { + core::BoundedArena* arena{}; + core::WorkerId worker; + Http2Limits limits{}; + Http2EventSink event_sink{}; + DependencyLogSink log_sink{}; + std::size_t header_bytes{}; + std::size_t header_fields{}; + AllocationHeader* free_allocations{}; + std::array pending_reset_streams{}; + std::size_t pending_reset_count{}; + core::Error data_provider_error{core::ErrorDomain::core, core::ErrorCode::invalid_state, + 0, "HTTP/2 data provider failed"}; + bool input_limit_failed{}; + bool pending_reset_overflow{}; + bool data_provider_failed{}; +}; + +[[nodiscard]] core::DependencyStatus native_status(int code) noexcept { + if (code == NGHTTP2_ERR_NOMEM) { + return core::DependencyStatus::exhaustion; + } + if (code == NGHTTP2_ERR_INVALID_ARGUMENT || code == NGHTTP2_ERR_INVALID_HEADER_BLOCK) { + return core::DependencyStatus::invalid_input; + } + if (code == NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE) { + return core::DependencyStatus::exhaustion; + } + return core::DependencyStatus::corrupt_data; +} + +[[nodiscard]] core::Error native_error(core::DependencyOperation operation, int code, + const DependencyLogSink& sink) noexcept { + const core::Error error = normalize_dependency_error( + core::DependencyId::nghttp2, operation, native_status(code), code); + log_dependency_error(sink, error); + return error; +} + +[[nodiscard]] core::Error core_error(core::ErrorCode code, + const char* diagnostic) noexcept { + return {core::ErrorDomain::core, code, 0, diagnostic}; +} + +[[nodiscard]] void* arena_allocate(NativeState& state, std::size_t size) noexcept { + const std::size_t payload_size = size == 0U ? 1U : size; + AllocationHeader* previous{}; + AllocationHeader* reusable = state.free_allocations; + while (reusable != nullptr && reusable->capacity < payload_size) { + previous = reusable; + reusable = reusable->next; + } + if (reusable != nullptr) { + if (previous == nullptr) { + state.free_allocations = reusable->next; + } else { + previous->next = reusable->next; + } + reusable->used_size = payload_size; + reusable->next = nullptr; + return static_cast(reusable + 1); + } + if (payload_size > std::numeric_limits::max() - sizeof(AllocationHeader)) { + return nullptr; + } + const auto allocation = state.arena->try_allocate( + state.worker, sizeof(AllocationHeader) + payload_size, alignof(std::max_align_t)); + if (!allocation.has_value()) { + return nullptr; + } + const auto bytes = allocation->bytes(); + if (!bytes.has_value()) { + return nullptr; + } + auto* const header = ::new (bytes->data()) AllocationHeader{ + payload_size, payload_size, nullptr}; + return static_cast(header + 1); +} + +void* memory_malloc(std::size_t size, void* context) noexcept { + return arena_allocate(*static_cast(context), size); +} + +void memory_free(void* pointer, void* context) noexcept { + if (pointer == nullptr) { + return; + } + auto& state = *static_cast(context); + auto* const header = static_cast(pointer) - 1; + header->next = state.free_allocations; + state.free_allocations = header; +} + +void* memory_calloc(std::size_t count, std::size_t size, void* context) noexcept { + auto& state = *static_cast(context); + if (count != 0U && size > std::numeric_limits::max() / count) { + return nullptr; + } + const std::size_t total = count * size; + void* const output = arena_allocate(state, total); + if (output != nullptr) { + std::memset(output, 0, total); + } + return output; +} + +void* memory_realloc(void* pointer, std::size_t size, void* context) noexcept { + auto& state = *static_cast(context); + if (pointer == nullptr) { + return arena_allocate(state, size); + } + if (size == 0U) { + memory_free(pointer, context); + return nullptr; + } + auto* const old_header = static_cast(pointer) - 1; + if (size <= old_header->capacity) { + old_header->used_size = size; + return pointer; + } + void* const output = arena_allocate(state, size); + if (output == nullptr) { + return nullptr; + } + const std::size_t copied = old_header->used_size < size ? old_header->used_size : size; + std::memcpy(output, pointer, copied); + memory_free(pointer, context); + return output; +} + +[[nodiscard]] Http2StreamId stream_id(std::int32_t value) noexcept { + const auto converted = Http2StreamId::from_wire(value); + return converted.has_value() ? *converted : Http2StreamId{}; +} + +[[nodiscard]] int callback_result(NativeState& state, const Http2Event& event, + bool pausable, bool rejectable) noexcept { + if (!state.event_sink.enabled()) { + return 0; + } + const Http2CallbackAction action = state.event_sink.write(state.event_sink.context, event); + if (action == Http2CallbackAction::continue_processing) { + return 0; + } + if (action == Http2CallbackAction::pause && pausable) { + return NGHTTP2_ERR_PAUSE; + } + if (action == Http2CallbackAction::reject_stream && rejectable) { + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + if (action == Http2CallbackAction::reject_stream && event.kind == Http2EventKind::data && + event.stream.valid()) { + const std::int32_t rejected_stream = event.stream.wire_value(); + for (std::size_t index = 0; index < state.pending_reset_count; ++index) { + if (state.pending_reset_streams[index] == rejected_stream) { + return 0; + } + } + if (state.pending_reset_count == state.pending_reset_streams.size()) { + state.pending_reset_overflow = true; + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + state.pending_reset_streams[state.pending_reset_count] = rejected_stream; + ++state.pending_reset_count; + return NGHTTP2_ERR_PAUSE; + } + return NGHTTP2_ERR_CALLBACK_FAILURE; +} + +ssize_t on_data_source_read(nghttp2_session*, std::int32_t, std::uint8_t* buffer, + std::size_t length, std::uint32_t* data_flags, + nghttp2_data_source* source, void* user_data) noexcept { + auto& state = *static_cast(user_data); + auto* const provider = static_cast(source->ptr); + if (provider == nullptr || !provider->enabled()) { + state.data_provider_error = core_error(core::ErrorCode::invalid_state, + "HTTP/2 data provider is inactive"); + state.data_provider_failed = true; + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + const auto output = core::MutableByteView::from( + {reinterpret_cast(buffer), length}); + if (!output.has_value()) { + state.data_provider_error = output.error(); + state.data_provider_failed = true; + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + const auto produced = provider->read(provider->context, *output); + if (!produced.has_value()) { + state.data_provider_error = produced.error(); + state.data_provider_failed = true; + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + if (produced->bytes > length || + (produced->state == Http2DataReadState::deferred && produced->bytes != 0U)) { + state.data_provider_error = core_error(core::ErrorCode::invalid_range, + "HTTP/2 data provider returned an invalid size"); + state.data_provider_failed = true; + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + if (produced->state == Http2DataReadState::deferred) { + return NGHTTP2_ERR_DEFERRED; + } + if (produced->state == Http2DataReadState::end) { + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + } + return static_cast(produced->bytes); +} + +int on_begin_headers(nghttp2_session*, const nghttp2_frame* frame, + void* user_data) noexcept { + auto& state = *static_cast(user_data); + state.header_bytes = 0; + state.header_fields = 0; + Http2Event event{}; + event.kind = Http2EventKind::headers_begin; + event.stream = stream_id(frame->hd.stream_id); + event.end_stream = (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) != 0U; + return callback_result(state, event, false, true); +} + +int on_header(nghttp2_session*, const nghttp2_frame* frame, + const std::uint8_t* name, std::size_t name_length, + const std::uint8_t* value, std::size_t value_length, + std::uint8_t flags, void* user_data) noexcept { + auto& state = *static_cast(user_data); + if (name_length > std::numeric_limits::max() - value_length || + state.header_bytes > std::numeric_limits::max() - name_length - value_length) { + state.input_limit_failed = true; + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + state.header_bytes += name_length + value_length; + ++state.header_fields; + if (state.header_bytes > state.limits.maximum_header_block_bytes || + state.header_fields > state.limits.maximum_header_fields) { + state.input_limit_failed = true; + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + const auto name_view = core::ByteView::from( + {reinterpret_cast(name), name_length}); + const auto value_view = core::ByteView::from( + {reinterpret_cast(value), value_length}); + if (!name_view.has_value() || !value_view.has_value()) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + Http2Event event{}; + event.kind = Http2EventKind::header; + event.stream = stream_id(frame->hd.stream_id); + event.name = *name_view; + event.value = *value_view; + event.end_stream = (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) != 0U; + event.sensitive = (flags & NGHTTP2_NV_FLAG_NO_INDEX) != 0U; + return callback_result(state, event, true, true); +} + +int on_data(nghttp2_session*, std::uint8_t flags, std::int32_t native_stream_id, + const std::uint8_t* data, std::size_t length, void* user_data) noexcept { + auto& state = *static_cast(user_data); + const auto data_view = core::ByteView::from( + {reinterpret_cast(data), length}); + if (!data_view.has_value()) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + Http2Event event{}; + event.kind = Http2EventKind::data; + event.stream = stream_id(native_stream_id); + event.data = *data_view; + event.end_stream = (flags & NGHTTP2_FLAG_END_STREAM) != 0U; + return callback_result(state, event, true, false); +} + +int on_frame_received(nghttp2_session*, const nghttp2_frame* frame, + void* user_data) noexcept { + auto& state = *static_cast(user_data); + Http2Event event{}; + event.kind = Http2EventKind::frame_received; + event.stream = stream_id(frame->hd.stream_id); + event.end_stream = (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) != 0U; + switch (frame->hd.type) { + case NGHTTP2_SETTINGS: + event.kind = Http2EventKind::settings; + event.settings_ack = (frame->hd.flags & NGHTTP2_FLAG_ACK) != 0U; + if (frame->settings.niv == 0U) { + return callback_result(state, event, false, false); + } + for (std::size_t index = 0; index < frame->settings.niv; ++index) { + event.code = static_cast(frame->settings.iv[index].settings_id); + event.setting_value = frame->settings.iv[index].value; + const int result = callback_result(state, event, false, false); + if (result != 0) { + return result; + } + } + return 0; + case NGHTTP2_RST_STREAM: + event.kind = Http2EventKind::reset; + event.code = frame->rst_stream.error_code; + break; + case NGHTTP2_GOAWAY: + if (frame->goaway.opaque_data_len > state.limits.maximum_debug_data_bytes) { + state.input_limit_failed = true; + return NGHTTP2_ERR_CALLBACK_FAILURE; + } + event.kind = Http2EventKind::goaway; + event.code = frame->goaway.error_code; + event.last_stream_id = frame->goaway.last_stream_id; + event.debug_data = *core::ByteView::from( + {reinterpret_cast(frame->goaway.opaque_data), + frame->goaway.opaque_data_len}); + break; + case NGHTTP2_WINDOW_UPDATE: + event.kind = Http2EventKind::window_update; + event.window_increment = frame->window_update.window_size_increment; + break; + default: + break; + } + return callback_result(state, event, false, false); +} + +int on_stream_closed(nghttp2_session*, std::int32_t native_stream_id, + std::uint32_t error_code, void* user_data) noexcept { + auto& state = *static_cast(user_data); + Http2Event event{}; + event.kind = Http2EventKind::stream_closed; + event.stream = stream_id(native_stream_id); + event.code = error_code; + return callback_result(state, event, false, false); +} + +[[nodiscard]] core::Result validate_limits(const Http2Limits& limits) noexcept { + if (limits.maximum_header_block_bytes == 0U || limits.maximum_header_fields == 0U || + limits.maximum_header_fields > Http2Limits::maximum_submission_headers) { + return std::unexpected{core_error(core::ErrorCode::invalid_range, + "HTTP/2 limits must be nonzero and bounded")}; + } + return {}; +} + +} // namespace + +Http2Session::Http2Session(Http2Session&& other) noexcept { move_from(std::move(other)); } + +Http2Session& Http2Session::operator=(Http2Session&& other) noexcept { + if (this != &other) { + release(); + move_from(std::move(other)); + } + return *this; +} + +Http2Session::~Http2Session() { release(); } + +core::Result Http2Session::create( + core::WorkerId worker, Http2Role role, core::BoundedArena& arena, + Http2Limits limits, Http2EventSink event_sink, + DependencyLogSink log_sink) noexcept { + if (role != Http2Role::client && role != Http2Role::server) { + return std::unexpected{core_error(core::ErrorCode::invalid_input, + "HTTP/2 role is invalid")}; + } + if (const auto valid = validate_limits(limits); !valid.has_value()) { + return std::unexpected{valid.error()}; + } + const auto state_storage = arena.try_allocate(worker, sizeof(NativeState), alignof(NativeState)); + if (!state_storage.has_value()) { + return std::unexpected{state_storage.error()}; + } + const auto state_bytes = state_storage->bytes(); + if (!state_bytes.has_value()) { + return std::unexpected{state_bytes.error()}; + } + auto* const state = ::new (state_bytes->data()) NativeState{ + &arena, worker, limits, event_sink, log_sink, 0, 0, nullptr, {}, 0, + core_error(core::ErrorCode::invalid_state, "HTTP/2 data provider failed"), + false, false, false}; + + nghttp2_session_callbacks* callbacks{}; + int result = nghttp2_session_callbacks_new(&callbacks); + if (result != 0) { + return std::unexpected{native_error(core::DependencyOperation::http2_session, + result, log_sink)}; + } + nghttp2_session_callbacks_set_on_begin_headers_callback(callbacks, on_begin_headers); + nghttp2_session_callbacks_set_on_header_callback(callbacks, on_header); + nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, on_data); + nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, on_frame_received); + nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, on_stream_closed); + + nghttp2_option* options{}; + result = nghttp2_option_new(&options); + if (result != 0) { + nghttp2_session_callbacks_del(callbacks); + return std::unexpected{native_error(core::DependencyOperation::http2_session, + result, log_sink)}; + } + nghttp2_option_set_no_auto_window_update(options, 1); + + nghttp2_mem memory{state, memory_malloc, memory_free, memory_calloc, memory_realloc}; + nghttp2_session* native_session{}; + if (role == Http2Role::client) { + result = nghttp2_session_client_new3(&native_session, callbacks, state, options, &memory); + } else { + result = nghttp2_session_server_new3(&native_session, callbacks, state, options, &memory); + } + nghttp2_option_del(options); + nghttp2_session_callbacks_del(callbacks); + if (result != 0) { + return std::unexpected{native_error(core::DependencyOperation::http2_session, + result, log_sink)}; + } + return Http2Session{native_session, state, arena, arena.generation()}; +} + +core::Result Http2Session::require_valid() const noexcept { + if (native_session_ == nullptr || native_state_ == nullptr || arena_ == nullptr || + arena_->generation() != arena_generation_) { + return std::unexpected{core_error(core::ErrorCode::invalid_state, + "HTTP/2 session is inactive or its arena was reset")}; + } + return {}; +} + +core::Result Http2Session::receive(core::ByteView input) noexcept { + if (const auto valid = require_valid(); !valid.has_value()) { + return std::unexpected{valid.error()}; + } + const auto* const bytes = reinterpret_cast(input.data()); + const ssize_t result = nghttp2_session_mem_recv( + static_cast(native_session_), bytes, input.size()); + auto& state = *static_cast(native_state_); + if (state.input_limit_failed) { + state.input_limit_failed = false; + return std::unexpected{core_error(core::ErrorCode::invalid_range, + "HTTP/2 peer input exceeds its configured bound")}; + } + if (state.pending_reset_overflow) { + state.pending_reset_overflow = false; + state.pending_reset_count = 0; + return std::unexpected{core_error(core::ErrorCode::exhaustion, + "HTTP/2 rejected-stream queue is exhausted")}; + } + if (result < 0) { + return std::unexpected{native_error(core::DependencyOperation::http2_receive, + static_cast(result), state.log_sink)}; + } + for (std::size_t index = 0; index < state.pending_reset_count; ++index) { + const std::int32_t reset_stream = state.pending_reset_streams[index]; + const int reset_result = nghttp2_submit_rst_stream( + static_cast(native_session_), NGHTTP2_FLAG_NONE, + reset_stream, NGHTTP2_CANCEL); + if (reset_result != 0 && reset_result != NGHTTP2_ERR_STREAM_CLOSED) { + state.pending_reset_count = 0; + return std::unexpected{native_error(core::DependencyOperation::http2_submit, + reset_result, state.log_sink)}; + } + } + state.pending_reset_count = 0; + return static_cast(result); +} + +core::Result Http2Session::next_output() noexcept { + if (const auto valid = require_valid(); !valid.has_value()) { + return std::unexpected{valid.error()}; + } + const std::uint8_t* data{}; + const ssize_t result = nghttp2_session_mem_send( + static_cast(native_session_), &data); + auto& state = *static_cast(native_state_); + if (state.data_provider_failed) { + state.data_provider_failed = false; + return std::unexpected{state.data_provider_error}; + } + if (result < 0) { + return std::unexpected{native_error(core::DependencyOperation::http2_send, + static_cast(result), state.log_sink)}; + } + return core::ByteView::from({reinterpret_cast(data), + static_cast(result)}); +} + +core::Result Http2Session::interest() const noexcept { + if (const auto valid = require_valid(); !valid.has_value()) { + return std::unexpected{valid.error()}; + } + auto* const session = static_cast(native_session_); + return Http2IoInterest{nghttp2_session_want_read(session) != 0, + nghttp2_session_want_write(session) != 0, false}; +} + +core::Result Http2Session::submit_headers_impl( + std::int32_t native_stream_id, std::span headers, + bool end_stream, Http2StreamId* assigned) noexcept { + if (const auto valid = require_valid(); !valid.has_value()) { + return std::unexpected{valid.error()}; + } + auto& state = *static_cast(native_state_); + if (headers.empty() || headers.size() > state.limits.maximum_header_fields || + headers.size() > Http2Limits::maximum_submission_headers) { + return std::unexpected{core_error(core::ErrorCode::invalid_range, + "HTTP/2 header submission exceeds its bound")}; + } + std::array native_headers{}; + std::size_t total{}; + for (std::size_t index = 0; index < headers.size(); ++index) { + const Http2Header& header = headers[index]; + if (header.name.empty() || header.name.size() > state.limits.maximum_header_block_bytes - total || + header.value.size() > state.limits.maximum_header_block_bytes - total - header.name.size()) { + return std::unexpected{core_error(core::ErrorCode::invalid_range, + "HTTP/2 header block exceeds its bound")}; + } + total += header.name.size() + header.value.size(); + native_headers[index] = nghttp2_nv{ + const_cast(reinterpret_cast(header.name.data())), + const_cast(reinterpret_cast(header.value.data())), + header.name.size(), header.value.size(), + static_cast(header.sensitive ? NGHTTP2_NV_FLAG_NO_INDEX + : NGHTTP2_NV_FLAG_NONE)}; + } + const std::uint8_t flags = static_cast( + NGHTTP2_FLAG_END_HEADERS | (end_stream ? NGHTTP2_FLAG_END_STREAM : 0)); + const std::int32_t result = nghttp2_submit_headers( + static_cast(native_session_), flags, native_stream_id, + nullptr, native_headers.data(), headers.size(), nullptr); + if (result < 0) { + return std::unexpected{native_error(core::DependencyOperation::http2_submit, + result, state.log_sink)}; + } + if (assigned != nullptr) { + const auto converted = Http2StreamId::from_wire(result); + if (!converted.has_value()) { + return std::unexpected{converted.error()}; + } + *assigned = *converted; + } + return {}; +} + +core::Result Http2Session::submit_headers( + std::span headers, bool end_stream) noexcept { + Http2StreamId assigned{}; + if (const auto result = submit_headers_impl(-1, headers, end_stream, &assigned); + !result.has_value()) { + return std::unexpected{result.error()}; + } + return assigned; +} + +core::Result Http2Session::submit_headers( + Http2StreamId stream, std::span headers, + bool end_stream) noexcept { + if (!stream.valid()) { + return std::unexpected{core_error(core::ErrorCode::invalid_input, + "HTTP/2 stream ID is invalid")}; + } + return submit_headers_impl(stream.wire_value(), headers, end_stream, nullptr); +} + +core::Result Http2Session::submit_settings( + std::span settings) noexcept { + if (const auto valid = require_valid(); !valid.has_value()) { + return std::unexpected{valid.error()}; + } + if (settings.size() > Http2Limits::maximum_settings) { + return std::unexpected{core_error(core::ErrorCode::invalid_range, + "HTTP/2 settings exceed their bound")}; + } + std::array entries{}; + for (std::size_t index = 0; index < settings.size(); ++index) { + entries[index] = {static_cast(settings[index].identifier), + settings[index].value}; + } + auto& state = *static_cast(native_state_); + const int result = nghttp2_submit_settings(static_cast(native_session_), + NGHTTP2_FLAG_NONE, entries.data(), settings.size()); + if (result != 0) { + return std::unexpected{native_error(core::DependencyOperation::http2_submit, + result, state.log_sink)}; + } + return {}; +} + +core::Result Http2Session::submit_data( + Http2StreamId stream, Http2DataProvider& provider, bool end_stream) noexcept { + if (const auto valid = require_valid(); !valid.has_value()) { + return std::unexpected{valid.error()}; + } + if (!stream.valid() || !provider.enabled()) { + return std::unexpected{core_error(core::ErrorCode::invalid_input, + "HTTP/2 data submission is invalid")}; + } + nghttp2_data_provider native_provider{}; + native_provider.source.ptr = &provider; + native_provider.read_callback = on_data_source_read; + auto& state = *static_cast(native_state_); + const std::uint8_t flags = end_stream ? NGHTTP2_FLAG_END_STREAM : NGHTTP2_FLAG_NONE; + const int result = nghttp2_submit_data(static_cast(native_session_), + flags, stream.wire_value(), &native_provider); + if (result != 0) { + return std::unexpected{native_error(core::DependencyOperation::http2_submit, + result, state.log_sink)}; + } + return {}; +} + +core::Result Http2Session::resume_data(Http2StreamId stream) noexcept { + if (const auto valid = require_valid(); !valid.has_value()) { + return std::unexpected{valid.error()}; + } + if (!stream.valid()) { + return std::unexpected{core_error(core::ErrorCode::invalid_input, + "HTTP/2 stream ID is invalid")}; + } + auto& state = *static_cast(native_state_); + const int result = nghttp2_session_resume_data( + static_cast(native_session_), stream.wire_value()); + if (result != 0) { + return std::unexpected{native_error(core::DependencyOperation::http2_submit, + result, state.log_sink)}; + } + return {}; +} + +core::Result Http2Session::submit_reset(Http2StreamId stream, + std::uint32_t error_code) noexcept { + if (const auto valid = require_valid(); !valid.has_value()) { + return std::unexpected{valid.error()}; + } + if (!stream.valid()) { + return std::unexpected{core_error(core::ErrorCode::invalid_input, + "HTTP/2 stream ID is invalid")}; + } + auto& state = *static_cast(native_state_); + const int result = nghttp2_submit_rst_stream(static_cast(native_session_), + NGHTTP2_FLAG_NONE, stream.wire_value(), error_code); + if (result != 0) { + return std::unexpected{native_error(core::DependencyOperation::http2_submit, + result, state.log_sink)}; + } + return {}; +} + +core::Result Http2Session::submit_goaway(std::int32_t last_stream_id, + std::uint32_t error_code, + core::ByteView debug_data) noexcept { + if (const auto valid = require_valid(); !valid.has_value()) { + return std::unexpected{valid.error()}; + } + auto& state = *static_cast(native_state_); + if (last_stream_id < 0 || debug_data.size() > state.limits.maximum_debug_data_bytes) { + return std::unexpected{core_error(core::ErrorCode::invalid_range, + "HTTP/2 GOAWAY arguments exceed their bounds")}; + } + const int result = nghttp2_submit_goaway( + static_cast(native_session_), NGHTTP2_FLAG_NONE, + last_stream_id, error_code, + reinterpret_cast(debug_data.data()), debug_data.size()); + if (result != 0) { + return std::unexpected{native_error(core::DependencyOperation::http2_submit, + result, state.log_sink)}; + } + return {}; +} + +core::Result Http2Session::submit_connection_window_update( + std::int32_t increment) noexcept { + if (const auto valid = require_valid(); !valid.has_value()) { + return std::unexpected{valid.error()}; + } + auto& state = *static_cast(native_state_); + const int result = nghttp2_submit_window_update( + static_cast(native_session_), NGHTTP2_FLAG_NONE, 0, increment); + if (result != 0) { + return std::unexpected{native_error(core::DependencyOperation::http2_submit, + result, state.log_sink)}; + } + return {}; +} + +core::Result Http2Session::submit_stream_window_update( + Http2StreamId stream, std::int32_t increment) noexcept { + if (const auto valid = require_valid(); !valid.has_value()) { + return std::unexpected{valid.error()}; + } + if (!stream.valid()) { + return std::unexpected{core_error(core::ErrorCode::invalid_input, + "HTTP/2 stream ID is invalid")}; + } + auto& state = *static_cast(native_state_); + const int result = nghttp2_submit_window_update( + static_cast(native_session_), NGHTTP2_FLAG_NONE, + stream.wire_value(), increment); + if (result != 0) { + return std::unexpected{native_error(core::DependencyOperation::http2_submit, + result, state.log_sink)}; + } + return {}; +} + +void Http2Session::release() noexcept { + if (native_session_ != nullptr && arena_ != nullptr && + arena_->generation() == arena_generation_) { + nghttp2_session_del(static_cast(native_session_)); + } + native_session_ = nullptr; + native_state_ = nullptr; + arena_ = nullptr; + arena_generation_ = 0; +} + +void Http2Session::move_from(Http2Session&& other) noexcept { + native_session_ = std::exchange(other.native_session_, nullptr); + native_state_ = std::exchange(other.native_state_, nullptr); + arena_ = std::exchange(other.arena_, nullptr); + arena_generation_ = std::exchange(other.arena_generation_, 0); +} + +} // namespace laghu::adapters diff --git a/src/core/contract/laghu/core/contract.hpp b/src/core/contract/laghu/core/contract.hpp index 5f31f5b..bf38a9d 100644 --- a/src/core/contract/laghu/core/contract.hpp +++ b/src/core/contract/laghu/core/contract.hpp @@ -114,6 +114,10 @@ enum class DependencyOperation : std::uint8_t { idna_lookup, password_verify, json_parse, + http2_session, + http2_receive, + http2_send, + http2_submit, }; class Error final { diff --git a/tests/adapters/http2.cpp b/tests/adapters/http2.cpp new file mode 100644 index 0000000..a3af53c --- /dev/null +++ b/tests/adapters/http2.cpp @@ -0,0 +1,731 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#include +#include +#include +#include +#include +#include + +#include "laghu_test_support.hpp" + +#include +#include + +namespace { + +using laghu::adapters::Http2CallbackAction; +using laghu::adapters::Http2DataProvider; +using laghu::adapters::Http2DataReadResult; +using laghu::adapters::Http2DataReadState; +using laghu::adapters::Http2Event; +using laghu::adapters::Http2EventKind; +using laghu::adapters::Http2EventSink; +using laghu::adapters::Http2Header; +using laghu::adapters::Http2Limits; +using laghu::adapters::Http2Role; +using laghu::adapters::Http2Session; +using laghu::adapters::Http2Setting; +using laghu::core::ArenaBlockSource; +using laghu::core::BoundedArena; +using laghu::core::ByteView; +using laghu::core::ErrorCode; +using laghu::core::MemoryBudget; +using laghu::core::MutableByteView; +using laghu::core::Result; +using laghu::core::WorkerId; + +struct FixedSource final { + alignas(std::max_align_t) std::array storage{}; + bool fail{}; + + static Result acquire(void* context, std::size_t minimum) noexcept { + auto& self = *static_cast(context); + if (self.fail || minimum > self.storage.size()) { + return std::unexpected{laghu::core::Error{ + laghu::core::ErrorDomain::core, ErrorCode::exhaustion, 0, + "HTTP/2 test block source exhausted"}}; + } + return MutableByteView::from(self.storage); + } + + static Result reset(void*) noexcept { return {}; } +}; + +[[nodiscard]] ArenaBlockSource block_source(FixedSource& source) noexcept { + return ArenaBlockSource{&source, FixedSource::acquire, FixedSource::reset}; +} + +[[nodiscard]] ByteView bytes(std::string_view text) noexcept { + return *ByteView::from({reinterpret_cast(text.data()), text.size()}); +} + +struct Events final { + std::size_t settings{}; + std::size_t settings_acks{}; + std::size_t headers{}; + std::size_t closed{}; + std::size_t reset{}; + std::size_t goaway{}; + std::size_t window_updates{}; + std::size_t data_events{}; + std::size_t data_bytes{}; + std::size_t ended_frames{}; + std::int32_t last_stream{}; + std::int32_t goaway_last_stream{}; + bool goaway_metadata{}; + bool fail_on_settings{}; + bool pause_on_header{}; + bool paused{}; + bool reject_data{}; + bool data_ended{}; + bool header_ended{}; + + static Http2CallbackAction write(void* context, const Http2Event& event) noexcept { + auto& self = *static_cast(context); + if (event.stream.valid()) { + self.last_stream = event.stream.wire_value(); + } + switch (event.kind) { + case Http2EventKind::settings: + ++self.settings; + self.settings_acks += event.settings_ack ? 1U : 0U; + return self.fail_on_settings ? Http2CallbackAction::fail_session + : Http2CallbackAction::continue_processing; + case Http2EventKind::header: + ++self.headers; + self.header_ended = self.header_ended || event.end_stream; + if (self.pause_on_header && !self.paused) { + self.paused = true; + return Http2CallbackAction::pause; + } + break; + case Http2EventKind::data: + ++self.data_events; + self.data_bytes += event.data.size(); + self.data_ended = self.data_ended || event.end_stream; + return self.reject_data ? Http2CallbackAction::reject_stream + : Http2CallbackAction::continue_processing; + case Http2EventKind::frame_received: + self.ended_frames += event.end_stream ? 1U : 0U; + break; + case Http2EventKind::stream_closed: + ++self.closed; + break; + case Http2EventKind::reset: + ++self.reset; + break; + case Http2EventKind::goaway: + ++self.goaway; + self.goaway_last_stream = event.last_stream_id; + self.goaway_metadata = event.code == 0 && event.debug_data.size() == 4U; + break; + case Http2EventKind::window_update: + ++self.window_updates; + break; + default: + break; + } + return Http2CallbackAction::continue_processing; + } +}; + +struct OutboundData final { + ByteView bytes{}; + std::size_t offset{}; + bool defer_once{}; + + static Result read(void* context, + MutableByteView output) noexcept { + auto& self = *static_cast(context); + if (self.defer_once) { + self.defer_once = false; + return Http2DataReadResult{0, Http2DataReadState::deferred}; + } + const std::size_t remaining = self.bytes.size() - self.offset; + const std::size_t count = remaining < output.size() ? remaining : output.size(); + const auto source = self.bytes.slice(self.offset, count); + if (!source.has_value()) { + return std::unexpected{source.error()}; + } + std::copy(source->span().begin(), source->span().end(), output.span().begin()); + self.offset += count; + return Http2DataReadResult{ + count, self.offset == self.bytes.size() ? Http2DataReadState::end + : Http2DataReadState::more}; + } +}; + +[[nodiscard]] constexpr Http2Limits limits() noexcept { + return Http2Limits{4096, 32, 128}; +} + +[[nodiscard]] bool reset_arena(BoundedArena& arena, WorkerId worker) noexcept { + const auto boundary = arena.quiescent_boundary(worker); + return boundary.has_value() && arena.reset(worker, *boundary).has_value(); +} + +[[nodiscard]] bool transfer(Http2Session& source, Http2Session& destination) noexcept { + for (std::size_t iteration = 0; iteration < 64; ++iteration) { + const auto output = source.next_output(); + if (!output.has_value()) { + return false; + } + if (output->empty()) { + return true; + } + const auto consumed = destination.receive(*output); + if (!consumed.has_value() || *consumed != output->size()) { + return false; + } + } + return false; +} + +[[nodiscard]] bool append_data_frame(std::span output, std::size_t& used, + std::int32_t stream_id, + std::span payload, + bool end_stream) noexcept { + if (stream_id <= 0 || payload.size() > 0x00ff'ffffU || used > output.size() || + payload.size() > output.size() - used || 9U > output.size() - used - payload.size()) { + return false; + } + const auto length = static_cast(payload.size()); + const auto stream = static_cast(stream_id); + output[used] = static_cast((length >> 16U) & 0xffU); + output[used + 1U] = static_cast((length >> 8U) & 0xffU); + output[used + 2U] = static_cast(length & 0xffU); + output[used + 3U] = std::byte{0}; + output[used + 4U] = end_stream ? std::byte{1} : std::byte{0}; + output[used + 5U] = static_cast((stream >> 24U) & 0x7fU); + output[used + 6U] = static_cast((stream >> 16U) & 0xffU); + output[used + 7U] = static_cast((stream >> 8U) & 0xffU); + output[used + 8U] = static_cast(stream & 0xffU); + std::copy(payload.begin(), payload.end(), + output.begin() + static_cast(used + 9U)); + used += 9U + payload.size(); + return true; +} + +[[nodiscard]] bool receive_data_frame(Http2Session& session, std::int32_t stream_id, + std::span payload, + bool end_stream) noexcept { + std::array frame{}; + std::size_t used{}; + if (!append_data_frame(frame, used, stream_id, payload, end_stream)) { + return false; + } + const auto input = ByteView::from({frame.data(), used}); + if (!input.has_value()) { + return false; + } + const auto consumed = session.receive(*input); + return consumed.has_value() && *consumed == input->size(); +} + +[[nodiscard]] bool check_exchange() noexcept { + const auto worker = WorkerId::from_uint64(60); + if (!worker.has_value()) { + return false; + } + FixedSource client_source{}; + FixedSource server_source{}; + MemoryBudget client_budget{*worker, client_source.storage.size()}; + MemoryBudget server_budget{*worker, server_source.storage.size()}; + BoundedArena client_arena{*worker, client_budget, block_source(client_source), 4096, + client_source.storage.size()}; + BoundedArena server_arena{*worker, server_budget, block_source(server_source), 4096, + server_source.storage.size()}; + const bool passed = [&]() noexcept { + Events client_events{}; + Events server_events{}; + auto client = Http2Session::create(*worker, Http2Role::client, client_arena, limits(), + Http2EventSink{&client_events, Events::write}); + auto server = Http2Session::create(*worker, Http2Role::server, server_arena, limits(), + Http2EventSink{&server_events, Events::write}); + if (!client.has_value() || !server.has_value()) { + return false; + } + constexpr std::array settings{Http2Setting{3, 16}, Http2Setting{4, 65535}}; + if (!client->submit_settings(settings).has_value() || + !server->submit_settings(settings).has_value() || + !transfer(*client, *server) || !transfer(*server, *client)) { + return false; + } + const std::array request_headers{ + Http2Header{bytes(":method"), bytes("GET"), false}, + Http2Header{bytes(":scheme"), bytes("https"), false}, + Http2Header{bytes(":authority"), bytes("example.test"), false}, + Http2Header{bytes(":path"), bytes("/"), false}, + }; + const auto stream = client->submit_headers(request_headers, true); + if (!stream.has_value() || !transfer(*client, *server) || + server_events.headers != request_headers.size() || + server_events.last_stream != stream->wire_value() || !server_events.header_ended) { + return false; + } + const std::array response_headers{ + Http2Header{bytes(":status"), bytes("200"), false}, + Http2Header{bytes("content-length"), bytes("0"), false}, + }; + if (!server->submit_headers(*stream, response_headers, true).has_value() || + !transfer(*server, *client) || client_events.headers != response_headers.size()) { + return false; + } + const auto interest = client->interest(); + return interest.has_value() && !interest->timer && client_events.settings > 0U && + server_events.settings > 0U && client_events.settings_acks > 0U && + server_events.settings_acks > 0U && client_events.closed > 0U && + server_events.closed > 0U; + }(); + return reset_arena(client_arena, *worker) && reset_arena(server_arena, *worker) && passed; +} + +[[nodiscard]] bool check_data_and_flow_control() noexcept { + const auto worker = WorkerId::from_uint64(67); + if (!worker.has_value()) { + return false; + } + FixedSource client_source{}; + FixedSource server_source{}; + MemoryBudget client_budget{*worker, client_source.storage.size()}; + MemoryBudget server_budget{*worker, server_source.storage.size()}; + BoundedArena client_arena{*worker, client_budget, block_source(client_source), 4096, + client_source.storage.size()}; + BoundedArena server_arena{*worker, server_budget, block_source(server_source), 4096, + server_source.storage.size()}; + const bool passed = [&]() noexcept { + Events client_events{}; + Events server_events{}; + auto client = Http2Session::create(*worker, Http2Role::client, client_arena, limits(), + Http2EventSink{&client_events, Events::write}); + auto server = Http2Session::create(*worker, Http2Role::server, server_arena, limits(), + Http2EventSink{&server_events, Events::write}); + constexpr std::array settings{Http2Setting{3, 8}}; + if (!client.has_value() || !server.has_value() || + !client->submit_settings(settings).has_value() || + !server->submit_settings(settings).has_value() || !transfer(*client, *server) || + !transfer(*server, *client)) { + return false; + } + const std::array headers{ + Http2Header{bytes(":method"), bytes("POST"), false}, + Http2Header{bytes(":scheme"), bytes("https"), false}, + Http2Header{bytes(":authority"), bytes("example.test"), false}, + Http2Header{bytes(":path"), bytes("/data"), false}, + }; + const auto data_stream = client->submit_headers(headers, false); + if (!data_stream.has_value() || !transfer(*client, *server) || + !receive_data_frame(*server, data_stream->wire_value(), bytes("abc").span(), true) || + server_events.data_events != 1U || server_events.data_bytes != 3U || + !server_events.data_ended) { + return false; + } + const auto automatic_output = server->next_output(); + if (!automatic_output.has_value() || !automatic_output->empty()) { + return false; + } + if (!server->submit_stream_window_update(*data_stream, 3).has_value() || + !transfer(*server, *client) || client_events.window_updates == 0U) { + return false; + } + + const auto outbound_stream = client->submit_headers(headers, false); + if (!outbound_stream.has_value() || !transfer(*client, *server)) { + return false; + } + OutboundData outbound{bytes("outbound"), 0, true}; + Http2DataProvider provider{&outbound, OutboundData::read}; + if (!client->submit_data(*outbound_stream, provider, true).has_value()) { + return false; + } + const auto deferred = client->next_output(); + const std::size_t data_events_before_resume = server_events.data_events; + if (!deferred.has_value() || !deferred->empty() || + !client->resume_data(*outbound_stream).has_value() || + !transfer(*client, *server) || + server_events.data_events <= data_events_before_resume || !server_events.data_ended) { + return false; + } + + const auto empty_stream = client->submit_headers(headers, false); + if (!empty_stream.has_value() || !transfer(*client, *server) || + !receive_data_frame(*server, empty_stream->wire_value(), {}, true) || + server_events.ended_frames == 0U) { + return false; + } + + const auto first_rejected_stream = client->submit_headers(headers, false); + const auto second_rejected_stream = client->submit_headers(headers, false); + if (!first_rejected_stream.has_value() || !second_rejected_stream.has_value() || + !transfer(*client, *server)) { + return false; + } + server_events.reject_data = true; + std::array rejected_frames{}; + std::size_t rejected_size{}; + if (!append_data_frame(rejected_frames, rejected_size, + first_rejected_stream->wire_value(), bytes("first").span(), false) || + !append_data_frame(rejected_frames, rejected_size, + second_rejected_stream->wire_value(), bytes("second").span(), false)) { + return false; + } + const auto rejected_input = ByteView::from({rejected_frames.data(), rejected_size}); + if (!rejected_input.has_value()) { + return false; + } + std::size_t rejected_offset{}; + while (rejected_offset < rejected_input->size()) { + const auto remaining = rejected_input->slice( + rejected_offset, rejected_input->size() - rejected_offset); + if (!remaining.has_value()) { + return false; + } + const auto rejected = server->receive(*remaining); + if (!rejected.has_value() || *rejected == 0U || + *rejected > remaining->size() || !transfer(*server, *client)) { + return false; + } + rejected_offset += *rejected; + } + return client_events.reset >= 2U; + }(); + return reset_arena(client_arena, *worker) && reset_arena(server_arena, *worker) && passed; +} + +[[nodiscard]] bool check_allocator_reuse() noexcept { + const auto worker = WorkerId::from_uint64(68); + if (!worker.has_value()) { + return false; + } + constexpr std::size_t session_capacity = 32768; + FixedSource client_source{}; + FixedSource server_source{}; + MemoryBudget client_budget{*worker, session_capacity}; + MemoryBudget server_budget{*worker, session_capacity}; + BoundedArena client_arena{*worker, client_budget, block_source(client_source), 4096, + session_capacity}; + BoundedArena server_arena{*worker, server_budget, block_source(server_source), 4096, + session_capacity}; + const bool passed = [&]() noexcept { + auto client = Http2Session::create(*worker, Http2Role::client, client_arena, limits()); + auto server = Http2Session::create(*worker, Http2Role::server, server_arena, limits()); + constexpr std::array settings{Http2Setting{3, 8}}; + if (!client.has_value() || !server.has_value() || + !client->submit_settings(settings).has_value() || + !server->submit_settings(settings).has_value() || !transfer(*client, *server) || + !transfer(*server, *client)) { + return false; + } + const std::array request{ + Http2Header{bytes(":method"), bytes("GET"), false}, + Http2Header{bytes(":scheme"), bytes("https"), false}, + Http2Header{bytes(":authority"), bytes("example.test"), false}, + Http2Header{bytes(":path"), bytes("/reuse"), false}, + }; + const std::array response{ + Http2Header{bytes(":status"), bytes("204"), false}, + }; + for (std::size_t iteration = 0; iteration < 128; ++iteration) { + const auto stream = client->submit_headers(request, true); + if (!stream.has_value() || !transfer(*client, *server) || + !server->submit_headers(*stream, response, true).has_value() || + !transfer(*server, *client)) { + return false; + } + } + return true; + }(); + return reset_arena(client_arena, *worker) && reset_arena(server_arena, *worker) && passed; +} + +[[nodiscard]] bool check_control_frames() noexcept { + const auto worker = WorkerId::from_uint64(61); + if (!worker.has_value()) { + return false; + } + FixedSource client_source{}; + FixedSource server_source{}; + MemoryBudget client_budget{*worker, client_source.storage.size()}; + MemoryBudget server_budget{*worker, server_source.storage.size()}; + BoundedArena client_arena{*worker, client_budget, block_source(client_source), 4096, + client_source.storage.size()}; + BoundedArena server_arena{*worker, server_budget, block_source(server_source), 4096, + server_source.storage.size()}; + const bool passed = [&]() noexcept { + Events client_events{}; + Events server_events{}; + auto client = Http2Session::create(*worker, Http2Role::client, client_arena, limits(), + Http2EventSink{&client_events, Events::write}); + auto server = Http2Session::create(*worker, Http2Role::server, server_arena, limits(), + Http2EventSink{&server_events, Events::write}); + if (!client.has_value() || !server.has_value() || !transfer(*client, *server) || + !transfer(*server, *client)) { + return false; + } + const std::array headers{ + Http2Header{bytes(":method"), bytes("GET"), false}, + Http2Header{bytes(":scheme"), bytes("https"), false}, + Http2Header{bytes(":authority"), bytes("example.test"), false}, + Http2Header{bytes(":path"), bytes("/control"), false}, + }; + const auto stream = client->submit_headers(headers, false); + if (!stream.has_value() || !transfer(*client, *server) || + !server->submit_stream_window_update(*stream, 1024).has_value() || + !server->submit_reset(*stream, 8).has_value()) { + return false; + } + const auto control_output = server->next_output(); + if (!control_output.has_value() || control_output->empty() || + !client->receive(*control_output).has_value() || + !transfer(*server, *client)) { + return false; + } + return server->submit_connection_window_update(1024).has_value(); + }(); + return reset_arena(client_arena, *worker) && reset_arena(server_arena, *worker) && passed; +} + +[[nodiscard]] bool check_goaway() noexcept { + const auto worker = WorkerId::from_uint64(66); + if (!worker.has_value()) { + return false; + } + FixedSource client_source{}; + FixedSource server_source{}; + MemoryBudget client_budget{*worker, client_source.storage.size()}; + MemoryBudget server_budget{*worker, server_source.storage.size()}; + BoundedArena client_arena{*worker, client_budget, block_source(client_source), 4096, + client_source.storage.size()}; + BoundedArena server_arena{*worker, server_budget, block_source(server_source), 4096, + server_source.storage.size()}; + const bool passed = [&]() noexcept { + Events events{}; + auto client = Http2Session::create(*worker, Http2Role::client, client_arena, limits(), + Http2EventSink{&events, Events::write}); + auto server = Http2Session::create(*worker, Http2Role::server, server_arena, limits()); + constexpr std::array settings{Http2Setting{3, 8}}; + if (!client.has_value() || !server.has_value() || + !client->submit_settings(settings).has_value() || + !server->submit_settings(settings).has_value() || !transfer(*client, *server) || + !transfer(*server, *client) || + !server->submit_goaway(0, 0, bytes("done")).has_value() || + !transfer(*server, *client)) { + return false; + } + return events.goaway == 1U && events.goaway_last_stream == 0 && events.goaway_metadata; + }(); + return reset_arena(client_arena, *worker) && reset_arena(server_arena, *worker) && passed; +} + +[[nodiscard]] bool check_pause_resumption() noexcept { + const auto worker = WorkerId::from_uint64(65); + if (!worker.has_value()) { + return false; + } + FixedSource client_source{}; + FixedSource server_source{}; + MemoryBudget client_budget{*worker, client_source.storage.size()}; + MemoryBudget server_budget{*worker, server_source.storage.size()}; + BoundedArena client_arena{*worker, client_budget, block_source(client_source), 4096, + client_source.storage.size()}; + BoundedArena server_arena{*worker, server_budget, block_source(server_source), 4096, + server_source.storage.size()}; + const bool passed = [&]() noexcept { + Events events{}; + auto client = Http2Session::create(*worker, Http2Role::client, client_arena, limits()); + auto server = Http2Session::create(*worker, Http2Role::server, server_arena, limits(), + Http2EventSink{&events, Events::write}); + constexpr std::array settings{Http2Setting{3, 8}}; + if (!client.has_value() || !server.has_value() || + !client->submit_settings(settings).has_value() || + !server->submit_settings(settings).has_value() || !transfer(*client, *server) || + !transfer(*server, *client)) { + return false; + } + const std::array headers{ + Http2Header{bytes(":method"), bytes("GET"), false}, + Http2Header{bytes(":scheme"), bytes("https"), false}, + Http2Header{bytes(":authority"), bytes("example.test"), false}, + Http2Header{bytes(":path"), bytes("/paused"), false}, + }; + if (!client->submit_headers(headers, true).has_value() || + !client->submit_headers(headers, true).has_value()) { + return false; + } + std::array wire{}; + std::size_t wire_size{}; + for (std::size_t iteration = 0; iteration < 8; ++iteration) { + const auto output = client->next_output(); + if (!output.has_value()) { + return false; + } + if (output->empty()) { + break; + } + if (output->size() > wire.size() - wire_size) { + return false; + } + std::copy(output->span().begin(), output->span().end(), + wire.begin() + static_cast(wire_size)); + wire_size += output->size(); + } + events.pause_on_header = true; + const auto input = *ByteView::from({wire.data(), wire_size}); + const auto consumed = server->receive(input); + if (!consumed.has_value() || *consumed == 0U || *consumed >= wire_size || !events.paused) { + return false; + } + const auto remaining = input.slice(*consumed, wire_size - *consumed); + if (!remaining.has_value()) { + return false; + } + const auto resumed = server->receive(*remaining); + return resumed.has_value() && *resumed == remaining->size() && events.headers == 8U; + }(); + return reset_arena(client_arena, *worker) && reset_arena(server_arena, *worker) && passed; +} + +[[nodiscard]] bool check_failures() noexcept { + const auto worker = WorkerId::from_uint64(62); + if (!worker.has_value()) { + return false; + } + FixedSource source{}; + MemoryBudget budget{*worker, 512}; + BoundedArena tiny_arena{*worker, budget, block_source(source), 128, 512}; + const auto invalid_role = Http2Session::create( + *worker, static_cast(255), tiny_arena, limits()); + if (invalid_role.has_value() || invalid_role.error().code() != ErrorCode::invalid_input) { + return false; + } + const auto exhausted = Http2Session::create(*worker, Http2Role::client, tiny_arena, limits()); + const bool bounded_failure = + !exhausted.has_value() && exhausted.error().code() == ErrorCode::exhaustion && + exhausted.error().domain() == laghu::core::ErrorDomain::dependency && + exhausted.error().dependency_id() == laghu::core::DependencyId::nghttp2; + if (!reset_arena(tiny_arena, *worker) || !bounded_failure) { + return false; + } + + FixedSource malformed_source{}; + MemoryBudget malformed_budget{*worker, malformed_source.storage.size()}; + BoundedArena malformed_arena{*worker, malformed_budget, block_source(malformed_source), 4096, + malformed_source.storage.size()}; + const bool passed = [&]() noexcept { + auto malformed = Http2Session::create(*worker, Http2Role::server, malformed_arena, limits()); + if (!malformed.has_value()) { + return false; + } + const auto bad = malformed->receive(bytes("not-http2")); + return !bad.has_value() && bad.error().domain() == laghu::core::ErrorDomain::dependency && + bad.error().dependency_id() == laghu::core::DependencyId::nghttp2; + }(); + return reset_arena(malformed_arena, *worker) && passed; +} + +[[nodiscard]] bool check_callback_containment() noexcept { + const auto worker = WorkerId::from_uint64(63); + if (!worker.has_value()) { + return false; + } + FixedSource client_source{}; + FixedSource server_source{}; + MemoryBudget client_budget{*worker, client_source.storage.size()}; + MemoryBudget server_budget{*worker, server_source.storage.size()}; + BoundedArena client_arena{*worker, client_budget, block_source(client_source), 4096, + client_source.storage.size()}; + BoundedArena server_arena{*worker, server_budget, block_source(server_source), 4096, + server_source.storage.size()}; + const bool passed = [&]() noexcept { + Events events{}; + events.fail_on_settings = true; + auto client = Http2Session::create(*worker, Http2Role::client, client_arena, limits()); + auto server = Http2Session::create(*worker, Http2Role::server, server_arena, limits(), + Http2EventSink{&events, Events::write}); + if (!client.has_value() || !server.has_value()) { + return false; + } + constexpr std::array settings{Http2Setting{3, 8}}; + if (!client->submit_settings(settings).has_value()) { + return false; + } + for (std::size_t iteration = 0; iteration < 4; ++iteration) { + const auto output = client->next_output(); + if (!output.has_value() || output->empty()) { + return false; + } + const auto received = server->receive(*output); + if (!received.has_value()) { + return received.error().domain() == laghu::core::ErrorDomain::dependency; + } + } + return false; + }(); + return reset_arena(client_arena, *worker) && reset_arena(server_arena, *worker) && passed; +} + +[[nodiscard]] bool check_incoming_header_limit() noexcept { + const auto worker = WorkerId::from_uint64(64); + if (!worker.has_value()) { + return false; + } + FixedSource client_source{}; + FixedSource server_source{}; + MemoryBudget client_budget{*worker, client_source.storage.size()}; + MemoryBudget server_budget{*worker, server_source.storage.size()}; + BoundedArena client_arena{*worker, client_budget, block_source(client_source), 4096, + client_source.storage.size()}; + BoundedArena server_arena{*worker, server_budget, block_source(server_source), 4096, + server_source.storage.size()}; + const bool passed = [&]() noexcept { + auto client = Http2Session::create(*worker, Http2Role::client, client_arena, limits()); + constexpr Http2Limits narrow_limits{64, 1, 16}; + auto server = Http2Session::create(*worker, Http2Role::server, server_arena, narrow_limits); + if (!client.has_value() || !server.has_value()) { + return false; + } + constexpr std::array settings{Http2Setting{3, 8}}; + const std::array headers{ + Http2Header{bytes(":method"), bytes("GET"), false}, + Http2Header{bytes(":scheme"), bytes("https"), false}, + Http2Header{bytes(":authority"), bytes("example.test"), false}, + Http2Header{bytes(":path"), bytes("/limited"), false}, + }; + if (!client->submit_settings(settings).has_value() || + !client->submit_headers(headers, true).has_value()) { + return false; + } + for (std::size_t iteration = 0; iteration < 8; ++iteration) { + const auto output = client->next_output(); + if (!output.has_value() || output->empty()) { + return false; + } + const auto received = server->receive(*output); + if (!received.has_value()) { + return received.error().domain() == laghu::core::ErrorDomain::core && + received.error().code() == ErrorCode::invalid_range; + } + } + return false; + }(); + return reset_arena(client_arena, *worker) && reset_arena(server_arena, *worker) && passed; +} + +} // namespace + +int main() { + constexpr std::array tests{ + laghu::test::TestCase{"adapters.http2.exchange", check_exchange}, + laghu::test::TestCase{"adapters.http2.data_and_flow_control", + check_data_and_flow_control}, + laghu::test::TestCase{"adapters.http2.allocator_reuse", check_allocator_reuse}, + laghu::test::TestCase{"adapters.http2.control_frames", check_control_frames}, + laghu::test::TestCase{"adapters.http2.goaway", check_goaway}, + laghu::test::TestCase{"adapters.http2.failures", check_failures}, + laghu::test::TestCase{"adapters.http2.callback_containment", check_callback_containment}, + laghu::test::TestCase{"adapters.http2.incoming_header_limit", + check_incoming_header_limit}, + laghu::test::TestCase{"adapters.http2.pause_resumption", check_pause_resumption}, + }; + return laghu::test::run_tests(tests); +} diff --git a/tests/adapters/http2_contract.cpp b/tests/adapters/http2_contract.cpp new file mode 100644 index 0000000..906dd55 --- /dev/null +++ b/tests/adapters/http2_contract.cpp @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: AGPL-3.0-only +#include + +#include + +static_assert(!std::is_copy_constructible_v); +static_assert(std::is_nothrow_move_constructible_v); +static_assert(std::is_trivially_copyable_v); + +int main() { return 0; }