diff --git a/CMakeLists.txt b/CMakeLists.txt index 982a9ba6a69..770c05eccec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -474,6 +474,12 @@ seastar_generate_ragel ( IN_FILE ${CMAKE_CURRENT_SOURCE_DIR}/src/http/request_parser.rl OUT_FILE ${Seastar_GEN_BINARY_DIR}/include/seastar/http/request_parser.hh) + seastar_generate_ragel ( + TARGET seastar_http2_frame_parser + VAR http2_frame_parser_file + IN_FILE ${CMAKE_CURRENT_SOURCE_DIR}/src/http/http2_frame_parser.rl + OUT_FILE ${Seastar_GEN_BINARY_DIR}/include/seastar/http/http2_frame_parser.hh) + seastar_generate_ragel ( TARGET seastar_http_response_parser VAR http_response_parser_file @@ -489,6 +495,7 @@ seastar_generate_protobuf ( add_library (seastar ${http_chunk_parsers_file} ${http_request_parser_file} + ${http2_frame_parser_file} ${proto_metrics2_files} ${seastar_dpdk_obj} include/seastar/core/abort_source.hh @@ -600,6 +607,7 @@ add_library (seastar include/seastar/http/mime_types.hh include/seastar/http/reply.hh include/seastar/http/request.hh + include/seastar/http/http2_connection.hh include/seastar/http/routes.hh include/seastar/http/short_streams.hh include/seastar/http/transformers.hh @@ -721,6 +729,7 @@ add_library (seastar src/http/transformers.cc src/http/url.cc src/http/client.cc + src/http/http2_connection.cc src/http/request.cc src/json/formatter.cc src/json/json_elements.cc @@ -771,6 +780,7 @@ add_dependencies (seastar seastar_http_chunk_parsers seastar_http_request_parser seastar_http_response_parser + seastar_http2_frame_parser seastar_proto_metrics2) target_include_directories (seastar diff --git a/include/seastar/http/http2_connection.hh b/include/seastar/http/http2_connection.hh new file mode 100644 index 00000000000..c6fd27603ec --- /dev/null +++ b/include/seastar/http/http2_connection.hh @@ -0,0 +1,93 @@ +/* + * This file is open source software, licensed to you under the terms + * of the Apache License, Version 2.0 (the "License"). See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. 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. + */ +/* + * Copyright (C) 2025 Scylladb, Ltd. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace seastar { + +namespace http { + +class http2_connection { +public: + // Represents a single HTTP/2 stream (a request/response exchange) + // As per RFC 7540 + struct h2_stream { + // A stream has a well-defined lifecycle (RFC 7540, Section 5.1). + enum class state { + IDLE, + RESERVED_LOCAL, + RESERVED_REMOTE, + OPEN, + HALF_CLOSED_REMOTE, + HALF_CLOSED_LOCAL, + CLOSED + }; + }; + +private: + httpd::routes& _routes; + connected_socket _fd; + input_stream _read_buf; + output_stream _write_buf; + http2_frame_header_parser _parser; + bool _done = false; + +public: + http2_connection(httpd::routes& routes, connected_socket&& fd, socket_address remote_addr, socket_address local_addr) + : _routes(routes) + , _fd(std::move(fd)) + , _read_buf(_fd.input()) + , _write_buf(_fd.output()) + {} + + // Main entry point to start processing the HTTP/2 connection. + future<> process(); + + #ifdef SEASTAR_HTTP2_TEST + http2_frame_header_parser& get_parser_for_testing() { return _parser; } + #endif + +private: + // The main loop for reading and dispatching frames. + future<> read_loop(); + future<> read_one_frame(); + + // Frame-specific handlers. + future<> handle_settings_frame(const temporary_buffer& payload); + future<> handle_headers_frame(uint32_t stream_id, uint8_t flags, const temporary_buffer& payload); + future<> handle_data_frame(uint32_t stream_id, uint8_t flags, const temporary_buffer& payload); + // ... other frame handlers (GOAWAY, RST_STREAM, etc.) +}; + + + +} // namespace http + +} // namespace seastar \ No newline at end of file diff --git a/src/http/http2_connection.cc b/src/http/http2_connection.cc new file mode 100644 index 00000000000..630b54480f5 --- /dev/null +++ b/src/http/http2_connection.cc @@ -0,0 +1,54 @@ +#include "seastar/core/future.hh" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace seastar { +namespace http { + +static logger h2log("http2"); + +future<> http2_connection::process() { + h2log.info("Starting HTTP/2 connection processing"); + return read_loop(); +} + +future<> http2_connection::read_loop() { + h2log.info("Entering HTTP/2 read loop"); + return do_until([this] { return _done; }, [this] { + return read_one_frame(); + }); +} + +future<> http2_connection::read_one_frame() { + // Read 9-byte HTTP/2 frame header + // According to the HTTP/2 spec, the frame header is always 9 bytes long. + // https://datatracker.ietf.org/doc/html/rfc7540#section-4.1 + return _read_buf.read_exactly(9).then([this](temporary_buffer hdr_buf) { + if (hdr_buf.size() < 9) { + h2log.error("Connection closed or incomplete frame header"); + _done = true; + return make_ready_future<>(); + } + // Parse the frame header + _parser.init(); + _parser.parse(hdr_buf.get_write(), hdr_buf.get_write() + 9); + uint32_t frame_len = _parser._length; + uint8_t frame_type = _parser._type; + //TODO: parse frame flags and streamid + h2log.info("Frame: type={} len={}", frame_type, frame_len); + _done = true; + // Depending on the the frame type, call the appropriate handler + return make_ready_future<>(); + }); +} + +} // namespace http +} // namespace seastar \ No newline at end of file diff --git a/src/http/http2_frame_parser.rl b/src/http/http2_frame_parser.rl new file mode 100644 index 00000000000..68525f37f15 --- /dev/null +++ b/src/http/http2_frame_parser.rl @@ -0,0 +1,87 @@ +/* + * HTTP/2 Frame Header Parser + * Parses the 9-byte HTTP/2 frame header into a struct. + */ + +#pragma once + +#include +#include + +namespace seastar { + +%%{ + machine http2_frame_parser; + + access _fsm_; + + action mark_start { + _frame_start = p; + } + + action store_length1 { + _length = (uint32_t)(uint8_t)_frame_start[0] << 16; + } + action store_length2 { + _length |= (uint32_t)(uint8_t)_frame_start[1] << 8; + } + action store_length3 { + _length |= (uint32_t)(uint8_t)_frame_start[2]; + } + action store_type { + _type = (uint8_t)_frame_start[3]; + } + action store_flags { + _flags = (uint8_t)_frame_start[4]; + } + + main := ( + any >mark_start + any @store_length1 + any @store_length2 + any @store_length3 + any @store_type + any @store_flags + ); +}%% + +class http2_frame_header_parser : public ragel_parser_base { + %% write data nofinal noprefix; +private: + const char* _frame_start = nullptr; + +public: + uint32_t _length = 0; + uint8_t _type = 0; + uint8_t _flags = 0; + + void init() { + init_base(); + _length = 0; + _type = 0; + _flags = 0; + %% write init; + } + + // Returns pointer to next byte after header if parsed, nullptr otherwise + char* parse(char* p, char* pe) { + char* start = p; +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wmisleading-indentation" +#endif +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wimplicit-fallthrough" + %% write exec; +#pragma GCC diagnostic pop +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + if (p - start >= 9) { + return p; + } + return nullptr; + } +}; + +} // namespace seastar \ No newline at end of file diff --git a/tests/unit/httpd_test.cc b/tests/unit/httpd_test.cc index 1f279492429..051bbf42284 100644 --- a/tests/unit/httpd_test.cc +++ b/tests/unit/httpd_test.cc @@ -1,7 +1,8 @@ /* * Copyright 2015 Cloudius Systems */ - + +#define SEASTAR_HTTP2_TEST #include #include #include @@ -20,6 +21,7 @@ #include #include #include "loopback_socket.hh" +#include "seastar/http/http2_connection.hh" #include #include #include @@ -2176,5 +2178,43 @@ SEASTAR_TEST_CASE(test_client_close_connection) { when_all(std::move(server), std::move(client)).discard_result().get(); } + + }); +} + +SEASTAR_TEST_CASE(test_http2_frame_parsing) { + + return seastar::async([] { + // Set up loopback connection factory and server socket + loopback_connection_factory lcf(1); + auto server_sock = lcf.get_server_socket(); + loopback_socket_impl lsi(lcf); + + // Start server coroutine to handle HTTP/2 connection + auto server = seastar::async([&] { + auto ar = server_sock.accept().get(); + // Create HTTP/2 connection object + seastar::httpd::routes dummy_routes; + seastar::http::http2_connection h2conn(dummy_routes, std::move(ar.connection), ar.remote_address, ar.remote_address); + h2conn.process().get(); + fmt::print("after http2 connection process, frame type {}\n", h2conn.get_parser_for_testing()._type); + BOOST_REQUIRE(h2conn.get_parser_for_testing()._type == 0x4); // 0x4 is the SETTINGS frame type + + fmt::print("finished processing http2 frame!!\n"); + }); + + // Client coroutine: connect and send a SETTINGS frame + auto client = seastar::async([&] { + connected_socket sock = lsi.connect(socket_address(ipv4_addr()), socket_address(ipv4_addr())).get(); + auto out = sock.output(); + + // 1. Send a SETTINGS frame (empty payload, just header) + char settings_frame[9] = {0,0,0, 0x4, 0x0, 0,0,0,0}; // length=0, type=4 (SETTINGS), flags=0, stream_id=0 + out.write(settings_frame, 9).get(); + out.flush().get(); + out.close().get(); + }); + // Wait for both client and server to finish + when_all(std::move(server), std::move(client)).get(); }); }