| /** @file |
| * |
| * A brief file description |
| * |
| * @section license License |
| * |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #include <catch2/catch_test_macros.hpp> |
| |
| #include <array> |
| #include <string_view> |
| |
| #include "iocore/eventsystem/VIO.h" |
| #include "proxy/http3/Http3FrameDispatcher.h" |
| #include "proxy/http3/Http3ProtocolEnforcer.h" |
| #include "proxy/http3/Http3StreamDataVIOAdaptor.h" |
| #include "Mock.h" |
| |
| namespace |
| { |
| class AliasedInterestsFrameHandler : public Http3FrameHandler |
| { |
| public: |
| std::vector<Http3FrameType> const & |
| interests() override |
| { |
| static std::vector<Http3FrameType> const types = {Http3FrameType::MAX_PUSH_ID, Http3FrameType::X_MAX_DEFINED}; |
| return types; |
| } |
| |
| Http3ErrorUPtr |
| handle_frame(std::shared_ptr<const Http3Frame> /* frame ATS_UNUSED */, Http3StreamType /* s_type ATS_UNUSED */) override |
| { |
| ++this->total_frame_received; |
| return nullptr; |
| } |
| |
| int total_frame_received = 0; |
| }; |
| } // namespace |
| |
| TEST_CASE("Http3FrameHandler dispatch", "[http3]") |
| { |
| Http3FrameDispatcher http3FrameDispatcher; |
| Http3MockFrameHandler handler; |
| Http3ProtocolEnforcer enforcer; |
| http3FrameDispatcher.add_handler(&handler); |
| http3FrameDispatcher.add_handler(&enforcer); |
| |
| MIOBuffer *buf = new_MIOBuffer(BUFFER_SIZE_INDEX_512); |
| IOBufferReader *reader = buf->alloc_reader(); |
| uint64_t nread = 0; |
| Http3ErrorUPtr error = Http3ErrorUPtr(nullptr); |
| |
| SECTION("Test good case") |
| { |
| uint8_t input[] = {// 1st frame (HEADERS) |
| 0x01, 0x04, 0x11, 0x22, 0x33, 0x44, |
| // 2nd frame (DATA) |
| 0x00, 0x04, 0xaa, 0xbb, 0xcc, 0xdd, |
| // 3rd frame (incomplete) |
| 0xff}; |
| |
| buf->write(input, sizeof(input)); |
| |
| // Initial state |
| CHECK(handler.total_frame_received == 0); |
| CHECK(nread == 0); |
| |
| error = http3FrameDispatcher.on_read_ready(0, Http3StreamType::UNKNOWN, *reader, nread); |
| CHECK(!error); |
| CHECK(handler.total_frame_received == 1); |
| CHECK(nread == 12); |
| } |
| |
| SECTION("Test good case with a multibyte frame type encoding") |
| { |
| uint8_t input[] = {// 1st frame (HEADERS) |
| 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x04, 0x11, 0x22, 0x33, 0x44, |
| // 2nd frame (DATA) |
| 0x00, 0x04, 0xaa, 0xbb, 0xcc, 0xdd, |
| // 3rd frame (incomplete) |
| 0xff}; |
| |
| // Initial state |
| CHECK(handler.total_frame_received == 0); |
| CHECK(nread == 0); |
| |
| SECTION("Write everything at once") |
| { |
| buf->write(input, sizeof(input)); |
| error = http3FrameDispatcher.on_read_ready(0, Http3StreamType::UNKNOWN, *reader, nread); |
| CHECK(!error); |
| CHECK(handler.total_frame_received == 1); |
| CHECK(nread == 19); |
| } |
| |
| SECTION("Write one byte at a time") |
| { |
| int total_nread{}; |
| for (uint8_t *it{input}; it < input + sizeof(input); ++it) { |
| buf->write(it, 1); |
| error = http3FrameDispatcher.on_read_ready(0, Http3StreamType::UNKNOWN, *reader, nread); |
| total_nread += nread; |
| CHECK(!error); |
| } |
| CHECK(handler.total_frame_received == 1); |
| CHECK(total_nread == 19); |
| } |
| } |
| |
| free_MIOBuffer(buf); |
| } |
| |
| TEST_CASE("Http3FrameHandler aliased interests", "[http3]") |
| { |
| Http3FrameDispatcher http3_frame_dispatcher; |
| AliasedInterestsFrameHandler handler; |
| http3_frame_dispatcher.add_handler(&handler); |
| |
| MIOBuffer *buf = new_MIOBuffer(BUFFER_SIZE_INDEX_512); |
| IOBufferReader *reader = buf->alloc_reader(); |
| uint64_t nread = 0; |
| uint8_t input[] = { |
| 0x0d, // Type: MAX_PUSH_ID |
| 0x01, // Length |
| 0x01, // Push ID |
| }; |
| |
| buf->write(input, sizeof(input)); |
| |
| auto error = http3_frame_dispatcher.on_read_ready(0, Http3StreamType::CONTROL, *reader, nread); |
| CHECK(!error); |
| CHECK(handler.total_frame_received == 1); |
| CHECK(nread == sizeof(input)); |
| |
| free_MIOBuffer(buf); |
| } |
| |
| TEST_CASE("control stream tests", "[http3]") |
| { |
| Http3FrameDispatcher http3FrameDispatcher; |
| Http3ProtocolEnforcer enforcer; |
| Http3MockFrameHandler handler; |
| |
| http3FrameDispatcher.add_handler(&enforcer); |
| http3FrameDispatcher.add_handler(&handler); |
| |
| MIOBuffer *buf = new_MIOBuffer(BUFFER_SIZE_INDEX_512); |
| IOBufferReader *reader = buf->alloc_reader(); |
| uint64_t nread = 0; |
| Http3ErrorUPtr error = Http3ErrorUPtr(nullptr); |
| |
| SECTION("Only one SETTINGS frame is allowed per the control stream") |
| { |
| uint8_t input[] = { |
| 0x04, // Type |
| 0x08, // Length |
| 0x06, // Identifier |
| 0x44, 0x00, // Value |
| 0x09, // Identifier |
| 0x0f, // Value |
| 0x4a, 0x0a, // Identifier |
| 0x00, // Value |
| 0x04, // Type |
| 0x08, // Length |
| 0x06, // Identifier |
| 0x44, 0x00, // Value |
| 0x09, // Identifier |
| 0x0f, // Value |
| 0x4a, 0x0a, // Identifier |
| 0x00, // Value |
| }; |
| |
| buf->write(input, sizeof(input)); |
| |
| // Initial state |
| CHECK(handler.total_frame_received == 0); |
| CHECK(nread == 0); |
| |
| error = http3FrameDispatcher.on_read_ready(0, Http3StreamType::CONTROL, *reader, nread); |
| REQUIRE(error); |
| CHECK(error->code == Http3ErrorCode::H3_FRAME_UNEXPECTED); |
| CHECK(handler.total_frame_received == 1); |
| CHECK(nread == sizeof(input)); |
| } |
| |
| SECTION("first frame of the control stream must be SETTINGS frame") |
| { |
| uint8_t input[] = { |
| 0x0d, // Type |
| 0x01, // Length |
| 0x01, // Push ID |
| 0x04, // Type |
| 0x08, // Length |
| 0x06, // Identifier |
| 0x44, 0x00, // Value |
| 0x09, // Identifier |
| 0x0f, // Value |
| 0x4a, 0x0a, // Identifier |
| 0x00, // Value |
| }; |
| |
| buf->write(input, sizeof(input)); |
| |
| // Initial state |
| CHECK(handler.total_frame_received == 0); |
| CHECK(nread == 0); |
| |
| error = http3FrameDispatcher.on_read_ready(0, Http3StreamType::CONTROL, *reader, nread); |
| REQUIRE(error); |
| CHECK(error->code == Http3ErrorCode::H3_MISSING_SETTINGS); |
| CHECK(handler.total_frame_received == 0); |
| CHECK(nread == 3); |
| } |
| |
| SECTION("DATA frame is not allowed on control stream") |
| { |
| uint8_t input[] = {0x04, // Type |
| 0x08, // Length |
| 0x06, // Identifier |
| 0x44, 0x00, // Value |
| 0x09, // Identifier |
| 0x0f, // Value |
| 0x4a, 0x0a, // Identifier |
| 0x00, // Value |
| 0x00, // Type |
| 0x04, // Length |
| 0x11, 0x22, 0x33, 0x44}; |
| |
| buf->write(input, sizeof(input)); |
| |
| // Initial state |
| CHECK(handler.total_frame_received == 0); |
| CHECK(nread == 0); |
| |
| error = http3FrameDispatcher.on_read_ready(0, Http3StreamType::CONTROL, *reader, nread); |
| REQUIRE(error); |
| CHECK(error->code == Http3ErrorCode::H3_FRAME_UNEXPECTED); |
| CHECK(handler.total_frame_received == 1); |
| CHECK(nread == sizeof(input)); |
| } |
| |
| SECTION("HEADERS frame is not allowed on control stream") |
| { |
| uint8_t input[] = {0x04, // Type |
| 0x08, // Length |
| 0x06, // Identifier |
| 0x44, 0x00, // Value |
| 0x09, // Identifier |
| 0x0f, // Value |
| 0x4a, 0x0a, // Identifier |
| 0x00, // Value |
| 0x01, // Type |
| 0x04, // Length |
| 0x11, 0x22, 0x33, 0x44}; |
| |
| buf->write(input, sizeof(input)); |
| |
| // Initial state |
| CHECK(handler.total_frame_received == 0); |
| CHECK(nread == 0); |
| |
| error = http3FrameDispatcher.on_read_ready(0, Http3StreamType::CONTROL, *reader, nread); |
| REQUIRE(error); |
| CHECK(error->code == Http3ErrorCode::H3_FRAME_UNEXPECTED); |
| CHECK(handler.total_frame_received == 1); |
| CHECK(nread == sizeof(input)); |
| } |
| |
| SECTION("HTTP/2 reserved frame is not allowed on control stream") |
| { |
| uint8_t input[] = {0x04, // Type |
| 0x08, // Length |
| 0x06, // Identifier |
| 0x44, 0x00, // Value |
| 0x09, // Identifier |
| 0x0f, // Value |
| 0x4a, 0x0a, // Identifier |
| 0x00, // Value |
| 0x06, // Type |
| 0x04, // Length |
| 0x11, 0x22, 0x33, 0x44}; |
| |
| buf->write(input, sizeof(input)); |
| |
| // Initial state |
| CHECK(handler.total_frame_received == 0); |
| CHECK(nread == 0); |
| |
| error = http3FrameDispatcher.on_read_ready(0, Http3StreamType::CONTROL, *reader, nread); |
| REQUIRE(error); |
| CHECK(error->code == Http3ErrorCode::H3_FRAME_UNEXPECTED); |
| CHECK(handler.total_frame_received == 1); |
| CHECK(nread == sizeof(input)); |
| } |
| |
| SECTION("GREASE reserved frame is ignored on control stream") |
| { |
| uint8_t input[] = {0x04, // Type |
| 0x08, // Length |
| 0x06, // Identifier |
| 0x44, 0x00, // Value |
| 0x09, // Identifier |
| 0x0f, // Value |
| 0x4a, 0x0a, // Identifier |
| 0x00, // Value |
| 0x21, // Type: reserved by the 0x21 + 0x1f * N pattern |
| 0x04, // Length |
| 0x11, 0x22, 0x33, 0x44}; |
| |
| buf->write(input, sizeof(input)); |
| |
| // Initial state |
| CHECK(handler.total_frame_received == 0); |
| CHECK(nread == 0); |
| |
| error = http3FrameDispatcher.on_read_ready(0, Http3StreamType::CONTROL, *reader, nread); |
| CHECK(!error); |
| CHECK(handler.total_frame_received == 1); |
| CHECK(nread == sizeof(input)); |
| } |
| |
| SECTION("padding should not be interpreted as a DATA frame", "[http3]") |
| { |
| uint8_t input[] = { |
| 0x40, 0x04, // Type |
| 0x03, // Length |
| 0x06, // Identifier |
| 0x44, 0x00, // Value |
| }; |
| |
| // Initial state |
| CHECK(handler.total_frame_received == 0); |
| CHECK(nread == 0); |
| |
| int total_nread{}; |
| for (uint8_t *it{input}; it < input + sizeof(input); ++it) { |
| buf->write(it, 1); |
| error = http3FrameDispatcher.on_read_ready(0, Http3StreamType::CONTROL, *reader, nread); |
| total_nread += nread; |
| CHECK(!error); |
| } |
| |
| CHECK(handler.total_frame_received == 1); |
| CHECK(total_nread == 6); |
| } |
| |
| free_MIOBuffer(buf); |
| } |
| |
| TEST_CASE("ignore unknown frames", "[http3]") |
| { |
| SECTION("ignore unkown frame") |
| { |
| uint8_t input[] = { |
| 0x0f // Type |
| }; |
| |
| Http3FrameDispatcher http3FrameDispatcher; |
| |
| MIOBuffer *buf = new_MIOBuffer(BUFFER_SIZE_INDEX_512); |
| IOBufferReader *reader = buf->alloc_reader(); |
| uint64_t nread = 0; |
| Http3ErrorUPtr error = Http3ErrorUPtr(nullptr); |
| |
| buf->write(input, sizeof(input)); |
| |
| CHECK(nread == 0); |
| error = http3FrameDispatcher.on_read_ready(0, Http3StreamType::UNKNOWN, *reader, nread); |
| CHECK(!error); |
| CHECK(nread == 0); |
| free_MIOBuffer(buf); |
| } |
| } |
| |
| TEST_CASE("Http3StreamDataVIOAdaptor buffers only the current DATA frame payload", "[http3]") |
| { |
| // The dispatcher hands each handler a reader whose size_limit covers just |
| // one frame. The adaptor must respect that limit, so the body it collects |
| // is the concatenated payloads and nothing else, no matter how the frames |
| // are split across reads. |
| MIOBuffer *sink_buffer = new_MIOBuffer(BUFFER_SIZE_INDEX_512); |
| IOBufferReader *sink_reader = sink_buffer->alloc_reader(); |
| VIO sink_vio; |
| |
| sink_vio.mutex = new_ProxyMutex(); |
| sink_vio.set_writer(sink_buffer); |
| |
| Http3FrameDispatcher http3FrameDispatcher; |
| Http3StreamDataVIOAdaptor adaptor(&sink_vio); |
| http3FrameDispatcher.add_handler(&adaptor); |
| |
| MIOBuffer *buf = new_MIOBuffer(BUFFER_SIZE_INDEX_512); |
| IOBufferReader *reader = buf->alloc_reader(); |
| uint64_t nread = 0; |
| |
| uint8_t input[] = {// 1st frame (DATA) |
| 0x00, 0x04, 'A', 'A', 'A', 'A', |
| // 2nd frame (DATA) |
| 0x00, 0x04, 'B', 'B', 'B', 'B'}; |
| |
| constexpr std::string_view expected_body = "AAAABBBB"; |
| |
| SECTION("Both frames arrive in a single read") |
| { |
| // The first frame's payload is followed in the same buffer by the second |
| // frame, so an unbounded copy would pull the trailing header and payload |
| // into the body as well. |
| buf->write(input, sizeof(input)); |
| |
| Http3ErrorUPtr error = http3FrameDispatcher.on_read_ready(0, Http3StreamType::UNKNOWN, *reader, nread); |
| CHECK(!error); |
| CHECK(nread == sizeof(input)); |
| } |
| |
| SECTION("Frames arrive one byte at a time") |
| { |
| // on_read_ready resets nread on every call, so total it up as we go. |
| uint64_t consumed = 0; |
| |
| for (uint8_t *it{input}; it < input + sizeof(input); ++it) { |
| buf->write(it, 1); |
| |
| Http3ErrorUPtr error = http3FrameDispatcher.on_read_ready(0, Http3StreamType::UNKNOWN, *reader, nread); |
| CHECK(!error); |
| consumed += nread; |
| } |
| CHECK(consumed == sizeof(input)); |
| } |
| |
| adaptor.finalize(); |
| REQUIRE(sink_reader->read_avail() == static_cast<int64_t>(expected_body.size())); |
| |
| std::array<char, expected_body.size()> body; |
| |
| sink_reader->memcpy(body.data(), body.size()); |
| CHECK(std::string_view(body.data(), body.size()) == expected_body); |
| |
| free_MIOBuffer(buf); |
| free_MIOBuffer(sink_buffer); |
| } |
| |
| TEST_CASE("HTTP/2 reserved frame type not allowed", "[http3]") |
| { |
| SECTION("Reject reserved frame type in non control stream") |
| { |
| uint8_t input[] = {// 1st frame (HEADERS) |
| 0x01, 0x04, 0x11, 0x22, 0x33, 0x44, |
| // 2nd frame (DATA) |
| 0x06, 0x04, 0xaa, 0xbb, 0xcc, 0xdd, |
| // 3rd frame (incomplete) |
| 0xff}; |
| |
| Http3FrameDispatcher http3FrameDispatcher; |
| Http3MockFrameHandler handler; |
| Http3ProtocolEnforcer enforcer; |
| http3FrameDispatcher.add_handler(&handler); |
| http3FrameDispatcher.add_handler(&enforcer); |
| |
| MIOBuffer *buf = new_MIOBuffer(BUFFER_SIZE_INDEX_512); |
| IOBufferReader *reader = buf->alloc_reader(); |
| uint64_t nread = 0; |
| Http3ErrorUPtr error = Http3ErrorUPtr(nullptr); |
| |
| buf->write(input, sizeof(input)); |
| |
| // Initial state |
| CHECK(handler.total_frame_received == 0); |
| CHECK(nread == 0); |
| |
| error = http3FrameDispatcher.on_read_ready(0, Http3StreamType::UNKNOWN, *reader, nread); |
| REQUIRE(error); |
| CHECK(error->code == Http3ErrorCode::H3_FRAME_UNEXPECTED); |
| CHECK(handler.total_frame_received == 0); |
| CHECK(nread == 12); |
| free_MIOBuffer(buf); |
| } |
| } |