├── src └── http.cpp ├── .gitignore ├── include └── cppcoro │ ├── fmt │ ├── type_logger.hpp │ └── stringable.hpp │ ├── http │ ├── http_request.hpp │ ├── http_response.hpp │ ├── http_client.hpp │ ├── http_server.hpp │ ├── http.hpp │ ├── request_processor.hpp │ ├── route_parameter.hpp │ ├── http_chunk_provider.hpp │ ├── details │ │ ├── router.hpp │ │ └── static_parser_handler.hpp │ ├── http_message.hpp │ ├── http_connection.hpp │ └── route_controller.hpp │ ├── details │ ├── type_index.hpp │ └── function_traits.hpp │ └── tcp │ └── tcp.hpp ├── examples ├── CMakeLists.txt ├── simple_co_http_server │ ├── CMakeLists.txt │ └── main.cpp ├── readme.cpp └── hello_world.cpp ├── tests ├── CMakeLists.txt ├── test_requests.cpp ├── test_route_controller.cpp ├── test_server.cpp └── test_chunked.cpp ├── LICENSE ├── README.md └── CMakeLists.txt /src/http.cpp: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cmake-build*/ 2 | .idea/ 3 | -------------------------------------------------------------------------------- /include/cppcoro/fmt/type_logger.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace cppcoro { 6 | template 7 | struct type_logger { 8 | 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /include/cppcoro/http/http_request.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file cppcoro/http_request.hpp 3 | */ 4 | #pragma once 5 | 6 | #include 7 | 8 | namespace cppcoro::http { 9 | using string_request = abstract_request; 10 | } 11 | -------------------------------------------------------------------------------- /include/cppcoro/http/http_response.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file cppcoro/http_response.hpp 3 | */ 4 | #pragma once 5 | 6 | #include 7 | 8 | namespace cppcoro::http { 9 | using string_response = abstract_response; 10 | } 11 | -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(readme readme.cpp) 2 | target_link_libraries(readme PRIVATE cppcoro::http) 3 | 4 | add_executable(hello_world hello_world.cpp) 5 | target_link_libraries(hello_world PRIVATE cppcoro::http) 6 | 7 | add_subdirectory(simple_co_http_server) 8 | -------------------------------------------------------------------------------- /examples/simple_co_http_server/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | conan_cmake_run( 2 | REQUIRES 3 | lyra/1.4.0 4 | BASIC_SETUP CMAKE_TARGETS 5 | BUILD outdated) 6 | 7 | add_executable(simple_co_http_server main.cpp) 8 | target_link_libraries(simple_co_http_server cppcoro::http CONAN_PKG::lyra) 9 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | FetchContent_Declare(_fetch_catch 2 | GIT_REPOSITORY https://github.com/catchorg/catch2 3 | GIT_TAG master 4 | ) 5 | FetchContent_MakeAvailable(_fetch_catch) 6 | include(${_fetch_catch_SOURCE_DIR}/contrib/Catch.cmake) 7 | 8 | link_libraries(cppcoro::http Catch2::Catch2) 9 | 10 | macro(basic_test _filename) 11 | get_filename_component(_target ${_filename} NAME_WE) 12 | add_executable(${_target} ${_filename}) 13 | catch_discover_tests(${_target}) 14 | endmacro() 15 | 16 | basic_test(test_requests.cpp) 17 | basic_test(test_route_controller.cpp) 18 | basic_test(test_server.cpp) 19 | basic_test(test_chunked.cpp) 20 | -------------------------------------------------------------------------------- /include/cppcoro/fmt/stringable.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | namespace fmt { 8 | 9 | namespace concepts { 10 | template 11 | concept stringable = requires(const T &v) { 12 | { v.to_string() } -> std::convertible_to; 13 | }; 14 | } 15 | 16 | template 17 | struct formatter : fmt::formatter { 18 | template 19 | auto format(const StringableT &obj, FormatContext& ctx) { 20 | return formatter::format(obj.to_string(), ctx); 21 | } 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /include/cppcoro/http/http_client.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file cppcoro/http_server.hpp 3 | */ 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | namespace cppcoro::http { 17 | 18 | class client : protected tcp::client 19 | { 20 | public: 21 | using tcp::client::client; 22 | using tcp::client::stop; 23 | using tcp::client::service; 24 | using connection_type = connection; 25 | 26 | task connect(net::ip_endpoint const &endpoint) { 27 | connection_type conn{*this, std::move(co_await tcp::client::connect(endpoint))}; 28 | co_return conn; 29 | } 30 | 31 | private: 32 | cppcoro::cancellation_source cancellation_source_; 33 | }; 34 | } // namespace cpporo::http 35 | -------------------------------------------------------------------------------- /include/cppcoro/details/type_index.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | namespace cppcoro::detail { 9 | 10 | /** 11 | * Useful template for resolving index of variadic template arguments 12 | */ 13 | template 14 | struct type_index; 15 | 16 | template 17 | struct type_index 18 | : std::integral_constant 19 | { 20 | }; 21 | 22 | template 23 | struct type_index 24 | : std::integral_constant::value> 25 | { 26 | }; 27 | 28 | template 29 | static constexpr std::size_t type_index_v = type_index::value; 30 | 31 | template 32 | struct type_at 33 | { 34 | using type = std::tuple_element_t>; 35 | }; 36 | 37 | template 38 | using type_at_t = typename type_at::type; 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sylvain Garcia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /include/cppcoro/http/http_server.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file cppcoro/http_server.hpp 3 | */ 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | namespace cppcoro::http { 16 | 17 | class server : protected tcp::server 18 | { 19 | public: 20 | using tcp::server::server; 21 | using tcp::server::stop; 22 | using tcp::server::service; 23 | using connection_type = connection; 24 | 25 | task listen() { 26 | auto conn_generator = accept(); 27 | while (!cs_.is_cancellation_requested()) { 28 | spdlog::debug("listening for new connection on {}", socket_.local_endpoint()); 29 | connection_type conn{*this, std::move(co_await accept())}; 30 | spdlog::debug("{} incoming connection: {}", conn.to_string(), conn.peer_address()); 31 | co_return conn; 32 | } 33 | throw operation_cancelled{}; 34 | } 35 | 36 | private: 37 | cppcoro::cancellation_source cancellation_source_; 38 | }; 39 | } // namespace cpporo::http 40 | -------------------------------------------------------------------------------- /tests/test_requests.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | using namespace cppcoro; 14 | 15 | SCENARIO("requests should be customizable", "[cppcoro-http][messages][request]") { 16 | io_service ios; 17 | GIVEN("A simple string request") { 18 | http::string_request req{http::method::post, "/", "hello"}; 19 | WHEN("The http header is built") { 20 | auto header = req.build_header(); 21 | THEN("It can be parsed") { 22 | (void) sync_wait(when_all([&]() -> task<> { 23 | auto _ = on_scope_exit([&] { 24 | ios.stop(); 25 | }); 26 | http::string_request result; 27 | http::request_parser parser; 28 | parser.parse(header); 29 | REQUIRE(!parser); 30 | parser.parse(req.body_access); 31 | REQUIRE(parser); 32 | co_await parser.load(result); 33 | REQUIRE(result.method == http::method::post); 34 | REQUIRE(result.body_access == "hello"); 35 | AND_THEN("The parsed request should give same header") { 36 | REQUIRE(result.build_header() == header); 37 | } 38 | }(), 39 | [&]() -> task<> { 40 | ios.process_events(); 41 | co_return; 42 | }())); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /include/cppcoro/http/http.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file cppcoro/http.hpp 3 | */ 4 | #pragma once 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | namespace cppcoro::http { 13 | 14 | namespace detail { 15 | #include 16 | } 17 | 18 | enum class method 19 | { 20 | get = detail::HTTP_GET, 21 | put = detail::HTTP_PUT, 22 | del = detail::HTTP_DELETE, 23 | post = detail::HTTP_POST, 24 | head = detail::HTTP_HEAD, 25 | options = detail::HTTP_OPTIONS, 26 | patch = detail::HTTP_PATCH, 27 | unknown 28 | }; 29 | 30 | using status = detail::http_status; 31 | using headers = std::map; 32 | 33 | namespace logging { 34 | static inline spdlog::level_t log_level = spdlog::level::warn; 35 | constexpr auto logger_name = "cppcoro::http"; 36 | inline static auto logger = [] () -> std::shared_ptr { 37 | if (auto logger = spdlog::get(logger_name); 38 | logger) { 39 | return logger; 40 | } 41 | return spdlog::stdout_color_mt(logger_name); 42 | }(); 43 | template 44 | auto logger_id(IdT &&id) { 45 | return fmt::format("{}::{}", logger_name, std::forward(id)); 46 | } 47 | template 48 | auto get_logger(IdT &&id) { 49 | const auto _id = logger_id(std::forward(id)); 50 | if (auto _logger = spdlog::get(_id); 51 | _logger) { 52 | return _logger; 53 | } 54 | auto &sinks = logging::logger->sinks(); 55 | auto this_logger = std::make_shared(_id, begin(sinks), end(sinks)); 56 | this_logger->set_level(spdlog::level::level_enum(log_level.load(std::memory_order_relaxed))); 57 | return this_logger; 58 | } 59 | template 60 | auto drop_logger(IdT &&id) { 61 | spdlog::drop(logger_id(std::forward(id))); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /include/cppcoro/http/request_processor.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file cppcoro/http/route_controller.hpp 3 | * @author Sylvain Garcia 4 | */ 5 | #pragma once 6 | 7 | #include 8 | #include 9 | 10 | namespace cppcoro::http { 11 | 12 | template 13 | class request_processor : public server 14 | { 15 | public: 16 | using session_type = SessionT; 17 | using server::server; 18 | 19 | task<> serve() { 20 | async_scope scope; 21 | try { 22 | while (true) { 23 | auto conn = co_await listen(); 24 | scope.spawn([](server *srv, http::server::connection_type conn) mutable -> task<> { 25 | session_type session{}; 26 | http::string_request default_request; 27 | auto init_request = [&](const http::request_parser &parser) -> http::detail::base_request& { 28 | auto *request = static_cast(srv)->prepare(parser, session); 29 | if (!request) { 30 | return default_request; 31 | } 32 | return *request; 33 | }; 34 | while (true) { 35 | try { 36 | // wait next connection request 37 | auto req = co_await conn.next(init_request); 38 | if (!req) 39 | break; // connection closed 40 | // process and send the response 41 | co_await conn.send(co_await static_cast(srv)->process(*req)); 42 | } catch (std::system_error &err) { 43 | if (err.code() == std::errc::connection_reset) { 44 | break; // connection reset by peer 45 | } else { 46 | throw err; 47 | } 48 | } catch (operation_cancelled &) { 49 | break; 50 | } 51 | } 52 | }(this, std::move(conn))); 53 | } 54 | } catch (operation_cancelled &) {} 55 | co_await scope.join(); 56 | } 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /tests/test_route_controller.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | using namespace cppcoro; 15 | 16 | struct session 17 | { 18 | int id = std::rand(); 19 | }; 20 | 21 | using hello_controller_def = http::route_controller; 25 | 26 | struct hello_controller : hello_controller_def 27 | { 28 | using hello_controller_def::hello_controller_def; 29 | 30 | auto on_post(std::string_view who) -> task { 31 | fmt::print("post on {}\n", session().id); 32 | co_return http::string_response{http::status::HTTP_STATUS_OK, fmt::format("post: {}", who)}; 33 | } 34 | auto on_get(const std::string &who) -> task { 35 | fmt::print("get on {}\n", session().id); 36 | co_return http::string_response{http::status::HTTP_STATUS_OK, fmt::format("get: {}", who)}; 37 | } 38 | }; 39 | 40 | SCENARIO("route controller are easy to use", "[cppcoro-http][router]") { 41 | cppcoro::io_service ios; 42 | static const auto test_endpoint = net::ip_endpoint::from_string("127.0.0.1:4242"); 43 | GIVEN("A simple route controller") { 44 | http::controller_server server{ios, 45 | *test_endpoint}; 46 | http::client client{ios}; 47 | 48 | (void) sync_wait(when_all( 49 | [&]() -> task<> { 50 | auto _ = on_scope_exit([&] { 51 | ios.stop(); 52 | }); 53 | co_await server.serve(); 54 | }(), 55 | [&]() -> task<> { 56 | auto _ = on_scope_exit([&] { 57 | server.stop(); 58 | }); 59 | using namespace std::chrono_literals; 60 | auto conn = co_await client.connect(*test_endpoint); 61 | auto resp = co_await conn.post("/hello/world"); 62 | REQUIRE(co_await resp->read_body() == "post: world"); 63 | resp = co_await conn.get("/hello/world"); 64 | REQUIRE(co_await resp->read_body() == "get: world"); 65 | }(), 66 | [&]() -> task<> { 67 | ios.process_events(); 68 | co_return; 69 | }() 70 | )); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CppCoro Http - http coroutine library for C++ 2 | 3 | > :warning: After few experiments the API in master is not satifying enought. 4 | The expected API should be closest to the [router-v2](https://github.com/Garcia6l20/cppcoro-http/tree/feature/router-v2) branch. 5 | But it has some strange multithreading issues I'll not investigate 6 | since [cppcoro](https://github.com/lewissbaker/cppcoro) looks inactive (library fixes needed). 7 | 8 | The 'cppcoro-http' provides a set of classes for creating http servers/clients. 9 | It is built on top of [cppcoro](https://github.com/lewissbaker/cppcoro) library. 10 | 11 | ## HTTP Server 12 | 13 | - example: 14 | 15 | ```c++ 16 | struct session { 17 | int id = std::rand(); 18 | }; 19 | 20 | using hello_controller_def = http::route_controller< 21 | R"(/hello/(\w+))", // route definition 22 | session, 23 | http::string_request, 24 | struct hello_controller>; 25 | 26 | struct hello_controller : hello_controller_def 27 | { 28 | using hello_controller_def::hello_controller_def; 29 | // method handlers 30 | auto on_post(std::string_view who) -> task { 31 | co_return http::string_response{http::status::HTTP_STATUS_OK, 32 | fmt::format("post at {}: hello {}", session().id, who)}; 33 | } 34 | auto on_get(std::string_view who) -> task { 35 | co_return http::string_response{http::status::HTTP_STATUS_OK, 36 | fmt::format("get at {}: hello {}", session().id, who)}; 37 | } 38 | }; 39 | 40 | io_service service; 41 | 42 | auto do_serve = [&]() -> task<> { 43 | auto _ = on_scope_exit([&] { 44 | service.stop(); 45 | }); 46 | http::controller_server server{ 47 | service, 48 | *net::ip_endpoint::from_string("127.0.0.1:4242")}; 49 | co_await server.serve(); 50 | }; 51 | (void) sync_wait(when_all( 52 | do_serve(), 53 | [&]() -> task<> { 54 | service.process_events(); 55 | co_return; 56 | }())); 57 | ``` 58 | 59 | ## Building 60 | 61 | > requirements: 62 | > - GCC11 63 | > - linux kernel version >= 5.5 64 | > - my [cppcoro](https://github.com/Garcia6l20/cppcoro) fork 65 | 66 | ```bash 67 | mkdir build && cd build 68 | cmake -DBUILD_EXAMPLES=ON .. 69 | make -j 70 | ``` 71 | 72 | ## Development 73 | 74 | You can also use cppcoro without installing it for development purposes: 75 | 76 | ```bash 77 | cmake -DCPPCORO_DEVEL=ON .. 78 | ``` 79 | 80 | ## Examples 81 | 82 | - *examples/readme.cpp*: Example in this README. 83 | - *examples/hello_world.cpp*: Basic showcase. 84 | - *examples/simple_co_http_server*: Same as `python3 -m http.server` in cppcoro. 85 | 86 | ## TODO 87 | 88 | - [x] chunked transfers 89 | - [ ] ssl support 90 | - [ ] ... 91 | -------------------------------------------------------------------------------- /include/cppcoro/http/route_parameter.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | namespace cppcoro::http { 12 | 13 | template 14 | struct route_parameter; 15 | 16 | template <> 17 | struct route_parameter { 18 | static constexpr int group_count() { return 0; } 19 | static std::string load(const std::string_view& input) { 20 | return {input.data(), input.data() + input.size()}; 21 | } 22 | 23 | static constexpr auto pattern = ctll::fixed_string{R"(.+(?=/)|.+)"}; 24 | }; 25 | 26 | template <> 27 | struct route_parameter { 28 | static constexpr int group_count() { return 0; } 29 | static std::string_view load(const std::string_view& input) { 30 | return {input.data(), input.size()}; 31 | } 32 | 33 | static constexpr auto pattern = ctll::fixed_string{R"(.+(?=/)|.+)"}; 34 | }; 35 | 36 | template <> 37 | struct route_parameter { 38 | static constexpr int group_count() { return 0; } 39 | static std::filesystem::path load(const std::string_view& input) { 40 | return {input.data()}; 41 | } 42 | static constexpr auto pattern = ctll::fixed_string{R"(.+)"}; 43 | }; 44 | 45 | template <> 46 | struct route_parameter { 47 | static constexpr int group_count() { return 0; } 48 | static int load(const std::string_view &input) { 49 | int result = 0; 50 | std::from_chars(input.data(), input.data() + input.size(), result); 51 | return result; 52 | } 53 | static constexpr auto pattern = ctll::fixed_string{R"(\d+)"}; 54 | }; 55 | 56 | template <> 57 | struct route_parameter { 58 | static constexpr int group_count() { return 0; } 59 | static double load(const std::string_view& input) { 60 | double result = 0; 61 | std::from_chars(input.data(), input.data() + input.size(), result); 62 | return result; 63 | } 64 | static constexpr auto pattern = ctll::fixed_string{R"(\d+\.?\d*)"}; 65 | }; 66 | 67 | template <> 68 | struct route_parameter { 69 | static bool load(const std::string_view& input) { 70 | static const std::vector true_values = {"yes", "on", "true"}; 71 | return std::any_of(true_values.begin(), true_values.end(), [&input](auto&&elem) { 72 | return std::equal(elem.begin(), elem.end(), input.begin(), input.end(), [](char left, char right) { 73 | return tolower(left) == tolower(right); 74 | }); 75 | }); 76 | } 77 | static constexpr auto pattern = ctll::fixed_string{R"(\w+)"}; 78 | }; 79 | 80 | } // namespace xdev::net 81 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.17) 2 | project(cppcoro_http) 3 | 4 | set(CMAKE_CXX_STANDARD 20) 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | 7 | if(NOT EXISTS "${CMAKE_BINARY_DIR}/conan.cmake") 8 | message(STATUS "Downloading conan.cmake from https://github.com/conan-io/cmake-conan") 9 | file(DOWNLOAD "https://github.com/conan-io/cmake-conan/raw/v0.15/conan.cmake" 10 | "${CMAKE_BINARY_DIR}/conan.cmake") 11 | endif() 12 | include("${CMAKE_BINARY_DIR}/conan.cmake") 13 | 14 | 15 | include(FetchContent) 16 | 17 | # 18 | # cppcoro 19 | # 20 | option(CPPCORO_DEVEL "Fetch cppcoro for development" ON) 21 | if(CPPCORO_DEVEL) 22 | FetchContent_Declare(_fetch_cppcoro 23 | GIT_REPOSITORY https://github.com/Garcia6l20/cppcoro 24 | ) 25 | FetchContent_MakeAvailable(_fetch_cppcoro) 26 | else() 27 | find_package(cppcoro REQUIRED) 28 | endif() 29 | 30 | # 31 | # nodejs/http_parser 32 | # 33 | FetchContent_Declare(_fetch_http_parser 34 | GIT_REPOSITORY https://github.com/nodejs/http-parser 35 | GIT_TAG master 36 | CONFIGURE_COMMAND "" 37 | INSTALL_COMMAND "" 38 | BUILD_COMMAND "" 39 | ) 40 | FetchContent_MakeAvailable(_fetch_http_parser) 41 | FetchContent_GetProperties(_fetch_http_parser) 42 | if(NOT EXISTS ${_fetch_http_parser_SOURCE_DIR}/http_parser.c) 43 | endif() 44 | add_library(http_parser STATIC 45 | ${_fetch_http_parser_SOURCE_DIR}/http_parser.c 46 | ${_fetch_http_parser_SOURCE_DIR}/http_parser.h 47 | ) 48 | target_include_directories(http_parser PUBLIC ${_fetch_http_parser_SOURCE_DIR}/) 49 | add_library(http_parser::http_parser ALIAS http_parser) 50 | 51 | conan_cmake_run( 52 | REQUIRES 53 | fmt/7.0.1 54 | spdlog/1.7.0 55 | ctre/2.8.2 56 | BASIC_SETUP CMAKE_TARGETS 57 | BUILD outdated) 58 | 59 | # 60 | # cppcoro/http 61 | # 62 | add_library(${PROJECT_NAME} STATIC 63 | include/cppcoro/tcp/tcp.hpp 64 | include/cppcoro/http/http.hpp 65 | include/cppcoro/http/http_message.hpp 66 | include/cppcoro/http/http_request.hpp 67 | include/cppcoro/http/http_response.hpp 68 | include/cppcoro/http/http_server.hpp 69 | include/cppcoro/http/http_client.hpp 70 | include/cppcoro/http/http_connection.hpp 71 | include/cppcoro/http/request_processor.hpp 72 | include/cppcoro/http/route_controller.hpp 73 | include/cppcoro/http/route_parameter.hpp 74 | 75 | include/cppcoro/http/details/router.hpp 76 | include/cppcoro/http/details/static_parser_handler.hpp 77 | 78 | include/cppcoro/details/function_traits.hpp 79 | include/cppcoro/details/type_index.hpp 80 | 81 | src/http.cpp 82 | ) 83 | target_include_directories(${PROJECT_NAME} PUBLIC include) 84 | target_link_libraries(${PROJECT_NAME} PUBLIC 85 | cppcoro::cppcoro 86 | http_parser::http_parser 87 | CONAN_PKG::ctre 88 | CONAN_PKG::fmt 89 | CONAN_PKG::spdlog) 90 | target_precompile_headers(${PROJECT_NAME} INTERFACE 91 | 92 | 93 | ) 94 | add_library(cppcoro::http ALIAS ${PROJECT_NAME}) 95 | 96 | option(BUILD_EXAMPLES "Build examples" ON) 97 | if (BUILD_EXAMPLES) 98 | add_subdirectory(examples) 99 | endif() 100 | 101 | enable_testing() 102 | if (BUILD_TESTING) 103 | add_subdirectory(tests) 104 | endif() 105 | -------------------------------------------------------------------------------- /examples/readme.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | using namespace cppcoro; 13 | 14 | int main() { 15 | struct session { 16 | int id = std::rand(); 17 | }; 18 | 19 | using hello_controller_def = http::route_controller< 20 | R"(/hello/(\w+))", // route definition 21 | session, 22 | http::string_request, 23 | struct hello_controller>; 24 | 25 | struct hello_controller : hello_controller_def 26 | { 27 | using hello_controller_def::hello_controller_def; 28 | // method handlers 29 | auto on_post(std::string_view who) -> task { 30 | co_return http::string_response{http::status::HTTP_STATUS_OK, 31 | fmt::format("post at {}: hello {}", session().id, who)}; 32 | } 33 | auto on_get(std::string_view who) -> task { 34 | co_return http::string_response{http::status::HTTP_STATUS_OK, 35 | fmt::format("get at {}: hello {}", session().id, who)}; 36 | } 37 | }; 38 | 39 | struct hello_chunk_provider : http::abstract_chunk_base { 40 | std::string_view who; 41 | using http::abstract_chunk_base::abstract_chunk_base; 42 | hello_chunk_provider(io_service& service, std::string_view who) : http::abstract_chunk_base{service}, who{who} {} 43 | async_generator read(size_t) { 44 | co_yield "hello\n"; 45 | co_yield fmt::format("{}\n", who); 46 | } 47 | }; 48 | using hello_chunked_response = http::abstract_response; 49 | 50 | using hello_chunk_controller_def = http::route_controller< 51 | R"(/chunk/(\w+))", // route definition 52 | session, 53 | http::string_request, 54 | struct hello_chunk_controller>; 55 | 56 | struct hello_chunk_controller : hello_chunk_controller_def 57 | { 58 | using hello_chunk_controller_def::hello_chunk_controller_def; 59 | // method handlers 60 | task on_get(std::string_view who) { 61 | co_return hello_chunked_response{http::status::HTTP_STATUS_OK, 62 | hello_chunk_provider {service(), who}}; 63 | } 64 | }; 65 | 66 | io_service service; 67 | 68 | auto do_serve = [&]() -> task<> { 69 | auto _ = on_scope_exit([&] { 70 | service.stop(); 71 | }); 72 | http::controller_server< 73 | session, 74 | hello_controller, 75 | hello_chunk_controller> server { 76 | service, 77 | *net::ip_endpoint::from_string("127.0.0.1:4242") 78 | }; 79 | co_await server.serve(); 80 | }; 81 | (void) sync_wait(when_all( 82 | do_serve(), 83 | [&]() -> task<> { 84 | service.process_events(); 85 | co_return; 86 | }())); 87 | return 0; 88 | } 89 | -------------------------------------------------------------------------------- /tests/test_server.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | #include 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | using namespace cppcoro; 16 | 17 | namespace rng = std::ranges; 18 | 19 | constexpr auto test_thread_count = 3; 20 | constexpr auto test_endpoint = "127.0.0.1:4242"; 21 | 22 | SCENARIO("echo server should work", "[cppcoro-http][server][echo]") { 23 | http::logging::log_level = spdlog::level::debug; 24 | 25 | io_service ios; 26 | 27 | struct session {}; 28 | 29 | using echo_route_controller_def = http::route_controller< 30 | R"(/echo)", // route definition 31 | session, 32 | http::string_request, 33 | struct echo_controller>; 34 | 35 | struct echo_controller : echo_route_controller_def 36 | { 37 | using echo_route_controller_def::echo_route_controller_def; 38 | auto on_get() -> task { 39 | co_return http::string_response {http::status::HTTP_STATUS_OK, 40 | fmt::format("{}", co_await request().read_body())}; 41 | } 42 | }; 43 | using echo_server = http::controller_server; 44 | echo_server server{ios, *net::ip_endpoint::from_string(test_endpoint)}; 45 | std::vector tp{test_thread_count}; 46 | auto start_threads = [&tp, &ios] { 47 | rng::generate(tp, [&ios] { 48 | return std::thread{[&] { 49 | ios.process_events(); 50 | }}; 51 | }); 52 | }; 53 | auto join_treads = [&tp] { 54 | rng::for_each(tp, [](auto &&t) { 55 | t.join(); 56 | }); 57 | }; 58 | GIVEN("An echo server") { 59 | WHEN("...") { 60 | http::client client{ios}; 61 | sync_wait(when_all( 62 | [&]() -> task<> { 63 | auto _ = on_scope_exit([&] { 64 | ios.stop(); 65 | }); 66 | co_await server.serve(); 67 | } (), 68 | [&]() -> task<> { 69 | auto _ = on_scope_exit([&] { 70 | server.stop(); 71 | }); 72 | auto conn = co_await client.connect(*net::ip_endpoint::from_string(test_endpoint)); 73 | auto response = co_await conn.get("/echo", "hello"); 74 | REQUIRE(response->status == http::status::HTTP_STATUS_OK); 75 | REQUIRE(co_await response->read_body() == "hello"); 76 | }(), 77 | // [&]() -> task<> { 78 | // auto conn = co_await client.connect(*net::ip_endpoint::from_string(test_endpoint)); 79 | // auto response = co_await conn.get("/echo", "olleh"); 80 | // REQUIRE(response->status == http::status::HTTP_STATUS_OK); 81 | // REQUIRE(co_await response->read_body() == "olleh"); 82 | // }(), 83 | [&]() -> task<> { 84 | start_threads(); 85 | co_return; 86 | }())); 87 | join_treads(); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /include/cppcoro/tcp/tcp.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file cppcoro/tcp.hpp 3 | */ 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | namespace cppcoro { 13 | namespace net { 14 | template 15 | auto create_tcp_socket(io_service &ios, const ip_endpoint &endpoint) { 16 | auto sock = socket{endpoint.is_ipv4() ? socket::create_tcpv4(ios) : socket::create_tcpv6(ios)}; 17 | if constexpr (bind) { 18 | sock.bind(endpoint); 19 | } 20 | return sock; 21 | } 22 | } 23 | namespace tcp { 24 | class connection 25 | { 26 | public: 27 | connection(connection &&other) noexcept 28 | : sock_{std::move(other.sock_)}, ct_{std::move(other.ct_)} {} 29 | 30 | connection(const connection &) = delete; 31 | 32 | connection(net::socket socket, cancellation_token ct) 33 | : sock_{std::move(socket)}, 34 | ct_{std::move(ct)} { 35 | } 36 | 37 | [[nodiscard]] const net::ip_endpoint &peer_address() const { 38 | return sock_.remote_endpoint(); 39 | } 40 | 41 | [[nodiscard]] const auto &socket() const { return sock_; } 42 | 43 | protected: 44 | net::socket sock_; 45 | cancellation_token ct_; 46 | }; 47 | 48 | class server 49 | { 50 | public: 51 | server(server &&other) noexcept: ios_{other.ios_}, endpoint_{std::move(other.endpoint_)}, 52 | socket_{std::move(other.socket_)}, cs_{other.cs_} {} 53 | 54 | server(const server &) = delete; 55 | 56 | server(io_service &ios, const net::ip_endpoint &endpoint) 57 | : ios_{ios}, endpoint_{endpoint}, socket_{net::create_tcp_socket(ios, endpoint_)} { 58 | socket_.listen(); 59 | } 60 | 61 | task accept() { 62 | auto sock = net::create_tcp_socket(ios_, endpoint_); 63 | co_await socket_.accept(sock, cs_.token()); 64 | co_return connection{std::move(sock), cs_.token()}; 65 | } 66 | 67 | void stop() { 68 | cs_.request_cancellation(); 69 | } 70 | 71 | auto token() noexcept { return cs_.token(); } 72 | 73 | auto &service() noexcept { return ios_; } 74 | 75 | protected: 76 | io_service &ios_; 77 | net::ip_endpoint endpoint_; 78 | net::socket socket_; 79 | cancellation_source cs_; 80 | }; 81 | 82 | class client 83 | { 84 | public: 85 | client(client &&other) noexcept: ios_{other.ios_}, cs_{other.cs_} {} 86 | 87 | client(const client &) = delete; 88 | 89 | client(io_service &ios) 90 | : ios_{ios} { 91 | } 92 | 93 | task connect(net::ip_endpoint const&endpoint) { 94 | auto sock = net::create_tcp_socket(ios_, endpoint); 95 | co_await sock.connect(endpoint, cs_.token()); 96 | co_return connection{std::move(sock), cs_.token()}; 97 | } 98 | 99 | void stop() { 100 | cs_.request_cancellation(); 101 | } 102 | 103 | auto &service() noexcept { return ios_; } 104 | 105 | protected: 106 | io_service &ios_; 107 | cancellation_source cs_; 108 | }; 109 | } 110 | } -------------------------------------------------------------------------------- /include/cppcoro/http/http_chunk_provider.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file cppcoro/http/http_chunk_provider.hpp 3 | * @author Garcia Sylvain 4 | */ 5 | #pragma once 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | namespace cppcoro::http { 15 | 16 | /** 17 | * @brief Abstract chunk base. 18 | * 19 | * Implements base requirements for chunk providers/processors (default service ctor, move ctor & assignment op). 20 | */ 21 | class abstract_chunk_base 22 | { 23 | std::reference_wrapper service_; 24 | public: 25 | // needed for default construction 26 | abstract_chunk_base(io_service &service) noexcept: service_{service} {} 27 | 28 | abstract_chunk_base(abstract_chunk_base &&other) noexcept = default; 29 | 30 | abstract_chunk_base &operator=(abstract_chunk_base &&other) noexcept = default; 31 | 32 | protected: 33 | auto &service() { return service_; } 34 | }; 35 | 36 | /** 37 | * @brief Read only file chunk provider. 38 | * 39 | * Chunk provider implementation for read_only_file access. 40 | */ 41 | struct read_only_file_chunk_provider : http::abstract_chunk_base 42 | { 43 | using abstract_chunk_base::abstract_chunk_base; 44 | 45 | std::string path_; 46 | 47 | read_only_file_chunk_provider(io_service &service, std::string_view path) noexcept: 48 | abstract_chunk_base{service}, path_{path} {} 49 | 50 | async_generator read(size_t chunk_size) { 51 | if (!path_.empty()) { 52 | auto f = read_only_file::open(service(), path_); 53 | std::string buffer; 54 | buffer.resize(chunk_size); 55 | uint64_t offset = 0; 56 | auto to_send = f.size(); 57 | size_t res; 58 | do { 59 | res = co_await f.read(offset, buffer.data(), chunk_size); 60 | to_send -= res; 61 | offset += res; 62 | co_yield std::string_view{buffer.data(), res}; 63 | } while (to_send); 64 | } 65 | } 66 | }; 67 | 68 | static_assert(std::constructible_from); 69 | static_assert(http::detail::ro_chunked_body); 70 | 71 | using read_only_file_chunked_response = http::abstract_response; 72 | using read_only_file_chunked_request = http::abstract_request; 73 | 74 | /** 75 | * @brief Write only file chunk processor. 76 | * 77 | * Chunk processor implementation for write_only_file access. 78 | */ 79 | struct write_only_file_processor : abstract_chunk_base 80 | { 81 | using abstract_chunk_base::abstract_chunk_base; 82 | 83 | void init(std::string_view path) { 84 | file_.emplace(write_only_file::open(service(), path, file_open_mode::create_always)); 85 | } 86 | 87 | std::optional file_; 88 | size_t offset = 0; 89 | 90 | task write(std::string_view chunk) { 91 | assert(file_); 92 | auto size = co_await file_->write(offset, chunk.data(), chunk.size()); 93 | offset += size; 94 | co_return size; 95 | } 96 | }; 97 | 98 | 99 | static_assert(std::constructible_from); 100 | static_assert(detail::wo_chunked_body); 101 | 102 | using write_only_file_chunked_response = abstract_response; 103 | using write_only_file_chunked_request = abstract_request; 104 | } 105 | -------------------------------------------------------------------------------- /include/cppcoro/details/function_traits.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace cppcoro::detail { 6 | 7 | namespace function_detail { 8 | 9 | struct parameters_tuple_all_enabled 10 | { 11 | template 12 | static constexpr bool enabled = true; 13 | }; 14 | 15 | template 16 | struct parameters_tuple_disable 17 | { 18 | template 19 | static constexpr bool enabled = !std::disjunction_v, std::remove_cvref_t>...>; 20 | }; 21 | 22 | template 23 | struct types 24 | { 25 | using is_mutable = IsMutable; 26 | using is_lambda = IsLambda; 27 | 28 | static constexpr auto is_function() { return !is_lambda(); } 29 | 30 | enum 31 | { 32 | arity = sizeof...(Args) 33 | }; 34 | 35 | using return_type = Ret; 36 | 37 | using tuple_type = std::tuple; 38 | 39 | template 40 | struct arg 41 | { 42 | using type = typename std::tuple_element::type; 43 | using clean_type = std::remove_cvref_t; 44 | }; 45 | 46 | using function_type = std::function; 47 | 48 | template 49 | struct parameters_tuple 50 | { 51 | 52 | template 53 | static constexpr auto __make_tuple() { 54 | if constexpr (!sizeof...(RestT)) { 55 | if constexpr (Predicate::template enabled) 56 | return std::make_tuple({}); 57 | else return std::tuple(); 58 | } else { 59 | if constexpr (Predicate::template enabled) 60 | return std::tuple_cat(std::make_tuple({}), __make_tuple()); 61 | else return __make_tuple(); 62 | } 63 | } 64 | 65 | struct _make 66 | { 67 | constexpr auto operator()() { 68 | if constexpr (sizeof...(Args) == 0) 69 | return std::make_tuple(); 70 | else return __make_tuple(); 71 | } 72 | }; 73 | 74 | static constexpr auto make() { 75 | return _make{}(); 76 | } 77 | 78 | using tuple_type = std::invoke_result_t<_make>; 79 | }; 80 | }; 81 | } 82 | 83 | template 84 | struct function_traits 85 | : function_traits::operator())> 86 | { 87 | }; 88 | 89 | // mutable lambda 90 | template 91 | struct function_traits 92 | : function_detail::types 93 | { 94 | }; 95 | 96 | // immutable lambda 97 | template 98 | struct function_traits 99 | : function_detail::types 100 | { 101 | }; 102 | 103 | // std::function 104 | template 105 | struct function_traits> 106 | : function_detail::types 107 | { 108 | }; 109 | 110 | // c-function 111 | template 112 | struct function_traits> 113 | : function_detail::types 114 | { 115 | }; 116 | } 117 | -------------------------------------------------------------------------------- /examples/hello_world.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | using namespace cppcoro; 18 | namespace rng = std::ranges; 19 | 20 | struct session 21 | { 22 | int id = std::rand(); 23 | }; 24 | 25 | using add_controller_def = http::route_controller< 26 | R"(/add/(\d+)/(\d+))", // route definition 27 | session, 28 | http::string_request, 29 | struct add_controller>; 30 | 31 | struct add_controller : add_controller_def 32 | { 33 | using add_controller_def::add_controller_def; 34 | 35 | auto on_get(int lhs, int rhs) -> task { 36 | co_return http::string_response{http::status::HTTP_STATUS_OK, 37 | fmt::format("{}", lhs + rhs)}; 38 | } 39 | }; 40 | 41 | using hello_controller_def = http::route_controller< 42 | R"(/hello/(\w+))", // route definition 43 | session, 44 | http::string_request, 45 | struct hello_controller>; 46 | 47 | struct hello_controller : hello_controller_def 48 | { 49 | using hello_controller_def::hello_controller_def; 50 | 51 | auto on_get(const std::string &who) -> task { 52 | co_return http::string_response{http::status::HTTP_STATUS_OK, 53 | fmt::format("Hello {}", who)}; 54 | } 55 | }; 56 | 57 | using cat_controller_def = http::route_controller< 58 | R"(/cat)", // route definition 59 | session, 60 | http::string_request, 61 | struct cat_controller>; 62 | 63 | struct cat_controller : cat_controller_def 64 | { 65 | using cat_controller_def::cat_controller_def; 66 | 67 | auto on_get() -> task { 68 | co_return http::read_only_file_chunked_response{http::status::HTTP_STATUS_OK, 69 | http::read_only_file_chunk_provider{service(), __FILE__}}; 70 | } 71 | }; 72 | 73 | using hello_server = http::controller_server; 77 | 78 | std::optional g_server; 79 | 80 | void at_exit(int) { 81 | fmt::print("exit requested\n"); 82 | if (g_server) { 83 | fmt::print("stopping server\n"); 84 | g_server->stop(); 85 | fmt::print("done\n"); 86 | } 87 | } 88 | 89 | int main(const int argc, const char **argv) { 90 | 91 | http::logging::log_level = spdlog::level::debug; 92 | 93 | using namespace cppcoro; 94 | 95 | std::vector args{argv + 1, argv + argc}; 96 | auto server_endpoint = net::ip_endpoint::from_string(args.empty() ? "127.0.0.1:4242" : args.at(0)); 97 | fmt::print("listening at '{}'\n", server_endpoint->to_string()); 98 | 99 | std::signal(SIGTERM, at_exit); 100 | std::signal(SIGINT, at_exit); 101 | 102 | io_service service; 103 | 104 | //#define SINGLE_THREAD 105 | #ifndef SINGLE_THREAD 106 | static const constinit int thread_count = 5; 107 | std::array thread_pool; 108 | rng::generate(thread_pool, [&] { 109 | return std::thread([&] { 110 | service.process_events(); 111 | }); 112 | }); 113 | #endif 114 | 115 | auto do_serve = [&]() -> task<> { 116 | auto _ = on_scope_exit([&] { 117 | service.stop(); 118 | }); 119 | g_server.emplace( 120 | service, 121 | *server_endpoint); 122 | co_await g_server->serve(); 123 | }; 124 | 125 | (void) sync_wait(when_all( 126 | do_serve() 127 | //#ifdef SINGLE_THREAD 128 | , [&]() -> task<> { 129 | service.process_events(); 130 | co_return; 131 | }() 132 | //#endif 133 | )); 134 | 135 | #ifndef SINGLE_THREAD 136 | rng::for_each(thread_pool, [](auto &&th) { 137 | th.join(); 138 | }); 139 | #endif 140 | 141 | std::cout << "Bye !\n"; 142 | return 0; 143 | } 144 | -------------------------------------------------------------------------------- /tests/test_chunked.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | using namespace cppcoro; 19 | 20 | SCENARIO("chunked transfers should work", "[cppcoro-http][server][chunked]") { 21 | io_service ios; 22 | 23 | GIVEN("An chunk server") { 24 | 25 | struct session 26 | { 27 | }; 28 | 29 | using test_reader_controller_def = http::route_controller< 30 | R"(/read)", // route definition 31 | session, 32 | http::string_request, 33 | struct test_reader_controller>; 34 | 35 | struct test_reader_controller : test_reader_controller_def 36 | { 37 | using test_reader_controller_def::test_reader_controller_def; 38 | 39 | auto on_get() -> task { 40 | co_return http::read_only_file_chunked_response{http::status::HTTP_STATUS_OK, 41 | http::read_only_file_chunk_provider{service(), __FILE__}}; 42 | } 43 | }; 44 | static_assert(not http::detail::has_init_request_handler); 45 | 46 | using test_writer_controller_def = http::route_controller< 47 | R"(/write/([\w\.]+))", // route definition 48 | session, 49 | http::write_only_file_chunked_request, 50 | struct test_writer_controller>; 51 | 52 | struct test_writer_controller : test_writer_controller_def { 53 | using test_writer_controller_def::test_writer_controller_def; 54 | 55 | void init_request(std::string_view filename, http::write_only_file_chunked_request &request) { 56 | request.body_access.init(filename); 57 | } 58 | 59 | task on_post(std::string_view filename) { 60 | co_return http::string_response{ 61 | http::status::HTTP_STATUS_OK, 62 | fmt::format("successfully wrote {}", filename) 63 | }; 64 | } 65 | }; 66 | static_assert(http::detail::has_init_request_handler); 67 | 68 | using chunk_server = http::controller_server; 72 | chunk_server server{ios, *net::ip_endpoint::from_string("127.0.0.1:4242")}; 73 | 74 | WHEN("...") { 75 | http::client client{ios}; 76 | sync_wait(when_all( 77 | [&]() -> task<> { 78 | auto _ = on_scope_exit([&] { 79 | ios.stop(); 80 | }); 81 | co_await server.serve(); 82 | }(), 83 | [&]() -> task<> { 84 | auto _ = on_scope_exit([&] { 85 | server.stop(); 86 | }); 87 | auto conn = co_await client.connect(*net::ip_endpoint::from_string("127.0.0.1:4242")); 88 | auto f = read_only_file::open(ios, __FILE__); 89 | std::string content; 90 | content.resize(f.size()); 91 | auto[body, f_size] = co_await when_all( 92 | [&]() -> task { 93 | auto response = co_await conn.get("/read"); 94 | REQUIRE(response->status == http::status::HTTP_STATUS_OK); 95 | co_return std::string{co_await response->read_body()}; 96 | }(), 97 | f.read(0, content.data(), content.size())); 98 | REQUIRE(body == content); 99 | auto response = co_await conn.post("/write/test.txt", std::move(body)); 100 | REQUIRE(response->status == http::status::HTTP_STATUS_OK); 101 | auto f2 = read_only_file::open(ios, "test.txt"); 102 | std::string content2; 103 | content2.resize(f2.size()); 104 | co_await f2.read(0, content2.data(), content2.size()); 105 | REQUIRE(content2 == content); // copied successful 106 | co_return; 107 | }(), 108 | [&]() -> task<> { 109 | ios.process_events(); 110 | co_return; 111 | }()) 112 | ); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /include/cppcoro/http/details/router.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | #include 15 | 16 | #include 17 | #include 18 | 19 | namespace cppcoro::http::detail { 20 | 21 | using namespace cppcoro::detail; 22 | 23 | template 24 | struct view_handler_traits_impl : FunctionTraitsT 25 | { 26 | using parameters_tuple_type = typename FunctionTraitsT::template parameters_tuple; 27 | using data_type = typename parameters_tuple_type::tuple_type; 28 | // TODO handle non-awaitables 29 | using await_result_type = std::conditional_t, void, typename FunctionTraitsT::return_type::value_type>; 30 | static const constexpr size_t data_arity = std::tuple_size_v; 31 | 32 | static constexpr auto make_tuple = parameters_tuple_type::make; 33 | 34 | template 35 | static void _load_data(data_type &data, const MatcherT &match) { 36 | if constexpr (type_idx < data_arity) { 37 | using ParamT = typename FunctionTraitsT::template arg::clean_type; 38 | std::get(data) = route_parameter::load(match.template get()); 39 | _load_data::group_count() + 1>(data, match); 40 | } 41 | } 42 | 43 | template 44 | static bool load_data(const MatcherT &match, data_type &data) { 45 | _load_data<0, 1>(data, match); 46 | return true; 47 | } 48 | }; 49 | 50 | template 51 | static constexpr void _load_data(TupleT &data, const MatcherT &match) { 52 | if constexpr (type_idx < max) { 53 | using ParamT = std::decay_t(data))>; 54 | std::get(data) = route_parameter::load(match.template get()); 55 | _load_data::group_count() + 1, max>(data, match); 56 | } 57 | } 58 | 59 | template 60 | static constexpr bool load_data(const MatcherT &match, TupleT &data) { 61 | _load_data<0, 1, std::tuple_size_v>(data, match); 62 | return true; 63 | } 64 | 65 | template 66 | static constexpr bool load_data(const MatcherT &match, TupleT &data) { 67 | _load_data<0, 1, max_size>(data, match); 68 | return true; 69 | } 70 | 71 | template 72 | struct view_handler_traits 73 | : view_handler_traits::operator())> 74 | { 75 | }; 76 | 77 | // mutable lambda 78 | template 79 | struct view_handler_traits 80 | : view_handler_traits_impl> 81 | { 82 | }; 83 | 84 | // immutable lambda 85 | template 86 | struct view_handler_traits 87 | : view_handler_traits_impl> 88 | { 89 | }; 90 | 91 | // std::function 92 | template 93 | struct view_handler_traits> 94 | : view_handler_traits_impl> 95 | { 96 | }; 97 | 98 | // c-function 99 | template 100 | struct view_handler_traits 101 | : view_handler_traits_impl> 102 | { 103 | }; 104 | 105 | // helper type for the visitor #4 106 | template 107 | struct overloaded : Ts ... 108 | { 109 | using Ts::operator()...; 110 | }; 111 | // explicit deduction guide (not needed as of C++20) 112 | template overloaded(Ts...) -> overloaded; 113 | 114 | } // namespace xdev::net::detail 115 | -------------------------------------------------------------------------------- /include/cppcoro/http/http_message.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file cppcoro/http/htt_message.hpp 3 | * @author Garcia Sylvain 4 | */ 5 | #pragma once 6 | 7 | #include 8 | 9 | #include 10 | 11 | namespace cppcoro::http { 12 | 13 | namespace detail { 14 | 15 | enum { max_body_size = 1024 }; 16 | 17 | struct base_message 18 | { 19 | base_message() = default; 20 | 21 | explicit base_message(http::headers &&headers) 22 | : headers{std::forward(headers)} {} 23 | 24 | base_message(base_message const &other) = delete; 25 | 26 | base_message &operator=(base_message const &other) = delete; 27 | 28 | base_message(base_message &&other) = default; 29 | 30 | base_message &operator=(base_message &&other) = default; 31 | 32 | http::headers headers; 33 | 34 | virtual bool is_chunked() = 0; 35 | virtual std::string build_header() = 0; 36 | virtual task read_body(size_t max_size = max_body_size) = 0; 37 | virtual task write_body(std::string_view data) = 0; 38 | }; 39 | 40 | struct base_request : base_message 41 | { 42 | static constexpr bool is_request = true; 43 | static constexpr bool is_response = false; 44 | using base_message::base_message; 45 | 46 | base_request(base_request &&other) = default; 47 | base_request& operator=(base_request &&other) = default; 48 | 49 | base_request(http::method method, std::string &&path, http::headers &&headers = {}) 50 | : base_message{std::forward(headers)}, method{method}, 51 | path{std::forward(path)} {} 52 | 53 | http::method method; 54 | std::string path; 55 | 56 | [[nodiscard]] auto method_str() const { 57 | return http_method_str(static_cast(method)); 58 | } 59 | 60 | std::string to_string() const { 61 | return fmt::format("{} {}", method_str(), path); 62 | } 63 | }; 64 | 65 | struct base_response : base_message 66 | { 67 | static constexpr bool is_response = true; 68 | static constexpr bool is_request = false; 69 | using base_message::base_message; 70 | 71 | base_response(base_response &&other) = default; 72 | base_response& operator=(base_response &&other) = default; 73 | 74 | base_response(http::status status, http::headers &&headers = {}) 75 | : base_message{std::forward(headers)}, status{status} {} 76 | 77 | http::status status; 78 | 79 | [[nodiscard]] auto status_str() const { 80 | return http_status_str(static_cast(status)); 81 | } 82 | 83 | std::string to_string() const { 84 | return status_str(); 85 | } 86 | }; 87 | 88 | template 89 | struct abstract_message : std::conditional_t<_is_response, base_response, base_request> 90 | { 91 | using base_type = std::conditional_t<_is_response, base_response, base_request>; 92 | static constexpr bool is_response = _is_response; 93 | static constexpr bool is_request = !_is_response; 94 | 95 | using base_type::base_type; 96 | 97 | using body_type = BodyT; 98 | BodyT body_access; 99 | 100 | std::optional> chunk_generator_; 101 | std::optional::iterator> chunk_generator_it_; 102 | 103 | abstract_message(http::status status, BodyT &&body = {}, http::headers &&headers = {}) requires (is_response) 104 | : base_response{status, std::forward(headers)} 105 | , body_access{std::forward(body)} { 106 | } 107 | 108 | abstract_message(http::method method, std::string &&path, BodyT &&body = {}, http::headers &&headers = {}) requires (is_request) 109 | : base_request{method, std::forward(path), std::forward(headers)} 110 | , body_access{std::forward(body)} { 111 | } 112 | 113 | // explicit abstract_message(base_type &&base) noexcept : base_type(std::move(base)) {} 114 | // abstract_message& operator=(base_type &&base) noexcept { 115 | // static_cast(*this) = std::move(base); 116 | // } 117 | 118 | bool is_chunked() final { 119 | if constexpr (ro_chunked_body or wo_chunked_body) { 120 | return true; 121 | } else { 122 | return false; 123 | } 124 | } 125 | 126 | task read_body(size_t max_size = max_body_size) final { 127 | if constexpr (ro_basic_body) { 128 | co_return std::string_view{body_access.data(), body_access.size()}; 129 | } else if constexpr (ro_chunked_body) { 130 | if (not chunk_generator_) { 131 | chunk_generator_ = body_access.read(max_size); 132 | chunk_generator_it_ = co_await chunk_generator_->begin(); 133 | if (*chunk_generator_it_ != chunk_generator_->end()) { 134 | co_return **chunk_generator_it_; 135 | } 136 | } else if (co_await ++*chunk_generator_it_ != chunk_generator_->end()) { 137 | co_return **chunk_generator_it_; 138 | } 139 | co_return std::string_view{}; 140 | } 141 | } 142 | 143 | task write_body(std::string_view data) final { 144 | if constexpr (wo_basic_body) { 145 | auto size = data.size(); 146 | this->body_access.append(data); 147 | co_return size; 148 | } else if constexpr (wo_chunked_body) { 149 | co_return co_await this->body_access.write(data); 150 | } 151 | } 152 | 153 | inline std::string build_header() final { 154 | std::string output = _header_base(); 155 | auto write_header = [&output](const std::string &field, const std::string &value) { 156 | output += fmt::format("{}: {}\r\n", field, value); 157 | }; 158 | if constexpr (ro_basic_body) { 159 | this->headers["Content-Length"] = std::to_string(this->body_access.size()); 160 | } else if constexpr (ro_chunked_body) { 161 | this->headers["Transfer-Encoding"] = "chunked"; 162 | } 163 | for (auto &[field, value] : this->headers) { 164 | write_header(field, value); 165 | } 166 | output += "\r\n"; 167 | return output; 168 | } 169 | 170 | private: 171 | inline auto _header_base() { 172 | if constexpr (is_response) { 173 | return fmt::format("HTTP/1.1 {} {}\r\n" 174 | "UserAgent: cppcoro-http/0.0\r\n", 175 | int(this->status), 176 | http_status_str(this->status)); 177 | } else { 178 | return fmt::format("{} {} HTTP/1.1\r\n" 179 | "UserAgent: cppcoro-http/0.0\r\n", 180 | this->method_str(), 181 | this->path); 182 | } 183 | } 184 | }; 185 | 186 | 187 | } 188 | 189 | template 190 | using abstract_response = detail::abstract_message; 191 | 192 | template 193 | using abstract_request = detail::abstract_message; 194 | 195 | struct request_parser : detail::static_parser_handler { 196 | using detail::static_parser_handler::static_parser_handler; 197 | }; 198 | 199 | struct response_parser : detail::static_parser_handler { 200 | using detail::static_parser_handler::static_parser_handler; 201 | }; 202 | } -------------------------------------------------------------------------------- /examples/simple_co_http_server/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | using namespace cppcoro; 15 | namespace fs = std::filesystem; 16 | namespace rng = std::ranges; 17 | 18 | struct session 19 | { 20 | 21 | }; 22 | 23 | using home_controller_def = http::route_controller< 24 | R"(/?([^\s]*)?)", 25 | session, 26 | http::string_request, 27 | struct home_controller>; 28 | 29 | struct home_controller : home_controller_def 30 | { 31 | using home_controller_def::home_controller_def; 32 | 33 | static constexpr std::string_view template_ = R"( 34 | 35 | 36 | 37 | 38 | 39 | 40 | {title} 41 | 42 | 43 |
44 |
45 |

Simple CppCoro HTTP Server

46 |

{path}

47 |
48 | 51 | {body} 52 |
53 | 54 | 55 | 56 | 57 | )"; 58 | 59 | fmt::memory_buffer make_body(std::string_view path) { 60 | auto chrooted_path = (fs::relative(fs::path{path.data(), path.data() + path.size()})).string(); 61 | if (chrooted_path.empty()) 62 | chrooted_path = "."; 63 | fmt::print("-- get: {} ({})\n", path, chrooted_path); 64 | fmt::memory_buffer out; 65 | fmt::format_to(out, R"(
)"); 66 | for (auto &p: fs::directory_iterator(chrooted_path)) { 67 | fmt::format_to(out, R"({path})", 68 | fmt::arg("path", fs::relative(p.path(), chrooted_path).c_str()), 69 | fmt::arg("full_path", fs::relative(p.path()).c_str())); 70 | } 71 | fmt::format_to(out, "
"); 72 | return out; 73 | } 74 | 75 | fmt::memory_buffer make_breadcrumb(std::string_view path) { 76 | fmt::memory_buffer buff; 77 | constexpr std::string_view init = R"(