commit af684cc837c85b9f0be573a5217a044c471d41e5 Author: sha512sum Date: Wed Mar 13 13:09:35 2024 +0000 v1 diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..456fc5b --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,54 @@ +cmake_minimum_required(VERSION 3.27) +project(cserver VERSION 1) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CXX_EXTENSIONS NO) +set(Boost_USE_MULTITHREADED ON) +find_package(fmt REQUIRED) +find_package(Boost 1.84.0 REQUIRED COMPONENTS url) +find_package(utempl REQUIRED) +find_package(llhttp REQUIRED) +find_package(OpenSSL REQUIRED) + +cmake_policy(SET CMP0079 NEW) + +include(GNUInstallDirs) +add_library(cserver INTERFACE) +target_include_directories( + cserver + INTERFACE $ + $) +target_link_libraries(cserver INTERFACE utempl::utempl ${Boost_LIBRARIES} llhttp uring ${OPENSSL_LIBRARIES} ) + +target_compile_features(cserver INTERFACE cxx_std_20) + +install(TARGETS cserver + EXPORT cserverTargets + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) + +include(CMakePackageConfigHelpers) +write_basic_package_version_file("cserverConfigVersion.cmake" + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion) + +configure_package_config_file( + "${PROJECT_SOURCE_DIR}/cmake/cserverConfig.cmake.in" + "${PROJECT_BINARY_DIR}/cserverConfig.cmake" + INSTALL_DESTINATION + ${CMAKE_INSTALL_DATAROOTDIR}/cserver/cmake) + +install(EXPORT cserverTargets + FILE cserverTargets.cmake + NAMESPACE cserver:: + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/cserver/cmake) + +install(FILES "${PROJECT_BINARY_DIR}/cserverConfig.cmake" + "${PROJECT_BINARY_DIR}/cserverConfigVersion.cmake" + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/cserver/cmake) + +install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/cserver DESTINATION include) + diff --git a/cmake/cserverConfig.cmake.in b/cmake/cserverConfig.cmake.in new file mode 100644 index 0000000..9c15f36 --- /dev/null +++ b/cmake/cserverConfig.cmake.in @@ -0,0 +1,4 @@ +@PACKAGE_INIT@ + +include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") +check_required_components("@PROJECT_NAME@") diff --git a/include/cserver/clients/http/component.hpp b/include/cserver/clients/http/component.hpp new file mode 100644 index 0000000..17cf04a --- /dev/null +++ b/include/cserver/clients/http/component.hpp @@ -0,0 +1,86 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cserver::server::http { + +inline constexpr auto ParseHttpHeader(std::string header) -> std::pair { + size_t pos = header.find(":"); + std::string key = header.substr(0, pos); + std::string value = header.substr(pos + 2); + return std::make_pair(key, value); + +}; +} // namespace cserver::server::http +namespace cserver::components { +template +struct HttpClient { + TaskProcessor& taskProcessor; + boost::asio::ssl::context ctx; + boost::asio::ip::tcp::resolver resolver; + static constexpr utempl::ConstexprString kName = "httpClient"; + inline constexpr auto CreateRequest() { + return clients::http::Request{*this}; + }; + inline constexpr HttpClient(auto, auto& context) : + taskProcessor(context.template FindComponent<"basicTaskProcessor">()), + ctx(boost::asio::ssl::context::method::sslv23_client), + resolver(this->taskProcessor.ioContext) {} + + template + static consteval auto Adder(const T& context) { + using Type = std::remove_cvref_t())>; + return context.TransformComponents( + [&](const ComponentConfig>&) -> ComponentConfig> { + return {}; + }); + }; + template + inline auto PerformRequest(T&& request) -> cserver::Task { + boost::asio::ssl::stream socket(this->taskProcessor.ioContext, this->ctx); + boost::asio::ip::tcp::resolver::iterator endpoint = + co_await this->resolver.async_resolve({request.request.url.host(), request.request.url.has_port() ? request.request.url.port() : request.request.url.scheme()}, boost::asio::use_awaitable); + co_await boost::asio::async_connect(socket.lowest_layer(), endpoint, boost::asio::use_awaitable); + co_await socket.async_handshake(boost::asio::ssl::stream_base::client, boost::asio::use_awaitable); + std::string req(request.request.ToString()); + co_await boost::asio::async_write(socket, boost::asio::buffer(req.data(), req.size()), boost::asio::use_awaitable); + std::string serverResponse; + co_await boost::asio::async_read_until(socket, boost::asio::dynamic_buffer(serverResponse), "\r\n\r\n", boost::asio::use_awaitable); + server::http::HTTPResponse response; + std::istringstream responseStream(std::move(serverResponse)); + std::string httpVersion; + responseStream >> httpVersion >> response.statusCode; + std::getline(responseStream, response.statusMessage); + std::string header; + while(std::getline(responseStream, header) && header.find(":") != std::string::npos) { + response.headers.insert(server::http::ParseHttpHeader(std::move(header))); + }; + if(response.statusCode != 200) { + co_return response; + }; + if(response.headers.contains("Content-Length")) { + auto size = std::stoi(response.headers["Content-Length"]); + response.body.reserve(size); + co_await boost::asio::async_read(socket, boost::asio::dynamic_buffer(response.body), boost::asio::transfer_at_least(size), boost::asio::use_awaitable); + co_return response; + }; + for(;;) { + auto [ec, n] = co_await boost::asio::async_read(socket, boost::asio::dynamic_buffer(response.body), boost::asio::transfer_at_least(1), boost::asio::as_tuple(boost::asio::use_awaitable)); + if(ec && ec == boost::asio::error::eof) { + break; + }; + if(ec) { + throw ec; + }; + }; + co_return response; + }; +}; + +} // namespace cserver::clients::http diff --git a/include/cserver/clients/http/request.hpp b/include/cserver/clients/http/request.hpp new file mode 100644 index 0000000..68cdb8b --- /dev/null +++ b/include/cserver/clients/http/request.hpp @@ -0,0 +1,92 @@ +#pragma once +#include +#include +#include + +namespace cserver::clients::http { +template +struct Request { + HttpClient& client; + server::http::HTTPRequest request; + Request(HttpClient& client) : + client(client) { + this->AddHeader("User-Agent", "cserver/1"); + }; + inline constexpr auto Post() && -> Request&& { + this->request.method = "POST"; + return std::move(*this); + }; + inline constexpr auto Post() & -> Request& { + this->request.method = "POST"; + return *this; + }; + inline constexpr auto Get() && -> Request&& { + this->request.method = "GET"; + return std::move(*this); + }; + inline constexpr auto Get() & -> Request& { + this->request.method = "GET"; + return *this; + }; + inline constexpr auto Put() && -> Request&& { + this->request.method = "PUT"; + return std::move(*this); + }; + inline constexpr auto Put() & -> Request& { + this->request.method = "PUT"; + return *this; + }; + inline constexpr auto SetCustomMethod(std::string method) & -> Request& { + this->request.method = std::move(method); + return *this; + }; + inline constexpr auto SetCustomMethod(std::string method) && -> Request&& { + this->request.method = std::move(method); + return std::move(*this); + }; + inline constexpr auto AddHeader(std::string first, std::string second) && -> Request&& { + this->request.headers.emplace(std::move(first), std::move(second)); + return std::move(*this); + }; + inline constexpr auto AddHeader(std::string first, std::string second) & -> Request& { + this->request.headers.emplace(std::move(first), std::move(second)); + return *this; + }; + inline constexpr auto AddHeaderIfNotExists(std::string check, std::string first, std::string second) & -> Request& { + if(!this->request.headers.contains(std::move(check))) { + return this->AddHeader(std::move(first), std::move(second)); + }; + return *this; + }; + inline constexpr auto AddHeaderIfNotExists(std::string check, std::string first, std::string second) && -> Request&& { + if(!this->request.headers.contains(std::move(check))) { + return std::move(*this).AddHeader(std::move(first), std::move(second)); + }; + return std::move(*this); + }; + inline constexpr auto Url(std::string url) & -> Request& { + this->request.url = boost::urls::url{std::move(url)}; + auto authority = this->request.url.authority(); + return this->AddHeader("Host", std::string{authority.data(), authority.size()}); + }; + inline constexpr auto Url(std::string url) && -> Request&& { + this->request.url = boost::urls::url{std::move(url)}; + auto authority = this->request.url.authority(); + return std::move(*this).AddHeader("Host", std::string{authority.data(), authority.size()}); + }; + inline constexpr auto Data(std::string data) && -> Request&& { + this->request.body = std::move(data); + return std::move(*this); + }; + inline constexpr auto ToString() const -> std::string { + return this->request.ToString(); + }; + inline auto Perform() && -> cserver::Task { + co_return co_await client.PerformRequest(std::move(*this).AddHeaderIfNotExists("Transfer-Encoding", "Content-Length", std::to_string(this->request.body.size()))); + }; + inline auto Perform() & -> cserver::Task { + co_return co_await client.PerformRequest(this->AddHeaderIfNotExists("Transfer-Encoding", "Content-Length", std::to_string(this->request.body.size()))); + }; +}; + +}; diff --git a/include/cserver/engine/basic/task_processor.hpp b/include/cserver/engine/basic/task_processor.hpp new file mode 100644 index 0000000..f223e89 --- /dev/null +++ b/include/cserver/engine/basic/task_processor.hpp @@ -0,0 +1,37 @@ +#pragma once +#include +#include +#include + +namespace cserver::engine::basic { + +template +struct TaskProcessor { + boost::asio::io_context ioContext; + std::array pool; + static constexpr utempl::ConstexprString kName = "basicTaskProcessor"; + inline constexpr TaskProcessor(auto, auto&) : + ioContext{}, + pool{} { + }; + + template + static consteval auto Adder(const T& context) { + constexpr std::size_t Count = T::kConfig.template Get().template Get<"threadPoolSize">(); + return context.TransformComponents( + [&](const ComponentConfig>&) -> ComponentConfig> { + return {}; + }); + }; + + inline auto Run() { + for(auto& thread : this->pool) { + thread = std::jthread([&]{ + boost::asio::executor_work_guardioContext.get_executor())> guard{this->ioContext.get_executor()}; + this->ioContext.run(); + }); + }; + }; +}; + +} diff --git a/include/cserver/engine/components.hpp b/include/cserver/engine/components.hpp new file mode 100644 index 0000000..46f5774 --- /dev/null +++ b/include/cserver/engine/components.hpp @@ -0,0 +1,131 @@ +#pragma once +#include +#include + +namespace cserver { + +template +struct NamedValue { + T value; +}; + +template +struct ComponentConfig {}; + +template +inline constexpr auto TransformIfOk(T&& value, auto&& f) { + if constexpr(requires{f(std::forward(value));}) { + return f(std::forward(value)); + } else { + return value; + }; +}; + + +template +struct ConstexprConfig { + utempl::Tuple data; + template + inline constexpr auto Get() const -> auto { + return [&](const ConstexprConfig...>&){ + constexpr auto list = utempl::TypeList...>{}; + constexpr std::size_t I = Find>(list); + if constexpr(I < sizeof...(Ts)) { + return utempl::Get(this->data).value; + }; + }(*this); + }; + template + inline constexpr auto Append(T&& value) const -> ConstexprConfig>> { + return {.data = TupleCat(this->data, utempl::MakeTuple(NamedValue>{std::forward(value)}))}; + }; +}; + +template +struct ServiceContext { + utempl::Tuple storage; + static constexpr auto kConfig = config; + static constexpr auto kList = utempl::TypeList{}; + inline constexpr ServiceContext() : + storage{ + [&](std::index_sequence) -> utempl::Tuple { + return {[&](utempl::Wrapper) { + return [&]() -> std::remove_cvref_t(storage))> { + return {decltype(Get(names)){}, *this}; + }; + }(utempl::Wrapper{})...}; + }(std::index_sequence_for()) + } {}; + inline constexpr auto Run() { + [&](std::index_sequence) { + ([](auto& component){ + if constexpr(requires{component.Run();}) { + component.Run(); + }; + }(Get(this->storage)), ...); + }(std::index_sequence_for()); + }; + template + inline constexpr auto FindComponent() -> auto& { + constexpr auto I = utempl::Find>(names); + return Get(this->storage); + }; + template + inline constexpr auto FindComponent() -> T& { + constexpr auto I = utempl::Find>(utempl::kTypeList); + return Get(this->storage); + }; + +}; + + + +template +struct ServiceContextBuilder { + static constexpr auto kList = utempl::kTypeList; + static constexpr auto kConfig = config; + template + static consteval auto Append() -> decltype(T::template Adder(ServiceContextBuilder>{})) { + return {}; + }; + + template + static consteval auto Append() -> ServiceContextBuilder> + requires (!requires(ServiceContextBuilder> builder) {T::template Adder(builder);}) { + return {}; + }; + + + template + static consteval auto AppendConfigParam() -> ServiceContextBuilder(Value), Ts...> { + return {}; + }; + + template + static consteval auto TransformComponents(F&& f) -> ServiceContextBuilder { + return {}; + }; + template + static consteval auto FindComponent() { + return [] + (const ServiceContextBuilder...>&) + -> decltype(utempl::Get>(utempl::TypeList...>{})>(utempl::TypeList{})) { + std::unreachable(); + }(ServiceContextBuilder{}); + }; + + static consteval auto Config() { + return config; + }; + + static constexpr auto Run() -> void { + [](utempl::TypeList...>) { + ServiceContext...>, TTs...> context; + context.Run(); + for(;;) { + std::this_thread::sleep_for(std::chrono::minutes(1)); + }; + }(utempl::TypeList{}); + }; +}; +} // namespace cserver diff --git a/include/cserver/engine/coroutine.hpp b/include/cserver/engine/coroutine.hpp new file mode 100644 index 0000000..6329c22 --- /dev/null +++ b/include/cserver/engine/coroutine.hpp @@ -0,0 +1,9 @@ +#pragma once +#include + +namespace cserver { + +template +using Task = boost::asio::awaitable; + +} // namespace cserver diff --git a/include/cserver/server/handlers/http_handler_base.hpp b/include/cserver/server/handlers/http_handler_base.hpp new file mode 100644 index 0000000..2e88002 --- /dev/null +++ b/include/cserver/server/handlers/http_handler_base.hpp @@ -0,0 +1,34 @@ +#pragma once +#include +#include +#include +#include + +namespace cserver::server::handlers { + +template +struct HTTPHandlerBase { + template + static consteval auto Adder(const auto& context) { + return context.TransformComponents([](const ComponentConfig) { + return ComponentConfig>>{}; + }); + }; + inline auto HandleRequest(cserver::server::http::HTTPRequest&& request, + cserver::server::http::HTTPResponse& response) -> cserver::Task { + try { + co_return co_await static_cast(*this).HandleRequestThrow(std::move(request), response); + } catch(const std::exception& err) { + fmt::println("Error in {}: {}", __PRETTY_FUNCTION__, err.what()); + } catch(...) { + fmt::println("Error in {}: Unknown Error", __PRETTY_FUNCTION__); + }; + response.statusCode = 500; + response.statusMessage = "Internal Server Error"; + co_return "Internal Server Error"; + }; + inline constexpr HTTPHandlerBase(auto, auto&) {}; + +}; + +} // namespace cserver::server::handlers diff --git a/include/cserver/server/http/http_request.hpp b/include/cserver/server/http/http_request.hpp new file mode 100644 index 0000000..c58c065 --- /dev/null +++ b/include/cserver/server/http/http_request.hpp @@ -0,0 +1,25 @@ +#pragma once +#include +#include +#include +#include + +namespace cserver::server::http { + +struct HTTPRequest { + std::string method = {}; + boost::urls::url url = {}; + std::unordered_map headers = {}; + std::string body = {}; + inline auto ToString() const -> std::string { + std::ostringstream stream; + stream << fmt::format("{} {} HTTP/1.1\r\n", this->method, this->url.path()); + for(const auto& header : this->headers) { + stream << fmt::format("{}: {}\r\n", header.first, header.second); + }; + stream << fmt::format("\r\n{}\r\n", this->body); + return stream.str(); + }; +}; + +} // namespace cserver::server::http diff --git a/include/cserver/server/http/http_request_parser.hpp b/include/cserver/server/http/http_request_parser.hpp new file mode 100644 index 0000000..368781e --- /dev/null +++ b/include/cserver/server/http/http_request_parser.hpp @@ -0,0 +1,71 @@ +#pragma once +#include +#include + +namespace cserver::server::http { + +struct HTTPRequestParser : private llhttp_t, public HTTPRequest { + bool err = false; + bool done = false; + std::string headerField = {}; + std::string headerValue = {}; + std::string urlString = {}; + inline HTTPRequestParser(std::string_view data) { + llhttp_settings_t settings; + llhttp_settings_init(&settings); + settings.on_method = HTTPRequestParser::OnMethod; + settings.on_url = HTTPRequestParser::OnUrl; + settings.on_url_complete = HTTPRequestParser::OnUrlComplete; + settings.on_header_field = HTTPRequestParser::OnHeaderField; + settings.on_header_value = HTTPRequestParser::OnHeaderValue; + settings.on_headers_complete = HTTPRequestParser::OnHeaderComplete; + settings.on_body = HTTPRequestParser::OnBody; + settings.on_message_complete = HTTPRequestParser::OnMessageComplete; + llhttp_init(this, HTTP_BOTH, &settings); + llhttp_execute(this, data.data(), data.size()); + }; + static inline auto OnMethod(llhttp_t* parser, const char* data, std::size_t size) -> int { + auto* self = static_cast(static_cast(parser)); + self->method.append(data, size); + return 0; + }; + static inline auto OnUrl(llhttp_t* parser, const char* data, std::size_t size) -> int { + auto* self = static_cast(parser); + self->urlString.append(data, size); + return 0; + }; + static inline auto OnUrlComplete(llhttp_t* parser) -> int { + auto* self = static_cast(parser); + self->url = boost::urls::url(self->urlString); + return 0; + }; + static inline auto OnHeaderField(llhttp_t* parser, const char* data, std::size_t size) -> int { + auto* self = static_cast(parser); + self->headerField.append(data, size); + return 0; + }; + static inline auto OnHeaderValue(llhttp_t* parser, const char* data, std::size_t size) -> int { + auto* self = static_cast(parser); + self->headerValue.append(data, size); + return 0; + }; + static inline auto OnHeaderComplete(llhttp_t* parser) -> int { + auto* self = static_cast(parser); + self->headers.emplace(std::move(self->headerField), std::move(self->headerValue)); + self->headerValue.clear(); + self->headerField.clear(); + return 0; + }; + static inline auto OnBody(llhttp_t* parser, const char* data, std::size_t size) -> int { + auto* self = static_cast(parser); + self->body.append(data, size); + return 0; + }; + static inline auto OnMessageComplete(llhttp_t* parser) -> int { + auto* self = static_cast(parser); + self->done = true; + return 0; + }; +}; + +} // namespace cserver::server::http diff --git a/include/cserver/server/http/http_response.hpp b/include/cserver/server/http/http_response.hpp new file mode 100644 index 0000000..e059508 --- /dev/null +++ b/include/cserver/server/http/http_response.hpp @@ -0,0 +1,26 @@ +#pragma once +#include +#include +#include + +namespace cserver::server::http { + +struct HTTPResponse { + unsigned short statusCode = 200; + std::string statusMessage = "OK"; + std::unordered_map headers = {}; + std::string body = {}; + inline auto ToString() const -> std::string { + std::ostringstream stream; + stream << fmt::format("HTTP/1.1 {} {}\r\n", this->statusCode, this->statusMessage); + for(const auto& header : this->headers) { + stream << fmt::format("{}: {}\r\n", header.first, header.second); + }; + if(!this->body.empty()) { + stream << fmt::format("\r\n{}\r\n", this->body); + }; + return stream.str(); + }; +}; + +} // namespace cserver::server::http diff --git a/include/cserver/server/server/server.hpp b/include/cserver/server/server/server.hpp new file mode 100644 index 0000000..3a4bb6a --- /dev/null +++ b/include/cserver/server/server/server.hpp @@ -0,0 +1,98 @@ +#pragma once +#include +#include +#include +#include +#include + + +namespace cserver::server::server { +namespace impl { +template +using GetTypeFromComponentConfig = decltype([](const ComponentConfig&) -> TT {}(std::declval())); + +template +inline constexpr utempl::ConstexprString kNameFromComponentConfig = + decltype([](const ComponentConfig&) { + return utempl::Wrapper{}; + } (std::declval()))::kValue; +} // namespace impl + +template +struct Server { + TaskProcessor& taskProcessor; + utempl::Tuple&...> handlers; + static constexpr utempl::ConstexprString kName = "server"; + unsigned short port; + static constexpr utempl::Tuple kNames = {impl::kNameFromComponentConfig...}; + static constexpr utempl::Tuple kPaths = {impl::GetTypeFromComponentConfig::kPath...}; + template + static consteval auto Adder(const T& context) { + constexpr utempl::ConstexprString tpName = [&]{ + if constexpr(requires{T::kConfig.template Get().template Get<"taskProcessor">();}) { + return T::kConfig.template Get().template Get<"taskProcessor">(); + } else { + return TPName; + }; + }(); + using TP = decltype(context.template FindComponent()); + return context.TransformComponents( + [&](const ComponentConfig>&) -> ComponentConfig> { + return {}; + }); + }; + template + inline constexpr Server(utempl::Wrapper, T& context) : + taskProcessor(context.template FindComponent()), + handlers{ + [&](std::index_sequence) -> utempl::Tuple&...> { + return {context.template FindComponent(kNames)>()...}; + }(std::index_sequence_for()) + }, + port(T::kConfig.template Get().template Get<"port">()) { + }; + auto Reader(boost::asio::ip::tcp::socket socket) -> Task { + std::string buffer; + buffer.reserve(socket.available()); + co_await boost::asio::async_read_until(socket, boost::asio::dynamic_buffer(buffer), "\r\n\r\n", boost::asio::use_awaitable); + http::HTTPRequest request = http::HTTPRequestParser{buffer}; + bool flag = false; + co_await [&](std::index_sequence) -> cserver::Task { + (co_await [&](utempl::Wrapper) -> cserver::Task { + if(request.url.path().substr(0, Get(kPaths).size()) == Get(kPaths)) { + flag = true; + http::HTTPResponse response{}; + response.body = co_await Get(this->handlers).HandleRequest(std::move(request), response); + + response.headers["Content-Length"] = std::to_string(response.body.size()); + response.headers["Server"] = "cserver/1"; + auto data = response.ToString(); + co_await boost::asio::async_write(socket, boost::asio::buffer(data.data(), data.size()), boost::asio::use_awaitable); + socket.close(); + }; + }(utempl::Wrapper{}), ...); + }(std::index_sequence_for()); + constexpr std::string_view error404 = "HTTP/1.1 404 Not Found\r\n" + "Content-Length: 0\r\n" + "\r\n"; + if(!flag) { + co_await boost::asio::async_write(socket, boost::asio::buffer(error404.data(), error404.size()), boost::asio::use_awaitable); + }; + }; + auto Task() -> cserver::Task { + auto executor = co_await boost::asio::this_coro::executor; + boost::asio::ip::tcp::acceptor acceptor{executor, {boost::asio::ip::tcp::v6(), this->port}}; + for(;;) { + auto socket = co_await acceptor.async_accept(boost::asio::use_awaitable); + boost::asio::co_spawn(executor, this->Reader(std::move(socket)), boost::asio::detached); + }; + }; + inline constexpr auto Run() -> void { + boost::asio::co_spawn(this->taskProcessor.ioContext, this->Task(), boost::asio::detached); + }; + inline ~Server() { + }; + template + using AddHandler = Server; +}; +} // namespace cserver::server::server