diff --git a/MODULE.bazel b/MODULE.bazel index 40339aa..af3218c 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,6 +1,6 @@ module( name = "khttpd", - version = "0.5.0", + version = "0.5.1", ) bazel_dep(name = "platforms", version = "1.1.0") diff --git a/example/HelloController.hpp b/example/HelloController.hpp index 5a60ae4..763e428 100644 --- a/example/HelloController.hpp +++ b/example/HelloController.hpp @@ -6,28 +6,28 @@ #define HELLOCONTROLLER_HPP #include "controller/http_controller.hpp" -class HelloController : public khttpd::framework::BaseController -{ -private: - std::string base_path() override - { - return "/hello"; - } +class HelloController + : public khttpd::framework::BaseController { + private: + std::string base_path() override { return "/hello"; } -public: - static std::shared_ptr create() { return std::make_shared(); } + public: + static std::shared_ptr create() { + return std::make_shared(); + } - std::shared_ptr register_routes(khttpd::framework::HttpRouter& router) override - { - KHTTPD_DOCUMENTED_ROUTE(get, "/hello", handle_hello, - {"Controller greeting", "Returns a greeting from a controller-managed route."}); + std::shared_ptr register_routes( + khttpd::framework::HttpRouter& router) override { + KHTTPD_DOCUMENTED_ROUTE( + get, "/hello", handle_hello, + {"Controller greeting", + "Returns a greeting from a controller-managed route."}); return shared_from_this(); } -private: - void handle_hello(khttpd::framework::HttpContext& ctx) - { + private: + void handle_hello(khttpd::framework::HttpContext& ctx) { std::string name = ctx.get_query_param("name").value_or("Guest"); ctx.set_status(boost::beast::http::status::ok); ctx.set_content_type("text/plain"); @@ -35,4 +35,4 @@ class HelloController : public khttpd::framework::BaseController -{ -public: - static std::shared_ptr create() { return std::make_shared(); } +class HelloStreamController + : public khttpd::framework::BaseController { + public: + static std::shared_ptr create() { + return std::make_shared(); + } - std::shared_ptr register_routes(khttpd::framework::HttpRouter& router) override - { - KHTTPD_DOCUMENTED_ROUTE(get, "/stream/:size", handle_stream, - {"Stream response chunks", "Streams up to 100 JSON chunks for the requested size."}); + std::shared_ptr register_routes( + khttpd::framework::HttpRouter& router) override { + KHTTPD_DOCUMENTED_ROUTE( + get, "/stream/:size", handle_stream, + {"Stream response chunks", + "Streams up to 100 JSON chunks for the requested size."}); return shared_from_this(); } -private: + private: size_t num_chunks_to_send_ = 0; - void handle_stream(khttpd::framework::HttpContext& ctx) - { + void handle_stream(khttpd::framework::HttpContext& ctx) { std::string num_str = ctx.get_path_param("size").value_or("0"); num_chunks_to_send_ = std::stoul(num_str); - if (num_chunks_to_send_ > 100) - { + if (num_chunks_to_send_ > 100) { num_chunks_to_send_ = 100; } ctx.set_status(boost::beast::http::status::ok); ctx.set_content_type("application/json"); - auto do_stream_chunk = [this, num_str](auto& session, const auto& writeHandler) - { - for (int i = 0; i < num_chunks_to_send_; i++) - { - auto json = fmt::format(R"("id": {}, "url": "/stream/{}", "args": , "headers": {})", - i, num_str, "\n"); - if (!writeHandler(json)) { break; } + auto do_stream_chunk = [this, num_str](auto& session, + const auto& writeHandler) { + for (int i = 0; i < num_chunks_to_send_; i++) { + auto json = fmt::format( + R"("id": {}, "url": "/stream/{}", "args": , "headers": {})", i, + num_str, "\n"); + if (!writeHandler(json)) { + break; + } } }; ctx.chunked(do_stream_chunk); } }; -#endif //HELLOSTREAMCONTROLLER_HPP +#endif // HELLOSTREAMCONTROLLER_HPP diff --git a/example/HelloWsController.hpp b/example/HelloWsController.hpp index ee4d262..e641d56 100644 --- a/example/HelloWsController.hpp +++ b/example/HelloWsController.hpp @@ -5,23 +5,28 @@ #ifndef HELLOWSCONTROLLER_HPP #define HELLOWSCONTROLLER_HPP -#include "controller/http_controller.hpp" #include -class HelloWsController : public khttpd::framework::BaseController -{ -public: - static std::shared_ptr create() { return std::make_shared(); } +#include "controller/http_controller.hpp" + +class HelloWsController + : public khttpd::framework::BaseController { + public: + static std::shared_ptr create() { + return std::make_shared(); + } - std::shared_ptr register_routes(khttpd::framework::HttpRouter& router) override - { - KHTTPD_DOCUMENTED_ROUTE(get, "/hellows", handle_hello, - {"WebSocket echo upgrade", "Explains how to connect to the echo WebSocket endpoint."}); + std::shared_ptr register_routes( + khttpd::framework::HttpRouter& router) override { + KHTTPD_DOCUMENTED_ROUTE( + get, "/hellows", handle_hello, + {"WebSocket echo upgrade", + "Explains how to connect to the echo WebSocket endpoint."}); return shared_from_this(); } - std::shared_ptr register_routes(khttpd::framework::WebsocketRouter& router) override - { + std::shared_ptr register_routes( + khttpd::framework::WebsocketRouter& router) override { // KHTTPD_WSROUTE("/hellows", onopen, onmessage, onclose, onerror); router.add_handler("/hellows", bind_handler(&std::decay_t::onopen), @@ -31,37 +36,33 @@ class HelloWsController : public khttpd::framework::BaseControllerWebSocket Echo Endpoint

This is a WebSocket endpoint. Please use a WebSocket client to connect.

"); + "

WebSocket Echo Endpoint

This is a WebSocket endpoint. " + "Please use a WebSocket client to connect.

"); } - void onopen(khttpd::framework::WebsocketContext& ctx) - { + void onopen(khttpd::framework::WebsocketContext& ctx) { spdlog::info("[WS: {}] Connection opened.", ctx.path); ctx.send("Welcome to the echo service!"); } - void onclose(khttpd::framework::WebsocketContext& ctx) - { + void onclose(khttpd::framework::WebsocketContext& ctx) { spdlog::info("[WS: {}] Connection closed.", ctx.path); } - void onerror(khttpd::framework::WebsocketContext& ctx) - { + void onerror(khttpd::framework::WebsocketContext& ctx) { spdlog::error("[WS: {}] Error: {}", ctx.path, ctx.error_code.message()); } - void onmessage(khttpd::framework::WebsocketContext& ctx) - { + void onmessage(khttpd::framework::WebsocketContext& ctx) { spdlog::info("[WS: {}] Received: {}", ctx.path, ctx.message); ctx.send("Echo: " + ctx.message, ctx.is_text); } }; -#endif //HELLOWSCONTROLLER_HPP +#endif // HELLOWSCONTROLLER_HPP diff --git a/example/MODULE.bazel b/example/MODULE.bazel index 1250be8..a8ff517 100644 --- a/example/MODULE.bazel +++ b/example/MODULE.bazel @@ -8,7 +8,7 @@ bazel_dep(name = "boost", version = "1.90.0.bcr.1") bazel_dep(name = "boost.asio", version = "1.90.0.bcr.1") bazel_dep(name = "boost.mysql", version = "1.90.0.bcr.1") bazel_dep(name = "spdlog", version = "1.17.0") -bazel_dep(name = "khttpd", version = "0.5.0") +bazel_dep(name = "khttpd", version = "0.5.1") local_path_override( module_name = "khttpd", path = "..", diff --git a/example/SseDemo.hpp b/example/SseDemo.hpp index 4fbba29..af829be 100644 --- a/example/SseDemo.hpp +++ b/example/SseDemo.hpp @@ -1,45 +1,48 @@ #ifndef KHTTPD_EXAMPLE_SSE_DEMO_HPP #define KHTTPD_EXAMPLE_SSE_DEMO_HPP +#include + #include #include #include #include -#include - #include "io_context_pool.hpp" #include "router/http_router.hpp" #include "sse/sse_session.hpp" -class SseDemo -{ -public: - static void register_routes(khttpd::framework::HttpRouter& router) - { - router.get("/events-demo", [](khttpd::framework::HttpContext& ctx) - { - ctx.set_status(boost::beast::http::status::ok); - ctx.set_content_type("text/html; charset=utf-8"); - ctx.set_body(demo_page()); - }, {"SSE browser demo", "Displays connection state and events received from the example SSE stream."}); +class SseDemo { + public: + static void register_routes(khttpd::framework::HttpRouter& router) { + router.get("/events-demo", + [](khttpd::framework::HttpContext& ctx) { + ctx.set_status(boost::beast::http::status::ok); + ctx.set_content_type("text/html; charset=utf-8"); + ctx.set_body(demo_page()); + }, + {"SSE browser demo", + "Displays connection state and events received from the " + "example SSE stream."}); - router.sse("/events", [](khttpd::framework::HttpContext&, - std::shared_ptr session) - { - auto state = std::make_shared(khttpd::framework::IoContextPool::instance().get_io_context()); - session->on_close([timer = state->timer](boost::system::error_code) - { - timer->cancel(); - }); - session->send({"welcome", R"({"message":"Connected to the khttpd SSE demo"})", "1", 1000}); - publish_tick(state, session); - }); + router.sse( + "/events", + [](khttpd::framework::HttpContext&, + std::shared_ptr session) { + auto state = std::make_shared( + khttpd::framework::IoContextPool::instance().get_io_context()); + session->on_close([timer = state->timer](boost::system::error_code) { + timer->cancel(); + }); + session->send({"welcome", + R"({"message":"Connected to the khttpd SSE demo"})", + "1", 1000}); + publish_tick(state, session); + }); } -private: - static const char* demo_page() - { + private: + static const char* demo_page() { return R"HTML( @@ -166,27 +169,26 @@ class SseDemo )HTML"; } - struct State - { + struct State { explicit State(boost::asio::io_context& ioc) - : timer(std::make_shared(ioc)) {} + : timer(std::make_shared(ioc)) {} std::shared_ptr timer; unsigned int sequence = 1; }; - static void publish_tick(const std::shared_ptr& state, - const std::shared_ptr& session) - { + static void publish_tick( + const std::shared_ptr& state, + const std::shared_ptr& session) { if (!session->is_open()) return; - if (!session->send({"tick", fmt::format(R"({{"sequence":{}}})", state->sequence++), "", std::nullopt})) - { + if (!session->send({"tick", + fmt::format(R"({{"sequence":{}}})", state->sequence++), + "", std::nullopt})) { session->close(); return; } state->timer->expires_after(std::chrono::seconds(1)); - state->timer->async_wait([state, session](boost::system::error_code ec) - { + state->timer->async_wait([state, session](boost::system::error_code ec) { if (!ec) publish_tick(state, session); }); } diff --git a/example/TypedHelloController.hpp b/example/TypedHelloController.hpp index 7f7691d..03f70e2 100644 --- a/example/TypedHelloController.hpp +++ b/example/TypedHelloController.hpp @@ -1,26 +1,22 @@ #ifndef KHTTPD_EXAMPLE_TYPED_HELLO_CONTROLLER_HPP_ #define KHTTPD_EXAMPLE_TYPED_HELLO_CONTROLLER_HPP_ -#include "framework/controller/http_controller.hpp" -#include "framework/router/http_result.hpp" - #include - #include #include -struct CreateGreetingRequest -{ +#include "framework/controller/http_controller.hpp" +#include "framework/router/http_result.hpp" + +struct CreateGreetingRequest { std::string name; }; -struct GreetingResponse -{ +struct GreetingResponse { std::string message; }; -struct GreetingErrorResponse -{ +struct GreetingErrorResponse { std::string code; std::string message; }; @@ -29,68 +25,69 @@ BOOST_DESCRIBE_STRUCT(CreateGreetingRequest, (), (name)) BOOST_DESCRIBE_STRUCT(GreetingResponse, (), (message)) BOOST_DESCRIBE_STRUCT(GreetingErrorResponse, (), (code, message)) -KHTTPD_OPENAPI_FIELD_DOCUMENTATION(CreateGreetingRequest, - KHTTPD_OPENAPI_FIELD(name, "Name to include in the greeting.")) +KHTTPD_OPENAPI_FIELD_DOCUMENTATION( + CreateGreetingRequest, + KHTTPD_OPENAPI_FIELD(name, "Name to include in the greeting.")) -KHTTPD_OPENAPI_FIELD_DOCUMENTATION(GreetingResponse, - KHTTPD_OPENAPI_FIELD(message, "Greeting text returned to the caller.")) +KHTTPD_OPENAPI_FIELD_DOCUMENTATION( + GreetingResponse, + KHTTPD_OPENAPI_FIELD(message, "Greeting text returned to the caller.")) -KHTTPD_OPENAPI_FIELD_DOCUMENTATION(GreetingErrorResponse, - KHTTPD_OPENAPI_FIELD(code, "Stable error code.") - KHTTPD_OPENAPI_FIELD(message, "Error message for the caller.")) +KHTTPD_OPENAPI_FIELD_DOCUMENTATION( + GreetingErrorResponse, + KHTTPD_OPENAPI_FIELD(code, "Stable error code.") + KHTTPD_OPENAPI_FIELD(message, "Error message for the caller.")) -class GreetingValidationError : public std::runtime_error -{ -public: +class GreetingValidationError : public std::runtime_error { + public: using std::runtime_error::runtime_error; }; -class TypedHelloController final : public khttpd::framework::BaseController -{ -public: - static std::shared_ptr create() - { +class TypedHelloController final + : public khttpd::framework::BaseController { + public: + static std::shared_ptr create() { return std::make_shared(); } - std::shared_ptr register_routes(khttpd::framework::HttpRouter& router) override - { + std::shared_ptr register_routes( + khttpd::framework::HttpRouter& router) override { KHTTPD_DOCUMENTED_TYPED_ROUTE(post, "/greetings", create_greeting, {"Create a greeting", - "Validates a name and returns a created greeting with its Location header."}); + "Validates a name and returns a created " + "greeting with its Location header."}); router.put(base_path() + "/greetings/{id}", shared_from_this(), &TypedHelloController::update_greeting, - {"Update a greeting", "Binds a path value, JSON DTO, query value, and HttpContext."}, + {"Update a greeting", + "Binds a path value, JSON DTO, query value, and HttpContext."}, khttpd::framework::PathParam{"id"}, khttpd::framework::Body{}, khttpd::framework::QueryParam{"excited", false}); return shared_from_this(); } -protected: - std::string base_path() override - { - return "/typed"; - } + protected: + std::string base_path() override { return "/typed"; } -private: - khttpd::framework::HttpResult create_greeting(const CreateGreetingRequest& request) const - { - if (request.name.empty()) throw GreetingValidationError("name must not be empty"); + private: + khttpd::framework::HttpResult create_greeting( + const CreateGreetingRequest& request) const { + if (request.name.empty()) + throw GreetingValidationError("name must not be empty"); auto result = khttpd::framework::HttpResult::created( - {"Hello, " + request.name + "!"}); + {"Hello, " + request.name + "!"}); return result.header("Location", "/typed/greetings/latest") - .header("X-Example-Handler", "typed"); + .header("X-Example-Handler", "typed"); } - GreetingResponse update_greeting(int id, const CreateGreetingRequest& request, bool excited, - khttpd::framework::HttpContext& context) const - { + GreetingResponse update_greeting( + int id, const CreateGreetingRequest& request, bool excited, + khttpd::framework::HttpContext& context) const { const auto request_id = context.get_header("X-Request-Id").value_or("none"); return {"Greeting " + std::to_string(id) + ": Hello, " + request.name + (excited ? "!" : ".") + " request=" + request_id}; } }; -#endif // KHTTPD_EXAMPLE_TYPED_HELLO_CONTROLLER_HPP_ +#endif // KHTTPD_EXAMPLE_TYPED_HELLO_CONTROLLER_HPP_ diff --git a/example/main.cpp b/example/main.cpp index c3259ab..b4cbc21 100644 --- a/example/main.cpp +++ b/example/main.cpp @@ -1,88 +1,94 @@ // app/main.cpp +#include +#include + +#include #include #include -#include "framework/server.hpp" -#include "framework/context/http_context.hpp" -#include "framework/context/websocket_context.hpp" -#include "framework/router/openapi.hpp" -#include -#include #include #include #include -#include -#include +#include #include "HelloController.hpp" #include "HelloStreamController.hpp" #include "HelloWsController.hpp" #include "SseDemo.hpp" #include "TypedHelloController.hpp" +#include "framework/context/http_context.hpp" +#include "framework/context/websocket_context.hpp" +#include "framework/router/openapi.hpp" +#include "framework/server.hpp" namespace net = boost::asio; using tcp = boost::asio::ip::tcp; namespace beast = boost::beast; -namespace -{ - void register_application_routes(khttpd::framework::HttpRouter& http_router, - khttpd::framework::WebsocketRouter& ws_router) - { - http_router.map_exception([](const GreetingValidationError&) - { - return khttpd::framework::HttpResult( - beast::http::status::bad_request, - {"INVALID_GREETING", "The greeting name must not be empty"}); - }); +namespace { +void register_application_routes( + khttpd::framework::HttpRouter& http_router, + khttpd::framework::WebsocketRouter& ws_router) { + http_router.map_exception( + [](const GreetingValidationError&) { + return khttpd::framework::HttpResult( + beast::http::status::bad_request, + {"INVALID_GREETING", "The greeting name must not be empty"}); + }); http_router.map_exception( - [](const khttpd::framework::TypedRequestValidationError& error) - { - return khttpd::framework::HttpResult( - beast::http::status::bad_request, - {"INVALID_TYPED_REQUEST", error.what()}); - }); + [](const khttpd::framework::TypedRequestValidationError& error) { + return khttpd::framework::HttpResult( + beast::http::status::bad_request, + {"INVALID_TYPED_REQUEST", error.what()}); + }); http_router.map_exception( - [](const khttpd::framework::TypedParameterValidationError& error) - { - return khttpd::framework::HttpResult( - beast::http::status::bad_request, - {"INVALID_ROUTE_PARAMETER", error.what()}); - }); - HelloController::create()->register_routes(http_router)->register_routes(ws_router); - HelloStreamController::create()->register_routes(http_router)->register_routes(ws_router); - HelloWsController::create()->register_routes(http_router)->register_routes(ws_router); - TypedHelloController::create()->register_routes(http_router)->register_routes(ws_router); + [](const khttpd::framework::TypedParameterValidationError& error) { + return khttpd::framework::HttpResult( + beast::http::status::bad_request, + {"INVALID_ROUTE_PARAMETER", error.what()}); + }); + HelloController::create() + ->register_routes(http_router) + ->register_routes(ws_router); + HelloStreamController::create() + ->register_routes(http_router) + ->register_routes(ws_router); + HelloWsController::create() + ->register_routes(http_router) + ->register_routes(ws_router); + TypedHelloController::create() + ->register_routes(http_router) + ->register_routes(ws_router); SseDemo::register_routes(http_router); - http_router.get("/", [](khttpd::framework::HttpContext& ctx) - { - ctx.set_status(beast::http::status::ok); - ctx.set_content_type("text/html"); - ctx.set_body( - R"(

Hello from khttpd!

API documentation

Try /hello?name=World or /info

Dynamic paths: /users/123, /users/profile, /items/book/id/456, /files/a/b/c.txt

POST examples: /api/json, /api/form, /api/upload

Watch the interactive SSE demo, or inspect the raw /events stream with curl -N.

Or connect to WebSocket

Or connect to WebSocket Chat

)"); - }, {"Example service home", "Links to the sample HTTP, streaming, WebSocket, and API documentation endpoints."}); - - http_router.get("/hello", [](khttpd::framework::HttpContext& ctx) - { + http_router.get( + "/", + [](khttpd::framework::HttpContext& ctx) { + ctx.set_status(beast::http::status::ok); + ctx.set_content_type("text/html"); + ctx.set_body( + R"(

Hello from khttpd!

API documentation

Try /hello?name=World or /info

Dynamic paths: /users/123, /users/profile, /items/book/id/456, /files/a/b/c.txt

POST examples: /api/json, /api/form, /api/upload

Watch the interactive SSE demo, or inspect the raw /events stream with curl -N.

Or connect to WebSocket

Or connect to WebSocket Chat

)"); + }, + {"Example service home", + "Links to the sample HTTP, streaming, WebSocket, and API documentation " + "endpoints."}); + + http_router.get("/hello", [](khttpd::framework::HttpContext& ctx) { std::string name = ctx.get_query_param("name").value_or("Guest"); ctx.set_status(beast::http::status::ok); ctx.set_content_type("text/plain"); ctx.set_body(fmt::format("Hello, {}!", name)); }); - http_router.get("/info", [](khttpd::framework::HttpContext& ctx) - { + http_router.get("/info", [](khttpd::framework::HttpContext& ctx) { std::string info_body = "

Server Info

"; info_body += fmt::format("

Path: {}

", ctx.path()); - info_body += fmt::format("

Method: {}

", beast::http::to_string(ctx.method())); + info_body += + fmt::format("

Method: {}

", beast::http::to_string(ctx.method())); auto user_agent = ctx.get_header(beast::http::field::user_agent); - if (user_agent) - { + if (user_agent) { info_body += fmt::format("

User-Agent: {}

", *user_agent); - } - else - { + } else { info_body += "

User-Agent: Not provided

"; } @@ -91,301 +97,288 @@ namespace ctx.set_body(info_body); }); - http_router.post("/api/json", [](khttpd::framework::HttpContext& ctx) - { - if (auto json_value = ctx.get_json()) - { - std::string response_body = "Received JSON:\n" + boost::json::serialize(*json_value); + http_router.post("/api/json", [](khttpd::framework::HttpContext& ctx) { + if (auto json_value = ctx.get_json()) { + std::string response_body = + "Received JSON:\n" + boost::json::serialize(*json_value); ctx.set_status(beast::http::status::ok); ctx.set_content_type("application/json"); ctx.set_body(response_body); - } - else - { + } else { ctx.set_status(beast::http::status::bad_request); ctx.set_content_type("text/plain"); ctx.set_body("Invalid JSON or Content-Type not application/json"); } }); - http_router.post("/api/form", [](khttpd::framework::HttpContext& ctx) - { + http_router.post("/api/form", [](khttpd::framework::HttpContext& ctx) { auto name = ctx.get_form_param("name"); auto email = ctx.get_form_param("email"); std::string response_body; - if (name || email) - { + if (name || email) { response_body = fmt::format("Received form data:\nName: {}\nEmail: {}", name.value_or("N/A"), email.value_or("N/A")); ctx.set_status(beast::http::status::ok); ctx.set_content_type("text/plain"); ctx.set_body(response_body); - } - else - { + } else { ctx.set_status(beast::http::status::bad_request); ctx.set_content_type("text/plain"); - ctx.set_body("Invalid form data or Content-Type not application/x-www-form-urlencoded"); + ctx.set_body( + "Invalid form data or Content-Type not " + "application/x-www-form-urlencoded"); } }); - http_router.post("/api/upload", [](khttpd::framework::HttpContext& ctx) - { + http_router.post("/api/upload", [](khttpd::framework::HttpContext& ctx) { auto content_type = ctx.get_header(beast::http::field::content_type); - if (content_type && content_type->find("multipart/form-data") != std::string::npos) - { + if (content_type && + content_type->find("multipart/form-data") != std::string::npos) { std::string response_msg = "Multipart/form-data received!\n"; - if (auto description = ctx.get_multipart_field("description")) - { + if (auto description = ctx.get_multipart_field("description")) { response_msg += fmt::format("Description: {}\n", *description); - } - else - { + } else { response_msg += "Description field not found.\n"; } - if (const auto* files = ctx.get_uploaded_files("file")) - { - if (!files->empty()) - { - for (const auto& file : *files) - { - response_msg += fmt::format("Uploaded File: {} ({} bytes, type: {})\n", file.filename, file.data.length(), - file.content_type); - // spdlog::debug("File '{}' content: \n{}", file.filename, file.data); + if (const auto* files = ctx.get_uploaded_files("file")) { + if (!files->empty()) { + for (const auto& file : *files) { + response_msg += fmt::format( + "Uploaded File: {} ({} bytes, type: {})\n", file.filename, + file.data.length(), file.content_type); + // spdlog::debug("File '{}' content: \n{}", file.filename, + // file.data); } - } - else - { + } else { response_msg += "No files uploaded for field 'file'.\n"; } - } - else - { + } else { response_msg += "File field 'file' not found.\n"; } ctx.set_status(beast::http::status::ok); ctx.set_content_type("text/plain"); ctx.set_body(response_msg); - } - else - { + } else { ctx.set_status(beast::http::status::bad_request); ctx.set_content_type("text/plain"); - ctx.set_body("Content-Type must be multipart/form-data for file uploads."); + ctx.set_body( + "Content-Type must be multipart/form-data for file uploads."); } }); - http_router.get("/users/profile", [](khttpd::framework::HttpContext& ctx) - { + http_router.get("/users/profile", [](khttpd::framework::HttpContext& ctx) { ctx.set_status(beast::http::status::ok); ctx.set_content_type("text/plain"); ctx.set_body("This is the user's profile page."); }); - http_router.get("/users/:id", [](khttpd::framework::HttpContext& ctx) - { + http_router.get("/users/:id", [](khttpd::framework::HttpContext& ctx) { auto user_id = ctx.get_path_param("id").value_or("unknown"); ctx.set_status(beast::http::status::ok); ctx.set_content_type("text/plain"); ctx.set_body(fmt::format("Fetching user with ID: {}", user_id)); }); - http_router.get("/items/:category/id/:item_id", [](khttpd::framework::HttpContext& ctx) - { - auto category = ctx.get_path_param("category").value_or("N/A"); - auto item_id = ctx.get_path_param("item_id").value_or("N/A"); - ctx.set_status(beast::http::status::ok); - ctx.set_content_type("text/plain"); - ctx.set_body(fmt::format("Fetching item {} in category {}", item_id, category)); - }); - - http_router.get("/files/:filepath", [](khttpd::framework::HttpContext& ctx) - { + http_router.get( + "/items/:category/id/:item_id", [](khttpd::framework::HttpContext& ctx) { + auto category = ctx.get_path_param("category").value_or("N/A"); + auto item_id = ctx.get_path_param("item_id").value_or("N/A"); + ctx.set_status(beast::http::status::ok); + ctx.set_content_type("text/plain"); + ctx.set_body( + fmt::format("Fetching item {} in category {}", item_id, category)); + }); + + http_router.get("/files/:filepath", [](khttpd::framework::HttpContext& ctx) { auto filepath = ctx.get_path_param("filepath").value_or("N/A"); ctx.set_status(beast::http::status::ok); ctx.set_content_type("text/plain"); ctx.set_body(fmt::format("Accessing file: {}", filepath)); }); - http_router.get("/ws", [](khttpd::framework::HttpContext& ctx) - { + http_router.get("/ws", [](khttpd::framework::HttpContext& ctx) { ctx.set_status(beast::http::status::upgrade_required); ctx.set_header(beast::http::field::upgrade, "websocket"); ctx.set_content_type("text/html"); ctx.set_body( - "

WebSocket Echo Endpoint

This is a WebSocket endpoint. Please use a WebSocket client to connect.

"); + "

WebSocket Echo Endpoint

This is a WebSocket endpoint. " + "Please use a WebSocket client to connect.

"); }); - http_router.get("/chat", [](khttpd::framework::HttpContext& ctx) - { + http_router.get("/chat", [](khttpd::framework::HttpContext& ctx) { ctx.set_status(beast::http::status::upgrade_required); ctx.set_header(beast::http::field::upgrade, "websocket"); ctx.set_content_type("text/html"); ctx.set_body( - "

WebSocket Chat Endpoint

This is a WebSocket chat endpoint. Please use a WebSocket client to connect.

"); + "

WebSocket Chat Endpoint

This is a WebSocket chat endpoint. " + "Please use a WebSocket client to connect.

"); }); - http_router.document_route("/hello", beast::http::verb::get, - {"Greet a visitor", "Returns a text greeting for the optional name query parameter."}); - http_router.document_route("/info", beast::http::verb::get, - {"Inspect the request", "Shows the method, path, and User-Agent received by the server."}); - http_router.document_route("/api/json", beast::http::verb::post, - {"Echo JSON", "Accepts an application/json body and returns the serialized payload."}); - http_router.document_route("/api/form", beast::http::verb::post, - {"Submit a form", "Reads name and email fields from an URL-encoded form body."}); - http_router.document_route("/api/upload", beast::http::verb::post, - {"Upload multipart data", "Reads a multipart description and optional uploaded file."}); - http_router.document_route("/users/profile", beast::http::verb::get, - {"Read the current profile", "Returns the static profile example before the dynamic user route."}); - http_router.document_route("/users/:id", beast::http::verb::get, - {"Read a user", "Returns a user identifier captured from the path."}); - http_router.document_route("/items/:category/id/:item_id", beast::http::verb::get, - {"Read an item", "Shows multiple path parameters in one route."}); - http_router.document_route("/files/:filepath", beast::http::verb::get, - {"Read a file path", "Demonstrates a final greedy path parameter."}); - http_router.document_route("/ws", beast::http::verb::get, - {"WebSocket echo upgrade", "Returns upgrade guidance for the echo WebSocket endpoint."}); - http_router.document_route("/chat", beast::http::verb::get, - {"WebSocket chat upgrade", "Returns upgrade guidance for the chat WebSocket endpoint."}); + http_router.document_route( + "/hello", beast::http::verb::get, + {"Greet a visitor", + "Returns a text greeting for the optional name query parameter."}); + http_router.document_route( + "/info", beast::http::verb::get, + {"Inspect the request", + "Shows the method, path, and User-Agent received by the server."}); + http_router.document_route( + "/api/json", beast::http::verb::post, + {"Echo JSON", + "Accepts an application/json body and returns the serialized payload."}); + http_router.document_route( + "/api/form", beast::http::verb::post, + {"Submit a form", + "Reads name and email fields from an URL-encoded form body."}); + http_router.document_route( + "/api/upload", beast::http::verb::post, + {"Upload multipart data", + "Reads a multipart description and optional uploaded file."}); + http_router.document_route( + "/users/profile", beast::http::verb::get, + {"Read the current profile", + "Returns the static profile example before the dynamic user route."}); + http_router.document_route( + "/users/:id", beast::http::verb::get, + {"Read a user", "Returns a user identifier captured from the path."}); + http_router.document_route( + "/items/:category/id/:item_id", beast::http::verb::get, + {"Read an item", "Shows multiple path parameters in one route."}); + http_router.document_route( + "/files/:filepath", beast::http::verb::get, + {"Read a file path", "Demonstrates a final greedy path parameter."}); + http_router.document_route( + "/ws", beast::http::verb::get, + {"WebSocket echo upgrade", + "Returns upgrade guidance for the echo WebSocket endpoint."}); + http_router.document_route( + "/chat", beast::http::verb::get, + {"WebSocket chat upgrade", + "Returns upgrade guidance for the chat WebSocket endpoint."}); ws_router.add_handler( - "/ws", - // onopen - []([[maybe_unused]] khttpd::framework::WebsocketContext& ctx) - { - spdlog::info("[WS: {}] Connection opened.", ctx.path); - ctx.send("Welcome to the echo service!"); - }, - // onmessage - [](khttpd::framework::WebsocketContext& ctx) - { - spdlog::info("[WS: {}] Received: {}", ctx.path, ctx.message); - ctx.send("Echo: " + ctx.message, ctx.is_text); - }, - // onclose - []([[maybe_unused]] khttpd::framework::WebsocketContext& ctx) - { - spdlog::info("[WS: {}] Connection closed.", ctx.path); - }, - // onerror - [](khttpd::framework::WebsocketContext& ctx) - { - spdlog::error("[WS: {}] Error: {}", ctx.path, ctx.error_code.message()); - } - ); + "/ws", + // onopen + []([[maybe_unused]] khttpd::framework::WebsocketContext& ctx) { + spdlog::info("[WS: {}] Connection opened.", ctx.path); + ctx.send("Welcome to the echo service!"); + }, + // onmessage + [](khttpd::framework::WebsocketContext& ctx) { + spdlog::info("[WS: {}] Received: {}", ctx.path, ctx.message); + ctx.send("Echo: " + ctx.message, ctx.is_text); + }, + // onclose + []([[maybe_unused]] khttpd::framework::WebsocketContext& ctx) { + spdlog::info("[WS: {}] Connection closed.", ctx.path); + }, + // onerror + [](khttpd::framework::WebsocketContext& ctx) { + spdlog::error("[WS: {}] Error: {}", ctx.path, ctx.error_code.message()); + }); ws_router.add_handler( - "/chat", - // onopen - []([[maybe_unused]] khttpd::framework::WebsocketContext& ctx) - { - spdlog::info("[WS: {}] Chat connection opened. Say hello!", ctx.path); - ctx.send("Welcome to the chat!"); - }, - // onmessage - [](khttpd::framework::WebsocketContext& ctx) - { - spdlog::info("[WS: {}] Chat message: {}", ctx.path, ctx.message); - ctx.send("You said: " + ctx.message, ctx.is_text); - }, - // onclose - []([[maybe_unused]] khttpd::framework::WebsocketContext& ctx) - { - spdlog::info("[WS: {}] Chat connection closed.", ctx.path); - }, - // onerror - [](khttpd::framework::WebsocketContext& ctx) - { - spdlog::error("[WS: {}] Chat error: {}", ctx.path, ctx.error_code.message()); - } - ); - - } + "/chat", + // onopen + []([[maybe_unused]] khttpd::framework::WebsocketContext& ctx) { + spdlog::info("[WS: {}] Chat connection opened. Say hello!", ctx.path); + ctx.send("Welcome to the chat!"); + }, + // onmessage + [](khttpd::framework::WebsocketContext& ctx) { + spdlog::info("[WS: {}] Chat message: {}", ctx.path, ctx.message); + ctx.send("You said: " + ctx.message, ctx.is_text); + }, + // onclose + []([[maybe_unused]] khttpd::framework::WebsocketContext& ctx) { + spdlog::info("[WS: {}] Chat connection closed.", ctx.path); + }, + // onerror + [](khttpd::framework::WebsocketContext& ctx) { + spdlog::error("[WS: {}] Chat error: {}", ctx.path, + ctx.error_code.message()); + }); +} - std::optional export_path_from_arguments(const int argc, char* argv[]) - { - constexpr std::string_view prefix = "--export-openapi="; - for (int index = 1; index < argc; ++index) - { - const std::string_view argument(argv[index]); - if (argument == "--export-openapi") - { - if (index + 1 >= argc) throw std::invalid_argument("--export-openapi requires an output path"); - return std::string(argv[index + 1]); - } - if (argument.compare(0, prefix.size(), prefix) == 0) - { - const auto path = argument.substr(prefix.size()); - if (path.empty()) throw std::invalid_argument("--export-openapi requires an output path"); - return std::string(path); - } +std::optional export_path_from_arguments(const int argc, + char* argv[]) { + constexpr std::string_view prefix = "--export-openapi="; + for (int index = 1; index < argc; ++index) { + const std::string_view argument(argv[index]); + if (argument == "--export-openapi") { + if (index + 1 >= argc) + throw std::invalid_argument("--export-openapi requires an output path"); + return std::string(argv[index + 1]); + } + if (argument.compare(0, prefix.size(), prefix) == 0) { + const auto path = argument.substr(prefix.size()); + if (path.empty()) + throw std::invalid_argument("--export-openapi requires an output path"); + return std::string(path); } - return std::nullopt; } + return std::nullopt; +} - bool openapi_docs_enabled_from_arguments(const int argc, char* argv[]) - { - bool enabled = true; - for (int index = 1; index < argc; ++index) - { - const std::string_view argument(argv[index]); - if (argument == "--enable-openapi-docs") enabled = true; - if (argument == "--disable-openapi-docs") enabled = false; - } - return enabled; +bool openapi_docs_enabled_from_arguments(const int argc, char* argv[]) { + bool enabled = true; + for (int index = 1; index < argc; ++index) { + const std::string_view argument(argv[index]); + if (argument == "--enable-openapi-docs") enabled = true; + if (argument == "--disable-openapi-docs") enabled = false; } + return enabled; } +} // namespace -int main(int argc, char* argv[]) -{ - try - { +int main(int argc, char* argv[]) { + try { const auto export_path = export_path_from_arguments(argc, argv); - if (export_path) - { + if (export_path) { khttpd::framework::HttpRouter http_router; khttpd::framework::WebsocketRouter ws_router; register_application_routes(http_router, ws_router); khttpd::framework::export_openapi(http_router, *export_path, - {"khttpd example API", "1.0.0"}); + {"khttpd example API", "1.0.0"}); spdlog::info("OpenAPI document exported to {}", *export_path); return 0; } auto const address = net::ip::make_address("0.0.0.0"); auto const port = static_cast(8080); - auto const num_threads = std::max(1, static_cast(std::thread::hardware_concurrency())); - spdlog::info("Starting khttpd server with {} worker threads...", num_threads); + auto const num_threads = + std::max(1, static_cast(std::thread::hardware_concurrency())); + spdlog::info("Starting khttpd server with {} worker threads...", + num_threads); std::string web_root_path = "web_root"; boost::filesystem::create_directories(web_root_path); - std::ofstream("web_root/index.html") << - "

Hello from Static Index!

Visit hello_static.html

"; - std::ofstream("web_root/hello_static.html") << - R"(

This is a static HTML file.

Static Image)"; + std::ofstream("web_root/index.html") + << "

Hello from Static Index!

Visit hello_static.html

"; + std::ofstream("web_root/hello_static.html") + << R"(

This is a static HTML file.

Static Image)"; auto server = std::make_shared( - tcp::endpoint{address, port}, web_root_path, num_threads); + tcp::endpoint{address, port}, web_root_path, num_threads); auto& http_router = server->get_http_router(); auto& ws_router = server->get_websocket_router(); register_application_routes(http_router, ws_router); khttpd::framework::install_openapi_routes( - http_router, {"khttpd example API", "1.0.0"}, "/openapi.json", "/docs", - openapi_docs_enabled_from_arguments(argc, argv)); + http_router, {"khttpd example API", "1.0.0"}, "/openapi.json", "/docs", + openapi_docs_enabled_from_arguments(argc, argv)); server->run(); spdlog::info("Application exited."); return 0; - } - catch (const std::exception& error) - { + } catch (const std::exception& error) { spdlog::error("Application failed: {}", error.what()); return 1; } - } diff --git a/framework/client/api_macros.hpp b/framework/client/api_macros.hpp index f4dfbda..335fb5f 100644 --- a/framework/client/api_macros.hpp +++ b/framework/client/api_macros.hpp @@ -1,7 +1,8 @@ #ifndef KHTTPD_FRAMEWORK_CLIENT_API_MACROS_HPP #define KHTTPD_FRAMEWORK_CLIENT_API_MACROS_HPP -// Compiler warning suppression (must come before any includes that might trigger warnings) +// Compiler warning suppression (must come before any includes that might +// trigger warnings) #if defined(__clang__) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" @@ -13,26 +14,32 @@ #endif // API Client definition macro (with single host) -#define KHTTPD_API_CLIENT(Name, Host) \ - class Name : public khttpd::framework::client::HttpClient { \ - public: \ - Name() { set_base_url(Host); } \ - explicit Name(boost::asio::io_context& ioc) : HttpClient(ioc) { set_base_url(Host); } +#define KHTTPD_API_CLIENT(Name, Host) \ + class Name : public khttpd::framework::client::HttpClient { \ + public: \ + Name() { set_base_url(Host); } \ + explicit Name(boost::asio::io_context& ioc) : HttpClient(ioc) { \ + set_base_url(Host); \ + } // API Client definition macro (with multi-host pool) -#define KHTTPD_API_CLIENT_POOL(Name, ...) \ - class Name : public khttpd::framework::client::HttpClient { \ - public: \ - Name() { \ - static const std::vector pool_hosts = { __VA_ARGS__ }; \ - set_base_url_pool(pool_hosts); \ - } \ - explicit Name(boost::asio::io_context& ioc) : HttpClient(ioc) { \ - static const std::vector pool_hosts = { __VA_ARGS__ }; \ - set_base_url_pool(pool_hosts); \ - } +#define KHTTPD_API_CLIENT_POOL(Name, ...) \ + class Name : public khttpd::framework::client::HttpClient { \ + public: \ + Name() { \ + static const std::vector \ + pool_hosts = {__VA_ARGS__}; \ + set_base_url_pool(pool_hosts); \ + } \ + explicit Name(boost::asio::io_context& ioc) : HttpClient(ioc) { \ + static const std::vector \ + pool_hosts = {__VA_ARGS__}; \ + set_base_url_pool(pool_hosts); \ + } -#define KHTTPD_API_CLIENT_END() }; +#define KHTTPD_API_CLIENT_END() \ + } \ + ; // Host entry shorthand — includes trailing comma for use in initializer lists #define KHTTPD_HOST(Url, W) {Url, W}, @@ -43,4 +50,4 @@ #pragma GCC diagnostic pop #endif -#endif // KHTTPD_FRAMEWORK_CLIENT_API_MACROS_HPP +#endif // KHTTPD_FRAMEWORK_CLIENT_API_MACROS_HPP diff --git a/framework/client/host_pool.cpp b/framework/client/host_pool.cpp index 0aa6999..a271803 100644 --- a/framework/client/host_pool.cpp +++ b/framework/client/host_pool.cpp @@ -1,50 +1,38 @@ #include "host_pool.hpp" -#include + #include +#include -namespace khttpd::framework::client -{ - HostPool::HostPool(std::vector hosts) - : hosts_(std::move(hosts)) - , total_weight_(0) - , rng_(std::random_device{}()) - { - for (const auto& h : hosts_) - { - urls_.push_back(h.url); - total_weight_ += std::max(1, h.weight); - cumulative_weights_.push_back(total_weight_); - } +namespace khttpd::framework::client { +HostPool::HostPool(std::vector hosts) + : hosts_(std::move(hosts)), total_weight_(0), rng_(std::random_device{}()) { + for (const auto& h : hosts_) { + urls_.push_back(h.url); + total_weight_ += std::max(1, h.weight); + cumulative_weights_.push_back(total_weight_); } +} - const std::string& HostPool::pick() - { - if (cumulative_weights_.empty()) - { - static const std::string empty; - return empty; - } - if (cumulative_weights_.size() == 1) - { - return urls_[0]; - } - - std::uniform_int_distribution dist(1, total_weight_); - std::lock_guard lock{rng_mutex_}; - int r = dist(rng_); - - auto it = std::lower_bound(cumulative_weights_.begin(), cumulative_weights_.end(), r); - size_t idx = std::distance(cumulative_weights_.begin(), it); - return urls_[idx]; +const std::string& HostPool::pick() { + if (cumulative_weights_.empty()) { + static const std::string empty; + return empty; } - - const std::vector& HostPool::all_urls() const - { - return urls_; + if (cumulative_weights_.size() == 1) { + return urls_[0]; } - int HostPool::total_weight() const - { - return total_weight_; - } + std::uniform_int_distribution dist(1, total_weight_); + std::lock_guard lock{rng_mutex_}; + int r = dist(rng_); + + auto it = std::lower_bound(cumulative_weights_.begin(), + cumulative_weights_.end(), r); + size_t idx = std::distance(cumulative_weights_.begin(), it); + return urls_[idx]; } + +const std::vector& HostPool::all_urls() const { return urls_; } + +int HostPool::total_weight() const { return total_weight_; } +} // namespace khttpd::framework::client diff --git a/framework/client/host_pool.hpp b/framework/client/host_pool.hpp index 2139110..77799db 100644 --- a/framework/client/host_pool.hpp +++ b/framework/client/host_pool.hpp @@ -1,42 +1,39 @@ #ifndef KHTTPD_FRAMEWORK_CLIENT_HOST_POOL_HPP #define KHTTPD_FRAMEWORK_CLIENT_HOST_POOL_HPP +#include +#include #include #include -#include -#include -namespace khttpd::framework::client -{ - struct HostEntry - { - std::string url; - int weight; - }; - - // Manages multiple base URLs with weighted random selection. - class HostPool - { - public: - explicit HostPool(std::vector hosts); - - // Pick one host URL based on weight (weighted random). - const std::string& pick(); - - // All unique host URLs. - const std::vector& all_urls() const; - - // Total weight sum. - int total_weight() const; - - private: - std::vector hosts_; - std::vector urls_; - std::vector cumulative_weights_; - int total_weight_; - std::mt19937 rng_; - std::mutex rng_mutex_; - }; -} +namespace khttpd::framework::client { +struct HostEntry { + std::string url; + int weight; +}; + +// Manages multiple base URLs with weighted random selection. +class HostPool { + public: + explicit HostPool(std::vector hosts); + + // Pick one host URL based on weight (weighted random). + const std::string& pick(); + + // All unique host URLs. + const std::vector& all_urls() const; + + // Total weight sum. + int total_weight() const; + + private: + std::vector hosts_; + std::vector urls_; + std::vector cumulative_weights_; + int total_weight_; + std::mt19937 rng_; + std::mutex rng_mutex_; +}; +} // namespace khttpd::framework::client #endif diff --git a/framework/client/http_client.cpp b/framework/client/http_client.cpp index e8b5440..c69c9fb 100644 --- a/framework/client/http_client.cpp +++ b/framework/client/http_client.cpp @@ -1,538 +1,504 @@ #include "http_client.hpp" + +#include #include #include -#include #include #include + #include "io_context_pool.hpp" -namespace khttpd::framework::client -{ - std::string replace_all(std::string str, const std::string& from, const std::string& to) - { - if (from.empty()) return str; - size_t start_pos = 0; - while ((start_pos = str.find(from, start_pos)) != std::string::npos) - { - str.replace(start_pos, from.length(), to); - start_pos += to.length(); - } - return str; +namespace khttpd::framework::client { +std::string replace_all(std::string str, const std::string& from, + const std::string& to) { + if (from.empty()) return str; + size_t start_pos = 0; + while ((start_pos = str.find(from, start_pos)) != std::string::npos) { + str.replace(start_pos, from.length(), to); + start_pos += to.length(); } + return str; +} - // ========================================== - // Abstract Session to handle common logic - // ========================================== - class Session : public std::enable_shared_from_this - { - protected: - HttpClient::ResponseCallback callback_; - http::request req_; - std::optional> response_parser_; - beast::flat_buffer buffer_; - std::chrono::seconds timeout_; - std::atomic completed_{false}; - - public: - Session(HttpClient::ResponseCallback callback, std::chrono::seconds timeout) - : callback_(std::move(callback)), timeout_(timeout) - { +// ========================================== +// Abstract Session to handle common logic +// ========================================== +class Session : public std::enable_shared_from_this { + protected: + HttpClient::ResponseCallback callback_; + http::request req_; + std::optional> response_parser_; + beast::flat_buffer buffer_; + std::chrono::seconds timeout_; + std::atomic completed_{false}; + + public: + Session(HttpClient::ResponseCallback callback, std::chrono::seconds timeout) + : callback_(std::move(callback)), timeout_(timeout) {} + + virtual ~Session() = default; + virtual void run(const std::string& host, const std::string& port, + http::request req) = 0; + virtual void cancel() = 0; + + protected: + void complete(beast::error_code ec, http::response res) { + if (!completed_.exchange(true) && callback_) { + callback_(ec, std::move(res)); } + } - virtual ~Session() = default; - virtual void run(const std::string& host, const std::string& port, http::request req) = 0; - virtual void cancel() = 0; + void on_fail(beast::error_code ec, const char* what) { + // Log if needed: spdlog::error("{}: {}", what, ec.message()); + boost::ignore_unused(what); + complete(ec, {}); + } +}; + +// ========================================== +// Plain HTTP Session +// ========================================== +class HttpSession : public Session { + beast::tcp_stream stream_; + tcp::resolver resolver_; + + // Helper: Downcast shared_from_this to avoid template deduction errors + std::shared_ptr get_shared() { + return std::static_pointer_cast(shared_from_this()); + } - protected: - void complete(beast::error_code ec, http::response res) - { - if (!completed_.exchange(true) && callback_) - { - callback_(ec, std::move(res)); - } - } + public: + HttpSession(net::io_context& ioc, HttpClient::ResponseCallback cb, + std::chrono::seconds timeout) + : Session(std::move(cb), timeout), + stream_(net::make_strand(ioc)), + resolver_(stream_.get_executor()) {} + + void run(const std::string& host, const std::string& port, + http::request req) override { + req_ = std::move(req); + stream_.expires_after(timeout_); + resolver_.async_resolve( + host, port, + beast::bind_front_handler(&HttpSession::on_resolve, get_shared())); + } - void on_fail(beast::error_code ec, const char* what) - { - // Log if needed: spdlog::error("{}: {}", what, ec.message()); - boost::ignore_unused(what); - complete(ec, {}); - } - }; - - // ========================================== - // Plain HTTP Session - // ========================================== - class HttpSession : public Session - { - beast::tcp_stream stream_; - tcp::resolver resolver_; - - // Helper: Downcast shared_from_this to avoid template deduction errors - std::shared_ptr get_shared() - { - return std::static_pointer_cast(shared_from_this()); - } + void cancel() override { + net::post(stream_.get_executor(), [self = get_shared()]() { + beast::error_code ignored; + self->resolver_.cancel(); + self->stream_.cancel(); + self->stream_.socket().shutdown(tcp::socket::shutdown_both, ignored); + self->stream_.socket().close(ignored); + }); + } - public: - HttpSession(net::io_context& ioc, HttpClient::ResponseCallback cb, std::chrono::seconds timeout) - : Session(std::move(cb), timeout), stream_(net::make_strand(ioc)), resolver_(stream_.get_executor()) - { - } + void on_resolve(beast::error_code ec, tcp::resolver::results_type results) { + if (ec) return on_fail(ec, "resolve"); + stream_.expires_after(timeout_); + stream_.async_connect(results, beast::bind_front_handler( + &HttpSession::on_connect, get_shared())); + } - void run(const std::string& host, const std::string& port, http::request req) override - { - req_ = std::move(req); - stream_.expires_after(timeout_); - resolver_.async_resolve(host, port, - beast::bind_front_handler(&HttpSession::on_resolve, get_shared())); - } + void on_connect(beast::error_code ec, + tcp::resolver::results_type::endpoint_type) { + if (ec) return on_fail(ec, "connect"); + stream_.expires_after(timeout_); + http::async_write( + stream_, req_, + beast::bind_front_handler(&HttpSession::on_write, get_shared())); + } - void cancel() override - { - net::post(stream_.get_executor(), [self = get_shared()]() - { - beast::error_code ignored; - self->resolver_.cancel(); - self->stream_.cancel(); - self->stream_.socket().shutdown(tcp::socket::shutdown_both, ignored); - self->stream_.socket().close(ignored); - }); - } + void on_write(beast::error_code ec, std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + if (ec) return on_fail(ec, "write"); - void on_resolve(beast::error_code ec, tcp::resolver::results_type results) - { - if (ec) return on_fail(ec, "resolve"); - stream_.expires_after(timeout_); - stream_.async_connect(results, - beast::bind_front_handler(&HttpSession::on_connect, get_shared())); - } + read_response(); + } - void on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type) - { - if (ec) return on_fail(ec, "connect"); - stream_.expires_after(timeout_); - http::async_write(stream_, req_, - beast::bind_front_handler(&HttpSession::on_write, get_shared())); - } + void read_response() { + response_parser_.emplace(); + response_parser_->body_limit((std::numeric_limits::max)()); + response_parser_->skip(req_.method() == http::verb::head); + http::async_read( + stream_, buffer_, *response_parser_, + beast::bind_front_handler(&HttpSession::on_read, get_shared())); + } - void on_write(beast::error_code ec, std::size_t bytes_transferred) - { - boost::ignore_unused(bytes_transferred); - if (ec) return on_fail(ec, "write"); + void on_read(beast::error_code ec, std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + if (ec) return on_fail(ec, "read"); + + auto response = response_parser_->release(); + if (response.result_int() >= 100 && response.result_int() < 200 && + response.result() != http::status::switching_protocols) + return read_response(); + beast::error_code ignored; + stream_.socket().shutdown(tcp::socket::shutdown_both, ignored); + complete({}, std::move(response)); + } +}; - read_response(); - } +// ========================================== +// HTTPS Session +// ========================================== +class HttpsSession : public Session { + beast::ssl_stream stream_; + tcp::resolver resolver_; - void read_response() - { - response_parser_.emplace(); - response_parser_->body_limit((std::numeric_limits::max)()); - response_parser_->skip(req_.method() == http::verb::head); - http::async_read(stream_, buffer_, *response_parser_, - beast::bind_front_handler(&HttpSession::on_read, get_shared())); - } + std::shared_ptr get_shared() { + return std::static_pointer_cast(shared_from_this()); + } - void on_read(beast::error_code ec, std::size_t bytes_transferred) - { - boost::ignore_unused(bytes_transferred); - if (ec) return on_fail(ec, "read"); + public: + HttpsSession(net::io_context& ioc, ssl::context& ctx, + HttpClient::ResponseCallback cb, std::chrono::seconds timeout) + : Session(std::move(cb), timeout), + stream_(net::make_strand(ioc), ctx), + resolver_(stream_.get_executor()) {} + + void run(const std::string& host, const std::string& port, + http::request req) override { + req_ = std::move(req); + if (!SSL_set_tlsext_host_name(stream_.native_handle(), host.c_str())) { + beast::error_code ec{static_cast(::ERR_get_error()), + net::error::get_ssl_category()}; + return on_fail(ec, "ssl_setup"); + } + stream_.set_verify_callback(ssl::host_name_verification(host)); + + stream_.next_layer().expires_after(timeout_); + resolver_.async_resolve( + host, port, + beast::bind_front_handler(&HttpsSession::on_resolve, get_shared())); + } - auto response = response_parser_->release(); - if (response.result_int() >= 100 && response.result_int() < 200 && - response.result() != http::status::switching_protocols) - return read_response(); + void cancel() override { + net::post(stream_.get_executor(), [self = get_shared()]() { beast::error_code ignored; - stream_.socket().shutdown(tcp::socket::shutdown_both, ignored); - complete({}, std::move(response)); - } - }; - - // ========================================== - // HTTPS Session - // ========================================== - class HttpsSession : public Session - { - beast::ssl_stream stream_; - tcp::resolver resolver_; - - std::shared_ptr get_shared() - { - return std::static_pointer_cast(shared_from_this()); - } + self->resolver_.cancel(); + beast::get_lowest_layer(self->stream_).cancel(); + beast::get_lowest_layer(self->stream_) + .socket() + .shutdown(tcp::socket::shutdown_both, ignored); + beast::get_lowest_layer(self->stream_).socket().close(ignored); + }); + } - public: - HttpsSession(net::io_context& ioc, ssl::context& ctx, HttpClient::ResponseCallback cb, std::chrono::seconds timeout) - : Session(std::move(cb), timeout), stream_(net::make_strand(ioc), ctx), resolver_(stream_.get_executor()) - { - } + void on_resolve(beast::error_code ec, tcp::resolver::results_type results) { + if (ec) return on_fail(ec, "resolve"); + stream_.next_layer().expires_after(timeout_); + beast::get_lowest_layer(stream_).async_connect( + results, + beast::bind_front_handler(&HttpsSession::on_connect, get_shared())); + } - void run(const std::string& host, const std::string& port, http::request req) override - { - req_ = std::move(req); - if (!SSL_set_tlsext_host_name(stream_.native_handle(), host.c_str())) - { - beast::error_code ec{static_cast(::ERR_get_error()), net::error::get_ssl_category()}; - return on_fail(ec, "ssl_setup"); - } - stream_.set_verify_callback(ssl::host_name_verification(host)); + void on_connect(beast::error_code ec, + tcp::resolver::results_type::endpoint_type) { + if (ec) return on_fail(ec, "connect"); + stream_.next_layer().expires_after(timeout_); + stream_.async_handshake( + ssl::stream_base::client, + beast::bind_front_handler(&HttpsSession::on_handshake, get_shared())); + } - stream_.next_layer().expires_after(timeout_); - resolver_.async_resolve(host, port, - beast::bind_front_handler(&HttpsSession::on_resolve, get_shared())); - } + void on_handshake(beast::error_code ec) { + if (ec) return on_fail(ec, "handshake"); + stream_.next_layer().expires_after(timeout_); + http::async_write( + stream_, req_, + beast::bind_front_handler(&HttpsSession::on_write, get_shared())); + } - void cancel() override - { - net::post(stream_.get_executor(), [self = get_shared()]() - { - beast::error_code ignored; - self->resolver_.cancel(); - beast::get_lowest_layer(self->stream_).cancel(); - beast::get_lowest_layer(self->stream_).socket().shutdown(tcp::socket::shutdown_both, ignored); - beast::get_lowest_layer(self->stream_).socket().close(ignored); - }); - } + void on_write(beast::error_code ec, std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + if (ec) return on_fail(ec, "write"); + read_response(); + } - void on_resolve(beast::error_code ec, tcp::resolver::results_type results) - { - if (ec) return on_fail(ec, "resolve"); - stream_.next_layer().expires_after(timeout_); - beast::get_lowest_layer(stream_).async_connect(results, - beast::bind_front_handler( - &HttpsSession::on_connect, get_shared())); - } + void read_response() { + response_parser_.emplace(); + response_parser_->body_limit((std::numeric_limits::max)()); + response_parser_->skip(req_.method() == http::verb::head); + http::async_read( + stream_, buffer_, *response_parser_, + beast::bind_front_handler(&HttpsSession::on_read, get_shared())); + } - void on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type) - { - if (ec) return on_fail(ec, "connect"); - stream_.next_layer().expires_after(timeout_); - stream_.async_handshake(ssl::stream_base::client, - beast::bind_front_handler(&HttpsSession::on_handshake, get_shared())); + void on_read(beast::error_code ec, std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + if (ec) { + if ((ec == ssl::error::stream_truncated || ec == net::error::eof) && + response_parser_ && response_parser_->is_done()) { + complete({}, response_parser_->release()); + return; + } + return on_fail(ec, "read"); } - void on_handshake(beast::error_code ec) - { - if (ec) return on_fail(ec, "handshake"); - stream_.next_layer().expires_after(timeout_); - http::async_write(stream_, req_, - beast::bind_front_handler(&HttpsSession::on_write, get_shared())); - } + auto response = response_parser_->release(); + if (response.result_int() >= 100 && response.result_int() < 200 && + response.result() != http::status::switching_protocols) + return read_response(); + final_response_ = std::move(response); + stream_.async_shutdown( + beast::bind_front_handler(&HttpsSession::on_shutdown, get_shared())); + } - void on_write(beast::error_code ec, std::size_t bytes_transferred) - { - boost::ignore_unused(bytes_transferred); - if (ec) return on_fail(ec, "write"); - read_response(); - } + void on_shutdown(beast::error_code ec) { + if (ec == net::error::eof || ec == ssl::error::stream_truncated) ec = {}; + complete(ec, std::move(final_response_)); + } - void read_response() - { - response_parser_.emplace(); - response_parser_->body_limit((std::numeric_limits::max)()); - response_parser_->skip(req_.method() == http::verb::head); - http::async_read(stream_, buffer_, *response_parser_, - beast::bind_front_handler(&HttpsSession::on_read, get_shared())); - } + http::response final_response_; +}; - void on_read(beast::error_code ec, std::size_t bytes_transferred) - { - boost::ignore_unused(bytes_transferred); - if (ec) - { - if ((ec == ssl::error::stream_truncated || ec == net::error::eof) && - response_parser_ && response_parser_->is_done()) - { - complete({}, response_parser_->release()); - return; - } - return on_fail(ec, "read"); - } +// 1. 傻瓜式:全局 IO + 默认 SSL +HttpClient::HttpClient() + : ioc_(IoContextPool::instance().get_io_context()) // 从单例获取 +{ + // 同样的默认 SSL 初始化逻辑 + own_ssl_ctx_ = std::make_shared(ssl::context::tls_client); + own_ssl_ctx_->set_default_verify_paths(); + own_ssl_ctx_->set_verify_mode(ssl::verify_peer); + ssl_ctx_ptr_ = own_ssl_ctx_.get(); +} - auto response = response_parser_->release(); - if (response.result_int() >= 100 && response.result_int() < 200 && - response.result() != http::status::switching_protocols) - return read_response(); - final_response_ = std::move(response); - stream_.async_shutdown(beast::bind_front_handler(&HttpsSession::on_shutdown, get_shared())); - } +// 2. 全局 IO + 自定义 SSL +HttpClient::HttpClient(ssl::context& ssl_ctx) + : ioc_(IoContextPool::instance().get_io_context()), + ssl_ctx_ptr_(&ssl_ctx) {} + +// 3. 自定义 IO + 默认 SSL (原逻辑) +HttpClient::HttpClient(net::io_context& ioc) : ioc_(ioc) { + own_ssl_ctx_ = std::make_shared(ssl::context::tls_client); + own_ssl_ctx_->set_default_verify_paths(); + own_ssl_ctx_->set_verify_mode(ssl::verify_peer); + ssl_ctx_ptr_ = own_ssl_ctx_.get(); +} - void on_shutdown(beast::error_code ec) - { - if (ec == net::error::eof || ec == ssl::error::stream_truncated) - ec = {}; - complete(ec, std::move(final_response_)); +// 4. 全自定义 +HttpClient::HttpClient(net::io_context& ioc, ssl::context& ssl_ctx) + : ioc_(ioc), ssl_ctx_ptr_(&ssl_ctx) {} + +void HttpClient::set_base_url(const std::string& url) { + host_pool_.reset(); // Clear pool, revert to single host + auto result = boost::urls::parse_uri(url); + if (result.has_value()) { + base_url_ = result.value(); + } else { + // Fallback for missing scheme + if (url.find("http") != 0) { + auto res2 = boost::urls::parse_uri("http://" + url); + if (res2.has_value()) base_url_ = res2.value(); } - - http::response final_response_; - }; - - // 1. 傻瓜式:全局 IO + 默认 SSL - HttpClient::HttpClient() - : ioc_(IoContextPool::instance().get_io_context()) // 从单例获取 - { - // 同样的默认 SSL 初始化逻辑 - own_ssl_ctx_ = std::make_shared(ssl::context::tls_client); - own_ssl_ctx_->set_default_verify_paths(); - own_ssl_ctx_->set_verify_mode(ssl::verify_peer); - ssl_ctx_ptr_ = own_ssl_ctx_.get(); } +} - // 2. 全局 IO + 自定义 SSL - HttpClient::HttpClient(ssl::context& ssl_ctx) - : ioc_(IoContextPool::instance().get_io_context()) - , ssl_ctx_ptr_(&ssl_ctx) - { +void HttpClient::set_base_url_pool(const std::vector& hosts) { + if (hosts.empty()) { + host_pool_.reset(); + return; } - - // 3. 自定义 IO + 默认 SSL (原逻辑) - HttpClient::HttpClient(net::io_context& ioc) - : ioc_(ioc) - { - own_ssl_ctx_ = std::make_shared(ssl::context::tls_client); - own_ssl_ctx_->set_default_verify_paths(); - own_ssl_ctx_->set_verify_mode(ssl::verify_peer); - ssl_ctx_ptr_ = own_ssl_ctx_.get(); + if (hosts.size() == 1) { + // Single host: fall back to set_base_url for simplicity + set_base_url(hosts[0].url); + host_pool_.reset(); + return; } + host_pool_ = std::make_unique(hosts); + base_url_.reset(); // Clear single host URL +} - // 4. 全自定义 - HttpClient::HttpClient(net::io_context& ioc, ssl::context& ssl_ctx) - : ioc_(ioc) - , ssl_ctx_ptr_(&ssl_ctx) - { - } +void HttpClient::set_default_header(const std::string& key, + const std::string& value) { + default_headers_[key] = value; +} - void HttpClient::set_base_url(const std::string& url) - { - host_pool_.reset(); // Clear pool, revert to single host - auto result = boost::urls::parse_uri(url); - if (result.has_value()) - { - base_url_ = result.value(); - } - else - { - // Fallback for missing scheme - if (url.find("http") != 0) - { - auto res2 = boost::urls::parse_uri("http://" + url); - if (res2.has_value()) base_url_ = res2.value(); - } - } - } +void HttpClient::set_bearer_token(const std::string& token) { + set_default_header("Authorization", "Bearer " + token); +} + +void HttpClient::set_timeout(std::chrono::seconds seconds) { + timeout_ = seconds; +} - void HttpClient::set_base_url_pool(const std::vector& hosts) - { - if (hosts.empty()) - { - host_pool_.reset(); - return; +HttpClient::UrlParts HttpClient::parse_target( + const std::string& path_in, + const std::map& query) { + boost::urls::url u; + const auto query_at = path_in.find('?'); + const auto fragment_at = path_in.find('#'); + const auto path_end = std::min(query_at, fragment_at); + const auto path_only = path_in.substr(0, path_end); + + // Use host pool if available (multi-host), otherwise use single base_url_ + if (host_pool_) { + const std::string& host_url = host_pool_->pick(); + auto pool_res = boost::urls::parse_uri(host_url); + if (pool_res.has_value()) { + u = pool_res.value(); + } else { + auto fallback = boost::urls::parse_uri("http://" + host_url); + if (fallback.has_value()) u = fallback.value(); + } + if (!path_only.empty()) { + if (path_only.front() != '/') + u.set_path(u.path() + "/" + path_only); + else + u.set_path(path_only); } - if (hosts.size() == 1) - { - // Single host: fall back to set_base_url for simplicity - set_base_url(hosts[0].url); - host_pool_.reset(); - return; + } else if (base_url_.has_value()) { + u = base_url_.value(); + if (!path_only.empty()) { + if (path_only.front() != '/') + u.set_path(u.path() + "/" + path_only); + else + u.set_path(path_only); } - host_pool_ = std::make_unique(hosts); - base_url_.reset(); // Clear single host URL - } - - void HttpClient::set_default_header(const std::string& key, const std::string& value) - { - default_headers_[key] = value; } - void HttpClient::set_bearer_token(const std::string& token) - { - set_default_header("Authorization", "Bearer " + token); + auto parse_res = boost::urls::parse_uri(path_in); + if (parse_res.has_value()) { + u = parse_res.value(); + } else if (!path_in.empty()) { + // Backward-compatible support for callers that pass a relative target + // such as "/health?verbose=1" in the path argument. Keep the path and + // query components separate so the query marker is never percent-encoded + // into the request path. + if (auto relative = boost::urls::parse_relative_ref(path_in); + relative.has_value()) { + u.params().clear(); + for (const auto& parameter : relative->params()) + u.params().append({parameter.key, parameter.value}); + } } - void HttpClient::set_timeout(std::chrono::seconds seconds) - { - timeout_ = seconds; + for (const auto& [k, v] : query) { + u.params().append({k, v}); } - HttpClient::UrlParts HttpClient::parse_target(const std::string& path_in, - const std::map& query) - { - boost::urls::url u; - - // Use host pool if available (multi-host), otherwise use single base_url_ - if (host_pool_) - { - const std::string& host_url = host_pool_->pick(); - auto pool_res = boost::urls::parse_uri(host_url); - if (pool_res.has_value()) - { - u = pool_res.value(); - } - else - { - auto fallback = boost::urls::parse_uri("http://" + host_url); - if (fallback.has_value()) u = fallback.value(); - } - if (!path_in.empty()) - { - if (path_in.front() != '/') u.set_path(u.path() + "/" + path_in); - else u.set_path(path_in); - } - } - else if (base_url_.has_value()) - { - u = base_url_.value(); - if (!path_in.empty()) - { - if (path_in.front() != '/') u.set_path(u.path() + "/" + path_in); - else u.set_path(path_in); - } - } + UrlParts parts; + parts.scheme = u.scheme(); + parts.host = u.host(); + parts.port = u.port(); + parts.target = u.encoded_target(); - auto parse_res = boost::urls::parse_uri(path_in); - if (parse_res.has_value()) - { - u = parse_res.value(); - } + if (parts.scheme.empty()) parts.scheme = "http"; + if (parts.target.empty()) parts.target = "/"; + if (parts.port.empty()) parts.port = (parts.scheme == "https") ? "443" : "80"; - for (const auto& [k, v] : query) - { - u.params().append({k, v}); - } + return parts; +} - UrlParts parts; - parts.scheme = u.scheme(); - parts.host = u.host(); - parts.port = u.port(); - parts.target = u.encoded_target(); +void HttpClient::request(http::verb method, std::string path, + const std::map& query_params, + const std::string& body, + const std::map& headers, + ResponseCallback callback) { + try { + auto parts = parse_target(path, query_params); - if (parts.scheme.empty()) parts.scheme = "http"; - if (parts.target.empty()) parts.target = "/"; - if (parts.port.empty()) parts.port = (parts.scheme == "https") ? "443" : "80"; + http::request req{method, parts.target, 11}; + req.set(http::field::host, parts.host); + req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); - return parts; - } + for (const auto& h : default_headers_) req.set(h.first, h.second); + for (const auto& h : headers) req.set(h.first, h.second); - void HttpClient::request(http::verb method, - std::string path, - const std::map& query_params, - const std::string& body, - const std::map& headers, - ResponseCallback callback) - { - try - { - auto parts = parse_target(path, query_params); - - http::request req{method, parts.target, 11}; - req.set(http::field::host, parts.host); - req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); - - for (const auto& h : default_headers_) req.set(h.first, h.second); - for (const auto& h : headers) req.set(h.first, h.second); - - if (!body.empty()) - { - req.body() = body; - req.prepare_payload(); - } + if (!body.empty()) { + req.body() = body; + req.prepare_payload(); + } - std::shared_ptr session; - if (parts.scheme == "https") - { - if (!ssl_ctx_ptr_) - { - if (callback) callback(beast::error_code(beast::errc::operation_not_supported, beast::system_category()), {}); - return; - } - session = std::make_shared(ioc_, *ssl_ctx_ptr_, std::move(callback), timeout_); - } - else - { - session = std::make_shared(ioc_, std::move(callback), timeout_); + std::shared_ptr session; + if (parts.scheme == "https") { + if (!ssl_ctx_ptr_) { + if (callback) + callback(beast::error_code(beast::errc::operation_not_supported, + beast::system_category()), + {}); + return; } - session->run(parts.host, parts.port, std::move(req)); - } - catch (const std::exception& e) - { - if (callback) callback(beast::error_code(beast::errc::invalid_argument, beast::system_category()), {}); - } + session = std::make_shared(ioc_, *ssl_ctx_ptr_, + std::move(callback), timeout_); + } else { + session = + std::make_shared(ioc_, std::move(callback), timeout_); + } + session->run(parts.host, parts.port, std::move(req)); + } catch (const std::exception& e) { + if (callback) + callback(beast::error_code(beast::errc::invalid_argument, + beast::system_category()), + {}); } +} - http::response HttpClient::request_sync( - http::verb method, - std::string path, +http::response HttpClient::request_sync( + http::verb method, std::string path, const std::map& query_params, const std::string& body, - const std::map& headers) - { - auto p = std::make_shared>>>(); - auto completed = std::make_shared>(false); - auto f = p->get_future(); - std::shared_ptr session; + const std::map& headers) { + auto p = std::make_shared>>>(); + auto completed = std::make_shared>(false); + auto f = p->get_future(); + std::shared_ptr session; - try - { - auto parts = parse_target(path, query_params); + try { + auto parts = parse_target(path, query_params); - http::request req{method, parts.target, 11}; - req.set(http::field::host, parts.host); - req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + http::request req{method, parts.target, 11}; + req.set(http::field::host, parts.host); + req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); - for (const auto& h : default_headers_) req.set(h.first, h.second); - for (const auto& h : headers) req.set(h.first, h.second); + for (const auto& h : default_headers_) req.set(h.first, h.second); + for (const auto& h : headers) req.set(h.first, h.second); - if (!body.empty()) - { - req.body() = body; - req.prepare_payload(); - } + if (!body.empty()) { + req.body() = body; + req.prepare_payload(); + } - auto callback = [p, completed](beast::error_code ec, http::response res) - { - if (!completed->exchange(true)) - { - p->set_value({ec, std::move(res)}); - } - }; - - if (parts.scheme == "https") - { - if (!ssl_ctx_ptr_) - { - throw boost::system::system_error( - beast::error_code(beast::errc::operation_not_supported, beast::system_category())); - } - session = std::make_shared(ioc_, *ssl_ctx_ptr_, std::move(callback), timeout_); + auto callback = [p, completed](beast::error_code ec, + http::response res) { + if (!completed->exchange(true)) { + p->set_value({ec, std::move(res)}); } - else - { - session = std::make_shared(ioc_, std::move(callback), timeout_); + }; + + if (parts.scheme == "https") { + if (!ssl_ctx_ptr_) { + throw boost::system::system_error(beast::error_code( + beast::errc::operation_not_supported, beast::system_category())); } - session->run(parts.host, parts.port, std::move(req)); - } - catch (const boost::system::system_error&) - { - throw; - } - catch (const std::exception&) - { - throw boost::system::system_error( - beast::error_code(beast::errc::invalid_argument, beast::system_category())); - } + session = std::make_shared(ioc_, *ssl_ctx_ptr_, + std::move(callback), timeout_); + } else { + session = + std::make_shared(ioc_, std::move(callback), timeout_); + } + session->run(parts.host, parts.port, std::move(req)); + } catch (const boost::system::system_error&) { + throw; + } catch (const std::exception&) { + throw boost::system::system_error(beast::error_code( + beast::errc::invalid_argument, beast::system_category())); + } - if (f.wait_for(timeout_ + std::chrono::seconds(1)) != std::future_status::ready) - { - completed->store(true); - if (session) session->cancel(); - throw boost::system::system_error(beast::error_code(beast::errc::timed_out, beast::system_category())); - } - auto result = f.get(); + if (f.wait_for(timeout_ + std::chrono::seconds(1)) != + std::future_status::ready) { + completed->store(true); + if (session) session->cancel(); + throw boost::system::system_error( + beast::error_code(beast::errc::timed_out, beast::system_category())); + } + auto result = f.get(); - if (result.first) - { - throw boost::system::system_error(result.first); - } - return result.second; + if (result.first) { + throw boost::system::system_error(result.first); } + return result.second; } +} // namespace khttpd::framework::client diff --git a/framework/client/http_client.hpp b/framework/client/http_client.hpp index 500b139..bf6ce29 100644 --- a/framework/client/http_client.hpp +++ b/framework/client/http_client.hpp @@ -1,151 +1,140 @@ #ifndef KHTTPD_FRAMEWORK_CLIENT_HTTP_CLIENT_HPP #define KHTTPD_FRAMEWORK_CLIENT_HTTP_CLIENT_HPP +#include +#include +#include #include #include -#include #include -#include -#include -#include -#include +#include #include - +#include #include +#include +#include #include +#include #include -#include -#include -#include #include -#include +#include #include "client/host_pool.hpp" -namespace khttpd::framework::client -{ - namespace beast = boost::beast; - namespace http = beast::http; - namespace net = boost::asio; - namespace ssl = boost::asio::ssl; - using tcp = boost::asio::ip::tcp; - - // Helper: String conversion - template - std::string to_string(const T& val) - { - if constexpr (std::is_convertible_v) - { - return std::string(val); - } - else if constexpr (std::is_arithmetic_v) - { - return std::to_string(val); - } - else - { - return std::to_string(val); - } +namespace khttpd::framework::client { +namespace beast = boost::beast; +namespace http = beast::http; +namespace net = boost::asio; +namespace ssl = boost::asio::ssl; +using tcp = boost::asio::ip::tcp; + +// Helper: String conversion +template +std::string to_string(const T& val) { + if constexpr (std::is_convertible_v) { + return std::string(val); + } else if constexpr (std::is_arithmetic_v) { + return std::to_string(val); + } else { + return std::to_string(val); } +} - inline std::string to_string(const std::string& val) { return val; } - - // Helper: Body serialization - template - std::string serialize_body(const T& value) - { - if constexpr (std::is_convertible_v) - { - return std::string(value); - } - else - { - // Assume it's serializable to JSON - return boost::json::serialize(boost::json::value_from(value)); - } - } +inline std::string to_string(const std::string& val) { return val; } - // Helper: Replace function - std::string replace_all(std::string str, const std::string& from, const std::string& to); - - // Verb conversion helper - inline http::verb verb_from_string(const std::string& s) - { - if (s == "GET" || s == "get") return http::verb::get; - if (s == "POST" || s == "post") return http::verb::post; - if (s == "PUT" || s == "put") return http::verb::put; - if (s == "DELETE" || s == "delete") return http::verb::delete_; - if (s == "PATCH" || s == "patch") return http::verb::patch; - if (s == "HEAD" || s == "head") return http::verb::head; - if (s == "OPTIONS" || s == "options") return http::verb::options; - return http::verb::get; +// Helper: Body serialization +template +std::string serialize_body(const T& value) { + if constexpr (std::is_convertible_v) { + return std::string(value); + } else { + // Assume it's serializable to JSON + return boost::json::serialize(boost::json::value_from(value)); } +} + +// Helper: Replace function +std::string replace_all(std::string str, const std::string& from, + const std::string& to); + +// Verb conversion helper +inline http::verb verb_from_string(const std::string& s) { + if (s == "GET" || s == "get") return http::verb::get; + if (s == "POST" || s == "post") return http::verb::post; + if (s == "PUT" || s == "put") return http::verb::put; + if (s == "DELETE" || s == "delete") return http::verb::delete_; + if (s == "PATCH" || s == "patch") return http::verb::patch; + if (s == "HEAD" || s == "head") return http::verb::head; + if (s == "OPTIONS" || s == "options") return http::verb::options; + return http::verb::get; +} - class HttpClient : public std::enable_shared_from_this - { - public: - using ResponseCallback = std::function)>; - - // 1. 【新增】傻瓜式构造函数:使用全局 IO 池,内部默认 SSL - HttpClient(); - - // 2. 【新增】使用全局 IO 池,但指定自定义 SSL - explicit HttpClient(ssl::context& ssl_ctx); - - // 3. 【保留】专家模式:指定外部 IO Context - explicit HttpClient(net::io_context& ioc); - HttpClient(net::io_context& ioc, ssl::context& ssl_ctx); - - virtual ~HttpClient() = default; - - // Configuration - void set_base_url(const std::string& url); - void set_base_url_pool(const std::vector& hosts); - void set_default_header(const std::string& key, const std::string& value); - void set_bearer_token(const std::string& token); - void set_timeout(std::chrono::seconds seconds); - - // Core Request Method (Used by Macros) - void request(http::verb method, - std::string path, // relative path or full url - const std::map& query_params, - const std::string& body, - const std::map& headers, - ResponseCallback callback); - - // Sync Request Method - http::response request_sync( - http::verb method, - std::string path, +class HttpClient : public std::enable_shared_from_this { + public: + using ResponseCallback = + std::function)>; + + // 1. 【新增】傻瓜式构造函数:使用全局 IO 池,内部默认 SSL + HttpClient(); + + // 2. 【新增】使用全局 IO 池,但指定自定义 SSL + explicit HttpClient(ssl::context& ssl_ctx); + + // 3. 【保留】专家模式:指定外部 IO Context + explicit HttpClient(net::io_context& ioc); + HttpClient(net::io_context& ioc, ssl::context& ssl_ctx); + + virtual ~HttpClient() = default; + + // Configuration + void set_base_url(const std::string& url); + void set_base_url_pool(const std::vector& hosts); + void set_default_header(const std::string& key, const std::string& value); + void set_bearer_token(const std::string& token); + void set_timeout(std::chrono::seconds seconds); + + // Core Request Method (Used by Macros) + void request(http::verb method, + std::string path, // relative path or full url + const std::map& query_params, + const std::string& body, + const std::map& headers, + ResponseCallback callback); + + // Sync Request Method + http::response request_sync( + http::verb method, std::string path, const std::map& query_params, const std::string& body, const std::map& headers); - private: - struct UrlParts - { - std::string scheme; - std::string host; - std::string port; - std::string target; - }; + private: + struct UrlParts { + std::string scheme; + std::string host; + std::string port; + std::string target; + }; - UrlParts parse_target(const std::string& path, const std::map& query); + UrlParts parse_target(const std::string& path, + const std::map& query); - net::io_context& ioc_; + net::io_context& ioc_; - // SSL Context Management - std::shared_ptr own_ssl_ctx_; // Holds ownership if created internally - ssl::context* ssl_ctx_ptr_; // Points to the active context + // SSL Context Management + std::shared_ptr + own_ssl_ctx_; // Holds ownership if created internally + ssl::context* ssl_ctx_ptr_; // Points to the active context - std::optional base_url_; - std::unique_ptr host_pool_; // Multi-host support (null if single host) - std::map default_headers_; - std::chrono::seconds timeout_{30}; - }; -} + std::optional base_url_; + std::unique_ptr + host_pool_; // Multi-host support (null if single host) + std::map default_headers_; + std::chrono::seconds timeout_{30}; +}; +} // namespace khttpd::framework::client // Include legacy macros (backward compatibility) #include "macros.hpp" -#endif // KHTTPD_FRAMEWORK_CLIENT_HTTP_CLIENT_HPP +#endif // KHTTPD_FRAMEWORK_CLIENT_HTTP_CLIENT_HPP diff --git a/framework/client/http_client_stream.cpp b/framework/client/http_client_stream.cpp index bd78db6..01454f1 100644 --- a/framework/client/http_client_stream.cpp +++ b/framework/client/http_client_stream.cpp @@ -5,295 +5,320 @@ #include #include #include -#include "io_context_pool.hpp" -namespace khttpd::framework::client -{ - namespace beast = boost::beast; - namespace http = beast::http; - namespace net = boost::asio; - namespace ssl = net::ssl; - using tcp = net::ip::tcp; +#include "io_context_pool.hpp" - namespace - { - std::shared_ptr default_ssl_context() - { - auto context = std::make_shared(ssl::context::tls_client); - context->set_default_verify_paths(); - context->set_verify_mode(ssl::verify_peer); - return context; - } +namespace khttpd::framework::client { +namespace beast = boost::beast; +namespace http = beast::http; +namespace net = boost::asio; +namespace ssl = net::ssl; +using tcp = net::ip::tcp; - std::shared_ptr borrowed_ssl_context(ssl::context& context) - { - return {&context, [](ssl::context*) {}}; - } - } +namespace { +std::shared_ptr default_ssl_context() { + auto context = std::make_shared(ssl::context::tls_client); + context->set_default_verify_paths(); + context->set_verify_mode(ssl::verify_peer); + return context; +} - struct HttpClientStream::Impl : std::enable_shared_from_this - { - using TlsStream = beast::ssl_stream; +std::shared_ptr borrowed_ssl_context(ssl::context& context) { + return {&context, [](ssl::context*) {}}; +} +} // namespace - net::any_io_executor executor; - tcp::resolver resolver; - std::shared_ptr ssl_context; - std::unique_ptr plain_stream; - std::unique_ptr tls_stream; - beast::flat_buffer read_buffer; - http::request request; - std::optional> request_serializer; - std::optional> response_parser; - http::verb request_method = http::verb::unknown; - std::string host; - std::string port; - bool use_tls = false; - bool started = false; - bool request_finished = false; +struct HttpClientStream::Impl + : std::enable_shared_from_this { + using TlsStream = beast::ssl_stream; - Impl(net::io_context& ioc, std::shared_ptr context) - : executor(net::make_strand(ioc)), resolver(executor), ssl_context(std::move(context)) - { - } + net::any_io_executor executor; + tcp::resolver resolver; + std::shared_ptr ssl_context; + std::unique_ptr plain_stream; + std::unique_ptr tls_stream; + beast::flat_buffer read_buffer; + http::request request; + std::optional> request_serializer; + std::optional> response_parser; + http::verb request_method = http::verb::unknown; + std::string host; + std::string port; + bool use_tls = false; + bool started = false; + bool request_finished = false; - void start(const std::string& url, RequestHead head, Callback callback) - { - net::post(executor, [self = shared_from_this(), url, head = std::move(head), - callback = std::move(callback)]() mutable - { self->start_on_executor(url, std::move(head), std::move(callback)); }); - } + Impl(net::io_context& ioc, std::shared_ptr context) + : executor(net::make_strand(ioc)), + resolver(executor), + ssl_context(std::move(context)) {} - void start_on_executor(const std::string& url, RequestHead head, Callback callback) - { - const auto parsed = boost::urls::parse_uri(url); - if (!parsed || (parsed->scheme() != "http" && parsed->scheme() != "https")) - return callback(make_error_code(boost::system::errc::operation_not_supported)); + void start(const std::string& url, RequestHead head, Callback callback) { + net::post(executor, [self = shared_from_this(), url, head = std::move(head), + callback = std::move(callback)]() mutable { + self->start_on_executor(url, std::move(head), std::move(callback)); + }); + } - use_tls = parsed->scheme() == "https"; - host = parsed->host(); - port = parsed->port().empty() ? (use_tls ? "443" : "80") : std::string(parsed->port()); - std::string target(parsed->encoded_target()); - if (target.empty()) target = "/"; - else if (target.front() == '?') target.insert(target.begin(), '/'); - request.method(head.method()); - request_method = head.method(); - request.target(target); - request.version(head.version()); - request.keep_alive(head.keep_alive()); - for (const auto& field : head) request.insert(field.name_string(), field.value()); - const bool default_port = (use_tls && port == "443") || (!use_tls && port == "80"); - request.set(http::field::host, default_port ? host : host + ":" + port); - request.body().more = true; - request_serializer.emplace(request); + void start_on_executor(const std::string& url, RequestHead head, + Callback callback) { + const auto parsed = boost::urls::parse_uri(url); + if (!parsed || (parsed->scheme() != "http" && parsed->scheme() != "https")) + return callback( + make_error_code(boost::system::errc::operation_not_supported)); - if (use_tls) - { - tls_stream = std::make_unique(executor, *ssl_context); - if (!SSL_set_tlsext_host_name(tls_stream->native_handle(), host.c_str())) - return callback(beast::error_code(static_cast(::ERR_get_error()), net::error::get_ssl_category())); - tls_stream->set_verify_callback(ssl::host_name_verification(host)); - } - else - { - plain_stream = std::make_unique(executor); - } + use_tls = parsed->scheme() == "https"; + host = parsed->host(); + port = parsed->port().empty() ? (use_tls ? "443" : "80") + : std::string(parsed->port()); + std::string target(parsed->encoded_target()); + if (target.empty()) + target = "/"; + else if (target.front() == '?') + target.insert(target.begin(), '/'); + request.method(head.method()); + request_method = head.method(); + request.target(target); + request.version(head.version()); + request.keep_alive(head.keep_alive()); + for (const auto& field : head) + request.insert(field.name_string(), field.value()); + const bool default_port = + (use_tls && port == "443") || (!use_tls && port == "80"); + request.set(http::field::host, default_port ? host : host + ":" + port); + request.body().more = true; + request_serializer.emplace(request); - resolver.async_resolve(host, port, [self = shared_from_this(), callback = std::move(callback)] - (beast::error_code ec, tcp::resolver::results_type results) mutable - { - if (ec) return callback(ec); - self->async_connect(std::move(results), std::move(callback)); - }); + if (use_tls) { + tls_stream = std::make_unique(executor, *ssl_context); + if (!SSL_set_tlsext_host_name(tls_stream->native_handle(), host.c_str())) + return callback(beast::error_code(static_cast(::ERR_get_error()), + net::error::get_ssl_category())); + tls_stream->set_verify_callback(ssl::host_name_verification(host)); + } else { + plain_stream = std::make_unique(executor); } - void async_connect(tcp::resolver::results_type results, Callback callback) - { - if (use_tls) - { - beast::get_lowest_layer(*tls_stream).async_connect(results, - [self = shared_from_this(), callback = std::move(callback)] - (beast::error_code ec, const tcp::endpoint&) mutable - { - if (ec) return callback(ec); - self->tls_stream->async_handshake(ssl::stream_base::client, - [self, callback = std::move(callback)](beast::error_code handshake_ec) mutable - { - if (handshake_ec) return callback(handshake_ec); - self->async_write_header(*self->tls_stream, std::move(callback)); + resolver.async_resolve( + host, port, + [self = shared_from_this(), callback = std::move(callback)]( + beast::error_code ec, tcp::resolver::results_type results) mutable { + if (ec) return callback(ec); + self->async_connect(std::move(results), std::move(callback)); + }); + } + + void async_connect(tcp::resolver::results_type results, Callback callback) { + if (use_tls) { + beast::get_lowest_layer(*tls_stream) + .async_connect( + results, + [self = shared_from_this(), callback = std::move(callback)]( + beast::error_code ec, const tcp::endpoint&) mutable { + if (ec) return callback(ec); + self->tls_stream->async_handshake( + ssl::stream_base::client, + [self, callback = std::move(callback)]( + beast::error_code handshake_ec) mutable { + if (handshake_ec) return callback(handshake_ec); + self->async_write_header(*self->tls_stream, + std::move(callback)); + }); }); - }); - } - else - { - plain_stream->async_connect(results, - [self = shared_from_this(), callback = std::move(callback)] - (beast::error_code ec, const tcp::endpoint&) mutable - { + } else { + plain_stream->async_connect( + results, [self = shared_from_this(), callback = std::move(callback)]( + beast::error_code ec, const tcp::endpoint&) mutable { if (ec) return callback(ec); self->async_write_header(*self->plain_stream, std::move(callback)); }); - } } + } - template - void async_write_header(Stream& stream, Callback callback) - { - http::async_write_header(stream, *request_serializer, - [self = shared_from_this(), callback = std::move(callback)] - (beast::error_code ec, std::size_t) mutable - { + template + void async_write_header(Stream& stream, Callback callback) { + http::async_write_header( + stream, *request_serializer, + [self = shared_from_this(), callback = std::move(callback)]( + beast::error_code ec, std::size_t) mutable { self->started = !ec; callback(ec); }); - } + } - void write(net::const_buffer source, Callback callback) - { - net::post(executor, [self = shared_from_this(), source, callback = std::move(callback)]() mutable - { self->write_on_executor(source, std::move(callback)); }); - } + void write(net::const_buffer source, Callback callback) { + net::post(executor, [self = shared_from_this(), source, + callback = std::move(callback)]() mutable { + self->write_on_executor(source, std::move(callback)); + }); + } - void write_on_executor(net::const_buffer source, Callback callback) - { - if (!started || request_finished) return callback(net::error::operation_aborted); - request.body().data = const_cast(source.data()); - request.body().size = source.size(); - request.body().more = true; - if (use_tls) async_write_body(*tls_stream, std::move(callback)); - else async_write_body(*plain_stream, std::move(callback)); - } + void write_on_executor(net::const_buffer source, Callback callback) { + if (!started || request_finished) + return callback(net::error::operation_aborted); + request.body().data = const_cast(source.data()); + request.body().size = source.size(); + request.body().more = true; + if (use_tls) + async_write_body(*tls_stream, std::move(callback)); + else + async_write_body(*plain_stream, std::move(callback)); + } - template - void async_write_body(Stream& stream, Callback callback) - { - http::async_write(stream, *request_serializer, - [callback = std::move(callback)](beast::error_code ec, std::size_t) mutable - { if (ec == http::error::need_buffer) ec = {}; callback(ec); }); - } + template + void async_write_body(Stream& stream, Callback callback) { + http::async_write(stream, *request_serializer, + [callback = std::move(callback)](beast::error_code ec, + std::size_t) mutable { + if (ec == http::error::need_buffer) ec = {}; + callback(ec); + }); + } - void finish(Callback callback) - { - net::post(executor, [self = shared_from_this(), callback = std::move(callback)]() mutable - { self->finish_on_executor(std::move(callback)); }); - } + void finish(Callback callback) { + net::post(executor, [self = shared_from_this(), + callback = std::move(callback)]() mutable { + self->finish_on_executor(std::move(callback)); + }); + } - void finish_on_executor(Callback callback) - { - if (!started || request_finished) return callback(net::error::operation_aborted); - request_finished = true; - if (request_serializer->is_done()) return callback({}); - request.body().data = nullptr; - request.body().size = 0; - request.body().more = false; - if (use_tls) async_write_body(*tls_stream, std::move(callback)); - else async_write_body(*plain_stream, std::move(callback)); - } + void finish_on_executor(Callback callback) { + if (!started || request_finished) + return callback(net::error::operation_aborted); + request_finished = true; + if (request_serializer->is_done()) return callback({}); + request.body().data = nullptr; + request.body().size = 0; + request.body().more = false; + if (use_tls) + async_write_body(*tls_stream, std::move(callback)); + else + async_write_body(*plain_stream, std::move(callback)); + } - void read_head(ResponseHeadCallback callback) - { - net::post(executor, [self = shared_from_this(), callback = std::move(callback)]() mutable - { self->read_head_on_executor(std::move(callback)); }); - } + void read_head(ResponseHeadCallback callback) { + net::post(executor, [self = shared_from_this(), + callback = std::move(callback)]() mutable { + self->read_head_on_executor(std::move(callback)); + }); + } - void read_head_on_executor(ResponseHeadCallback callback) - { - if (!started) return callback(net::error::operation_aborted, {}); - if (use_tls) async_read_head(*tls_stream, std::move(callback)); - else async_read_head(*plain_stream, std::move(callback)); - } + void read_head_on_executor(ResponseHeadCallback callback) { + if (!started) return callback(net::error::operation_aborted, {}); + if (use_tls) + async_read_head(*tls_stream, std::move(callback)); + else + async_read_head(*plain_stream, std::move(callback)); + } - template - void async_read_head(Stream& stream, ResponseHeadCallback callback) - { - response_parser.emplace(); - response_parser->body_limit((std::numeric_limits::max)()); - response_parser->skip(request_method == http::verb::head); - http::async_read_header(stream, read_buffer, *response_parser, - [self = shared_from_this(), callback = std::move(callback)](beast::error_code ec, std::size_t) mutable - { + template + void async_read_head(Stream& stream, ResponseHeadCallback callback) { + response_parser.emplace(); + response_parser->body_limit((std::numeric_limits::max)()); + response_parser->skip(request_method == http::verb::head); + http::async_read_header( + stream, read_buffer, *response_parser, + [self = shared_from_this(), callback = std::move(callback)]( + beast::error_code ec, std::size_t) mutable { ResponseHead head; - if (!ec) - { + if (!ec) { const auto& source = self->response_parser->get(); head.result(source.result()); head.version(source.version()); head.keep_alive(source.keep_alive()); - for (const auto& field : source) head.insert(field.name_string(), field.value()); + for (const auto& field : source) + head.insert(field.name_string(), field.value()); if (head.result_int() >= 100 && head.result_int() < 200 && - head.result() != http::status::switching_protocols) - { - if (self->use_tls) return self->async_read_head(*self->tls_stream, std::move(callback)); - return self->async_read_head(*self->plain_stream, std::move(callback)); + head.result() != http::status::switching_protocols) { + if (self->use_tls) + return self->async_read_head(*self->tls_stream, + std::move(callback)); + return self->async_read_head(*self->plain_stream, + std::move(callback)); } } callback(ec, std::move(head)); }); - } + } - void read(net::mutable_buffer target, ReadCallback callback) - { - net::post(executor, [self = shared_from_this(), target, callback = std::move(callback)]() mutable - { self->read_on_executor(target, std::move(callback)); }); - } + void read(net::mutable_buffer target, ReadCallback callback) { + net::post(executor, [self = shared_from_this(), target, + callback = std::move(callback)]() mutable { + self->read_on_executor(target, std::move(callback)); + }); + } - void read_on_executor(net::mutable_buffer target, ReadCallback callback) - { - if (!started) return callback(net::error::operation_aborted, 0, true); - if (!response_parser) return callback(net::error::operation_aborted, 0, true); - if (response_parser->is_done()) return callback({}, 0, true); - auto& body = response_parser->get().body(); - body.data = target.data(); - body.size = target.size(); - if (use_tls) async_read_body(*tls_stream, target.size(), std::move(callback)); - else async_read_body(*plain_stream, target.size(), std::move(callback)); - } + void read_on_executor(net::mutable_buffer target, ReadCallback callback) { + if (!started) return callback(net::error::operation_aborted, 0, true); + if (!response_parser) + return callback(net::error::operation_aborted, 0, true); + if (response_parser->is_done()) return callback({}, 0, true); + auto& body = response_parser->get().body(); + body.data = target.data(); + body.size = target.size(); + if (use_tls) + async_read_body(*tls_stream, target.size(), std::move(callback)); + else + async_read_body(*plain_stream, target.size(), std::move(callback)); + } - template - void async_read_body(Stream& stream, std::size_t capacity, ReadCallback callback) - { - http::async_read_some(stream, read_buffer, *response_parser, - [self = shared_from_this(), capacity, callback = std::move(callback)] - (beast::error_code ec, std::size_t) mutable - { + template + void async_read_body(Stream& stream, std::size_t capacity, + ReadCallback callback) { + http::async_read_some( + stream, read_buffer, *response_parser, + [self = shared_from_this(), capacity, callback = std::move(callback)]( + beast::error_code ec, std::size_t) mutable { if (ec == http::error::need_buffer) ec = {}; - const auto produced = capacity - self->response_parser->get().body().size; + const auto produced = + capacity - self->response_parser->get().body().size; callback(ec, produced, self->response_parser->is_done()); }); - } + } - void cancel() - { - beast::error_code ignored; - resolver.cancel(); - if (tls_stream) - { - beast::get_lowest_layer(*tls_stream).cancel(); - beast::get_lowest_layer(*tls_stream).socket().close(ignored); - } - if (plain_stream) - { - plain_stream->cancel(); - plain_stream->socket().close(ignored); - } + void cancel() { + beast::error_code ignored; + resolver.cancel(); + if (tls_stream) { + beast::get_lowest_layer(*tls_stream).cancel(); + beast::get_lowest_layer(*tls_stream).socket().close(ignored); + } + if (plain_stream) { + plain_stream->cancel(); + plain_stream->socket().close(ignored); } - }; + } +}; - HttpClientStream::HttpClientStream() - : impl_(std::make_shared(IoContextPool::instance().get_io_context(), default_ssl_context())) {} - HttpClientStream::HttpClientStream(ssl::context& context) - : impl_(std::make_shared(IoContextPool::instance().get_io_context(), borrowed_ssl_context(context))) {} - HttpClientStream::HttpClientStream(net::io_context& ioc) +HttpClientStream::HttpClientStream() + : impl_(std::make_shared(IoContextPool::instance().get_io_context(), + default_ssl_context())) {} +HttpClientStream::HttpClientStream(ssl::context& context) + : impl_(std::make_shared(IoContextPool::instance().get_io_context(), + borrowed_ssl_context(context))) {} +HttpClientStream::HttpClientStream(net::io_context& ioc) : impl_(std::make_shared(ioc, default_ssl_context())) {} - HttpClientStream::HttpClientStream(net::io_context& ioc, ssl::context& context) +HttpClientStream::HttpClientStream(net::io_context& ioc, ssl::context& context) : impl_(std::make_shared(ioc, borrowed_ssl_context(context))) {} - HttpClientStream::~HttpClientStream() { if (impl_) impl_->cancel(); } - void HttpClientStream::async_start(const std::string& url, RequestHead head, Callback cb) - { impl_->start(url, std::move(head), std::move(cb)); } - void HttpClientStream::async_write_some(net::const_buffer buffer, Callback cb) - { impl_->write(buffer, std::move(cb)); } - void HttpClientStream::async_finish_request(Callback cb) { impl_->finish(std::move(cb)); } - void HttpClientStream::async_read_response_head(ResponseHeadCallback cb) { impl_->read_head(std::move(cb)); } - void HttpClientStream::async_read_some(net::mutable_buffer buffer, ReadCallback cb) - { impl_->read(buffer, std::move(cb)); } - void HttpClientStream::cancel() { impl_->cancel(); } +HttpClientStream::~HttpClientStream() { + if (impl_) impl_->cancel(); +} +void HttpClientStream::async_start(const std::string& url, RequestHead head, + Callback cb) { + impl_->start(url, std::move(head), std::move(cb)); +} +void HttpClientStream::async_write_some(net::const_buffer buffer, Callback cb) { + impl_->write(buffer, std::move(cb)); +} +void HttpClientStream::async_finish_request(Callback cb) { + impl_->finish(std::move(cb)); +} +void HttpClientStream::async_read_response_head(ResponseHeadCallback cb) { + impl_->read_head(std::move(cb)); +} +void HttpClientStream::async_read_some(net::mutable_buffer buffer, + ReadCallback cb) { + impl_->read(buffer, std::move(cb)); } +void HttpClientStream::cancel() { impl_->cancel(); } +} // namespace khttpd::framework::client diff --git a/framework/client/http_client_stream.hpp b/framework/client/http_client_stream.hpp index 7a099a4..803d8d3 100644 --- a/framework/client/http_client_stream.hpp +++ b/framework/client/http_client_stream.hpp @@ -8,33 +8,37 @@ #include #include -namespace khttpd::framework::client -{ - class HttpClientStream : public std::enable_shared_from_this - { - public: - using RequestHead = boost::beast::http::request; - using ResponseHead = boost::beast::http::response; - using Callback = std::function; - using ResponseHeadCallback = std::function; - using ReadCallback = std::function; +namespace khttpd::framework::client { +class HttpClientStream : public std::enable_shared_from_this { + public: + using RequestHead = + boost::beast::http::request; + using ResponseHead = + boost::beast::http::response; + using Callback = std::function; + using ResponseHeadCallback = + std::function; + using ReadCallback = + std::function; - HttpClientStream(); - explicit HttpClientStream(boost::asio::ssl::context& ssl_context); - explicit HttpClientStream(boost::asio::io_context& ioc); - HttpClientStream(boost::asio::io_context& ioc, boost::asio::ssl::context& ssl_context); - ~HttpClientStream(); - void async_start(const std::string& url, RequestHead head, Callback callback); - void async_write_some(boost::asio::const_buffer buffer, Callback callback); - void async_finish_request(Callback callback); - void async_read_response_head(ResponseHeadCallback callback); - void async_read_some(boost::asio::mutable_buffer buffer, ReadCallback callback); - void cancel(); + HttpClientStream(); + explicit HttpClientStream(boost::asio::ssl::context& ssl_context); + explicit HttpClientStream(boost::asio::io_context& ioc); + HttpClientStream(boost::asio::io_context& ioc, + boost::asio::ssl::context& ssl_context); + ~HttpClientStream(); + void async_start(const std::string& url, RequestHead head, Callback callback); + void async_write_some(boost::asio::const_buffer buffer, Callback callback); + void async_finish_request(Callback callback); + void async_read_response_head(ResponseHeadCallback callback); + void async_read_some(boost::asio::mutable_buffer buffer, + ReadCallback callback); + void cancel(); - private: - struct Impl; - std::shared_ptr impl_; - }; -} + private: + struct Impl; + std::shared_ptr impl_; +}; +} // namespace khttpd::framework::client #endif diff --git a/framework/client/http_proxy_session.cpp b/framework/client/http_proxy_session.cpp index ff03e3f..a7b2c85 100644 --- a/framework/client/http_proxy_session.cpp +++ b/framework/client/http_proxy_session.cpp @@ -1,152 +1,186 @@ #include "http_proxy_session.hpp" + #include + #include "io_context_pool.hpp" -namespace khttpd::framework::client -{ - namespace net = boost::asio; - namespace beast = boost::beast; +namespace khttpd::framework::client { +namespace net = boost::asio; +namespace beast = boost::beast; - namespace - { - template - void filter_hop_by_hop(Message& message) - { - const bool chunked = message.chunked(); - if (const auto connection = message[beast::http::field::connection]; !connection.empty()) - { - std::string tokens(connection); - std::size_t begin = 0; - while (begin < tokens.size()) - { - const auto end = tokens.find(',', begin); - auto token = tokens.substr(begin, end == std::string::npos ? end : end - begin); - const auto first = token.find_first_not_of(" \t"); - if (first != std::string::npos) - { - token = token.substr(first, token.find_last_not_of(" \t") - first + 1); - message.erase(token); - } - if (end == std::string::npos) break; - begin = end + 1; - } +namespace { +template +void filter_hop_by_hop(Message& message) { + const bool chunked = message.chunked(); + if (const auto connection = message[beast::http::field::connection]; + !connection.empty()) { + std::string tokens(connection); + std::size_t begin = 0; + while (begin < tokens.size()) { + const auto end = tokens.find(',', begin); + auto token = + tokens.substr(begin, end == std::string::npos ? end : end - begin); + const auto first = token.find_first_not_of(" \t"); + if (first != std::string::npos) { + token = token.substr(first, token.find_last_not_of(" \t") - first + 1); + message.erase(token); } - message.erase(beast::http::field::connection); - message.erase(beast::http::field::keep_alive); - message.erase(beast::http::field::proxy_authenticate); - message.erase(beast::http::field::proxy_authorization); - message.erase(beast::http::field::te); - message.erase(beast::http::field::trailer); - message.erase(beast::http::field::transfer_encoding); - message.erase(beast::http::field::upgrade); - message.erase("Proxy-Connection"); - if (chunked) message.chunked(true); + if (end == std::string::npos) break; + begin = end + 1; } } + message.erase(beast::http::field::connection); + message.erase(beast::http::field::keep_alive); + message.erase(beast::http::field::proxy_authenticate); + message.erase(beast::http::field::proxy_authorization); + message.erase(beast::http::field::te); + message.erase(beast::http::field::trailer); + message.erase(beast::http::field::transfer_encoding); + message.erase(beast::http::field::upgrade); + message.erase("Proxy-Connection"); + if (chunked) message.chunked(true); +} +} // namespace - HttpProxySession::HttpProxySession(std::shared_ptr inbound, - std::shared_ptr downstream, std::size_t buffer_size) - : HttpProxySession(IoContextPool::instance().get_io_context(), std::move(inbound), - std::move(downstream), buffer_size) {} +HttpProxySession::HttpProxySession( + std::shared_ptr inbound, + std::shared_ptr downstream, std::size_t buffer_size) + : HttpProxySession(IoContextPool::instance().get_io_context(), + std::move(inbound), std::move(downstream), buffer_size) { +} - HttpProxySession::HttpProxySession(std::shared_ptr inbound, - std::shared_ptr downstream, - net::ssl::context& ssl_context, std::size_t buffer_size) - : HttpProxySession(IoContextPool::instance().get_io_context(), std::move(inbound), - std::move(downstream), ssl_context, buffer_size) {} +HttpProxySession::HttpProxySession( + std::shared_ptr inbound, + std::shared_ptr downstream, + net::ssl::context& ssl_context, std::size_t buffer_size) + : HttpProxySession(IoContextPool::instance().get_io_context(), + std::move(inbound), std::move(downstream), ssl_context, + buffer_size) {} - HttpProxySession::HttpProxySession(net::io_context& ioc, std::shared_ptr inbound, - std::shared_ptr downstream, std::size_t buffer_size) - : inbound_(std::move(inbound)), downstream_(std::move(downstream)), +HttpProxySession::HttpProxySession( + net::io_context& ioc, std::shared_ptr inbound, + std::shared_ptr downstream, std::size_t buffer_size) + : inbound_(std::move(inbound)), + downstream_(std::move(downstream)), upstream_(std::make_shared(ioc)), - request_buffer_(buffer_size), response_buffer_(buffer_size) {} + request_buffer_(buffer_size), + response_buffer_(buffer_size) {} - HttpProxySession::HttpProxySession(net::io_context& ioc, std::shared_ptr inbound, - std::shared_ptr downstream, - net::ssl::context& ssl_context, std::size_t buffer_size) - : inbound_(std::move(inbound)), downstream_(std::move(downstream)), +HttpProxySession::HttpProxySession( + net::io_context& ioc, std::shared_ptr inbound, + std::shared_ptr downstream, + net::ssl::context& ssl_context, std::size_t buffer_size) + : inbound_(std::move(inbound)), + downstream_(std::move(downstream)), upstream_(std::make_shared(ioc, ssl_context)), - request_buffer_(buffer_size), response_buffer_(buffer_size) {} + request_buffer_(buffer_size), + response_buffer_(buffer_size) {} - void HttpProxySession::start(const std::string& url, HttpClientStream::RequestHead head, CompleteCallback callback) - { - complete_ = std::move(callback); - head.erase(boost::beast::http::field::expect); - filter_hop_by_hop(head); - upstream_->async_start(url, std::move(head), [self = shared_from_this()](beast::error_code ec) - { if (ec) self->finish(ec); else self->pump_request(); }); - } +void HttpProxySession::start(const std::string& url, + HttpClientStream::RequestHead head, + CompleteCallback callback) { + complete_ = std::move(callback); + head.erase(boost::beast::http::field::expect); + filter_hop_by_hop(head); + upstream_->async_start(url, std::move(head), + [self = shared_from_this()](beast::error_code ec) { + if (ec) + self->finish(ec); + else + self->pump_request(); + }); +} - void HttpProxySession::pump_request() - { - inbound_->async_read_some(net::buffer(request_buffer_), [self = shared_from_this()] - (beast::error_code ec, std::size_t n, bool done) - { - if (ec) return self->finish(ec); - if (n) - { - self->upstream_->async_write_some(net::buffer(self->request_buffer_.data(), n), - [self, done](beast::error_code write_ec) - { if (write_ec) self->finish(write_ec); else if (done) self->finish_request(); else self->pump_request(); }); - } - else if (done) self->finish_request(); - else self->pump_request(); - }); - } +void HttpProxySession::pump_request() { + inbound_->async_read_some( + net::buffer(request_buffer_), + [self = shared_from_this()](beast::error_code ec, std::size_t n, + bool done) { + if (ec) return self->finish(ec); + if (n) { + self->upstream_->async_write_some( + net::buffer(self->request_buffer_.data(), n), + [self, done](beast::error_code write_ec) { + if (write_ec) + self->finish(write_ec); + else if (done) + self->finish_request(); + else + self->pump_request(); + }); + } else if (done) + self->finish_request(); + else + self->pump_request(); + }); +} - void HttpProxySession::finish_request() - { - upstream_->async_finish_request([self = shared_from_this()](beast::error_code ec) - { if (ec) self->finish(ec); else self->read_response_head(); }); - } +void HttpProxySession::finish_request() { + upstream_->async_finish_request( + [self = shared_from_this()](beast::error_code ec) { + if (ec) + self->finish(ec); + else + self->read_response_head(); + }); +} - void HttpProxySession::read_response_head() - { - upstream_->async_read_response_head([self = shared_from_this()] - (beast::error_code ec, HttpClientStream::ResponseHead head) - { - if (ec) return self->finish(ec); - filter_hop_by_hop(head); - self->downstream_->async_start(std::move(head), [self](beast::error_code start_ec) - { if (start_ec) self->finish(start_ec); else self->pump_response(); }); - }); - } +void HttpProxySession::read_response_head() { + upstream_->async_read_response_head( + [self = shared_from_this()](beast::error_code ec, + HttpClientStream::ResponseHead head) { + if (ec) return self->finish(ec); + filter_hop_by_hop(head); + self->downstream_->async_start(std::move(head), + [self](beast::error_code start_ec) { + if (start_ec) + self->finish(start_ec); + else + self->pump_response(); + }); + }); +} - void HttpProxySession::pump_response() - { - upstream_->async_read_some(net::buffer(response_buffer_), [self = shared_from_this()] - (beast::error_code ec, std::size_t n, bool done) - { - if (ec) return self->finish(ec); - if (n) - { - self->downstream_->async_write_some(net::buffer(self->response_buffer_.data(), n), - [self, done](beast::error_code write_ec) - { - if (write_ec) return self->finish(write_ec); - if (!done) return self->pump_response(); - self->downstream_->async_finish([self](beast::error_code finish_ec) { self->finish(finish_ec); }); - }); - } - else if (done) self->downstream_->async_finish([self](beast::error_code finish_ec) { self->finish(finish_ec); }); - else self->pump_response(); - }); - } +void HttpProxySession::pump_response() { + upstream_->async_read_some( + net::buffer(response_buffer_), + [self = shared_from_this()](beast::error_code ec, std::size_t n, + bool done) { + if (ec) return self->finish(ec); + if (n) { + self->downstream_->async_write_some( + net::buffer(self->response_buffer_.data(), n), + [self, done](beast::error_code write_ec) { + if (write_ec) return self->finish(write_ec); + if (!done) return self->pump_response(); + self->downstream_->async_finish( + [self](beast::error_code finish_ec) { + self->finish(finish_ec); + }); + }); + } else if (done) + self->downstream_->async_finish( + [self](beast::error_code finish_ec) { self->finish(finish_ec); }); + else + self->pump_response(); + }); +} - void HttpProxySession::cancel() - { - if (inbound_) inbound_->cancel(); - if (upstream_) upstream_->cancel(); - if (downstream_) downstream_->cancel(); - } +void HttpProxySession::cancel() { + if (inbound_) inbound_->cancel(); + if (upstream_) upstream_->cancel(); + if (downstream_) downstream_->cancel(); +} - void HttpProxySession::finish(beast::error_code ec) - { - if (completed_) return; - completed_ = true; - if (ec) { spdlog::error("HttpProxySession failed: {}", ec.message()); cancel(); } - inbound_.reset(); - downstream_.reset(); - if (complete_) complete_(ec); +void HttpProxySession::finish(beast::error_code ec) { + if (completed_) return; + completed_ = true; + if (ec) { + spdlog::error("HttpProxySession failed: {}", ec.message()); + cancel(); } + inbound_.reset(); + downstream_.reset(); + if (complete_) complete_(ec); } +} // namespace khttpd::framework::client diff --git a/framework/client/http_proxy_session.hpp b/framework/client/http_proxy_session.hpp index c2eddd5..01bd7b1 100644 --- a/framework/client/http_proxy_session.hpp +++ b/framework/client/http_proxy_session.hpp @@ -1,51 +1,51 @@ #ifndef KHTTPD_FRAMEWORK_CLIENT_HTTP_PROXY_SESSION_HPP #define KHTTPD_FRAMEWORK_CLIENT_HTTP_PROXY_SESSION_HPP -#include "http_client_stream.hpp" +#include + #include "context/http_request_stream.hpp" #include "context/http_response_stream.hpp" -#include +#include "http_client_stream.hpp" -namespace khttpd::framework::client -{ - class HttpProxySession : public std::enable_shared_from_this - { - public: - using CompleteCallback = std::function; - HttpProxySession(std::shared_ptr inbound, - std::shared_ptr downstream, - std::size_t buffer_size = 64 * 1024); - HttpProxySession(std::shared_ptr inbound, - std::shared_ptr downstream, - boost::asio::ssl::context& ssl_context, - std::size_t buffer_size = 64 * 1024); - HttpProxySession(boost::asio::io_context& ioc, - std::shared_ptr inbound, - std::shared_ptr downstream, - std::size_t buffer_size = 64 * 1024); - HttpProxySession(boost::asio::io_context& ioc, - std::shared_ptr inbound, - std::shared_ptr downstream, - boost::asio::ssl::context& ssl_context, - std::size_t buffer_size = 64 * 1024); - void start(const std::string& upstream_url, HttpClientStream::RequestHead head, - CompleteCallback callback = {}); - void cancel(); +namespace khttpd::framework::client { +class HttpProxySession : public std::enable_shared_from_this { + public: + using CompleteCallback = std::function; + HttpProxySession(std::shared_ptr inbound, + std::shared_ptr downstream, + std::size_t buffer_size = 64 * 1024); + HttpProxySession(std::shared_ptr inbound, + std::shared_ptr downstream, + boost::asio::ssl::context& ssl_context, + std::size_t buffer_size = 64 * 1024); + HttpProxySession(boost::asio::io_context& ioc, + std::shared_ptr inbound, + std::shared_ptr downstream, + std::size_t buffer_size = 64 * 1024); + HttpProxySession(boost::asio::io_context& ioc, + std::shared_ptr inbound, + std::shared_ptr downstream, + boost::asio::ssl::context& ssl_context, + std::size_t buffer_size = 64 * 1024); + void start(const std::string& upstream_url, + HttpClientStream::RequestHead head, + CompleteCallback callback = {}); + void cancel(); - private: - std::shared_ptr inbound_; - std::shared_ptr downstream_; - std::shared_ptr upstream_; - std::vector request_buffer_; - std::vector response_buffer_; - CompleteCallback complete_; - bool completed_ = false; - void pump_request(); - void finish_request(); - void read_response_head(); - void pump_response(); - void finish(boost::system::error_code ec); - }; -} + private: + std::shared_ptr inbound_; + std::shared_ptr downstream_; + std::shared_ptr upstream_; + std::vector request_buffer_; + std::vector response_buffer_; + CompleteCallback complete_; + bool completed_ = false; + void pump_request(); + void finish_request(); + void read_response_head(); + void pump_response(); + void finish(boost::system::error_code ec); +}; +} // namespace khttpd::framework::client #endif diff --git a/framework/client/macros.hpp b/framework/client/macros.hpp index 78b7438..81fcd20 100644 --- a/framework/client/macros.hpp +++ b/framework/client/macros.hpp @@ -1,11 +1,11 @@ #ifndef KHTTPD_FRAMEWORK_CLIENT_MACROS_HPP #define KHTTPD_FRAMEWORK_CLIENT_MACROS_HPP -#include -#include -#include #include #include +#include +#include +#include // ========================================================================= // Compiler Warning Suppression @@ -30,8 +30,8 @@ // Argument Tags // ========================================================================= #define QUERY(Type, Name, Key) (QUERY_TAG, Type, Name, Key) -#define PATH(Type, Name) (PATH_TAG, Type, Name) -#define BODY(Type, Name) (BODY_TAG, Type, Name) +#define PATH(Type, Name) (PATH_TAG, Type, Name) +#define BODY(Type, Name) (BODY_TAG, Type, Name) #define HEADER(Type, Name, Key) (HEADER_TAG, Type, Name, Key) // ========================================================================= @@ -40,7 +40,8 @@ // 之前的 POP_TAG 方式在 MSVC 上容易出错。 // 我们改为直接展开 Tuple: -// SIG_DISPATCH((TAG, Type, Name)) -> SIG_DISPATCH_I(TAG, Type, Name) -> SIG_TAG(Type, Name) +// SIG_DISPATCH((TAG, Type, Name)) -> SIG_DISPATCH_I(TAG, Type, Name) -> +// SIG_TAG(Type, Name) #define SIG_DISPATCH(Tuple) EXPAND(SIG_DISPATCH_I Tuple) #define SIG_DISPATCH_I(Tag, ...) EXPAND(SIG_##Tag(__VA_ARGS__)) @@ -58,74 +59,95 @@ #define SIG_HEADER_TAG(Type, Name, Key) Type Name // Process Logic -#define PROC_QUERY_TAG(Type, Name, Key) query_params.emplace(Key, khttpd::framework::client::to_string(Name)) -#define PROC_PATH_TAG(Type, Name) path_str = khttpd::framework::client::replace_all(path_str, ":" #Name, khttpd::framework::client::to_string(Name)) -#define PROC_BODY_TAG(Type, Name) body_str = khttpd::framework::client::serialize_body(Name) -#define PROC_HEADER_TAG(Type, Name, Key) header_map.emplace(Key, khttpd::framework::client::to_string(Name)) +#define PROC_QUERY_TAG(Type, Name, Key) \ + query_params.emplace(Key, khttpd::framework::client::to_string(Name)) +#define PROC_PATH_TAG(Type, Name) \ + path_str = khttpd::framework::client::replace_all( \ + path_str, ":" #Name, khttpd::framework::client::to_string(Name)) +#define PROC_BODY_TAG(Type, Name) \ + body_str = khttpd::framework::client::serialize_body(Name) +#define PROC_HEADER_TAG(Type, Name, Key) \ + header_map.emplace(Key, khttpd::framework::client::to_string(Name)) // ========================================================================= // API Function Body Generators // ========================================================================= -#define API_FUNC_BODY(METHOD, PATH_TEMPLATE, ...) \ - std::string path_str = PATH_TEMPLATE; \ - std::map query_params; \ - std::map header_map; \ - std::string body_str; \ - __VA_ARGS__ \ - this->request(METHOD, path_str, query_params, body_str, header_map, std::move(callback)); +#define API_FUNC_BODY(METHOD, PATH_TEMPLATE, ...) \ + std::string path_str = PATH_TEMPLATE; \ + std::map query_params; \ + std::map header_map; \ + std::string body_str; \ + __VA_ARGS__ \ + this->request(METHOD, path_str, query_params, body_str, header_map, \ + std::move(callback)); -#define API_FUNC_BODY_SYNC(METHOD, PATH_TEMPLATE, ...) \ - std::string path_str = PATH_TEMPLATE; \ - std::map query_params; \ - std::map header_map; \ - std::string body_str; \ - __VA_ARGS__ \ - return this->request_sync(METHOD, path_str, query_params, body_str, header_map); +#define API_FUNC_BODY_SYNC(METHOD, PATH_TEMPLATE, ...) \ + std::string path_str = PATH_TEMPLATE; \ + std::map query_params; \ + std::map header_map; \ + std::string body_str; \ + __VA_ARGS__ \ + return this->request_sync(METHOD, path_str, query_params, body_str, \ + header_map); // ========================================================================= // N-Argument Macro Implementations // ========================================================================= -#define API_CALL_0(METHOD, PT, NAME) \ - void NAME(khttpd::framework::client::HttpClient::ResponseCallback callback) { \ - API_FUNC_BODY(METHOD, PT, ) \ - } \ - boost::beast::http::response NAME##_sync() { \ - API_FUNC_BODY_SYNC(METHOD, PT, ) \ - } - -#define API_CALL_1(METHOD, PT, NAME, A) \ - void NAME(SIG_DISPATCH(A), khttpd::framework::client::HttpClient::ResponseCallback callback) { \ - API_FUNC_BODY(METHOD, PT, PROC_DISPATCH(A);) \ - } \ - auto NAME##_sync(SIG_DISPATCH(A)) { \ - API_FUNC_BODY_SYNC(METHOD, PT, PROC_DISPATCH(A);) \ - } - -#define API_CALL_2(METHOD, PT, NAME, A, B) \ - void NAME(SIG_DISPATCH(A), SIG_DISPATCH(B), khttpd::framework::client::HttpClient::ResponseCallback callback) { \ - API_FUNC_BODY(METHOD, PT, PROC_DISPATCH(A); PROC_DISPATCH(B);) \ - } \ - auto NAME##_sync(SIG_DISPATCH(A), SIG_DISPATCH(B)) { \ - API_FUNC_BODY_SYNC(METHOD, PT, PROC_DISPATCH(A); PROC_DISPATCH(B);) \ - } - -#define API_CALL_3(METHOD, PT, NAME, A, B, C) \ - void NAME(SIG_DISPATCH(A), SIG_DISPATCH(B), SIG_DISPATCH(C), khttpd::framework::client::HttpClient::ResponseCallback callback) { \ - API_FUNC_BODY(METHOD, PT, PROC_DISPATCH(A); PROC_DISPATCH(B); PROC_DISPATCH(C);) \ - } \ - auto NAME##_sync(SIG_DISPATCH(A), SIG_DISPATCH(B), SIG_DISPATCH(C)) { \ - API_FUNC_BODY_SYNC(METHOD, PT, PROC_DISPATCH(A); PROC_DISPATCH(B); PROC_DISPATCH(C);) \ - } - -#define API_CALL_4(METHOD, PT, NAME, A, B, C, D) \ - void NAME(SIG_DISPATCH(A), SIG_DISPATCH(B), SIG_DISPATCH(C), SIG_DISPATCH(D), khttpd::framework::client::HttpClient::ResponseCallback callback) { \ - API_FUNC_BODY(METHOD, PT, PROC_DISPATCH(A); PROC_DISPATCH(B); PROC_DISPATCH(C); PROC_DISPATCH(D);) \ - } \ - auto NAME##_sync(SIG_DISPATCH(A), SIG_DISPATCH(B), SIG_DISPATCH(C), SIG_DISPATCH(D)) { \ - API_FUNC_BODY_SYNC(METHOD, PT, PROC_DISPATCH(A); PROC_DISPATCH(B); PROC_DISPATCH(C); PROC_DISPATCH(D);) \ - } +#define API_CALL_0(METHOD, PT, NAME) \ + void NAME(khttpd::framework::client::HttpClient::ResponseCallback callback){ \ + API_FUNC_BODY( \ + METHOD, \ + PT, )} boost::beast::http::response \ + NAME##_sync() { \ + API_FUNC_BODY_SYNC(METHOD, PT, ) \ + } + +#define API_CALL_1(METHOD, PT, NAME, A) \ + void NAME( \ + SIG_DISPATCH(A), \ + khttpd::framework::client::HttpClient::ResponseCallback callback) { \ + API_FUNC_BODY(METHOD, PT, PROC_DISPATCH(A);) \ + } \ + auto NAME##_sync(SIG_DISPATCH(A)) { \ + API_FUNC_BODY_SYNC(METHOD, PT, PROC_DISPATCH(A);) \ + } + +#define API_CALL_2(METHOD, PT, NAME, A, B) \ + void NAME( \ + SIG_DISPATCH(A), SIG_DISPATCH(B), \ + khttpd::framework::client::HttpClient::ResponseCallback callback) { \ + API_FUNC_BODY(METHOD, PT, PROC_DISPATCH(A); PROC_DISPATCH(B);) \ + } \ + auto NAME##_sync(SIG_DISPATCH(A), SIG_DISPATCH(B)) { \ + API_FUNC_BODY_SYNC(METHOD, PT, PROC_DISPATCH(A); PROC_DISPATCH(B);) \ + } + +#define API_CALL_3(METHOD, PT, NAME, A, B, C) \ + void NAME( \ + SIG_DISPATCH(A), SIG_DISPATCH(B), SIG_DISPATCH(C), \ + khttpd::framework::client::HttpClient::ResponseCallback callback) { \ + API_FUNC_BODY(METHOD, PT, PROC_DISPATCH(A); PROC_DISPATCH(B); \ + PROC_DISPATCH(C);) \ + } \ + auto NAME##_sync(SIG_DISPATCH(A), SIG_DISPATCH(B), SIG_DISPATCH(C)) { \ + API_FUNC_BODY_SYNC(METHOD, PT, PROC_DISPATCH(A); PROC_DISPATCH(B); \ + PROC_DISPATCH(C);) \ + } + +#define API_CALL_4(METHOD, PT, NAME, A, B, C, D) \ + void NAME( \ + SIG_DISPATCH(A), SIG_DISPATCH(B), SIG_DISPATCH(C), SIG_DISPATCH(D), \ + khttpd::framework::client::HttpClient::ResponseCallback callback) { \ + API_FUNC_BODY(METHOD, PT, PROC_DISPATCH(A); PROC_DISPATCH(B); \ + PROC_DISPATCH(C); PROC_DISPATCH(D);) \ + } \ + auto NAME##_sync(SIG_DISPATCH(A), SIG_DISPATCH(B), SIG_DISPATCH(C), \ + SIG_DISPATCH(D)) { \ + API_FUNC_BODY_SYNC(METHOD, PT, PROC_DISPATCH(A); PROC_DISPATCH(B); \ + PROC_DISPATCH(C); PROC_DISPATCH(D);) \ + } // ========================================================================= // Dispatcher Logic (关键修复 3:修正宏选择计数) @@ -135,7 +157,9 @@ // 在这里使用 EXPAND 包裹整个 GET_MACRO 调用。 // 这解决了 "not enough arguments" 警告,并确保正确选择 API_CALL_x 宏。 -#define API_CALL(...) EXPAND(GET_MACRO(__VA_ARGS__, API_CALL_4, API_CALL_3, API_CALL_2, API_CALL_1, API_CALL_0, DUMMY)(__VA_ARGS__)) +#define API_CALL(...) \ + EXPAND(GET_MACRO(__VA_ARGS__, API_CALL_4, API_CALL_3, API_CALL_2, \ + API_CALL_1, API_CALL_0, DUMMY)(__VA_ARGS__)) #if defined(__clang__) #pragma clang diagnostic pop @@ -143,4 +167,4 @@ #pragma GCC diagnostic pop #endif -#endif // KHTTPD_FRAMEWORK_CLIENT_MACROS_HPP +#endif // KHTTPD_FRAMEWORK_CLIENT_MACROS_HPP diff --git a/framework/client/sse_client.cpp b/framework/client/sse_client.cpp index 6273931..a1ad287 100644 --- a/framework/client/sse_client.cpp +++ b/framework/client/sse_client.cpp @@ -1,230 +1,214 @@ #include "client/sse_client.hpp" +#include + +#include +#include #include #include #include -#include -#include #include #include #include #include #include -#include - #include "client/http_client_stream.hpp" #include "sse/sse_parser.hpp" -namespace khttpd::framework::client -{ - namespace http = boost::beast::http; - namespace net = boost::asio; - - namespace - { - bool is_event_stream_content_type(const boost::beast::string_view value) - { - std::string media_type(value.data(), value.size()); - const auto parameters = media_type.find(';'); - if (parameters != std::string::npos) media_type.erase(parameters); - while (!media_type.empty() && std::isspace(static_cast(media_type.back()))) - media_type.pop_back(); - auto first = media_type.begin(); - while (first != media_type.end() && std::isspace(static_cast(*first))) ++first; - media_type.erase(media_type.begin(), first); - std::transform(media_type.begin(), media_type.end(), media_type.begin(), - [](const unsigned char c) { return static_cast(std::tolower(c)); }); - return media_type == "text/event-stream"; - } - } - - struct SseClient::Impl : std::enable_shared_from_this - { - std::shared_ptr stream; - sse::SseParser parser; - std::array read_buffer{}; - EventHandler event_handler; - CloseHandler close_handler; - std::mutex mutex; - bool connect_started = false; - bool terminal = false; - - Impl(net::io_context& ioc, const std::size_t max_event_bytes) - : stream(std::make_shared(ioc)), parser(max_event_bytes) {} - Impl(net::io_context& ioc, net::ssl::context& context, const std::size_t max_event_bytes) - : stream(std::make_shared(ioc, context)), parser(max_event_bytes) {} - - void connect(const std::string& url, - const std::map& headers, - EventHandler on_event, - CloseHandler on_close) +namespace khttpd::framework::client { +namespace http = boost::beast::http; +namespace net = boost::asio; + +namespace { +bool is_event_stream_content_type(const boost::beast::string_view value) { + std::string media_type(value.data(), value.size()); + const auto parameters = media_type.find(';'); + if (parameters != std::string::npos) media_type.erase(parameters); + while (!media_type.empty() && + std::isspace(static_cast(media_type.back()))) + media_type.pop_back(); + auto first = media_type.begin(); + while (first != media_type.end() && + std::isspace(static_cast(*first))) + ++first; + media_type.erase(media_type.begin(), first); + std::transform( + media_type.begin(), media_type.end(), media_type.begin(), + [](const unsigned char c) { return static_cast(std::tolower(c)); }); + return media_type == "text/event-stream"; +} +} // namespace + +struct SseClient::Impl : std::enable_shared_from_this { + std::shared_ptr stream; + sse::SseParser parser; + std::array read_buffer{}; + EventHandler event_handler; + CloseHandler close_handler; + std::mutex mutex; + bool connect_started = false; + bool terminal = false; + + Impl(net::io_context& ioc, const std::size_t max_event_bytes) + : stream(std::make_shared(ioc)), + parser(max_event_bytes) {} + Impl(net::io_context& ioc, net::ssl::context& context, + const std::size_t max_event_bytes) + : stream(std::make_shared(ioc, context)), + parser(max_event_bytes) {} + + void connect(const std::string& url, + const std::map& headers, + EventHandler on_event, CloseHandler on_close) { + bool duplicate = false; { - bool duplicate = false; - { - std::lock_guard lock(mutex); - if (connect_started) - duplicate = true; - else - { - connect_started = true; - event_handler = std::move(on_event); - close_handler = std::move(on_close); - } - } - if (duplicate) - { - invoke_close_handler(on_close, make_error_code(boost::system::errc::operation_in_progress)); - return; + std::lock_guard lock(mutex); + if (connect_started) + duplicate = true; + else { + connect_started = true; + event_handler = std::move(on_event); + close_handler = std::move(on_close); } + } + if (duplicate) { + invoke_close_handler( + on_close, + make_error_code(boost::system::errc::operation_in_progress)); + return; + } - HttpClientStream::RequestHead request{http::verb::get, "/", 11}; - request.keep_alive(true); - for (const auto& [name, value] : headers) request.set(name, value); - request.set(http::field::accept, "text/event-stream"); - request.set(http::field::cache_control, "no-cache"); - stream->async_start(url, std::move(request), - [self = shared_from_this()](boost::system::error_code ec) - { + HttpClientStream::RequestHead request{http::verb::get, "/", 11}; + request.keep_alive(true); + for (const auto& [name, value] : headers) request.set(name, value); + request.set(http::field::accept, "text/event-stream"); + request.set(http::field::cache_control, "no-cache"); + stream->async_start( + url, std::move(request), + [self = shared_from_this()](boost::system::error_code ec) { if (ec) return self->finish(ec); - self->stream->async_finish_request([self](boost::system::error_code finish_ec) - { - if (finish_ec) return self->finish(finish_ec); - self->read_head(); - }); + self->stream->async_finish_request( + [self](boost::system::error_code finish_ec) { + if (finish_ec) return self->finish(finish_ec); + self->read_head(); + }); }); - } + } - void read_head() - { - stream->async_read_response_head( - [self = shared_from_this()](boost::system::error_code ec, HttpClientStream::ResponseHead head) - { + void read_head() { + stream->async_read_response_head( + [self = shared_from_this()](boost::system::error_code ec, + HttpClientStream::ResponseHead head) { if (ec) return self->finish(ec); if (head.result() != http::status::ok || !is_event_stream_content_type(head[http::field::content_type])) - return self->finish(make_error_code(boost::system::errc::protocol_error)); + return self->finish( + make_error_code(boost::system::errc::protocol_error)); self->read_next(); }); - } + } - void read_next() - { - stream->async_read_some(net::buffer(read_buffer), - [self = shared_from_this()](boost::system::error_code ec, const std::size_t size, const bool done) - { - if (size != 0) - { - try - { - const auto events = self->parser.feed(std::string_view(self->read_buffer.data(), size)); + void read_next() { + stream->async_read_some( + net::buffer(read_buffer), + [self = shared_from_this()](boost::system::error_code ec, + const std::size_t size, const bool done) { + if (size != 0) { + try { + const auto events = self->parser.feed( + std::string_view(self->read_buffer.data(), size)); for (const auto& event : events) self->emit(event); - } - catch (const std::length_error&) - { + } catch (const std::length_error&) { return self->finish(net::error::message_size); } } if (self->is_terminal()) return; if (done) return self->finish({}); - if (ec == net::error::eof || ec == http::error::end_of_stream) return self->finish({}); + if (ec == net::error::eof || ec == http::error::end_of_stream) + return self->finish({}); if (ec) return self->finish(ec); self->read_next(); }); - } - - void emit(const sse::SseEvent& event) - { - EventHandler handler; - { - std::lock_guard lock(mutex); - if (terminal) return; - handler = event_handler; - } - if (!handler) return; - try - { - handler(event); - } - catch (const std::exception& error) - { - spdlog::error("SSE event callback failed: {}", error.what()); - finish(net::error::operation_aborted); - } - catch (...) - { - spdlog::error("SSE event callback failed with a non-standard exception"); - finish(net::error::operation_aborted); - } - } + } - bool is_terminal() + void emit(const sse::SseEvent& event) { + EventHandler handler; { std::lock_guard lock(mutex); - return terminal; + if (terminal) return; + handler = event_handler; } - - void finish(const boost::system::error_code ec) - { - CloseHandler handler; - { - std::lock_guard lock(mutex); - if (terminal) return; - terminal = true; - handler = std::move(close_handler); - event_handler = {}; - } - if (ec) stream->cancel(); - invoke_close_handler(handler, ec); + if (!handler) return; + try { + handler(event); + } catch (const std::exception& error) { + spdlog::error("SSE event callback failed: {}", error.what()); + finish(net::error::operation_aborted); + } catch (...) { + spdlog::error("SSE event callback failed with a non-standard exception"); + finish(net::error::operation_aborted); } + } - void cancel() + bool is_terminal() { + std::lock_guard lock(mutex); + return terminal; + } + + void finish(const boost::system::error_code ec) { + CloseHandler handler; { - stream->cancel(); - finish(net::error::operation_aborted); + std::lock_guard lock(mutex); + if (terminal) return; + terminal = true; + handler = std::move(close_handler); + event_handler = {}; } + if (ec) stream->cancel(); + invoke_close_handler(handler, ec); + } + + void cancel() { + stream->cancel(); + finish(net::error::operation_aborted); + } - void abandon() + void abandon() { { - { - std::lock_guard lock(mutex); - terminal = true; - event_handler = {}; - close_handler = {}; - } - stream->cancel(); + std::lock_guard lock(mutex); + terminal = true; + event_handler = {}; + close_handler = {}; } + stream->cancel(); + } - static void invoke_close_handler(const CloseHandler& handler, const boost::system::error_code ec) - { - if (!handler) return; - try - { - handler(ec); - } - catch (const std::exception& error) - { - spdlog::error("SSE close callback failed: {}", error.what()); - } - catch (...) - { - spdlog::error("SSE close callback failed with a non-standard exception"); - } + static void invoke_close_handler(const CloseHandler& handler, + const boost::system::error_code ec) { + if (!handler) return; + try { + handler(ec); + } catch (const std::exception& error) { + spdlog::error("SSE close callback failed: {}", error.what()); + } catch (...) { + spdlog::error("SSE close callback failed with a non-standard exception"); } - }; + } +}; - SseClient::SseClient(net::io_context& ioc, const std::size_t max_event_bytes) +SseClient::SseClient(net::io_context& ioc, const std::size_t max_event_bytes) : impl_(std::make_shared(ioc, max_event_bytes)) {} - SseClient::SseClient(net::io_context& ioc, net::ssl::context& context, - const std::size_t max_event_bytes) +SseClient::SseClient(net::io_context& ioc, net::ssl::context& context, + const std::size_t max_event_bytes) : impl_(std::make_shared(ioc, context, max_event_bytes)) {} - SseClient::~SseClient() { if (impl_) impl_->abandon(); } - void SseClient::connect(const std::string& url, - const std::map& headers, - EventHandler on_event, - CloseHandler on_close) - { - impl_->connect(url, headers, std::move(on_event), std::move(on_close)); - } - void SseClient::cancel() { impl_->cancel(); } +SseClient::~SseClient() { + if (impl_) impl_->abandon(); +} +void SseClient::connect(const std::string& url, + const std::map& headers, + EventHandler on_event, CloseHandler on_close) { + impl_->connect(url, headers, std::move(on_event), std::move(on_close)); } +void SseClient::cancel() { impl_->cancel(); } +} // namespace khttpd::framework::client diff --git a/framework/client/sse_client.hpp b/framework/client/sse_client.hpp index d309179..2b3eaf0 100644 --- a/framework/client/sse_client.hpp +++ b/framework/client/sse_client.hpp @@ -12,33 +12,31 @@ #include "sse/sse_event.hpp" -namespace khttpd::framework::client -{ - class SseClient - { - public: - using EventHandler = std::function; - using CloseHandler = std::function; - - explicit SseClient(boost::asio::io_context& ioc, - std::size_t max_event_bytes = 1024 * 1024); - SseClient(boost::asio::io_context& ioc, boost::asio::ssl::context& ssl_context, - std::size_t max_event_bytes = 1024 * 1024); - ~SseClient(); - - SseClient(const SseClient&) = delete; - SseClient& operator=(const SseClient&) = delete; - - void connect(const std::string& url, - const std::map& headers, - EventHandler on_event, - CloseHandler on_close); - void cancel(); - - private: - struct Impl; - std::shared_ptr impl_; - }; -} +namespace khttpd::framework::client { +class SseClient { + public: + using EventHandler = std::function; + using CloseHandler = std::function; + + explicit SseClient(boost::asio::io_context& ioc, + std::size_t max_event_bytes = 1024 * 1024); + SseClient(boost::asio::io_context& ioc, + boost::asio::ssl::context& ssl_context, + std::size_t max_event_bytes = 1024 * 1024); + ~SseClient(); + + SseClient(const SseClient&) = delete; + SseClient& operator=(const SseClient&) = delete; + + void connect(const std::string& url, + const std::map& headers, + EventHandler on_event, CloseHandler on_close); + void cancel(); + + private: + struct Impl; + std::shared_ptr impl_; +}; +} // namespace khttpd::framework::client #endif diff --git a/framework/client/websocket_client.cpp b/framework/client/websocket_client.cpp index f47293b..a284507 100644 --- a/framework/client/websocket_client.cpp +++ b/framework/client/websocket_client.cpp @@ -1,640 +1,644 @@ #include "websocket_client.hpp" + +#include +#include #include #include -#include -#include #include "io_context_pool.hpp" -namespace khttpd::framework::client -{ - struct WebsocketClient::State - { - std::mutex mutex; - bool alive = true; - bool close_notified = false; - MessageHandler on_message; - FrameHandler on_frame; - ErrorHandler on_error; - CloseHandler on_close; - std::string negotiated_subprotocol; - }; - - // ========================================== - // Internal Session Abstraction - // ========================================== - struct WebsocketSessionImpl : public std::enable_shared_from_this - { - std::weak_ptr state_; - std::string host_; - beast::flat_buffer buffer_; - std::deque write_queue_; - bool is_writing_ = false; - bool closing_ = false; - bool close_started_ = false; - bool connect_notified_ = false; - - explicit WebsocketSessionImpl(std::shared_ptr state) : state_(std::move(state)) - { +namespace khttpd::framework::client { +struct WebsocketClient::State { + std::mutex mutex; + bool alive = true; + bool close_notified = false; + MessageHandler on_message; + FrameHandler on_frame; + ErrorHandler on_error; + CloseHandler on_close; + std::string negotiated_subprotocol; +}; + +// ========================================== +// Internal Session Abstraction +// ========================================== +struct WebsocketSessionImpl + : public std::enable_shared_from_this { + std::weak_ptr state_; + std::string host_; + beast::flat_buffer buffer_; + std::deque write_queue_; + bool is_writing_ = false; + bool closing_ = false; + bool close_started_ = false; + bool connect_notified_ = false; + + explicit WebsocketSessionImpl(std::shared_ptr state) + : state_(std::move(state)) {} + + virtual ~WebsocketSessionImpl() = default; + + virtual void run(const std::string& host, const std::string& port, + const std::string& target, + const std::map& headers, + WebsocketClient::ConnectCallback cb) = 0; + virtual void close() = 0; + + void set_negotiated_subprotocol(const beast::string_view value) { + if (auto state = state_.lock()) { + std::lock_guard lock{state->mutex}; + if (state->alive) + state->negotiated_subprotocol.assign(value.data(), value.size()); } + } - virtual ~WebsocketSessionImpl() = default; + // 核心发送逻辑:入队 + void queue_write(WebsocketFrame frame) { + net::post(get_executor(), + beast::bind_front_handler(&WebsocketSessionImpl::on_queue_write, + shared_from_this(), std::move(frame))); + } - virtual void run(const std::string& host, const std::string& port, const std::string& target, - const std::map& headers, WebsocketClient::ConnectCallback cb) = 0; - virtual void close() = 0; + protected: + virtual net::any_io_executor get_executor() = 0; + virtual void do_write_from_queue() = 0; - void set_negotiated_subprotocol(const beast::string_view value) + void notify_frame(const WebsocketFrame& frame) { + auto state = state_.lock(); + if (!state) return; + WebsocketClient::FrameHandler frame_handler; + WebsocketClient::MessageHandler message_handler; { - if (auto state = state_.lock()) - { - std::lock_guard lock{state->mutex}; - if (state->alive) state->negotiated_subprotocol.assign(value.data(), value.size()); - } + std::lock_guard lock{state->mutex}; + if (!state->alive) return; + frame_handler = state->on_frame; + message_handler = state->on_message; } + if (frame_handler) frame_handler(frame); + if (frame.type == WebsocketFrameType::text && message_handler) + message_handler(frame.payload); + } - // 核心发送逻辑:入队 - void queue_write(WebsocketFrame frame) + void notify_error(beast::error_code ec) { + auto state = state_.lock(); + if (!state) return; + WebsocketClient::ErrorHandler handler; { - net::post(get_executor(), beast::bind_front_handler( - &WebsocketSessionImpl::on_queue_write, shared_from_this(), std::move(frame))); + std::lock_guard lock{state->mutex}; + if (!state->alive) return; + handler = state->on_error; } + if (handler) handler(ec); + } - protected: - virtual net::any_io_executor get_executor() = 0; - virtual void do_write_from_queue() = 0; - - void notify_frame(const WebsocketFrame& frame) + void notify_close() { + auto state = state_.lock(); + if (!state) return; + WebsocketClient::CloseHandler handler; { - auto state = state_.lock(); - if (!state) return; - WebsocketClient::FrameHandler frame_handler; - WebsocketClient::MessageHandler message_handler; - { - std::lock_guard lock{state->mutex}; - if (!state->alive) return; - frame_handler = state->on_frame; - message_handler = state->on_message; - } - if (frame_handler) frame_handler(frame); - if (frame.type == WebsocketFrameType::text && message_handler) message_handler(frame.payload); + std::lock_guard lock{state->mutex}; + if (!state->alive || state->close_notified) return; + state->close_notified = true; + handler = state->on_close; } + if (handler) handler(); + } - void notify_error(beast::error_code ec) - { - auto state = state_.lock(); - if (!state) return; - WebsocketClient::ErrorHandler handler; - { - std::lock_guard lock{state->mutex}; - if (!state->alive) return; - handler = state->on_error; - } - if (handler) handler(ec); - } + void notify_connect(WebsocketClient::ConnectCallback& callback, + beast::error_code ec) { + if (connect_notified_ || closing_) return; + connect_notified_ = true; - void notify_close() + auto state = state_.lock(); + if (!state) return; { - auto state = state_.lock(); - if (!state) return; - WebsocketClient::CloseHandler handler; - { - std::lock_guard lock{state->mutex}; - if (!state->alive || state->close_notified) return; - state->close_notified = true; - handler = state->on_close; - } - if (handler) handler(); - } - - void notify_connect(WebsocketClient::ConnectCallback& callback, beast::error_code ec) - { - if (connect_notified_ || closing_) return; - connect_notified_ = true; - - auto state = state_.lock(); - if (!state) return; - { - std::lock_guard lock{state->mutex}; - if (!state->alive) return; - } - if (callback) callback(ec); + std::lock_guard lock{state->mutex}; + if (!state->alive) return; } + if (callback) callback(ec); + } - void on_queue_write(WebsocketFrame frame) - { - if (closing_) return; - write_queue_.push_back(std::move(frame)); - if (!is_writing_) - { - is_writing_ = true; - do_write_from_queue(); - } + void on_queue_write(WebsocketFrame frame) { + if (closing_) return; + write_queue_.push_back(std::move(frame)); + if (!is_writing_) { + is_writing_ = true; + do_write_from_queue(); } + } - // 通用的读循环处理 - void process_read_result(beast::error_code ec, std::size_t bytes, bool text, const websocket::close_reason& reason) - { - boost::ignore_unused(bytes); - if (ec) - { - // 修改:增加 operation_aborted 到关闭判定条件中 - // 当 async_read 被取消(例如正在关闭时),也应视为连接断开 - if (ec == websocket::error::closed || - ec == net::error::eof || + // 通用的读循环处理 + void process_read_result(beast::error_code ec, std::size_t bytes, bool text, + const websocket::close_reason& reason) { + boost::ignore_unused(bytes); + if (ec) { + // 修改:增加 operation_aborted 到关闭判定条件中 + // 当 async_read 被取消(例如正在关闭时),也应视为连接断开 + if (ec == websocket::error::closed || ec == net::error::eof || ec == ssl::error::stream_truncated || ec == boost::asio::error::connection_reset || ec == boost::asio::error::bad_descriptor || - ec == boost::asio::error::operation_aborted) - { - if (ec == websocket::error::closed) - notify_frame({WebsocketFrameType::close, {}, static_cast(reason.code), reason.reason.c_str()}); - notify_close(); - } - else - { - notify_error(ec); - } - return; + ec == boost::asio::error::operation_aborted) { + if (ec == websocket::error::closed) + notify_frame({WebsocketFrameType::close, + {}, + static_cast(reason.code), + reason.reason.c_str()}); + notify_close(); + } else { + notify_error(ec); } - - notify_frame({text ? WebsocketFrameType::text : WebsocketFrameType::binary, - beast::buffers_to_string(buffer_.data())}); - buffer_.consume(buffer_.size()); + return; } - // 通用的写完成处理 - void process_write_result(beast::error_code ec) - { - if (ec) - { - is_writing_ = false; // Stop writing on error - if (ec == websocket::error::closed || ec == net::error::operation_aborted || ec == net::error::bad_descriptor) - { - notify_close(); - } - else - { - notify_error(ec); - } - return; - } - - write_queue_.pop_front(); + notify_frame({text ? WebsocketFrameType::text : WebsocketFrameType::binary, + beast::buffers_to_string(buffer_.data())}); + buffer_.consume(buffer_.size()); + } - if (!write_queue_.empty()) - { - do_write_from_queue(); - } - else - { - is_writing_ = false; + // 通用的写完成处理 + void process_write_result(beast::error_code ec) { + if (ec) { + is_writing_ = false; // Stop writing on error + if (ec == websocket::error::closed || + ec == net::error::operation_aborted || + ec == net::error::bad_descriptor) { + notify_close(); + } else { + notify_error(ec); } - } - }; - - // ========================================== - // Plain TCP Session (ws://) - // ========================================== - class PlainWebsocketSession : public WebsocketSessionImpl - { - websocket::stream ws_; - websocket::response_type handshake_response_; - tcp::resolver resolver_; - WebsocketClient::ConnectCallback connect_cb_; - - public: - PlainWebsocketSession(net::io_context& ioc, std::shared_ptr state) - : WebsocketSessionImpl(std::move(state)), ws_(net::make_strand(ioc)), resolver_(ws_.get_executor()) - { - } - - net::any_io_executor get_executor() override { return ws_.get_executor(); } - - void run(const std::string& host, const std::string& port, const std::string& target, - const std::map& headers, WebsocketClient::ConnectCallback cb) override - { - host_ = host; - connect_cb_ = std::move(cb); - - resolver_.async_resolve(host, port, beast::bind_front_handler(&PlainWebsocketSession::on_resolve, - std::static_pointer_cast( - shared_from_this()), target, headers)); - } - - void close() override - { - net::post(ws_.get_executor(), [self = std::static_pointer_cast(shared_from_this())]() - { - self->closing_ = true; - self->write_queue_.clear(); - beast::error_code ignored; - self->resolver_.cancel(); - beast::get_lowest_layer(self->ws_).cancel(); - - if (self->ws_.is_open() && !self->close_started_) - { - self->close_started_ = true; - self->ws_.async_close(websocket::close_code::normal, [self](beast::error_code) - { - /* ignore close error */ - self->notify_close(); - }); - return; - } - - beast::get_lowest_layer(self->ws_).socket().shutdown(tcp::socket::shutdown_both, ignored); - beast::get_lowest_layer(self->ws_).socket().close(ignored); - }); - } - - protected: - void do_write_from_queue() override - { - const auto& frame = write_queue_.front(); - if (frame.type == WebsocketFrameType::ping) - return ws_.async_ping(websocket::ping_data(frame.payload), [self = std::static_pointer_cast(shared_from_this())](beast::error_code ec) { self->process_write_result(ec); }); - if (frame.type == WebsocketFrameType::pong) - return ws_.async_pong(websocket::ping_data(frame.payload), [self = std::static_pointer_cast(shared_from_this())](beast::error_code ec) { self->process_write_result(ec); }); - ws_.text(frame.type == WebsocketFrameType::text); - ws_.async_write(net::buffer(frame.payload), beast::bind_front_handler(&PlainWebsocketSession::on_write, - std::static_pointer_cast(shared_from_this()))); - } - - private: - void on_resolve(std::string target, std::map headers, beast::error_code ec, - tcp::resolver::results_type results) - { - if (closing_) return; - if (ec) return fail(ec); - beast::get_lowest_layer(ws_).async_connect(results, beast::bind_front_handler( - &PlainWebsocketSession::on_connect, - std::static_pointer_cast(shared_from_this()), - target, headers)); + return; } - void on_connect(std::string target, std::map headers, beast::error_code ec, - tcp::resolver::results_type::endpoint_type) - { - if (closing_) return; - if (ec) return fail(ec); - - ws_.set_option(websocket::stream_base::timeout::suggested(beast::role_type::client)); - - // Set Headers - ws_.set_option(websocket::stream_base::decorator([headers](websocket::request_type& req) - { - req.set(beast::http::field::user_agent, BOOST_BEAST_VERSION_STRING); - for (const auto& h : headers) req.set(h.first, h.second); - })); - - ws_.async_handshake(handshake_response_, host_, target, - beast::bind_front_handler(&PlainWebsocketSession::on_handshake, - std::static_pointer_cast( - shared_from_this()))); - } + write_queue_.pop_front(); - void on_handshake(beast::error_code ec) - { - if (closing_) return; - if (ec) return fail(ec); - set_negotiated_subprotocol(handshake_response_[beast::http::field::sec_websocket_protocol]); - notify_connect(connect_cb_, ec); - do_read(); + if (!write_queue_.empty()) { + do_write_from_queue(); + } else { + is_writing_ = false; } + } +}; + +// ========================================== +// Plain TCP Session (ws://) +// ========================================== +class PlainWebsocketSession : public WebsocketSessionImpl { + websocket::stream ws_; + websocket::response_type handshake_response_; + tcp::resolver resolver_; + WebsocketClient::ConnectCallback connect_cb_; + + public: + PlainWebsocketSession(net::io_context& ioc, + std::shared_ptr state) + : WebsocketSessionImpl(std::move(state)), + ws_(net::make_strand(ioc)), + resolver_(ws_.get_executor()) {} + + net::any_io_executor get_executor() override { return ws_.get_executor(); } + + void run(const std::string& host, const std::string& port, + const std::string& target, + const std::map& headers, + WebsocketClient::ConnectCallback cb) override { + host_ = host; + connect_cb_ = std::move(cb); + + resolver_.async_resolve( + host, port, + beast::bind_front_handler( + &PlainWebsocketSession::on_resolve, + std::static_pointer_cast(shared_from_this()), + target, headers)); + } - void do_read() - { - ws_.async_read(buffer_, beast::bind_front_handler(&PlainWebsocketSession::on_read, - std::static_pointer_cast( - shared_from_this()))); - } + void close() override { + net::post(ws_.get_executor(), + [self = std::static_pointer_cast( + shared_from_this())]() { + self->closing_ = true; + self->write_queue_.clear(); + beast::error_code ignored; + self->resolver_.cancel(); + beast::get_lowest_layer(self->ws_).cancel(); + + if (self->ws_.is_open() && !self->close_started_) { + self->close_started_ = true; + self->ws_.async_close(websocket::close_code::normal, + [self](beast::error_code) { + /* ignore close error */ + self->notify_close(); + }); + return; + } + + beast::get_lowest_layer(self->ws_).socket().shutdown( + tcp::socket::shutdown_both, ignored); + beast::get_lowest_layer(self->ws_).socket().close(ignored); + }); + } - void on_read(beast::error_code ec, std::size_t bytes) - { - process_read_result(ec, bytes, ws_.got_text(), ws_.reason()); - if (!ec) do_read(); - } + protected: + void do_write_from_queue() override { + const auto& frame = write_queue_.front(); + if (frame.type == WebsocketFrameType::ping) + return ws_.async_ping( + websocket::ping_data(frame.payload), + [self = std::static_pointer_cast( + shared_from_this())](beast::error_code ec) { + self->process_write_result(ec); + }); + if (frame.type == WebsocketFrameType::pong) + return ws_.async_pong( + websocket::ping_data(frame.payload), + [self = std::static_pointer_cast( + shared_from_this())](beast::error_code ec) { + self->process_write_result(ec); + }); + ws_.text(frame.type == WebsocketFrameType::text); + ws_.async_write(net::buffer(frame.payload), + beast::bind_front_handler( + &PlainWebsocketSession::on_write, + std::static_pointer_cast( + shared_from_this()))); + } - void on_write(beast::error_code ec, std::size_t) - { - process_write_result(ec); - } + private: + void on_resolve(std::string target, + std::map headers, + beast::error_code ec, tcp::resolver::results_type results) { + if (closing_) return; + if (ec) return fail(ec); + beast::get_lowest_layer(ws_).async_connect( + results, + beast::bind_front_handler( + &PlainWebsocketSession::on_connect, + std::static_pointer_cast(shared_from_this()), + target, headers)); + } - void fail(beast::error_code ec) - { - notify_connect(connect_cb_, ec); - } - }; + void on_connect(std::string target, + std::map headers, + beast::error_code ec, + tcp::resolver::results_type::endpoint_type) { + if (closing_) return; + if (ec) return fail(ec); + + ws_.set_option( + websocket::stream_base::timeout::suggested(beast::role_type::client)); + + // Set Headers + ws_.set_option(websocket::stream_base::decorator( + [headers](websocket::request_type& req) { + req.set(beast::http::field::user_agent, BOOST_BEAST_VERSION_STRING); + for (const auto& h : headers) req.set(h.first, h.second); + })); + + ws_.async_handshake(handshake_response_, host_, target, + beast::bind_front_handler( + &PlainWebsocketSession::on_handshake, + std::static_pointer_cast( + shared_from_this()))); + } - // ========================================== - // SSL Session (wss://) - // ========================================== - class SslWebsocketSession : public WebsocketSessionImpl - { - websocket::stream> ws_; - websocket::response_type handshake_response_; - tcp::resolver resolver_; - WebsocketClient::ConnectCallback connect_cb_; - - public: - SslWebsocketSession(net::io_context& ioc, ssl::context& ctx, std::shared_ptr state) - : WebsocketSessionImpl(std::move(state)), ws_(net::make_strand(ioc), ctx), resolver_(ws_.get_executor()) - { - } + void on_handshake(beast::error_code ec) { + if (closing_) return; + if (ec) return fail(ec); + set_negotiated_subprotocol( + handshake_response_[beast::http::field::sec_websocket_protocol]); + notify_connect(connect_cb_, ec); + do_read(); + } - net::any_io_executor get_executor() override { return ws_.get_executor(); } + void do_read() { + ws_.async_read(buffer_, beast::bind_front_handler( + &PlainWebsocketSession::on_read, + std::static_pointer_cast( + shared_from_this()))); + } - void run(const std::string& host, const std::string& port, const std::string& target, - const std::map& headers, WebsocketClient::ConnectCallback cb) override - { - host_ = host; - connect_cb_ = std::move(cb); + void on_read(beast::error_code ec, std::size_t bytes) { + process_read_result(ec, bytes, ws_.got_text(), ws_.reason()); + if (!ec) do_read(); + } - if (!SSL_set_tlsext_host_name(ws_.next_layer().native_handle(), host.c_str())) - { - return fail(beast::error_code(static_cast(::ERR_get_error()), net::error::get_ssl_category())); - } - ws_.next_layer().set_verify_callback(ssl::host_name_verification(host)); + void on_write(beast::error_code ec, std::size_t) { process_write_result(ec); } + + void fail(beast::error_code ec) { notify_connect(connect_cb_, ec); } +}; + +// ========================================== +// SSL Session (wss://) +// ========================================== +class SslWebsocketSession : public WebsocketSessionImpl { + websocket::stream> ws_; + websocket::response_type handshake_response_; + tcp::resolver resolver_; + WebsocketClient::ConnectCallback connect_cb_; + + public: + SslWebsocketSession(net::io_context& ioc, ssl::context& ctx, + std::shared_ptr state) + : WebsocketSessionImpl(std::move(state)), + ws_(net::make_strand(ioc), ctx), + resolver_(ws_.get_executor()) {} + + net::any_io_executor get_executor() override { return ws_.get_executor(); } + + void run(const std::string& host, const std::string& port, + const std::string& target, + const std::map& headers, + WebsocketClient::ConnectCallback cb) override { + host_ = host; + connect_cb_ = std::move(cb); + + if (!SSL_set_tlsext_host_name(ws_.next_layer().native_handle(), + host.c_str())) { + return fail(beast::error_code(static_cast(::ERR_get_error()), + net::error::get_ssl_category())); + } + ws_.next_layer().set_verify_callback(ssl::host_name_verification(host)); + + resolver_.async_resolve( + host, port, + beast::bind_front_handler( + &SslWebsocketSession::on_resolve, + std::static_pointer_cast(shared_from_this()), + target, headers)); + } - resolver_.async_resolve(host, port, beast::bind_front_handler(&SslWebsocketSession::on_resolve, - std::static_pointer_cast( - shared_from_this()), target, headers)); - } + void close() override { + net::post(ws_.get_executor(), + [self = std::static_pointer_cast( + shared_from_this())]() { + self->closing_ = true; + self->write_queue_.clear(); + beast::error_code ignored; + self->resolver_.cancel(); + beast::get_lowest_layer(self->ws_).cancel(); + + if (self->ws_.is_open() && !self->close_started_) { + self->close_started_ = true; + self->ws_.async_close( + websocket::close_code::normal, + [self](beast::error_code) { self->notify_close(); }); + return; + } + + beast::get_lowest_layer(self->ws_).socket().shutdown( + tcp::socket::shutdown_both, ignored); + beast::get_lowest_layer(self->ws_).socket().close(ignored); + }); + } - void close() override - { - net::post(ws_.get_executor(), [self = std::static_pointer_cast(shared_from_this())]() - { - self->closing_ = true; - self->write_queue_.clear(); - beast::error_code ignored; - self->resolver_.cancel(); - beast::get_lowest_layer(self->ws_).cancel(); - - if (self->ws_.is_open() && !self->close_started_) - { - self->close_started_ = true; - self->ws_.async_close(websocket::close_code::normal, [self](beast::error_code) - { - self->notify_close(); + protected: + void do_write_from_queue() override { + const auto& frame = write_queue_.front(); + if (frame.type == WebsocketFrameType::ping) + return ws_.async_ping( + websocket::ping_data(frame.payload), + [self = std::static_pointer_cast( + shared_from_this())](beast::error_code ec) { + self->process_write_result(ec); + }); + if (frame.type == WebsocketFrameType::pong) + return ws_.async_pong( + websocket::ping_data(frame.payload), + [self = std::static_pointer_cast( + shared_from_this())](beast::error_code ec) { + self->process_write_result(ec); }); - return; - } + ws_.text(frame.type == WebsocketFrameType::text); + ws_.async_write( + net::buffer(frame.payload), + beast::bind_front_handler( + &SslWebsocketSession::on_write, + std::static_pointer_cast(shared_from_this()))); + } - beast::get_lowest_layer(self->ws_).socket().shutdown(tcp::socket::shutdown_both, ignored); - beast::get_lowest_layer(self->ws_).socket().close(ignored); - }); - } + private: + void on_resolve(std::string target, + std::map headers, + beast::error_code ec, tcp::resolver::results_type results) { + if (closing_) return; + if (ec) return fail(ec); + beast::get_lowest_layer(ws_).async_connect( + results, + beast::bind_front_handler( + &SslWebsocketSession::on_connect, + std::static_pointer_cast(shared_from_this()), + target, headers)); + } - protected: - void do_write_from_queue() override - { - const auto& frame = write_queue_.front(); - if (frame.type == WebsocketFrameType::ping) - return ws_.async_ping(websocket::ping_data(frame.payload), [self = std::static_pointer_cast(shared_from_this())](beast::error_code ec) { self->process_write_result(ec); }); - if (frame.type == WebsocketFrameType::pong) - return ws_.async_pong(websocket::ping_data(frame.payload), [self = std::static_pointer_cast(shared_from_this())](beast::error_code ec) { self->process_write_result(ec); }); - ws_.text(frame.type == WebsocketFrameType::text); - ws_.async_write(net::buffer(frame.payload), beast::bind_front_handler(&SslWebsocketSession::on_write, - std::static_pointer_cast(shared_from_this()))); - } + void on_connect(std::string target, + std::map headers, + beast::error_code ec, + tcp::resolver::results_type::endpoint_type) { + if (closing_) return; + if (ec) return fail(ec); + ws_.next_layer().async_handshake( + ssl::stream_base::client, + beast::bind_front_handler( + &SslWebsocketSession::on_ssl_handshake, + std::static_pointer_cast(shared_from_this()), + target, headers)); + } - private: - void on_resolve(std::string target, std::map headers, beast::error_code ec, - tcp::resolver::results_type results) - { - if (closing_) return; - if (ec) return fail(ec); - beast::get_lowest_layer(ws_).async_connect(results, beast::bind_front_handler( - &SslWebsocketSession::on_connect, - std::static_pointer_cast(shared_from_this()), - target, headers)); - } + void on_ssl_handshake(std::string target, + std::map headers, + beast::error_code ec) { + if (closing_) return; + if (ec) return fail(ec); + + ws_.set_option( + websocket::stream_base::timeout::suggested(beast::role_type::client)); + ws_.set_option(websocket::stream_base::decorator( + [headers](websocket::request_type& req) { + req.set(beast::http::field::user_agent, BOOST_BEAST_VERSION_STRING); + for (const auto& h : headers) req.set(h.first, h.second); + })); + + ws_.async_handshake( + handshake_response_, host_, target, + beast::bind_front_handler( + &SslWebsocketSession::on_handshake, + std::static_pointer_cast(shared_from_this()))); + } - void on_connect(std::string target, std::map headers, beast::error_code ec, - tcp::resolver::results_type::endpoint_type) - { - if (closing_) return; - if (ec) return fail(ec); - ws_.next_layer().async_handshake(ssl::stream_base::client, - beast::bind_front_handler(&SslWebsocketSession::on_ssl_handshake, - std::static_pointer_cast( - shared_from_this()), target, headers)); - } + void on_handshake(beast::error_code ec) { + if (closing_) return; + if (ec) return fail(ec); + set_negotiated_subprotocol( + handshake_response_[beast::http::field::sec_websocket_protocol]); + notify_connect(connect_cb_, ec); + do_read(); + } - void on_ssl_handshake(std::string target, std::map headers, beast::error_code ec) - { - if (closing_) return; - if (ec) return fail(ec); - - ws_.set_option(websocket::stream_base::timeout::suggested(beast::role_type::client)); - ws_.set_option(websocket::stream_base::decorator([headers](websocket::request_type& req) - { - req.set(beast::http::field::user_agent, BOOST_BEAST_VERSION_STRING); - for (const auto& h : headers) req.set(h.first, h.second); - })); - - ws_.async_handshake(handshake_response_, host_, target, - beast::bind_front_handler(&SslWebsocketSession::on_handshake, - std::static_pointer_cast(shared_from_this()))); - } + void do_read() { + ws_.async_read( + buffer_, + beast::bind_front_handler( + &SslWebsocketSession::on_read, + std::static_pointer_cast(shared_from_this()))); + } - void on_handshake(beast::error_code ec) - { - if (closing_) return; - if (ec) return fail(ec); - set_negotiated_subprotocol(handshake_response_[beast::http::field::sec_websocket_protocol]); - notify_connect(connect_cb_, ec); - do_read(); - } + void on_read(beast::error_code ec, std::size_t bytes) { + process_read_result(ec, bytes, ws_.got_text(), ws_.reason()); + if (!ec) do_read(); + } - void do_read() - { - ws_.async_read(buffer_, beast::bind_front_handler(&SslWebsocketSession::on_read, - std::static_pointer_cast( - shared_from_this()))); - } + void on_write(beast::error_code ec, std::size_t) { process_write_result(ec); } - void on_read(beast::error_code ec, std::size_t bytes) - { - process_read_result(ec, bytes, ws_.got_text(), ws_.reason()); - if (!ec) do_read(); - } + void fail(beast::error_code ec) { notify_connect(connect_cb_, ec); } +}; - void on_write(beast::error_code ec, std::size_t) - { - process_write_result(ec); - } +// ========================================== +// WebsocketClient Implementation +// ========================================== - void fail(beast::error_code ec) - { - notify_connect(connect_cb_, ec); - } - }; +WebsocketClient::WebsocketClient() + : ioc_(IoContextPool::instance().get_io_context()), + state_(std::make_shared()) { + own_ssl_ctx_ = std::make_shared(ssl::context::tls_client); + own_ssl_ctx_->set_default_verify_paths(); + own_ssl_ctx_->set_verify_mode(ssl::verify_peer); + ssl_ctx_ptr_ = own_ssl_ctx_.get(); +} - // ========================================== - // WebsocketClient Implementation - // ========================================== +WebsocketClient::WebsocketClient(net::io_context& ioc) + : ioc_(ioc), state_(std::make_shared()) { + // Default SSL Context + own_ssl_ctx_ = std::make_shared(ssl::context::tls_client); + own_ssl_ctx_->set_default_verify_paths(); + own_ssl_ctx_->set_verify_mode(ssl::verify_peer); + ssl_ctx_ptr_ = own_ssl_ctx_.get(); +} - WebsocketClient::WebsocketClient() - : ioc_(IoContextPool::instance().get_io_context()), - state_(std::make_shared()) - { - own_ssl_ctx_ = std::make_shared(ssl::context::tls_client); - own_ssl_ctx_->set_default_verify_paths(); - own_ssl_ctx_->set_verify_mode(ssl::verify_peer); - ssl_ctx_ptr_ = own_ssl_ctx_.get(); - } +WebsocketClient::WebsocketClient(net::io_context& ioc, ssl::context& ssl_ctx) + : ioc_(ioc), ssl_ctx_ptr_(&ssl_ctx), state_(std::make_shared()) {} - WebsocketClient::WebsocketClient(net::io_context& ioc) - : ioc_(ioc), - state_(std::make_shared()) +WebsocketClient::~WebsocketClient() { { - // Default SSL Context - own_ssl_ctx_ = std::make_shared(ssl::context::tls_client); - own_ssl_ctx_->set_default_verify_paths(); - own_ssl_ctx_->set_verify_mode(ssl::verify_peer); - ssl_ctx_ptr_ = own_ssl_ctx_.get(); + std::lock_guard lock{state_->mutex}; + state_->alive = false; + state_->on_message = nullptr; + state_->on_error = nullptr; + state_->on_close = nullptr; } + close(); +} - WebsocketClient::WebsocketClient(net::io_context& ioc, ssl::context& ssl_ctx) - : ioc_(ioc), ssl_ctx_ptr_(&ssl_ctx), state_(std::make_shared()) - { - } +void WebsocketClient::set_header(const std::string& key, + const std::string& value) { + headers_[key] = value; +} - WebsocketClient::~WebsocketClient() - { - { - std::lock_guard lock{state_->mutex}; - state_->alive = false; - state_->on_message = nullptr; - state_->on_error = nullptr; - state_->on_close = nullptr; - } - close(); +void WebsocketClient::set_subprotocols( + const std::vector& subprotocols) { + std::string value; + for (const auto& protocol : subprotocols) { + if (protocol.empty()) continue; + if (!value.empty()) value += ", "; + value += protocol; } + if (value.empty()) + headers_.erase("Sec-WebSocket-Protocol"); + else + headers_["Sec-WebSocket-Protocol"] = std::move(value); +} - void WebsocketClient::set_header(const std::string& key, const std::string& value) - { - headers_[key] = value; - } +std::string WebsocketClient::negotiated_subprotocol() const { + std::lock_guard lock{state_->mutex}; + return state_->negotiated_subprotocol; +} - void WebsocketClient::set_subprotocols(const std::vector& subprotocols) - { - std::string value; - for (const auto& protocol : subprotocols) - { - if (protocol.empty()) continue; - if (!value.empty()) value += ", "; - value += protocol; - } - if (value.empty()) headers_.erase("Sec-WebSocket-Protocol"); - else headers_["Sec-WebSocket-Protocol"] = std::move(value); +void WebsocketClient::connect(const std::string& url, + ConnectCallback callback) { + auto url_result = boost::urls::parse_uri(url); + if (!url_result.has_value()) { + if (callback) callback(beast::error_code(beast::http::error::bad_target)); + return; } - - std::string WebsocketClient::negotiated_subprotocol() const - { - std::lock_guard lock{state_->mutex}; - return state_->negotiated_subprotocol; + auto u = url_result.value(); + std::string host = u.host(); + std::string scheme = u.scheme(); + std::string port = u.port(); + std::string target(u.encoded_target()); + if (target.empty()) + target = "/"; + else if (target.front() == '?') + target.insert(target.begin(), '/'); + + if (scheme != "ws" && scheme != "wss") { + if (callback) + callback(make_error_code(boost::system::errc::operation_not_supported)); + return; } - void WebsocketClient::connect(const std::string& url, ConnectCallback callback) - { - auto url_result = boost::urls::parse_uri(url); - if (!url_result.has_value()) - { - if (callback) callback(beast::error_code(beast::http::error::bad_target)); - return; - } - auto u = url_result.value(); - std::string host = u.host(); - std::string scheme = u.scheme(); - std::string port = u.port(); - std::string target(u.encoded_target()); - if (target.empty()) target = "/"; - else if (target.front() == '?') target.insert(target.begin(), '/'); - - if (scheme != "ws" && scheme != "wss") - { - if (callback) callback(make_error_code(boost::system::errc::operation_not_supported)); + if (port.empty()) port = (scheme == "wss") ? "443" : "80"; + + if (scheme == "wss") { + if (!ssl_ctx_ptr_) { + if (callback) + callback(beast::error_code(beast::errc::operation_not_supported, + beast::system_category())); return; } - - if (port.empty()) port = (scheme == "wss") ? "443" : "80"; - - if (scheme == "wss") { - if (!ssl_ctx_ptr_) - { - if (callback) callback(beast::error_code(beast::errc::operation_not_supported, beast::system_category())); - return; - } - { - std::lock_guard lock{state_->mutex}; - state_->close_notified = false; - state_->negotiated_subprotocol.clear(); - } - auto s = std::make_shared(ioc_, *ssl_ctx_ptr_, state_); - session_ = s; - s->run(host, port, target, headers_, std::move(callback)); + std::lock_guard lock{state_->mutex}; + state_->close_notified = false; + state_->negotiated_subprotocol.clear(); } - else + auto s = std::make_shared(ioc_, *ssl_ctx_ptr_, state_); + session_ = s; + s->run(host, port, target, headers_, std::move(callback)); + } else { { - { - std::lock_guard lock{state_->mutex}; - state_->close_notified = false; - state_->negotiated_subprotocol.clear(); - } - auto s = std::make_shared(ioc_, state_); - session_ = s; - s->run(host, port, target, headers_, std::move(callback)); + std::lock_guard lock{state_->mutex}; + state_->close_notified = false; + state_->negotiated_subprotocol.clear(); } + auto s = std::make_shared(ioc_, state_); + session_ = s; + s->run(host, port, target, headers_, std::move(callback)); } +} - void WebsocketClient::send(const std::string& message) - { - send({WebsocketFrameType::text, message}); - } +void WebsocketClient::send(const std::string& message) { + send({WebsocketFrameType::text, message}); +} - void WebsocketClient::send(WebsocketFrame frame) - { - if (session_) - { - session_->queue_write(std::move(frame)); - } +void WebsocketClient::send(WebsocketFrame frame) { + if (session_) { + session_->queue_write(std::move(frame)); } +} - void WebsocketClient::close() - { - if (session_) - { - session_->close(); - // session_ = nullptr; // keep alive for handlers to finish - } +void WebsocketClient::close() { + if (session_) { + session_->close(); + // session_ = nullptr; // keep alive for handlers to finish } +} - void WebsocketClient::set_on_message(MessageHandler handler) - { - std::lock_guard lock{state_->mutex}; - state_->on_message = std::move(handler); - } +void WebsocketClient::set_on_message(MessageHandler handler) { + std::lock_guard lock{state_->mutex}; + state_->on_message = std::move(handler); +} - void WebsocketClient::set_on_frame(FrameHandler handler) - { - std::lock_guard lock{state_->mutex}; - state_->on_frame = std::move(handler); - } +void WebsocketClient::set_on_frame(FrameHandler handler) { + std::lock_guard lock{state_->mutex}; + state_->on_frame = std::move(handler); +} - void WebsocketClient::set_on_error(ErrorHandler handler) - { - std::lock_guard lock{state_->mutex}; - state_->on_error = std::move(handler); - } +void WebsocketClient::set_on_error(ErrorHandler handler) { + std::lock_guard lock{state_->mutex}; + state_->on_error = std::move(handler); +} - void WebsocketClient::set_on_close(CloseHandler handler) - { - std::lock_guard lock{state_->mutex}; - state_->on_close = std::move(handler); - } +void WebsocketClient::set_on_close(CloseHandler handler) { + std::lock_guard lock{state_->mutex}; + state_->on_close = std::move(handler); } +} // namespace khttpd::framework::client diff --git a/framework/client/websocket_client.hpp b/framework/client/websocket_client.hpp index 77c807c..27a49ea 100644 --- a/framework/client/websocket_client.hpp +++ b/framework/client/websocket_client.hpp @@ -1,84 +1,82 @@ #ifndef KHTTPD_FRAMEWORK_CLIENT_WEBSOCKET_CLIENT_HPP #define KHTTPD_FRAMEWORK_CLIENT_WEBSOCKET_CLIENT_HPP -#include -#include -#include -#include #include #include - +#include +#include +#include +#include +#include #include +#include #include #include #include -#include -#include + #include "context/websocket_context.hpp" -namespace khttpd::framework::client -{ - namespace beast = boost::beast; - namespace websocket = beast::websocket; - namespace net = boost::asio; - namespace ssl = boost::asio::ssl; - using tcp = boost::asio::ip::tcp; - - // 前置声明内部会话接口 - struct WebsocketSessionImpl; - - class WebsocketClient : public std::enable_shared_from_this - { - public: - struct State; - - using ConnectCallback = std::function; - using MessageHandler = std::function; - using FrameHandler = std::function; - using ErrorHandler = std::function; - using CloseHandler = std::function; - - WebsocketClient(); - // 构造函数:支持默认 SSL 或 外部 SSL Context - explicit WebsocketClient(net::io_context& ioc); - WebsocketClient(net::io_context& ioc, ssl::context& ssl_ctx); - ~WebsocketClient(); - - // 连接 URL (支持 ws:// 和 wss://) - void connect(const std::string& url, ConnectCallback callback); - - // 发送消息 (线程安全,支持并发调用) - void send(const std::string& message); - void send(WebsocketFrame frame); - - // 关闭连接 - void close(); - - // 配置 - void set_header(const std::string& key, const std::string& value); - void set_subprotocols(const std::vector& subprotocols); - std::string negotiated_subprotocol() const; - void set_on_message(MessageHandler handler); - void set_on_frame(FrameHandler handler); - void set_on_error(ErrorHandler handler); - void set_on_close(CloseHandler handler); - - private: - friend WebsocketSessionImpl; - net::io_context& ioc_; - - // SSL Context Management - std::shared_ptr own_ssl_ctx_; - ssl::context* ssl_ctx_ptr_; - - std::shared_ptr state_; - - // Headers to send during handshake - std::map headers_; - - // 多态的内部会话 (持有实际的 websocket stream) - std::shared_ptr session_; - }; -} - -#endif // KHTTPD_FRAMEWORK_CLIENT_WEBSOCKET_CLIENT_HPP +namespace khttpd::framework::client { +namespace beast = boost::beast; +namespace websocket = beast::websocket; +namespace net = boost::asio; +namespace ssl = boost::asio::ssl; +using tcp = boost::asio::ip::tcp; + +// 前置声明内部会话接口 +struct WebsocketSessionImpl; + +class WebsocketClient : public std::enable_shared_from_this { + public: + struct State; + + using ConnectCallback = std::function; + using MessageHandler = std::function; + using FrameHandler = std::function; + using ErrorHandler = std::function; + using CloseHandler = std::function; + + WebsocketClient(); + // 构造函数:支持默认 SSL 或 外部 SSL Context + explicit WebsocketClient(net::io_context& ioc); + WebsocketClient(net::io_context& ioc, ssl::context& ssl_ctx); + ~WebsocketClient(); + + // 连接 URL (支持 ws:// 和 wss://) + void connect(const std::string& url, ConnectCallback callback); + + // 发送消息 (线程安全,支持并发调用) + void send(const std::string& message); + void send(WebsocketFrame frame); + + // 关闭连接 + void close(); + + // 配置 + void set_header(const std::string& key, const std::string& value); + void set_subprotocols(const std::vector& subprotocols); + std::string negotiated_subprotocol() const; + void set_on_message(MessageHandler handler); + void set_on_frame(FrameHandler handler); + void set_on_error(ErrorHandler handler); + void set_on_close(CloseHandler handler); + + private: + friend WebsocketSessionImpl; + net::io_context& ioc_; + + // SSL Context Management + std::shared_ptr own_ssl_ctx_; + ssl::context* ssl_ctx_ptr_; + + std::shared_ptr state_; + + // Headers to send during handshake + std::map headers_; + + // 多态的内部会话 (持有实际的 websocket stream) + std::shared_ptr session_; +}; +} // namespace khttpd::framework::client + +#endif // KHTTPD_FRAMEWORK_CLIENT_WEBSOCKET_CLIENT_HPP diff --git a/framework/context/http_context.cpp b/framework/context/http_context.cpp index e812a7c..d4eefea 100644 --- a/framework/context/http_context.cpp +++ b/framework/context/http_context.cpp @@ -1,522 +1,477 @@ // framework/context/http_context.cpp #include "http_context.hpp" + #include #include -#include // for std::remove_if -#include // for std::quoted (not directly used here, but useful for debugging) -#include -#include + +#include // for std::remove_if #include #include +#include // for std::quoted (not directly used here, but useful for debugging) +#include +#include + +namespace khttpd::framework { +// Helper to trim whitespace from a string +std::string HttpContext::trim(const std::string& str) { + const size_t first = str.find_first_not_of(" \t\n\r"); + if (std::string::npos == first) { + return ""; + } + const size_t last = str.find_last_not_of(" \t\n\r"); + return str.substr(first, last - first + 1); +} -namespace khttpd::framework -{ - // Helper to trim whitespace from a string - std::string HttpContext::trim(const std::string& str) - { - const size_t first = str.find_first_not_of(" \t\n\r"); - if (std::string::npos == first) - { +// Helper to extract a header value from a block of headers +std::string HttpContext::extract_header_value(const std::string& headers, + const std::string& header_name) { + const std::string search_str = "\r\n" + header_name + ":"; + size_t pos = headers.find(search_str); + if (pos == std::string::npos) { + // Also check for start of line for the first header + if (headers.rfind(header_name + ":", 0) == 0) { + // Check if it starts with header_name: + pos = 0; + } else { return ""; } - const size_t last = str.find_last_not_of(" \t\n\r"); - return str.substr(first, last - first + 1); - } - - // Helper to extract a header value from a block of headers - std::string HttpContext::extract_header_value(const std::string& headers, const std::string& header_name) - { - const std::string search_str = "\r\n" + header_name + ":"; - size_t pos = headers.find(search_str); - if (pos == std::string::npos) - { - // Also check for start of line for the first header - if (headers.rfind(header_name + ":", 0) == 0) - { - // Check if it starts with header_name: - pos = 0; - } - else - { - return ""; - } - } - else - { - pos += search_str.length(); - } + } else { + pos += search_str.length(); + } - size_t end_pos = headers.find("\r\n", pos); - if (end_pos == std::string::npos) - { - end_pos = headers.length(); - } - return trim(headers.substr(pos, end_pos - pos)); + size_t end_pos = headers.find("\r\n", pos); + if (end_pos == std::string::npos) { + end_pos = headers.length(); } + return trim(headers.substr(pos, end_pos - pos)); +} + +HttpContext::HttpContext( + Request& req, Response& res, + std::optional peer_endpoint) + : req_(req), res_(res), peer_endpoint_(std::move(peer_endpoint)) { + res_.version(req_.version()); + res_.keep_alive(req_.keep_alive()); + res_.set(boost::beast::http::field::server, BOOST_BEAST_VERSION_STRING); + res_.set(boost::beast::http::field::content_type, "text/plain"); +} +HttpContext::~HttpContext() = default; - HttpContext::HttpContext(Request& req, Response& res, - std::optional peer_endpoint) - : req_(req), res_(res), peer_endpoint_(std::move(peer_endpoint)) - { - res_.version(req_.version()); - res_.keep_alive(req_.keep_alive()); - res_.set(boost::beast::http::field::server, BOOST_BEAST_VERSION_STRING); - res_.set(boost::beast::http::field::content_type, "text/plain"); +void HttpContext::parse_url_components() const { + if (url_parsed_) { + return; + } + if (boost::system::result url_result = + boost::urls::parse_relative_ref(req_.target()); + url_result.has_value()) { + parsed_url_ = url_result.value(); + cached_path_ = parsed_url_.path(); + } else { + cached_path_ = std::string(req_.target()); + spdlog::warn( + "Failed to parse request target '{}' as relative-ref: {}. Query " + "parameters may not be available.", + std::string(req_.target()), url_result.error().message()); } + url_parsed_ = true; +} - HttpContext::~HttpContext() = default; +const std::string& HttpContext::path() const { + parse_url_components(); + return cached_path_; +} - void HttpContext::parse_url_components() const - { - if (url_parsed_) - { - return; - } - if (boost::system::result url_result = boost::urls::parse_relative_ref(req_.target()); - url_result.has_value()) - { - parsed_url_ = url_result.value(); - cached_path_ = parsed_url_.path(); - } - else - { - cached_path_ = std::string(req_.target()); - spdlog::warn("Failed to parse request target '{}' as relative-ref: {}. Query parameters may not be available.", - std::string(req_.target()), url_result.error().message()); - } - url_parsed_ = true; - } +boost::beast::http::verb HttpContext::method() const { return req_.method(); } + +std::string HttpContext::body() const { return req_.body(); } - const std::string& HttpContext::path() const - { - parse_url_components(); - return cached_path_; +std::optional HttpContext::get_query_param( + const std::string& key) const { + parse_url_components(); + for (auto param : parsed_url_.params()) { + if (param.key == key) return std::string(param.value); } + return std::nullopt; +} - boost::beast::http::verb HttpContext::method() const - { - return req_.method(); +std::optional HttpContext::get_path_param( + const std::string& key) const { + if (const auto it = path_params_.find(key); it != path_params_.end()) { + return it->second; } + return std::nullopt; +} - std::string HttpContext::body() const - { - return req_.body(); +std::optional HttpContext::get_header( + const boost::beast::string_view name) const { + if (const auto it = req_.find(name); it != req_.end()) { + return std::string(it->value()); } + return std::nullopt; +} - std::optional HttpContext::get_query_param(const std::string& key) const - { - parse_url_components(); - for (auto param : parsed_url_.params()) - { - if (param.key == key)return std::string(param.value); - } - return std::nullopt; +std::optional> HttpContext::get_headers( + boost::beast::string_view name) const { + auto range = req_.equal_range(name); + std::vector list{}; + for (auto it = range.first; it != range.second; ++it) { + list.push_back(it->value()); } + return list; +} - std::optional HttpContext::get_path_param(const std::string& key) const - { - if (const auto it = path_params_.find(key); it != path_params_.end()) - { - return it->second; - } +std::optional> HttpContext::get_headers( + boost::beast::http::field name) const { + return get_headers(boost::beast::http::to_string(name)); +} + +std::optional HttpContext::get_json() const { + if (cached_json_.has_value()) { + return cached_json_; + } + if (const auto content_type = + get_header(boost::beast::http::field::content_type); + !content_type || + content_type->find("application/json") == std::string::npos) { return std::nullopt; } - - std::optional HttpContext::get_header(const boost::beast::string_view name) const - { - if (const auto it = req_.find(name); it != req_.end()) - { - return std::string(it->value()); - } + try { + cached_json_ = boost::json::parse(body()); + return cached_json_; + } catch (const boost::system::system_error& e) { + spdlog::warn("Error parsing JSON body: {}", e.what()); + return std::nullopt; + } catch (const std::exception& e) { + spdlog::warn("Unexpected error parsing JSON body: {}", e.what()); return std::nullopt; } +} - std::optional> HttpContext::get_headers(boost::beast::string_view name) const - { - auto range = req_.equal_range(name); - std::vector list{}; - for (auto it = range.first; it != range.second; ++it) - { - list.push_back(it->value()); - } - return list; +void HttpContext::parse_form_params() const { + if (form_params_parsed_) { + return; } - - std::optional> HttpContext::get_headers(boost::beast::http::field name) const - { - return get_headers(boost::beast::http::to_string(name)); + if (const auto content_type = + get_header(boost::beast::http::field::content_type); + !content_type || + content_type->find("application/x-www-form-urlencoded") == + std::string::npos) { + form_params_parsed_ = true; + return; } + const auto query_string = "?" + body(); - std::optional HttpContext::get_json() const - { - if (cached_json_.has_value()) - { - return cached_json_; - } - if (const auto content_type = get_header(boost::beast::http::field::content_type); - !content_type || content_type->find("application/json") == std::string::npos) - { - return std::nullopt; - } - try - { - cached_json_ = boost::json::parse(body()); - return cached_json_; - } - catch (const boost::system::system_error& e) - { - spdlog::warn("Error parsing JSON body: {}", e.what()); - return std::nullopt; - } - catch (const std::exception& e) - { - spdlog::warn("Unexpected error parsing JSON body: {}", e.what()); - return std::nullopt; + if (auto url_query_result = boost::urls::parse_relative_ref(query_string); + url_query_result.has_value()) { + const auto url = url_query_result.value(); + for (const auto& param : url.params()) { + cached_form_params_[param.key] = param.value; } + } else { + spdlog::warn("Error parsing x-www-form-urlencoded body: {}", + url_query_result.error().message()); } + form_params_parsed_ = true; +} - void HttpContext::parse_form_params() const - { - if (form_params_parsed_) - { - return; - } - if (const auto content_type = get_header(boost::beast::http::field::content_type); - !content_type || content_type->find("application/x-www-form-urlencoded") == std::string::npos) - { - form_params_parsed_ = true; - return; - } - const auto query_string = "?" + body(); - - if (auto url_query_result = boost::urls::parse_relative_ref(query_string); url_query_result.has_value()) - { - const auto url = url_query_result.value(); - for (const auto& param : url.params()) - { - cached_form_params_[param.key] = param.value; - } - } - else - { - spdlog::warn("Error parsing x-www-form-urlencoded body: {}", url_query_result.error().message()); - } - form_params_parsed_ = true; +std::optional HttpContext::get_form_param( + const std::string& key) const { + parse_form_params(); + if (const auto it = cached_form_params_.find(key); + it != cached_form_params_.end()) { + return it->second; } + return std::nullopt; +} - std::optional HttpContext::get_form_param(const std::string& key) const - { - parse_form_params(); - if (const auto it = cached_form_params_.find(key); it != cached_form_params_.end()) - { - return it->second; - } - return std::nullopt; +// Multipart/form-data 解析实现 +void HttpContext::parse_multipart_data() const { + if (multipart_parsed_) { + return; } - // Multipart/form-data 解析实现 - void HttpContext::parse_multipart_data() const - { - if (multipart_parsed_) - { - return; - } - - auto content_type_header = get_header(boost::beast::http::field::content_type); - if (!content_type_header || content_type_header->find("multipart/form-data") == std::string::npos) - { - multipart_parsed_ = true; // Not multipart, mark as parsed to avoid re-checking - return; - } + auto content_type_header = + get_header(boost::beast::http::field::content_type); + if (!content_type_header || + content_type_header->find("multipart/form-data") == std::string::npos) { + multipart_parsed_ = + true; // Not multipart, mark as parsed to avoid re-checking + return; + } - // Extract boundary from Content-Type header - size_t boundary_pos = content_type_header->find("boundary="); - if (boundary_pos == std::string::npos) - { - spdlog::warn("Multipart/form-data: No boundary found in Content-Type header."); - multipart_parsed_ = true; - return; - } - std::string boundary = content_type_header->substr(boundary_pos + std::string("boundary=").length()); - boundary = trim(boundary); - if (boundary.length() >= 2 && boundary.front() == '"' && boundary.back() == '"') - { - boundary = boundary.substr(1, boundary.length() - 2); - } + // Extract boundary from Content-Type header + size_t boundary_pos = content_type_header->find("boundary="); + if (boundary_pos == std::string::npos) { + spdlog::warn( + "Multipart/form-data: No boundary found in Content-Type header."); + multipart_parsed_ = true; + return; + } + std::string boundary = content_type_header->substr( + boundary_pos + std::string("boundary=").length()); + boundary = trim(boundary); + if (boundary.length() >= 2 && boundary.front() == '"' && + boundary.back() == '"') { + boundary = boundary.substr(1, boundary.length() - 2); + } - std::string full_boundary = "--" + boundary; - std::string final_boundary = full_boundary + "--"; - std::string body_str = body(); // Get the full request body as string + std::string full_boundary = "--" + boundary; + std::string final_boundary = full_boundary + "--"; + std::string body_str = body(); // Get the full request body as string - size_t current_body_pos = 0; + size_t current_body_pos = 0; - // Skip preamble before the first boundary - current_body_pos = body_str.find(full_boundary); - if (current_body_pos == std::string::npos) - { - spdlog::warn("Multipart/form-data: First boundary not found."); - multipart_parsed_ = true; - return; + // Skip preamble before the first boundary + current_body_pos = body_str.find(full_boundary); + if (current_body_pos == std::string::npos) { + spdlog::warn("Multipart/form-data: First boundary not found."); + multipart_parsed_ = true; + return; + } + current_body_pos += full_boundary.length(); // Move past the first boundary + + // Loop through parts + while (current_body_pos < body_str.length()) { + // Find end of headers (first \r\n\r\n after boundary) + size_t header_end_pos = body_str.find("\r\n\r\n", current_body_pos); + if (header_end_pos == std::string::npos) { + spdlog::warn( + "Multipart/form-data: Malformed part - no header end found."); + break; + } + + std::string part_headers = + body_str.substr(current_body_pos, header_end_pos - current_body_pos); + // Trim leading \r\n from headers if present (from previous boundary) + if (part_headers.length() >= 2 && part_headers[0] == '\r' && + part_headers[1] == '\n') { + part_headers = part_headers.substr(2); + } + + const size_t data_start = header_end_pos + 4; + size_t boundary_marker_pos = + body_str.find("\r\n" + full_boundary, data_start); + if (boundary_marker_pos == std::string::npos) { + spdlog::warn( + "Multipart/form-data: Next boundary not found. Malformed or " + "premature end."); + break; // Malformed or end of stream + } + const size_t next_boundary_pos = boundary_marker_pos + 2; + + std::string part_data = + body_str.substr(data_start, boundary_marker_pos - data_start); + + // Parse Content-Disposition + std::string content_disposition = + extract_header_value(part_headers, "Content-Disposition"); + if (content_disposition.empty()) { + spdlog::warn( + "Multipart/form-data: Part with no Content-Disposition header."); + current_body_pos = next_boundary_pos + full_boundary.length(); + continue; } - current_body_pos += full_boundary.length(); // Move past the first boundary - - // Loop through parts - while (current_body_pos < body_str.length()) - { - // Find end of headers (first \r\n\r\n after boundary) - size_t header_end_pos = body_str.find("\r\n\r\n", current_body_pos); - if (header_end_pos == std::string::npos) - { - spdlog::warn("Multipart/form-data: Malformed part - no header end found."); - break; - } - std::string part_headers = body_str.substr(current_body_pos, header_end_pos - current_body_pos); - // Trim leading \r\n from headers if present (from previous boundary) - if (part_headers.length() >= 2 && part_headers[0] == '\r' && part_headers[1] == '\n') - { - part_headers = part_headers.substr(2); - } + std::string name_str; + std::string filename_str; - const size_t data_start = header_end_pos + 4; - size_t boundary_marker_pos = body_str.find("\r\n" + full_boundary, data_start); - if (boundary_marker_pos == std::string::npos) - { - spdlog::warn("Multipart/form-data: Next boundary not found. Malformed or premature end."); - break; // Malformed or end of stream + std::regex disposition_regex( + "name=\"([^\"]+)\"(?:;\\s*filename=\"([^\"]+)\")?"); + if (std::smatch matches; + std::regex_search(content_disposition, matches, disposition_regex)) { + if (matches.size() > 1) { + name_str = matches[1].str(); } - const size_t next_boundary_pos = boundary_marker_pos + 2; - - std::string part_data = body_str.substr(data_start, boundary_marker_pos - data_start); - - // Parse Content-Disposition - std::string content_disposition = extract_header_value(part_headers, "Content-Disposition"); - if (content_disposition.empty()) - { - spdlog::warn("Multipart/form-data: Part with no Content-Disposition header."); - current_body_pos = next_boundary_pos + full_boundary.length(); - continue; - } - - std::string name_str; - std::string filename_str; - - std::regex disposition_regex("name=\"([^\"]+)\"(?:;\\s*filename=\"([^\"]+)\")?"); - if (std::smatch matches; std::regex_search(content_disposition, matches, disposition_regex)) - { - if (matches.size() > 1) - { - name_str = matches[1].str(); - } - if (matches.size() > 2 && matches[2].matched) - { - // Check if filename group was captured - filename_str = matches[2].str(); - } + if (matches.size() > 2 && matches[2].matched) { + // Check if filename group was captured + filename_str = matches[2].str(); } + } - if (!name_str.empty()) - { - if (!filename_str.empty()) - { - // This is a file part - MultipartFile file; - file.filename = filename_str; - file.content_type = extract_header_value(part_headers, "Content-Type"); - file.data = part_data; - cached_multipart_files_[name_str].push_back(std::move(file)); - } - else - { - // This is a regular form field - cached_multipart_fields_[name_str] = part_data; - } + if (!name_str.empty()) { + if (!filename_str.empty()) { + // This is a file part + MultipartFile file; + file.filename = filename_str; + file.content_type = extract_header_value(part_headers, "Content-Type"); + file.data = part_data; + cached_multipart_files_[name_str].push_back(std::move(file)); + } else { + // This is a regular form field + cached_multipart_fields_[name_str] = part_data; } + } - current_body_pos = next_boundary_pos + full_boundary.length(); - if (body_str.substr(next_boundary_pos, final_boundary.length()) == final_boundary) - { - break; // Reached the final boundary - } + current_body_pos = next_boundary_pos + full_boundary.length(); + if (body_str.substr(next_boundary_pos, final_boundary.length()) == + final_boundary) { + break; // Reached the final boundary } + } - multipart_parsed_ = true; + multipart_parsed_ = true; +} + +std::optional HttpContext::get_multipart_field( + const std::string& key) const { + parse_multipart_data(); + if (const auto it = cached_multipart_fields_.find(key); + it != cached_multipart_fields_.end()) { + return it->second; } + return std::nullopt; +} - std::optional HttpContext::get_multipart_field(const std::string& key) const - { - parse_multipart_data(); - if (const auto it = cached_multipart_fields_.find(key); it != cached_multipart_fields_.end()) - { - return it->second; - } - return std::nullopt; +const std::vector* HttpContext::get_uploaded_files( + const std::string& field_name) const { + parse_multipart_data(); + const auto it = cached_multipart_files_.find(field_name); + if (it != cached_multipart_files_.end()) { + return &it->second; } + return nullptr; // or return std::optional>> +} - const std::vector* HttpContext::get_uploaded_files(const std::string& field_name) const - { - parse_multipart_data(); - const auto it = cached_multipart_files_.find(field_name); - if (it != cached_multipart_files_.end()) - { - return &it->second; - } - return nullptr; // or return std::optional>> +void HttpContext::parse_cookies() const { + if (cookies_parsed_) { + return; } - void HttpContext::parse_cookies() const - { - if (cookies_parsed_) - { - return; - } + auto cookie_headers = get_headers(boost::beast::http::field::cookie); + if (!cookie_headers) { + cookies_parsed_ = true; + return; + } - auto cookie_headers = get_headers(boost::beast::http::field::cookie); - if (!cookie_headers) - { - cookies_parsed_ = true; - return; - } + for (const auto& header_val : *cookie_headers) { + std::string::size_type pos = 0; + while (pos < header_val.length()) { + auto end_pos = header_val.find(';', pos); + if (end_pos == std::string::npos) { + end_pos = header_val.length(); + } - for (const auto& header_val : *cookie_headers) - { - std::string::size_type pos = 0; - while (pos < header_val.length()) - { - auto end_pos = header_val.find(';', pos); - if (end_pos == std::string::npos) - { - end_pos = header_val.length(); - } - - std::string cookie_pair = header_val.substr(pos, end_pos - pos); - auto eq_pos = cookie_pair.find('='); - if (eq_pos != std::string::npos) - { - std::string key = trim(cookie_pair.substr(0, eq_pos)); - std::string value = trim(cookie_pair.substr(eq_pos + 1)); - cached_cookies_[key].push_back(value); - } - - pos = end_pos + 1; + std::string cookie_pair = header_val.substr(pos, end_pos - pos); + auto eq_pos = cookie_pair.find('='); + if (eq_pos != std::string::npos) { + std::string key = trim(cookie_pair.substr(0, eq_pos)); + std::string value = trim(cookie_pair.substr(eq_pos + 1)); + cached_cookies_[key].push_back(value); } + + pos = end_pos + 1; } - cookies_parsed_ = true; } + cookies_parsed_ = true; +} - std::optional HttpContext::get_cookie(const std::string& key) const - { - parse_cookies(); - if (auto it = cached_cookies_.find(key); it != cached_cookies_.end() && !it->second.empty()) - { - return it->second.front(); - } - return std::nullopt; +std::optional HttpContext::get_cookie( + const std::string& key) const { + parse_cookies(); + if (auto it = cached_cookies_.find(key); + it != cached_cookies_.end() && !it->second.empty()) { + return it->second.front(); } + return std::nullopt; +} - std::vector HttpContext::get_cookies(const std::string& key) const - { - parse_cookies(); - if (auto it = cached_cookies_.find(key); it != cached_cookies_.end()) - { - return it->second; - } - return {}; - } - - void HttpContext::set_cookie(const std::string& key, const std::string& value, const CookieOptions& options) const - { - // Reject values containing characters that could break the header or enable injection - if (key.find(';') != std::string::npos || key.find(',') != std::string::npos || - key.find('\r') != std::string::npos || key.find('\n') != std::string::npos || - key.find('=') != std::string::npos) - { - spdlog::warn("Invalid cookie key '{}' - contains prohibited characters", key); - return; - } - if (value.find(';') != std::string::npos || value.find(',') != std::string::npos || - value.find('\r') != std::string::npos || value.find('\n') != std::string::npos) - { - spdlog::warn("Invalid cookie value '{}' - contains prohibited characters", value); - return; - } +std::vector HttpContext::get_cookies( + const std::string& key) const { + parse_cookies(); + if (auto it = cached_cookies_.find(key); it != cached_cookies_.end()) { + return it->second; + } + return {}; +} - std::string cookie_str = key + "=" + value; +void HttpContext::set_cookie(const std::string& key, const std::string& value, + const CookieOptions& options) const { + // Reject values containing characters that could break the header or enable + // injection + if (key.find(';') != std::string::npos || + key.find(',') != std::string::npos || + key.find('\r') != std::string::npos || + key.find('\n') != std::string::npos || + key.find('=') != std::string::npos) { + spdlog::warn("Invalid cookie key '{}' - contains prohibited characters", + key); + return; + } + if (value.find(';') != std::string::npos || + value.find(',') != std::string::npos || + value.find('\r') != std::string::npos || + value.find('\n') != std::string::npos) { + spdlog::warn("Invalid cookie value '{}' - contains prohibited characters", + value); + return; + } - if (options.max_age >= 0) - { - cookie_str += "; Max-Age=" + std::to_string(options.max_age); - } + std::string cookie_str = key + "=" + value; - if (!options.domain.empty()) - { - cookie_str += "; Domain=" + options.domain; - } + if (options.max_age >= 0) { + cookie_str += "; Max-Age=" + std::to_string(options.max_age); + } - if (!options.path.empty()) - { - cookie_str += "; Path=" + options.path; - } + if (!options.domain.empty()) { + cookie_str += "; Domain=" + options.domain; + } - if (options.secure) - { - cookie_str += "; Secure"; - } + if (!options.path.empty()) { + cookie_str += "; Path=" + options.path; + } - if (options.http_only) - { - cookie_str += "; HttpOnly"; - } + if (options.secure) { + cookie_str += "; Secure"; + } - if (!options.same_site.empty()) - { - cookie_str += "; SameSite=" + options.same_site; - } + if (options.http_only) { + cookie_str += "; HttpOnly"; + } - res_.insert(boost::beast::http::field::set_cookie, cookie_str); + if (!options.same_site.empty()) { + cookie_str += "; SameSite=" + options.same_site; } + res_.insert(boost::beast::http::field::set_cookie, cookie_str); +} - void HttpContext::set_status(const boost::beast::http::status status) const - { - res_.result(status); - } +void HttpContext::set_status(const boost::beast::http::status status) const { + res_.result(status); +} - void HttpContext::set_body(std::string body) const - { - res_.body() = std::move(body); - res_.prepare_payload(); - } +void HttpContext::set_body(std::string body) const { + res_.body() = std::move(body); + res_.prepare_payload(); +} - void HttpContext::chunked(const HttpStreamHandler& handler) - { - res_.chunked(handler != nullptr); - this->do_stream_chunk = handler; - } +void HttpContext::chunked(const HttpStreamHandler& handler) { + res_.chunked(handler != nullptr); + this->do_stream_chunk = handler; +} - void HttpContext::set_header(const boost::beast::string_view name, const boost::beast::string_view value) const - { - res_.set(name, value); - } +void HttpContext::set_header(const boost::beast::string_view name, + const boost::beast::string_view value) const { + res_.set(name, value); +} - void HttpContext::set_header(const boost::beast::http::field name, const boost::beast::string_view value) const - { - set_header(boost::beast::http::to_string(name), value); - } +void HttpContext::set_header(const boost::beast::http::field name, + const boost::beast::string_view value) const { + set_header(boost::beast::http::to_string(name), value); +} - std::optional HttpContext::get_header(const boost::beast::http::field name) const - { - return get_header(boost::beast::http::to_string(name)); - } +std::optional HttpContext::get_header( + const boost::beast::http::field name) const { + return get_header(boost::beast::http::to_string(name)); +} - void HttpContext::set_content_type(const boost::beast::string_view type) const - { - res_.set(boost::beast::http::field::content_type, type); - } +void HttpContext::set_content_type(const boost::beast::string_view type) const { + res_.set(boost::beast::http::field::content_type, type); +} - void HttpContext::set_path_params(std::map params) const - { - path_params_ = std::move(params); - } +void HttpContext::set_path_params( + std::map params) const { + path_params_ = std::move(params); } +} // namespace khttpd::framework diff --git a/framework/context/http_context.hpp b/framework/context/http_context.hpp index eda0d1a..1cf706f 100644 --- a/framework/context/http_context.hpp +++ b/framework/context/http_context.hpp @@ -1,173 +1,174 @@ #ifndef KHTTPD_FRAMEWORK_CONTEXT_HTTP_CONTEXT_HPP_ #define KHTTPD_FRAMEWORK_CONTEXT_HTTP_CONTEXT_HPP_ +#include +#include #include -#include #include -#include +#include #include -#include -#include +#include #include #include +#include #include -#include - -namespace khttpd::framework -{ - struct MultipartFile - { - std::string filename; - std::string content_type; - std::string data; - }; - - struct CookieOptions - { - int max_age = -1; // -1 means session cookie, 0 means delete - std::string path = "/"; - std::string domain = ""; - bool secure = false; - bool http_only = true; - std::string same_site = "Lax"; // Strict, Lax, None - }; - - - class HttpContext - { - public: - using Request = boost::beast::http::request; - using Response = boost::beast::http::response; - using WriteHandler = std::function; - using HttpStreamHandler = std::function; - - HttpContext(Request& req, Response& res, - std::optional peer_endpoint = std::nullopt); - ~HttpContext(); - - const std::string& path() const; - boost::beast::http::verb method() const; - std::string body() const; - std::optional get_query_param(const std::string& key) const; - std::optional get_path_param(const std::string& key) const; - std::optional get_header(boost::beast::string_view name) const; - std::optional get_header(boost::beast::http::field name) const; - std::optional> get_headers(boost::beast::string_view name) const; - std::optional> get_headers(boost::beast::http::field name) const; - - // The transport peer, captured from the accepted socket. Unlike forwarded - // headers this value cannot be supplied by the HTTP client. - const std::optional& peer_endpoint() const { return peer_endpoint_; } - std::optional peer_address() const - { return peer_endpoint_ ? std::optional{peer_endpoint_->address()} : std::nullopt; } - - // Cookie support - std::optional get_cookie(const std::string& key) const; - std::vector get_cookies(const std::string& key) const; - void set_cookie(const std::string& key, const std::string& value, const CookieOptions& options = {}) const; - - std::optional get_json() const; - std::optional get_form_param(const std::string& key) const; - - std::optional get_multipart_field(const std::string& key) const; - const std::vector* get_uploaded_files(const std::string& field_name) const; - - - void set_status(boost::beast::http::status status) const; - void set_body(std::string body) const; - - template - void set_body_json(T const& t, boost::json::serialize_options const& opts = {}) - { - set_content_type("application/json"); - set_body(boost::json::serialize(t, opts)); - } - - template - void set_body_from(T const& t, boost::json::storage_ptr sp = {}, boost::json::serialize_options const& opts = {}) - { - set_body_json(boost::json::value_from(t, sp), opts); - } - - void chunked(const HttpStreamHandler& handler); - void set_header(boost::beast::string_view name, boost::beast::string_view value) const; - void set_header(boost::beast::http::field name, boost::beast::string_view value) const; - void set_content_type(boost::beast::string_view type) const; - - Request& get_request() const { return req_; } - Response& get_response() const { return res_; } - Request& get_request() { return req_; } - Response& get_response() { return res_; } - HttpStreamHandler get_stream_handler() const { return do_stream_chunk; } - - void set_path_params(std::map params) const; - // Extended data for interceptors/handlers - void set_attribute(const std::string& key, std::any value) const - { - extended_data_[key] = std::move(value); +namespace khttpd::framework { +struct MultipartFile { + std::string filename; + std::string content_type; + std::string data; +}; + +struct CookieOptions { + int max_age = -1; // -1 means session cookie, 0 means delete + std::string path = "/"; + std::string domain = ""; + bool secure = false; + bool http_only = true; + std::string same_site = "Lax"; // Strict, Lax, None +}; + +class HttpContext { + public: + using Request = boost::beast::http::request; + using Response = + boost::beast::http::response; + using WriteHandler = std::function; + using HttpStreamHandler = + std::function; + + HttpContext(Request& req, Response& res, + std::optional peer_endpoint = + std::nullopt); + ~HttpContext(); + + const std::string& path() const; + boost::beast::http::verb method() const; + std::string body() const; + std::optional get_query_param(const std::string& key) const; + std::optional get_path_param(const std::string& key) const; + std::optional get_header(boost::beast::string_view name) const; + std::optional get_header(boost::beast::http::field name) const; + std::optional> get_headers( + boost::beast::string_view name) const; + std::optional> get_headers( + boost::beast::http::field name) const; + + // The transport peer, captured from the accepted socket. Unlike forwarded + // headers this value cannot be supplied by the HTTP client. + const std::optional& peer_endpoint() const { + return peer_endpoint_; + } + std::optional peer_address() const { + return peer_endpoint_ ? std::optional{peer_endpoint_->address()} + : std::nullopt; + } + + // Cookie support + std::optional get_cookie(const std::string& key) const; + std::vector get_cookies(const std::string& key) const; + void set_cookie(const std::string& key, const std::string& value, + const CookieOptions& options = {}) const; + + std::optional get_json() const; + std::optional get_form_param(const std::string& key) const; + + std::optional get_multipart_field(const std::string& key) const; + const std::vector* get_uploaded_files( + const std::string& field_name) const; + + void set_status(boost::beast::http::status status) const; + void set_body(std::string body) const; + + template + void set_body_json(T const& t, + boost::json::serialize_options const& opts = {}) { + set_content_type("application/json"); + set_body(boost::json::serialize(t, opts)); + } + + template + void set_body_from(T const& t, boost::json::storage_ptr sp = {}, + boost::json::serialize_options const& opts = {}) { + set_body_json(boost::json::value_from(t, sp), opts); + } + + void chunked(const HttpStreamHandler& handler); + void set_header(boost::beast::string_view name, + boost::beast::string_view value) const; + void set_header(boost::beast::http::field name, + boost::beast::string_view value) const; + void set_content_type(boost::beast::string_view type) const; + + Request& get_request() const { return req_; } + Response& get_response() const { return res_; } + Request& get_request() { return req_; } + Response& get_response() { return res_; } + HttpStreamHandler get_stream_handler() const { return do_stream_chunk; } + + void set_path_params(std::map params) const; + + // Extended data for interceptors/handlers + void set_attribute(const std::string& key, std::any value) const { + extended_data_[key] = std::move(value); + } + + std::any get_attribute(const std::string& key) const { + auto it = extended_data_.find(key); + if (it != extended_data_.end()) { + return it->second; } - - std::any get_attribute(const std::string& key) const - { - auto it = extended_data_.find(key); - if (it != extended_data_.end()) - { - return it->second; + return {}; + } + + template + std::optional get_attribute_as(const std::string& key) const { + auto it = extended_data_.find(key); + if (it != extended_data_.end()) { + try { + return std::any_cast(it->second); + } catch (const std::bad_any_cast&) { + return std::nullopt; } - return {}; } - - template - std::optional get_attribute_as(const std::string& key) const - { - auto it = extended_data_.find(key); - if (it != extended_data_.end()) - { - try - { - return std::any_cast(it->second); - } - catch (const std::bad_any_cast&) - { - return std::nullopt; - } - } - return std::nullopt; - } - - private: - Request& req_; - Response& res_; - std::optional peer_endpoint_; - mutable std::map query_params_; - mutable std::string cached_path_; - mutable boost::urls::url_view parsed_url_; - mutable bool url_parsed_ = false; - - mutable std::map path_params_; - - mutable std::optional cached_json_; - mutable std::map cached_form_params_; - mutable bool form_params_parsed_ = false; - - mutable std::map cached_multipart_fields_; - mutable std::map> cached_multipart_files_; - mutable bool multipart_parsed_ = false; - HttpStreamHandler do_stream_chunk = nullptr; - - mutable std::map extended_data_; - - mutable std::map> cached_cookies_; - mutable bool cookies_parsed_ = false; - void parse_cookies() const; - - void parse_url_components() const; - void parse_form_params() const; - void parse_multipart_data() const; - - static std::string trim(const std::string& str); - static std::string extract_header_value(const std::string& headers, const std::string& header_name); - }; -} -#endif // KHTTPD_FRAMEWORK_CONTEXT_HTTP_CONTEXT_HPP_ + return std::nullopt; + } + + private: + Request& req_; + Response& res_; + std::optional peer_endpoint_; + mutable std::map query_params_; + mutable std::string cached_path_; + mutable boost::urls::url_view parsed_url_; + mutable bool url_parsed_ = false; + + mutable std::map path_params_; + + mutable std::optional cached_json_; + mutable std::map cached_form_params_; + mutable bool form_params_parsed_ = false; + + mutable std::map cached_multipart_fields_; + mutable std::map> + cached_multipart_files_; + mutable bool multipart_parsed_ = false; + HttpStreamHandler do_stream_chunk = nullptr; + + mutable std::map extended_data_; + + mutable std::map> cached_cookies_; + mutable bool cookies_parsed_ = false; + void parse_cookies() const; + + void parse_url_components() const; + void parse_form_params() const; + void parse_multipart_data() const; + + static std::string trim(const std::string& str); + static std::string extract_header_value(const std::string& headers, + const std::string& header_name); +}; +} // namespace khttpd::framework +#endif // KHTTPD_FRAMEWORK_CONTEXT_HTTP_CONTEXT_HPP_ diff --git a/framework/context/http_request_stream.hpp b/framework/context/http_request_stream.hpp index 330884a..b5195dc 100644 --- a/framework/context/http_request_stream.hpp +++ b/framework/context/http_request_stream.hpp @@ -5,22 +5,22 @@ #include #include -namespace khttpd::framework -{ - // A request body is consumed serially: callers must wait for a read callback - // before issuing the next read. This is the backpressure boundary used by - // streaming proxy handlers. - class HttpRequestStream - { - public: - using ReadCallback = std::function; - virtual ~HttpRequestStream() = default; +namespace khttpd::framework { +// A request body is consumed serially: callers must wait for a read callback +// before issuing the next read. This is the backpressure boundary used by +// streaming proxy handlers. +class HttpRequestStream { + public: + using ReadCallback = + std::function; + virtual ~HttpRequestStream() = default; - virtual void async_read_some(boost::asio::mutable_buffer buffer, ReadCallback callback) = 0; - // Cancels only request-body consumption. The response side remains usable. - virtual void cancel_read() { cancel(); } - virtual void cancel() = 0; - }; -} + virtual void async_read_some(boost::asio::mutable_buffer buffer, + ReadCallback callback) = 0; + // Cancels only request-body consumption. The response side remains usable. + virtual void cancel_read() { cancel(); } + virtual void cancel() = 0; +}; +} // namespace khttpd::framework #endif diff --git a/framework/context/http_response_stream.hpp b/framework/context/http_response_stream.hpp index 82c654d..ab43159 100644 --- a/framework/context/http_response_stream.hpp +++ b/framework/context/http_response_stream.hpp @@ -5,25 +5,26 @@ #include #include -namespace khttpd::framework -{ - class HttpResponseStream - { - public: - using ResponseHead = boost::beast::http::response; - using Callback = std::function; - virtual ~HttpResponseStream() = default; - virtual void async_start(ResponseHead head, Callback callback) = 0; - virtual void async_write_some(boost::asio::const_buffer buffer, Callback callback) = 0; - virtual void async_finish(Callback callback) = 0; - // Completes when the peer closes its read side or sends unexpected data while a response stream is active. - // The default no-op disables disconnect observation for custom response-stream implementations. - virtual void async_wait_disconnect(Callback callback) { (void)callback; } - virtual void cancel_disconnect_wait() {} - // Stops an inbound request-body read while preserving this response stream. - virtual void cancel_request_body() { cancel(); } - virtual void cancel() = 0; - }; -} +namespace khttpd::framework { +class HttpResponseStream { + public: + using ResponseHead = + boost::beast::http::response; + using Callback = std::function; + virtual ~HttpResponseStream() = default; + virtual void async_start(ResponseHead head, Callback callback) = 0; + virtual void async_write_some(boost::asio::const_buffer buffer, + Callback callback) = 0; + virtual void async_finish(Callback callback) = 0; + // Completes when the peer closes its read side or sends unexpected data while + // a response stream is active. The default no-op disables disconnect + // observation for custom response-stream implementations. + virtual void async_wait_disconnect(Callback callback) { (void)callback; } + virtual void cancel_disconnect_wait() {} + // Stops an inbound request-body read while preserving this response stream. + virtual void cancel_request_body() { cancel(); } + virtual void cancel() = 0; +}; +} // namespace khttpd::framework #endif diff --git a/framework/context/websocket_context.cpp b/framework/context/websocket_context.cpp index a5430f7..8516ca1 100644 --- a/framework/context/websocket_context.cpp +++ b/framework/context/websocket_context.cpp @@ -1,96 +1,103 @@ // framework/context/websocket_context.cpp #include "websocket_context.hpp" -#include "websocket/websocket_session.hpp" + #include -#include #include #include +#include -namespace khttpd::framework -{ - WebsocketContext::WebsocketContext(std::weak_ptr session, std::string msg, bool text, - std::string path_str) - : session_weak_ptr(std::move(session)), message(std::move(msg)), is_text(text), path(std::move(path_str)) - { - if (const auto session_shared_ptr = session_weak_ptr.lock()) - { - id = session_shared_ptr->id; - } - frame.type = text ? WebsocketFrameType::text : WebsocketFrameType::binary; - frame.payload = message; - } +#include "websocket/websocket_session.hpp" - WebsocketContext::WebsocketContext(std::weak_ptr session, std::string path_str, - boost::beast::error_code ec) - : session_weak_ptr(std::move(session)), is_text(false), error_code(ec), path(std::move(path_str)) - { - if (const auto session_shared_ptr = session_weak_ptr.lock()) - { - id = session_shared_ptr->id; - } +namespace khttpd::framework { +WebsocketContext::WebsocketContext(std::weak_ptr session, + std::string msg, bool text, + std::string path_str) + : session_weak_ptr(std::move(session)), + message(std::move(msg)), + is_text(text), + path(std::move(path_str)) { + if (const auto session_shared_ptr = session_weak_ptr.lock()) { + id = session_shared_ptr->id; } + frame.type = text ? WebsocketFrameType::text : WebsocketFrameType::binary; + frame.payload = message; +} - - void WebsocketContext::send(const std::string& msg, bool is_text_msg) - { - if (auto session_shared_ptr = session_weak_ptr.lock()) - { - session_shared_ptr->send_message(msg, is_text_msg); - } - else - { - spdlog::error("Attempted to send WS message to expired session (path: {}).", path); - } +WebsocketContext::WebsocketContext(std::weak_ptr session, + std::string path_str, + boost::beast::error_code ec) + : session_weak_ptr(std::move(session)), + is_text(false), + error_code(ec), + path(std::move(path_str)) { + if (const auto session_shared_ptr = session_weak_ptr.lock()) { + id = session_shared_ptr->id; } +} - void WebsocketContext::send(WebsocketFrame outbound_frame) - { - if (auto session = session_weak_ptr.lock()) session->send_frame(std::move(outbound_frame)); - else spdlog::error("Attempted to send WS frame to expired session (path: {}).", path); +void WebsocketContext::send(const std::string& msg, bool is_text_msg) { + if (auto session_shared_ptr = session_weak_ptr.lock()) { + session_shared_ptr->send_message(msg, is_text_msg); + } else { + spdlog::error("Attempted to send WS message to expired session (path: {}).", + path); } +} - const WebsocketHandshakeRequest& WebsocketContext::handshake() const - { - static const WebsocketHandshakeRequest empty; - if (const auto session = session_weak_ptr.lock()) return session->handshake(); - return empty; - } +void WebsocketContext::send(WebsocketFrame outbound_frame) { + if (auto session = session_weak_ptr.lock()) + session->send_frame(std::move(outbound_frame)); + else + spdlog::error("Attempted to send WS frame to expired session (path: {}).", + path); +} - std::optional WebsocketContext::get_header(const std::string& name) const - { - const auto values = get_headers(name); - if (values.empty()) return std::nullopt; - return values.front(); - } +const WebsocketHandshakeRequest& WebsocketContext::handshake() const { + static const WebsocketHandshakeRequest empty; + if (const auto session = session_weak_ptr.lock()) return session->handshake(); + return empty; +} - std::vector WebsocketContext::get_headers(const std::string& name) const - { - std::vector values; - const auto equal_ci = [](const std::string& a, const std::string& b) - { - return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(), - [](unsigned char x, unsigned char y) { return std::tolower(x) == std::tolower(y); }); - }; - for (const auto& [key, value] : handshake().headers) - if (equal_ci(key, name)) values.push_back(value); - return values; - } +std::optional WebsocketContext::get_header( + const std::string& name) const { + const auto values = get_headers(name); + if (values.empty()) return std::nullopt; + return values.front(); +} - std::optional WebsocketContext::get_query_param(const std::string& key) const - { - const auto it = handshake().query_params.find(key); - return it == handshake().query_params.end() ? std::nullopt : std::optional(it->second); - } +std::vector WebsocketContext::get_headers( + const std::string& name) const { + std::vector values; + const auto equal_ci = [](const std::string& a, const std::string& b) { + return a.size() == b.size() && + std::equal(a.begin(), a.end(), b.begin(), + [](unsigned char x, unsigned char y) { + return std::tolower(x) == std::tolower(y); + }); + }; + for (const auto& [key, value] : handshake().headers) + if (equal_ci(key, name)) values.push_back(value); + return values; +} - std::optional WebsocketContext::get_path_param(const std::string& key) const - { - const auto it = path_params_.find(key); - return it == path_params_.end() ? std::nullopt : std::optional(it->second); - } +std::optional WebsocketContext::get_query_param( + const std::string& key) const { + const auto it = handshake().query_params.find(key); + return it == handshake().query_params.end() + ? std::nullopt + : std::optional(it->second); +} - void WebsocketContext::set_path_params(std::map params) - { - path_params_ = std::move(params); - } +std::optional WebsocketContext::get_path_param( + const std::string& key) const { + const auto it = path_params_.find(key); + return it == path_params_.end() ? std::nullopt + : std::optional(it->second); +} + +void WebsocketContext::set_path_params( + std::map params) { + path_params_ = std::move(params); } +} // namespace khttpd::framework diff --git a/framework/context/websocket_context.hpp b/framework/context/websocket_context.hpp index a755f92..f2b4a44 100644 --- a/framework/context/websocket_context.hpp +++ b/framework/context/websocket_context.hpp @@ -1,96 +1,92 @@ #ifndef KHTTPD_FRAMEWORK_WEBSOCKET_CONTEXT_HPP #define KHTTPD_FRAMEWORK_WEBSOCKET_CONTEXT_HPP -#include -#include +#include #include #include -#include +#include #include +#include #include -namespace khttpd::framework -{ - class WebsocketSession; +namespace khttpd::framework { +class WebsocketSession; - enum class WebsocketFrameType { text, binary, ping, pong, close }; +enum class WebsocketFrameType { text, binary, ping, pong, close }; - struct WebsocketFrame - { - WebsocketFrameType type = WebsocketFrameType::text; - std::string payload; - uint16_t close_code = 1000; - std::string close_reason; - }; +struct WebsocketFrame { + WebsocketFrameType type = WebsocketFrameType::text; + std::string payload; + uint16_t close_code = 1000; + std::string close_reason; +}; - // HeaderList deliberately keeps duplicate fields (notably Cookie and - // Sec-WebSocket-Extensions) in their original handshake order. - using WebsocketHeaderList = std::vector>; +// HeaderList deliberately keeps duplicate fields (notably Cookie and +// Sec-WebSocket-Extensions) in their original handshake order. +using WebsocketHeaderList = std::vector>; - struct WebsocketHandshakeRequest - { - std::string target; - std::string path; - WebsocketHeaderList headers; - std::map query_params; - std::vector subprotocols; - }; +struct WebsocketHandshakeRequest { + std::string target; + std::string path; + WebsocketHeaderList headers; + std::map query_params; + std::vector subprotocols; +}; - class WebsocketContext - { - public: - std::string id; - std::weak_ptr session_weak_ptr; - std::string message; - bool is_text; - boost::beast::error_code error_code; - std::string path; - WebsocketFrame frame; +class WebsocketContext { + public: + std::string id; + std::weak_ptr session_weak_ptr; + std::string message; + bool is_text; + boost::beast::error_code error_code; + std::string path; + WebsocketFrame frame; - std::map extended_data; + std::map extended_data; - WebsocketContext(std::weak_ptr session, std::string msg, bool text, - std::string path); - WebsocketContext(std::weak_ptr session, std::string path, - boost::beast::error_code ec = {}); + WebsocketContext(std::weak_ptr session, std::string msg, + bool text, std::string path); + WebsocketContext(std::weak_ptr session, std::string path, + boost::beast::error_code ec = {}); - void send(const std::string& msg, bool is_text = true); - void send(WebsocketFrame frame); + void send(const std::string& msg, bool is_text = true); + void send(WebsocketFrame frame); - const WebsocketHandshakeRequest& handshake() const; - std::optional get_header(const std::string& name) const; - std::vector get_headers(const std::string& name) const; - std::optional get_query_param(const std::string& key) const; - std::optional get_path_param(const std::string& key) const; - void set_path_params(std::map params); + const WebsocketHandshakeRequest& handshake() const; + std::optional get_header(const std::string& name) const; + std::vector get_headers(const std::string& name) const; + std::optional get_query_param(const std::string& key) const; + std::optional get_path_param(const std::string& key) const; + void set_path_params(std::map params); - void set_attribute(const std::string& key, std::any value) { - extended_data[key] = std::move(value); - } + void set_attribute(const std::string& key, std::any value) { + extended_data[key] = std::move(value); + } - std::any get_attribute(const std::string& key) const { - auto it = extended_data.find(key); - if (it != extended_data.end()) { - return it->second; - } - return {}; + std::any get_attribute(const std::string& key) const { + auto it = extended_data.find(key); + if (it != extended_data.end()) { + return it->second; } + return {}; + } - template - std::optional get_attribute_as(const std::string& key) const { - auto it = extended_data.find(key); - if (it != extended_data.end()) { - try { - return std::any_cast(it->second); - } catch (const std::bad_any_cast&) { - return std::nullopt; - } - } + template + std::optional get_attribute_as(const std::string& key) const { + auto it = extended_data.find(key); + if (it != extended_data.end()) { + try { + return std::any_cast(it->second); + } catch (const std::bad_any_cast&) { return std::nullopt; + } } + return std::nullopt; + } - private: - std::map path_params_; - }; -} -#endif // KHTTPD_FRAMEWORK_WEBSOCKET_CONTEXT_HPP + private: + std::map path_params_; +}; +} // namespace khttpd::framework +#endif // KHTTPD_FRAMEWORK_WEBSOCKET_CONTEXT_HPP diff --git a/framework/controller/http_controller.hpp b/framework/controller/http_controller.hpp index 2a6272b..23e8fa4 100644 --- a/framework/controller/http_controller.hpp +++ b/framework/controller/http_controller.hpp @@ -2,100 +2,101 @@ #ifndef KHTTPD_FRAMEWORK_CONTROLLER_HTTP_CONTROLLER_HPP #define KHTTPD_FRAMEWORK_CONTROLLER_HTTP_CONTROLLER_HPP -#include "router/websocket_router.hpp" #include "router/http_router.hpp" +#include "router/websocket_router.hpp" - -namespace khttpd::framework -{ +namespace khttpd::framework { #ifndef KHTTPD_ROUTE #define KHTTPD_ROUTE(VERB, PATH, METHOD_NAME) \ -router.VERB(base_path() + PATH, bind_handler(&std::decay_t::METHOD_NAME)) + router.VERB(base_path() + PATH, \ + bind_handler(&std::decay_t::METHOD_NAME)) #endif #ifndef KHTTPD_DOCUMENTED_ROUTE -#define KHTTPD_DOCUMENTED_ROUTE(VERB, PATH, METHOD_NAME, ...) \ -router.VERB(base_path() + PATH, bind_handler(&std::decay_t::METHOD_NAME), __VA_ARGS__) +#define KHTTPD_DOCUMENTED_ROUTE(VERB, PATH, METHOD_NAME, ...) \ + router.VERB(base_path() + PATH, \ + bind_handler(&std::decay_t::METHOD_NAME), \ + __VA_ARGS__) #endif #ifndef KHTTPD_TYPED_ROUTE -#define KHTTPD_TYPED_ROUTE(VERB, PATH, METHOD_NAME) \ -router.VERB(base_path() + PATH, this->shared_from_this(), \ - &std::decay_t::METHOD_NAME) +#define KHTTPD_TYPED_ROUTE(VERB, PATH, METHOD_NAME) \ + router.VERB(base_path() + PATH, this->shared_from_this(), \ + &std::decay_t::METHOD_NAME) #endif #ifndef KHTTPD_DOCUMENTED_TYPED_ROUTE #define KHTTPD_DOCUMENTED_TYPED_ROUTE(VERB, PATH, METHOD_NAME, ...) \ -router.VERB(base_path() + PATH, this->shared_from_this(), &std::decay_t::METHOD_NAME, __VA_ARGS__) + router.VERB(base_path() + PATH, this->shared_from_this(), \ + &std::decay_t::METHOD_NAME, __VA_ARGS__) #endif #ifndef KHTTPD_WSROUTE #define KHTTPD_WSROUTE_NULL_HANDLER nullptr #define KHTTPD_WSROUTE_BIND_HANDLER(METHOD_NAME) \ - bind_handler(&std::decay_t::METHOD_NAME) + bind_handler(&std::decay_t::METHOD_NAME) -#define KHTTPD_WSROUTE_2(PATH, ONMESSAGE_METHOD_NAME) \ - router.add_handler(base_path() + PATH, \ - KHTTPD_WSROUTE_NULL_HANDLER, /* ONOPEN */ \ - KHTTPD_WSROUTE_BIND_HANDLER(ONMESSAGE_METHOD_NAME), \ - KHTTPD_WSROUTE_NULL_HANDLER, /* ONCLOSE */ \ - KHTTPD_WSROUTE_NULL_HANDLER /* ONERROR */ \ - ) +#define KHTTPD_WSROUTE_2(PATH, ONMESSAGE_METHOD_NAME) \ + router.add_handler(base_path() + PATH, \ + KHTTPD_WSROUTE_NULL_HANDLER, /* ONOPEN */ \ + KHTTPD_WSROUTE_BIND_HANDLER(ONMESSAGE_METHOD_NAME), \ + KHTTPD_WSROUTE_NULL_HANDLER, /* ONCLOSE */ \ + KHTTPD_WSROUTE_NULL_HANDLER /* ONERROR */ \ + ) #define KHTTPD_WSROUTE_3(PATH, ONMESSAGE_METHOD_NAME, ONCLOSE_METHOD_NAME) \ - router.add_handler(base_path() + PATH, \ - KHTTPD_WSROUTE_NULL_HANDLER, /* ONOPEN */ \ - KHTTPD_WSROUTE_BIND_HANDLER(ONMESSAGE_METHOD_NAME), \ - KHTTPD_WSROUTE_BIND_HANDLER(ONCLOSE_METHOD_NAME), \ - KHTTPD_WSROUTE_NULL_HANDLER /* ONERROR */ \ - ) - -#define KHTTPD_WSROUTE_4(PATH, ONOPEN_METHOD_NAME, ONMESSAGE_METHOD_NAME, ONCLOSE_METHOD_NAME) \ - router.add_handler(base_path() + PATH, \ - KHTTPD_WSROUTE_BIND_HANDLER(ONOPEN_METHOD_NAME), \ - KHTTPD_WSROUTE_BIND_HANDLER(ONMESSAGE_METHOD_NAME), \ - KHTTPD_WSROUTE_BIND_HANDLER(ONCLOSE_METHOD_NAME), \ - KHTTPD_WSROUTE_NULL_HANDLER /* ONERROR */ \ - ) - -#define KHTTPD_WSROUTE_5(PATH, ONOPEN_METHOD_NAME, ONMESSAGE_METHOD_NAME, ONCLOSE_METHOD_NAME, ONERROR_METHOD_NAME) \ - router.add_handler(base_path() + PATH, \ - KHTTPD_WSROUTE_BIND_HANDLER(ONOPEN_METHOD_NAME), \ - KHTTPD_WSROUTE_BIND_HANDLER(ONMESSAGE_METHOD_NAME), \ - KHTTPD_WSROUTE_BIND_HANDLER(ONCLOSE_METHOD_NAME), \ - KHTTPD_WSROUTE_BIND_HANDLER(ONERROR_METHOD_NAME) \ - ) + router.add_handler(base_path() + PATH, \ + KHTTPD_WSROUTE_NULL_HANDLER, /* ONOPEN */ \ + KHTTPD_WSROUTE_BIND_HANDLER(ONMESSAGE_METHOD_NAME), \ + KHTTPD_WSROUTE_BIND_HANDLER(ONCLOSE_METHOD_NAME), \ + KHTTPD_WSROUTE_NULL_HANDLER /* ONERROR */ \ + ) + +#define KHTTPD_WSROUTE_4(PATH, ONOPEN_METHOD_NAME, ONMESSAGE_METHOD_NAME, \ + ONCLOSE_METHOD_NAME) \ + router.add_handler(base_path() + PATH, \ + KHTTPD_WSROUTE_BIND_HANDLER(ONOPEN_METHOD_NAME), \ + KHTTPD_WSROUTE_BIND_HANDLER(ONMESSAGE_METHOD_NAME), \ + KHTTPD_WSROUTE_BIND_HANDLER(ONCLOSE_METHOD_NAME), \ + KHTTPD_WSROUTE_NULL_HANDLER /* ONERROR */ \ + ) + +#define KHTTPD_WSROUTE_5(PATH, ONOPEN_METHOD_NAME, ONMESSAGE_METHOD_NAME, \ + ONCLOSE_METHOD_NAME, ONERROR_METHOD_NAME) \ + router.add_handler(base_path() + PATH, \ + KHTTPD_WSROUTE_BIND_HANDLER(ONOPEN_METHOD_NAME), \ + KHTTPD_WSROUTE_BIND_HANDLER(ONMESSAGE_METHOD_NAME), \ + KHTTPD_WSROUTE_BIND_HANDLER(ONCLOSE_METHOD_NAME), \ + KHTTPD_WSROUTE_BIND_HANDLER(ONERROR_METHOD_NAME)) #define KHTTPD_WSROUTE_GET_MACRO(_1, _2, _3, _4, _5, N, ...) N -#define KHTTPD_WSROUTE(...) \ - KHTTPD_WSROUTE_GET_MACRO(__VA_ARGS__, KHTTPD_WSROUTE_5, KHTTPD_WSROUTE_4, KHTTPD_WSROUTE_3, KHTTPD_WSROUTE_2)(__VA_ARGS__) +#define KHTTPD_WSROUTE(...) \ + KHTTPD_WSROUTE_GET_MACRO(__VA_ARGS__, KHTTPD_WSROUTE_5, KHTTPD_WSROUTE_4, \ + KHTTPD_WSROUTE_3, KHTTPD_WSROUTE_2) \ + (__VA_ARGS__) #endif +template +class BaseController : public std::enable_shared_from_this { + public: + virtual ~BaseController() = default; - template - class BaseController : public std::enable_shared_from_this - { - public: - virtual ~BaseController() = default; - - virtual std::shared_ptr register_routes(HttpRouter& router) = 0; + virtual std::shared_ptr register_routes( + HttpRouter& router) = 0; - virtual std::shared_ptr register_routes(WebsocketRouter& router) - { - return this->shared_from_this(); - } + virtual std::shared_ptr register_routes( + WebsocketRouter& router) { + return this->shared_from_this(); + } - protected: - virtual std::string base_path() - { - return ""; - } + protected: + virtual std::string base_path() { return ""; } - template - auto bind_handler(MethodPtr method_ptr) - { - return std::bind(method_ptr, this->shared_from_this(), std::placeholders::_1); - } - }; -} // namespace khttpd::framework + template + auto bind_handler(MethodPtr method_ptr) { + return std::bind(method_ptr, this->shared_from_this(), + std::placeholders::_1); + } +}; +} // namespace khttpd::framework -#endif // KHTTPD_FRAMEWORK_CONTROLLER_HTTP_CONTROLLER_HPP +#endif // KHTTPD_FRAMEWORK_CONTROLLER_HTTP_CONTROLLER_HPP diff --git a/framework/cron/CronJob.hpp b/framework/cron/CronJob.hpp index fb67e25..8013548 100644 --- a/framework/cron/CronJob.hpp +++ b/framework/cron/CronJob.hpp @@ -1,130 +1,111 @@ #ifndef KHTTPD_FRAMEWORK_CRON_JOB_HPP #define KHTTPD_FRAMEWORK_CRON_JOB_HPP -#include -#include -#include -#include +#include + #include -#include #include -#include +#include +#include +#include +#include +#include + #include "croncpp.hpp" #include "io_context_pool.hpp" -namespace khttpd::framework -{ - class CronJob : public std::enable_shared_from_this - { - public: - explicit CronJob(const std::string& expression) - : timer_(IoContextPool::instance().get_io_context()) - , expression_(expression) - , is_running_(false) - { - try - { - cron_expr_ = cron::make_cron(expression); - } - catch (const std::exception& e) - { - spdlog::error("[CronJob] Invalid expression '{}': {}", expression, e.what()); - throw; +namespace khttpd::framework { +class CronJob : public std::enable_shared_from_this { + public: + explicit CronJob(const std::string& expression) + : timer_(IoContextPool::instance().get_io_context()), + expression_(expression), + is_running_(false) { + try { + cron_expr_ = cron::make_cron(expression); + } catch (const std::exception& e) { + spdlog::error("[CronJob] Invalid expression '{}': {}", expression, + e.what()); + throw; + } + } + + virtual ~CronJob() {} + + /** + * @brief 启动任务 + * @param delay_ms 延迟启动时间(毫秒),默认为 0(立即计算下一次执行时间) + */ + void start( + std::chrono::milliseconds delay_ms = std::chrono::milliseconds(0)) { + bool expected = false; + if (is_running_.compare_exchange_strong(expected, true)) { + if (delay_ms.count() > 0) { + // 延迟启动逻辑 + timer_.expires_after(delay_ms); + auto self = shared_from_this(); + timer_.async_wait([this, self](const boost::system::error_code& ec) { + if (!ec && is_running_) { + schedule_next(); // 延迟结束后,开始正常的 cron 调度 + } + }); + } else { + // 立即启动 + schedule_next(); } } + } - virtual ~CronJob() - { - } + void stop() { + is_running_ = false; + timer_.cancel(); + } - /** - * @brief 启动任务 - * @param delay_ms 延迟启动时间(毫秒),默认为 0(立即计算下一次执行时间) - */ - void start(std::chrono::milliseconds delay_ms = std::chrono::milliseconds(0)) - { - bool expected = false; - if (is_running_.compare_exchange_strong(expected, true)) - { - if (delay_ms.count() > 0) - { - // 延迟启动逻辑 - timer_.expires_after(delay_ms); - auto self = shared_from_this(); - timer_.async_wait([this, self](const boost::system::error_code& ec) - { - if (!ec && is_running_) - { - schedule_next(); // 延迟结束后,开始正常的 cron 调度 - } - }); - } - else - { - // 立即启动 - schedule_next(); - } - } - } + // 判断当前是否在运行状态 + bool is_running() const { return is_running_; } - void stop() - { - is_running_ = false; - timer_.cancel(); - } + protected: + virtual void run() = 0; + + private: + void schedule_next() { + if (!is_running_) return; - // 判断当前是否在运行状态 - bool is_running() const { return is_running_; } + auto now_time_t = std::time(nullptr); + std::time_t next_time_t = cron::cron_next(cron_expr_, now_time_t); + auto next_time_point = std::chrono::system_clock::from_time_t(next_time_t); - protected: - virtual void run() = 0; + timer_.expires_at(next_time_point); - private: - void schedule_next() - { + auto self = shared_from_this(); + + timer_.async_wait([this, self](const boost::system::error_code& ec) { + if (ec == boost::asio::error::operation_aborted) return; if (!is_running_) return; - auto now_time_t = std::time(nullptr); - std::time_t next_time_t = cron::cron_next(cron_expr_, now_time_t); - auto next_time_point = std::chrono::system_clock::from_time_t(next_time_t); - - timer_.expires_at(next_time_point); - - auto self = shared_from_this(); - - timer_.async_wait([this, self](const boost::system::error_code& ec) - { - if (ec == boost::asio::error::operation_aborted) return; - if (!is_running_) return; - - if (ec) - { - spdlog::error("[CronJob] Timer error: {}", ec.message()); - return; - } - - try - { - this->run(); - } - catch (const std::exception& e) - { - spdlog::error("[CronJob] Task exception: {}", e.what()); - } - - if (is_running_) - { - schedule_next(); - } - }); - } + if (ec) { + spdlog::error("[CronJob] Timer error: {}", ec.message()); + return; + } - private: - boost::asio::system_timer timer_; - std::string expression_; - cron::cronexpr cron_expr_; - std::atomic is_running_; - }; -} + try { + this->run(); + } catch (const std::exception& e) { + spdlog::error("[CronJob] Task exception: {}", e.what()); + } + + if (is_running_) { + schedule_next(); + } + }); + } + + private: + boost::asio::system_timer timer_; + std::string expression_; + cron::cronexpr cron_expr_; + std::atomic is_running_; +}; +} // namespace khttpd::framework #endif diff --git a/framework/cron/CronScheduler.hpp b/framework/cron/CronScheduler.hpp index 85aa803..ac85f27 100644 --- a/framework/cron/CronScheduler.hpp +++ b/framework/cron/CronScheduler.hpp @@ -1,119 +1,105 @@ #ifndef KHTTPD_FRAMEWORK_CRON_SCHEDULER_HPP #define KHTTPD_FRAMEWORK_CRON_SCHEDULER_HPP -#include "CronJob.hpp" +#include #include #include -#include #include -#include +#include -namespace khttpd::framework -{ - class CronScheduler - { - private: - // 内部通用实现类:LambdaCronJob - // 专门用于执行 std::function 的任务 - class LambdaCronJob : public CronJob - { - public: - LambdaCronJob(const std::string& expr, std::function func) - : CronJob(expr), func_(std::move(func)) - { - } - - protected: - void run() override - { - if (func_) func_(); - } - - private: - std::function func_; - }; - - public: - // 单例获取 - static CronScheduler& instance() - { - static CronScheduler instance; - return instance; - } +#include "CronJob.hpp" - // 禁止拷贝 - CronScheduler(const CronScheduler&) = delete; - CronScheduler& operator=(const CronScheduler&) = delete; - - /** - * @brief 调度一个 Cron 任务 - * - * @param expression Cron 表达式 (如 "* * * * * *") - * @param task 回调函数 - * @param delay_ms 首次启动延迟 (默认 0) - * @return std::shared_ptr 返回任务句柄。 - * 注意:即使忽略返回值,任务也会自动运行(因为 ASIO 内部持有了 shared_ptr)。 - * 保留返回值是为了让你有机会调用 .stop()。 - */ - std::shared_ptr schedule( - const std::string& expression, - std::function task, - std::chrono::milliseconds delay_ms = std::chrono::milliseconds(0)) - { - auto job = std::make_shared(expression, std::move(task)); - job->start(delay_ms); - - { - std::lock_guard lock{mtx_}; - jobs_.push_back(job); - } - return job; - } +namespace khttpd::framework { +class CronScheduler { + private: + // 内部通用实现类:LambdaCronJob + // 专门用于执行 std::function 的任务 + class LambdaCronJob : public CronJob { + public: + LambdaCronJob(const std::string& expr, std::function func) + : CronJob(expr), func_(std::move(func)) {} - void stop_all() - { - std::vector> jobs; - { - std::lock_guard lock{mtx_}; - jobs.swap(jobs_); - } - for (const auto& job : jobs) - { - job->stop(); - } + protected: + void run() override { + if (func_) func_(); } - void unschedule(const std::shared_ptr& job) - { - if (!job) return; - job->stop(); + private: + std::function func_; + }; - std::lock_guard lock{mtx_}; - jobs_.erase(std::remove(jobs_.begin(), jobs_.end(), job), jobs_.end()); - } + public: + // 单例获取 + static CronScheduler& instance() { + static CronScheduler instance; + return instance; + } + + // 禁止拷贝 + CronScheduler(const CronScheduler&) = delete; + CronScheduler& operator=(const CronScheduler&) = delete; + + /** + * @brief 调度一个 Cron 任务 + * + * @param expression Cron 表达式 (如 "* * * * * *") + * @param task 回调函数 + * @param delay_ms 首次启动延迟 (默认 0) + * @return std::shared_ptr 返回任务句柄。 + * 注意:即使忽略返回值,任务也会自动运行(因为 ASIO 内部持有了 + * shared_ptr)。 保留返回值是为了让你有机会调用 .stop()。 + */ + std::shared_ptr schedule( + const std::string& expression, std::function task, + std::chrono::milliseconds delay_ms = std::chrono::milliseconds(0)) { + auto job = std::make_shared(expression, std::move(task)); + job->start(delay_ms); - void prune_stopped() { std::lock_guard lock{mtx_}; - jobs_.erase(std::remove_if(jobs_.begin(), jobs_.end(), - [](const std::shared_ptr& job) - { - return !job || !job->is_running(); - }), - jobs_.end()); + jobs_.push_back(job); } + return job; + } - std::size_t job_count() const + void stop_all() { + std::vector> jobs; { std::lock_guard lock{mtx_}; - return jobs_.size(); + jobs.swap(jobs_); } + for (const auto& job : jobs) { + job->stop(); + } + } - private: - CronScheduler() = default; - mutable std::mutex mtx_; - std::vector> jobs_; - }; -} + void unschedule(const std::shared_ptr& job) { + if (!job) return; + job->stop(); + + std::lock_guard lock{mtx_}; + jobs_.erase(std::remove(jobs_.begin(), jobs_.end(), job), jobs_.end()); + } + + void prune_stopped() { + std::lock_guard lock{mtx_}; + jobs_.erase(std::remove_if(jobs_.begin(), jobs_.end(), + [](const std::shared_ptr& job) { + return !job || !job->is_running(); + }), + jobs_.end()); + } + + std::size_t job_count() const { + std::lock_guard lock{mtx_}; + return jobs_.size(); + } + + private: + CronScheduler() = default; + mutable std::mutex mtx_; + std::vector> jobs_; +}; +} // namespace khttpd::framework -#endif // KHTTPD_FRAMEWORK_CRON_SCHEDULER_HPP +#endif // KHTTPD_FRAMEWORK_CRON_SCHEDULER_HPP diff --git a/framework/cron/croncpp.hpp b/framework/cron/croncpp.hpp index 4a9e075..bb7b3bb 100644 --- a/framework/cron/croncpp.hpp +++ b/framework/cron/croncpp.hpp @@ -4,934 +4,758 @@ #ifndef KHTTPD_FRAMEWORK_CRONCPP_HPP #define KHTTPD_FRAMEWORK_CRONCPP_HPP -#include -#include -#include +#include #include #include +#include #include #include -#include -#include +#include +#include +#include #if __cplusplus > 201402L #include #define CRONCPP_IS_CPP17 #endif -namespace cron -{ +namespace cron { #ifdef CRONCPP_IS_CPP17 -#define CRONCPP_STRING_VIEW std::string_view -#define CRONCPP_STRING_VIEW_NPOS std::string_view::npos -#define CRONCPP_CONSTEXPTR constexpr +#define CRONCPP_STRING_VIEW std::string_view +#define CRONCPP_STRING_VIEW_NPOS std::string_view::npos +#define CRONCPP_CONSTEXPTR constexpr #else -#define CRONCPP_STRING_VIEW std::string const & -#define CRONCPP_STRING_VIEW_NPOS std::string::npos -#define CRONCPP_CONSTEXPTR +#define CRONCPP_STRING_VIEW std::string const& +#define CRONCPP_STRING_VIEW_NPOS std::string::npos +#define CRONCPP_CONSTEXPTR #endif - using cron_int = uint8_t; +using cron_int = uint8_t; - constexpr std::time_t INVALID_TIME = static_cast(-1); +constexpr std::time_t INVALID_TIME = static_cast(-1); - constexpr size_t INVALID_INDEX = static_cast(-1); +constexpr size_t INVALID_INDEX = static_cast(-1); - class cronexpr; +class cronexpr; - namespace detail - { - enum class cron_field - { - second, - minute, - hour_of_day, - day_of_week, - day_of_month, - month, - year - }; +namespace detail { +enum class cron_field { + second, + minute, + hour_of_day, + day_of_week, + day_of_month, + month, + year +}; - template - static bool find_next(cronexpr const& cex, - std::tm& date, - size_t const dot); - } - - struct bad_cronexpr : public std::runtime_error - { - public: - explicit bad_cronexpr(CRONCPP_STRING_VIEW message) : - std::runtime_error(message.data()) - { - } - }; +template +static bool find_next(cronexpr const& cex, std::tm& date, size_t const dot); +} // namespace detail +struct bad_cronexpr : public std::runtime_error { + public: + explicit bad_cronexpr(CRONCPP_STRING_VIEW message) + : std::runtime_error(message.data()) {} +}; - struct cron_standard_traits - { - static const cron_int CRON_MIN_SECONDS = 0; - static const cron_int CRON_MAX_SECONDS = 59; +struct cron_standard_traits { + static const cron_int CRON_MIN_SECONDS = 0; + static const cron_int CRON_MAX_SECONDS = 59; - static const cron_int CRON_MIN_MINUTES = 0; - static const cron_int CRON_MAX_MINUTES = 59; + static const cron_int CRON_MIN_MINUTES = 0; + static const cron_int CRON_MAX_MINUTES = 59; - static const cron_int CRON_MIN_HOURS = 0; - static const cron_int CRON_MAX_HOURS = 23; + static const cron_int CRON_MIN_HOURS = 0; + static const cron_int CRON_MAX_HOURS = 23; - static const cron_int CRON_MIN_DAYS_OF_WEEK = 0; - static const cron_int CRON_MAX_DAYS_OF_WEEK = 6; + static const cron_int CRON_MIN_DAYS_OF_WEEK = 0; + static const cron_int CRON_MAX_DAYS_OF_WEEK = 6; - static const cron_int CRON_MIN_DAYS_OF_MONTH = 1; - static const cron_int CRON_MAX_DAYS_OF_MONTH = 31; + static const cron_int CRON_MIN_DAYS_OF_MONTH = 1; + static const cron_int CRON_MAX_DAYS_OF_MONTH = 31; - static const cron_int CRON_MIN_MONTHS = 1; - static const cron_int CRON_MAX_MONTHS = 12; + static const cron_int CRON_MIN_MONTHS = 1; + static const cron_int CRON_MAX_MONTHS = 12; - static const cron_int CRON_MAX_YEARS_DIFF = 4; + static const cron_int CRON_MAX_YEARS_DIFF = 4; #ifdef CRONCPP_IS_CPP17 - static const inline std::vector DAYS = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; - static const inline std::vector MONTHS = { - "NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" - }; + static const inline std::vector DAYS = { + "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; + static const inline std::vector MONTHS = { + "NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", + "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; #else - static std::vector& DAYS() - { - static std::vector days = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; - return days; - } + static std::vector& DAYS() { + static std::vector days = {"SUN", "MON", "TUE", "WED", + "THU", "FRI", "SAT"}; + return days; + } - static std::vector& MONTHS() - { - static std::vector months = { - "NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" - }; - return months; - } + static std::vector& MONTHS() { + static std::vector months = {"NIL", "JAN", "FEB", "MAR", "APR", + "MAY", "JUN", "JUL", "AUG", "SEP", + "OCT", "NOV", "DEC"}; + return months; + } #endif - }; +}; - struct cron_oracle_traits - { - static const cron_int CRON_MIN_SECONDS = 0; - static const cron_int CRON_MAX_SECONDS = 59; +struct cron_oracle_traits { + static const cron_int CRON_MIN_SECONDS = 0; + static const cron_int CRON_MAX_SECONDS = 59; - static const cron_int CRON_MIN_MINUTES = 0; - static const cron_int CRON_MAX_MINUTES = 59; + static const cron_int CRON_MIN_MINUTES = 0; + static const cron_int CRON_MAX_MINUTES = 59; - static const cron_int CRON_MIN_HOURS = 0; - static const cron_int CRON_MAX_HOURS = 23; + static const cron_int CRON_MIN_HOURS = 0; + static const cron_int CRON_MAX_HOURS = 23; - static const cron_int CRON_MIN_DAYS_OF_WEEK = 1; - static const cron_int CRON_MAX_DAYS_OF_WEEK = 7; + static const cron_int CRON_MIN_DAYS_OF_WEEK = 1; + static const cron_int CRON_MAX_DAYS_OF_WEEK = 7; - static const cron_int CRON_MIN_DAYS_OF_MONTH = 1; - static const cron_int CRON_MAX_DAYS_OF_MONTH = 31; + static const cron_int CRON_MIN_DAYS_OF_MONTH = 1; + static const cron_int CRON_MAX_DAYS_OF_MONTH = 31; - static const cron_int CRON_MIN_MONTHS = 0; - static const cron_int CRON_MAX_MONTHS = 11; + static const cron_int CRON_MIN_MONTHS = 0; + static const cron_int CRON_MAX_MONTHS = 11; - static const cron_int CRON_MAX_YEARS_DIFF = 4; + static const cron_int CRON_MAX_YEARS_DIFF = 4; #ifdef CRONCPP_IS_CPP17 - static const inline std::vector DAYS = {"NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; - static const inline std::vector MONTHS = { - "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" - }; + static const inline std::vector DAYS = { + "NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; + static const inline std::vector MONTHS = { + "JAN", "FEB", "MAR", "APR", "MAY", "JUN", + "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; #else - static std::vector& DAYS() - { - static std::vector days = {"NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; - return days; - } + static std::vector& DAYS() { + static std::vector days = {"NIL", "SUN", "MON", "TUE", + "WED", "THU", "FRI", "SAT"}; + return days; + } - static std::vector& MONTHS() - { - static std::vector months = { - "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" - }; - return months; - } + static std::vector& MONTHS() { + static std::vector months = {"JAN", "FEB", "MAR", "APR", + "MAY", "JUN", "JUL", "AUG", + "SEP", "OCT", "NOV", "DEC"}; + return months; + } #endif - }; +}; - struct cron_quartz_traits - { - static const cron_int CRON_MIN_SECONDS = 0; - static const cron_int CRON_MAX_SECONDS = 59; +struct cron_quartz_traits { + static const cron_int CRON_MIN_SECONDS = 0; + static const cron_int CRON_MAX_SECONDS = 59; - static const cron_int CRON_MIN_MINUTES = 0; - static const cron_int CRON_MAX_MINUTES = 59; + static const cron_int CRON_MIN_MINUTES = 0; + static const cron_int CRON_MAX_MINUTES = 59; - static const cron_int CRON_MIN_HOURS = 0; - static const cron_int CRON_MAX_HOURS = 23; + static const cron_int CRON_MIN_HOURS = 0; + static const cron_int CRON_MAX_HOURS = 23; - static const cron_int CRON_MIN_DAYS_OF_WEEK = 1; - static const cron_int CRON_MAX_DAYS_OF_WEEK = 7; + static const cron_int CRON_MIN_DAYS_OF_WEEK = 1; + static const cron_int CRON_MAX_DAYS_OF_WEEK = 7; - static const cron_int CRON_MIN_DAYS_OF_MONTH = 1; - static const cron_int CRON_MAX_DAYS_OF_MONTH = 31; + static const cron_int CRON_MIN_DAYS_OF_MONTH = 1; + static const cron_int CRON_MAX_DAYS_OF_MONTH = 31; - static const cron_int CRON_MIN_MONTHS = 1; - static const cron_int CRON_MAX_MONTHS = 12; + static const cron_int CRON_MIN_MONTHS = 1; + static const cron_int CRON_MAX_MONTHS = 12; - static const cron_int CRON_MAX_YEARS_DIFF = 4; + static const cron_int CRON_MAX_YEARS_DIFF = 4; #ifdef CRONCPP_IS_CPP17 - static const inline std::vector DAYS = {"NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; - static const inline std::vector MONTHS = { - "NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" - }; + static const inline std::vector DAYS = { + "NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; + static const inline std::vector MONTHS = { + "NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", + "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; #else - static std::vector& DAYS() - { - static std::vector days = {"NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; - return days; - } - - static std::vector& MONTHS() - { - static std::vector months = { - "NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" - }; - return months; - } -#endif - }; - - class cronexpr; - - template - static cronexpr make_cron(CRONCPP_STRING_VIEW expr); - - class cronexpr - { - std::bitset<60> seconds; - std::bitset<60> minutes; - std::bitset<24> hours; - std::bitset<7> days_of_week; - std::bitset<31> days_of_month; - std::bitset<12> months; - std::string expr; - - friend bool operator==(cronexpr const& e1, cronexpr const& e2); - friend bool operator!=(cronexpr const& e1, cronexpr const& e2); - - template - friend bool detail::find_next(cronexpr const& cex, - std::tm& date, - size_t const dot); - - friend std::string to_cronstr(cronexpr const& cex); - friend std::string to_string(cronexpr const& cex); - - template - friend cronexpr make_cron(CRONCPP_STRING_VIEW expr); - }; - - inline bool operator==(cronexpr const& e1, cronexpr const& e2) - { - return - e1.seconds == e2.seconds && - e1.minutes == e2.minutes && - e1.hours == e2.hours && - e1.days_of_week == e2.days_of_week && - e1.days_of_month == e2.days_of_month && - e1.months == e2.months; + static std::vector& DAYS() { + static std::vector days = {"NIL", "SUN", "MON", "TUE", + "WED", "THU", "FRI", "SAT"}; + return days; } - inline bool operator!=(cronexpr const& e1, cronexpr const& e2) - { - return !(e1 == e2); + static std::vector& MONTHS() { + static std::vector months = {"NIL", "JAN", "FEB", "MAR", "APR", + "MAY", "JUN", "JUL", "AUG", "SEP", + "OCT", "NOV", "DEC"}; + return months; } +#endif +}; - inline std::string to_string(cronexpr const& cex) - { - return - cex.seconds.to_string() + " " + - cex.minutes.to_string() + " " + - cex.hours.to_string() + " " + - cex.days_of_month.to_string() + " " + - cex.months.to_string() + " " + - cex.days_of_week.to_string(); - } +class cronexpr; - inline std::string to_cronstr(cronexpr const& cex) - { - return cex.expr; - } +template +static cronexpr make_cron(CRONCPP_STRING_VIEW expr); - namespace utils - { - inline std::time_t tm_to_time(std::tm& date) - { - return std::mktime(&date); - } +class cronexpr { + std::bitset<60> seconds; + std::bitset<60> minutes; + std::bitset<24> hours; + std::bitset<7> days_of_week; + std::bitset<31> days_of_month; + std::bitset<12> months; + std::string expr; - inline std::tm* time_to_tm(std::time_t const* date, std::tm* const out) - { + friend bool operator==(cronexpr const& e1, cronexpr const& e2); + friend bool operator!=(cronexpr const& e1, cronexpr const& e2); + + template + friend bool detail::find_next(cronexpr const& cex, std::tm& date, + size_t const dot); + + friend std::string to_cronstr(cronexpr const& cex); + friend std::string to_string(cronexpr const& cex); + + template + friend cronexpr make_cron(CRONCPP_STRING_VIEW expr); +}; + +inline bool operator==(cronexpr const& e1, cronexpr const& e2) { + return e1.seconds == e2.seconds && e1.minutes == e2.minutes && + e1.hours == e2.hours && e1.days_of_week == e2.days_of_week && + e1.days_of_month == e2.days_of_month && e1.months == e2.months; +} + +inline bool operator!=(cronexpr const& e1, cronexpr const& e2) { + return !(e1 == e2); +} + +inline std::string to_string(cronexpr const& cex) { + return cex.seconds.to_string() + " " + cex.minutes.to_string() + " " + + cex.hours.to_string() + " " + cex.days_of_month.to_string() + " " + + cex.months.to_string() + " " + cex.days_of_week.to_string(); +} + +inline std::string to_cronstr(cronexpr const& cex) { return cex.expr; } + +namespace utils { +inline std::time_t tm_to_time(std::tm& date) { return std::mktime(&date); } + +inline std::tm* time_to_tm(std::time_t const* date, std::tm* const out) { #ifdef _WIN32 - errno_t err = localtime_s(out, date); - return 0 == err ? out : nullptr; + errno_t err = localtime_s(out, date); + return 0 == err ? out : nullptr; #else - return localtime_r(date, out); + return localtime_r(date, out); #endif - } +} - inline std::tm to_tm(CRONCPP_STRING_VIEW time) - { - std::tm result; +inline std::tm to_tm(CRONCPP_STRING_VIEW time) { + std::tm result; #if __cplusplus > 201103L - std::istringstream str(time.data()); - str.imbue(std::locale(setlocale(LC_ALL, nullptr))); + std::istringstream str(time.data()); + str.imbue(std::locale(setlocale(LC_ALL, nullptr))); - str >> std::get_time(&result, "%Y-%m-%d %H:%M:%S"); - if (str.fail()) throw std::runtime_error("Parsing date failed!"); + str >> std::get_time(&result, "%Y-%m-%d %H:%M:%S"); + if (str.fail()) throw std::runtime_error("Parsing date failed!"); #else - int year = 1900; - int month = 1; - int day = 1; - int hour = 0; - int minute = 0; - int second = 0; - sscanf(time.data(), "%d-%d-%d %d:%d:%d", &year, &month, &day, &hour, &minute, &second); - result.tm_year = year - 1900; - result.tm_mon = month - 1; - result.tm_mday = day; - result.tm_hour = hour; - result.tm_min = minute; - result.tm_sec = second; + int year = 1900; + int month = 1; + int day = 1; + int hour = 0; + int minute = 0; + int second = 0; + sscanf(time.data(), "%d-%d-%d %d:%d:%d", &year, &month, &day, &hour, &minute, + &second); + result.tm_year = year - 1900; + result.tm_mon = month - 1; + result.tm_mday = day; + result.tm_hour = hour; + result.tm_min = minute; + result.tm_sec = second; #endif - result.tm_isdst = -1; // DST info not available + result.tm_isdst = -1; // DST info not available - return result; - } + return result; +} - inline std::string to_string(std::tm const& tm) - { +inline std::string to_string(std::tm const& tm) { #if __cplusplus > 201103L - std::ostringstream str; - str.imbue(std::locale(setlocale(LC_ALL, nullptr))); - str << std::put_time(&tm, "%Y-%m-%d %H:%M:%S"); - if (str.fail()) throw std::runtime_error("Writing date failed!"); + std::ostringstream str; + str.imbue(std::locale(setlocale(LC_ALL, nullptr))); + str << std::put_time(&tm, "%Y-%m-%d %H:%M:%S"); + if (str.fail()) throw std::runtime_error("Writing date failed!"); - return str.str(); + return str.str(); #else - char buff[70] = {0}; - strftime(buff, sizeof(buff), "%Y-%m-%d %H:%M:%S", &tm); - return std::string(buff); + char buff[70] = {0}; + strftime(buff, sizeof(buff), "%Y-%m-%d %H:%M:%S", &tm); + return std::string(buff); #endif - } +} - inline std::string to_upper(std::string text) - { - std::transform(std::begin(text), std::end(text), - std::begin(text), [](char const c) { return static_cast(std::toupper(c)); }); +inline std::string to_upper(std::string text) { + std::transform( + std::begin(text), std::end(text), std::begin(text), + [](char const c) { return static_cast(std::toupper(c)); }); - return text; - } + return text; +} - static std::vector split(CRONCPP_STRING_VIEW text, char const delimiter) - { - std::vector tokens; - std::string token; - std::istringstream tokenStream(text.data()); - while (std::getline(tokenStream, token, delimiter)) - { - tokens.push_back(token); - } - return tokens; - } +static std::vector split(CRONCPP_STRING_VIEW text, + char const delimiter) { + std::vector tokens; + std::string token; + std::istringstream tokenStream(text.data()); + while (std::getline(tokenStream, token, delimiter)) { + tokens.push_back(token); + } + return tokens; +} - CRONCPP_CONSTEXPTR inline bool contains(CRONCPP_STRING_VIEW text, char const ch) noexcept - { - return CRONCPP_STRING_VIEW_NPOS != text.find_first_of(ch); - } +CRONCPP_CONSTEXPTR inline bool contains(CRONCPP_STRING_VIEW text, + char const ch) noexcept { + return CRONCPP_STRING_VIEW_NPOS != text.find_first_of(ch); +} +} // namespace utils + +namespace detail { +inline cron_int to_cron_int(CRONCPP_STRING_VIEW text) { + try { + return static_cast(std::stoul(text.data())); + } catch (std::exception const& ex) { + throw bad_cronexpr(ex.what()); } +} - namespace detail - { - inline cron_int to_cron_int(CRONCPP_STRING_VIEW text) - { - try - { - return static_cast(std::stoul(text.data())); - } - catch (std::exception const& ex) - { - throw bad_cronexpr(ex.what()); - } - } +static std::string replace_ordinals( + std::string text, std::vector const& replacement) { + for (size_t i = 0; i < replacement.size(); ++i) { + auto pos = text.find(replacement[i]); + if (std::string::npos != pos) text.replace(pos, 3, std::to_string(i)); + } - static std::string replace_ordinals( - std::string text, - std::vector const& replacement) - { - for (size_t i = 0; i < replacement.size(); ++i) - { - auto pos = text.find(replacement[i]); - if (std::string::npos != pos) - text.replace(pos, 3, std::to_string(i)); - } + return text; +} - return text; - } +static std::pair make_range(CRONCPP_STRING_VIEW field, + cron_int const minval, + cron_int const maxval) { + cron_int first = 0; + cron_int last = 0; + if (field.size() == 1 && field[0] == '*') { + first = minval; + last = maxval; + } else if (!utils::contains(field, '-')) { + first = to_cron_int(field); + last = first; + } else { + auto parts = utils::split(field, '-'); + if (parts.size() != 2) + throw bad_cronexpr("Specified range requires two fields"); + + first = to_cron_int(parts[0]); + last = to_cron_int(parts[1]); + } - static std::pair make_range( - CRONCPP_STRING_VIEW field, - cron_int const minval, - cron_int const maxval) - { - cron_int first = 0; - cron_int last = 0; - if (field.size() == 1 && field[0] == '*') - { - first = minval; - last = maxval; - } - else if (!utils::contains(field, '-')) - { - first = to_cron_int(field); - last = first; - } - else - { - auto parts = utils::split(field, '-'); - if (parts.size() != 2) - throw bad_cronexpr("Specified range requires two fields"); - - first = to_cron_int(parts[0]); - last = to_cron_int(parts[1]); - } + if (first > maxval || last > maxval) { + throw bad_cronexpr("Specified range exceeds maximum"); + } + if (first < minval || last < minval) { + throw bad_cronexpr("Specified range is less than minimum"); + } + if (first > last) { + throw bad_cronexpr("Specified range start exceeds range end"); + } - if (first > maxval || last > maxval) - { - throw bad_cronexpr("Specified range exceeds maximum"); - } - if (first < minval || last < minval) - { - throw bad_cronexpr("Specified range is less than minimum"); - } - if (first > last) - { - throw bad_cronexpr("Specified range start exceeds range end"); - } + return {first, last}; +} - return {first, last}; - } +template +static void set_cron_field(CRONCPP_STRING_VIEW value, std::bitset& target, + cron_int const minval, cron_int const maxval) { + if (value.length() > 0 && value[value.length() - 1] == ',') + throw bad_cronexpr("Value cannot end with comma"); - template - static void set_cron_field( - CRONCPP_STRING_VIEW value, - std::bitset& target, - cron_int const minval, - cron_int const maxval) - { - if (value.length() > 0 && value[value.length() - 1] == ',') - throw bad_cronexpr("Value cannot end with comma"); - - auto fields = utils::split(value, ','); - if (fields.empty()) - throw bad_cronexpr("Expression parsing error"); - - for (auto const& field : fields) - { - if (!utils::contains(field, '/')) - { + auto fields = utils::split(value, ','); + if (fields.empty()) throw bad_cronexpr("Expression parsing error"); + + for (auto const& field : fields) { + if (!utils::contains(field, '/')) { #ifdef CRONCPP_IS_CPP17 - auto [first, last] = detail::make_range(field, minval, maxval); + auto [first, last] = detail::make_range(field, minval, maxval); #else - auto range = detail::make_range(field, minval, maxval); - auto first = range.first; - auto last = range.second; + auto range = detail::make_range(field, minval, maxval); + auto first = range.first; + auto last = range.second; #endif - for (cron_int i = first - minval; i <= last - minval; ++i) - { - target.set(i); - } - } - else - { - auto parts = utils::split(field, '/'); - if (parts.size() != 2) - throw bad_cronexpr("Incrementer must have two fields"); + for (cron_int i = first - minval; i <= last - minval; ++i) { + target.set(i); + } + } else { + auto parts = utils::split(field, '/'); + if (parts.size() != 2) + throw bad_cronexpr("Incrementer must have two fields"); #ifdef CRONCPP_IS_CPP17 - auto [first, last] = detail::make_range(parts[0], minval, maxval); + auto [first, last] = detail::make_range(parts[0], minval, maxval); #else - auto range = detail::make_range(parts[0], minval, maxval); - auto first = range.first; - auto last = range.second; + auto range = detail::make_range(parts[0], minval, maxval); + auto first = range.first; + auto last = range.second; #endif - if (!utils::contains(parts[0], '-')) - { - last = maxval; - } + if (!utils::contains(parts[0], '-')) { + last = maxval; + } - auto delta = detail::to_cron_int(parts[1]); - if (delta <= 0) - throw bad_cronexpr("Incrementer must be a positive value"); + auto delta = detail::to_cron_int(parts[1]); + if (delta <= 0) + throw bad_cronexpr("Incrementer must be a positive value"); - for (cron_int i = first - minval; i <= last - minval; i += delta) - { - target.set(i); - } - } + for (cron_int i = first - minval; i <= last - minval; i += delta) { + target.set(i); } } + } +} - template - static void set_cron_days_of_week( - std::string value, - std::bitset<7>& target) - { - auto days = utils::to_upper(value); - auto days_replaced = detail::replace_ordinals( - days, +template +static void set_cron_days_of_week(std::string value, std::bitset<7>& target) { + auto days = utils::to_upper(value); + auto days_replaced = detail::replace_ordinals(days, #ifdef CRONCPP_IS_CPP17 - Traits::DAYS + Traits::DAYS #else - Traits::DAYS() + Traits::DAYS() #endif - ); + ); - if (days_replaced.size() == 1 && days_replaced[0] == '?') - days_replaced[0] = '*'; + if (days_replaced.size() == 1 && days_replaced[0] == '?') + days_replaced[0] = '*'; - set_cron_field( - days_replaced, - target, - Traits::CRON_MIN_DAYS_OF_WEEK, - Traits::CRON_MAX_DAYS_OF_WEEK); - } + set_cron_field(days_replaced, target, Traits::CRON_MIN_DAYS_OF_WEEK, + Traits::CRON_MAX_DAYS_OF_WEEK); +} - template - static void set_cron_days_of_month( - std::string value, - std::bitset<31>& target) - { - if (value.size() == 1 && value[0] == '?') - value[0] = '*'; - - set_cron_field( - value, - target, - Traits::CRON_MIN_DAYS_OF_MONTH, - Traits::CRON_MAX_DAYS_OF_MONTH); - } +template +static void set_cron_days_of_month(std::string value, std::bitset<31>& target) { + if (value.size() == 1 && value[0] == '?') value[0] = '*'; - template - static void set_cron_month( - std::string value, - std::bitset<12>& target) - { - auto month = utils::to_upper(value); - auto month_replaced = replace_ordinals( - month, + set_cron_field(value, target, Traits::CRON_MIN_DAYS_OF_MONTH, + Traits::CRON_MAX_DAYS_OF_MONTH); +} + +template +static void set_cron_month(std::string value, std::bitset<12>& target) { + auto month = utils::to_upper(value); + auto month_replaced = replace_ordinals(month, #ifdef CRONCPP_IS_CPP17 - Traits::MONTHS + Traits::MONTHS #else - Traits::MONTHS() + Traits::MONTHS() #endif - ); + ); - set_cron_field( - month_replaced, - target, - Traits::CRON_MIN_MONTHS, - Traits::CRON_MAX_MONTHS); - } + set_cron_field(month_replaced, target, Traits::CRON_MIN_MONTHS, + Traits::CRON_MAX_MONTHS); +} - template - inline size_t next_set_bit( - std::bitset const& target, - size_t /*minimum*/, - size_t /*maximum*/, - size_t offset) - { - for (auto i = offset; i < N; ++i) - { - if (target.test(i)) return i; - } +template +inline size_t next_set_bit(std::bitset const& target, size_t /*minimum*/, + size_t /*maximum*/, size_t offset) { + for (auto i = offset; i < N; ++i) { + if (target.test(i)) return i; + } - return INVALID_INDEX; - } + return INVALID_INDEX; +} - inline void add_to_field( - std::tm& date, - cron_field const field, - int const val) - { - switch (field) - { - case cron_field::second: - date.tm_sec += val; - break; - case cron_field::minute: - date.tm_min += val; - break; - case cron_field::hour_of_day: - date.tm_hour += val; - break; - case cron_field::day_of_week: - case cron_field::day_of_month: - date.tm_mday += val; - date.tm_isdst = -1; - break; - case cron_field::month: - date.tm_mon += val; - date.tm_isdst = -1; - break; - case cron_field::year: - date.tm_year += val; - break; - } +inline void add_to_field(std::tm& date, cron_field const field, int const val) { + switch (field) { + case cron_field::second: + date.tm_sec += val; + break; + case cron_field::minute: + date.tm_min += val; + break; + case cron_field::hour_of_day: + date.tm_hour += val; + break; + case cron_field::day_of_week: + case cron_field::day_of_month: + date.tm_mday += val; + date.tm_isdst = -1; + break; + case cron_field::month: + date.tm_mon += val; + date.tm_isdst = -1; + break; + case cron_field::year: + date.tm_year += val; + break; + } - if (INVALID_TIME == utils::tm_to_time(date)) - throw bad_cronexpr("Invalid time expression"); - } + if (INVALID_TIME == utils::tm_to_time(date)) + throw bad_cronexpr("Invalid time expression"); +} - inline void set_field( - std::tm& date, - cron_field const field, - int const val) - { - switch (field) - { - case cron_field::second: - date.tm_sec = val; - break; - case cron_field::minute: - date.tm_min = val; - break; - case cron_field::hour_of_day: - date.tm_hour = val; - break; - case cron_field::day_of_week: - date.tm_wday = val; - break; - case cron_field::day_of_month: - date.tm_mday = val; - date.tm_isdst = -1; - break; - case cron_field::month: - date.tm_mon = val; - date.tm_isdst = -1; - break; - case cron_field::year: - date.tm_year = val; - break; - } +inline void set_field(std::tm& date, cron_field const field, int const val) { + switch (field) { + case cron_field::second: + date.tm_sec = val; + break; + case cron_field::minute: + date.tm_min = val; + break; + case cron_field::hour_of_day: + date.tm_hour = val; + break; + case cron_field::day_of_week: + date.tm_wday = val; + break; + case cron_field::day_of_month: + date.tm_mday = val; + date.tm_isdst = -1; + break; + case cron_field::month: + date.tm_mon = val; + date.tm_isdst = -1; + break; + case cron_field::year: + date.tm_year = val; + break; + } - if (INVALID_TIME == utils::tm_to_time(date)) - throw bad_cronexpr("Invalid time expression"); - } + if (INVALID_TIME == utils::tm_to_time(date)) + throw bad_cronexpr("Invalid time expression"); +} - inline void reset_field( - std::tm& date, - cron_field const field) - { - switch (field) - { - case cron_field::second: - date.tm_sec = 0; - break; - case cron_field::minute: - date.tm_min = 0; - break; - case cron_field::hour_of_day: - date.tm_hour = 0; - break; - case cron_field::day_of_week: - date.tm_wday = 0; - break; - case cron_field::day_of_month: - date.tm_mday = 1; - date.tm_isdst = -1; - break; - case cron_field::month: - date.tm_mon = 0; - date.tm_isdst = -1; - break; - case cron_field::year: - date.tm_year = 0; - break; - } +inline void reset_field(std::tm& date, cron_field const field) { + switch (field) { + case cron_field::second: + date.tm_sec = 0; + break; + case cron_field::minute: + date.tm_min = 0; + break; + case cron_field::hour_of_day: + date.tm_hour = 0; + break; + case cron_field::day_of_week: + date.tm_wday = 0; + break; + case cron_field::day_of_month: + date.tm_mday = 1; + date.tm_isdst = -1; + break; + case cron_field::month: + date.tm_mon = 0; + date.tm_isdst = -1; + break; + case cron_field::year: + date.tm_year = 0; + break; + } - if (INVALID_TIME == utils::tm_to_time(date)) - throw bad_cronexpr("Invalid time expression"); - } + if (INVALID_TIME == utils::tm_to_time(date)) + throw bad_cronexpr("Invalid time expression"); +} - inline void reset_all_fields( - std::tm& date, - std::bitset<7> const& marked_fields) - { - for (size_t i = 0; i < marked_fields.size(); ++i) - { - if (marked_fields.test(i)) - reset_field(date, static_cast(i)); - } - } +inline void reset_all_fields(std::tm& date, + std::bitset<7> const& marked_fields) { + for (size_t i = 0; i < marked_fields.size(); ++i) { + if (marked_fields.test(i)) reset_field(date, static_cast(i)); + } +} - inline void mark_field( - std::bitset<7>& orders, - cron_field const field) - { - if (!orders.test(static_cast(field))) - orders.set(static_cast(field)); - } +inline void mark_field(std::bitset<7>& orders, cron_field const field) { + if (!orders.test(static_cast(field))) + orders.set(static_cast(field)); +} - template - static size_t find_next( - std::bitset const& target, - std::tm& date, - unsigned int const minimum, - unsigned int const maximum, - unsigned int const value, - cron_field const field, - cron_field const next_field, - std::bitset<7> const& marked_fields) - { - auto next_value = next_set_bit(target, minimum, maximum, value); - if (INVALID_INDEX == next_value) - { - add_to_field(date, next_field, 1); - reset_field(date, field); - next_value = next_set_bit(target, minimum, maximum, 0); - } +template +static size_t find_next(std::bitset const& target, std::tm& date, + unsigned int const minimum, unsigned int const maximum, + unsigned int const value, cron_field const field, + cron_field const next_field, + std::bitset<7> const& marked_fields) { + auto next_value = next_set_bit(target, minimum, maximum, value); + if (INVALID_INDEX == next_value) { + add_to_field(date, next_field, 1); + reset_field(date, field); + next_value = next_set_bit(target, minimum, maximum, 0); + } - if (INVALID_INDEX == next_value || next_value != value) - { - set_field(date, field, static_cast(next_value)); - reset_all_fields(date, marked_fields); - } + if (INVALID_INDEX == next_value || next_value != value) { + set_field(date, field, static_cast(next_value)); + reset_all_fields(date, marked_fields); + } - return next_value; - } + return next_value; +} - template - static size_t find_next_day( - std::tm& date, - std::bitset<31> const& days_of_month, - size_t day_of_month, - std::bitset<7> const& days_of_week, - size_t day_of_week, - std::bitset<7> const& marked_fields) - { - unsigned int count = 0; - unsigned int maximum = 366; - while ( - (!days_of_month.test(day_of_month - Traits::CRON_MIN_DAYS_OF_MONTH) || - !days_of_week.test(day_of_week - Traits::CRON_MIN_DAYS_OF_WEEK)) - && count++ < maximum) - { - add_to_field(date, cron_field::day_of_month, 1); - - day_of_month = date.tm_mday; - day_of_week = date.tm_wday; - - reset_all_fields(date, marked_fields); - } +template +static size_t find_next_day(std::tm& date, std::bitset<31> const& days_of_month, + size_t day_of_month, + std::bitset<7> const& days_of_week, + size_t day_of_week, + std::bitset<7> const& marked_fields) { + unsigned int count = 0; + unsigned int maximum = 366; + while ((!days_of_month.test(day_of_month - Traits::CRON_MIN_DAYS_OF_MONTH) || + !days_of_week.test(day_of_week - Traits::CRON_MIN_DAYS_OF_WEEK)) && + count++ < maximum) { + add_to_field(date, cron_field::day_of_month, 1); + + day_of_month = date.tm_mday; + day_of_week = date.tm_wday; + + reset_all_fields(date, marked_fields); + } - return day_of_month; - } + return day_of_month; +} - template - static bool find_next(cronexpr const& cex, - std::tm& date, - size_t const dot) - { - bool res = true; - - std::bitset<7> marked_fields{0}; - std::bitset<7> empty_list{0}; - - unsigned int second = date.tm_sec; - auto updated_second = find_next( - cex.seconds, - date, - Traits::CRON_MIN_SECONDS, - Traits::CRON_MAX_SECONDS, - second, - cron_field::second, - cron_field::minute, - empty_list); - - if (second == updated_second) - { - mark_field(marked_fields, cron_field::second); - } +template +static bool find_next(cronexpr const& cex, std::tm& date, size_t const dot) { + bool res = true; - unsigned int minute = date.tm_min; - auto update_minute = find_next( - cex.minutes, - date, - Traits::CRON_MIN_MINUTES, - Traits::CRON_MAX_MINUTES, - minute, - cron_field::minute, - cron_field::hour_of_day, - marked_fields); - if (minute == update_minute) - { - mark_field(marked_fields, cron_field::minute); - } - else - { - res = find_next(cex, date, dot); - if (!res) return res; - } + std::bitset<7> marked_fields{0}; + std::bitset<7> empty_list{0}; - unsigned int hour = date.tm_hour; - auto updated_hour = find_next( - cex.hours, - date, - Traits::CRON_MIN_HOURS, - Traits::CRON_MAX_HOURS, - hour, - cron_field::hour_of_day, - cron_field::day_of_week, - marked_fields); - if (hour == updated_hour) - { - mark_field(marked_fields, cron_field::hour_of_day); - } - else - { - res = find_next(cex, date, dot); - if (!res) return res; - } + unsigned int second = date.tm_sec; + auto updated_second = find_next( + cex.seconds, date, Traits::CRON_MIN_SECONDS, Traits::CRON_MAX_SECONDS, + second, cron_field::second, cron_field::minute, empty_list); - unsigned int day_of_week = date.tm_wday; - unsigned int day_of_month = date.tm_mday; - auto updated_day_of_month = find_next_day( - date, - cex.days_of_month, - day_of_month, - cex.days_of_week, - day_of_week, - marked_fields); - if (day_of_month == updated_day_of_month) - { - mark_field(marked_fields, cron_field::day_of_month); - } - else - { - res = find_next(cex, date, dot); - if (!res) return res; - } + if (second == updated_second) { + mark_field(marked_fields, cron_field::second); + } - unsigned int month = date.tm_mon; - auto updated_month = find_next( - cex.months, - date, - Traits::CRON_MIN_MONTHS, - Traits::CRON_MAX_MONTHS, - month, - cron_field::month, - cron_field::year, - marked_fields); - if (month != updated_month) - { - if (date.tm_year - dot > Traits::CRON_MAX_YEARS_DIFF) - return false; - - res = find_next(cex, date, dot); - if (!res) return res; - } + unsigned int minute = date.tm_min; + auto update_minute = find_next( + cex.minutes, date, Traits::CRON_MIN_MINUTES, Traits::CRON_MAX_MINUTES, + minute, cron_field::minute, cron_field::hour_of_day, marked_fields); + if (minute == update_minute) { + mark_field(marked_fields, cron_field::minute); + } else { + res = find_next(cex, date, dot); + if (!res) return res; + } - return res; - } + unsigned int hour = date.tm_hour; + auto updated_hour = find_next( + cex.hours, date, Traits::CRON_MIN_HOURS, Traits::CRON_MAX_HOURS, hour, + cron_field::hour_of_day, cron_field::day_of_week, marked_fields); + if (hour == updated_hour) { + mark_field(marked_fields, cron_field::hour_of_day); + } else { + res = find_next(cex, date, dot); + if (!res) return res; } - template - static cronexpr make_cron(CRONCPP_STRING_VIEW expr) - { - cronexpr cex; + unsigned int day_of_week = date.tm_wday; + unsigned int day_of_month = date.tm_mday; + auto updated_day_of_month = + find_next_day(date, cex.days_of_month, day_of_month, + cex.days_of_week, day_of_week, marked_fields); + if (day_of_month == updated_day_of_month) { + mark_field(marked_fields, cron_field::day_of_month); + } else { + res = find_next(cex, date, dot); + if (!res) return res; + } - if (expr.empty()) - throw bad_cronexpr("Invalid empty cron expression"); + unsigned int month = date.tm_mon; + auto updated_month = find_next( + cex.months, date, Traits::CRON_MIN_MONTHS, Traits::CRON_MAX_MONTHS, month, + cron_field::month, cron_field::year, marked_fields); + if (month != updated_month) { + if (date.tm_year - dot > Traits::CRON_MAX_YEARS_DIFF) return false; - auto fields = utils::split(expr, ' '); - fields.erase( - std::remove_if(std::begin(fields), std::end(fields), - [](CRONCPP_STRING_VIEW s) { return s.empty(); }), - std::end(fields)); - if (fields.size() != 6) - throw bad_cronexpr("cron expression must have six fields"); + res = find_next(cex, date, dot); + if (!res) return res; + } - detail::set_cron_field(fields[0], cex.seconds, Traits::CRON_MIN_SECONDS, Traits::CRON_MAX_SECONDS); - detail::set_cron_field(fields[1], cex.minutes, Traits::CRON_MIN_MINUTES, Traits::CRON_MAX_MINUTES); - detail::set_cron_field(fields[2], cex.hours, Traits::CRON_MIN_HOURS, Traits::CRON_MAX_HOURS); + return res; +} +} // namespace detail - detail::set_cron_days_of_week(fields[5], cex.days_of_week); +template +static cronexpr make_cron(CRONCPP_STRING_VIEW expr) { + cronexpr cex; - detail::set_cron_days_of_month(fields[3], cex.days_of_month); + if (expr.empty()) throw bad_cronexpr("Invalid empty cron expression"); - detail::set_cron_month(fields[4], cex.months); + auto fields = utils::split(expr, ' '); + fields.erase(std::remove_if(std::begin(fields), std::end(fields), + [](CRONCPP_STRING_VIEW s) { return s.empty(); }), + std::end(fields)); + if (fields.size() != 6) + throw bad_cronexpr("cron expression must have six fields"); - cex.expr = expr; + detail::set_cron_field(fields[0], cex.seconds, Traits::CRON_MIN_SECONDS, + Traits::CRON_MAX_SECONDS); + detail::set_cron_field(fields[1], cex.minutes, Traits::CRON_MIN_MINUTES, + Traits::CRON_MAX_MINUTES); + detail::set_cron_field(fields[2], cex.hours, Traits::CRON_MIN_HOURS, + Traits::CRON_MAX_HOURS); - return cex; - } + detail::set_cron_days_of_week(fields[5], cex.days_of_week); - template - static std::tm cron_next(cronexpr const& cex, std::tm date) - { - time_t original = utils::tm_to_time(date); - if (INVALID_TIME == original) return {}; + detail::set_cron_days_of_month(fields[3], cex.days_of_month); - if (!detail::find_next(cex, date, date.tm_year)) - return {}; + detail::set_cron_month(fields[4], cex.months); - time_t calculated = utils::tm_to_time(date); - if (INVALID_TIME == calculated) return {}; + cex.expr = expr; - if (calculated == original) - { - add_to_field(date, detail::cron_field::second, 1); - if (!detail::find_next(cex, date, date.tm_year)) - return {}; - } + return cex; +} + +template +static std::tm cron_next(cronexpr const& cex, std::tm date) { + time_t original = utils::tm_to_time(date); + if (INVALID_TIME == original) return {}; + + if (!detail::find_next(cex, date, date.tm_year)) return {}; - return date; + time_t calculated = utils::tm_to_time(date); + if (INVALID_TIME == calculated) return {}; + + if (calculated == original) { + add_to_field(date, detail::cron_field::second, 1); + if (!detail::find_next(cex, date, date.tm_year)) return {}; } - template - static std::time_t cron_next(cronexpr const& cex, std::time_t const& date) - { - std::tm val; - std::tm* dt = utils::time_to_tm(&date, &val); - if (dt == nullptr) return INVALID_TIME; + return date; +} - time_t original = utils::tm_to_time(*dt); - if (INVALID_TIME == original) return INVALID_TIME; +template +static std::time_t cron_next(cronexpr const& cex, std::time_t const& date) { + std::tm val; + std::tm* dt = utils::time_to_tm(&date, &val); + if (dt == nullptr) return INVALID_TIME; - if (!detail::find_next(cex, *dt, dt->tm_year)) - return INVALID_TIME; + time_t original = utils::tm_to_time(*dt); + if (INVALID_TIME == original) return INVALID_TIME; - time_t calculated = utils::tm_to_time(*dt); - if (INVALID_TIME == calculated) return calculated; + if (!detail::find_next(cex, *dt, dt->tm_year)) return INVALID_TIME; - if (calculated == original) - { - add_to_field(*dt, detail::cron_field::second, 1); - if (!detail::find_next(cex, *dt, dt->tm_year)) - return INVALID_TIME; - } + time_t calculated = utils::tm_to_time(*dt); + if (INVALID_TIME == calculated) return calculated; - return utils::tm_to_time(*dt); + if (calculated == original) { + add_to_field(*dt, detail::cron_field::second, 1); + if (!detail::find_next(cex, *dt, dt->tm_year)) return INVALID_TIME; } - template - static std::chrono::system_clock::time_point cron_next(cronexpr const& cex, - std::chrono::system_clock::time_point const& time_point) - { - return std::chrono::system_clock::from_time_t( + return utils::tm_to_time(*dt); +} + +template +static std::chrono::system_clock::time_point cron_next( + cronexpr const& cex, + std::chrono::system_clock::time_point const& time_point) { + return std::chrono::system_clock::from_time_t( cron_next(cex, std::chrono::system_clock::to_time_t(time_point))); - } } -#endif //KHTTPD_FRAMEWORK_CRONCPP_HPP +} // namespace cron +#endif // KHTTPD_FRAMEWORK_CRONCPP_HPP diff --git a/framework/di/di_container.hpp b/framework/di/di_container.hpp index 1430b0a..f9fe30a 100644 --- a/framework/di/di_container.hpp +++ b/framework/di/di_container.hpp @@ -4,127 +4,121 @@ #ifndef DI_CONTAINER_HPP #define DI_CONTAINER_HPP -#include // For std::shared_ptr -#include // For std::type_index -#include // For std::map -#include // For std::set -#include // For std::function -#include // For std::runtime_error -#include // For typeid(T).name() -#include // For std::mutex #include -namespace khttpd -{ - namespace framework - { - class ComponentBase - { - public: - virtual ~ComponentBase() = default; - }; +#include // For std::function +#include // For std::map +#include // For std::shared_ptr +#include // For std::mutex +#include // For std::set +#include // For std::runtime_error +#include // For typeid(T).name() +#include // For std::type_index + +namespace khttpd { +namespace framework { +class ComponentBase { + public: + virtual ~ComponentBase() = default; +}; + +class DI_Container { + public: + static DI_Container& instance() { + static DI_Container container; + return container; + } + + template + void register_component() { + const auto type_idx = std::type_index(typeid(T)); - class DI_Container - { - public: - static DI_Container& instance() - { - static DI_Container container; - return container; - } - - template - void register_component() - { - const auto type_idx = std::type_index(typeid(T)); - - std::unique_lock lock(mtx_); - if (component_factories_.count(type_idx)) - { - spdlog::warn("Component {} already registered. Overwriting.", typeid(T).name()); - } - - auto factory = [](const DI_Container& container) -> std::shared_ptr - { - return std::make_shared(container.resolve()...); - }; - - component_factories_[type_idx] = factory; - } - - template - std::shared_ptr resolve() const - { - const auto type_idx = std::type_index(typeid(T)); - - std::unique_lock lock(mtx_); - - // Check singleton cache - if (const auto it_singleton = singletons_.find(type_idx); it_singleton != singletons_.end()) - { - return std::static_pointer_cast(it_singleton->second); - } - - // Circular dependency detection - if (resolving_.count(type_idx)) - { - throw std::runtime_error("Circular dependency detected while resolving: " + std::string(typeid(T).name())); - } - - // Find factory - const auto it_factory = component_factories_.find(type_idx); - if (it_factory == component_factories_.end()) - { - throw std::runtime_error("Component not registered or dependency missing: " + std::string(typeid(T).name())); - } - - // Mark as resolving - resolving_.insert(type_idx); - - try - { - // Temporarily release lock during factory execution to avoid deadlocks - // if factories call resolve() recursively (for different types) - lock.unlock(); - - std::shared_ptr instance = std::static_pointer_cast(it_factory->second(*this)); - - lock.lock(); - resolving_.erase(type_idx); - singletons_[type_idx] = instance; - return instance; - } - catch (...) - { - // Clean up resolving set on exception - lock.lock(); - resolving_.erase(type_idx); - throw; - } - } - - void clear() - { - std::unique_lock lock(mtx_); - component_factories_.clear(); - singletons_.clear(); - resolving_.clear(); - } - - DI_Container(const DI_Container&) = delete; - DI_Container& operator=(const DI_Container&) = delete; - - private: - DI_Container() = default; - - mutable std::mutex mtx_; - std::map(const DI_Container&)>> component_factories_; - mutable std::map> singletons_; - mutable std::set resolving_; + std::unique_lock lock(mtx_); + if (component_factories_.count(type_idx)) { + spdlog::warn("Component {} already registered. Overwriting.", + typeid(T).name()); + } + + auto factory = [](const DI_Container& container) -> std::shared_ptr { + return std::make_shared(container.resolve()...); }; + + component_factories_[type_idx] = factory; + } + + template + std::shared_ptr resolve() const { + const auto type_idx = std::type_index(typeid(T)); + + std::unique_lock lock(mtx_); + + // Check singleton cache + if (const auto it_singleton = singletons_.find(type_idx); + it_singleton != singletons_.end()) { + return std::static_pointer_cast(it_singleton->second); + } + + // Circular dependency detection + if (resolving_.count(type_idx)) { + throw std::runtime_error( + "Circular dependency detected while resolving: " + + std::string(typeid(T).name())); + } + + // Find factory + const auto it_factory = component_factories_.find(type_idx); + if (it_factory == component_factories_.end()) { + throw std::runtime_error( + "Component not registered or dependency missing: " + + std::string(typeid(T).name())); + } + + // Mark as resolving + resolving_.insert(type_idx); + + try { + // Temporarily release lock during factory execution to avoid deadlocks + // if factories call resolve() recursively (for different types) + lock.unlock(); + + std::shared_ptr instance = + std::static_pointer_cast(it_factory->second(*this)); + + lock.lock(); + resolving_.erase(type_idx); + singletons_[type_idx] = instance; + return instance; + } catch (...) { + // Clean up resolving set on exception + lock.lock(); + resolving_.erase(type_idx); + throw; + } + } + + void clear() { + std::unique_lock lock(mtx_); + component_factories_.clear(); + singletons_.clear(); + resolving_.clear(); } - using DI_Container = framework::DI_Container; -} + DI_Container(const DI_Container&) = delete; + DI_Container& operator=(const DI_Container&) = delete; + + private: + DI_Container() = default; + + mutable std::mutex mtx_; + std::map(const DI_Container&)>> + component_factories_; + mutable std::map> singletons_; + mutable std::set resolving_; +}; +} // namespace framework + +using DI_Container = framework::DI_Container; +} // namespace khttpd -#endif //DI_CONTAINER_HPP +#endif // DI_CONTAINER_HPP diff --git a/framework/dto/TagInvoke.hpp b/framework/dto/TagInvoke.hpp index 201a32e..0c0b723 100644 --- a/framework/dto/TagInvoke.hpp +++ b/framework/dto/TagInvoke.hpp @@ -5,66 +5,60 @@ #ifndef BOOST_TAGINVOKE_HPP #define BOOST_TAGINVOKE_HPP -#include #include +#include #include #include -namespace boost::json -{ - namespace desc = boost::describe; - namespace mp11 = boost::mp11; +namespace boost::json { +namespace desc = boost::describe; +namespace mp11 = boost::mp11; - // ================================================================= - // 1. 通用序列化 (Struct -> JSON) - // C++17 改进: 使用 std::enable_if_t 简化语法 - // ================================================================= - template - auto tag_invoke(value_from_tag, value& jv, T const& t) - -> std::enable_if_t::value> - { - auto& obj = jv.emplace_object(); +// ================================================================= +// 1. 通用序列化 (Struct -> JSON) +// C++17 改进: 使用 std::enable_if_t 简化语法 +// ================================================================= +template +auto tag_invoke(value_from_tag, value& jv, T const& t) + -> std::enable_if_t::value> { + auto& obj = jv.emplace_object(); - using Md = desc::describe_members; + using Md = desc::describe_members; - mp11::mp_for_each([&](auto D) - { - // 使用 emplace 稍微高效一点,直接构造 value - obj.emplace(D.name, value_from(t.*D.pointer)); - }); - } + mp11::mp_for_each([&](auto D) { + // 使用 emplace 稍微高效一点,直接构造 value + obj.emplace(D.name, value_from(t.*D.pointer)); + }); +} - // ================================================================= - // 2. 通用反序列化 (JSON -> Struct) - // C++17 改进: 使用 std::enable_if_t 和 std::remove_reference_t - // ================================================================= - template - auto tag_invoke(value_to_tag, value const& jv) - -> std::enable_if_t::value, T> - { - // 只有 T 是 DefaultConstructible 时才能这样写 - T t{}; +// ================================================================= +// 2. 通用反序列化 (JSON -> Struct) +// C++17 改进: 使用 std::enable_if_t 和 std::remove_reference_t +// ================================================================= +template +auto tag_invoke(value_to_tag, value const& jv) + -> std::enable_if_t::value, T> { + // 只有 T 是 DefaultConstructible 时才能这样写 + T t{}; - // 如果 JSON 不是 object,as_object() 会抛出异常,这是符合预期的行为 - auto const& obj = jv.as_object(); + // 如果 JSON 不是 object,as_object() 会抛出异常,这是符合预期的行为 + auto const& obj = jv.as_object(); - using Md = desc::describe_members; + using Md = desc::describe_members; - mp11::mp_for_each([&](auto D) - { - // 查找 key 是否存在 - if (auto it = obj.find(D.name); it != obj.end()) - { - // C++17: 使用 remove_reference_t 简化类型获取 - using MemberT = std::remove_reference_t; + mp11::mp_for_each([&](auto D) { + // 查找 key 是否存在 + if (auto it = obj.find(D.name); it != obj.end()) { + // C++17: 使用 remove_reference_t 简化类型获取 + using MemberT = std::remove_reference_t; - // 将 json value 转换为具体的成员类型并赋值 - t.*D.pointer = value_to(it->value()); - } - }); + // 将 json value 转换为具体的成员类型并赋值 + t.*D.pointer = value_to(it->value()); + } + }); - return t; - } -} // namespace boost::json + return t; +} +} // namespace boost::json -#endif //BOOST_TAGINVOKE_HPP +#endif // BOOST_TAGINVOKE_HPP diff --git a/framework/exception/exception_handler.hpp b/framework/exception/exception_handler.hpp index b679be5..55a5cf1 100644 --- a/framework/exception/exception_handler.hpp +++ b/framework/exception/exception_handler.hpp @@ -1,100 +1,83 @@ #ifndef KHTTPD_FRAMEWORK_EXCEPTION_EXCEPTION_HANDLER_HPP_ #define KHTTPD_FRAMEWORK_EXCEPTION_EXCEPTION_HANDLER_HPP_ -#include "context/http_context.hpp" - #include #include -#include #include +#include -namespace khttpd::framework -{ - class ExceptionHandlerBase - { - public: - virtual ~ExceptionHandlerBase() = default; - - /** - * @brief Tries to handle the exception. - * @param eptr The exception pointer to handle. - * @param ctx The HTTP context. - * @return true if the exception was handled, false otherwise. - */ - virtual bool try_handle(std::exception_ptr eptr, HttpContext& ctx) - { - return false; - } - }; +#include "context/http_context.hpp" + +namespace khttpd::framework { +class ExceptionHandlerBase { + public: + virtual ~ExceptionHandlerBase() = default; /** - * @brief A helper class that dispatches exceptions to registered handlers. - * Allows registering handlers for different exception types in a single instance. + * @brief Tries to handle the exception. + * @param eptr The exception pointer to handle. + * @param ctx The HTTP context. + * @return true if the exception was handled, false otherwise. */ - class ExceptionDispatcher : public ExceptionHandlerBase - { - public: - template - void on(std::function handler) - { - handlers_.push_back([handler](std::exception_ptr eptr, HttpContext& ctx) -> bool - { - try - { - std::rethrow_exception(eptr); - } - catch (typename std::conditional::value || std::is_fundamental::value, E, const E&>::type e) - { - handler(e, ctx); - return true; - } - catch (...) - { - return false; - } - }); - } + virtual bool try_handle(std::exception_ptr eptr, HttpContext& ctx) { + return false; + } +}; + +/** + * @brief A helper class that dispatches exceptions to registered handlers. + * Allows registering handlers for different exception types in a single + * instance. + */ +class ExceptionDispatcher : public ExceptionHandlerBase { + public: + template + void on(std::function handler) { + handlers_.push_back( + [handler](std::exception_ptr eptr, HttpContext& ctx) -> bool { + try { + std::rethrow_exception(eptr); + } catch (typename std::conditional::value || + std::is_fundamental::value, + E, const E&>::type e) { + handler(e, ctx); + return true; + } catch (...) { + return false; + } + }); + } - bool try_handle(std::exception_ptr eptr, HttpContext& ctx) override - { - for (const auto& handler : handlers_) - { - if (handler(eptr, ctx)) - { - return true; - } + bool try_handle(std::exception_ptr eptr, HttpContext& ctx) override { + for (const auto& handler : handlers_) { + if (handler(eptr, ctx)) { + return true; } - return false; } + return false; + } - private: - using Delegate = std::function; - std::vector handlers_; - }; + private: + using Delegate = std::function; + std::vector handlers_; +}; - template - class ExceptionHandler : public ExceptionHandlerBase - { - public: - bool try_handle(std::exception_ptr eptr, HttpContext& ctx) override - { - try - { - std::rethrow_exception(eptr); - } - catch (const E& e) - { - handle(e, ctx); - return true; - } - catch (...) - { - return false; - } +template +class ExceptionHandler : public ExceptionHandlerBase { + public: + bool try_handle(std::exception_ptr eptr, HttpContext& ctx) override { + try { + std::rethrow_exception(eptr); + } catch (const E& e) { + handle(e, ctx); + return true; + } catch (...) { + return false; } + } - virtual void handle(const E& e, HttpContext& ctx) = 0; - }; -} + virtual void handle(const E& e, HttpContext& ctx) = 0; +}; +} // namespace khttpd::framework -#endif // KHTTPD_FRAMEWORK_EXCEPTION_EXCEPTION_HANDLER_HPP_ +#endif // KHTTPD_FRAMEWORK_EXCEPTION_EXCEPTION_HANDLER_HPP_ diff --git a/framework/exception/http_exception.hpp b/framework/exception/http_exception.hpp index d9be705..b35e8a6 100644 --- a/framework/exception/http_exception.hpp +++ b/framework/exception/http_exception.hpp @@ -1,11 +1,7 @@ #ifndef KHTTPD_FRAMEWORK_EXCEPTION_HTTP_EXCEPTION_HPP_ #define KHTTPD_FRAMEWORK_EXCEPTION_HTTP_EXCEPTION_HPP_ -#include "exception/exception_handler.hpp" -#include "router/http_result.hpp" - #include - #include #include #include @@ -14,75 +10,63 @@ #include #include -namespace khttpd::framework -{ - class HttpException : public std::runtime_error - { - public: - template - HttpException(const boost::beast::http::status status, - Body&& body, - std::string internal_message = "HTTP request failed") +#include "exception/exception_handler.hpp" +#include "router/http_result.hpp" + +namespace khttpd::framework { +class HttpException : public std::runtime_error { + public: + template + HttpException(const boost::beast::http::status status, Body&& body, + std::string internal_message = "HTTP request failed") : std::runtime_error(std::move(internal_message)), status_(status), - body_(boost::json::value_from(std::forward(body))) - { - } + body_(boost::json::value_from(std::forward(body))) {} - HttpException& header(std::string name, std::string value) - { - detail::validate_response_header(name, value); - headers_.push_back({std::move(name), std::move(value)}); - return *this; - } - - boost::beast::http::status status() const noexcept { return status_; } - const boost::json::value& body() const noexcept { return body_; } - const std::vector& headers() const noexcept { return headers_; } + HttpException& header(std::string name, std::string value) { + detail::validate_response_header(name, value); + headers_.push_back({std::move(name), std::move(value)}); + return *this; + } - void apply(HttpContext& context) const - { - context.set_status(status_); - context.set_body_json(body_); - detail::apply_headers(context, headers_); - } + boost::beast::http::status status() const noexcept { return status_; } + const boost::json::value& body() const noexcept { return body_; } + const std::vector& headers() const noexcept { return headers_; } - private: - boost::beast::http::status status_; - boost::json::value body_; - std::vector headers_; - }; + void apply(HttpContext& context) const { + context.set_status(status_); + context.set_body_json(body_); + detail::apply_headers(context, headers_); + } - namespace detail - { - template - class TypedExceptionMapper final : public ExceptionHandlerBase - { - public: - explicit TypedExceptionMapper(Mapper mapper) : mapper_(std::move(mapper)) {} + private: + boost::beast::http::status status_; + boost::json::value body_; + std::vector headers_; +}; - bool try_handle(std::exception_ptr exception, HttpContext& context) override - { - try - { - std::rethrow_exception(exception); - } - catch (const Exception& value) - { - auto response = std::invoke(mapper_, value); - apply_typed_response(context, std::move(response)); - return true; - } - catch (...) - { - return false; - } - } +namespace detail { +template +class TypedExceptionMapper final : public ExceptionHandlerBase { + public: + explicit TypedExceptionMapper(Mapper mapper) : mapper_(std::move(mapper)) {} - private: - Mapper mapper_; - }; + bool try_handle(std::exception_ptr exception, HttpContext& context) override { + try { + std::rethrow_exception(exception); + } catch (const Exception& value) { + auto response = std::invoke(mapper_, value); + apply_typed_response(context, std::move(response)); + return true; + } catch (...) { + return false; + } } -} -#endif // KHTTPD_FRAMEWORK_EXCEPTION_HTTP_EXCEPTION_HPP_ + private: + Mapper mapper_; +}; +} // namespace detail +} // namespace khttpd::framework + +#endif // KHTTPD_FRAMEWORK_EXCEPTION_HTTP_EXCEPTION_HPP_ diff --git a/framework/interceptor/interceptor.hpp b/framework/interceptor/interceptor.hpp index 7d6e124..2e9b35a 100644 --- a/framework/interceptor/interceptor.hpp +++ b/framework/interceptor/interceptor.hpp @@ -1,49 +1,45 @@ #ifndef KHTTPD_FRAMEWORK_INTERCEPTOR_INTERCEPTOR_HPP_ #define KHTTPD_FRAMEWORK_INTERCEPTOR_INTERCEPTOR_HPP_ -#include "context/http_context.hpp" #include -namespace khttpd::framework -{ - enum class InterceptorResult - { - Continue, - Stop // Intercepted, do not proceed to next interceptor or route handler - }; - - class Interceptor - { - public: - using RequestCompletion = std::function; - virtual ~Interceptor() = default; - - /** - * @brief Pre-request handler. - * @param ctx The HTTP context. - * @return InterceptorResult::Continue to proceed, InterceptorResult::Stop to stop processing. - */ - virtual InterceptorResult handle_request(HttpContext& ctx) - { - return InterceptorResult::Continue; - } - - // Override this for remote authentication or other asynchronous policy - // checks. Invoke complete exactly once; it may be invoked from any thread. - // The default preserves existing synchronous interceptors. - virtual void async_handle_request(HttpContext& ctx, RequestCompletion complete) - { - complete(handle_request(ctx)); - } - - /** - * @brief Post-response handler. - * @param ctx The HTTP context. - */ - virtual void handle_response(HttpContext& ctx) - { - } - }; -} - -#endif // KHTTPD_FRAMEWORK_INTERCEPTOR_INTERCEPTOR_HPP_ +#include "context/http_context.hpp" + +namespace khttpd::framework { +enum class InterceptorResult { + Continue, + Stop // Intercepted, do not proceed to next interceptor or route handler +}; + +class Interceptor { + public: + using RequestCompletion = std::function; + virtual ~Interceptor() = default; + + /** + * @brief Pre-request handler. + * @param ctx The HTTP context. + * @return InterceptorResult::Continue to proceed, InterceptorResult::Stop to + * stop processing. + */ + virtual InterceptorResult handle_request(HttpContext& ctx) { + return InterceptorResult::Continue; + } + + // Override this for remote authentication or other asynchronous policy + // checks. Invoke complete exactly once; it may be invoked from any thread. + // The default preserves existing synchronous interceptors. + virtual void async_handle_request(HttpContext& ctx, + RequestCompletion complete) { + complete(handle_request(ctx)); + } + + /** + * @brief Post-response handler. + * @param ctx The HTTP context. + */ + virtual void handle_response(HttpContext& ctx) {} +}; +} // namespace khttpd::framework + +#endif // KHTTPD_FRAMEWORK_INTERCEPTOR_INTERCEPTOR_HPP_ diff --git a/framework/io_context_pool.hpp b/framework/io_context_pool.hpp index 3781c70..5262658 100644 --- a/framework/io_context_pool.hpp +++ b/framework/io_context_pool.hpp @@ -1,101 +1,79 @@ #ifndef KHTTPD_FRAMEWORK_CLIENT_IO_CONTEXT_POOL_HPP #define KHTTPD_FRAMEWORK_CLIENT_IO_CONTEXT_POOL_HPP -#include +#include #include -#include -#include +#include +#include #include #include -#include -#include +#include +#include -namespace khttpd::framework -{ - class IoContextPool - { - public: - // 获取单例实例 - static IoContextPool& instance(unsigned int num_threads = 0) - { - if (num_threads == 0) - { - num_threads = std::thread::hardware_concurrency(); - if (num_threads == 0) num_threads = 1; - } - static IoContextPool instance{num_threads}; - return instance; +namespace khttpd::framework { +class IoContextPool { + public: + // 获取单例实例 + static IoContextPool& instance(unsigned int num_threads = 0) { + if (num_threads == 0) { + num_threads = std::thread::hardware_concurrency(); + if (num_threads == 0) num_threads = 1; } + static IoContextPool instance{num_threads}; + return instance; + } - // 获取共享的 io_context - boost::asio::io_context& get_io_context() - { - return ioc_; - } + // 获取共享的 io_context + boost::asio::io_context& get_io_context() { return ioc_; } - // 获取当前运行的线程数量 - size_t get_thread_count() const - { - return threads_.size(); - } + // 获取当前运行的线程数量 + size_t get_thread_count() const { return threads_.size(); } - ~IoContextPool() - { - shutdown(); - } + ~IoContextPool() { shutdown(); } - void stop() - { - // 快速路径:重置 work guard 并通知 io_context 停止 - // 不在此函数中 join 线程,避免信号处理线程中 join 自身导致崩溃 - if (!stopped_.exchange(true)) - { - work_guard_.reset(); - ioc_.stop(); - } + void stop() { + // 快速路径:重置 work guard 并通知 io_context 停止 + // 不在此函数中 join 线程,避免信号处理线程中 join 自身导致崩溃 + if (!stopped_.exchange(true)) { + work_guard_.reset(); + ioc_.stop(); } + } - // 等待所有工作线程结束(由析构函数调用,不在信号上下文中) - void shutdown() - { - if (!stopped_.exchange(true)) - { - work_guard_.reset(); - ioc_.stop(); - } + // 等待所有工作线程结束(由析构函数调用,不在信号上下文中) + void shutdown() { + if (!stopped_.exchange(true)) { + work_guard_.reset(); + ioc_.stop(); + } - // 等待线程结束 — 只在析构路径调用,不在信号处理中 - for (auto& t : threads_) - { - if (t.joinable()) - { - t.join(); - } + // 等待线程结束 — 只在析构路径调用,不在信号处理中 + for (auto& t : threads_) { + if (t.joinable()) { + t.join(); } - threads_.clear(); } + threads_.clear(); + } - private: - explicit IoContextPool(unsigned int count = std::thread::hardware_concurrency()) - : work_guard_(boost::asio::make_work_guard(ioc_)) - { - if (count <= 0) count = 1; + private: + explicit IoContextPool( + unsigned int count = std::thread::hardware_concurrency()) + : work_guard_(boost::asio::make_work_guard(ioc_)) { + if (count <= 0) count = 1; - threads_.reserve(count * 2); + threads_.reserve(count * 2); - for (unsigned int i = 0; i < count; ++i) - { - threads_.emplace_back([this]() - { - ioc_.run(); - }); - } + for (unsigned int i = 0; i < count; ++i) { + threads_.emplace_back([this]() { ioc_.run(); }); } + } - boost::asio::io_context ioc_; - boost::asio::executor_work_guard work_guard_; - std::vector threads_; - std::atomic stopped_{false}; - }; -} -#endif // KHTTPD_FRAMEWORK_CLIENT_IO_CONTEXT_POOL_HPP + boost::asio::io_context ioc_; + boost::asio::executor_work_guard + work_guard_; + std::vector threads_; + std::atomic stopped_{false}; +}; +} // namespace khttpd::framework +#endif // KHTTPD_FRAMEWORK_CLIENT_IO_CONTEXT_POOL_HPP diff --git a/framework/router/http_result.hpp b/framework/router/http_result.hpp index 066772b..62d20e7 100644 --- a/framework/router/http_result.hpp +++ b/framework/router/http_result.hpp @@ -1,11 +1,8 @@ #ifndef KHTTPD_FRAMEWORK_ROUTER_HTTP_RESULT_HPP_ #define KHTTPD_FRAMEWORK_ROUTER_HTTP_RESULT_HPP_ -#include "context/http_context.hpp" - -#include - #include +#include #include #include #include @@ -13,163 +10,146 @@ #include #include -namespace khttpd::framework -{ - struct HttpHeader - { - std::string name; - std::string value; - }; - - namespace detail - { - inline std::string ascii_lower(std::string value) - { - std::transform(value.begin(), value.end(), value.begin(), [](const unsigned char c) - { - return static_cast(std::tolower(c)); - }); - return value; - } - - inline bool is_header_token_character(const unsigned char c) - { - return std::isalnum(c) != 0 || std::string("!#$%&'*+-.^_`|~").find(static_cast(c)) != std::string::npos; - } - - inline void validate_response_header(const std::string& name, const std::string& value) - { - if (name.empty() || !std::all_of(name.begin(), name.end(), [](const unsigned char c) - { +#include "context/http_context.hpp" + +namespace khttpd::framework { +struct HttpHeader { + std::string name; + std::string value; +}; + +namespace detail { +inline std::string ascii_lower(std::string value) { + std::transform( + value.begin(), value.end(), value.begin(), + [](const unsigned char c) { return static_cast(std::tolower(c)); }); + return value; +} + +inline bool is_header_token_character(const unsigned char c) { + return std::isalnum(c) != 0 || + std::string("!#$%&'*+-.^_`|~").find(static_cast(c)) != + std::string::npos; +} + +inline void validate_response_header(const std::string& name, + const std::string& value) { + if (name.empty() || + !std::all_of(name.begin(), name.end(), [](const unsigned char c) { return is_header_token_character(c); - })) - { - throw std::invalid_argument("invalid HTTP response header name"); - } + })) { + throw std::invalid_argument("invalid HTTP response header name"); + } - if (std::any_of(value.begin(), value.end(), [](const unsigned char c) - { + if (std::any_of(value.begin(), value.end(), [](const unsigned char c) { return (c < 0x20 && c != '\t') || c == 0x7f; - })) - { - throw std::invalid_argument("invalid HTTP response header value"); - } - - const auto normalized = ascii_lower(name); - if (normalized == "content-length" || normalized == "transfer-encoding" || - normalized == "connection" || normalized == "keep-alive" || - normalized == "proxy-connection" || normalized == "te" || - normalized == "trailer" || normalized == "upgrade") - { - throw std::invalid_argument("HTTP framing and hop-by-hop headers are managed by khttpd"); - } - } - - inline void apply_headers(HttpContext& context, const std::vector& headers) - { - for (const auto& header : headers) - { - context.set_header(header.name, header.value); - } - } + })) { + throw std::invalid_argument("invalid HTTP response header value"); + } + + const auto normalized = ascii_lower(name); + if (normalized == "content-length" || normalized == "transfer-encoding" || + normalized == "connection" || normalized == "keep-alive" || + normalized == "proxy-connection" || normalized == "te" || + normalized == "trailer" || normalized == "upgrade") { + throw std::invalid_argument( + "HTTP framing and hop-by-hop headers are managed by khttpd"); + } +} + +inline void apply_headers(HttpContext& context, + const std::vector& headers) { + for (const auto& header : headers) { + context.set_header(header.name, header.value); + } +} +} // namespace detail + +template +class HttpResult { + public: + HttpResult(const boost::beast::http::status status, T body) + : status_(status), body_(std::move(body)) {} + + static HttpResult ok(T body) { + return HttpResult(boost::beast::http::status::ok, std::move(body)); + } + + static HttpResult created(T body) { + return HttpResult(boost::beast::http::status::created, std::move(body)); + } + + HttpResult& header(std::string name, std::string value) { + detail::validate_response_header(name, value); + headers_.push_back({std::move(name), std::move(value)}); + return *this; + } + + boost::beast::http::status status() const noexcept { return status_; } + const T& body() const noexcept { return body_; } + const std::vector& headers() const noexcept { return headers_; } + + private: + boost::beast::http::status status_; + T body_; + std::vector headers_; +}; + +template <> +class HttpResult { + public: + explicit HttpResult(const boost::beast::http::status status) + : status_(status) {} + + static HttpResult no_content() { + return HttpResult(boost::beast::http::status::no_content); } - template - class HttpResult - { - public: - HttpResult(const boost::beast::http::status status, T body) - : status_(status), body_(std::move(body)) - { - } - - static HttpResult ok(T body) - { - return HttpResult(boost::beast::http::status::ok, std::move(body)); - } - - static HttpResult created(T body) - { - return HttpResult(boost::beast::http::status::created, std::move(body)); - } - - HttpResult& header(std::string name, std::string value) - { - detail::validate_response_header(name, value); - headers_.push_back({std::move(name), std::move(value)}); - return *this; - } - - boost::beast::http::status status() const noexcept { return status_; } - const T& body() const noexcept { return body_; } - const std::vector& headers() const noexcept { return headers_; } - - private: - boost::beast::http::status status_; - T body_; - std::vector headers_; - }; - - template <> - class HttpResult - { - public: - explicit HttpResult(const boost::beast::http::status status) : status_(status) {} - - static HttpResult no_content() - { - return HttpResult(boost::beast::http::status::no_content); - } - - HttpResult& header(std::string name, std::string value) - { - detail::validate_response_header(name, value); - headers_.push_back({std::move(name), std::move(value)}); - return *this; - } - - boost::beast::http::status status() const noexcept { return status_; } - const std::vector& headers() const noexcept { return headers_; } - - private: - boost::beast::http::status status_; - std::vector headers_; - }; - - namespace detail - { - template - struct is_http_result : std::false_type {}; - - template - struct is_http_result> : std::true_type {}; - - template - inline constexpr bool is_http_result_v = is_http_result>::value; - - template - void apply_typed_response(HttpContext& context, const HttpResult& result) - { - context.set_status(result.status()); - context.set_body_from(result.body()); - apply_headers(context, result.headers()); - } - - inline void apply_typed_response(HttpContext& context, const HttpResult& result) - { - context.set_status(result.status()); - context.set_body(""); - context.get_response().erase(boost::beast::http::field::content_type); - apply_headers(context, result.headers()); - } - - template , int> = 0> - void apply_typed_response(HttpContext& context, T&& body) - { - context.set_status(boost::beast::http::status::ok); - context.set_body_from(std::forward(body)); - } + HttpResult& header(std::string name, std::string value) { + detail::validate_response_header(name, value); + headers_.push_back({std::move(name), std::move(value)}); + return *this; } + + boost::beast::http::status status() const noexcept { return status_; } + const std::vector& headers() const noexcept { return headers_; } + + private: + boost::beast::http::status status_; + std::vector headers_; +}; + +namespace detail { +template +struct is_http_result : std::false_type {}; + +template +struct is_http_result> : std::true_type {}; + +template +inline constexpr bool is_http_result_v = is_http_result>::value; + +template +void apply_typed_response(HttpContext& context, const HttpResult& result) { + context.set_status(result.status()); + context.set_body_from(result.body()); + apply_headers(context, result.headers()); +} + +inline void apply_typed_response(HttpContext& context, + const HttpResult& result) { + context.set_status(result.status()); + context.set_body(""); + context.get_response().erase(boost::beast::http::field::content_type); + apply_headers(context, result.headers()); +} + +template , int> = 0> +void apply_typed_response(HttpContext& context, T&& body) { + context.set_status(boost::beast::http::status::ok); + context.set_body_from(std::forward(body)); } +} // namespace detail +} // namespace khttpd::framework -#endif // KHTTPD_FRAMEWORK_ROUTER_HTTP_RESULT_HPP_ +#endif // KHTTPD_FRAMEWORK_ROUTER_HTTP_RESULT_HPP_ diff --git a/framework/router/http_router.cpp b/framework/router/http_router.cpp index 0d996d0..8beba38 100644 --- a/framework/router/http_router.cpp +++ b/framework/router/http_router.cpp @@ -1,730 +1,681 @@ // framework/router/http_router.cpp #include "http_router.hpp" -#include "sse/sse_session.hpp" + #include #include + #include #include #include #include -namespace khttpd::framework -{ - namespace - { - std::string escape_html(const std::string_view value) - { - std::string escaped; - escaped.reserve(value.size()); - for (const char character : value) - { - switch (character) - { - case '&': escaped += "&"; break; - case '<': escaped += "<"; break; - case '>': escaped += ">"; break; - case '\"': escaped += """; break; - case '\'': escaped += "'"; break; - default: escaped += character; break; - } - } - return escaped; - } +#include "sse/sse_session.hpp" - std::string make_html_error_page(const int status, const std::string_view title, - const std::string_view message, const std::string_view detail) - { - return fmt::format( - R"({0} {1} · khttpd
khttpd

HTTP {0}

{1}

{2}

{3}
)", - status, escape_html(title), escape_html(message), escape_html(detail)); - } +namespace khttpd::framework { +namespace { +std::string escape_html(const std::string_view value) { + std::string escaped; + escaped.reserve(value.size()); + for (const char character : value) { + switch (character) { + case '&': + escaped += "&"; + break; + case '<': + escaped += "<"; + break; + case '>': + escaped += ">"; + break; + case '\"': + escaped += """; + break; + case '\'': + escaped += "'"; + break; + default: + escaped += character; + break; + } + } + return escaped; +} - void write_internal_server_error(HttpContext& ctx) - { - ctx.set_status(boost::beast::http::status::internal_server_error); - boost::json::object error; - error.emplace("code", "INTERNAL_SERVER_ERROR"); - error.emplace("message", "Internal server error"); - ctx.set_body_json(error); - } +std::string make_html_error_page(const int status, const std::string_view title, + const std::string_view message, + const std::string_view detail) { + return fmt::format( + R"({0} {1} · khttpd
khttpd

HTTP {0}

{1}

{2}

{3}
)", + status, escape_html(title), escape_html(message), escape_html(detail)); +} - void reset_exception_response(HttpContext& ctx) - { - auto& response = ctx.get_response(); - response = {}; - response.version(ctx.get_request().version()); - response.keep_alive(ctx.get_request().keep_alive()); - response.set(boost::beast::http::field::server, BOOST_BEAST_VERSION_STRING); - } - } +void write_internal_server_error(HttpContext& ctx) { + ctx.set_status(boost::beast::http::status::internal_server_error); + boost::json::object error; + error.emplace("code", "INTERNAL_SERVER_ERROR"); + error.emplace("message", "Internal server error"); + ctx.set_body_json(error); +} - HttpRouter::HttpRouter() = default; +void reset_exception_response(HttpContext& ctx) { + auto& response = ctx.get_response(); + response = {}; + response.version(ctx.get_request().version()); + response.keep_alive(ctx.get_request().keep_alive()); + response.set(boost::beast::http::field::server, BOOST_BEAST_VERSION_STRING); +} +} // namespace - void HttpRouter::set_base_path(std::string path) - { - if (!path.empty() && path.front() != '/') throw std::invalid_argument("router base path must start with '/'"); - while (path.size() > 1 && path.back() == '/') path.pop_back(); - base_path_ = path == "/" ? std::string{} : std::move(path); - } +HttpRouter::HttpRouter() = default; - std::string HttpRouter::apply_base_path(const std::string& path) const - { - if (base_path_.empty()) return path; - if (path.empty() || path.front() != '/') throw std::invalid_argument("route path must start with '/'"); - return base_path_ + (path == "/" ? std::string{} : path); - } +void HttpRouter::set_base_path(std::string path) { + if (!path.empty() && path.front() != '/') + throw std::invalid_argument("router base path must start with '/'"); + while (path.size() > 1 && path.back() == '/') path.pop_back(); + base_path_ = path == "/" ? std::string{} : std::move(path); +} - std::tuple, int, int> HttpRouter::parse_path_pattern( - const std::string& path_pattern) - { - std::string regex_str = "^"; - std::vector param_names; - std::regex param_regex(R"((?::([a-zA-Z_][a-zA-Z0-9_]*)|\{([a-zA-Z_][a-zA-Z0-9_]*)\}))"); +std::string HttpRouter::apply_base_path(const std::string& path) const { + if (base_path_.empty()) return path; + if (path.empty() || path.front() != '/') + throw std::invalid_argument("route path must start with '/'"); + return base_path_ + (path == "/" ? std::string{} : path); +} - int literal_segments = 0; - int dynamic_segments = 0; +std::tuple, int, int> +HttpRouter::parse_path_pattern(const std::string& path_pattern) { + std::string regex_str = "^"; + std::vector param_names; + std::regex param_regex( + R"((?::([a-zA-Z_][a-zA-Z0-9_]*)|\{([a-zA-Z_][a-zA-Z0-9_]*)\}))"); - std::regex escape_regex(R"([\\\.\+\*\?\|\(\)\[\]\{\}\^\$])"); + int literal_segments = 0; + int dynamic_segments = 0; - auto current_pos = path_pattern.begin(); - int param_count = 0; - std::sregex_iterator end; + std::regex escape_regex(R"([\\\.\+\*\?\|\(\)\[\]\{\}\^\$])"); - for (std::sregex_iterator temp_it(path_pattern.begin(), path_pattern.end(), param_regex); temp_it != end; ++temp_it) - { - param_count++; - } + auto current_pos = path_pattern.begin(); + int param_count = 0; + std::sregex_iterator end; - int current_param_index = 0; - - auto it = std::sregex_iterator(path_pattern.begin(), path_pattern.end(), param_regex); - - while (it != end) - { - auto literal_part = std::string(current_pos, it->prefix().second); - if (!literal_part.empty()) - { - size_t start = 0; - size_t found = literal_part.find('/'); - while (found != std::string::npos) - { - if (found > start) - { - literal_segments++; - } - start = found + 1; - found = literal_part.find('/', start); - } - if (literal_part.length() > start) - { - literal_segments++; - } - } - regex_str += std::regex_replace(literal_part, escape_regex, "\\$&"); + for (std::sregex_iterator temp_it(path_pattern.begin(), path_pattern.end(), + param_regex); + temp_it != end; ++temp_it) { + param_count++; + } - param_names.push_back((*it)[1].matched ? (*it)[1].str() : (*it)[2].str()); - dynamic_segments++; + int current_param_index = 0; - if (current_param_index == param_count - 1) - { - regex_str += "(.*)"; - } - else - { - regex_str += "([^/]+)"; - } - current_param_index++; + auto it = std::sregex_iterator(path_pattern.begin(), path_pattern.end(), + param_regex); - current_pos = it->suffix().first; - ++it; - } - auto tail_literal_part = std::string(current_pos, path_pattern.end()); - if (!tail_literal_part.empty()) - { + while (it != end) { + auto literal_part = std::string(current_pos, it->prefix().second); + if (!literal_part.empty()) { size_t start = 0; - size_t found = tail_literal_part.find('/'); - while (found != std::string::npos) - { - if (found > start) - { + size_t found = literal_part.find('/'); + while (found != std::string::npos) { + if (found > start) { literal_segments++; } start = found + 1; - found = tail_literal_part.find('/', start); + found = literal_part.find('/', start); } - if (tail_literal_part.length() > start) - { + if (literal_part.length() > start) { literal_segments++; } } - regex_str += std::regex_replace(tail_literal_part, escape_regex, "\\$&"); - regex_str += "$"; + regex_str += std::regex_replace(literal_part, escape_regex, "\\$&"); - return {std::regex(regex_str), param_names, literal_segments, dynamic_segments}; - } + param_names.push_back((*it)[1].matched ? (*it)[1].str() : (*it)[2].str()); + dynamic_segments++; - void HttpRouter::add_route(const std::string& path_pattern, const boost::beast::http::verb method, - HttpHandler handler, - std::optional request_schema, - std::optional response_schema, - const bool documented, - RouteDocumentation documentation, - std::vector parameters) - { - const auto registered_path = apply_base_path(path_pattern); - if (documented) - record_route_descriptor(registered_path, method, std::move(request_schema), std::move(response_schema), - std::move(documentation), std::move(parameters)); - else - route_descriptors_.erase(std::remove_if(route_descriptors_.begin(), route_descriptors_.end(), - [&](const RouteDescriptor& descriptor) - { - return descriptor.path == registered_path && descriptor.method == method; - }), route_descriptors_.end()); - - for (auto& entry : routes_) - { - if (entry.original_path == registered_path) - { - entry.handlers[method] = std::move(handler); - spdlog::debug("Updated handler for route: {} {}", std::string(boost::beast::http::to_string(method)), - registered_path); - return; - } + if (current_param_index == param_count - 1) { + regex_str += "(.*)"; + } else { + regex_str += "([^/]+)"; } + current_param_index++; - RouteEntry new_entry; - new_entry.original_path = registered_path; - auto [regex, params, literal_count, dynamic_count] = parse_path_pattern(registered_path); - new_entry.path_regex = std::move(regex); - new_entry.param_names = std::move(params); - new_entry.literal_segments_count = literal_count; - new_entry.dynamic_segments_count = dynamic_count; - new_entry.handlers[method] = std::move(handler); - - routes_.push_back(std::move(new_entry)); - std::sort(routes_.begin(), routes_.end(), RouteEntry::compare_specificity); - spdlog::debug("Registered dynamic route: {} {} (literal:{}, dynamic:{})", - std::string(boost::beast::http::to_string(method)), registered_path, literal_count, dynamic_count); + current_pos = it->suffix().first; + ++it; } - - void HttpRouter::add_typed_route(const std::string& path_pattern, - const boost::beast::http::verb method, - detail::TypedRouteHandler handler, - RouteDocumentation documentation) - { - const auto parsed = parse_path_pattern(path_pattern); - const auto& path_parameter_names = std::get<1>(parsed); - for (std::size_t index = 0; index < handler.parameters.size(); ++index) - { - const auto& parameter = handler.parameters[index]; - if (parameter.name.empty()) - throw std::invalid_argument("Typed route parameter names cannot be empty"); - if (parameter.location == RouteParameterLocation::path && - std::find(path_parameter_names.begin(), path_parameter_names.end(), parameter.name) == - path_parameter_names.end()) - throw std::invalid_argument("Path parameter '" + parameter.name + - "' is not present in route " + path_pattern); - for (std::size_t previous = 0; previous < index; ++previous) - { - if (handler.parameters[previous].location == parameter.location && - handler.parameters[previous].name == parameter.name) - throw std::invalid_argument("Duplicate typed route parameter '" + parameter.name + "'"); + auto tail_literal_part = std::string(current_pos, path_pattern.end()); + if (!tail_literal_part.empty()) { + size_t start = 0; + size_t found = tail_literal_part.find('/'); + while (found != std::string::npos) { + if (found > start) { + literal_segments++; } + start = found + 1; + found = tail_literal_part.find('/', start); + } + if (tail_literal_part.length() > start) { + literal_segments++; } - - add_route(path_pattern, method, std::move(handler.handler), - std::move(handler.request_schema), std::move(handler.response_schema), true, - std::move(documentation), std::move(handler.parameters)); } + regex_str += std::regex_replace(tail_literal_part, escape_regex, "\\$&"); + regex_str += "$"; - void HttpRouter::record_route_descriptor(const std::string& path, - const boost::beast::http::verb method, - std::optional request_schema, - std::optional response_schema, - RouteDocumentation documentation, - std::vector parameters) - { - if (documentation.request_schema) request_schema = documentation.request_schema; - if (documentation.response_schema) response_schema = documentation.response_schema; - for (auto& descriptor : route_descriptors_) - { - if (descriptor.path == path && descriptor.method == method) - { - descriptor.request_schema = std::move(request_schema); - descriptor.response_schema = std::move(response_schema); - descriptor.parameters = std::move(parameters); - if (!documentation.summary.empty() || !documentation.description.empty() || - !documentation.headers.empty() || documentation.request_schema || documentation.response_schema) - descriptor.documentation = std::move(documentation); - return; - } + return {std::regex(regex_str), param_names, literal_segments, + dynamic_segments}; +} + +void HttpRouter::add_route( + const std::string& path_pattern, const boost::beast::http::verb method, + HttpHandler handler, std::optional request_schema, + std::optional response_schema, const bool documented, + RouteDocumentation documentation, + std::vector parameters) { + const auto registered_path = apply_base_path(path_pattern); + if (documented) + record_route_descriptor(registered_path, method, std::move(request_schema), + std::move(response_schema), + std::move(documentation), std::move(parameters)); + else + route_descriptors_.erase( + std::remove_if(route_descriptors_.begin(), route_descriptors_.end(), + [&](const RouteDescriptor& descriptor) { + return descriptor.path == registered_path && + descriptor.method == method; + }), + route_descriptors_.end()); + + for (auto& entry : routes_) { + if (entry.original_path == registered_path) { + entry.handlers[method] = std::move(handler); + spdlog::debug("Updated handler for route: {} {}", + std::string(boost::beast::http::to_string(method)), + registered_path); + return; } - route_descriptors_.push_back( - {path, method, std::move(request_schema), std::move(response_schema), std::move(documentation), - std::move(parameters)}); } - std::vector HttpRouter::route_descriptors() const - { - return route_descriptors_; - } + RouteEntry new_entry; + new_entry.original_path = registered_path; + auto [regex, params, literal_count, dynamic_count] = + parse_path_pattern(registered_path); + new_entry.path_regex = std::move(regex); + new_entry.param_names = std::move(params); + new_entry.literal_segments_count = literal_count; + new_entry.dynamic_segments_count = dynamic_count; + new_entry.handlers[method] = std::move(handler); - void HttpRouter::document_route(const std::string& path, const boost::beast::http::verb method, - RouteDocumentation documentation) - { - for (auto& descriptor : route_descriptors_) - { - if (descriptor.path == path && descriptor.method == method) - { + routes_.push_back(std::move(new_entry)); + std::sort(routes_.begin(), routes_.end(), RouteEntry::compare_specificity); + spdlog::debug("Registered dynamic route: {} {} (literal:{}, dynamic:{})", + std::string(boost::beast::http::to_string(method)), + registered_path, literal_count, dynamic_count); +} + +void HttpRouter::add_typed_route(const std::string& path_pattern, + const boost::beast::http::verb method, + detail::TypedRouteHandler handler, + RouteDocumentation documentation) { + const auto parsed = parse_path_pattern(path_pattern); + const auto& path_parameter_names = std::get<1>(parsed); + for (std::size_t index = 0; index < handler.parameters.size(); ++index) { + const auto& parameter = handler.parameters[index]; + if (parameter.name.empty()) + throw std::invalid_argument( + "Typed route parameter names cannot be empty"); + if (parameter.location == RouteParameterLocation::path && + std::find(path_parameter_names.begin(), path_parameter_names.end(), + parameter.name) == path_parameter_names.end()) + throw std::invalid_argument("Path parameter '" + parameter.name + + "' is not present in route " + path_pattern); + for (std::size_t previous = 0; previous < index; ++previous) { + if (handler.parameters[previous].location == parameter.location && + handler.parameters[previous].name == parameter.name) + throw std::invalid_argument("Duplicate typed route parameter '" + + parameter.name + "'"); + } + } + + add_route(path_pattern, method, std::move(handler.handler), + std::move(handler.request_schema), + std::move(handler.response_schema), true, std::move(documentation), + std::move(handler.parameters)); +} + +void HttpRouter::record_route_descriptor( + const std::string& path, const boost::beast::http::verb method, + std::optional request_schema, + std::optional response_schema, + RouteDocumentation documentation, + std::vector parameters) { + if (documentation.request_schema) + request_schema = documentation.request_schema; + if (documentation.response_schema) + response_schema = documentation.response_schema; + for (auto& descriptor : route_descriptors_) { + if (descriptor.path == path && descriptor.method == method) { + descriptor.request_schema = std::move(request_schema); + descriptor.response_schema = std::move(response_schema); + descriptor.parameters = std::move(parameters); + if (!documentation.summary.empty() || + !documentation.description.empty() || + !documentation.headers.empty() || documentation.request_schema || + documentation.response_schema) descriptor.documentation = std::move(documentation); - return; - } + return; } - throw std::invalid_argument("Cannot document an unregistered route: " + path); } + route_descriptors_.push_back( + {path, method, std::move(request_schema), std::move(response_schema), + std::move(documentation), std::move(parameters)}); +} - void HttpRouter::get(const std::string& path, HttpHandler handler) - { - add_route(path, boost::beast::http::verb::get, std::move(handler)); - } +std::vector HttpRouter::route_descriptors() const { + return route_descriptors_; +} - void HttpRouter::get(const std::string& path, HttpHandler handler, RouteDocumentation documentation) - { - add_route(path, boost::beast::http::verb::get, std::move(handler), std::nullopt, std::nullopt, true, - std::move(documentation)); +void HttpRouter::document_route(const std::string& path, + const boost::beast::http::verb method, + RouteDocumentation documentation) { + for (auto& descriptor : route_descriptors_) { + if (descriptor.path == path && descriptor.method == method) { + descriptor.documentation = std::move(documentation); + return; + } } + throw std::invalid_argument("Cannot document an unregistered route: " + path); +} - void HttpRouter::post(const std::string& path, HttpHandler handler) - { - add_route(path, boost::beast::http::verb::post, std::move(handler)); - } +void HttpRouter::get(const std::string& path, HttpHandler handler) { + add_route(path, boost::beast::http::verb::get, std::move(handler)); +} - void HttpRouter::post(const std::string& path, HttpHandler handler, RouteDocumentation documentation) - { - add_route(path, boost::beast::http::verb::post, std::move(handler), std::nullopt, std::nullopt, true, - std::move(documentation)); - } +void HttpRouter::get(const std::string& path, HttpHandler handler, + RouteDocumentation documentation) { + add_route(path, boost::beast::http::verb::get, std::move(handler), + std::nullopt, std::nullopt, true, std::move(documentation)); +} - void HttpRouter::put(const std::string& path, HttpHandler handler) - { - add_route(path, boost::beast::http::verb::put, std::move(handler)); - } +void HttpRouter::post(const std::string& path, HttpHandler handler) { + add_route(path, boost::beast::http::verb::post, std::move(handler)); +} - void HttpRouter::put(const std::string& path, HttpHandler handler, RouteDocumentation documentation) - { - add_route(path, boost::beast::http::verb::put, std::move(handler), std::nullopt, std::nullopt, true, - std::move(documentation)); - } +void HttpRouter::post(const std::string& path, HttpHandler handler, + RouteDocumentation documentation) { + add_route(path, boost::beast::http::verb::post, std::move(handler), + std::nullopt, std::nullopt, true, std::move(documentation)); +} - void HttpRouter::del(const std::string& path, HttpHandler handler) - { - add_route(path, boost::beast::http::verb::delete_, std::move(handler)); - } +void HttpRouter::put(const std::string& path, HttpHandler handler) { + add_route(path, boost::beast::http::verb::put, std::move(handler)); +} - void HttpRouter::del(const std::string& path, HttpHandler handler, RouteDocumentation documentation) - { - add_route(path, boost::beast::http::verb::delete_, std::move(handler), std::nullopt, std::nullopt, true, - std::move(documentation)); - } +void HttpRouter::put(const std::string& path, HttpHandler handler, + RouteDocumentation documentation) { + add_route(path, boost::beast::http::verb::put, std::move(handler), + std::nullopt, std::nullopt, true, std::move(documentation)); +} - void HttpRouter::options(const std::string& path, HttpHandler handler) - { - add_route(path, boost::beast::http::verb::options, std::move(handler)); - } +void HttpRouter::del(const std::string& path, HttpHandler handler) { + add_route(path, boost::beast::http::verb::delete_, std::move(handler)); +} - void HttpRouter::options(const std::string& path, HttpHandler handler, RouteDocumentation documentation) - { - add_route(path, boost::beast::http::verb::options, std::move(handler), std::nullopt, std::nullopt, true, - std::move(documentation)); - } +void HttpRouter::del(const std::string& path, HttpHandler handler, + RouteDocumentation documentation) { + add_route(path, boost::beast::http::verb::delete_, std::move(handler), + std::nullopt, std::nullopt, true, std::move(documentation)); +} - void HttpRouter::stream(const std::string& path_pattern, const boost::beast::http::verb method, - HttpStreamHandler handler) - { - const auto registered_path = apply_base_path(path_pattern); - record_route_descriptor(registered_path, method); - for (auto& entry : routes_) - { - if (entry.original_path == registered_path) - { - entry.stream_handlers[method] = std::move(handler); - return; - } - } - RouteEntry entry; - entry.original_path = registered_path; - auto [regex, params, literal_count, dynamic_count] = parse_path_pattern(registered_path); - entry.path_regex = std::move(regex); - entry.param_names = std::move(params); - entry.literal_segments_count = literal_count; - entry.dynamic_segments_count = dynamic_count; - entry.stream_handlers[method] = std::move(handler); - routes_.push_back(std::move(entry)); - std::sort(routes_.begin(), routes_.end(), RouteEntry::compare_specificity); - } +void HttpRouter::options(const std::string& path, HttpHandler handler) { + add_route(path, boost::beast::http::verb::options, std::move(handler)); +} - bool HttpRouter::is_stream_route(const std::string& path, const boost::beast::http::verb method) const - { - for (const auto& entry : routes_) - { - if (!std::regex_match(path, entry.path_regex)) continue; - return entry.stream_handlers.find(method) != entry.stream_handlers.end(); +void HttpRouter::options(const std::string& path, HttpHandler handler, + RouteDocumentation documentation) { + add_route(path, boost::beast::http::verb::options, std::move(handler), + std::nullopt, std::nullopt, true, std::move(documentation)); +} + +void HttpRouter::stream(const std::string& path_pattern, + const boost::beast::http::verb method, + HttpStreamHandler handler) { + const auto registered_path = apply_base_path(path_pattern); + record_route_descriptor(registered_path, method); + for (auto& entry : routes_) { + if (entry.original_path == registered_path) { + entry.stream_handlers[method] = std::move(handler); + return; } - return false; } + RouteEntry entry; + entry.original_path = registered_path; + auto [regex, params, literal_count, dynamic_count] = + parse_path_pattern(registered_path); + entry.path_regex = std::move(regex); + entry.param_names = std::move(params); + entry.literal_segments_count = literal_count; + entry.dynamic_segments_count = dynamic_count; + entry.stream_handlers[method] = std::move(handler); + routes_.push_back(std::move(entry)); + std::sort(routes_.begin(), routes_.end(), RouteEntry::compare_specificity); +} - void HttpRouter::sse(const std::string& path, SseHandler handler, const std::size_t max_pending_bytes) - { - stream(path, boost::beast::http::verb::get, - [handler = std::move(handler), max_pending_bytes](HttpContext& ctx, std::shared_ptr, - std::shared_ptr response, HttpStreamComplete) - { - const auto content_length = ctx.get_request()[boost::beast::http::field::content_length]; - if (ctx.get_request().chunked() || (!content_length.empty() && content_length != "0")) - response->cancel_request_body(); - auto session = std::make_shared(std::move(response), ctx.get_request().version(), - ctx.get_request().keep_alive(), max_pending_bytes); - handler(ctx, session); - session->start(); - }); +bool HttpRouter::is_stream_route(const std::string& path, + const boost::beast::http::verb method) const { + for (const auto& entry : routes_) { + if (!std::regex_match(path, entry.path_regex)) continue; + return entry.stream_handlers.find(method) != entry.stream_handlers.end(); } + return false; +} - bool HttpRouter::dispatch_stream(HttpContext& ctx, std::shared_ptr stream, - std::shared_ptr response_stream, - HttpStreamComplete complete) const - { - for (const auto& entry : routes_) - { - std::smatch matches; - if (!std::regex_match(ctx.path(), matches, entry.path_regex)) continue; - const auto handler = entry.stream_handlers.find(ctx.method()); - if (handler == entry.stream_handlers.end()) return false; - std::map params; - for (size_t i = 0; i < entry.param_names.size() && i + 1 < matches.size(); ++i) - params[entry.param_names[i]] = matches[i + 1].str(); - ctx.set_path_params(std::move(params)); - handler->second(ctx, std::move(stream), std::move(response_stream), std::move(complete)); - return true; - } - return false; - } +void HttpRouter::sse(const std::string& path, SseHandler handler, + const std::size_t max_pending_bytes) { + stream(path, boost::beast::http::verb::get, + [handler = std::move(handler), max_pending_bytes]( + HttpContext& ctx, std::shared_ptr, + std::shared_ptr response, HttpStreamComplete) { + const auto content_length = + ctx.get_request()[boost::beast::http::field::content_length]; + if (ctx.get_request().chunked() || + (!content_length.empty() && content_length != "0")) + response->cancel_request_body(); + auto session = std::make_shared( + std::move(response), ctx.get_request().version(), + ctx.get_request().keep_alive(), max_pending_bytes); + handler(ctx, session); + session->start(); + }); +} - void HttpRouter::add_interceptor(std::shared_ptr interceptor) - { - interceptors_.push_back(std::move(interceptor)); +bool HttpRouter::dispatch_stream( + HttpContext& ctx, std::shared_ptr stream, + std::shared_ptr response_stream, + HttpStreamComplete complete) const { + for (const auto& entry : routes_) { + std::smatch matches; + if (!std::regex_match(ctx.path(), matches, entry.path_regex)) continue; + const auto handler = entry.stream_handlers.find(ctx.method()); + if (handler == entry.stream_handlers.end()) return false; + std::map params; + for (size_t i = 0; i < entry.param_names.size() && i + 1 < matches.size(); + ++i) + params[entry.param_names[i]] = matches[i + 1].str(); + ctx.set_path_params(std::move(params)); + handler->second(ctx, std::move(stream), std::move(response_stream), + std::move(complete)); + return true; } + return false; +} - void HttpRouter::async_run_pre_interceptors(HttpContext& ctx, InterceptorCompletion complete) const - { - struct State : std::enable_shared_from_this - { - const HttpRouter* router; - const std::vector>* interceptors; - HttpContext* ctx; - InterceptorCompletion complete; - std::size_t index = 0; - - void advance(InterceptorResult result) - { - if (result == InterceptorResult::Stop || index == interceptors->size()) - return complete(result); - auto interceptor = (*interceptors)[index++]; - try - { - interceptor->async_handle_request(*ctx, [self = shared_from_this()](InterceptorResult next_result) - { self->advance(next_result); }); - } - catch (...) - { - router->handle_exception(std::current_exception(), *ctx); - complete(InterceptorResult::Stop); - } - } - }; - auto state = std::make_shared(); - state->router = this; - state->interceptors = &interceptors_; - state->ctx = &ctx; - state->complete = std::move(complete); - state->advance(InterceptorResult::Continue); - } +void HttpRouter::add_interceptor(std::shared_ptr interceptor) { + interceptors_.push_back(std::move(interceptor)); +} - void HttpRouter::async_route(const std::string& path, boost::beast::http::verb method, - HttpAsyncHandler handler) - { - const auto registered_path = apply_base_path(path); - record_route_descriptor(registered_path, method); - auto [path_regex, param_names, literal_count, dynamic_count] = parse_path_pattern(registered_path); - for (auto& entry : routes_) - { - if (entry.original_path == registered_path) - { - entry.async_handlers[method] = std::move(handler); - return; +void HttpRouter::async_run_pre_interceptors( + HttpContext& ctx, InterceptorCompletion complete) const { + struct State : std::enable_shared_from_this { + const HttpRouter* router; + const std::vector>* interceptors; + HttpContext* ctx; + InterceptorCompletion complete; + std::size_t index = 0; + + void advance(InterceptorResult result) { + if (result == InterceptorResult::Stop || index == interceptors->size()) + return complete(result); + auto interceptor = (*interceptors)[index++]; + try { + interceptor->async_handle_request( + *ctx, [self = shared_from_this()](InterceptorResult next_result) { + self->advance(next_result); + }); + } catch (...) { + router->handle_exception(std::current_exception(), *ctx); + complete(InterceptorResult::Stop); } } - RouteEntry entry; - entry.original_path = registered_path; - entry.path_regex = std::move(path_regex); - entry.param_names = std::move(param_names); - entry.literal_segments_count = literal_count; - entry.dynamic_segments_count = dynamic_count; - entry.async_handlers[method] = std::move(handler); - routes_.push_back(std::move(entry)); - std::sort(routes_.begin(), routes_.end(), RouteEntry::compare_specificity); - } + }; + auto state = std::make_shared(); + state->router = this; + state->interceptors = &interceptors_; + state->ctx = &ctx; + state->complete = std::move(complete); + state->advance(InterceptorResult::Continue); +} - bool HttpRouter::dispatch_async(HttpContext& ctx, HttpAsyncComplete complete) const - { - for (const auto& entry : routes_) - { - std::smatch matches; - if (!std::regex_match(ctx.path(), matches, entry.path_regex)) continue; - auto handler = entry.async_handlers.find(ctx.method()); - if (handler == entry.async_handlers.end() && ctx.method() == boost::beast::http::verb::head) - handler = entry.async_handlers.find(boost::beast::http::verb::get); - if (handler == entry.async_handlers.end()) return false; - std::map params; - for (size_t i = 0; i < entry.param_names.size() && i + 1 < matches.size(); ++i) - params[entry.param_names[i]] = matches[i + 1].str(); - ctx.set_path_params(std::move(params)); - handler->second(ctx, std::move(complete)); - return true; +void HttpRouter::async_route(const std::string& path, + boost::beast::http::verb method, + HttpAsyncHandler handler) { + const auto registered_path = apply_base_path(path); + record_route_descriptor(registered_path, method); + auto [path_regex, param_names, literal_count, dynamic_count] = + parse_path_pattern(registered_path); + for (auto& entry : routes_) { + if (entry.original_path == registered_path) { + entry.async_handlers[method] = std::move(handler); + return; } - return false; } + RouteEntry entry; + entry.original_path = registered_path; + entry.path_regex = std::move(path_regex); + entry.param_names = std::move(param_names); + entry.literal_segments_count = literal_count; + entry.dynamic_segments_count = dynamic_count; + entry.async_handlers[method] = std::move(handler); + routes_.push_back(std::move(entry)); + std::sort(routes_.begin(), routes_.end(), RouteEntry::compare_specificity); +} - InterceptorResult HttpRouter::run_pre_interceptors(HttpContext& ctx) const - { - for (const auto& interceptor : interceptors_) - { - if (interceptor->handle_request(ctx) == InterceptorResult::Stop) - { - return InterceptorResult::Stop; - } - } - return InterceptorResult::Continue; +bool HttpRouter::dispatch_async(HttpContext& ctx, + HttpAsyncComplete complete) const { + for (const auto& entry : routes_) { + std::smatch matches; + if (!std::regex_match(ctx.path(), matches, entry.path_regex)) continue; + auto handler = entry.async_handlers.find(ctx.method()); + if (handler == entry.async_handlers.end() && + ctx.method() == boost::beast::http::verb::head) + handler = entry.async_handlers.find(boost::beast::http::verb::get); + if (handler == entry.async_handlers.end()) return false; + std::map params; + for (size_t i = 0; i < entry.param_names.size() && i + 1 < matches.size(); + ++i) + params[entry.param_names[i]] = matches[i + 1].str(); + ctx.set_path_params(std::move(params)); + handler->second(ctx, std::move(complete)); + return true; } + return false; +} - void HttpRouter::run_post_interceptors(HttpContext& ctx) const - { - for (auto it = interceptors_.rbegin(); it != interceptors_.rend(); ++it) - { - (*it)->handle_response(ctx); +InterceptorResult HttpRouter::run_pre_interceptors(HttpContext& ctx) const { + for (const auto& interceptor : interceptors_) { + if (interceptor->handle_request(ctx) == InterceptorResult::Stop) { + return InterceptorResult::Stop; } } + return InterceptorResult::Continue; +} - bool HttpRouter::dispatch(HttpContext& ctx, const std::function& static_file_fun) const - { - const std::string request_path = ctx.path(); - const boost::beast::http::verb request_method = ctx.method(); - - for (const auto& entry : routes_) - { - if (std::smatch matches; std::regex_match(request_path, matches, entry.path_regex)) - { - auto method_it = entry.handlers.find(request_method); - if (method_it == entry.handlers.end() && request_method == boost::beast::http::verb::head) - { - method_it = entry.handlers.find(boost::beast::http::verb::get); - } - if (method_it != entry.handlers.end()) - { - std::map path_params; - for (size_t i = 0; i < entry.param_names.size(); ++i) - { - if (i + 1 < matches.size()) - { - path_params[entry.param_names[i]] = matches[i + 1].str(); - } - } - ctx.set_path_params(std::move(path_params)); +void HttpRouter::run_post_interceptors(HttpContext& ctx) const { + for (auto it = interceptors_.rbegin(); it != interceptors_.rend(); ++it) { + (*it)->handle_response(ctx); + } +} - try - { - method_it->second(ctx); - } - catch (...) - { - handle_exception(std::current_exception(), ctx); +bool HttpRouter::dispatch(HttpContext& ctx, + const std::function& static_file_fun) const { + const std::string request_path = ctx.path(); + const boost::beast::http::verb request_method = ctx.method(); + + for (const auto& entry : routes_) { + if (std::smatch matches; + std::regex_match(request_path, matches, entry.path_regex)) { + auto method_it = entry.handlers.find(request_method); + if (method_it == entry.handlers.end() && + request_method == boost::beast::http::verb::head) { + method_it = entry.handlers.find(boost::beast::http::verb::get); + } + if (method_it != entry.handlers.end()) { + std::map path_params; + for (size_t i = 0; i < entry.param_names.size(); ++i) { + if (i + 1 < matches.size()) { + path_params[entry.param_names[i]] = matches[i + 1].str(); } - return true; } - if (request_method != boost::beast::http::verb::get && request_method != boost::beast::http::verb::head) - { - handle_method_not_allowed(ctx, entry.handlers); // 传递允许的方法映射 - return true; + ctx.set_path_params(std::move(path_params)); + + try { + method_it->second(ctx); + } catch (...) { + handle_exception(std::current_exception(), ctx); } + return true; + } + if (request_method != boost::beast::http::verb::get && + request_method != boost::beast::http::verb::head) { + handle_method_not_allowed(ctx, entry.handlers); // 传递允许的方法映射 + return true; } } - - if (!static_file_fun || !static_file_fun()) - { - handle_not_found(ctx); - return false; - } - return true; } - void HttpRouter::handle_not_found(HttpContext& ctx) const - { - ctx.set_status(boost::beast::http::status::not_found); - spdlog::warn("404 Not Found: {}", ctx.path()); - if (not_found_handler_) - { - not_found_handler_(ctx); - return; - } - ctx.set_content_type("text/html"); - ctx.set_body(make_html_error_page(404, "Page not found", "The requested resource could not be found.", - ctx.path())); + if (!static_file_fun || !static_file_fun()) { + handle_not_found(ctx); + return false; } + return true; +} - void HttpRouter::handle_method_not_allowed( - HttpContext& ctx, const std::map& allowed_methods) const - { - ctx.set_status(boost::beast::http::status::method_not_allowed); - - std::string allowed_methods_str; - bool first = true; - for (const auto& pair : allowed_methods) - { - if (!first) allowed_methods_str += ", "; - allowed_methods_str += boost::beast::http::to_string(pair.first); - first = false; - } - ctx.set_header(boost::beast::http::field::allow, allowed_methods_str); - spdlog::warn("405 Method Not Allowed: {} {}", std::string(boost::beast::http::to_string(ctx.method())), - ctx.path()); - - if (method_not_allowed_handler_) - { - method_not_allowed_handler_(ctx); - return; - } +void HttpRouter::handle_not_found(HttpContext& ctx) const { + ctx.set_status(boost::beast::http::status::not_found); + spdlog::warn("404 Not Found: {}", ctx.path()); + if (not_found_handler_) { + not_found_handler_(ctx); + return; + } + ctx.set_content_type("text/html"); + ctx.set_body(make_html_error_page( + 404, "Page not found", "The requested resource could not be found.", + ctx.path())); +} - ctx.set_content_type("text/html"); - ctx.set_body(make_html_error_page(405, "Method Not Allowed", - "The request method is not allowed for this resource.", - fmt::format("{} {}", boost::beast::http::to_string(ctx.method()), ctx.path()))); - } +void HttpRouter::handle_method_not_allowed( + HttpContext& ctx, + const std::map& allowed_methods) + const { + ctx.set_status(boost::beast::http::status::method_not_allowed); + + std::string allowed_methods_str; + bool first = true; + for (const auto& pair : allowed_methods) { + if (!first) allowed_methods_str += ", "; + allowed_methods_str += boost::beast::http::to_string(pair.first); + first = false; + } + ctx.set_header(boost::beast::http::field::allow, allowed_methods_str); + spdlog::warn("405 Method Not Allowed: {} {}", + std::string(boost::beast::http::to_string(ctx.method())), + ctx.path()); + + if (method_not_allowed_handler_) { + method_not_allowed_handler_(ctx); + return; + } + + ctx.set_content_type("text/html"); + ctx.set_body(make_html_error_page( + 405, "Method Not Allowed", + "The request method is not allowed for this resource.", + fmt::format("{} {}", boost::beast::http::to_string(ctx.method()), + ctx.path()))); +} - void HttpRouter::add_exception_handler(std::shared_ptr handler) - { - exception_handlers_.push_back(std::move(handler)); - } +void HttpRouter::add_exception_handler( + std::shared_ptr handler) { + exception_handlers_.push_back(std::move(handler)); +} - void HttpRouter::set_unknown_exception_handler(UnknownExceptionHandler handler) - { - unknown_exception_handler_ = std::move(handler); - } +void HttpRouter::set_unknown_exception_handler( + UnknownExceptionHandler handler) { + unknown_exception_handler_ = std::move(handler); +} - void HttpRouter::set_not_found_handler(HttpHandler handler) - { - not_found_handler_ = std::move(handler); - } +void HttpRouter::set_not_found_handler(HttpHandler handler) { + not_found_handler_ = std::move(handler); +} - void HttpRouter::set_method_not_allowed_handler(HttpHandler handler) - { - method_not_allowed_handler_ = std::move(handler); - } +void HttpRouter::set_method_not_allowed_handler(HttpHandler handler) { + method_not_allowed_handler_ = std::move(handler); +} - void HttpRouter::handle_exception(std::exception_ptr eptr, HttpContext& ctx) const - { - reset_exception_response(ctx); +void HttpRouter::handle_exception(std::exception_ptr eptr, + HttpContext& ctx) const { + reset_exception_response(ctx); - if (!eptr) - { - // Should not happen, but safeguard against null pointer - spdlog::error("handle_exception called with null exception_ptr"); - handle_unknown_exception(ctx); - return; - } + if (!eptr) { + // Should not happen, but safeguard against null pointer + spdlog::error("handle_exception called with null exception_ptr"); + handle_unknown_exception(ctx); + return; + } - try - { - std::rethrow_exception(eptr); - } - catch (const HttpException& exception) - { - spdlog::warn("HTTP exception: {}", exception.what()); - exception.apply(ctx); - return; - } - catch (...) - { - } + try { + std::rethrow_exception(eptr); + } catch (const HttpException& exception) { + spdlog::warn("HTTP exception: {}", exception.what()); + exception.apply(ctx); + return; + } catch (...) { + } - for (const auto& handler : exception_handlers_) - { - try - { - if (handler->try_handle(eptr, ctx)) - { - return; - } - } - catch (const std::exception& mapper_error) - { - spdlog::error("Exception handler failed: {}", mapper_error.what()); - write_internal_server_error(ctx); - return; - } - catch (...) - { - spdlog::error("Exception handler failed with a non-standard exception."); - write_internal_server_error(ctx); + for (const auto& handler : exception_handlers_) { + try { + if (handler->try_handle(eptr, ctx)) { return; } - } - - try - { - std::rethrow_exception(eptr); - } - catch (const TypedParameterValidationError& error) - { - detail::write_invalid_request_parameter(ctx, error.what()); - return; - } - catch (const TypedRequestValidationError&) - { - detail::write_invalid_request_body(ctx); + } catch (const std::exception& mapper_error) { + spdlog::error("Exception handler failed: {}", mapper_error.what()); + write_internal_server_error(ctx); return; - } - catch (...) - { - } - - // Default handling for std::exception if no specific handler matched - try - { - std::rethrow_exception(eptr); - } - catch (const std::exception& e) - { - spdlog::error("Unhandled exception: {}", e.what()); + } catch (...) { + spdlog::error("Exception handler failed with a non-standard exception."); write_internal_server_error(ctx); return; } - catch (...) - { - // Fall through to unknown exception handler - } - - handle_unknown_exception(ctx); } - void HttpRouter::handle_unknown_exception(HttpContext& ctx) const - { - if (unknown_exception_handler_) - { - unknown_exception_handler_(ctx); - return; - } + try { + std::rethrow_exception(eptr); + } catch (const TypedParameterValidationError& error) { + detail::write_invalid_request_parameter(ctx, error.what()); + return; + } catch (const TypedRequestValidationError&) { + detail::write_invalid_request_body(ctx); + return; + } catch (...) { + } - spdlog::error("Unknown exception occurred."); + // Default handling for std::exception if no specific handler matched + try { + std::rethrow_exception(eptr); + } catch (const std::exception& e) { + spdlog::error("Unhandled exception: {}", e.what()); write_internal_server_error(ctx); + return; + } catch (...) { + // Fall through to unknown exception handler } + + handle_unknown_exception(ctx); +} + +void HttpRouter::handle_unknown_exception(HttpContext& ctx) const { + if (unknown_exception_handler_) { + unknown_exception_handler_(ctx); + return; + } + + spdlog::error("Unknown exception occurred."); + write_internal_server_error(ctx); } +} // namespace khttpd::framework diff --git a/framework/router/http_router.hpp b/framework/router/http_router.hpp index df01783..fe3d24b 100644 --- a/framework/router/http_router.hpp +++ b/framework/router/http_router.hpp @@ -2,578 +2,721 @@ #ifndef KHTTPD_FRAMEWORK_ROUTER_HTTP_ROUT #define KHTTPD_FRAMEWORK_ROUTER_HTTP_ROUT +#include +#include +#include +#include +#include +#include +#include + #include "context/http_context.hpp" #include "context/http_request_stream.hpp" #include "context/http_response_stream.hpp" -#include "interceptor/interceptor.hpp" #include "exception/exception_handler.hpp" #include "exception/http_exception.hpp" +#include "interceptor/interceptor.hpp" #include "router/typed_route.hpp" -#include -#include -#include -#include -#include -#include -#include - -namespace khttpd::framework -{ - namespace sse { class SseSession; } - struct OpenApiInfo; - class HttpRouter; - void install_openapi_routes(HttpRouter& router, const OpenApiInfo& info, - const std::string& spec_path, const std::string& docs_path, bool enabled); - - using HttpHandler = std::function; - using HttpAsyncComplete = std::function; - using HttpAsyncHandler = std::function; - using HttpStreamComplete = std::function; - using HttpStreamHandler = std::function, - std::shared_ptr, HttpStreamComplete)>; - using SseHandler = std::function)>; - using UnknownExceptionHandler = std::function; - - struct RouteHeader - { - std::string name; - std::string description; - bool required = false; - }; - - struct RouteDocumentation - { - std::string summary; - std::string description; - std::vector headers; - std::optional request_schema; - std::optional response_schema; - }; - - struct RouteDescriptor - { - std::string path; - boost::beast::http::verb method; - std::optional request_schema; - std::optional response_schema; - RouteDocumentation documentation; - std::vector parameters; - }; - - // 路由条目结构 - struct RouteEntry - { - std::string original_path; - std::regex path_regex; - std::vector param_names; - std::map handlers; - std::map async_handlers; - std::map stream_handlers; - int literal_segments_count = 0; - int dynamic_segments_count = 0; - - // 比较函数:用于根据特异性对路由进行排序 - // 返回 true 表示 a 应该排在 b 之前 (a 更具体) - static bool compare_specificity(const RouteEntry& a, const RouteEntry& b) - { - // 首先比较字面路径段数量,多的优先 - if (a.literal_segments_count != b.literal_segments_count) - { - return a.literal_segments_count > b.literal_segments_count; - } - // 如果字面路径段数量相同,则比较动态路径段数量,少的优先 - return a.dynamic_segments_count < b.dynamic_segments_count; - } - }; - - class HttpRouter - { - public: - HttpRouter(); - - void set_base_path(std::string path); - const std::string& base_path() const { return base_path_; } - - void get(const std::string& path, HttpHandler handler); - void post(const std::string& path, HttpHandler handler); - void put(const std::string& path, HttpHandler handler); - void del(const std::string& path, HttpHandler handler); - void options(const std::string& path, HttpHandler handler); - - void get(const std::string& path, HttpHandler handler, RouteDocumentation documentation); - void post(const std::string& path, HttpHandler handler, RouteDocumentation documentation); - void put(const std::string& path, HttpHandler handler, RouteDocumentation documentation); - void del(const std::string& path, HttpHandler handler, RouteDocumentation documentation); - void options(const std::string& path, HttpHandler handler, RouteDocumentation documentation); - - template , int> = 0> - void get(const std::string& path, Handler&& handler) - { - add_typed_route(path, boost::beast::http::verb::get, - detail::make_typed_handler(std::forward(handler))); - } - - template && - (detail::is_route_parameter_descriptor_v && ...), int> = 0> - void get(const std::string& path, Handler&& handler, FirstDescriptor&& first, - Descriptors&&... descriptors) - { - add_typed_route(path, boost::beast::http::verb::get, - detail::make_parameterized_typed_handler( - std::forward(handler), std::forward(first), - std::forward(descriptors)...)); - } - - template && - (detail::is_route_parameter_descriptor_v && ...), int> = 0> - void get(const std::string& path, Handler&& handler, RouteDocumentation documentation, - FirstDescriptor&& first, Descriptors&&... descriptors) - { - add_typed_route(path, boost::beast::http::verb::get, - detail::make_parameterized_typed_handler( - std::forward(handler), std::forward(first), - std::forward(descriptors)...), std::move(documentation)); - } - - template , int> = 0> - void post(const std::string& path, Handler&& handler) - { - add_typed_route(path, boost::beast::http::verb::post, - detail::make_typed_handler(std::forward(handler))); - } - template && - (detail::is_route_parameter_descriptor_v && ...), int> = 0> - void post(const std::string& path, Handler&& handler, FirstDescriptor&& first, - Descriptors&&... descriptors) - { - add_typed_route(path, boost::beast::http::verb::post, - detail::make_parameterized_typed_handler( - std::forward(handler), std::forward(first), - std::forward(descriptors)...)); - } - - template && - (detail::is_route_parameter_descriptor_v && ...), int> = 0> - void post(const std::string& path, Handler&& handler, RouteDocumentation documentation, - FirstDescriptor&& first, Descriptors&&... descriptors) - { - add_typed_route(path, boost::beast::http::verb::post, - detail::make_parameterized_typed_handler( - std::forward(handler), std::forward(first), - std::forward(descriptors)...), std::move(documentation)); - } - - template , int> = 0> - void put(const std::string& path, Handler&& handler) - { - add_typed_route(path, boost::beast::http::verb::put, - detail::make_typed_handler(std::forward(handler))); - } - - template && - (detail::is_route_parameter_descriptor_v && ...), int> = 0> - void put(const std::string& path, Handler&& handler, FirstDescriptor&& first, - Descriptors&&... descriptors) - { - add_typed_route(path, boost::beast::http::verb::put, - detail::make_parameterized_typed_handler( - std::forward(handler), std::forward(first), - std::forward(descriptors)...)); - } - - template && - (detail::is_route_parameter_descriptor_v && ...), int> = 0> - void put(const std::string& path, Handler&& handler, RouteDocumentation documentation, - FirstDescriptor&& first, Descriptors&&... descriptors) - { - add_typed_route(path, boost::beast::http::verb::put, - detail::make_parameterized_typed_handler( - std::forward(handler), std::forward(first), - std::forward(descriptors)...), std::move(documentation)); - } - - template , int> = 0> - void del(const std::string& path, Handler&& handler) - { - add_typed_route(path, boost::beast::http::verb::delete_, - detail::make_typed_handler(std::forward(handler))); - } - - template && - (detail::is_route_parameter_descriptor_v && ...), int> = 0> - void del(const std::string& path, Handler&& handler, FirstDescriptor&& first, - Descriptors&&... descriptors) - { - add_typed_route(path, boost::beast::http::verb::delete_, - detail::make_parameterized_typed_handler( - std::forward(handler), std::forward(first), - std::forward(descriptors)...)); - } - - template && - (detail::is_route_parameter_descriptor_v && ...), int> = 0> - void del(const std::string& path, Handler&& handler, RouteDocumentation documentation, - FirstDescriptor&& first, Descriptors&&... descriptors) - { - add_typed_route(path, boost::beast::http::verb::delete_, - detail::make_parameterized_typed_handler( - std::forward(handler), std::forward(first), - std::forward(descriptors)...), std::move(documentation)); - } - - template , int> = 0> - void options(const std::string& path, Handler&& handler) - { - add_typed_route(path, boost::beast::http::verb::options, - detail::make_typed_handler(std::forward(handler))); - } - - template && - (detail::is_route_parameter_descriptor_v && ...), int> = 0> - void options(const std::string& path, Handler&& handler, FirstDescriptor&& first, - Descriptors&&... descriptors) - { - add_typed_route(path, boost::beast::http::verb::options, - detail::make_parameterized_typed_handler( - std::forward(handler), std::forward(first), - std::forward(descriptors)...)); - } - - template && - (detail::is_route_parameter_descriptor_v && ...), int> = 0> - void options(const std::string& path, Handler&& handler, RouteDocumentation documentation, - FirstDescriptor&& first, Descriptors&&... descriptors) - { - add_typed_route(path, boost::beast::http::verb::options, - detail::make_parameterized_typed_handler( - std::forward(handler), std::forward(first), - std::forward(descriptors)...), std::move(documentation)); - } - - template , int> = 0> - void get(const std::string& path, Handler&& handler, RouteDocumentation documentation) - { - add_typed_route(path, boost::beast::http::verb::get, - detail::make_typed_handler(std::forward(handler)), std::move(documentation)); - } - - template , int> = 0> - void post(const std::string& path, Handler&& handler, RouteDocumentation documentation) - { - add_typed_route(path, boost::beast::http::verb::post, - detail::make_typed_handler(std::forward(handler)), std::move(documentation)); - } - - template , int> = 0> - void put(const std::string& path, Handler&& handler, RouteDocumentation documentation) - { - add_typed_route(path, boost::beast::http::verb::put, - detail::make_typed_handler(std::forward(handler)), std::move(documentation)); - } - - template , int> = 0> - void del(const std::string& path, Handler&& handler, RouteDocumentation documentation) - { - add_typed_route(path, boost::beast::http::verb::delete_, - detail::make_typed_handler(std::forward(handler)), std::move(documentation)); - } - - template , int> = 0> - void options(const std::string& path, Handler&& handler, RouteDocumentation documentation) - { - add_typed_route(path, boost::beast::http::verb::options, - detail::make_typed_handler(std::forward(handler)), std::move(documentation)); - } - - template - void get(const std::string& path, std::shared_ptr controller, Method method) - { - add_typed_route(path, boost::beast::http::verb::get, - detail::make_typed_member_handler(std::move(controller), method)); - } - - template && - (detail::is_route_parameter_descriptor_v && ...), int> = 0> - void get(const std::string& path, std::shared_ptr controller, Method method, - FirstDescriptor&& first, Descriptors&&... descriptors) - { - add_typed_route(path, boost::beast::http::verb::get, +namespace khttpd::framework { +namespace sse { +class SseSession; +} +struct OpenApiInfo; +class HttpRouter; +void install_openapi_routes(HttpRouter& router, const OpenApiInfo& info, + const std::string& spec_path, + const std::string& docs_path, bool enabled); + +using HttpHandler = std::function; +using HttpAsyncComplete = std::function; +using HttpAsyncHandler = std::function; +using HttpStreamComplete = std::function; +using HttpStreamHandler = std::function, + std::shared_ptr, HttpStreamComplete)>; +using SseHandler = + std::function)>; +using UnknownExceptionHandler = std::function; + +struct RouteHeader { + std::string name; + std::string description; + bool required = false; +}; + +struct RouteDocumentation { + std::string summary; + std::string description; + std::vector headers; + std::optional request_schema; + std::optional response_schema; +}; + +struct RouteDescriptor { + std::string path; + boost::beast::http::verb method; + std::optional request_schema; + std::optional response_schema; + RouteDocumentation documentation; + std::vector parameters; +}; + +// 路由条目结构 +struct RouteEntry { + std::string original_path; + std::regex path_regex; + std::vector param_names; + std::map handlers; + std::map async_handlers; + std::map stream_handlers; + int literal_segments_count = 0; + int dynamic_segments_count = 0; + + // 比较函数:用于根据特异性对路由进行排序 + // 返回 true 表示 a 应该排在 b 之前 (a 更具体) + static bool compare_specificity(const RouteEntry& a, const RouteEntry& b) { + // 首先比较字面路径段数量,多的优先 + if (a.literal_segments_count != b.literal_segments_count) { + return a.literal_segments_count > b.literal_segments_count; + } + // 如果字面路径段数量相同,则比较动态路径段数量,少的优先 + return a.dynamic_segments_count < b.dynamic_segments_count; + } +}; + +class HttpRouter { + public: + HttpRouter(); + + void set_base_path(std::string path); + const std::string& base_path() const { return base_path_; } + + void get(const std::string& path, HttpHandler handler); + void post(const std::string& path, HttpHandler handler); + void put(const std::string& path, HttpHandler handler); + void del(const std::string& path, HttpHandler handler); + void options(const std::string& path, HttpHandler handler); + + void get(const std::string& path, HttpHandler handler, + RouteDocumentation documentation); + void post(const std::string& path, HttpHandler handler, + RouteDocumentation documentation); + void put(const std::string& path, HttpHandler handler, + RouteDocumentation documentation); + void del(const std::string& path, HttpHandler handler, + RouteDocumentation documentation); + void options(const std::string& path, HttpHandler handler, + RouteDocumentation documentation); + + template < + class Handler, + std::enable_if_t, int> = 0> + void get(const std::string& path, Handler&& handler) { + add_typed_route(path, boost::beast::http::verb::get, + detail::make_typed_handler(std::forward(handler))); + } + + template < + class Handler, class FirstDescriptor, class... Descriptors, + std::enable_if_t< + detail::is_route_parameter_descriptor_v && + (detail::is_route_parameter_descriptor_v && ...), + int> = 0> + void get(const std::string& path, Handler&& handler, FirstDescriptor&& first, + Descriptors&&... descriptors) { + add_typed_route(path, boost::beast::http::verb::get, + detail::make_parameterized_typed_handler( + std::forward(handler), + std::forward(first), + std::forward(descriptors)...)); + } + + template < + class Handler, class FirstDescriptor, class... Descriptors, + std::enable_if_t< + detail::is_route_parameter_descriptor_v && + (detail::is_route_parameter_descriptor_v && ...), + int> = 0> + void get(const std::string& path, Handler&& handler, + RouteDocumentation documentation, FirstDescriptor&& first, + Descriptors&&... descriptors) { + add_typed_route(path, boost::beast::http::verb::get, + detail::make_parameterized_typed_handler( + std::forward(handler), + std::forward(first), + std::forward(descriptors)...), + std::move(documentation)); + } + + template < + class Handler, + std::enable_if_t, int> = 0> + void post(const std::string& path, Handler&& handler) { + add_typed_route(path, boost::beast::http::verb::post, + detail::make_typed_handler(std::forward(handler))); + } + + template < + class Handler, class FirstDescriptor, class... Descriptors, + std::enable_if_t< + detail::is_route_parameter_descriptor_v && + (detail::is_route_parameter_descriptor_v && ...), + int> = 0> + void post(const std::string& path, Handler&& handler, FirstDescriptor&& first, + Descriptors&&... descriptors) { + add_typed_route(path, boost::beast::http::verb::post, + detail::make_parameterized_typed_handler( + std::forward(handler), + std::forward(first), + std::forward(descriptors)...)); + } + + template < + class Handler, class FirstDescriptor, class... Descriptors, + std::enable_if_t< + detail::is_route_parameter_descriptor_v && + (detail::is_route_parameter_descriptor_v && ...), + int> = 0> + void post(const std::string& path, Handler&& handler, + RouteDocumentation documentation, FirstDescriptor&& first, + Descriptors&&... descriptors) { + add_typed_route(path, boost::beast::http::verb::post, + detail::make_parameterized_typed_handler( + std::forward(handler), + std::forward(first), + std::forward(descriptors)...), + std::move(documentation)); + } + + template < + class Handler, + std::enable_if_t, int> = 0> + void put(const std::string& path, Handler&& handler) { + add_typed_route(path, boost::beast::http::verb::put, + detail::make_typed_handler(std::forward(handler))); + } + + template < + class Handler, class FirstDescriptor, class... Descriptors, + std::enable_if_t< + detail::is_route_parameter_descriptor_v && + (detail::is_route_parameter_descriptor_v && ...), + int> = 0> + void put(const std::string& path, Handler&& handler, FirstDescriptor&& first, + Descriptors&&... descriptors) { + add_typed_route(path, boost::beast::http::verb::put, + detail::make_parameterized_typed_handler( + std::forward(handler), + std::forward(first), + std::forward(descriptors)...)); + } + + template < + class Handler, class FirstDescriptor, class... Descriptors, + std::enable_if_t< + detail::is_route_parameter_descriptor_v && + (detail::is_route_parameter_descriptor_v && ...), + int> = 0> + void put(const std::string& path, Handler&& handler, + RouteDocumentation documentation, FirstDescriptor&& first, + Descriptors&&... descriptors) { + add_typed_route(path, boost::beast::http::verb::put, + detail::make_parameterized_typed_handler( + std::forward(handler), + std::forward(first), + std::forward(descriptors)...), + std::move(documentation)); + } + + template < + class Handler, + std::enable_if_t, int> = 0> + void del(const std::string& path, Handler&& handler) { + add_typed_route(path, boost::beast::http::verb::delete_, + detail::make_typed_handler(std::forward(handler))); + } + + template < + class Handler, class FirstDescriptor, class... Descriptors, + std::enable_if_t< + detail::is_route_parameter_descriptor_v && + (detail::is_route_parameter_descriptor_v && ...), + int> = 0> + void del(const std::string& path, Handler&& handler, FirstDescriptor&& first, + Descriptors&&... descriptors) { + add_typed_route(path, boost::beast::http::verb::delete_, + detail::make_parameterized_typed_handler( + std::forward(handler), + std::forward(first), + std::forward(descriptors)...)); + } + + template < + class Handler, class FirstDescriptor, class... Descriptors, + std::enable_if_t< + detail::is_route_parameter_descriptor_v && + (detail::is_route_parameter_descriptor_v && ...), + int> = 0> + void del(const std::string& path, Handler&& handler, + RouteDocumentation documentation, FirstDescriptor&& first, + Descriptors&&... descriptors) { + add_typed_route(path, boost::beast::http::verb::delete_, + detail::make_parameterized_typed_handler( + std::forward(handler), + std::forward(first), + std::forward(descriptors)...), + std::move(documentation)); + } + + template < + class Handler, + std::enable_if_t, int> = 0> + void options(const std::string& path, Handler&& handler) { + add_typed_route(path, boost::beast::http::verb::options, + detail::make_typed_handler(std::forward(handler))); + } + + template < + class Handler, class FirstDescriptor, class... Descriptors, + std::enable_if_t< + detail::is_route_parameter_descriptor_v && + (detail::is_route_parameter_descriptor_v && ...), + int> = 0> + void options(const std::string& path, Handler&& handler, + FirstDescriptor&& first, Descriptors&&... descriptors) { + add_typed_route(path, boost::beast::http::verb::options, + detail::make_parameterized_typed_handler( + std::forward(handler), + std::forward(first), + std::forward(descriptors)...)); + } + + template < + class Handler, class FirstDescriptor, class... Descriptors, + std::enable_if_t< + detail::is_route_parameter_descriptor_v && + (detail::is_route_parameter_descriptor_v && ...), + int> = 0> + void options(const std::string& path, Handler&& handler, + RouteDocumentation documentation, FirstDescriptor&& first, + Descriptors&&... descriptors) { + add_typed_route(path, boost::beast::http::verb::options, + detail::make_parameterized_typed_handler( + std::forward(handler), + std::forward(first), + std::forward(descriptors)...), + std::move(documentation)); + } + + template < + class Handler, + std::enable_if_t, int> = 0> + void get(const std::string& path, Handler&& handler, + RouteDocumentation documentation) { + add_typed_route(path, boost::beast::http::verb::get, + detail::make_typed_handler(std::forward(handler)), + std::move(documentation)); + } + + template < + class Handler, + std::enable_if_t, int> = 0> + void post(const std::string& path, Handler&& handler, + RouteDocumentation documentation) { + add_typed_route(path, boost::beast::http::verb::post, + detail::make_typed_handler(std::forward(handler)), + std::move(documentation)); + } + + template < + class Handler, + std::enable_if_t, int> = 0> + void put(const std::string& path, Handler&& handler, + RouteDocumentation documentation) { + add_typed_route(path, boost::beast::http::verb::put, + detail::make_typed_handler(std::forward(handler)), + std::move(documentation)); + } + + template < + class Handler, + std::enable_if_t, int> = 0> + void del(const std::string& path, Handler&& handler, + RouteDocumentation documentation) { + add_typed_route(path, boost::beast::http::verb::delete_, + detail::make_typed_handler(std::forward(handler)), + std::move(documentation)); + } + + template < + class Handler, + std::enable_if_t, int> = 0> + void options(const std::string& path, Handler&& handler, + RouteDocumentation documentation) { + add_typed_route(path, boost::beast::http::verb::options, + detail::make_typed_handler(std::forward(handler)), + std::move(documentation)); + } + + template + void get(const std::string& path, std::shared_ptr controller, + Method method) { + add_typed_route( + path, boost::beast::http::verb::get, + detail::make_typed_member_handler(std::move(controller), method)); + } + + template < + class Controller, class Method, class FirstDescriptor, + class... Descriptors, + std::enable_if_t< + detail::is_route_parameter_descriptor_v && + (detail::is_route_parameter_descriptor_v && ...), + int> = 0> + void get(const std::string& path, std::shared_ptr controller, + Method method, FirstDescriptor&& first, + Descriptors&&... descriptors) { + add_typed_route( + path, boost::beast::http::verb::get, detail::make_parameterized_member_handler( - std::move(controller), method, std::forward(first), - std::forward(descriptors)...)); - } - - template && - (detail::is_route_parameter_descriptor_v && ...), int> = 0> - void get(const std::string& path, std::shared_ptr controller, Method method, - RouteDocumentation documentation, FirstDescriptor&& first, Descriptors&&... descriptors) - { - add_typed_route(path, boost::beast::http::verb::get, + std::move(controller), method, std::forward(first), + std::forward(descriptors)...)); + } + + template < + class Controller, class Method, class FirstDescriptor, + class... Descriptors, + std::enable_if_t< + detail::is_route_parameter_descriptor_v && + (detail::is_route_parameter_descriptor_v && ...), + int> = 0> + void get(const std::string& path, std::shared_ptr controller, + Method method, RouteDocumentation documentation, + FirstDescriptor&& first, Descriptors&&... descriptors) { + add_typed_route( + path, boost::beast::http::verb::get, detail::make_parameterized_member_handler( - std::move(controller), method, std::forward(first), - std::forward(descriptors)...), std::move(documentation)); - } - - template - void post(const std::string& path, std::shared_ptr controller, Method method) - { - add_typed_route(path, boost::beast::http::verb::post, - detail::make_typed_member_handler(std::move(controller), method)); - } - - template && - (detail::is_route_parameter_descriptor_v && ...), int> = 0> - void post(const std::string& path, std::shared_ptr controller, Method method, - FirstDescriptor&& first, Descriptors&&... descriptors) - { - add_typed_route(path, boost::beast::http::verb::post, + std::move(controller), method, std::forward(first), + std::forward(descriptors)...), + std::move(documentation)); + } + + template + void post(const std::string& path, std::shared_ptr controller, + Method method) { + add_typed_route( + path, boost::beast::http::verb::post, + detail::make_typed_member_handler(std::move(controller), method)); + } + + template < + class Controller, class Method, class FirstDescriptor, + class... Descriptors, + std::enable_if_t< + detail::is_route_parameter_descriptor_v && + (detail::is_route_parameter_descriptor_v && ...), + int> = 0> + void post(const std::string& path, std::shared_ptr controller, + Method method, FirstDescriptor&& first, + Descriptors&&... descriptors) { + add_typed_route( + path, boost::beast::http::verb::post, detail::make_parameterized_member_handler( - std::move(controller), method, std::forward(first), - std::forward(descriptors)...)); - } - - template && - (detail::is_route_parameter_descriptor_v && ...), int> = 0> - void post(const std::string& path, std::shared_ptr controller, Method method, - RouteDocumentation documentation, FirstDescriptor&& first, Descriptors&&... descriptors) - { - add_typed_route(path, boost::beast::http::verb::post, + std::move(controller), method, std::forward(first), + std::forward(descriptors)...)); + } + + template < + class Controller, class Method, class FirstDescriptor, + class... Descriptors, + std::enable_if_t< + detail::is_route_parameter_descriptor_v && + (detail::is_route_parameter_descriptor_v && ...), + int> = 0> + void post(const std::string& path, std::shared_ptr controller, + Method method, RouteDocumentation documentation, + FirstDescriptor&& first, Descriptors&&... descriptors) { + add_typed_route( + path, boost::beast::http::verb::post, detail::make_parameterized_member_handler( - std::move(controller), method, std::forward(first), - std::forward(descriptors)...), std::move(documentation)); - } - - template - void put(const std::string& path, std::shared_ptr controller, Method method) - { - add_typed_route(path, boost::beast::http::verb::put, - detail::make_typed_member_handler(std::move(controller), method)); - } - - template && - (detail::is_route_parameter_descriptor_v && ...), int> = 0> - void put(const std::string& path, std::shared_ptr controller, Method method, - FirstDescriptor&& first, Descriptors&&... descriptors) - { - add_typed_route(path, boost::beast::http::verb::put, + std::move(controller), method, std::forward(first), + std::forward(descriptors)...), + std::move(documentation)); + } + + template + void put(const std::string& path, std::shared_ptr controller, + Method method) { + add_typed_route( + path, boost::beast::http::verb::put, + detail::make_typed_member_handler(std::move(controller), method)); + } + + template < + class Controller, class Method, class FirstDescriptor, + class... Descriptors, + std::enable_if_t< + detail::is_route_parameter_descriptor_v && + (detail::is_route_parameter_descriptor_v && ...), + int> = 0> + void put(const std::string& path, std::shared_ptr controller, + Method method, FirstDescriptor&& first, + Descriptors&&... descriptors) { + add_typed_route( + path, boost::beast::http::verb::put, detail::make_parameterized_member_handler( - std::move(controller), method, std::forward(first), - std::forward(descriptors)...)); - } - - template && - (detail::is_route_parameter_descriptor_v && ...), int> = 0> - void put(const std::string& path, std::shared_ptr controller, Method method, - RouteDocumentation documentation, FirstDescriptor&& first, Descriptors&&... descriptors) - { - add_typed_route(path, boost::beast::http::verb::put, + std::move(controller), method, std::forward(first), + std::forward(descriptors)...)); + } + + template < + class Controller, class Method, class FirstDescriptor, + class... Descriptors, + std::enable_if_t< + detail::is_route_parameter_descriptor_v && + (detail::is_route_parameter_descriptor_v && ...), + int> = 0> + void put(const std::string& path, std::shared_ptr controller, + Method method, RouteDocumentation documentation, + FirstDescriptor&& first, Descriptors&&... descriptors) { + add_typed_route( + path, boost::beast::http::verb::put, detail::make_parameterized_member_handler( - std::move(controller), method, std::forward(first), - std::forward(descriptors)...), std::move(documentation)); - } - - template - void del(const std::string& path, std::shared_ptr controller, Method method) - { - add_typed_route(path, boost::beast::http::verb::delete_, - detail::make_typed_member_handler(std::move(controller), method)); - } - - template && - (detail::is_route_parameter_descriptor_v && ...), int> = 0> - void del(const std::string& path, std::shared_ptr controller, Method method, - FirstDescriptor&& first, Descriptors&&... descriptors) - { - add_typed_route(path, boost::beast::http::verb::delete_, + std::move(controller), method, std::forward(first), + std::forward(descriptors)...), + std::move(documentation)); + } + + template + void del(const std::string& path, std::shared_ptr controller, + Method method) { + add_typed_route( + path, boost::beast::http::verb::delete_, + detail::make_typed_member_handler(std::move(controller), method)); + } + + template < + class Controller, class Method, class FirstDescriptor, + class... Descriptors, + std::enable_if_t< + detail::is_route_parameter_descriptor_v && + (detail::is_route_parameter_descriptor_v && ...), + int> = 0> + void del(const std::string& path, std::shared_ptr controller, + Method method, FirstDescriptor&& first, + Descriptors&&... descriptors) { + add_typed_route( + path, boost::beast::http::verb::delete_, detail::make_parameterized_member_handler( - std::move(controller), method, std::forward(first), - std::forward(descriptors)...)); - } - - template && - (detail::is_route_parameter_descriptor_v && ...), int> = 0> - void del(const std::string& path, std::shared_ptr controller, Method method, - RouteDocumentation documentation, FirstDescriptor&& first, Descriptors&&... descriptors) - { - add_typed_route(path, boost::beast::http::verb::delete_, + std::move(controller), method, std::forward(first), + std::forward(descriptors)...)); + } + + template < + class Controller, class Method, class FirstDescriptor, + class... Descriptors, + std::enable_if_t< + detail::is_route_parameter_descriptor_v && + (detail::is_route_parameter_descriptor_v && ...), + int> = 0> + void del(const std::string& path, std::shared_ptr controller, + Method method, RouteDocumentation documentation, + FirstDescriptor&& first, Descriptors&&... descriptors) { + add_typed_route( + path, boost::beast::http::verb::delete_, detail::make_parameterized_member_handler( - std::move(controller), method, std::forward(first), - std::forward(descriptors)...), std::move(documentation)); - } - - template - void options(const std::string& path, std::shared_ptr controller, Method method) - { - add_typed_route(path, boost::beast::http::verb::options, - detail::make_typed_member_handler(std::move(controller), method)); - } - - template && - (detail::is_route_parameter_descriptor_v && ...), int> = 0> - void options(const std::string& path, std::shared_ptr controller, Method method, - FirstDescriptor&& first, Descriptors&&... descriptors) - { - add_typed_route(path, boost::beast::http::verb::options, + std::move(controller), method, std::forward(first), + std::forward(descriptors)...), + std::move(documentation)); + } + + template + void options(const std::string& path, std::shared_ptr controller, + Method method) { + add_typed_route( + path, boost::beast::http::verb::options, + detail::make_typed_member_handler(std::move(controller), method)); + } + + template < + class Controller, class Method, class FirstDescriptor, + class... Descriptors, + std::enable_if_t< + detail::is_route_parameter_descriptor_v && + (detail::is_route_parameter_descriptor_v && ...), + int> = 0> + void options(const std::string& path, std::shared_ptr controller, + Method method, FirstDescriptor&& first, + Descriptors&&... descriptors) { + add_typed_route( + path, boost::beast::http::verb::options, detail::make_parameterized_member_handler( - std::move(controller), method, std::forward(first), - std::forward(descriptors)...)); - } - - template && - (detail::is_route_parameter_descriptor_v && ...), int> = 0> - void options(const std::string& path, std::shared_ptr controller, Method method, - RouteDocumentation documentation, FirstDescriptor&& first, - Descriptors&&... descriptors) - { - add_typed_route(path, boost::beast::http::verb::options, + std::move(controller), method, std::forward(first), + std::forward(descriptors)...)); + } + + template < + class Controller, class Method, class FirstDescriptor, + class... Descriptors, + std::enable_if_t< + detail::is_route_parameter_descriptor_v && + (detail::is_route_parameter_descriptor_v && ...), + int> = 0> + void options(const std::string& path, std::shared_ptr controller, + Method method, RouteDocumentation documentation, + FirstDescriptor&& first, Descriptors&&... descriptors) { + add_typed_route( + path, boost::beast::http::verb::options, detail::make_parameterized_member_handler( - std::move(controller), method, std::forward(first), - std::forward(descriptors)...), std::move(documentation)); - } - - template - void get(const std::string& path, std::shared_ptr controller, Method method, - RouteDocumentation documentation) - { - add_typed_route(path, boost::beast::http::verb::get, - detail::make_typed_member_handler(std::move(controller), method), std::move(documentation)); - } - - template - void post(const std::string& path, std::shared_ptr controller, Method method, - RouteDocumentation documentation) - { - add_typed_route(path, boost::beast::http::verb::post, - detail::make_typed_member_handler(std::move(controller), method), std::move(documentation)); - } - - template - void put(const std::string& path, std::shared_ptr controller, Method method, - RouteDocumentation documentation) - { - add_typed_route(path, boost::beast::http::verb::put, - detail::make_typed_member_handler(std::move(controller), method), std::move(documentation)); - } - - template - void del(const std::string& path, std::shared_ptr controller, Method method, - RouteDocumentation documentation) - { - add_typed_route(path, boost::beast::http::verb::delete_, - detail::make_typed_member_handler(std::move(controller), method), std::move(documentation)); - } - - template - void options(const std::string& path, std::shared_ptr controller, Method method, - RouteDocumentation documentation) - { - add_typed_route(path, boost::beast::http::verb::options, - detail::make_typed_member_handler(std::move(controller), method), std::move(documentation)); - } - // Async handlers must invoke complete exactly once, from any thread. - void async_route(const std::string& path, boost::beast::http::verb method, HttpAsyncHandler handler); - void stream(const std::string& path, boost::beast::http::verb method, HttpStreamHandler handler); - void sse(const std::string& path, SseHandler handler, - std::size_t max_pending_bytes = 1024 * 1024); - - // Used by HttpSession after it has parsed only the request header. - bool is_stream_route(const std::string& path, boost::beast::http::verb method) const; - bool dispatch_stream(HttpContext& ctx, std::shared_ptr stream, - std::shared_ptr response_stream, - HttpStreamComplete complete) const; - - void add_interceptor(std::shared_ptr interceptor); - InterceptorResult run_pre_interceptors(HttpContext& ctx) const; - using InterceptorCompletion = std::function; - void async_run_pre_interceptors(HttpContext& ctx, InterceptorCompletion complete) const; - void run_post_interceptors(HttpContext& ctx) const; - - // Exception handling - void add_exception_handler(std::shared_ptr handler); - - template - void map_exception(Mapper&& mapper) - { - using StoredMapper = std::decay_t; - add_exception_handler(std::make_shared>( - StoredMapper(std::forward(mapper)))); - } - - void set_unknown_exception_handler(UnknownExceptionHandler handler); - void handle_exception(std::exception_ptr eptr, HttpContext& ctx) const; - void handle_unknown_exception(HttpContext& ctx) const; - - // Optional service-level replacements for the framework's default HTML error pages. Pass an empty handler to reset. - // The context is pre-populated with the corresponding HTTP status. The 405 handler also receives Allow. - void set_not_found_handler(HttpHandler handler); - void set_method_not_allowed_handler(HttpHandler handler); - - bool dispatch(HttpContext& ctx, const std::function& static_file_fun = nullptr) const; - bool dispatch_async(HttpContext& ctx, HttpAsyncComplete complete) const; - - // Returns handler-free copies suitable for documentation and inspection. - std::vector route_descriptors() const; - - // Adds or replaces OpenAPI summary and description for a registered route. - void document_route(const std::string& path, boost::beast::http::verb method, - RouteDocumentation documentation); - - private: - friend void install_openapi_routes(HttpRouter& router, const OpenApiInfo& info, - const std::string& spec_path, const std::string& docs_path, bool enabled); - - std::vector routes_; - std::vector route_descriptors_; - std::string base_path_; - std::vector> interceptors_; - - std::vector> exception_handlers_; - UnknownExceptionHandler unknown_exception_handler_; - HttpHandler not_found_handler_; - HttpHandler method_not_allowed_handler_; - - void add_route(const std::string& path_pattern, boost::beast::http::verb method, HttpHandler handler, - std::optional request_schema = std::nullopt, - std::optional response_schema = std::nullopt, - bool documented = true, - RouteDocumentation documentation = {}, - std::vector parameters = {}); - std::string apply_base_path(const std::string& path) const; - void add_typed_route(const std::string& path_pattern, boost::beast::http::verb method, - detail::TypedRouteHandler handler, RouteDocumentation documentation = {}); - void record_route_descriptor(const std::string& path, boost::beast::http::verb method, - std::optional request_schema = std::nullopt, - std::optional response_schema = std::nullopt, - RouteDocumentation documentation = {}, - std::vector parameters = {}); - - static std::tuple, int, int> parse_path_pattern( - const std::string& path_pattern); - - void handle_not_found(HttpContext& ctx) const; - void handle_method_not_allowed(HttpContext& ctx, - const std::map& allowed_methods) const; - }; -} -#endif // KHTTPD_FRAMEWORK_ROUTER_HTTP_ROUT + std::move(controller), method, std::forward(first), + std::forward(descriptors)...), + std::move(documentation)); + } + + template + void get(const std::string& path, std::shared_ptr controller, + Method method, RouteDocumentation documentation) { + add_typed_route( + path, boost::beast::http::verb::get, + detail::make_typed_member_handler(std::move(controller), method), + std::move(documentation)); + } + + template + void post(const std::string& path, std::shared_ptr controller, + Method method, RouteDocumentation documentation) { + add_typed_route( + path, boost::beast::http::verb::post, + detail::make_typed_member_handler(std::move(controller), method), + std::move(documentation)); + } + + template + void put(const std::string& path, std::shared_ptr controller, + Method method, RouteDocumentation documentation) { + add_typed_route( + path, boost::beast::http::verb::put, + detail::make_typed_member_handler(std::move(controller), method), + std::move(documentation)); + } + + template + void del(const std::string& path, std::shared_ptr controller, + Method method, RouteDocumentation documentation) { + add_typed_route( + path, boost::beast::http::verb::delete_, + detail::make_typed_member_handler(std::move(controller), method), + std::move(documentation)); + } + + template + void options(const std::string& path, std::shared_ptr controller, + Method method, RouteDocumentation documentation) { + add_typed_route( + path, boost::beast::http::verb::options, + detail::make_typed_member_handler(std::move(controller), method), + std::move(documentation)); + } + // Async handlers must invoke complete exactly once, from any thread. + void async_route(const std::string& path, boost::beast::http::verb method, + HttpAsyncHandler handler); + void stream(const std::string& path, boost::beast::http::verb method, + HttpStreamHandler handler); + void sse(const std::string& path, SseHandler handler, + std::size_t max_pending_bytes = 1024 * 1024); + + // Used by HttpSession after it has parsed only the request header. + bool is_stream_route(const std::string& path, + boost::beast::http::verb method) const; + bool dispatch_stream(HttpContext& ctx, + std::shared_ptr stream, + std::shared_ptr response_stream, + HttpStreamComplete complete) const; + + void add_interceptor(std::shared_ptr interceptor); + InterceptorResult run_pre_interceptors(HttpContext& ctx) const; + using InterceptorCompletion = std::function; + void async_run_pre_interceptors(HttpContext& ctx, + InterceptorCompletion complete) const; + void run_post_interceptors(HttpContext& ctx) const; + + // Exception handling + void add_exception_handler(std::shared_ptr handler); + + template + void map_exception(Mapper&& mapper) { + using StoredMapper = std::decay_t; + add_exception_handler( + std::make_shared>( + StoredMapper(std::forward(mapper)))); + } + + void set_unknown_exception_handler(UnknownExceptionHandler handler); + void handle_exception(std::exception_ptr eptr, HttpContext& ctx) const; + void handle_unknown_exception(HttpContext& ctx) const; + + // Optional service-level replacements for the framework's default HTML error + // pages. Pass an empty handler to reset. The context is pre-populated with + // the corresponding HTTP status. The 405 handler also receives Allow. + void set_not_found_handler(HttpHandler handler); + void set_method_not_allowed_handler(HttpHandler handler); + + bool dispatch(HttpContext& ctx, + const std::function& static_file_fun = nullptr) const; + bool dispatch_async(HttpContext& ctx, HttpAsyncComplete complete) const; + + // Returns handler-free copies suitable for documentation and inspection. + std::vector route_descriptors() const; + + // Adds or replaces OpenAPI summary and description for a registered route. + void document_route(const std::string& path, boost::beast::http::verb method, + RouteDocumentation documentation); + + private: + friend void install_openapi_routes(HttpRouter& router, + const OpenApiInfo& info, + const std::string& spec_path, + const std::string& docs_path, + bool enabled); + + std::vector routes_; + std::vector route_descriptors_; + std::string base_path_; + std::vector> interceptors_; + + std::vector> exception_handlers_; + UnknownExceptionHandler unknown_exception_handler_; + HttpHandler not_found_handler_; + HttpHandler method_not_allowed_handler_; + + void add_route( + const std::string& path_pattern, boost::beast::http::verb method, + HttpHandler handler, + std::optional request_schema = std::nullopt, + std::optional response_schema = std::nullopt, + bool documented = true, RouteDocumentation documentation = {}, + std::vector parameters = {}); + std::string apply_base_path(const std::string& path) const; + void add_typed_route(const std::string& path_pattern, + boost::beast::http::verb method, + detail::TypedRouteHandler handler, + RouteDocumentation documentation = {}); + void record_route_descriptor( + const std::string& path, boost::beast::http::verb method, + std::optional request_schema = std::nullopt, + std::optional response_schema = std::nullopt, + RouteDocumentation documentation = {}, + std::vector parameters = {}); + + static std::tuple, int, int> + parse_path_pattern(const std::string& path_pattern); + + void handle_not_found(HttpContext& ctx) const; + void handle_method_not_allowed( + HttpContext& ctx, + const std::map& allowed_methods) + const; +}; +} // namespace khttpd::framework +#endif // KHTTPD_FRAMEWORK_ROUTER_HTTP_ROUT diff --git a/framework/router/openapi.cpp b/framework/router/openapi.cpp index 32d4630..41223d9 100644 --- a/framework/router/openapi.cpp +++ b/framework/router/openapi.cpp @@ -1,7 +1,5 @@ #include "router/openapi.hpp" -#include "router/http_router.hpp" - #include #include #include @@ -14,528 +12,561 @@ #include #include -namespace khttpd::framework -{ - namespace - { - struct DocumentedPath - { - std::string path; - std::vector parameters; - }; - - DocumentedPath document_path(const std::string& route_path) - { - static const std::regex parameter_pattern( - R"((?::([a-zA-Z_][a-zA-Z0-9_]*)|\{([a-zA-Z_][a-zA-Z0-9_]*)\}))"); - DocumentedPath result; - auto current = route_path.cbegin(); - const std::sregex_iterator end; - for (auto it = std::sregex_iterator(route_path.cbegin(), route_path.cend(), parameter_pattern); - it != end; ++it) - { - result.path.append(current, it->prefix().second); - const auto name = (*it)[1].matched ? (*it)[1].str() : (*it)[2].str(); - result.path += "{" + name + "}"; - result.parameters.push_back(name); - current = it->suffix().first; - } - result.path.append(current, route_path.cend()); - return result; - } +#include "router/http_router.hpp" - std::string method_name(const boost::beast::http::verb method) - { - std::string result(boost::beast::http::to_string(method)); - std::transform(result.begin(), result.end(), result.begin(), [](const unsigned char c) - { - return static_cast(std::tolower(c)); - }); - return result; - } +namespace khttpd::framework { +namespace { +struct DocumentedPath { + std::string path; + std::vector parameters; +}; - boost::json::object make_operation(const RouteDescriptor& descriptor, - const std::vector& path_parameters) - { - boost::json::object operation; - if (!descriptor.documentation.summary.empty()) - operation.emplace("summary", descriptor.documentation.summary); - if (!descriptor.documentation.description.empty()) - operation.emplace("description", descriptor.documentation.description); - boost::json::array parameters; - const auto find_parameter = [&](const RouteParameterLocation location, - const std::string& name) -> const RouteParameterDocumentation* - { - const auto it = std::find_if(descriptor.parameters.begin(), descriptor.parameters.end(), - [&](const RouteParameterDocumentation& parameter) - { - return parameter.location == location && parameter.name == name; - }); - return it == descriptor.parameters.end() ? nullptr : &*it; - }; - for (std::size_t index = 0; index < path_parameters.size(); ++index) - { - boost::json::object parameter; - parameter.emplace("name", path_parameters[index]); - parameter.emplace("in", "path"); - parameter.emplace("required", true); - const auto* documented = find_parameter(RouteParameterLocation::path, path_parameters[index]); - parameter.emplace("schema", documented != nullptr ? documented->schema : - boost::json::value(boost::json::object{{"type", "string"}})); - if (index + 1 == path_parameters.size()) parameter.emplace("x-khttpd-greedy", true); - parameters.emplace_back(std::move(parameter)); - } - for (const auto& documented : descriptor.parameters) - { - if (documented.location != RouteParameterLocation::query) continue; - boost::json::object parameter; - parameter.emplace("name", documented.name); - parameter.emplace("in", "query"); - parameter.emplace("required", documented.required); - parameter.emplace("schema", documented.schema); - parameters.emplace_back(std::move(parameter)); - } - for (const auto& header : descriptor.documentation.headers) - { - if (header.name.empty()) continue; - boost::json::object parameter; - parameter.emplace("name", header.name); - parameter.emplace("in", "header"); - if (!header.description.empty()) parameter.emplace("description", header.description); - parameter.emplace("required", header.required); - parameter.emplace("schema", boost::json::object{{"type", "string"}}); - parameters.emplace_back(std::move(parameter)); - } - if (!parameters.empty()) - operation.emplace("parameters", std::move(parameters)); - - if (descriptor.request_schema) - { - boost::json::object media_type; - media_type.emplace("schema", *descriptor.request_schema); - boost::json::object content; - content.emplace("application/json", std::move(media_type)); - boost::json::object request_body; - request_body.emplace("required", true); - request_body.emplace("content", std::move(content)); - operation.emplace("requestBody", std::move(request_body)); - } +DocumentedPath document_path(const std::string& route_path) { + static const std::regex parameter_pattern( + R"((?::([a-zA-Z_][a-zA-Z0-9_]*)|\{([a-zA-Z_][a-zA-Z0-9_]*)\}))"); + DocumentedPath result; + auto current = route_path.cbegin(); + const std::sregex_iterator end; + for (auto it = std::sregex_iterator(route_path.cbegin(), route_path.cend(), + parameter_pattern); + it != end; ++it) { + result.path.append(current, it->prefix().second); + const auto name = (*it)[1].matched ? (*it)[1].str() : (*it)[2].str(); + result.path += "{" + name + "}"; + result.parameters.push_back(name); + current = it->suffix().first; + } + result.path.append(current, route_path.cend()); + return result; +} - boost::json::object response; - response.emplace("description", "Successful response"); - if (descriptor.response_schema) - { - boost::json::object media_type; - media_type.emplace("schema", *descriptor.response_schema); - boost::json::object content; - content.emplace("application/json", std::move(media_type)); - response.emplace("content", std::move(content)); - } - boost::json::object responses; - responses.emplace("200", std::move(response)); - operation.emplace("responses", std::move(responses)); - return operation; - } +std::string method_name(const boost::beast::http::verb method) { + std::string result(boost::beast::http::to_string(method)); + std::transform( + result.begin(), result.end(), result.begin(), + [](const unsigned char c) { return static_cast(std::tolower(c)); }); + return result; +} - void validate_documentation_path(const std::string& path, const char* name) - { - const auto literal_character = [](const unsigned char character) - { - return std::isalnum(character) != 0 || character == '/' || character == '.' || - character == '_' || character == '-' || character == '~'; - }; - if (path.empty() || path.front() != '/' || - !std::all_of(path.begin(), path.end(), literal_character)) - throw std::invalid_argument(std::string(name) + " must be an absolute literal HTTP path"); - } +boost::json::object make_operation( + const RouteDescriptor& descriptor, + const std::vector& path_parameters) { + boost::json::object operation; + if (!descriptor.documentation.summary.empty()) + operation.emplace("summary", descriptor.documentation.summary); + if (!descriptor.documentation.description.empty()) + operation.emplace("description", descriptor.documentation.description); + boost::json::array parameters; + const auto find_parameter = + [&](const RouteParameterLocation location, + const std::string& name) -> const RouteParameterDocumentation* { + const auto it = std::find_if( + descriptor.parameters.begin(), descriptor.parameters.end(), + [&](const RouteParameterDocumentation& parameter) { + return parameter.location == location && parameter.name == name; + }); + return it == descriptor.parameters.end() ? nullptr : &*it; + }; + for (std::size_t index = 0; index < path_parameters.size(); ++index) { + boost::json::object parameter; + parameter.emplace("name", path_parameters[index]); + parameter.emplace("in", "path"); + parameter.emplace("required", true); + const auto* documented = + find_parameter(RouteParameterLocation::path, path_parameters[index]); + parameter.emplace( + "schema", + documented != nullptr + ? documented->schema + : boost::json::value(boost::json::object{{"type", "string"}})); + if (index + 1 == path_parameters.size()) + parameter.emplace("x-khttpd-greedy", true); + parameters.emplace_back(std::move(parameter)); + } + for (const auto& documented : descriptor.parameters) { + if (documented.location != RouteParameterLocation::query) continue; + boost::json::object parameter; + parameter.emplace("name", documented.name); + parameter.emplace("in", "query"); + parameter.emplace("required", documented.required); + parameter.emplace("schema", documented.schema); + parameters.emplace_back(std::move(parameter)); + } + for (const auto& header : descriptor.documentation.headers) { + if (header.name.empty()) continue; + boost::json::object parameter; + parameter.emplace("name", header.name); + parameter.emplace("in", "header"); + if (!header.description.empty()) + parameter.emplace("description", header.description); + parameter.emplace("required", header.required); + parameter.emplace("schema", boost::json::object{{"type", "string"}}); + parameters.emplace_back(std::move(parameter)); + } + if (!parameters.empty()) + operation.emplace("parameters", std::move(parameters)); - std::string escape_html(const std::string& input) - { - std::string escaped; - escaped.reserve(input.size()); - for (const char character : input) - { - switch (character) - { - case '&': escaped += "&"; break; - case '<': escaped += "<"; break; - case '>': escaped += ">"; break; - case '\"': escaped += """; break; - case '\'': escaped += "'"; break; - default: escaped += character; break; - } - } - return escaped; - } + if (descriptor.request_schema) { + boost::json::object media_type; + media_type.emplace("schema", *descriptor.request_schema); + boost::json::object content; + content.emplace("application/json", std::move(media_type)); + boost::json::object request_body; + request_body.emplace("required", true); + request_body.emplace("content", std::move(content)); + operation.emplace("requestBody", std::move(request_body)); + } - std::string uppercase(const std::string& input) - { - std::string result(input); - std::transform(result.begin(), result.end(), result.begin(), [](const unsigned char character) - { - return static_cast(std::toupper(character)); - }); - return result; - } + boost::json::object response; + response.emplace("description", "Successful response"); + if (descriptor.response_schema) { + boost::json::object media_type; + media_type.emplace("schema", *descriptor.response_schema); + boost::json::object content; + content.emplace("application/json", std::move(media_type)); + response.emplace("content", std::move(content)); + } + boost::json::object responses; + responses.emplace("200", std::move(response)); + operation.emplace("responses", std::move(responses)); + return operation; +} - std::string shell_single_quote(const std::string& input) - { - std::string result = "'"; - for (const char character : input) - { - if (character == '\'') result += "'\\''"; - else result += character; - } - return result + "'"; - } +void validate_documentation_path(const std::string& path, const char* name) { + const auto literal_character = [](const unsigned char character) { + return std::isalnum(character) != 0 || character == '/' || + character == '.' || character == '_' || character == '-' || + character == '~'; + }; + if (path.empty() || path.front() != '/' || + !std::all_of(path.begin(), path.end(), literal_character)) + throw std::invalid_argument(std::string(name) + + " must be an absolute literal HTTP path"); +} - std::string operation_anchor(const std::string& method, const std::string& path) - { - std::string result(method); - bool last_was_separator = false; - for (const unsigned char character : path) - { - if (std::isalnum(character) != 0) - { - result += static_cast(std::tolower(character)); - last_was_separator = false; - } - else if (!last_was_separator) - { - result += '-'; - last_was_separator = true; - } - } - while (!result.empty() && result.back() == '-') result.pop_back(); - return result; +std::string escape_html(const std::string& input) { + std::string escaped; + escaped.reserve(input.size()); + for (const char character : input) { + switch (character) { + case '&': + escaped += "&"; + break; + case '<': + escaped += "<"; + break; + case '>': + escaped += ">"; + break; + case '\"': + escaped += """; + break; + case '\'': + escaped += "'"; + break; + default: + escaped += character; + break; } + } + return escaped; +} - std::string method_class(const std::string& method) - { - if (method == "get" || method == "post" || method == "put" || method == "patch" || - method == "delete" || method == "options" || method == "head") - return method; - return "other"; - } +std::string uppercase(const std::string& input) { + std::string result(input); + std::transform(result.begin(), result.end(), result.begin(), + [](const unsigned char character) { + return static_cast(std::toupper(character)); + }); + return result; +} - bool schema_property_is_required(const boost::json::object& schema, - const boost::json::string_view property_name) - { - const auto* required = schema.if_contains("required"); - if (required == nullptr || !required->is_array()) return false; - return std::any_of(required->as_array().begin(), required->as_array().end(), - [property_name](const boost::json::value& value) - { - return value.is_string() && value.as_string() == property_name; - }); - } +std::string shell_single_quote(const std::string& input) { + std::string result = "'"; + for (const char character : input) { + if (character == '\'') + result += "'\\''"; + else + result += character; + } + return result + "'"; +} - std::string schema_type_name(const boost::json::object& schema) - { - const auto* type = schema.if_contains("type"); - if (type != nullptr && type->is_string()) return std::string(type->as_string()); - if (schema.contains("properties")) return "object"; - if (schema.contains("items")) return "array"; - return "value"; +std::string operation_anchor(const std::string& method, + const std::string& path) { + std::string result(method); + bool last_was_separator = false; + for (const unsigned char character : path) { + if (std::isalnum(character) != 0) { + result += static_cast(std::tolower(character)); + last_was_separator = false; + } else if (!last_was_separator) { + result += '-'; + last_was_separator = true; } + } + while (!result.empty() && result.back() == '-') result.pop_back(); + return result; +} - std::string render_schema_view(const boost::json::value& schema_value, - const bool include_raw = true, - const std::size_t depth = 0) - { - if (!schema_value.is_object() || depth > 8) - return "
" +
-          escape_html(boost::json::serialize(schema_value)) + "
"; - - const auto& schema = schema_value.as_object(); - const auto type = schema_type_name(schema); - std::string result = "
" - "" + escape_html(type) + ""; - const auto* format = schema.if_contains("format"); - if (format != nullptr && format->is_string()) - result += "" + escape_html(std::string(format->as_string())) + ""; - result += "
"; +std::string method_class(const std::string& method) { + if (method == "get" || method == "post" || method == "put" || + method == "patch" || method == "delete" || method == "options" || + method == "head") + return method; + return "other"; +} - const auto* properties = schema.if_contains("properties"); - if (type == "object" && properties != nullptr && properties->is_object()) - { - if (properties->as_object().empty()) - { - result += "

Object fields are not reflected.

"; - } - else - { - result += "
"; - for (const auto& property : properties->as_object()) - { - const auto required = schema_property_is_required(schema, property.key()); - result += "
" + - escape_html(std::string(property.key())) + ""; - if (property.value().is_object()) - result += "" + - escape_html(schema_type_name(property.value().as_object())) + ""; - if (required) result += "required"; - result += "
"; - if (property.value().is_object()) - { - const auto& property_schema = property.value().as_object(); - const auto* description = property_schema.if_contains("description"); - if (description != nullptr && description->is_string()) - result += "

" + - escape_html(std::string(description->as_string())) + "

"; - if (property_schema.contains("properties")) - result += render_schema_view(property.value(), false, depth + 1); - const auto* items = property_schema.if_contains("items"); - if (items != nullptr) - result += "
Items" + - render_schema_view(*items, false, depth + 1) + "
"; - } - result += "
"; - } - result += "
"; - } - } - else - { - const auto* items = schema.if_contains("items"); - if (type == "array" && items != nullptr) - result += "
Items" + - render_schema_view(*items, false, depth + 1) + "
"; - } +bool schema_property_is_required(const boost::json::object& schema, + const boost::json::string_view property_name) { + const auto* required = schema.if_contains("required"); + if (required == nullptr || !required->is_array()) return false; + return std::any_of(required->as_array().begin(), required->as_array().end(), + [property_name](const boost::json::value& value) { + return value.is_string() && + value.as_string() == property_name; + }); +} - if (include_raw) - result += "
JSON Schema
" +
-          escape_html(boost::json::serialize(schema_value)) + "
"; - return result + "
"; - } +std::string schema_type_name(const boost::json::object& schema) { + const auto* type = schema.if_contains("type"); + if (type != nullptr && type->is_string()) + return std::string(type->as_string()); + if (schema.contains("properties")) return "object"; + if (schema.contains("items")) return "array"; + return "value"; +} - boost::json::value schema_example(const boost::json::value& schema_value, - const std::size_t depth = 0) - { - if (!schema_value.is_object() || depth > 8) return nullptr; - const auto& schema = schema_value.as_object(); - const auto* example = schema.if_contains("example"); - if (example != nullptr) return *example; - const auto* default_value = schema.if_contains("default"); - if (default_value != nullptr) return *default_value; - const auto* enum_values = schema.if_contains("enum"); - if (enum_values != nullptr && enum_values->is_array() && !enum_values->as_array().empty()) - return enum_values->as_array().front(); - - const auto type = schema_type_name(schema); - if (type == "object") - { - boost::json::object result; - const auto* properties = schema.if_contains("properties"); - if (properties != nullptr && properties->is_object()) - for (const auto& property : properties->as_object()) - result.emplace(property.key(), schema_example(property.value(), depth + 1)); - return result; +std::string render_schema_view(const boost::json::value& schema_value, + const bool include_raw = true, + const std::size_t depth = 0) { + if (!schema_value.is_object() || depth > 8) + return "
" +
+           escape_html(boost::json::serialize(schema_value)) + "
"; + + const auto& schema = schema_value.as_object(); + const auto type = schema_type_name(schema); + std::string result = + "
" + "" + + escape_html(type) + ""; + const auto* format = schema.if_contains("format"); + if (format != nullptr && format->is_string()) + result += "" + + escape_html(std::string(format->as_string())) + ""; + result += "
"; + + const auto* properties = schema.if_contains("properties"); + if (type == "object" && properties != nullptr && properties->is_object()) { + if (properties->as_object().empty()) { + result += + "

Object fields are not " + "reflected.

"; + } else { + result += "
"; + for (const auto& property : properties->as_object()) { + const auto required = + schema_property_is_required(schema, property.key()); + result += + "
" + + escape_html(std::string(property.key())) + ""; + if (property.value().is_object()) + result += + "" + + escape_html(schema_type_name(property.value().as_object())) + + ""; + if (required) result += "required"; + result += "
"; + if (property.value().is_object()) { + const auto& property_schema = property.value().as_object(); + const auto* description = property_schema.if_contains("description"); + if (description != nullptr && description->is_string()) + result += "

" + + escape_html(std::string(description->as_string())) + + "

"; + if (property_schema.contains("properties")) + result += render_schema_view(property.value(), false, depth + 1); + const auto* items = property_schema.if_contains("items"); + if (items != nullptr) + result += "
Items" + + render_schema_view(*items, false, depth + 1) + "
"; + } + result += "
"; } - if (type == "array") return boost::json::array{}; - if (type == "string") return ""; - if (type == "integer" || type == "number") return 0; - if (type == "boolean") return false; - return nullptr; + result += "
"; } + } else { + const auto* items = schema.if_contains("items"); + if (type == "array" && items != nullptr) + result += "
Items" + + render_schema_view(*items, false, depth + 1) + "
"; + } - const boost::json::value* json_request_schema(const boost::json::object& operation) - { - const auto* request_body = operation.if_contains("requestBody"); - if (request_body == nullptr || !request_body->is_object()) return nullptr; - const auto* content = request_body->as_object().if_contains("content"); - if (content == nullptr || !content->is_object()) return nullptr; - const auto* media_type = content->as_object().if_contains("application/json"); - if (media_type == nullptr || !media_type->is_object()) return nullptr; - return media_type->as_object().if_contains("schema"); - } + if (include_raw) + result += + "
JSON " + "Schema
" +
+        escape_html(boost::json::serialize(schema_value)) +
+        "
"; + return result + "
"; +} + +boost::json::value schema_example(const boost::json::value& schema_value, + const std::size_t depth = 0) { + if (!schema_value.is_object() || depth > 8) return nullptr; + const auto& schema = schema_value.as_object(); + const auto* example = schema.if_contains("example"); + if (example != nullptr) return *example; + const auto* default_value = schema.if_contains("default"); + if (default_value != nullptr) return *default_value; + const auto* enum_values = schema.if_contains("enum"); + if (enum_values != nullptr && enum_values->is_array() && + !enum_values->as_array().empty()) + return enum_values->as_array().front(); + + const auto type = schema_type_name(schema); + if (type == "object") { + boost::json::object result; + const auto* properties = schema.if_contains("properties"); + if (properties != nullptr && properties->is_object()) + for (const auto& property : properties->as_object()) + result.emplace(property.key(), + schema_example(property.value(), depth + 1)); + return result; + } + if (type == "array") return boost::json::array{}; + if (type == "string") return ""; + if (type == "integer" || type == "number") return 0; + if (type == "boolean") return false; + return nullptr; +} + +const boost::json::value* json_request_schema( + const boost::json::object& operation) { + const auto* request_body = operation.if_contains("requestBody"); + if (request_body == nullptr || !request_body->is_object()) return nullptr; + const auto* content = request_body->as_object().if_contains("content"); + if (content == nullptr || !content->is_object()) return nullptr; + const auto* media_type = content->as_object().if_contains("application/json"); + if (media_type == nullptr || !media_type->is_object()) return nullptr; + return media_type->as_object().if_contains("schema"); +} + +std::string render_parameters(const boost::json::object& operation) { + std::string result = + "

Parameters

"; + const auto* parameters = operation.if_contains("parameters"); + if (parameters == nullptr || !parameters->is_array() || + parameters->as_array().empty()) + return result + "

No parameters.

"; + + result += + "
" + ""; + for (const auto& parameter_value : parameters->as_array()) { + if (!parameter_value.is_object()) continue; + const auto& parameter = parameter_value.as_object(); + const auto* name = parameter.if_contains("name"); + const auto* location = parameter.if_contains("in"); + const auto* description = parameter.if_contains("description"); + const auto* required = parameter.if_contains("required"); + const auto* schema = parameter.if_contains("schema"); + result += ""; + } + return result + "
NameLocationDescriptionRequiredSchema
" + + escape_html(name != nullptr && name->is_string() + ? std::string(name->as_string()) + : "") + + "" + + escape_html(location != nullptr && location->is_string() + ? std::string(location->as_string()) + : "") + + "" + + escape_html(description != nullptr && description->is_string() + ? std::string(description->as_string()) + : "") + + "" + + (required != nullptr && required->is_bool() && required->as_bool() + ? "Yes" + : "No") + + "" + + (schema != nullptr + ? "" + escape_html(boost::json::serialize(*schema)) + + "" + : "—") + + "
"; +} + +std::string render_request_body(const boost::json::object& operation) { + std::string result = + "

Request body

"; + const auto* request_body_value = operation.if_contains("requestBody"); + if (request_body_value == nullptr || !request_body_value->is_object()) + return result + "

No request body.

"; - std::string render_parameters(const boost::json::object& operation) - { - std::string result = "

Parameters

"; - const auto* parameters = operation.if_contains("parameters"); - if (parameters == nullptr || !parameters->is_array() || parameters->as_array().empty()) - return result + "

No parameters.

"; - - result += "
" - ""; - for (const auto& parameter_value : parameters->as_array()) - { - if (!parameter_value.is_object()) continue; - const auto& parameter = parameter_value.as_object(); - const auto* name = parameter.if_contains("name"); - const auto* location = parameter.if_contains("in"); - const auto* description = parameter.if_contains("description"); - const auto* required = parameter.if_contains("required"); - const auto* schema = parameter.if_contains("schema"); - result += ""; + const auto& request_body = request_body_value->as_object(); + const auto* required = request_body.if_contains("required"); + result += "

"; + result += required != nullptr && required->is_bool() && required->as_bool() + ? "Required" + : "Optional"; + result += "

"; + const auto* content_value = request_body.if_contains("content"); + if (content_value != nullptr && content_value->is_object()) { + for (const auto& media_type : content_value->as_object()) { + result += "
" + + escape_html(std::string(media_type.key())) + "
"; + if (media_type.value().is_object()) { + const auto* schema = + media_type.value().as_object().if_contains("schema"); + if (schema != nullptr) result += render_schema_view(*schema); } - return result + "
NameLocationDescriptionRequiredSchema
" + - escape_html(name != nullptr && name->is_string() ? std::string(name->as_string()) : "") + - "" + - escape_html(location != nullptr && location->is_string() ? - std::string(location->as_string()) : "") + "" + - escape_html(description != nullptr && description->is_string() ? - std::string(description->as_string()) : "") + "" + - (required != nullptr && required->is_bool() && required->as_bool() ? "Yes" : "No") + - "" + - (schema != nullptr ? "" + escape_html(boost::json::serialize(*schema)) + "" : "—") + - "
"; } + } + return result + ""; +} + +std::string render_responses(const boost::json::object& operation) { + std::string result = + "

Responses

"; + const auto* responses_value = operation.if_contains("responses"); + if (responses_value == nullptr || !responses_value->is_object() || + responses_value->as_object().empty()) + return result + "

No responses documented.

"; - std::string render_request_body(const boost::json::object& operation) - { - std::string result = "

Request body

"; - const auto* request_body_value = operation.if_contains("requestBody"); - if (request_body_value == nullptr || !request_body_value->is_object()) - return result + "

No request body.

"; - - const auto& request_body = request_body_value->as_object(); - const auto* required = request_body.if_contains("required"); - result += "

"; - result += required != nullptr && required->is_bool() && required->as_bool() ? "Required" : "Optional"; - result += "

"; - const auto* content_value = request_body.if_contains("content"); - if (content_value != nullptr && content_value->is_object()) - { - for (const auto& media_type : content_value->as_object()) - { + for (const auto& response_entry : responses_value->as_object()) { + result += "
" + + escape_html(std::string(response_entry.key())) + ""; + if (response_entry.value().is_object()) { + const auto& response = response_entry.value().as_object(); + const auto* description = response.if_contains("description"); + if (description != nullptr && description->is_string()) + result += "" + + escape_html(std::string(description->as_string())) + + ""; + result += "
"; + const auto* content_value = response.if_contains("content"); + if (content_value != nullptr && content_value->is_object()) { + for (const auto& media_type : content_value->as_object()) { result += "
" + - escape_html(std::string(media_type.key())) + "
"; - if (media_type.value().is_object()) - { - const auto* schema = media_type.value().as_object().if_contains("schema"); + escape_html(std::string(media_type.key())) + + "
"; + if (media_type.value().is_object()) { + const auto* schema = + media_type.value().as_object().if_contains("schema"); if (schema != nullptr) result += render_schema_view(*schema); } } } - return result + ""; + } else { + result += ""; } + result += ""; + } + return result + ""; +} - std::string render_responses(const boost::json::object& operation) - { - std::string result = "

Responses

"; - const auto* responses_value = operation.if_contains("responses"); - if (responses_value == nullptr || !responses_value->is_object() || - responses_value->as_object().empty()) - return result + "

No responses documented.

"; - - for (const auto& response_entry : responses_value->as_object()) - { - result += "
" + - escape_html(std::string(response_entry.key())) + ""; - if (response_entry.value().is_object()) - { - const auto& response = response_entry.value().as_object(); - const auto* description = response.if_contains("description"); - if (description != nullptr && description->is_string()) - result += "" + escape_html(std::string(description->as_string())) + ""; - result += "
"; - const auto* content_value = response.if_contains("content"); - if (content_value != nullptr && content_value->is_object()) - { - for (const auto& media_type : content_value->as_object()) - { - result += "
" + - escape_html(std::string(media_type.key())) + "
"; - if (media_type.value().is_object()) - { - const auto* schema = media_type.value().as_object().if_contains("schema"); - if (schema != nullptr) result += render_schema_view(*schema); - } - } - } - } - else - { - result += "
"; - } - result += ""; - } - return result + ""; - } +std::string render_operation_description(const boost::json::object& operation) { + std::string result; + const auto* summary = operation.if_contains("summary"); + if (summary != nullptr && summary->is_string()) + result += "

" + + escape_html(std::string(summary->as_string())) + "

"; + const auto* description = operation.if_contains("description"); + if (description != nullptr && description->is_string()) + result += "

" + + escape_html(std::string(description->as_string())) + "

"; + return result; +} - std::string render_operation_description(const boost::json::object& operation) - { - std::string result; - const auto* summary = operation.if_contains("summary"); - if (summary != nullptr && summary->is_string()) - result += "

" + - escape_html(std::string(summary->as_string())) + "

"; - const auto* description = operation.if_contains("description"); - if (description != nullptr && description->is_string()) - result += "

" + - escape_html(std::string(description->as_string())) + "

"; - return result; - } +std::string render_try_panel(const boost::json::object& operation, + const std::string& method, + const std::string& path) { + const auto upper_method = uppercase(method); + const auto* request_schema = json_request_schema(operation); + const std::string request_body = + request_schema != nullptr + ? boost::json::serialize(schema_example(*request_schema)) + : ""; + std::string curl_command = + "curl -i -X " + upper_method + " " + shell_single_quote(path); + if (!request_body.empty() && upper_method != "GET" && upper_method != "HEAD") + curl_command += " \\\n -H " + + shell_single_quote("Content-Type: application/json") + + " \\\n --data-binary " + shell_single_quote(request_body); - std::string render_try_panel(const boost::json::object& operation, - const std::string& method, - const std::string& path) - { - const auto upper_method = uppercase(method); - const auto* request_schema = json_request_schema(operation); - const std::string request_body = request_schema != nullptr ? - boost::json::serialize(schema_example(*request_schema)) : ""; - std::string curl_command = "curl -i -X " + upper_method + " " + shell_single_quote(path); - if (!request_body.empty() && upper_method != "GET" && upper_method != "HEAD") - curl_command += " \\\n -H " + shell_single_quote("Content-Type: application/json") + - " \\\n --data-binary " + shell_single_quote(request_body); - - std::string result = "
"; - result += "Try request
" - "

Requests are sent to this server without browser credentials.

"; - - const auto* parameters = operation.if_contains("parameters"); - if (parameters != nullptr && parameters->is_array()) - { - for (const auto& parameter_value : parameters->as_array()) - { - if (!parameter_value.is_object()) continue; - const auto& parameter = parameter_value.as_object(); - const auto* location = parameter.if_contains("in"); - const auto* name = parameter.if_contains("name"); - if (location == nullptr || !location->is_string() || name == nullptr || !name->is_string()) - continue; - const std::string parameter_name(name->as_string()); - const auto* required = parameter.if_contains("required"); - const bool is_required = required != nullptr && required->is_bool() && required->as_bool(); - if (location->as_string() == "path") - { - result += ""; - } - else if (location->as_string() == "header") - { - result += ""; - } - } - } + std::string result = + "
"; + result += + "Try request
" + "

Requests are sent to this server without browser " + "credentials.

"; - if (request_schema != nullptr) - result += ""; - - result += "
" +
-        escape_html(curl_command) + "
"; - return result; + const auto* parameters = operation.if_contains("parameters"); + if (parameters != nullptr && parameters->is_array()) { + for (const auto& parameter_value : parameters->as_array()) { + if (!parameter_value.is_object()) continue; + const auto& parameter = parameter_value.as_object(); + const auto* location = parameter.if_contains("in"); + const auto* name = parameter.if_contains("name"); + if (location == nullptr || !location->is_string() || name == nullptr || + !name->is_string()) + continue; + const std::string parameter_name(name->as_string()); + const auto* required = parameter.if_contains("required"); + const bool is_required = + required != nullptr && required->is_bool() && required->as_bool(); + if (location->as_string() == "path") { + result += + ""; + } else if (location->as_string() == "header") { + result += + ""; + } } + } + + if (request_schema != nullptr) + result += + ""; - std::string documentation_script() - { - return R"SCRIPT((() => { + result += + "
" +
+      escape_html(curl_command) +
+      "
"; + return result; +} + +std::string documentation_script() { + return R"SCRIPT((() => { const shellQuote = value => "'" + value.replace(/'/g, "'\\''") + "'"; function requestFor(panel, requireParameters) { @@ -657,258 +688,366 @@ namespace khttpd::framework } }); })();)SCRIPT"; - } +} - std::string make_csp_nonce() - { - static constexpr char hexadecimal[] = "0123456789abcdef"; - std::random_device source; - std::string nonce; - nonce.reserve(32); - for (std::size_t index = 0; index < 16; ++index) - { - const auto value = source(); - nonce += hexadecimal[(value >> 4U) & 0x0fU]; - nonce += hexadecimal[value & 0x0fU]; - } - return nonce; +std::string make_csp_nonce() { + static constexpr char hexadecimal[] = "0123456789abcdef"; + std::random_device source; + std::string nonce; + nonce.reserve(32); + for (std::size_t index = 0; index < 16; ++index) { + const auto value = source(); + nonce += hexadecimal[(value >> 4U) & 0x0fU]; + nonce += hexadecimal[value & 0x0fU]; + } + return nonce; +} + +std::string make_documentation_page(const boost::json::object& document, + const OpenApiInfo& info, + const std::string& spec_path, + const std::string& script_nonce) { + const auto& paths = document.at("paths").as_object(); + struct OperationView { + std::string path; + std::string method; + std::string anchor; + const boost::json::object* operation; + }; + std::vector operations; + std::map anchors; + for (const auto& path : paths) { + for (const auto& operation : path.value().as_object()) { + const std::string method(operation.key()); + std::string anchor = operation_anchor(method, std::string(path.key())); + const auto occurrence = ++anchors[anchor]; + if (occurrence > 1) anchor += "-" + std::to_string(occurrence); + operations.push_back({std::string(path.key()), method, std::move(anchor), + &operation.value().as_object()}); } + } - std::string make_documentation_page(const boost::json::object& document, - const OpenApiInfo& info, - const std::string& spec_path, - const std::string& script_nonce) - { - const auto& paths = document.at("paths").as_object(); - struct OperationView - { - std::string path; - std::string method; - std::string anchor; - const boost::json::object* operation; - }; - std::vector operations; - std::map anchors; - for (const auto& path : paths) - { - for (const auto& operation : path.value().as_object()) - { - const std::string method(operation.key()); - std::string anchor = operation_anchor(method, std::string(path.key())); - const auto occurrence = ++anchors[anchor]; - if (occurrence > 1) anchor += "-" + std::to_string(occurrence); - operations.push_back({std::string(path.key()), method, std::move(anchor), - &operation.value().as_object()}); - } - } + std::string page = + "" + "" + + escape_html(info.title) + + " · API documentation

" + "OpenAPI 3.1

" + + escape_html(info.title) + "

Version " + + escape_html(info.version) + "

View raw specification  ↗

API " + "reference

" + "

Select an endpoint to inspect its contract and schemas.

"; - boost::json::object generate_openapi(const HttpRouter& router, const OpenApiInfo& info) - { - using Operations = std::map; - std::map documented_paths; + for (const auto& operation : operations) { + const auto css_class = method_class(operation.method); + page += "
" + + escape_html(uppercase(operation.method)) + "

" + + escape_html(operation.path) + + "

" + + render_operation_description(*operation.operation) + + render_parameters(*operation.operation) + + render_request_body(*operation.operation) + + render_responses(*operation.operation) + + render_try_panel(*operation.operation, operation.method, + operation.path) + + "
Raw operation JSON
" +
+            escape_html(boost::json::serialize(*operation.operation)) +
+            "
"; + } + page += "
"; + return page; +} +} // namespace - for (const auto& descriptor : router.route_descriptors()) - { - auto path = document_path(descriptor.path); - documented_paths[path.path][method_name(descriptor.method)] = - make_operation(descriptor, path.parameters); - } +boost::json::object generate_openapi(const HttpRouter& router, + const OpenApiInfo& info) { + using Operations = std::map; + std::map documented_paths; - boost::json::object paths; - for (auto& [path, methods] : documented_paths) - { - boost::json::object path_item; - for (auto& [method, operation] : methods) - path_item.emplace(method, std::move(operation)); - paths.emplace(path, std::move(path_item)); - } + for (const auto& descriptor : router.route_descriptors()) { + auto path = document_path(descriptor.path); + documented_paths[path.path][method_name(descriptor.method)] = + make_operation(descriptor, path.parameters); + } - boost::json::object document; - document.emplace("openapi", "3.1.0"); - document.emplace("info", boost::json::object{{"title", info.title}, {"version", info.version}}); - document.emplace("paths", std::move(paths)); - return document; + boost::json::object paths; + for (auto& [path, methods] : documented_paths) { + boost::json::object path_item; + for (auto& [method, operation] : methods) + path_item.emplace(method, std::move(operation)); + paths.emplace(path, std::move(path_item)); } - void install_openapi_routes(HttpRouter& router, const OpenApiInfo& info, - const std::string& spec_path, const std::string& docs_path, - const bool enabled) - { - if (!enabled) return; - - validate_documentation_path(spec_path, "OpenAPI specification path"); - validate_documentation_path(docs_path, "OpenAPI documentation path"); - if (spec_path == docs_path) - throw std::invalid_argument("OpenAPI specification and documentation paths must differ"); - - for (const auto& descriptor : router.route_descriptors()) - { - if (descriptor.method == boost::beast::http::verb::get && - (descriptor.path == spec_path || descriptor.path == docs_path)) - throw std::invalid_argument("OpenAPI documentation path conflicts with an existing GET route: " + - descriptor.path); - } + boost::json::object document; + document.emplace("openapi", "3.1.0"); + document.emplace("info", boost::json::object{{"title", info.title}, + {"version", info.version}}); + document.emplace("paths", std::move(paths)); + return document; +} + +void install_openapi_routes(HttpRouter& router, const OpenApiInfo& info, + const std::string& spec_path, + const std::string& docs_path, const bool enabled) { + if (!enabled) return; + + validate_documentation_path(spec_path, "OpenAPI specification path"); + validate_documentation_path(docs_path, "OpenAPI documentation path"); + if (spec_path == docs_path) + throw std::invalid_argument( + "OpenAPI specification and documentation paths must differ"); + + for (const auto& descriptor : router.route_descriptors()) { + if (descriptor.method == boost::beast::http::verb::get && + (descriptor.path == spec_path || descriptor.path == docs_path)) + throw std::invalid_argument( + "OpenAPI documentation path conflicts with an existing GET route: " + + descriptor.path); + } - const auto document = generate_openapi(router, info); - auto serialized_document = std::make_shared(boost::json::serialize(document)); - router.add_route(spec_path, boost::beast::http::verb::get, - [serialized_document](HttpContext& context) - { + const auto document = generate_openapi(router, info); + auto serialized_document = + std::make_shared(boost::json::serialize(document)); + router.add_route( + spec_path, boost::beast::http::verb::get, + [serialized_document](HttpContext& context) { context.set_status(boost::beast::http::status::ok); context.set_content_type("application/json"); context.set_header("X-Content-Type-Options", "nosniff"); context.set_body(*serialized_document); - }, std::nullopt, std::nullopt, false); + }, + std::nullopt, std::nullopt, false); - auto script_nonce = std::make_shared(make_csp_nonce()); - auto page = std::make_shared( + auto script_nonce = std::make_shared(make_csp_nonce()); + auto page = std::make_shared( make_documentation_page(document, info, spec_path, *script_nonce)); - router.add_route(docs_path, boost::beast::http::verb::get, - [page, script_nonce](HttpContext& context) - { + router.add_route( + docs_path, boost::beast::http::verb::get, + [page, script_nonce](HttpContext& context) { context.set_status(boost::beast::http::status::ok); context.set_content_type("text/html"); context.set_header("X-Content-Type-Options", "nosniff"); - context.set_header("Content-Security-Policy", - "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; " - "script-src 'nonce-" + *script_nonce + "'; connect-src 'self'; " - "form-action 'none'; frame-ancestors 'none'"); + context.set_header( + "Content-Security-Policy", + "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; " + "script-src 'nonce-" + + *script_nonce + + "'; connect-src 'self'; " + "form-action 'none'; frame-ancestors 'none'"); context.set_header("Referrer-Policy", "no-referrer"); context.set_body(*page); - }, std::nullopt, std::nullopt, false); - } + }, + std::nullopt, std::nullopt, false); +} - void export_openapi(const HttpRouter& router, const std::string& output_path, - const OpenApiInfo& info) - { - if (output_path.empty()) throw std::invalid_argument("OpenAPI output path must not be empty"); +void export_openapi(const HttpRouter& router, const std::string& output_path, + const OpenApiInfo& info) { + if (output_path.empty()) + throw std::invalid_argument("OpenAPI output path must not be empty"); - std::ofstream output(output_path, std::ios::binary | std::ios::trunc); - if (!output.is_open()) - throw std::runtime_error("Unable to open OpenAPI output path: " + output_path); + std::ofstream output(output_path, std::ios::binary | std::ios::trunc); + if (!output.is_open()) + throw std::runtime_error("Unable to open OpenAPI output path: " + + output_path); - output << boost::json::serialize(generate_openapi(router, info)) << '\n'; - output.close(); - if (!output) - throw std::runtime_error("Unable to write OpenAPI output path: " + output_path); - } + output << boost::json::serialize(generate_openapi(router, info)) << '\n'; + output.close(); + if (!output) + throw std::runtime_error("Unable to write OpenAPI output path: " + + output_path); } +} // namespace khttpd::framework diff --git a/framework/router/openapi.hpp b/framework/router/openapi.hpp index 5d30c40..96bbd73 100644 --- a/framework/router/openapi.hpp +++ b/framework/router/openapi.hpp @@ -2,28 +2,26 @@ #define KHTTPD_FRAMEWORK_ROUTER_OPENAPI_HPP_ #include - #include -namespace khttpd::framework -{ - class HttpRouter; +namespace khttpd::framework { +class HttpRouter; - struct OpenApiInfo - { - std::string title = "khttpd API"; - std::string version = "1.0.0"; - }; +struct OpenApiInfo { + std::string title = "khttpd API"; + std::string version = "1.0.0"; +}; - boost::json::object generate_openapi(const HttpRouter& router, const OpenApiInfo& info = {}); +boost::json::object generate_openapi(const HttpRouter& router, + const OpenApiInfo& info = {}); - void install_openapi_routes(HttpRouter& router, const OpenApiInfo& info = {}, - const std::string& spec_path = "/openapi.json", - const std::string& docs_path = "/docs", - bool enabled = true); +void install_openapi_routes(HttpRouter& router, const OpenApiInfo& info = {}, + const std::string& spec_path = "/openapi.json", + const std::string& docs_path = "/docs", + bool enabled = true); - void export_openapi(const HttpRouter& router, const std::string& output_path, - const OpenApiInfo& info = {}); -} +void export_openapi(const HttpRouter& router, const std::string& output_path, + const OpenApiInfo& info = {}); +} // namespace khttpd::framework -#endif // KHTTPD_FRAMEWORK_ROUTER_OPENAPI_HPP_ +#endif // KHTTPD_FRAMEWORK_ROUTER_OPENAPI_HPP_ diff --git a/framework/router/openapi_schema.hpp b/framework/router/openapi_schema.hpp index 0ef33b8..64b7336 100644 --- a/framework/router/openapi_schema.hpp +++ b/framework/router/openapi_schema.hpp @@ -4,121 +4,105 @@ #include #include #include - #include #include #include #include #include -namespace khttpd::framework -{ - // Specialize this trait for a described DTO to add OpenAPI descriptions to its fields. - template - struct OpenApiFieldDocumentation - { - static std::string_view description(std::string_view) { return {}; } - }; -} +namespace khttpd::framework { +// Specialize this trait for a described DTO to add OpenAPI descriptions to its +// fields. +template +struct OpenApiFieldDocumentation { + static std::string_view description(std::string_view) { return {}; } +}; +} // namespace khttpd::framework -// Declares OpenAPI documentation for fields reflected with BOOST_DESCRIBE_STRUCT. -// Place one KHTTPD_OPENAPI_FIELD entry per documented DTO member. +// Declares OpenAPI documentation for fields reflected with +// BOOST_DESCRIBE_STRUCT. Place one KHTTPD_OPENAPI_FIELD entry per documented +// DTO member. #define KHTTPD_OPENAPI_FIELD(member, text) \ if (field == #member) return text; -#define KHTTPD_OPENAPI_FIELD_DOCUMENTATION(Type, ...) \ - template <> \ - struct khttpd::framework::OpenApiFieldDocumentation \ - { \ - static std::string_view description(const std::string_view field) \ - { \ - __VA_ARGS__ \ - return {}; \ - } \ +#define KHTTPD_OPENAPI_FIELD_DOCUMENTATION(Type, ...) \ + template <> \ + struct khttpd::framework::OpenApiFieldDocumentation { \ + static std::string_view description(const std::string_view field) { \ + __VA_ARGS__ \ + return {}; \ + } \ }; -namespace khttpd::framework::detail -{ - template - struct is_optional : std::false_type {}; +namespace khttpd::framework::detail { +template +struct is_optional : std::false_type {}; - template - struct is_optional> : std::true_type - { - using value_type = T; - }; +template +struct is_optional> : std::true_type { + using value_type = T; +}; - template - inline constexpr bool is_optional_v = is_optional::value; +template +inline constexpr bool is_optional_v = is_optional::value; - template - struct is_vector : std::false_type {}; +template +struct is_vector : std::false_type {}; - template - struct is_vector> : std::true_type - { - using value_type = T; - }; - - template - boost::json::value openapi_schema() - { - using Value = std::remove_cv_t>; - boost::json::object schema; +template +struct is_vector> : std::true_type { + using value_type = T; +}; - if constexpr (is_optional_v) - { - return openapi_schema::value_type>(); - } - else if constexpr (std::is_same_v) - { - schema.emplace("type", "boolean"); - } - else if constexpr (std::is_integral_v) - { - schema.emplace("type", "integer"); - } - else if constexpr (std::is_floating_point_v) - { - schema.emplace("type", "number"); - } - else if constexpr (std::is_same_v || std::is_enum_v) - { - schema.emplace("type", "string"); - } - else if constexpr (is_vector::value) - { - schema.emplace("type", "array"); - schema.emplace("items", openapi_schema::value_type>()); - } - else if constexpr (boost::describe::has_describe_members::value) - { - boost::json::object properties; - boost::json::array required; - using Members = boost::describe::describe_members; - boost::mp11::mp_for_each([&](auto descriptor) - { - using Member = std::remove_cv_t().*descriptor.pointer)>>; - auto property_schema = openapi_schema().as_object(); - const auto description = OpenApiFieldDocumentation::description(descriptor.name); - if (!description.empty()) property_schema.emplace("description", description); - properties.emplace(descriptor.name, std::move(property_schema)); - if constexpr (!is_optional_v) required.emplace_back(descriptor.name); - }); - schema.emplace("type", "object"); - schema.emplace("properties", std::move(properties)); - if (!required.empty()) schema.emplace("required", std::move(required)); - } - else - { - // C++17 cannot reflect arbitrary tag_invoke converters. They remain valid typed - // routes, but their generated schema is intentionally conservative. - schema.emplace("type", "object"); - schema.emplace("properties", boost::json::object{}); - } +template +boost::json::value openapi_schema() { + using Value = std::remove_cv_t>; + boost::json::object schema; - return schema; + if constexpr (is_optional_v) { + return openapi_schema::value_type>(); + } else if constexpr (std::is_same_v) { + schema.emplace("type", "boolean"); + } else if constexpr (std::is_integral_v) { + schema.emplace("type", "integer"); + } else if constexpr (std::is_floating_point_v) { + schema.emplace("type", "number"); + } else if constexpr (std::is_same_v || + std::is_enum_v) { + schema.emplace("type", "string"); + } else if constexpr (is_vector::value) { + schema.emplace("type", "array"); + schema.emplace("items", + openapi_schema::value_type>()); + } else if constexpr (boost::describe::has_describe_members::value) { + boost::json::object properties; + boost::json::array required; + using Members = + boost::describe::describe_members; + boost::mp11::mp_for_each([&](auto descriptor) { + using Member = std::remove_cv_t().*descriptor.pointer)>>; + auto property_schema = openapi_schema().as_object(); + const auto description = + OpenApiFieldDocumentation::description(descriptor.name); + if (!description.empty()) + property_schema.emplace("description", description); + properties.emplace(descriptor.name, std::move(property_schema)); + if constexpr (!is_optional_v) + required.emplace_back(descriptor.name); + }); + schema.emplace("type", "object"); + schema.emplace("properties", std::move(properties)); + if (!required.empty()) schema.emplace("required", std::move(required)); + } else { + // C++17 cannot reflect arbitrary tag_invoke converters. They remain valid + // typed routes, but their generated schema is intentionally conservative. + schema.emplace("type", "object"); + schema.emplace("properties", boost::json::object{}); } + + return schema; } +} // namespace khttpd::framework::detail -#endif // KHTTPD_FRAMEWORK_ROUTER_OPENAPI_SCHEMA_HPP_ +#endif // KHTTPD_FRAMEWORK_ROUTER_OPENAPI_SCHEMA_HPP_ diff --git a/framework/router/route_parameter.hpp b/framework/router/route_parameter.hpp index 1513e56..58f2452 100644 --- a/framework/router/route_parameter.hpp +++ b/framework/router/route_parameter.hpp @@ -2,85 +2,81 @@ #define KHTTPD_FRAMEWORK_ROUTER_ROUTE_PARAMETER_HPP_ #include - #include #include #include #include #include -namespace khttpd::framework -{ - enum class RouteParameterLocation - { - path, - query, - }; - - struct RouteParameterDocumentation - { - std::string name; - RouteParameterLocation location; - bool required; - boost::json::value schema; - }; - - template - struct PathParam - { - using value_type = T; - - explicit PathParam(std::string parameter_name) : name(std::move(parameter_name)) {} - - std::string name; - }; - - template - struct QueryParam - { - using value_type = T; - - explicit QueryParam(std::string parameter_name) : name(std::move(parameter_name)) {} - QueryParam(std::string parameter_name, T default_parameter_value) - : name(std::move(parameter_name)), default_value(std::move(default_parameter_value)) {} - - std::string name; - std::optional default_value; - }; - - template - struct Body - { - using value_type = T; - }; -} - -namespace khttpd::framework::detail -{ - template - struct is_route_parameter_descriptor : std::false_type {}; - - template - struct is_route_parameter_descriptor> : std::true_type {}; - - template - struct is_route_parameter_descriptor> : std::true_type {}; - - template - struct is_route_parameter_descriptor> : std::true_type {}; - - template - inline constexpr bool is_route_parameter_descriptor_v = +namespace khttpd::framework { +enum class RouteParameterLocation { + path, + query, +}; + +struct RouteParameterDocumentation { + std::string name; + RouteParameterLocation location; + bool required; + boost::json::value schema; +}; + +template +struct PathParam { + using value_type = T; + + explicit PathParam(std::string parameter_name) + : name(std::move(parameter_name)) {} + + std::string name; +}; + +template +struct QueryParam { + using value_type = T; + + explicit QueryParam(std::string parameter_name) + : name(std::move(parameter_name)) {} + QueryParam(std::string parameter_name, T default_parameter_value) + : name(std::move(parameter_name)), + default_value(std::move(default_parameter_value)) {} + + std::string name; + std::optional default_value; +}; + +template +struct Body { + using value_type = T; +}; +} // namespace khttpd::framework + +namespace khttpd::framework::detail { +template +struct is_route_parameter_descriptor : std::false_type {}; + +template +struct is_route_parameter_descriptor> : std::true_type {}; + +template +struct is_route_parameter_descriptor> : std::true_type {}; + +template +struct is_route_parameter_descriptor> : std::true_type {}; + +template +inline constexpr bool is_route_parameter_descriptor_v = is_route_parameter_descriptor>::value; - template - struct is_body_descriptor : std::false_type {}; +template +struct is_body_descriptor : std::false_type {}; - template - struct is_body_descriptor> : std::true_type {}; +template +struct is_body_descriptor> : std::true_type {}; - template - inline constexpr bool is_body_descriptor_v = is_body_descriptor>::value; -} +template +inline constexpr bool is_body_descriptor_v = + is_body_descriptor>::value; +} // namespace khttpd::framework::detail #endif diff --git a/framework/router/typed_route.hpp b/framework/router/typed_route.hpp index 8fe238e..edea970 100644 --- a/framework/router/typed_route.hpp +++ b/framework/router/typed_route.hpp @@ -1,16 +1,11 @@ #ifndef KHTTPD_FRAMEWORK_ROUTER_TYPED_ROUTE_HPP_ #define KHTTPD_FRAMEWORK_ROUTER_TYPED_ROUTE_HPP_ -#include "router/http_result.hpp" -#include "router/openapi_schema.hpp" -#include "router/route_parameter.hpp" - -#include - #include +#include +#include #include #include -#include #include #include #include @@ -18,454 +13,439 @@ #include #include #include -#include #include +#include #include -namespace khttpd::framework -{ - // Thrown when a typed route cannot parse a request body before invoking its handler. - class TypedRequestValidationError final : public std::runtime_error - { - public: - using std::runtime_error::runtime_error; - }; +#include "router/http_result.hpp" +#include "router/openapi_schema.hpp" +#include "router/route_parameter.hpp" - class TypedParameterValidationError final : public std::runtime_error - { - public: - using std::runtime_error::runtime_error; - }; +namespace khttpd::framework { +// Thrown when a typed route cannot parse a request body before invoking its +// handler. +class TypedRequestValidationError final : public std::runtime_error { + public: + using std::runtime_error::runtime_error; +}; + +class TypedParameterValidationError final : public std::runtime_error { + public: + using std::runtime_error::runtime_error; +}; +} // namespace khttpd::framework + +namespace khttpd::framework::detail { +struct TypedRouteHandler { + std::function handler; + std::optional request_schema; + std::optional response_schema; + std::vector parameters; +}; + +template +struct typed_response_body { + using type = T; +}; + +template +struct typed_response_body> { + using type = T; +}; + +template +struct callable_signature { + using return_type = R; + static constexpr std::size_t arity = sizeof...(Args); + + template + using argument = std::tuple_element_t>; +}; + +template +struct callable_traits + : callable_traits::operator())> {}; + +template +struct callable_traits : callable_signature {}; + +template +struct callable_traits : callable_signature {}; + +template +struct callable_traits> + : callable_signature {}; + +template +struct callable_traits : callable_signature {}; + +template +struct callable_traits + : callable_signature {}; + +template +using remove_cvref_t = std::remove_cv_t>; + +inline void write_invalid_request_body(HttpContext& context) { + context.set_status(boost::beast::http::status::bad_request); + boost::json::object error; + error.emplace("code", "INVALID_REQUEST_BODY"); + error.emplace("message", + "Request body must be valid JSON matching the expected schema"); + context.set_body_json(error); } -namespace khttpd::framework::detail -{ - struct TypedRouteHandler - { - std::function handler; - std::optional request_schema; - std::optional response_schema; - std::vector parameters; - }; - - template - struct typed_response_body - { - using type = T; - }; - - template - struct typed_response_body> - { - using type = T; - }; - - template - struct callable_signature - { - using return_type = R; - static constexpr std::size_t arity = sizeof...(Args); - - template - using argument = std::tuple_element_t>; - }; - - template - struct callable_traits : callable_traits::operator())> {}; - - template - struct callable_traits : callable_signature {}; - - template - struct callable_traits : callable_signature {}; - - template - struct callable_traits> : callable_signature {}; - - template - struct callable_traits : callable_signature {}; - - template - struct callable_traits : callable_signature {}; - - template - using remove_cvref_t = std::remove_cv_t>; - - inline void write_invalid_request_body(HttpContext& context) - { - context.set_status(boost::beast::http::status::bad_request); - boost::json::object error; - error.emplace("code", "INVALID_REQUEST_BODY"); - error.emplace("message", "Request body must be valid JSON matching the expected schema"); - context.set_body_json(error); - } - - inline void write_invalid_request_parameter(HttpContext& context, const std::string& message) - { - context.set_status(boost::beast::http::status::bad_request); - boost::json::object error; - error.emplace("code", "INVALID_REQUEST_PARAMETER"); - error.emplace("message", message); - context.set_body_json(error); - } - - inline bool is_json_media_type(std::string content_type) - { - if (const auto semicolon = content_type.find(';'); semicolon != std::string::npos) - { - content_type.resize(semicolon); - } +inline void write_invalid_request_parameter(HttpContext& context, + const std::string& message) { + context.set_status(boost::beast::http::status::bad_request); + boost::json::object error; + error.emplace("code", "INVALID_REQUEST_PARAMETER"); + error.emplace("message", message); + context.set_body_json(error); +} - const auto first = std::find_if_not(content_type.begin(), content_type.end(), [](const unsigned char c) - { - return std::isspace(c) != 0; - }); - const auto last = std::find_if_not(content_type.rbegin(), content_type.rend(), [](const unsigned char c) - { - return std::isspace(c) != 0; - }).base(); - if (first >= last) return false; - - const auto media_type = ascii_lower(std::string(first, last)); - constexpr std::string_view prefix = "application/"; - constexpr std::string_view suffix = "+json"; - return media_type == "application/json" || - (media_type.size() > prefix.size() + suffix.size() && - media_type.compare(0, prefix.size(), prefix) == 0 && - media_type.compare(media_type.size() - suffix.size(), suffix.size(), suffix) == 0); +inline bool is_json_media_type(std::string content_type) { + if (const auto semicolon = content_type.find(';'); + semicolon != std::string::npos) { + content_type.resize(semicolon); } - template - T parse_parameter_value(const std::string& value, const char* location, const std::string& name) - { - using Value = remove_cvref_t; - if constexpr (is_optional_v) - { - using Item = typename is_optional::value_type; - return Value{parse_parameter_value(value, location, name)}; - } - else if constexpr (std::is_same_v) - { - return value; - } - else if constexpr (std::is_same_v) - { - if (value == "true") return true; - if (value == "false") return false; - } - else if constexpr (std::is_integral_v) - { - Value converted{}; - const auto result = std::from_chars(value.data(), value.data() + value.size(), converted); - if (result.ec == std::errc{} && result.ptr == value.data() + value.size()) return converted; - } - else if constexpr (std::is_floating_point_v) - { - Value converted{}; - const auto result = std::from_chars(value.data(), value.data() + value.size(), converted); - if (result.ec == std::errc{} && result.ptr == value.data() + value.size()) return converted; - } - else - { - static_assert(std::is_same_v, - "route parameters support string, bool, integral, and floating-point values"); - } + const auto first = std::find_if_not( + content_type.begin(), content_type.end(), + [](const unsigned char c) { return std::isspace(c) != 0; }); + const auto last = std::find_if_not(content_type.rbegin(), content_type.rend(), + [](const unsigned char c) { + return std::isspace(c) != 0; + }) + .base(); + if (first >= last) return false; + + const auto media_type = ascii_lower(std::string(first, last)); + constexpr std::string_view prefix = "application/"; + constexpr std::string_view suffix = "+json"; + return media_type == "application/json" || + (media_type.size() > prefix.size() + suffix.size() && + media_type.compare(0, prefix.size(), prefix) == 0 && + media_type.compare(media_type.size() - suffix.size(), suffix.size(), + suffix) == 0); +} - throw TypedParameterValidationError( - std::string("Invalid ") + location + " parameter '" + name + "'"); +template +T parse_parameter_value(const std::string& value, const char* location, + const std::string& name) { + using Value = remove_cvref_t; + if constexpr (is_optional_v) { + using Item = typename is_optional::value_type; + return Value{parse_parameter_value(value, location, name)}; + } else if constexpr (std::is_same_v) { + return value; + } else if constexpr (std::is_same_v) { + if (value == "true") return true; + if (value == "false") return false; + } else if constexpr (std::is_integral_v) { + Value converted{}; + const auto result = + std::from_chars(value.data(), value.data() + value.size(), converted); + if (result.ec == std::errc{} && result.ptr == value.data() + value.size()) + return converted; + } else if constexpr (std::is_floating_point_v) { + Value converted{}; + const auto result = + std::from_chars(value.data(), value.data() + value.size(), converted); + if (result.ec == std::errc{} && result.ptr == value.data() + value.size()) + return converted; + } else { + static_assert(std::is_same_v, + "route parameters support string, bool, integral, and " + "floating-point values"); } - template - T read_route_parameter(const PathParam& descriptor, HttpContext& context) - { - const auto value = context.get_path_param(descriptor.name); - if (!value) - throw TypedParameterValidationError("Missing path parameter '" + descriptor.name + "'"); - return parse_parameter_value(*value, "path", descriptor.name); - } + throw TypedParameterValidationError(std::string("Invalid ") + location + + " parameter '" + name + "'"); +} - template - T read_route_parameter(const QueryParam& descriptor, HttpContext& context) - { - const auto value = context.get_query_param(descriptor.name); - if (value) return parse_parameter_value(*value, "query", descriptor.name); - if (descriptor.default_value) return *descriptor.default_value; - if constexpr (is_optional_v) return std::nullopt; - throw TypedParameterValidationError("Missing query parameter '" + descriptor.name + "'"); - } +template +T read_route_parameter(const PathParam& descriptor, HttpContext& context) { + const auto value = context.get_path_param(descriptor.name); + if (!value) + throw TypedParameterValidationError("Missing path parameter '" + + descriptor.name + "'"); + return parse_parameter_value(*value, "path", descriptor.name); +} - template - T read_route_parameter(const Body&, HttpContext& context) - { - const auto content_type = context.get_header(boost::beast::http::field::content_type); - if (!content_type || !is_json_media_type(*content_type)) - throw TypedRequestValidationError("Request body must be valid JSON matching the expected schema"); +template +T read_route_parameter(const QueryParam& descriptor, HttpContext& context) { + const auto value = context.get_query_param(descriptor.name); + if (value) return parse_parameter_value(*value, "query", descriptor.name); + if (descriptor.default_value) return *descriptor.default_value; + if constexpr (is_optional_v) return std::nullopt; + throw TypedParameterValidationError("Missing query parameter '" + + descriptor.name + "'"); +} - try - { - return boost::json::value_to(boost::json::parse(context.body())); - } - catch (const std::bad_alloc&) - { - throw; - } - catch (const std::exception&) - { - throw TypedRequestValidationError("Request body must be valid JSON matching the expected schema"); - } +template +T read_route_parameter(const Body&, HttpContext& context) { + const auto content_type = + context.get_header(boost::beast::http::field::content_type); + if (!content_type || !is_json_media_type(*content_type)) + throw TypedRequestValidationError( + "Request body must be valid JSON matching the expected schema"); + + try { + return boost::json::value_to(boost::json::parse(context.body())); + } catch (const std::bad_alloc&) { + throw; + } catch (const std::exception&) { + throw TypedRequestValidationError( + "Request body must be valid JSON matching the expected schema"); } +} - template - void set_request_schema(std::optional& schema) - { - using Value = std::decay_t; - if constexpr (std::is_same_v>) - schema.emplace(openapi_schema()); - } +template +void set_request_schema(std::optional& schema) { + using Value = std::decay_t; + if constexpr (std::is_same_v>) + schema.emplace(openapi_schema()); +} - template - void append_parameter_documentation(std::vector& parameters, - const PathParam& descriptor) - { - parameters.push_back( - {descriptor.name, RouteParameterLocation::path, true, openapi_schema()}); - } +template +void append_parameter_documentation( + std::vector& parameters, + const PathParam& descriptor) { + parameters.push_back({descriptor.name, RouteParameterLocation::path, true, + openapi_schema()}); +} - template - void append_parameter_documentation(std::vector& parameters, - const QueryParam& descriptor) - { - auto schema = openapi_schema(); - if (descriptor.default_value) - { - if constexpr (is_optional_v) - { - if (*descriptor.default_value) - schema.as_object().emplace("default", boost::json::value_from(**descriptor.default_value)); - } - else - { - schema.as_object().emplace("default", boost::json::value_from(*descriptor.default_value)); - } +template +void append_parameter_documentation( + std::vector& parameters, + const QueryParam& descriptor) { + auto schema = openapi_schema(); + if (descriptor.default_value) { + if constexpr (is_optional_v) { + if (*descriptor.default_value) + schema.as_object().emplace( + "default", boost::json::value_from(**descriptor.default_value)); + } else { + schema.as_object().emplace( + "default", boost::json::value_from(*descriptor.default_value)); } - parameters.push_back({descriptor.name, RouteParameterLocation::query, - !descriptor.default_value && !is_optional_v, std::move(schema)}); } + parameters.push_back({descriptor.name, RouteParameterLocation::query, + !descriptor.default_value && !is_optional_v, + std::move(schema)}); +} - template - void append_parameter_documentation(std::vector&, const Body&) - { - } +template +void append_parameter_documentation(std::vector&, + const Body&) {} + +template +TypedRouteHandler make_parameterized_typed_handler_with_response( + Handler&& input_handler, Descriptors&&... input_descriptors) { + static_assert((is_route_parameter_descriptor_v && ...), + "all typed route bindings must be route parameter descriptors"); + static_assert( + (0U + ... + (is_body_descriptor_v ? 1U : 0U)) <= 1U, + "a typed route can register at most one Body descriptor"); + using StoredHandler = std::decay_t; + static_assert(!std::is_void_v, + "typed handlers must return a body or HttpResult"); + static_assert(!std::is_reference_v, + "typed handlers must return responses by value"); + + std::vector parameters; + (append_parameter_documentation(parameters, input_descriptors), ...); + auto descriptors = + std::make_tuple(std::forward(input_descriptors)...); + auto adapted = [handler = StoredHandler(std::forward(input_handler)), + descriptors = + std::move(descriptors)](HttpContext& context) mutable { + auto values = std::apply( + [&context](const auto&... descriptor) { + return std::make_tuple(read_route_parameter(descriptor, context)...); + }, + descriptors); + + std::apply( + [&](auto&... value) { + if constexpr (std::is_invocable_v) { + auto response = std::invoke(handler, value..., context); + apply_typed_response(context, std::move(response)); + } else { + static_assert( + std::is_invocable_v, + "typed handler arguments must match the registered route " + "descriptors"); + auto response = std::invoke(handler, value...); + apply_typed_response(context, std::move(response)); + } + }, + values); + }; - template - TypedRouteHandler make_parameterized_typed_handler_with_response( - Handler&& input_handler, Descriptors&&... input_descriptors) - { - static_assert((is_route_parameter_descriptor_v && ...), - "all typed route bindings must be route parameter descriptors"); - static_assert((0U + ... + (is_body_descriptor_v ? 1U : 0U)) <= 1U, - "a typed route can register at most one Body descriptor"); - using StoredHandler = std::decay_t; - static_assert(!std::is_void_v, - "typed handlers must return a body or HttpResult"); - static_assert(!std::is_reference_v, - "typed handlers must return responses by value"); - - std::vector parameters; - (append_parameter_documentation(parameters, input_descriptors), ...); - auto descriptors = std::make_tuple(std::forward(input_descriptors)...); - auto adapted = [handler = StoredHandler(std::forward(input_handler)), - descriptors = std::move(descriptors)](HttpContext& context) mutable - { - auto values = std::apply([&context](const auto&... descriptor) - { - return std::make_tuple(read_route_parameter(descriptor, context)...); - }, descriptors); - - std::apply([&](auto&... value) - { - if constexpr (std::is_invocable_v) - { - auto response = std::invoke(handler, value..., context); - apply_typed_response(context, std::move(response)); - } - else - { - static_assert(std::is_invocable_v, - "typed handler arguments must match the registered route descriptors"); - auto response = std::invoke(handler, value...); - apply_typed_response(context, std::move(response)); - } - }, values); - }; - - std::optional request_schema; - (set_request_schema>(request_schema), ...); - using ResponseBody = typename typed_response_body::type; - std::optional response_schema; - if constexpr (!std::is_void_v) response_schema.emplace(openapi_schema()); - return {std::move(adapted), std::move(request_schema), std::move(response_schema), - std::move(parameters)}; - } + std::optional request_schema; + (set_request_schema>(request_schema), ...); + using ResponseBody = typename typed_response_body::type; + std::optional response_schema; + if constexpr (!std::is_void_v) + response_schema.emplace(openapi_schema()); + return {std::move(adapted), std::move(request_schema), + std::move(response_schema), std::move(parameters)}; +} - template - TypedRouteHandler make_parameterized_typed_handler(Handler&& input_handler, - Descriptors&&... input_descriptors) - { - using Response = typename callable_traits>::return_type; - return make_parameterized_typed_handler_with_response( - std::forward(input_handler), std::forward(input_descriptors)...); - } +template +TypedRouteHandler make_parameterized_typed_handler( + Handler&& input_handler, Descriptors&&... input_descriptors) { + using Response = typename callable_traits>::return_type; + return make_parameterized_typed_handler_with_response( + std::forward(input_handler), + std::forward(input_descriptors)...); +} - template - TypedRouteHandler make_parameterized_member_handler( +template +TypedRouteHandler make_parameterized_member_handler( std::shared_ptr controller, Response (Controller::*method)(Arguments...), - Descriptors&&... descriptors) - { - auto bound = [controller = std::move(controller), method](auto&... value) -> Response - { - return std::invoke(method, *controller, value...); - }; - return make_parameterized_typed_handler_with_response( + Descriptors&&... descriptors) { + auto bound = [controller = std::move(controller), + method](auto&... value) -> Response { + return std::invoke(method, *controller, value...); + }; + return make_parameterized_typed_handler_with_response( std::move(bound), std::forward(descriptors)...); - } +} - template - TypedRouteHandler make_parameterized_member_handler( +template +TypedRouteHandler make_parameterized_member_handler( std::shared_ptr controller, Response (Controller::*method)(Arguments...) const, - Descriptors&&... descriptors) - { - auto bound = [controller = std::move(controller), method](auto&... value) -> Response - { - return std::invoke(method, *controller, value...); - }; - return make_parameterized_typed_handler_with_response( + Descriptors&&... descriptors) { + auto bound = [controller = std::move(controller), + method](auto&... value) -> Response { + return std::invoke(method, *controller, value...); + }; + return make_parameterized_typed_handler_with_response( std::move(bound), std::forward(descriptors)...); +} + +template +TypedRouteHandler make_typed_handler(Handler&& input_handler) { + using StoredHandler = std::decay_t; + using Traits = callable_traits; + static_assert(Traits::arity == 1 || Traits::arity == 2, + "typed handlers must accept (const Request&) or (const " + "Request&, HttpContext&)"); + + using RequestArgument = typename Traits::template argument<0>; + using Request = remove_cvref_t; + using Response = typename Traits::return_type; + + static_assert( + !std::is_same_v, + "legacy HttpContext handlers must use the existing route overload"); + static_assert(!std::is_void_v, + "typed handlers must return a body or HttpResult"); + static_assert(!std::is_reference_v, + "typed handlers must return responses by value"); + + if constexpr (Traits::arity == 2) { + using ContextArgument = typename Traits::template argument<1>; + static_assert( + std::is_same_v, + "the optional second typed-handler argument must be HttpContext&"); } - template - TypedRouteHandler make_typed_handler(Handler&& input_handler) - { - using StoredHandler = std::decay_t; - using Traits = callable_traits; - static_assert(Traits::arity == 1 || Traits::arity == 2, - "typed handlers must accept (const Request&) or (const Request&, HttpContext&)"); - - using RequestArgument = typename Traits::template argument<0>; - using Request = remove_cvref_t; - using Response = typename Traits::return_type; - - static_assert(!std::is_same_v, - "legacy HttpContext handlers must use the existing route overload"); - static_assert(!std::is_void_v, - "typed handlers must return a body or HttpResult"); - static_assert(!std::is_reference_v, - "typed handlers must return responses by value"); - - if constexpr (Traits::arity == 2) - { - using ContextArgument = typename Traits::template argument<1>; - static_assert(std::is_same_v, - "the optional second typed-handler argument must be HttpContext&"); + auto adapted = [handler = StoredHandler(std::forward( + input_handler))](HttpContext& context) mutable { + const auto content_type = + context.get_header(boost::beast::http::field::content_type); + if (!content_type || !is_json_media_type(*content_type)) { + throw TypedRequestValidationError( + "Request body must be valid JSON matching the expected schema"); } - auto adapted = [handler = StoredHandler(std::forward(input_handler))](HttpContext& context) mutable - { - const auto content_type = context.get_header(boost::beast::http::field::content_type); - if (!content_type || !is_json_media_type(*content_type)) - { - throw TypedRequestValidationError( - "Request body must be valid JSON matching the expected schema"); - } - - std::optional json; - std::optional request; - try - { - json.emplace(boost::json::parse(context.body())); - request.emplace(boost::json::value_to(*json)); - } - catch (const std::bad_alloc&) - { - throw; - } - catch (const std::exception&) - { - throw TypedRequestValidationError( + std::optional json; + std::optional request; + try { + json.emplace(boost::json::parse(context.body())); + request.emplace(boost::json::value_to(*json)); + } catch (const std::bad_alloc&) { + throw; + } catch (const std::exception&) { + throw TypedRequestValidationError( "Request body must be valid JSON matching the expected schema"); - } - - if constexpr (Traits::arity == 1) - { - auto response = std::invoke(handler, static_cast(*request)); - apply_typed_response(context, std::move(response)); - } - else - { - auto response = std::invoke(handler, static_cast(*request), context); - apply_typed_response(context, std::move(response)); - } - }; - - using ResponseBody = typename typed_response_body::type; - std::optional response_schema; - if constexpr (!std::is_void_v) response_schema.emplace(openapi_schema()); - return {std::move(adapted), openapi_schema(), std::move(response_schema), {}}; - } + } + + if constexpr (Traits::arity == 1) { + auto response = + std::invoke(handler, static_cast(*request)); + apply_typed_response(context, std::move(response)); + } else { + auto response = + std::invoke(handler, static_cast(*request), context); + apply_typed_response(context, std::move(response)); + } + }; + + using ResponseBody = typename typed_response_body::type; + std::optional response_schema; + if constexpr (!std::is_void_v) + response_schema.emplace(openapi_schema()); + return {std::move(adapted), + openapi_schema(), + std::move(response_schema), + {}}; +} - template - TypedRouteHandler make_typed_member_handler( +template +TypedRouteHandler make_typed_member_handler( std::shared_ptr controller, - Response (Controller::*method)(const Request&)) - { - std::function bound = - [controller = std::move(controller), method](const Request& request) - { + Response (Controller::*method)(const Request&)) { + std::function bound = + [controller = std::move(controller), method](const Request& request) { return std::invoke(method, *controller, request); }; - return make_typed_handler(std::move(bound)); - } + return make_typed_handler(std::move(bound)); +} - template - TypedRouteHandler make_typed_member_handler( +template +TypedRouteHandler make_typed_member_handler( std::shared_ptr controller, - Response (Controller::*method)(const Request&) const) - { - std::function bound = - [controller = std::move(controller), method](const Request& request) - { + Response (Controller::*method)(const Request&) const) { + std::function bound = + [controller = std::move(controller), method](const Request& request) { return std::invoke(method, *controller, request); }; - return make_typed_handler(std::move(bound)); - } + return make_typed_handler(std::move(bound)); +} - template - TypedRouteHandler make_typed_member_handler( +template +TypedRouteHandler make_typed_member_handler( std::shared_ptr controller, - Response (Controller::*method)(const Request&, HttpContext&)) - { - std::function bound = - [controller = std::move(controller), method](const Request& request, HttpContext& context) - { + Response (Controller::*method)(const Request&, HttpContext&)) { + std::function bound = + [controller = std::move(controller), method](const Request& request, + HttpContext& context) { return std::invoke(method, *controller, request, context); }; - return make_typed_handler(std::move(bound)); - } + return make_typed_handler(std::move(bound)); +} - template - TypedRouteHandler make_typed_member_handler( +template +TypedRouteHandler make_typed_member_handler( std::shared_ptr controller, - Response (Controller::*method)(const Request&, HttpContext&) const) - { - std::function bound = - [controller = std::move(controller), method](const Request& request, HttpContext& context) - { + Response (Controller::*method)(const Request&, HttpContext&) const) { + std::function bound = + [controller = std::move(controller), method](const Request& request, + HttpContext& context) { return std::invoke(method, *controller, request, context); }; - return make_typed_handler(std::move(bound)); - } + return make_typed_handler(std::move(bound)); } +} // namespace khttpd::framework::detail -#endif // KHTTPD_FRAMEWORK_ROUTER_TYPED_ROUTE_HPP_ +#endif // KHTTPD_FRAMEWORK_ROUTER_TYPED_ROUTE_HPP_ diff --git a/framework/router/websocket_router.cpp b/framework/router/websocket_router.cpp index d2b93f7..346696b 100644 --- a/framework/router/websocket_router.cpp +++ b/framework/router/websocket_router.cpp @@ -1,99 +1,124 @@ #include "websocket_router.hpp" +#include + #include #include -#include + #include "websocket/websocket_session.hpp" -namespace khttpd::framework -{ - bool websocket::send_message(const std::string& id, const std::string& msg, const bool is_text) - { - return WebsocketSession::send_message(id, msg, is_text); - } +namespace khttpd::framework { +bool websocket::send_message(const std::string& id, const std::string& msg, + const bool is_text) { + return WebsocketSession::send_message(id, msg, is_text); +} - size_t websocket::send_message(const std::vector& ids, const std::string& msg, const bool is_text) - { - return WebsocketSession::send_message(ids, msg, is_text); - } +size_t websocket::send_message(const std::vector& ids, + const std::string& msg, const bool is_text) { + return WebsocketSession::send_message(ids, msg, is_text); +} - WebsocketRouter::WebsocketRouter() = default; +WebsocketRouter::WebsocketRouter() = default; - void WebsocketRouter::set_base_path(std::string path) - { - if (!path.empty() && path.front() != '/') throw std::invalid_argument("router base path must start with '/'"); - while (path.size() > 1 && path.back() == '/') path.pop_back(); - base_path_ = path == "/" ? std::string{} : std::move(path); - } +void WebsocketRouter::set_base_path(std::string path) { + if (!path.empty() && path.front() != '/') + throw std::invalid_argument("router base path must start with '/'"); + while (path.size() > 1 && path.back() == '/') path.pop_back(); + base_path_ = path == "/" ? std::string{} : std::move(path); +} - void WebsocketRouter::add_handler(const std::string& path, WebsocketOpenHandler on_open, - WebsocketMessageHandler on_message, WebsocketCloseHandler on_close, - WebsocketErrorHandler on_error) - { - const auto registered_path = base_path_.empty() ? path : base_path_ + path; - WebsocketRouteEntry entry{std::move(on_open), std::move(on_message), std::move(on_close), std::move(on_error)}; - std::unique_lock lock(handlers_mutex_); - for (auto& route : handlers_) - if (route.original_path == registered_path) { route.handlers = std::move(entry); return; } - auto [regex, params, literal_count, dynamic_count] = parse_path_pattern(registered_path); - handlers_.push_back({registered_path, std::move(regex), std::move(params), literal_count, dynamic_count, std::move(entry)}); - std::sort(handlers_.begin(), handlers_.end(), WebsocketRoute::compare_specificity); - spdlog::debug("Registered WebSocket handlers for path: {}", registered_path); - } +void WebsocketRouter::add_handler(const std::string& path, + WebsocketOpenHandler on_open, + WebsocketMessageHandler on_message, + WebsocketCloseHandler on_close, + WebsocketErrorHandler on_error) { + const auto registered_path = base_path_.empty() ? path : base_path_ + path; + WebsocketRouteEntry entry{std::move(on_open), std::move(on_message), + std::move(on_close), std::move(on_error)}; + std::unique_lock lock(handlers_mutex_); + for (auto& route : handlers_) + if (route.original_path == registered_path) { + route.handlers = std::move(entry); + return; + } + auto [regex, params, literal_count, dynamic_count] = + parse_path_pattern(registered_path); + handlers_.push_back({registered_path, std::move(regex), std::move(params), + literal_count, dynamic_count, std::move(entry)}); + std::sort(handlers_.begin(), handlers_.end(), + WebsocketRoute::compare_specificity); + spdlog::debug("Registered WebSocket handlers for path: {}", registered_path); +} - void WebsocketRouter::dispatch_open(const std::string& path, WebsocketContext& ctx) - { dispatch(path, ctx, [&](const WebsocketRouteEntry& h) { if (h.on_open) h.on_open(ctx); }); } - void WebsocketRouter::dispatch_message(const std::string& path, WebsocketContext& ctx) - { dispatch(path, ctx, [&](const WebsocketRouteEntry& h) { if (h.on_message) h.on_message(ctx); }); } - void WebsocketRouter::dispatch_close(const std::string& path, WebsocketContext& ctx) - { dispatch(path, ctx, [&](const WebsocketRouteEntry& h) { if (h.on_close) h.on_close(ctx); }); } - void WebsocketRouter::dispatch_error(const std::string& path, WebsocketContext& ctx) - { dispatch(path, ctx, [&](const WebsocketRouteEntry& h) { if (h.on_error) h.on_error(ctx); }); } +void WebsocketRouter::dispatch_open(const std::string& path, + WebsocketContext& ctx) { + dispatch(path, ctx, [&](const WebsocketRouteEntry& h) { + if (h.on_open) h.on_open(ctx); + }); +} +void WebsocketRouter::dispatch_message(const std::string& path, + WebsocketContext& ctx) { + dispatch(path, ctx, [&](const WebsocketRouteEntry& h) { + if (h.on_message) h.on_message(ctx); + }); +} +void WebsocketRouter::dispatch_close(const std::string& path, + WebsocketContext& ctx) { + dispatch(path, ctx, [&](const WebsocketRouteEntry& h) { + if (h.on_close) h.on_close(ctx); + }); +} +void WebsocketRouter::dispatch_error(const std::string& path, + WebsocketContext& ctx) { + dispatch(path, ctx, [&](const WebsocketRouteEntry& h) { + if (h.on_error) h.on_error(ctx); + }); +} - std::tuple, int, int> - WebsocketRouter::parse_path_pattern(const std::string& pattern) - { - std::string expression = "^"; - std::vector names; - std::regex parameter(":([a-zA-Z_][a-zA-Z0-9_]*)"); - std::regex escaped(R"([\\\.\+\*\?\|\(\)\[\]\{\}\^\$])"); - std::sregex_iterator end, it(pattern.begin(), pattern.end(), parameter); - const int total = std::distance(it, end); - auto cursor = pattern.begin(); - int literals = 0, dynamics = 0; - for (int index = 0; it != end; ++it, ++index) - { - const std::string literal(cursor, it->prefix().second); - literals += static_cast(std::count(literal.begin(), literal.end(), '/')); - expression += std::regex_replace(literal, escaped, "\\$&"); - names.push_back((*it)[1]); ++dynamics; - expression += index + 1 == total ? "(.*)" : "([^/]+)"; - cursor = it->suffix().first; - } - const std::string tail(cursor, pattern.end()); - literals += static_cast(std::count(tail.begin(), tail.end(), '/')); - expression += std::regex_replace(tail, escaped, "\\$&"); - return {std::regex(expression + "$"), names, literals, dynamics}; +std::tuple, int, int> +WebsocketRouter::parse_path_pattern(const std::string& pattern) { + std::string expression = "^"; + std::vector names; + std::regex parameter(":([a-zA-Z_][a-zA-Z0-9_]*)"); + std::regex escaped(R"([\\\.\+\*\?\|\(\)\[\]\{\}\^\$])"); + std::sregex_iterator end, it(pattern.begin(), pattern.end(), parameter); + const int total = std::distance(it, end); + auto cursor = pattern.begin(); + int literals = 0, dynamics = 0; + for (int index = 0; it != end; ++it, ++index) { + const std::string literal(cursor, it->prefix().second); + literals += + static_cast(std::count(literal.begin(), literal.end(), '/')); + expression += std::regex_replace(literal, escaped, "\\$&"); + names.push_back((*it)[1]); + ++dynamics; + expression += index + 1 == total ? "(.*)" : "([^/]+)"; + cursor = it->suffix().first; } + const std::string tail(cursor, pattern.end()); + literals += static_cast(std::count(tail.begin(), tail.end(), '/')); + expression += std::regex_replace(tail, escaped, "\\$&"); + return {std::regex(expression + "$"), names, literals, dynamics}; +} - void WebsocketRouter::dispatch(const std::string& path, WebsocketContext& ctx, - const std::function& invoke) +void WebsocketRouter::dispatch( + const std::string& path, WebsocketContext& ctx, + const std::function& invoke) { { - { - std::shared_lock lock(handlers_mutex_); - for (const auto& route : handlers_) - { - std::smatch match; - if (!std::regex_match(path, match, route.path_regex)) continue; - std::map params; - for (size_t i = 0; i < route.param_names.size(); ++i) params[route.param_names[i]] = match[i + 1].str(); - ctx.set_path_params(std::move(params)); - const auto handler = route.handlers; - lock.unlock(); - invoke(handler); - return; - } + std::shared_lock lock(handlers_mutex_); + for (const auto& route : handlers_) { + std::smatch match; + if (!std::regex_match(path, match, route.path_regex)) continue; + std::map params; + for (size_t i = 0; i < route.param_names.size(); ++i) + params[route.param_names[i]] = match[i + 1].str(); + ctx.set_path_params(std::move(params)); + const auto handler = route.handlers; + lock.unlock(); + invoke(handler); + return; } - spdlog::warn("No WebSocket handler found for path: {}", path); } + spdlog::warn("No WebSocket handler found for path: {}", path); } +} // namespace khttpd::framework diff --git a/framework/router/websocket_router.hpp b/framework/router/websocket_router.hpp index 38e0672..ba8ae52 100644 --- a/framework/router/websocket_router.hpp +++ b/framework/router/websocket_router.hpp @@ -3,78 +3,76 @@ #define KHTTPD_FRAMEWORK_ROUTER_WEBSOCKET_ROUTER_HPP #include -#include #include #include #include +#include #include #include + #include "context/websocket_context.hpp" -namespace khttpd::framework -{ - namespace websocket - { - bool send_message(const std::string& id, const std::string& msg, bool is_text); - size_t send_message(const std::vector& ids, const std::string& msg, bool is_text); - } +namespace khttpd::framework { +namespace websocket { +bool send_message(const std::string& id, const std::string& msg, bool is_text); +size_t send_message(const std::vector& ids, const std::string& msg, + bool is_text); +} // namespace websocket - using WebsocketOpenHandler = std::function; - using WebsocketMessageHandler = std::function; - using WebsocketCloseHandler = std::function; - using WebsocketErrorHandler = std::function; +using WebsocketOpenHandler = std::function; +using WebsocketMessageHandler = std::function; +using WebsocketCloseHandler = std::function; +using WebsocketErrorHandler = std::function; - // 路由条目,包含所有事件的处理器 - struct WebsocketRouteEntry - { - WebsocketOpenHandler on_open; - WebsocketMessageHandler on_message; - WebsocketCloseHandler on_close; - WebsocketErrorHandler on_error; - }; +// 路由条目,包含所有事件的处理器 +struct WebsocketRouteEntry { + WebsocketOpenHandler on_open; + WebsocketMessageHandler on_message; + WebsocketCloseHandler on_close; + WebsocketErrorHandler on_error; +}; - struct WebsocketRoute - { - std::string original_path; - std::regex path_regex; - std::vector param_names; - int literal_segments_count = 0; - int dynamic_segments_count = 0; - WebsocketRouteEntry handlers; - static bool compare_specificity(const WebsocketRoute& a, const WebsocketRoute& b) - { - return a.literal_segments_count == b.literal_segments_count - ? a.dynamic_segments_count < b.dynamic_segments_count - : a.literal_segments_count > b.literal_segments_count; - } - }; +struct WebsocketRoute { + std::string original_path; + std::regex path_regex; + std::vector param_names; + int literal_segments_count = 0; + int dynamic_segments_count = 0; + WebsocketRouteEntry handlers; + static bool compare_specificity(const WebsocketRoute& a, + const WebsocketRoute& b) { + return a.literal_segments_count == b.literal_segments_count + ? a.dynamic_segments_count < b.dynamic_segments_count + : a.literal_segments_count > b.literal_segments_count; + } +}; - class WebsocketRouter - { - public: - WebsocketRouter(); +class WebsocketRouter { + public: + WebsocketRouter(); - void set_base_path(std::string path); - const std::string& base_path() const { return base_path_; } + void set_base_path(std::string path); + const std::string& base_path() const { return base_path_; } - void add_handler(const std::string& path, - WebsocketOpenHandler on_open = {nullptr}, - WebsocketMessageHandler on_message = {nullptr}, - WebsocketCloseHandler on_close = {nullptr}, - WebsocketErrorHandler on_error = {nullptr}); + void add_handler(const std::string& path, + WebsocketOpenHandler on_open = {nullptr}, + WebsocketMessageHandler on_message = {nullptr}, + WebsocketCloseHandler on_close = {nullptr}, + WebsocketErrorHandler on_error = {nullptr}); - void dispatch_open(const std::string& path, WebsocketContext& ctx); - void dispatch_message(const std::string& path, WebsocketContext& ctx); - void dispatch_close(const std::string& path, WebsocketContext& ctx); - void dispatch_error(const std::string& path, WebsocketContext& ctx); + void dispatch_open(const std::string& path, WebsocketContext& ctx); + void dispatch_message(const std::string& path, WebsocketContext& ctx); + void dispatch_close(const std::string& path, WebsocketContext& ctx); + void dispatch_error(const std::string& path, WebsocketContext& ctx); - private: - static std::tuple, int, int> parse_path_pattern(const std::string& pattern); - void dispatch(const std::string& path, WebsocketContext& ctx, - const std::function& invoke); - std::vector handlers_; - std::string base_path_; - mutable std::shared_mutex handlers_mutex_; - }; -} -#endif // KHTTPD_FRAMEWORK_ROUTER_WEBSOCKET_ROUTER_HPP + private: + static std::tuple, int, int> + parse_path_pattern(const std::string& pattern); + void dispatch(const std::string& path, WebsocketContext& ctx, + const std::function& invoke); + std::vector handlers_; + std::string base_path_; + mutable std::shared_mutex handlers_mutex_; +}; +} // namespace khttpd::framework +#endif // KHTTPD_FRAMEWORK_ROUTER_WEBSOCKET_ROUTER_HPP diff --git a/framework/server.cpp b/framework/server.cpp index 1a14616..bd1dcb6 100644 --- a/framework/server.cpp +++ b/framework/server.cpp @@ -1,174 +1,155 @@ // framework/server.cpp #include "server.hpp" -#include "session/http_session.hpp" // 需要HttpSession + #include #include + #include #include #include "io_context_pool.hpp" +#include "session/http_session.hpp" // 需要HttpSession -namespace khttpd::framework -{ - Server::Server(const tcp::endpoint& endpoint, std::string web_root, int num_threads) - : signals_(IoContextPool::instance(num_threads).get_io_context(), SIGINT, SIGTERM), +namespace khttpd::framework { +Server::Server(const tcp::endpoint& endpoint, std::string web_root, + int num_threads) + : signals_(IoContextPool::instance(num_threads).get_io_context(), SIGINT, + SIGTERM), web_root_(std::move(web_root)), - acceptor_(net::make_strand(IoContextPool::instance().get_io_context())) - { - boost::beast::error_code ec; - - acceptor_.open(endpoint.protocol(), ec); - if (ec) - { - spdlog::error("Server open error: {}", ec.message()); - throw std::runtime_error(fmt::format("Failed to open acceptor: {}", ec.message())); - } + acceptor_(net::make_strand(IoContextPool::instance().get_io_context())) { + boost::beast::error_code ec; + + acceptor_.open(endpoint.protocol(), ec); + if (ec) { + spdlog::error("Server open error: {}", ec.message()); + throw std::runtime_error( + fmt::format("Failed to open acceptor: {}", ec.message())); + } - acceptor_.set_option(net::socket_base::reuse_address(true), ec); - if (ec) - { - spdlog::error("Server set_option reuse_address error: {}", ec.message()); - throw std::runtime_error(fmt::format("Failed to set reuse_address: {}", ec.message())); - } + acceptor_.set_option(net::socket_base::reuse_address(true), ec); + if (ec) { + spdlog::error("Server set_option reuse_address error: {}", ec.message()); + throw std::runtime_error( + fmt::format("Failed to set reuse_address: {}", ec.message())); + } - acceptor_.bind(endpoint, ec); - if (ec) - { - spdlog::error("Server bind error: {}", ec.message()); - throw std::runtime_error(fmt::format("Failed to bind acceptor: {}", ec.message())); - } + acceptor_.bind(endpoint, ec); + if (ec) { + spdlog::error("Server bind error: {}", ec.message()); + throw std::runtime_error( + fmt::format("Failed to bind acceptor: {}", ec.message())); + } - acceptor_.listen(net::socket_base::max_listen_connections, ec); - if (ec) - { - spdlog::error("Server listen error: {}", ec.message()); - throw std::runtime_error(fmt::format("Failed to listen: {}", ec.message())); + acceptor_.listen(net::socket_base::max_listen_connections, ec); + if (ec) { + spdlog::error("Server listen error: {}", ec.message()); + throw std::runtime_error(fmt::format("Failed to listen: {}", ec.message())); + } + + if (!web_root_.empty()) { + // Pre-compute canonical web root path once (not per-connection). + boost::system::error_code path_ec; + canonical_web_root_ = boost::filesystem::canonical(web_root_, path_ec); + if (path_ec) { + spdlog::warn("Cannot canonicalize web_root '{}': {}", web_root_, + path_ec.message()); } - if (!web_root_.empty()) - { - // Pre-compute canonical web root path once (not per-connection). - boost::system::error_code path_ec; - canonical_web_root_ = boost::filesystem::canonical(web_root_, path_ec); - if (path_ec) - { - spdlog::warn("Cannot canonicalize web_root '{}': {}", web_root_, path_ec.message()); - } - - if (!boost::filesystem::exists(web_root_, ec)) - { - spdlog::warn("Web root directory '{}' does not exist. Static file serving may fail. Error: {}", - web_root_, ec.message()); - } - else if (!boost::filesystem::is_directory(web_root_, ec)) - { - spdlog::warn("Web root path '{}' is not a directory. Static file serving may fail. Error: {}", - web_root_, ec.message()); - } + if (!boost::filesystem::exists(web_root_, ec)) { + spdlog::warn( + "Web root directory '{}' does not exist. Static file serving may " + "fail. Error: {}", + web_root_, ec.message()); + } else if (!boost::filesystem::is_directory(web_root_, ec)) { + spdlog::warn( + "Web root path '{}' is not a directory. Static file serving may " + "fail. Error: {}", + web_root_, ec.message()); } } +} - HttpRouter& Server::get_http_router() - { - return http_router_; - } +HttpRouter& Server::get_http_router() { return http_router_; } - const HttpRouter& Server::get_http_router() const - { - return http_router_; - } +const HttpRouter& Server::get_http_router() const { return http_router_; } - void Server::add_interceptor(std::shared_ptr interceptor) - { - http_router_.add_interceptor(interceptor); - } - - WebsocketRouter& Server::get_websocket_router() - { - return websocket_router_; - } +void Server::add_interceptor(std::shared_ptr interceptor) { + http_router_.add_interceptor(interceptor); +} - const WebsocketRouter& Server::get_websocket_router() const - { - return websocket_router_; - } +WebsocketRouter& Server::get_websocket_router() { return websocket_router_; } - tcp::endpoint Server::local_endpoint() const - { - return acceptor_.local_endpoint(); - } +const WebsocketRouter& Server::get_websocket_router() const { + return websocket_router_; +} - void Server::set_max_buffered_request_body_size(std::uint64_t bytes) - { - max_buffered_request_body_size_.store(bytes, std::memory_order_relaxed); - } +tcp::endpoint Server::local_endpoint() const { + return acceptor_.local_endpoint(); +} - std::uint64_t Server::get_max_buffered_request_body_size() const - { - return max_buffered_request_body_size_.load(std::memory_order_relaxed); - } +void Server::set_max_buffered_request_body_size(std::uint64_t bytes) { + max_buffered_request_body_size_.store(bytes, std::memory_order_relaxed); +} - void Server::run() - { - spdlog::info("Server listening on {}:{}", acceptor_.local_endpoint().address().to_string(), - acceptor_.local_endpoint().port()); +std::uint64_t Server::get_max_buffered_request_body_size() const { + return max_buffered_request_body_size_.load(std::memory_order_relaxed); +} - signals_.async_wait(beast::bind_front_handler(&Server::handle_signal, shared_from_this())); +void Server::run() { + spdlog::info("Server listening on {}:{}", + acceptor_.local_endpoint().address().to_string(), + acceptor_.local_endpoint().port()); - do_accept(); + signals_.async_wait( + beast::bind_front_handler(&Server::handle_signal, shared_from_this())); - IoContextPool::instance().get_io_context().run(); + do_accept(); - spdlog::info("Server workers stopped."); - } + IoContextPool::instance().get_io_context().run(); - void Server::stop() - { - boost::beast::error_code ec; - acceptor_.close(ec); - if (ec) - { - spdlog::error("Server acceptor close error: {}", ec.message()); - } + spdlog::info("Server workers stopped."); +} - IoContextPool::instance().stop(); - spdlog::info("Server stopped."); +void Server::stop() { + boost::beast::error_code ec; + acceptor_.close(ec); + if (ec) { + spdlog::error("Server acceptor close error: {}", ec.message()); } - void Server::do_accept() - { - acceptor_.async_accept( + IoContextPool::instance().stop(); + spdlog::info("Server stopped."); +} + +void Server::do_accept() { + acceptor_.async_accept( net::make_strand(IoContextPool::instance().get_io_context()), beast::bind_front_handler(&Server::on_accept, shared_from_this())); - } - - void Server::on_accept(boost::beast::error_code ec, tcp::socket socket) - { - if (ec) - { - if (ec != boost::system::errc::operation_canceled) - { - spdlog::error("Server on_accept error: {}", ec.message()); - } - } - else - { - std::make_shared(std::move(socket), http_router_, websocket_router_, web_root_, - canonical_web_root_, get_max_buffered_request_body_size())->run(); - } +} - if (acceptor_.is_open()) - { - do_accept(); +void Server::on_accept(boost::beast::error_code ec, tcp::socket socket) { + if (ec) { + if (ec != boost::system::errc::operation_canceled) { + spdlog::error("Server on_accept error: {}", ec.message()); } + } else { + std::make_shared( + std::move(socket), http_router_, websocket_router_, web_root_, + canonical_web_root_, get_max_buffered_request_body_size()) + ->run(); } - void Server::handle_signal(const boost::beast::error_code& error, int signal_number) - { - if (!error) - { - spdlog::info("Received signal {}, shutting down gracefully...", signal_number); - stop(); - } + if (acceptor_.is_open()) { + do_accept(); + } +} + +void Server::handle_signal(const boost::beast::error_code& error, + int signal_number) { + if (!error) { + spdlog::info("Received signal {}, shutting down gracefully...", + signal_number); + stop(); } -} // namespace khttpd::framework +} +} // namespace khttpd::framework diff --git a/framework/server.hpp b/framework/server.hpp index 70b3f33..597781f 100644 --- a/framework/server.hpp +++ b/framework/server.hpp @@ -2,63 +2,64 @@ #ifndef KHTTPD_FRAMEWORK_SERVER_HPP #define KHTTPD_FRAMEWORK_SERVER_HPP +#include #include -#include #include +#include #include +#include #include #include -#include -#include #include "router/http_router.hpp" #include "router/websocket_router.hpp" -namespace khttpd::framework -{ - namespace net = boost::asio; - using tcp = boost::asio::ip::tcp; +namespace khttpd::framework { +namespace net = boost::asio; +using tcp = boost::asio::ip::tcp; - class Server : public std::enable_shared_from_this - { - public: - static constexpr std::uint64_t default_max_buffered_request_body_size = 16ULL * 1024 * 1024; +class Server : public std::enable_shared_from_this { + public: + static constexpr std::uint64_t default_max_buffered_request_body_size = + 16ULL * 1024 * 1024; - Server(const tcp::endpoint& endpoint, std::string web_root, int num_threads = 1); + Server(const tcp::endpoint& endpoint, std::string web_root, + int num_threads = 1); - HttpRouter& get_http_router(); - const HttpRouter& get_http_router() const; + HttpRouter& get_http_router(); + const HttpRouter& get_http_router() const; - void add_interceptor(std::shared_ptr interceptor); + void add_interceptor(std::shared_ptr interceptor); - WebsocketRouter& get_websocket_router(); - const WebsocketRouter& get_websocket_router() const; + WebsocketRouter& get_websocket_router(); + const WebsocketRouter& get_websocket_router() const; - tcp::endpoint local_endpoint() const; + tcp::endpoint local_endpoint() const; - // Limit for routes that buffer the complete request body in memory. - // Stream routes are not subject to this limit. - void set_max_buffered_request_body_size(std::uint64_t bytes); - std::uint64_t get_max_buffered_request_body_size() const; + // Limit for routes that buffer the complete request body in memory. + // Stream routes are not subject to this limit. + void set_max_buffered_request_body_size(std::uint64_t bytes); + std::uint64_t get_max_buffered_request_body_size() const; - void run(); + void run(); - void stop(); + void stop(); - private: - net::signal_set signals_; - const std::string web_root_; - boost::filesystem::path canonical_web_root_; + private: + net::signal_set signals_; + const std::string web_root_; + boost::filesystem::path canonical_web_root_; - tcp::acceptor acceptor_; + tcp::acceptor acceptor_; - HttpRouter http_router_; - WebsocketRouter websocket_router_; - std::atomic max_buffered_request_body_size_{default_max_buffered_request_body_size}; + HttpRouter http_router_; + WebsocketRouter websocket_router_; + std::atomic max_buffered_request_body_size_{ + default_max_buffered_request_body_size}; - void do_accept(); - void on_accept(boost::beast::error_code ec, tcp::socket socket); - void handle_signal(const boost::beast::error_code& error, int signal_number); - }; -} + void do_accept(); + void on_accept(boost::beast::error_code ec, tcp::socket socket); + void handle_signal(const boost::beast::error_code& error, int signal_number); +}; +} // namespace khttpd::framework #endif diff --git a/framework/session/http_session.cpp b/framework/session/http_session.cpp index 848c850..711d398 100644 --- a/framework/session/http_session.cpp +++ b/framework/session/http_session.cpp @@ -1,19 +1,19 @@ #include "http_session.hpp" -#include "context/http_context.hpp" -#include #include #include + +#include +#include #include #include #include -#include +#include "context/http_context.hpp" using namespace khttpd::framework; -struct HttpSession::ChunkWriteState -{ +struct HttpSession::ChunkWriteState { std::queue queue; std::mutex mutex; bool writing = false; @@ -23,75 +23,94 @@ struct HttpSession::ChunkWriteState net::executor_work_guard guard; explicit ChunkWriteState(beast::tcp_stream::executor_type executor) - : guard(executor) - { - } + : guard(executor) {} }; -class HttpSession::RequestStreamImpl final : public HttpRequestStream -{ +class HttpSession::RequestStreamImpl final : public HttpRequestStream { std::shared_ptr session_; -public: - explicit RequestStreamImpl(const std::shared_ptr& session) : session_(session) {} - void async_read_some(net::mutable_buffer target, ReadCallback callback) override - { - if (session_) session_->async_read_stream_body(target, std::move(callback)); - else callback(net::error::operation_aborted, 0, true); + + public: + explicit RequestStreamImpl(const std::shared_ptr& session) + : session_(session) {} + void async_read_some(net::mutable_buffer target, + ReadCallback callback) override { + if (session_) + session_->async_read_stream_body(target, std::move(callback)); + else + callback(net::error::operation_aborted, 0, true); } - void cancel() override - { + void cancel() override { if (session_) session_->cancel_session(); } - void cancel_read() override { if (session_) session_->cancel_stream_body(); } + void cancel_read() override { + if (session_) session_->cancel_stream_body(); + } }; -class HttpSession::ResponseStreamImpl final : public HttpResponseStream -{ +class HttpSession::ResponseStreamImpl final : public HttpResponseStream { std::shared_ptr session_; -public: - explicit ResponseStreamImpl(const std::shared_ptr& session) : session_(session) {} - void async_start(ResponseHead head, Callback cb) override - { if (session_) session_->start_stream_response(std::move(head), std::move(cb)); else cb(net::error::operation_aborted); } - void async_write_some(net::const_buffer b, Callback cb) override - { if (session_) session_->write_stream_response(b, std::move(cb)); else cb(net::error::operation_aborted); } - void async_finish(Callback cb) override - { if (session_) session_->finish_stream_response(std::move(cb)); else cb(net::error::operation_aborted); } - void async_wait_disconnect(Callback cb) override - { if (session_) session_->wait_stream_disconnect(std::move(cb)); } - void cancel_disconnect_wait() override - { if (session_) session_->cancel_stream_disconnect_wait(); } - void cancel_request_body() override { if (session_) session_->cancel_stream_body(); } - void cancel() override { if (session_) session_->cancel_session(); } + + public: + explicit ResponseStreamImpl(const std::shared_ptr& session) + : session_(session) {} + void async_start(ResponseHead head, Callback cb) override { + if (session_) + session_->start_stream_response(std::move(head), std::move(cb)); + else + cb(net::error::operation_aborted); + } + void async_write_some(net::const_buffer b, Callback cb) override { + if (session_) + session_->write_stream_response(b, std::move(cb)); + else + cb(net::error::operation_aborted); + } + void async_finish(Callback cb) override { + if (session_) + session_->finish_stream_response(std::move(cb)); + else + cb(net::error::operation_aborted); + } + void async_wait_disconnect(Callback cb) override { + if (session_) session_->wait_stream_disconnect(std::move(cb)); + } + void cancel_disconnect_wait() override { + if (session_) session_->cancel_stream_disconnect_wait(); + } + void cancel_request_body() override { + if (session_) session_->cancel_stream_body(); + } + void cancel() override { + if (session_) session_->cancel_session(); + } }; -HttpSession::HttpSession(tcp::socket&& socket, HttpRouter& router, WebsocketRouter& ws_router, +HttpSession::HttpSession(tcp::socket&& socket, HttpRouter& router, + WebsocketRouter& ws_router, const std::string& web_root, const boost::filesystem::path& canonical_web_root, std::uint64_t max_buffered_request_body_size) - : stream_(std::move(socket)), - router_(router), - websocket_router_(ws_router), - web_root_path_(web_root), - canonical_web_root_path_(canonical_web_root), - max_buffered_request_body_size_(max_buffered_request_body_size) -{ + : stream_(std::move(socket)), + router_(router), + websocket_router_(ws_router), + web_root_path_(web_root), + canonical_web_root_path_(canonical_web_root), + max_buffered_request_body_size_(max_buffered_request_body_size) { beast::error_code peer_ec; peer_endpoint_ = stream_.socket().remote_endpoint(peer_ec); if (peer_ec) peer_endpoint_.reset(); - if (canonical_web_root_path_.empty()) - { + if (canonical_web_root_path_.empty()) { disable_web_root_ = true; } } -void HttpSession::run() -{ - net::dispatch(stream_.get_executor(), - beast::bind_front_handler(&HttpSession::do_read, shared_from_this())); +void HttpSession::run() { + net::dispatch( + stream_.get_executor(), + beast::bind_front_handler(&HttpSession::do_read, shared_from_this())); } -void HttpSession::do_read() -{ +void HttpSession::do_read() { req_ = {}; res_ = {}; ctx.reset(); @@ -101,88 +120,98 @@ void HttpSession::do_read() request_parser_.emplace(); request_parser_->header_limit(64 * 1024); request_parser_->body_limit((std::numeric_limits::max)()); - http::async_read_header(stream_, buffer_, *request_parser_, - beast::bind_front_handler(&HttpSession::on_read_header, shared_from_this())); + http::async_read_header( + stream_, buffer_, *request_parser_, + beast::bind_front_handler(&HttpSession::on_read_header, + shared_from_this())); } -void HttpSession::copy_request_head() -{ +void HttpSession::copy_request_head() { const auto& source = request_parser_->get(); req_.method(source.method()); req_.target(source.target()); req_.version(source.version()); req_.keep_alive(source.keep_alive()); - for (const auto& field : source) req_.insert(field.name_string(), field.value()); + for (const auto& field : source) + req_.insert(field.name_string(), field.value()); } -void HttpSession::on_read_header(const beast::error_code& ec, std::size_t bytes_transferred) -{ +void HttpSession::on_read_header(const beast::error_code& ec, + std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); if (ec == http::error::end_of_stream) return do_close(); - if (ec) { spdlog::error("HttpSession header read error: {}", ec.message()); return do_close(); } + if (ec) { + spdlog::error("HttpSession header read error: {}", ec.message()); + return do_close(); + } copy_request_head(); - if (beast::websocket::is_upgrade(req_)) - { + if (beast::websocket::is_upgrade(req_)) { ctx = std::make_shared(req_, res_, peer_endpoint_); - return router_.async_run_pre_interceptors(*ctx, [self = shared_from_this()](InterceptorResult result) - { - net::post(self->stream_.get_executor(), [self, result] - { - if (result == InterceptorResult::Continue) self->handle_websocket_upgrade(); - else - { - self->router_.run_post_interceptors(*self->ctx); - self->send_context_response(); - } - }); - }); + return router_.async_run_pre_interceptors( + *ctx, [self = shared_from_this()](InterceptorResult result) { + net::post(self->stream_.get_executor(), [self, result] { + if (result == InterceptorResult::Continue) + self->handle_websocket_upgrade(); + else { + self->router_.run_post_interceptors(*self->ctx); + self->send_context_response(); + } + }); + }); } std::string path(req_.target()); - if (const auto query = path.find('?'); query != std::string::npos) path.resize(query); + if (const auto query = path.find('?'); query != std::string::npos) + path.resize(query); const bool stream_route = router_.is_stream_route(path, req_.method()); - if (!stream_route) - { + if (!stream_route) { const auto content_length = request_parser_->content_length(); if (content_length && *content_length > max_buffered_request_body_size_) return send_payload_too_large(); } const auto expect = req_[http::field::expect]; - if (boost::beast::iequals(expect, "100-continue")) - { - auto response = std::make_shared>(http::status::continue_, req_.version()); - http::async_write(stream_, *response, [self = shared_from_this(), response, stream_route] - (beast::error_code write_ec, std::size_t) - { - if (write_ec) return self->do_close(); - if (stream_route) self->handle_stream_request(); else self->read_buffered_body(); - }); + if (boost::beast::iequals(expect, "100-continue")) { + auto response = std::make_shared>( + http::status::continue_, req_.version()); + http::async_write(stream_, *response, + [self = shared_from_this(), response, stream_route]( + beast::error_code write_ec, std::size_t) { + if (write_ec) return self->do_close(); + if (stream_route) + self->handle_stream_request(); + else + self->read_buffered_body(); + }); return; } if (stream_route) return handle_stream_request(); read_buffered_body(); } -void HttpSession::read_buffered_body() -{ - if (request_parser_->is_done()) - { +void HttpSession::read_buffered_body() { + if (request_parser_->is_done()) { req_.body() = std::move(buffered_body_); return handle_request(); } auto& body = request_parser_->get().body(); body.data = buffered_body_chunk_.data(); body.size = buffered_body_chunk_.size(); - http::async_read_some(stream_, buffer_, *request_parser_, - beast::bind_front_handler(&HttpSession::on_read_buffered_body, shared_from_this())); + http::async_read_some( + stream_, buffer_, *request_parser_, + beast::bind_front_handler(&HttpSession::on_read_buffered_body, + shared_from_this())); } -void HttpSession::on_read_buffered_body(beast::error_code ec, std::size_t bytes_transferred) -{ +void HttpSession::on_read_buffered_body(beast::error_code ec, + std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); if (ec == http::error::need_buffer) ec = {}; - if (ec) { spdlog::error("HttpSession body read error: {}", ec.message()); return do_close(); } - const auto produced = buffered_body_chunk_.size() - request_parser_->get().body().size; + if (ec) { + spdlog::error("HttpSession body read error: {}", ec.message()); + return do_close(); + } + const auto produced = + buffered_body_chunk_.size() - request_parser_->get().body().size; if (buffered_body_.size() > max_buffered_request_body_size_ || produced > max_buffered_request_body_size_ - buffered_body_.size()) return send_payload_too_large(); @@ -190,82 +219,88 @@ void HttpSession::on_read_buffered_body(beast::error_code ec, std::size_t bytes_ read_buffered_body(); } -void HttpSession::send_payload_too_large() -{ - http::response response{http::status::payload_too_large, req_.version()}; +void HttpSession::send_payload_too_large() { + http::response response{http::status::payload_too_large, + req_.version()}; response.keep_alive(false); response.body() = "request body exceeds buffered route limit"; response.prepare_payload(); send_response(std::move(response)); } -void HttpSession::handle_stream_request() -{ +void HttpSession::handle_stream_request() { ctx = std::make_shared(req_, res_, peer_endpoint_); auto stream = std::make_shared(shared_from_this()); - auto response_stream = std::make_shared(shared_from_this()); + auto response_stream = + std::make_shared(shared_from_this()); std::weak_ptr weak = shared_from_this(); - const auto complete = [weak] - { - if (auto self = weak.lock()) net::post(self->stream_.get_executor(), [self] - { - if (self->stream_completed_) return; - self->stream_completed_ = true; - self->router_.run_post_interceptors(*self->ctx); - self->send_context_response(); - }); - }; - try - { - router_.async_run_pre_interceptors(*ctx, - [self = shared_from_this(), stream = std::move(stream), response_stream = std::move(response_stream), complete] - (InterceptorResult result) mutable - { - net::post(self->stream_.get_executor(), - [self, result, stream = std::move(stream), response_stream = std::move(response_stream), complete]() mutable - { - if (result == InterceptorResult::Stop) return complete(); - try - { - if (!self->router_.dispatch_stream(*self->ctx, std::move(stream), std::move(response_stream), complete)) - { - self->ctx->set_status(http::status::not_found); - self->ctx->set_body("stream route not found"); - complete(); - } - } - catch (...) { self->router_.handle_exception(std::current_exception(), *self->ctx); complete(); } - }); + const auto complete = [weak] { + if (auto self = weak.lock()) + net::post(self->stream_.get_executor(), [self] { + if (self->stream_completed_) return; + self->stream_completed_ = true; + self->router_.run_post_interceptors(*self->ctx); + self->send_context_response(); }); + }; + try { + router_.async_run_pre_interceptors( + *ctx, [self = shared_from_this(), stream = std::move(stream), + response_stream = std::move(response_stream), + complete](InterceptorResult result) mutable { + net::post(self->stream_.get_executor(), + [self, result, stream = std::move(stream), + response_stream = std::move(response_stream), + complete]() mutable { + if (result == InterceptorResult::Stop) return complete(); + try { + if (!self->router_.dispatch_stream( + *self->ctx, std::move(stream), + std::move(response_stream), complete)) { + self->ctx->set_status(http::status::not_found); + self->ctx->set_body("stream route not found"); + complete(); + } + } catch (...) { + self->router_.handle_exception(std::current_exception(), + *self->ctx); + complete(); + } + }); + }); + } catch (...) { + router_.handle_exception(std::current_exception(), *ctx); + complete(); } - catch (...) { router_.handle_exception(std::current_exception(), *ctx); complete(); } } -void HttpSession::async_read_stream_body(net::mutable_buffer target, HttpRequestStream::ReadCallback callback) -{ +void HttpSession::async_read_stream_body( + net::mutable_buffer target, HttpRequestStream::ReadCallback callback) { auto self = shared_from_this(); - net::post(stream_.get_executor(), [self, target, callback = std::move(callback)]() mutable - { - if (self->request_body_cancelled_) return callback(net::error::operation_aborted, 0, true); - if (!self->request_parser_ || self->request_parser_->is_done()) return callback({}, 0, true); + net::post(stream_.get_executor(), [self, target, + callback = std::move(callback)]() mutable { + if (self->request_body_cancelled_) + return callback(net::error::operation_aborted, 0, true); + if (!self->request_parser_ || self->request_parser_->is_done()) + return callback({}, 0, true); auto& body = self->request_parser_->get().body(); body.data = target.data(); body.size = target.size(); - http::async_read_some(self->stream_, self->buffer_, *self->request_parser_, - [self, capacity = target.size(), callback = std::move(callback)](beast::error_code ec, std::size_t) mutable - { - if (ec == http::error::need_buffer) ec = {}; - const auto produced = capacity - self->request_parser_->get().body().size; - callback(ec, produced, self->request_parser_->is_done()); - }); + http::async_read_some( + self->stream_, self->buffer_, *self->request_parser_, + [self, capacity = target.size(), callback = std::move(callback)]( + beast::error_code ec, std::size_t) mutable { + if (ec == http::error::need_buffer) ec = {}; + const auto produced = + capacity - self->request_parser_->get().body().size; + callback(ec, produced, self->request_parser_->is_done()); + }); }); } -void HttpSession::cancel_stream_body() -{ +void HttpSession::cancel_stream_body() { auto self = shared_from_this(); - net::post(stream_.get_executor(), [self] - { + net::post(stream_.get_executor(), [self] { self->request_body_cancelled_ = true; // Cancel an outstanding body read without closing the socket. Subsequent // response writes remain valid; the connection is made non-persistent @@ -275,128 +310,151 @@ void HttpSession::cancel_stream_body() }); } -void HttpSession::cancel_session() -{ +void HttpSession::cancel_session() { auto self = shared_from_this(); - net::post(stream_.get_executor(), [self] - { + net::post(stream_.get_executor(), [self] { beast::error_code ignored; self->stream_.cancel(); self->stream_.socket().shutdown(tcp::socket::shutdown_both, ignored); }); } -void HttpSession::start_stream_response(HttpResponseStream::ResponseHead head, HttpResponseStream::Callback callback) -{ +void HttpSession::start_stream_response(HttpResponseStream::ResponseHead head, + HttpResponseStream::Callback callback) { auto self = shared_from_this(); - net::post(stream_.get_executor(), [self, head = std::move(head), callback = std::move(callback)]() mutable - { + net::post(stream_.get_executor(), [self, head = std::move(head), + callback = std::move(callback)]() mutable { self->disconnect_wait_cancelled_ = false; self->streaming_response_ = {}; - self->streaming_response_.result(head.result()); self->streaming_response_.version(head.version()); + self->streaming_response_.result(head.result()); + self->streaming_response_.version(head.version()); self->streaming_response_.keep_alive(head.keep_alive()); - if (self->request_body_cancelled_) self->streaming_response_.keep_alive(false); - for (const auto& field : head) self->streaming_response_.insert(field.name_string(), field.value()); - if (!self->streaming_response_.has_content_length() && !self->streaming_response_.chunked()) + if (self->request_body_cancelled_) + self->streaming_response_.keep_alive(false); + for (const auto& field : head) + self->streaming_response_.insert(field.name_string(), field.value()); + if (!self->streaming_response_.has_content_length() && + !self->streaming_response_.chunked()) self->streaming_response_.chunked(true); self->streaming_response_.body().more = true; self->streaming_response_serializer_.emplace(self->streaming_response_); - http::async_write_header(self->stream_, *self->streaming_response_serializer_, - [callback = std::move(callback)](beast::error_code ec, std::size_t) mutable - { callback(ec); }); + http::async_write_header( + self->stream_, *self->streaming_response_serializer_, + [callback = std::move(callback)]( + beast::error_code ec, std::size_t) mutable { callback(ec); }); }); } -void HttpSession::write_stream_response(net::const_buffer source, HttpResponseStream::Callback callback) -{ +void HttpSession::write_stream_response(net::const_buffer source, + HttpResponseStream::Callback callback) { auto self = shared_from_this(); - net::post(stream_.get_executor(), [self, source, callback = std::move(callback)]() mutable - { - if (!self->streaming_response_serializer_) return callback(net::error::operation_aborted); + net::post(stream_.get_executor(), [self, source, + callback = std::move(callback)]() mutable { + if (!self->streaming_response_serializer_) + return callback(net::error::operation_aborted); auto& body = self->streaming_response_.body(); - body.data = const_cast(source.data()); body.size = source.size(); body.more = true; + body.data = const_cast(source.data()); + body.size = source.size(); + body.more = true; http::async_write(self->stream_, *self->streaming_response_serializer_, - [callback = std::move(callback)](beast::error_code ec, std::size_t) mutable - { if (ec == http::error::need_buffer) ec = {}; callback(ec); }); + [callback = std::move(callback)](beast::error_code ec, + std::size_t) mutable { + if (ec == http::error::need_buffer) ec = {}; + callback(ec); + }); }); } -void HttpSession::finish_stream_response(HttpResponseStream::Callback callback) -{ +void HttpSession::finish_stream_response( + HttpResponseStream::Callback callback) { auto self = shared_from_this(); - net::post(stream_.get_executor(), [self, callback = std::move(callback)]() mutable - { - if (!self->streaming_response_serializer_) return callback(net::error::operation_aborted); - if (self->streaming_response_serializer_->is_done()) - { - callback({}); - const bool keep_alive = self->streaming_response_.keep_alive(); - self->streaming_response_serializer_.reset(); - if (keep_alive) self->do_read(); else self->do_close(); - return; - } - auto& body = self->streaming_response_.body(); body.data = nullptr; body.size = 0; body.more = false; - http::async_write(self->stream_, *self->streaming_response_serializer_, - [self, callback = std::move(callback)](beast::error_code ec, std::size_t) mutable - { - callback(ec); - const bool keep_alive = self->streaming_response_.keep_alive(); - self->streaming_response_serializer_.reset(); - if (!ec && keep_alive) self->do_read(); else self->do_close(); + net::post( + stream_.get_executor(), [self, callback = std::move(callback)]() mutable { + if (!self->streaming_response_serializer_) + return callback(net::error::operation_aborted); + if (self->streaming_response_serializer_->is_done()) { + callback({}); + const bool keep_alive = self->streaming_response_.keep_alive(); + self->streaming_response_serializer_.reset(); + if (keep_alive) + self->do_read(); + else + self->do_close(); + return; + } + auto& body = self->streaming_response_.body(); + body.data = nullptr; + body.size = 0; + body.more = false; + http::async_write(self->stream_, *self->streaming_response_serializer_, + [self, callback = std::move(callback)]( + beast::error_code ec, std::size_t) mutable { + callback(ec); + const bool keep_alive = + self->streaming_response_.keep_alive(); + self->streaming_response_serializer_.reset(); + if (!ec && keep_alive) + self->do_read(); + else + self->do_close(); + }); }); - }); } -void HttpSession::wait_stream_disconnect(HttpResponseStream::Callback callback) -{ +void HttpSession::wait_stream_disconnect( + HttpResponseStream::Callback callback) { auto self = shared_from_this(); - net::post(stream_.get_executor(), [self, callback = std::move(callback)]() mutable - { - // A request body is still readable on this connection and must not be mistaken for a peer disconnect. + net::post(stream_.get_executor(), [self, + callback = std::move(callback)]() mutable { + // A request body is still readable on this connection and must not be + // mistaken for a peer disconnect. if (self->disconnect_wait_cancelled_) return callback(net::error::operation_aborted); - if ((self->request_parser_ && !self->request_parser_->is_done()) || self->disconnect_wait_active_) return; + if ((self->request_parser_ && !self->request_parser_->is_done()) || + self->disconnect_wait_active_) + return; self->disconnect_wait_active_ = true; - self->stream_.socket().async_receive(net::buffer(self->disconnect_probe_), - net::bind_cancellation_slot(self->disconnect_wait_cancellation_.slot(), - [self, callback = std::move(callback)](beast::error_code ec, const std::size_t bytes) mutable - { - self->disconnect_wait_active_ = false; - if (!ec) - ec = bytes == 0 ? net::error::eof : make_error_code(boost::system::errc::protocol_error); - callback(ec); - })); + self->stream_.socket().async_receive( + net::buffer(self->disconnect_probe_), + net::bind_cancellation_slot( + self->disconnect_wait_cancellation_.slot(), + [self, callback = std::move(callback)]( + beast::error_code ec, const std::size_t bytes) mutable { + self->disconnect_wait_active_ = false; + if (!ec) + ec = bytes == 0 + ? net::error::eof + : make_error_code(boost::system::errc::protocol_error); + callback(ec); + })); }); } -void HttpSession::cancel_stream_disconnect_wait() -{ +void HttpSession::cancel_stream_disconnect_wait() { auto self = shared_from_this(); - net::dispatch(stream_.get_executor(), [self] - { + net::dispatch(stream_.get_executor(), [self] { self->disconnect_wait_cancelled_ = true; if (self->disconnect_wait_active_) - self->disconnect_wait_cancellation_.emit(net::cancellation_type::terminal); + self->disconnect_wait_cancellation_.emit( + net::cancellation_type::terminal); }); } -void HttpSession::on_read(const beast::error_code& ec, std::size_t bytes_transferred) -{ +void HttpSession::on_read(const beast::error_code& ec, + std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); - if (ec == http::error::end_of_stream) - { + if (ec == http::error::end_of_stream) { return do_close(); } - if (ec) - { + if (ec) { spdlog::error("HttpSession on_read error: {}", ec.message()); return; } - if (beast::websocket::is_upgrade(req_)) - { - spdlog::debug("Detected WebSocket upgrade request for target: {}", std::string(req_.target())); + if (beast::websocket::is_upgrade(req_)) { + spdlog::debug("Detected WebSocket upgrade request for target: {}", + std::string(req_.target())); handle_websocket_upgrade(); return; } @@ -404,55 +462,56 @@ void HttpSession::on_read(const beast::error_code& ec, std::size_t bytes_transfe handle_request(); } -void HttpSession::handle_request() -{ +void HttpSession::handle_request() { res_ = {}; ctx = std::make_shared(req_, res_, peer_endpoint_); - try - { - router_.async_run_pre_interceptors(*ctx, [self = shared_from_this()](InterceptorResult result) - { net::post(self->stream_.get_executor(), [self, result] { self->dispatch_request_after_interceptors(result); }); }); - } - catch (...) - { + try { + router_.async_run_pre_interceptors( + *ctx, [self = shared_from_this()](InterceptorResult result) { + net::post(self->stream_.get_executor(), [self, result] { + self->dispatch_request_after_interceptors(result); + }); + }); + } catch (...) { router_.handle_exception(std::current_exception(), *ctx); - try - { + try { router_.run_post_interceptors(*ctx); - } - catch (...) - { + } catch (...) { router_.handle_exception(std::current_exception(), *ctx); } send_context_response(); } } -void HttpSession::dispatch_request_after_interceptors(InterceptorResult result) -{ +void HttpSession::dispatch_request_after_interceptors( + InterceptorResult result) { bool post_interceptors_started = false; - try - { - if (result == InterceptorResult::Stop) - { + try { + if (result == InterceptorResult::Stop) { post_interceptors_started = true; router_.run_post_interceptors(*ctx); return send_context_response(); } // Asynchronous routes take precedence when registered for this method. - if (router_.dispatch_async(*ctx, [self = shared_from_this()] - { net::post(self->stream_.get_executor(), [self] - { - try { self->router_.run_post_interceptors(*self->ctx); self->send_context_response(); } - catch (...) { self->router_.handle_exception(std::current_exception(), *self->ctx); self->send_context_response(); } - }); })) return; + if (router_.dispatch_async(*ctx, [self = shared_from_this()] { + net::post(self->stream_.get_executor(), [self] { + try { + self->router_.run_post_interceptors(*self->ctx); + self->send_context_response(); + } catch (...) { + self->router_.handle_exception(std::current_exception(), + *self->ctx); + self->send_context_response(); + } + }); + })) + return; bool static_file_served = false; - router_.dispatch(*ctx, [this, &static_file_served] - { + router_.dispatch(*ctx, [this, &static_file_served] { if (req_.method() == http::verb::get || req_.method() == http::verb::head) static_file_served = do_serve_static_file(); return static_file_served; @@ -461,19 +520,13 @@ void HttpSession::dispatch_request_after_interceptors(InterceptorResult result) post_interceptors_started = true; router_.run_post_interceptors(*ctx); send_context_response(); - } - catch (...) - { + } catch (...) { router_.handle_exception(std::current_exception(), *ctx); - if (!post_interceptors_started) - { - try - { + if (!post_interceptors_started) { + try { post_interceptors_started = true; router_.run_post_interceptors(*ctx); - } - catch (...) - { + } catch (...) { router_.handle_exception(std::current_exception(), *ctx); } } @@ -481,128 +534,120 @@ void HttpSession::dispatch_request_after_interceptors(InterceptorResult result) } } -void HttpSession::send_context_response() -{ +void HttpSession::send_context_response() { if (request_body_cancelled_) res_.keep_alive(false); - if (req_.method() == http::verb::head) - { + if (req_.method() == http::verb::head) { http::response head{res_.result(), res_.version()}; head.keep_alive(res_.keep_alive()); - for (const auto& field : res_) head.insert(field.name_string(), field.value()); + for (const auto& field : res_) + head.insert(field.name_string(), field.value()); head.erase(http::field::transfer_encoding); if (!head.has_content_length()) head.content_length(res_.body().size()); return send_response(std::move(head)); } - if (res_.chunked()) send_chunked_response(); - else - { + if (res_.chunked()) + send_chunked_response(); + else { if (!res_.has_content_length()) res_.prepare_payload(); send_response(std::move(res_)); } } // Extract path from request target (query-stripped) -namespace -{ - std::string extract_path_from_target(std::string_view target) - { - auto qpos = target.find('?'); - if (qpos != std::string_view::npos) - { - return std::string(target.substr(0, qpos)); - } - return std::string(target); +namespace { +std::string extract_path_from_target(std::string_view target) { + auto qpos = target.find('?'); + if (qpos != std::string_view::npos) { + return std::string(target.substr(0, qpos)); } + return std::string(target); +} - bool is_path_within_root(const boost::filesystem::path& candidate, - const boost::filesystem::path& root) - { - auto root_it = root.begin(); - auto candidate_it = candidate.begin(); +bool is_path_within_root(const boost::filesystem::path& candidate, + const boost::filesystem::path& root) { + auto root_it = root.begin(); + auto candidate_it = candidate.begin(); - for (; root_it != root.end(); ++root_it, ++candidate_it) - { - if (candidate_it == candidate.end() || *root_it != *candidate_it) - { - return false; - } + for (; root_it != root.end(); ++root_it, ++candidate_it) { + if (candidate_it == candidate.end() || *root_it != *candidate_it) { + return false; } - - return true; } + + return true; } +} // namespace -bool HttpSession::do_serve_static_file() -{ - if (canonical_web_root_path_.empty()) - { +bool HttpSession::do_serve_static_file() { + if (canonical_web_root_path_.empty()) { return false; } std::string request_path_str = extract_path_from_target(req_.target()); boost::filesystem::path request_path(request_path_str); - if (request_path == "/") - { + if (request_path == "/") { request_path = "/index.html"; } - boost::filesystem::path full_local_path = web_root_path_ / request_path.relative_path(); + boost::filesystem::path full_local_path = + web_root_path_ / request_path.relative_path(); boost::system::error_code ec; // 1. Normalize path to prevent directory traversal full_local_path = boost::filesystem::canonical(full_local_path, ec); - if (ec) - { - if (ec == boost::system::errc::no_such_file_or_directory) - { + if (ec) { + if (ec == boost::system::errc::no_such_file_or_directory) { return false; } - spdlog::error("Error canonicalizing path '{}': {}", full_local_path.string(), ec.message()); - http::response forbidden_res{http::status::forbidden, req_.version()}; + spdlog::error("Error canonicalizing path '{}': {}", + full_local_path.string(), ec.message()); + http::response forbidden_res{http::status::forbidden, + req_.version()}; forbidden_res.keep_alive(req_.keep_alive()); forbidden_res.set(http::field::server, BOOST_BEAST_VERSION_STRING); forbidden_res.set(http::field::content_type, "text/html"); - forbidden_res.body() = fmt::format("

403 Forbidden

Access denied due to invalid path.

"); + forbidden_res.body() = fmt::format( + "

403 Forbidden

Access denied due to invalid path.

"); forbidden_res.prepare_payload(); send_response(std::move(forbidden_res)); return true; } // 2. Security: ensure path is within web root - if (!is_path_within_root(full_local_path, canonical_web_root_path_)) - { - http::response forbidden_res{http::status::forbidden, req_.version()}; + if (!is_path_within_root(full_local_path, canonical_web_root_path_)) { + http::response forbidden_res{http::status::forbidden, + req_.version()}; forbidden_res.keep_alive(req_.keep_alive()); forbidden_res.set(http::field::server, BOOST_BEAST_VERSION_STRING); forbidden_res.set(http::field::content_type, "text/html"); - forbidden_res.body() = fmt::format("

403 Forbidden

Access denied: Path traversal attempt detected.

"); + forbidden_res.body() = fmt::format( + "

403 Forbidden

Access denied: Path traversal attempt " + "detected.

"); forbidden_res.prepare_payload(); send_response(std::move(forbidden_res)); return true; } // 3. Check if directory - if (boost::filesystem::is_directory(full_local_path, ec)) - { - if (ec) - { - spdlog::error("Error checking if path is directory '{}': {}", full_local_path.string(), ec.message()); + if (boost::filesystem::is_directory(full_local_path, ec)) { + if (ec) { + spdlog::error("Error checking if path is directory '{}': {}", + full_local_path.string(), ec.message()); return false; } boost::filesystem::path index_file_path = full_local_path / "index.html"; - if (boost::filesystem::is_regular_file(index_file_path, ec)) - { + if (boost::filesystem::is_regular_file(index_file_path, ec)) { full_local_path = index_file_path; - } - else - { - http::response forbidden_res{http::status::forbidden, req_.version()}; + } else { + http::response forbidden_res{http::status::forbidden, + req_.version()}; forbidden_res.keep_alive(req_.keep_alive()); forbidden_res.set(http::field::server, BOOST_BEAST_VERSION_STRING); forbidden_res.set(http::field::content_type, "text/html"); - forbidden_res.body() = fmt::format("

403 Forbidden

Directory listing not allowed.

"); + forbidden_res.body() = fmt::format( + "

403 Forbidden

Directory listing not allowed.

"); forbidden_res.prepare_payload(); send_response(std::move(forbidden_res)); return true; @@ -610,8 +655,7 @@ bool HttpSession::do_serve_static_file() } // 4. Final check: regular file - if (!boost::filesystem::is_regular_file(full_local_path, ec) || ec) - { + if (!boost::filesystem::is_regular_file(full_local_path, ec) || ec) { return false; } @@ -622,32 +666,37 @@ bool HttpSession::do_serve_static_file() file_res.result(http::status::ok); file_res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - file_res.body().open(full_local_path.string().c_str(), beast::file_mode::scan, ec); - if (ec) - { - spdlog::error("Error opening file {}: {}", full_local_path.string(), ec.message()); - http::response internal_error_res{http::status::internal_server_error, req_.version()}; + file_res.body().open(full_local_path.string().c_str(), beast::file_mode::scan, + ec); + if (ec) { + spdlog::error("Error opening file {}: {}", full_local_path.string(), + ec.message()); + http::response internal_error_res{ + http::status::internal_server_error, req_.version()}; internal_error_res.keep_alive(req_.keep_alive()); internal_error_res.set(http::field::server, BOOST_BEAST_VERSION_STRING); internal_error_res.set(http::field::content_type, "text/html"); - internal_error_res.body() = "

500 Internal Server Error

Could not open the requested file.

"; + internal_error_res.body() = + "

500 Internal Server Error

Could not open the requested " + "file.

"; internal_error_res.prepare_payload(); send_response(std::move(internal_error_res)); return true; } std::string extension = full_local_path.extension().string(); - std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); + std::transform(extension.begin(), extension.end(), extension.begin(), + ::tolower); file_res.set(http::field::content_type, mime_type_from_extension(extension)); file_res.prepare_payload(); - if (req_.method() == http::verb::head) - { + if (req_.method() == http::verb::head) { http::response head_res{http::status::ok, req_.version()}; head_res.keep_alive(req_.keep_alive()); head_res.set(http::field::server, BOOST_BEAST_VERSION_STRING); - head_res.set(http::field::content_type, mime_type_from_extension(extension)); + head_res.set(http::field::content_type, + mime_type_from_extension(extension)); head_res.content_length(file_res.body().size()); send_response(std::move(head_res)); return true; @@ -657,29 +706,27 @@ bool HttpSession::do_serve_static_file() return true; } -void HttpSession::send_chunked_response() -{ +void HttpSession::send_chunked_response() { res_.body() = ""; sr_.emplace(res_); - http::async_write_header(stream_, *sr_, - beast::bind_front_handler( - &HttpSession::on_write_header, - shared_from_this())); + http::async_write_header( + stream_, *sr_, + beast::bind_front_handler(&HttpSession::on_write_header, + shared_from_this())); } -void HttpSession::send_response(http::message_generator msg) -{ +void HttpSession::send_response(http::message_generator msg) { bool keep_alive = msg.keep_alive(); beast::async_write(stream_, std::move(msg), - beast::bind_front_handler(&HttpSession::on_write, shared_from_this(), keep_alive)); + beast::bind_front_handler(&HttpSession::on_write, + shared_from_this(), keep_alive)); } -void HttpSession::on_write_header(beast::error_code ec, std::size_t bytes_transferred) -{ +void HttpSession::on_write_header(beast::error_code ec, + std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); - if (ec) - { + if (ec) { spdlog::error("HttpSession on_write_header error: {}", ec.message()); return; } @@ -689,35 +736,29 @@ void HttpSession::on_write_header(beast::error_code ec, std::size_t bytes_transf auto self = shared_from_this(); auto state = std::make_shared(stream_.get_executor()); - auto schedule_write = [self, state]() - { + auto schedule_write = [self, state]() { bool should_start = false; { std::unique_lock lock{state->mutex}; - if (!state->writing) - { + if (!state->writing) { state->writing = true; should_start = true; } } - if (should_start) - { - net::post(self->stream_.get_executor(), [self, state]() - { - self->do_write_chunk(state); - }); + if (should_start) { + net::post(self->stream_.get_executor(), + [self, state]() { self->do_write_chunk(state); }); } }; - auto write_chunk = [state, schedule_write](const std::string& buffer) -> bool - { + auto write_chunk = [state, + schedule_write](const std::string& buffer) -> bool { std::stringstream ss; ss << std::hex << buffer.length() << "\r\n" << buffer << "\r\n"; { std::unique_lock lock{state->mutex}; - if (state->error) - { + if (state->error) { return false; } state->queue.push(ss.str()); @@ -726,10 +767,8 @@ void HttpSession::on_write_header(beast::error_code ec, std::size_t bytes_transf return true; }; - std::thread([self, state, write_chunk, schedule_write]() - { - if (self->ctx->get_stream_handler()) - { + std::thread([self, state, write_chunk, schedule_write]() { + if (self->ctx->get_stream_handler()) { self->ctx->get_stream_handler()(*self->ctx, write_chunk); } @@ -742,16 +781,13 @@ void HttpSession::on_write_header(beast::error_code ec, std::size_t bytes_transf }).detach(); } -void HttpSession::do_write_chunk(std::shared_ptr state) -{ +void HttpSession::do_write_chunk(std::shared_ptr state) { std::shared_ptr data; { std::unique_lock lock{state->mutex}; - if (state->queue.empty()) - { + if (state->queue.empty()) { state->writing = false; - if (state->final_queued && !state->completed) - { + if (state->final_queued && !state->completed) { state->completed = true; lock.unlock(); state->guard.reset(); @@ -765,10 +801,9 @@ void HttpSession::do_write_chunk(std::shared_ptr state) } net::async_write(stream_, net::buffer(*data), - [self = shared_from_this(), state, data](beast::error_code ec, std::size_t bytes) - { - if (ec) - { + [self = shared_from_this(), state, data]( + beast::error_code ec, std::size_t bytes) { + if (ec) { { std::unique_lock lock{state->mutex}; state->error = ec; @@ -783,60 +818,52 @@ void HttpSession::do_write_chunk(std::shared_ptr state) }); } -void HttpSession::do_write_final_chunk() -{ - net::async_write(stream_, net::buffer("0\r\n\r\n"), - beast::bind_front_handler( - &HttpSession::on_shutdown, - shared_from_this(), res_.keep_alive())); +void HttpSession::do_write_final_chunk() { + net::async_write( + stream_, net::buffer("0\r\n\r\n"), + beast::bind_front_handler(&HttpSession::on_shutdown, shared_from_this(), + res_.keep_alive())); } -void HttpSession::on_shutdown(bool keep_alive, beast::error_code ec, std::size_t bytes_transferred) -{ +void HttpSession::on_shutdown(bool keep_alive, beast::error_code ec, + std::size_t bytes_transferred) { on_write(keep_alive, ec, bytes_transferred); } -void HttpSession::on_write(bool keep_alive, beast::error_code ec, std::size_t bytes_transferred) -{ +void HttpSession::on_write(bool keep_alive, beast::error_code ec, + std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); - if (ec) - { + if (ec) { spdlog::error("HttpSession on_write error: {}", ec.message()); return; } - if (!keep_alive) - { + if (!keep_alive) { return do_close(); } do_read(); } -void HttpSession::do_close() -{ +void HttpSession::do_close() { spdlog::debug("HttpSession closing connection"); beast::error_code ec; stream_.socket().shutdown(tcp::socket::shutdown_send, ec); - if (ec) - { + if (ec) { spdlog::error("HttpSession shutdown error: {}", ec.message()); } } -void HttpSession::handle_websocket_upgrade() -{ - ws_session_ = std::make_shared(stream_.release_socket(), websocket_router_, - std::string(req_.target())); +void HttpSession::handle_websocket_upgrade() { + ws_session_ = std::make_shared( + stream_.release_socket(), websocket_router_, std::string(req_.target())); ws_session_->run_handshake(req_); } - // 辅助函数:根据文件扩展名获取 MIME 类型 -std::string HttpSession::mime_type_from_extension(const std::string& ext) -{ +std::string HttpSession::mime_type_from_extension(const std::string& ext) { if (ext == ".html" || ext == ".htm") return "text/html"; if (ext == ".css") return "text/css"; if (ext == ".js") return "application/javascript"; diff --git a/framework/session/http_session.hpp b/framework/session/http_session.hpp index b8db901..4cc9872 100644 --- a/framework/session/http_session.hpp +++ b/framework/session/http_session.hpp @@ -1,116 +1,123 @@ #ifndef KHTTPD_HTTP_SESSION_HPP #define KHTTPD_HTTP_SESSION_HPP -#include -#include -#include +#include #include +#include +#include +#include +#include #include #include #include -#include -#include + #include "router/http_router.hpp" #include "websocket/websocket_session.hpp" +namespace khttpd::framework { +namespace beast = boost::beast; +namespace http = beast::http; +namespace net = boost::asio; +using tcp = boost::asio::ip::tcp; +class HttpRouter; +class WebsocketRouter; +class WebsocketSession; -namespace khttpd::framework -{ - namespace beast = boost::beast; - namespace http = beast::http; - namespace net = boost::asio; - using tcp = boost::asio::ip::tcp; - class HttpRouter; - class WebsocketRouter; - class WebsocketSession; - - // 处理单个HTTP连接的会话 - class HttpSession : public std::enable_shared_from_this - { - public: - static constexpr std::uint64_t default_max_buffered_request_body_size = 16ULL * 1024 * 1024; +// 处理单个HTTP连接的会话 +class HttpSession : public std::enable_shared_from_this { + public: + static constexpr std::uint64_t default_max_buffered_request_body_size = + 16ULL * 1024 * 1024; - HttpSession(tcp::socket&& socket, HttpRouter& router, WebsocketRouter& ws_router, - const std::string& web_root, - const boost::filesystem::path& canonical_web_root, - std::uint64_t max_buffered_request_body_size = default_max_buffered_request_body_size); + HttpSession(tcp::socket&& socket, HttpRouter& router, + WebsocketRouter& ws_router, const std::string& web_root, + const boost::filesystem::path& canonical_web_root, + std::uint64_t max_buffered_request_body_size = + default_max_buffered_request_body_size); - // 启动会话 - void run(); + // 启动会话 + void run(); - private: - struct ChunkWriteState; - class RequestStreamImpl; - class ResponseStreamImpl; + private: + struct ChunkWriteState; + class RequestStreamImpl; + class ResponseStreamImpl; - bool disable_web_root_ = false; - beast::tcp_stream stream_; - beast::flat_buffer buffer_; - http::request req_; - std::optional> request_parser_; - std::array buffered_body_chunk_{}; - std::string buffered_body_; - http::response res_; - HttpRouter& router_; - WebsocketRouter& websocket_router_; - const boost::filesystem::path web_root_path_; - const boost::filesystem::path canonical_web_root_path_; - const std::uint64_t max_buffered_request_body_size_; - std::shared_ptr ws_session_; - std::optional> sr_; - http::response streaming_response_; - std::optional> streaming_response_serializer_; - std::shared_ptr ctx = nullptr; - bool stream_completed_ = false; - std::optional peer_endpoint_; - bool request_body_cancelled_ = false; - std::array disconnect_probe_{}; - net::cancellation_signal disconnect_wait_cancellation_; - bool disconnect_wait_active_ = false; - bool disconnect_wait_cancelled_ = false; + bool disable_web_root_ = false; + beast::tcp_stream stream_; + beast::flat_buffer buffer_; + http::request req_; + std::optional> request_parser_; + std::array buffered_body_chunk_{}; + std::string buffered_body_; + http::response res_; + HttpRouter& router_; + WebsocketRouter& websocket_router_; + const boost::filesystem::path web_root_path_; + const boost::filesystem::path canonical_web_root_path_; + const std::uint64_t max_buffered_request_body_size_; + std::shared_ptr ws_session_; + std::optional> sr_; + http::response streaming_response_; + std::optional> + streaming_response_serializer_; + std::shared_ptr ctx = nullptr; + bool stream_completed_ = false; + std::optional peer_endpoint_; + bool request_body_cancelled_ = false; + std::array disconnect_probe_{}; + net::cancellation_signal disconnect_wait_cancellation_; + bool disconnect_wait_active_ = false; + bool disconnect_wait_cancelled_ = false; - // Chunked streaming support - std::shared_ptr> chunk_queue_; - std::shared_ptr chunk_mtx_; - std::shared_ptr chunk_writing_; - std::shared_ptr chunk_error_; + // Chunked streaming support + std::shared_ptr> chunk_queue_; + std::shared_ptr chunk_mtx_; + std::shared_ptr chunk_writing_; + std::shared_ptr chunk_error_; - void do_read(); - void on_read_header(const beast::error_code& ec, std::size_t bytes_transferred); - void read_buffered_body(); - void on_read_buffered_body(beast::error_code ec, std::size_t bytes_transferred); - void send_payload_too_large(); - void copy_request_head(); - void handle_stream_request(); - void async_read_stream_body(net::mutable_buffer target, HttpRequestStream::ReadCallback callback); - void cancel_stream_body(); - void cancel_session(); - void start_stream_response(HttpResponseStream::ResponseHead head, HttpResponseStream::Callback callback); - void write_stream_response(net::const_buffer source, HttpResponseStream::Callback callback); - void finish_stream_response(HttpResponseStream::Callback callback); - void wait_stream_disconnect(HttpResponseStream::Callback callback); - void cancel_stream_disconnect_wait(); - void on_read(const beast::error_code& ec, std::size_t bytes_transferred); + void do_read(); + void on_read_header(const beast::error_code& ec, + std::size_t bytes_transferred); + void read_buffered_body(); + void on_read_buffered_body(beast::error_code ec, + std::size_t bytes_transferred); + void send_payload_too_large(); + void copy_request_head(); + void handle_stream_request(); + void async_read_stream_body(net::mutable_buffer target, + HttpRequestStream::ReadCallback callback); + void cancel_stream_body(); + void cancel_session(); + void start_stream_response(HttpResponseStream::ResponseHead head, + HttpResponseStream::Callback callback); + void write_stream_response(net::const_buffer source, + HttpResponseStream::Callback callback); + void finish_stream_response(HttpResponseStream::Callback callback); + void wait_stream_disconnect(HttpResponseStream::Callback callback); + void cancel_stream_disconnect_wait(); + void on_read(const beast::error_code& ec, std::size_t bytes_transferred); - void handle_request(); - void dispatch_request_after_interceptors(InterceptorResult result); - void send_context_response(); - // 新增:尝试处理静态文件请求 - bool do_serve_static_file(); + void handle_request(); + void dispatch_request_after_interceptors(InterceptorResult result); + void send_context_response(); + // 新增:尝试处理静态文件请求 + bool do_serve_static_file(); - void send_chunked_response(); - void send_response(http::message_generator msg); - void on_write_header(beast::error_code ec, std::size_t bytes_transferred); - void do_write_chunk(std::shared_ptr state); - void on_write(bool keep_alive, beast::error_code ec, std::size_t bytes_transferred); + void send_chunked_response(); + void send_response(http::message_generator msg); + void on_write_header(beast::error_code ec, std::size_t bytes_transferred); + void do_write_chunk(std::shared_ptr state); + void on_write(bool keep_alive, beast::error_code ec, + std::size_t bytes_transferred); - void do_write_final_chunk(); - void on_shutdown(bool keep_alive, beast::error_code ec, std::size_t); - void do_close(); + void do_write_final_chunk(); + void on_shutdown(bool keep_alive, beast::error_code ec, std::size_t); + void do_close(); - void handle_websocket_upgrade(); - // 辅助函数:根据文件扩展名获取 MIME 类型 - static std::string mime_type_from_extension(const std::string& ext); - }; -} -#endif // KHTTPD_HTTP_SESSION_HPP + void handle_websocket_upgrade(); + // 辅助函数:根据文件扩展名获取 MIME 类型 + static std::string mime_type_from_extension(const std::string& ext); +}; +} // namespace khttpd::framework +#endif // KHTTPD_HTTP_SESSION_HPP diff --git a/framework/sse/sse_event.hpp b/framework/sse/sse_event.hpp index a871700..c241ba0 100644 --- a/framework/sse/sse_event.hpp +++ b/framework/sse/sse_event.hpp @@ -5,18 +5,16 @@ #include #include -namespace khttpd::framework::sse -{ - struct SseEvent - { - std::string event; - std::string data; - std::string id; - std::optional retry; - }; +namespace khttpd::framework::sse { +struct SseEvent { + std::string event; + std::string data; + std::string id; + std::optional retry; +}; - std::string format_sse_event(const SseEvent& event); - std::string format_sse_comment(const std::string& comment); -} +std::string format_sse_event(const SseEvent& event); +std::string format_sse_comment(const std::string& comment); +} // namespace khttpd::framework::sse #endif diff --git a/framework/sse/sse_parser.cpp b/framework/sse/sse_parser.cpp index aeab727..1404460 100644 --- a/framework/sse/sse_parser.cpp +++ b/framework/sse/sse_parser.cpp @@ -5,140 +5,151 @@ #include #include -namespace khttpd::framework::sse -{ - namespace - { - std::string single_line(std::string value) - { - value.erase(std::remove_if(value.begin(), value.end(), [](const char c) { return c == '\r' || c == '\n'; }), value.end()); - return value; - } +namespace khttpd::framework::sse { +namespace { +std::string single_line(std::string value) { + value.erase( + std::remove_if(value.begin(), value.end(), + [](const char c) { return c == '\r' || c == '\n'; }), + value.end()); + return value; +} - void append_data_lines(std::string& output, const std::string& data) - { - std::size_t begin = 0; - while (true) - { - const auto end = data.find_first_of("\r\n", begin); - output += "data: " + data.substr(begin, end == std::string::npos ? end : end - begin) + "\n"; - if (end == std::string::npos) break; - begin = end + (data[end] == '\r' && end + 1 < data.size() && data[end + 1] == '\n' ? 2 : 1); - } - } +void append_data_lines(std::string& output, const std::string& data) { + std::size_t begin = 0; + while (true) { + const auto end = data.find_first_of("\r\n", begin); + output += "data: " + + data.substr(begin, end == std::string::npos ? end : end - begin) + + "\n"; + if (end == std::string::npos) break; + begin = end + + (data[end] == '\r' && end + 1 < data.size() && data[end + 1] == '\n' + ? 2 + : 1); } +} +} // namespace - std::string format_sse_event(const SseEvent& event) - { - std::string output; - if (!event.event.empty()) output += "event: " + single_line(event.event) + "\n"; - if (!event.id.empty()) output += "id: " + single_line(event.id) + "\n"; - if (event.retry) output += "retry: " + std::to_string(*event.retry) + "\n"; - append_data_lines(output, event.data); - output += "\n"; - return output; - } +std::string format_sse_event(const SseEvent& event) { + std::string output; + if (!event.event.empty()) + output += "event: " + single_line(event.event) + "\n"; + if (!event.id.empty()) output += "id: " + single_line(event.id) + "\n"; + if (event.retry) output += "retry: " + std::to_string(*event.retry) + "\n"; + append_data_lines(output, event.data); + output += "\n"; + return output; +} - std::string format_sse_comment(const std::string& comment) - { - return ": " + single_line(comment) + "\n\n"; - } +std::string format_sse_comment(const std::string& comment) { + return ": " + single_line(comment) + "\n\n"; +} - std::vector SseParser::feed(const std::string_view bytes) - { - std::vector events; - std::size_t offset = 0; - const auto configured_chunk_limit = max_event_bytes_ == (std::numeric_limits::max)() - ? max_event_bytes_ : max_event_bytes_ + 1; - const auto chunk_limit = std::max(configured_chunk_limit, 3); - while (offset < bytes.size()) - { - const auto available = chunk_limit - buffer_.size(); - const auto chunk_size = std::min(available, bytes.size() - offset); - buffer_.append(bytes.data() + offset, chunk_size); - offset += chunk_size; +std::vector SseParser::feed(const std::string_view bytes) { + std::vector events; + std::size_t offset = 0; + const auto configured_chunk_limit = + max_event_bytes_ == (std::numeric_limits::max)() + ? max_event_bytes_ + : max_event_bytes_ + 1; + const auto chunk_limit = std::max(configured_chunk_limit, 3); + while (offset < bytes.size()) { + const auto available = chunk_limit - buffer_.size(); + const auto chunk_size = std::min(available, bytes.size() - offset); + buffer_.append(bytes.data() + offset, chunk_size); + offset += chunk_size; - if (skip_leading_lf_ && !buffer_.empty()) - { - if (buffer_.front() == '\n') buffer_.erase(0, 1); - skip_leading_lf_ = false; - } - if (at_stream_start_) - { - constexpr std::string_view utf8_bom{"\xef\xbb\xbf", 3}; - const auto prefix_size = std::min(buffer_.size(), utf8_bom.size()); - if (buffer_.compare(0, prefix_size, utf8_bom.data(), prefix_size) == 0 && - buffer_.size() < utf8_bom.size()) - continue; - if (buffer_.compare(0, utf8_bom.size(), utf8_bom.data(), utf8_bom.size()) == 0) - buffer_.erase(0, utf8_bom.size()); - at_stream_start_ = false; - } + if (skip_leading_lf_ && !buffer_.empty()) { + if (buffer_.front() == '\n') buffer_.erase(0, 1); + skip_leading_lf_ = false; + } + if (at_stream_start_) { + constexpr std::string_view utf8_bom{"\xef\xbb\xbf", 3}; + const auto prefix_size = std::min(buffer_.size(), utf8_bom.size()); + if (buffer_.compare(0, prefix_size, utf8_bom.data(), prefix_size) == 0 && + buffer_.size() < utf8_bom.size()) + continue; + if (buffer_.compare(0, utf8_bom.size(), utf8_bom.data(), + utf8_bom.size()) == 0) + buffer_.erase(0, utf8_bom.size()); + at_stream_start_ = false; + } - std::size_t consumed = 0; - while (consumed < buffer_.size()) - { - const auto newline = buffer_.find_first_of("\r\n", consumed); - if (newline == std::string::npos) break; - if (newline - consumed > max_event_bytes_) - throw std::length_error("SSE line exceeds configured event limit"); - const auto line = buffer_.substr(consumed, newline - consumed); - process_line(line, events); - if (buffer_[newline] == '\r' && newline + 1 < buffer_.size() && buffer_[newline + 1] == '\n') - consumed = newline + 2; - else - { - consumed = newline + 1; - if (buffer_[newline] == '\r' && consumed == buffer_.size()) skip_leading_lf_ = true; - } - } - if (consumed != 0) buffer_.erase(0, consumed); - if (!at_stream_start_ && buffer_.size() > max_event_bytes_) + std::size_t consumed = 0; + while (consumed < buffer_.size()) { + const auto newline = buffer_.find_first_of("\r\n", consumed); + if (newline == std::string::npos) break; + if (newline - consumed > max_event_bytes_) throw std::length_error("SSE line exceeds configured event limit"); + const auto line = buffer_.substr(consumed, newline - consumed); + process_line(line, events); + if (buffer_[newline] == '\r' && newline + 1 < buffer_.size() && + buffer_[newline + 1] == '\n') + consumed = newline + 2; + else { + consumed = newline + 1; + if (buffer_[newline] == '\r' && consumed == buffer_.size()) + skip_leading_lf_ = true; + } } - return events; + if (consumed != 0) buffer_.erase(0, consumed); + if (!at_stream_start_ && buffer_.size() > max_event_bytes_) + throw std::length_error("SSE line exceeds configured event limit"); } + return events; +} - void SseParser::process_line(const std::string& line, std::vector& events) - { - if (line.empty()) - { - if (!data_.empty()) - { - data_.pop_back(); - events.push_back({event_.empty() ? "message" : event_, data_, last_event_id_, retry_}); - } - event_.clear(); - data_.clear(); - retry_.reset(); - return; +void SseParser::process_line(const std::string& line, + std::vector& events) { + if (line.empty()) { + if (!data_.empty()) { + data_.pop_back(); + events.push_back( + {event_.empty() ? "message" : event_, data_, last_event_id_, retry_}); } - if (line.front() == ':') return; - const auto colon = line.find(':'); - const auto field = line.substr(0, colon); - auto value = colon == std::string::npos ? std::string{} : line.substr(colon + 1); - if (!value.empty() && value.front() == ' ') value.erase(0, 1); - if (value.size() > max_event_bytes_) - throw std::length_error("SSE field exceeds configured event limit"); - if (field == "event") event_ = value; - else if (field == "data") - { - if (value.size() >= max_event_bytes_ || data_.size() > max_event_bytes_ - value.size() - 1) - throw std::length_error("SSE event data exceeds configured event limit"); - data_ += value; data_ += '\n'; - } - else if (field == "id" && value.find('\0') == std::string::npos) last_event_id_ = value; - else if (field == "retry" && !value.empty() && - std::all_of(value.begin(), value.end(), - [](const unsigned char c) { return std::isdigit(c) != 0; })) - { - try { retry_ = std::stoull(value); } catch (...) { retry_.reset(); } + event_.clear(); + data_.clear(); + retry_.reset(); + return; + } + if (line.front() == ':') return; + const auto colon = line.find(':'); + const auto field = line.substr(0, colon); + auto value = + colon == std::string::npos ? std::string{} : line.substr(colon + 1); + if (!value.empty() && value.front() == ' ') value.erase(0, 1); + if (value.size() > max_event_bytes_) + throw std::length_error("SSE field exceeds configured event limit"); + if (field == "event") + event_ = value; + else if (field == "data") { + if (value.size() >= max_event_bytes_ || + data_.size() > max_event_bytes_ - value.size() - 1) + throw std::length_error("SSE event data exceeds configured event limit"); + data_ += value; + data_ += '\n'; + } else if (field == "id" && value.find('\0') == std::string::npos) + last_event_id_ = value; + else if (field == "retry" && !value.empty() && + std::all_of(value.begin(), value.end(), [](const unsigned char c) { + return std::isdigit(c) != 0; + })) { + try { + retry_ = std::stoull(value); + } catch (...) { + retry_.reset(); } } +} - void SseParser::reset() - { - buffer_.clear(); event_.clear(); data_.clear(); last_event_id_.clear(); retry_.reset(); - at_stream_start_ = true; skip_leading_lf_ = false; - } +void SseParser::reset() { + buffer_.clear(); + event_.clear(); + data_.clear(); + last_event_id_.clear(); + retry_.reset(); + at_stream_start_ = true; + skip_leading_lf_ = false; } +} // namespace khttpd::framework::sse diff --git a/framework/sse/sse_parser.hpp b/framework/sse/sse_parser.hpp index c0cc3e0..9c49841 100644 --- a/framework/sse/sse_parser.hpp +++ b/framework/sse/sse_parser.hpp @@ -1,36 +1,34 @@ #ifndef KHTTPD_FRAMEWORK_SSE_SSE_PARSER_HPP #define KHTTPD_FRAMEWORK_SSE_SSE_PARSER_HPP -#include "sse/sse_event.hpp" - #include #include #include #include -namespace khttpd::framework::sse -{ - class SseParser - { - public: - static constexpr std::size_t default_max_event_bytes = 1024 * 1024; +#include "sse/sse_event.hpp" + +namespace khttpd::framework::sse { +class SseParser { + public: + static constexpr std::size_t default_max_event_bytes = 1024 * 1024; - explicit SseParser(std::size_t max_event_bytes = default_max_event_bytes) + explicit SseParser(std::size_t max_event_bytes = default_max_event_bytes) : max_event_bytes_(max_event_bytes) {} - std::vector feed(std::string_view bytes); - void reset(); + std::vector feed(std::string_view bytes); + void reset(); - private: - void process_line(const std::string& line, std::vector& events); - std::string buffer_; - std::string event_; - std::string data_; - std::string last_event_id_; - std::optional retry_; - bool at_stream_start_ = true; - bool skip_leading_lf_ = false; - std::size_t max_event_bytes_; - }; -} + private: + void process_line(const std::string& line, std::vector& events); + std::string buffer_; + std::string event_; + std::string data_; + std::string last_event_id_; + std::optional retry_; + bool at_stream_start_ = true; + bool skip_leading_lf_ = false; + std::size_t max_event_bytes_; +}; +} // namespace khttpd::framework::sse #endif diff --git a/framework/sse/sse_session.cpp b/framework/sse/sse_session.cpp index 7902b0f..52771e0 100644 --- a/framework/sse/sse_session.cpp +++ b/framework/sse/sse_session.cpp @@ -1,118 +1,115 @@ #include "sse/sse_session.hpp" +#include + #include #include #include #include #include -#include - -namespace khttpd::framework::sse -{ - struct SseSession::Impl : std::enable_shared_from_this - { - std::shared_ptr response; - int version; - bool keep_alive; - mutable std::mutex mutex; - std::deque> queue; - std::size_t pending_bytes = 0; - std::size_t max_pending_bytes; - bool start_requested = false; - bool started = false; - bool writing = false; - bool closing = false; - bool closed = false; - boost::system::error_code close_error; - CloseHandler close_handler; +namespace khttpd::framework::sse { +struct SseSession::Impl : std::enable_shared_from_this { + std::shared_ptr response; + int version; + bool keep_alive; + mutable std::mutex mutex; + std::deque> queue; + std::size_t pending_bytes = 0; + std::size_t max_pending_bytes; + bool start_requested = false; + bool started = false; + bool writing = false; + bool closing = false; + bool closed = false; + boost::system::error_code close_error; + CloseHandler close_handler; - Impl(std::shared_ptr value, const int v, const bool keep, - const std::size_t maximum_pending_bytes) - : response(std::move(value)), version(v), keep_alive(keep), + Impl(std::shared_ptr value, const int v, const bool keep, + const std::size_t maximum_pending_bytes) + : response(std::move(value)), + version(v), + keep_alive(keep), max_pending_bytes(maximum_pending_bytes) {} - void start() + void start() { { + std::lock_guard lock(mutex); + if (start_requested || closed) return; + start_requested = true; + } + HttpResponseStream::ResponseHead head{boost::beast::http::status::ok, + version}; + head.keep_alive(keep_alive); + head.set(boost::beast::http::field::content_type, "text/event-stream"); + head.set(boost::beast::http::field::cache_control, "no-cache"); + head.set("X-Accel-Buffering", "no"); + response->async_start(std::move(head), [self = shared_from_this()]( + boost::system::error_code ec) { + if (ec) return self->finish(ec); + bool watch_disconnect = false; { - std::lock_guard lock(mutex); - if (start_requested || closed) return; - start_requested = true; + std::lock_guard lock(self->mutex); + self->started = true; + watch_disconnect = !self->closing && !self->closed; } - HttpResponseStream::ResponseHead head{boost::beast::http::status::ok, version}; - head.keep_alive(keep_alive); - head.set(boost::beast::http::field::content_type, "text/event-stream"); - head.set(boost::beast::http::field::cache_control, "no-cache"); - head.set("X-Accel-Buffering", "no"); - response->async_start(std::move(head), [self = shared_from_this()](boost::system::error_code ec) - { - if (ec) return self->finish(ec); - bool watch_disconnect = false; - { - std::lock_guard lock(self->mutex); - self->started = true; - watch_disconnect = !self->closing && !self->closed; - } - if (watch_disconnect) self->wait_for_disconnect(); - self->write_next(); - }); - } + if (watch_disconnect) self->wait_for_disconnect(); + self->write_next(); + }); + } - void wait_for_disconnect() - { - response->async_wait_disconnect([self = shared_from_this()](boost::system::error_code ec) - { - if (ec == boost::asio::error::operation_aborted) return; - self->response->cancel(); - self->finish(ec ? ec : boost::asio::error::eof); - }); - } + void wait_for_disconnect() { + response->async_wait_disconnect( + [self = shared_from_this()](boost::system::error_code ec) { + if (ec == boost::asio::error::operation_aborted) return; + self->response->cancel(); + self->finish(ec ? ec : boost::asio::error::eof); + }); + } - bool enqueue(std::string value) + bool enqueue(std::string value) { { - { - std::lock_guard lock(mutex); - if (closing || closed) return false; - if (value.size() > max_pending_bytes || pending_bytes > max_pending_bytes - value.size()) return false; - pending_bytes += value.size(); - queue.push_back(std::make_shared(std::move(value))); - } - write_next(); - return true; + std::lock_guard lock(mutex); + if (closing || closed) return false; + if (value.size() > max_pending_bytes || + pending_bytes > max_pending_bytes - value.size()) + return false; + pending_bytes += value.size(); + queue.push_back(std::make_shared(std::move(value))); } + write_next(); + return true; + } - void write_next() + void write_next() { + std::shared_ptr value; + bool should_finish = false; { - std::shared_ptr value; - bool should_finish = false; - { - std::lock_guard lock(mutex); - if (!started || writing || closed) return; - if (queue.empty()) - { - if (!closing) return; - writing = true; - should_finish = true; - } - else - { - writing = true; - value = queue.front(); - } + std::lock_guard lock(mutex); + if (!started || writing || closed) return; + if (queue.empty()) { + if (!closing) return; + writing = true; + should_finish = true; + } else { + writing = true; + value = queue.front(); } - if (should_finish) - { - response->async_finish([self = shared_from_this()](boost::system::error_code ec) { self->finish(ec); }); - return; - } - response->async_write_some(boost::asio::buffer(*value), - [self = shared_from_this(), value](boost::system::error_code ec) - { + } + if (should_finish) { + response->async_finish( + [self = shared_from_this()](boost::system::error_code ec) { + self->finish(ec); + }); + return; + } + response->async_write_some( + boost::asio::buffer(*value), + [self = shared_from_this(), value](boost::system::error_code ec) { if (ec) return self->finish(ec); { std::lock_guard lock(self->mutex); - if (!self->queue.empty()) - { + if (!self->queue.empty()) { self->pending_bytes -= self->queue.front()->size(); self->queue.pop_front(); } @@ -120,67 +117,75 @@ namespace khttpd::framework::sse } self->write_next(); }); - } + } - void finish(boost::system::error_code ec) + void finish(boost::system::error_code ec) { + CloseHandler handler; { - CloseHandler handler; - { - std::lock_guard lock(mutex); - if (closed) return; - closed = true; - close_error = ec; - writing = false; - pending_bytes = 0; - queue.clear(); - handler = std::move(close_handler); - } - response->cancel_disconnect_wait(); - invoke_close_handler(handler, ec); + std::lock_guard lock(mutex); + if (closed) return; + closed = true; + close_error = ec; + writing = false; + pending_bytes = 0; + queue.clear(); + handler = std::move(close_handler); } + response->cancel_disconnect_wait(); + invoke_close_handler(handler, ec); + } - static void invoke_close_handler(const CloseHandler& handler, const boost::system::error_code ec) - { - if (!handler) return; - try - { - handler(ec); - } - catch (const std::exception& error) - { - spdlog::error("SSE session close callback failed: {}", error.what()); - } - catch (...) - { - spdlog::error("SSE session close callback failed with a non-standard exception"); - } + static void invoke_close_handler(const CloseHandler& handler, + const boost::system::error_code ec) { + if (!handler) return; + try { + handler(ec); + } catch (const std::exception& error) { + spdlog::error("SSE session close callback failed: {}", error.what()); + } catch (...) { + spdlog::error( + "SSE session close callback failed with a non-standard exception"); } - }; + } +}; - SseSession::SseSession(std::shared_ptr response, const int version, const bool keep_alive, - const std::size_t max_pending_bytes) - : impl_(std::make_shared(std::move(response), version, keep_alive, max_pending_bytes)) {} - void SseSession::start() { impl_->start(); } - bool SseSession::send(SseEvent event) { return impl_->enqueue(format_sse_event(event)); } - bool SseSession::send_comment(std::string comment) { return impl_->enqueue(format_sse_comment(comment)); } - void SseSession::close() +SseSession::SseSession(std::shared_ptr response, + const int version, const bool keep_alive, + const std::size_t max_pending_bytes) + : impl_(std::make_shared(std::move(response), version, keep_alive, + max_pending_bytes)) {} +void SseSession::start() { impl_->start(); } +bool SseSession::send(SseEvent event) { + return impl_->enqueue(format_sse_event(event)); +} +bool SseSession::send_comment(std::string comment) { + return impl_->enqueue(format_sse_comment(comment)); +} +void SseSession::close() { + impl_->response->cancel_disconnect_wait(); { - impl_->response->cancel_disconnect_wait(); - { std::lock_guard lock(impl_->mutex); impl_->closing = true; } - impl_->write_next(); + std::lock_guard lock(impl_->mutex); + impl_->closing = true; } - void SseSession::cancel() { impl_->response->cancel(); impl_->finish(boost::asio::error::operation_aborted); } - void SseSession::on_close(CloseHandler handler) + impl_->write_next(); +} +void SseSession::cancel() { + impl_->response->cancel(); + impl_->finish(boost::asio::error::operation_aborted); +} +void SseSession::on_close(CloseHandler handler) { + boost::system::error_code error; + bool already_closed = false; { - boost::system::error_code error; - bool already_closed = false; - { - std::lock_guard lock(impl_->mutex); - already_closed = impl_->closed; - error = impl_->close_error; - if (!already_closed) impl_->close_handler = std::move(handler); - } - if (already_closed) Impl::invoke_close_handler(handler, error); + std::lock_guard lock(impl_->mutex); + already_closed = impl_->closed; + error = impl_->close_error; + if (!already_closed) impl_->close_handler = std::move(handler); } - bool SseSession::is_open() const { std::lock_guard lock(impl_->mutex); return !impl_->closing && !impl_->closed; } + if (already_closed) Impl::invoke_close_handler(handler, error); +} +bool SseSession::is_open() const { + std::lock_guard lock(impl_->mutex); + return !impl_->closing && !impl_->closed; } +} // namespace khttpd::framework::sse diff --git a/framework/sse/sse_session.hpp b/framework/sse/sse_session.hpp index fb3a5d9..b60bd00 100644 --- a/framework/sse/sse_session.hpp +++ b/framework/sse/sse_session.hpp @@ -1,36 +1,35 @@ #ifndef KHTTPD_FRAMEWORK_SSE_SSE_SESSION_HPP #define KHTTPD_FRAMEWORK_SSE_SSE_SESSION_HPP -#include "context/http_response_stream.hpp" -#include "sse/sse_event.hpp" - #include #include #include #include -namespace khttpd::framework::sse -{ - class SseSession : public std::enable_shared_from_this - { - public: - using CloseHandler = std::function; - static constexpr std::size_t default_max_pending_bytes = 1024 * 1024; +#include "context/http_response_stream.hpp" +#include "sse/sse_event.hpp" + +namespace khttpd::framework::sse { +class SseSession : public std::enable_shared_from_this { + public: + using CloseHandler = std::function; + static constexpr std::size_t default_max_pending_bytes = 1024 * 1024; - SseSession(std::shared_ptr response, int http_version, bool keep_alive, - std::size_t max_pending_bytes = default_max_pending_bytes); - void start(); - bool send(SseEvent event); - bool send_comment(std::string comment); - void close(); - void cancel(); - void on_close(CloseHandler handler); - bool is_open() const; + SseSession(std::shared_ptr response, int http_version, + bool keep_alive, + std::size_t max_pending_bytes = default_max_pending_bytes); + void start(); + bool send(SseEvent event); + bool send_comment(std::string comment); + void close(); + void cancel(); + void on_close(CloseHandler handler); + bool is_open() const; - private: - struct Impl; - std::shared_ptr impl_; - }; -} + private: + struct Impl; + std::shared_ptr impl_; +}; +} // namespace khttpd::framework::sse #endif diff --git a/framework/tests/buffered_body_limit_test.cpp b/framework/tests/buffered_body_limit_test.cpp index 8f08e1c..d8de953 100644 --- a/framework/tests/buffered_body_limit_test.cpp +++ b/framework/tests/buffered_body_limit_test.cpp @@ -1,41 +1,43 @@ -#include "http_session_test_harness.hpp" -#include "framework/server.hpp" - #include +#include "framework/server.hpp" +#include "http_session_test_harness.hpp" + namespace fw = khttpd::framework; namespace test = khttpd::framework::tests; namespace http = boost::beast::http; -namespace -{ - http::response send_body(std::uint64_t limit, std::string body, - bool chunked, bool& handled) - { - test::TempWebRoot web_root; - fw::HttpRouter router; - fw::WebsocketRouter websocket_router; - router.post("/body", [&handled](fw::HttpContext& ctx) - { - handled = true; - ctx.set_body(std::to_string(ctx.get_request().body().size())); - }); - http::request request{http::verb::post, "/body", 11}; - request.body() = std::move(body); - if (chunked) request.chunked(true); else request.prepare_payload(); - request.keep_alive(false); - return test::round_trip(router, websocket_router, web_root.path, std::move(request), limit); - } +namespace { +http::response send_body(std::uint64_t limit, + std::string body, bool chunked, + bool& handled) { + test::TempWebRoot web_root; + fw::HttpRouter router; + fw::WebsocketRouter websocket_router; + router.post("/body", [&handled](fw::HttpContext& ctx) { + handled = true; + ctx.set_body(std::to_string(ctx.get_request().body().size())); + }); + http::request request{http::verb::post, "/body", 11}; + request.body() = std::move(body); + if (chunked) + request.chunked(true); + else + request.prepare_payload(); + request.keep_alive(false); + return test::round_trip(router, websocket_router, web_root.path, + std::move(request), limit); } +} // namespace -TEST(BufferedBodyLimitTest, DefaultLimitRemainsSixteenMiB) -{ - EXPECT_EQ(fw::HttpSession::default_max_buffered_request_body_size, 16ULL * 1024 * 1024); - EXPECT_EQ(fw::Server::default_max_buffered_request_body_size, 16ULL * 1024 * 1024); +TEST(BufferedBodyLimitTest, DefaultLimitRemainsSixteenMiB) { + EXPECT_EQ(fw::HttpSession::default_max_buffered_request_body_size, + 16ULL * 1024 * 1024); + EXPECT_EQ(fw::Server::default_max_buffered_request_body_size, + 16ULL * 1024 * 1024); } -TEST(BufferedBodyLimitTest, ZeroLimitAcceptsEmptyBody) -{ +TEST(BufferedBodyLimitTest, ZeroLimitAcceptsEmptyBody) { bool handled = false; const auto response = send_body(0, {}, false, handled); EXPECT_EQ(response.result(), http::status::ok); @@ -43,8 +45,7 @@ TEST(BufferedBodyLimitTest, ZeroLimitAcceptsEmptyBody) EXPECT_EQ(response.body(), "0"); } -TEST(BufferedBodyLimitTest, ZeroLimitRejectsOneByte) -{ +TEST(BufferedBodyLimitTest, ZeroLimitRejectsOneByte) { bool handled = false; const auto response = send_body(0, "x", false, handled); EXPECT_EQ(response.result(), http::status::payload_too_large); @@ -52,8 +53,7 @@ TEST(BufferedBodyLimitTest, ZeroLimitRejectsOneByte) EXPECT_FALSE(response.keep_alive()); } -TEST(BufferedBodyLimitTest, ContentLengthAtLimitIsAccepted) -{ +TEST(BufferedBodyLimitTest, ContentLengthAtLimitIsAccepted) { bool handled = false; const auto response = send_body(17, std::string(17, 'x'), false, handled); EXPECT_EQ(response.result(), http::status::ok); @@ -61,8 +61,7 @@ TEST(BufferedBodyLimitTest, ContentLengthAtLimitIsAccepted) EXPECT_EQ(response.body(), "17"); } -TEST(BufferedBodyLimitTest, ChunkedBodyIsRejectedAfterCumulativeLimit) -{ +TEST(BufferedBodyLimitTest, ChunkedBodyIsRejectedAfterCumulativeLimit) { bool handled = false; const auto response = send_body(31, std::string(32, 'x'), true, handled); EXPECT_EQ(response.result(), http::status::payload_too_large); diff --git a/framework/tests/client_test.cpp b/framework/tests/client_test.cpp index 53d1060..20f6a4f 100644 --- a/framework/tests/client_test.cpp +++ b/framework/tests/client_test.cpp @@ -1,22 +1,23 @@ -#include "framework/client/http_client.hpp" -#include "framework/client/http_client_stream.hpp" -#include "framework/client/websocket_client.hpp" -#include "framework/client/api_macros.hpp" -#include "framework/client/host_pool.hpp" #include -#include +#include + +#include +#include #include #include +#include #include -#include -#include -#include -#include +#include #include +#include +#include #include -#include -#include +#include "framework/client/api_macros.hpp" +#include "framework/client/host_pool.hpp" +#include "framework/client/http_client.hpp" +#include "framework/client/http_client_stream.hpp" +#include "framework/client/websocket_client.hpp" #include "io_context_pool.hpp" using namespace khttpd::framework::client; @@ -26,50 +27,44 @@ namespace websocket = boost::beast::websocket; namespace net = boost::asio; using tcp = boost::asio::ip::tcp; -namespace -{ -class ClientTestLogger : public ::testing::EmptyTestEventListener -{ -public: - void OnTestStart(const ::testing::TestInfo& test_info) override - { - spdlog::info("[client_test] START {}.{}", test_info.test_suite_name(), test_info.name()); +namespace { +class ClientTestLogger : public ::testing::EmptyTestEventListener { + public: + void OnTestStart(const ::testing::TestInfo& test_info) override { + spdlog::info("[client_test] START {}.{}", test_info.test_suite_name(), + test_info.name()); } - void OnTestEnd(const ::testing::TestInfo& test_info) override - { - spdlog::info("[client_test] END {}.{} result={}", test_info.test_suite_name(), test_info.name(), + void OnTestEnd(const ::testing::TestInfo& test_info) override { + spdlog::info("[client_test] END {}.{} result={}", + test_info.test_suite_name(), test_info.name(), test_info.result()->Passed() ? "PASS" : "FAIL"); } }; -class ClientTestLoggerEnvironment : public ::testing::Environment -{ -public: - void SetUp() override - { +class ClientTestLoggerEnvironment : public ::testing::Environment { + public: + void SetUp() override { spdlog::set_level(spdlog::level::info); - ::testing::UnitTest::GetInstance()->listeners().Append(new ClientTestLogger()); + ::testing::UnitTest::GetInstance()->listeners().Append( + new ClientTestLogger()); spdlog::info("[client_test] logger installed"); } }; const auto* const kClientTestLoggerEnvironment = - ::testing::AddGlobalTestEnvironment(new ClientTestLoggerEnvironment()); + ::testing::AddGlobalTestEnvironment(new ClientTestLoggerEnvironment()); -class LocalHttpEchoServer -{ -public: +class LocalHttpEchoServer { + public: LocalHttpEchoServer() - : acceptor_(ioc_, tcp::endpoint(net::ip::address_v4::loopback(), 0)), - port_(acceptor_.local_endpoint().port()) - { + : acceptor_(ioc_, tcp::endpoint(net::ip::address_v4::loopback(), 0)), + port_(acceptor_.local_endpoint().port()) { do_accept(); thread_ = std::thread([this]() { ioc_.run(); }); } - ~LocalHttpEchoServer() - { + ~LocalHttpEchoServer() { spdlog::info("[client_test] LocalHttpEchoServer stopping"); boost::system::error_code ignored; acceptor_.close(ignored); @@ -80,59 +75,49 @@ class LocalHttpEchoServer std::lock_guard lock(workers_mutex_); workers.swap(workers_); } - for (auto& worker : workers) - { + for (auto& worker : workers) { if (worker.joinable()) worker.join(); } spdlog::info("[client_test] LocalHttpEchoServer stopped"); } - std::string base_url() const - { + std::string base_url() const { return "http://127.0.0.1:" + std::to_string(port_); } -private: - void do_accept() - { - acceptor_.async_accept([this](boost::system::error_code ec, tcp::socket socket) - { - if (!ec) - { - std::lock_guard lock(workers_mutex_); - workers_.emplace_back([socket = std::move(socket)]() mutable - { - handle_session(std::move(socket)); + private: + void do_accept() { + acceptor_.async_accept( + [this](boost::system::error_code ec, tcp::socket socket) { + if (!ec) { + std::lock_guard lock(workers_mutex_); + workers_.emplace_back([socket = std::move(socket)]() mutable { + handle_session(std::move(socket)); + }); + } + + if (acceptor_.is_open()) { + do_accept(); + } }); - } - - if (acceptor_.is_open()) - { - do_accept(); - } - }); } - static std::string query_value(std::string target, const std::string& key) - { + static std::string query_value(std::string target, const std::string& key) { const auto query_pos = target.find('?'); if (query_pos == std::string::npos) return ""; std::string query = target.substr(query_pos + 1); std::istringstream parts(query); std::string item; - while (std::getline(parts, item, '&')) - { + while (std::getline(parts, item, '&')) { const auto eq_pos = item.find('='); - if (eq_pos != std::string::npos && item.substr(0, eq_pos) == key) - { + if (eq_pos != std::string::npos && item.substr(0, eq_pos) == key) { return item.substr(eq_pos + 1); } } return ""; } - static void handle_session(tcp::socket socket) - { + static void handle_session(tcp::socket socket) { beast::flat_buffer buffer; boost::system::error_code ec; http::request_parser parser; @@ -147,32 +132,24 @@ class LocalHttpEchoServer res.keep_alive(false); const std::string target(req.target()); - if (target.rfind("/get", 0) == 0) - { + if (target.rfind("/get", 0) == 0) { res.body() = "{\"foo\":\"" + query_value(target, "foo") + "\",\"id\":\"" + - query_value(target, "id") + "\",\"msg\":\"" + query_value(target, "msg") + "\"}"; - } - else if (target.rfind("/headers", 0) == 0) - { + query_value(target, "id") + "\",\"msg\":\"" + + query_value(target, "msg") + "\"}"; + } else if (target.rfind("/headers", 0) == 0) { std::string body = "{"; - for (const auto& field : req) - { - body += "\"" + std::string(field.name_string()) + "\":\"" + std::string(field.value()) + "\","; + for (const auto& field : req) { + body += "\"" + std::string(field.name_string()) + "\":\"" + + std::string(field.value()) + "\","; } body += "\"done\":true}"; res.body() = std::move(body); - } - else if (target.rfind("/post", 0) == 0) - { + } else if (target.rfind("/post", 0) == 0) { res.body() = "{\"data\":" + req.body() + "}"; - } - else if (target.rfind("/stream", 0) == 0) - { + } else if (target.rfind("/stream", 0) == 0) { res.set(http::field::content_type, "application/octet-stream"); res.body() = req.body(); - } - else - { + } else { res.body() = "{\"target\":\"" + target + "\"}"; } @@ -189,26 +166,21 @@ class LocalHttpEchoServer std::vector workers_; }; -class LocalWebSocketEchoServer -{ -public: +class LocalWebSocketEchoServer { + public: LocalWebSocketEchoServer() - : acceptor_(ioc_, tcp::endpoint(net::ip::address_v4::loopback(), 0)), - port_(acceptor_.local_endpoint().port()) - { + : acceptor_(ioc_, tcp::endpoint(net::ip::address_v4::loopback(), 0)), + port_(acceptor_.local_endpoint().port()) { do_accept(); thread_ = std::thread([this]() { ioc_.run(); }); } - ~LocalWebSocketEchoServer() - { + ~LocalWebSocketEchoServer() { spdlog::info("[client_test] LocalWebSocketEchoServer stopping"); - net::post(ioc_, [this]() - { + net::post(ioc_, [this]() { boost::system::error_code ignored; acceptor_.close(ignored); - if (ws_) - { + if (ws_) { beast::get_lowest_layer(*ws_).cancel(ignored); beast::get_lowest_layer(*ws_).close(ignored); } @@ -218,45 +190,40 @@ class LocalWebSocketEchoServer spdlog::info("[client_test] LocalWebSocketEchoServer stopped"); } - std::string url() const - { - return "ws://127.0.0.1:" + std::to_string(port_); - } + std::string url() const { return "ws://127.0.0.1:" + std::to_string(port_); } -private: - void do_accept() - { - acceptor_.async_accept([this](boost::system::error_code ec, tcp::socket socket) - { + private: + void do_accept() { + acceptor_.async_accept([this](boost::system::error_code ec, + tcp::socket socket) { if (ec) return; ws_ = std::make_shared>(std::move(socket)); - ws_->async_accept([this, ws = ws_](boost::system::error_code accept_ec) - { + ws_->async_accept([this, ws = ws_](boost::system::error_code accept_ec) { if (accept_ec) return; do_read(ws, 0); }); }); } - void do_read(std::shared_ptr> ws, int count) - { - if (count >= 5) - { - ws->async_close(websocket::close_code::normal, [](boost::system::error_code) {}); + void do_read(std::shared_ptr> ws, int count) { + if (count >= 5) { + ws->async_close(websocket::close_code::normal, + [](boost::system::error_code) {}); return; } auto buffer = std::make_shared(); - ws->async_read(*buffer, [this, ws, buffer, count](boost::system::error_code ec, std::size_t) - { + ws->async_read(*buffer, [this, ws, buffer, count]( + boost::system::error_code ec, std::size_t) { if (ec) return; ws->text(ws->got_text()); - ws->async_write(buffer->data(), [this, ws, count](boost::system::error_code write_ec, std::size_t) - { - if (write_ec) return; - do_read(ws, count + 1); - }); + ws->async_write( + buffer->data(), + [this, ws, count](boost::system::error_code write_ec, std::size_t) { + if (write_ec) return; + do_read(ws, count + 1); + }); }); } @@ -267,22 +234,19 @@ class LocalWebSocketEchoServer std::shared_ptr> ws_; }; -LocalHttpEchoServer& local_http_echo_server() -{ +LocalHttpEchoServer& local_http_echo_server() { static LocalHttpEchoServer server; return server; } -} +} // namespace // ========================================== // 1. 定义 PostmanEchoClient 类 // ========================================== -class PostmanEchoClient : public HttpClient -{ -public: +class PostmanEchoClient : public HttpClient { + public: // 构造函数:注入 ioc,并设置默认 Base URL - PostmanEchoClient() - { + PostmanEchoClient() { set_base_url(local_http_echo_server().base_url()); // 设置一个较长的超时时间,防止 CI 环境网络慢 set_timeout(std::chrono::seconds(10)); @@ -295,8 +259,7 @@ class PostmanEchoClient : public HttpClient // 1. GET 请求,带查询参数 // Endpoint: /get?foo=bar API_CALL(http::verb::get, "/get", echo_get, - QUERY(std::string, foo_val, "foo"), - QUERY(int, id_val, "id")) + QUERY(std::string, foo_val, "foo"), QUERY(int, id_val, "id")) // 2. POST 请求,带 JSON Body // Endpoint: /post @@ -311,7 +274,8 @@ class PostmanEchoClient : public HttpClient HEADER(std::string, user_token, "X-User-Token")) // 4. PUT 请求,带路径参数 - // Endpoint: /put (Postman echo 实际上忽略路径后的东西,但我们可以测试 URL 拼接) + // Endpoint: /put (Postman echo 实际上忽略路径后的东西,但我们可以测试 URL + // 拼接) API_CALL(http::verb::put, "/put", echo_put_dummy) }; @@ -319,49 +283,40 @@ class PostmanEchoClient : public HttpClient // 2. 测试用例 // ========================================== -class ClientTest : public ::testing::Test -{ -protected: +class ClientTest : public ::testing::Test { + protected: boost::asio::io_context ioc; std::shared_ptr client; // 辅助:用于在主线程等待异步结果 - void run_until_complete() - { + void run_until_complete() { ioc.run(); - ioc.restart(); // 重置以便下次使用 + ioc.restart(); // 重置以便下次使用 } - void SetUp() override - { - client = std::make_shared(); - } + void SetUp() override { client = std::make_shared(); } }; - // 辅助宏:等待异步结果 // 如果 5 秒没结果,这就认为超时失败 -#define WAIT_FOR_ASYNC(future) \ - ASSERT_EQ(future.wait_for(std::chrono::seconds(5)), std::future_status::ready) << "Async operation timed out"; +#define WAIT_FOR_ASYNC(future) \ + ASSERT_EQ(future.wait_for(std::chrono::seconds(5)), \ + std::future_status::ready) \ + << "Async operation timed out"; -TEST_F(ClientTest, GetWithQueryParams) -{ +TEST_F(ClientTest, GetWithQueryParams) { // 创建一个 promise 用于通知主线程任务完成 std::promise promise; auto future = promise.get_future(); - client->echo_get("hello", 123, [&](auto ec, auto res) - { + client->echo_get("hello", 123, [&](auto ec, auto res) { // 这里的代码在后台线程运行 - if (!ec) - { + if (!ec) { EXPECT_EQ(res.result(), http::status::ok); std::string body = res.body(); EXPECT_TRUE(body.find("\"foo\":\"hello\"") != std::string::npos); EXPECT_TRUE(body.find("\"id\":\"123\"") != std::string::npos); - } - else - { + } else { ADD_FAILURE() << "Network error: " << ec.message(); } @@ -373,8 +328,19 @@ TEST_F(ClientTest, GetWithQueryParams) WAIT_FOR_ASYNC(future); } -TEST_F(ClientTest, PostJsonBody) -{ +TEST(HttpClientLocalTest, SyncRequestAcceptsQueryInPathForCompatibility) { + HttpClient client; + client.set_base_url(local_http_echo_server().base_url()); + client.set_timeout(std::chrono::seconds(5)); + + const auto response = client.request_sync( + http::verb::get, "/get?foo=legacy&id=7", {}, "", {}); + ASSERT_EQ(response.result(), http::status::ok); + EXPECT_NE(response.body().find("\"foo\":\"legacy\""), std::string::npos); + EXPECT_NE(response.body().find("\"id\":\"7\""), std::string::npos); +} + +TEST_F(ClientTest, PostJsonBody) { std::promise promise; auto future = promise.get_future(); @@ -382,16 +348,12 @@ TEST_F(ClientTest, PostJsonBody) jv["message"] = "test_payload"; jv["count"] = 99; - client->echo_post(jv, [&](auto ec, auto res) - { - if (!ec) - { + client->echo_post(jv, [&](auto ec, auto res) { + if (!ec) { EXPECT_EQ(res.result(), http::status::ok); std::string body = res.body(); EXPECT_TRUE(body.find("test_payload") != std::string::npos); - } - else - { + } else { ADD_FAILURE() << "Network error: " << ec.message(); } promise.set_value(); @@ -400,30 +362,25 @@ TEST_F(ClientTest, PostJsonBody) WAIT_FOR_ASYNC(future); } -TEST_F(ClientTest, CustomHeaders) -{ +TEST_F(ClientTest, CustomHeaders) { std::promise promise; auto future = promise.get_future(); std::string rid = "req-unique-id-001"; std::string token = "secret-token-abc"; - client->echo_headers(rid, token, [&](auto ec, auto res) - { - if (!ec) - { + client->echo_headers(rid, token, [&](auto ec, auto res) { + if (!ec) { EXPECT_EQ(res.result(), http::status::ok); std::string body = res.body(); bool has_rid = body.find("x-my-request-id") != std::string::npos || - body.find("X-My-Request-Id") != std::string::npos; + body.find("X-My-Request-Id") != std::string::npos; bool has_val = body.find(rid) != std::string::npos; EXPECT_TRUE(has_rid) << "Missing Header Key"; EXPECT_TRUE(has_val) << "Missing Header Value"; - } - else - { + } else { ADD_FAILURE() << "Network error: " << ec.message(); } promise.set_value(); @@ -432,17 +389,14 @@ TEST_F(ClientTest, CustomHeaders) WAIT_FOR_ASYNC(future); } -TEST_F(ClientTest, GlobalDefaultHeader) -{ +TEST_F(ClientTest, GlobalDefaultHeader) { std::promise promise; auto future = promise.get_future(); client->set_default_header("X-App-Version", "v1.0.0-beta"); - client->echo_headers("id-1", "token-1", [&](auto ec, auto res) - { - if (!ec) - { + client->echo_headers("id-1", "token-1", [&](auto ec, auto res) { + if (!ec) { std::string body = res.body(); EXPECT_TRUE(body.find("v1.0.0-beta") != std::string::npos); } @@ -453,44 +407,33 @@ TEST_F(ClientTest, GlobalDefaultHeader) } // 同步调用测试 (现在非常安全,不会死锁) -TEST_F(ClientTest, SyncCallSafe) -{ - try - { +TEST_F(ClientTest, SyncCallSafe) { + try { // 主线程调用,后台线程执行,future wait 自动处理 auto res = client->echo_get_sync("sync_world", 999); EXPECT_EQ(res.result(), http::status::ok); std::string body = res.body(); EXPECT_TRUE(body.find("sync_world") != std::string::npos); - } - catch (const std::exception& e) - { + } catch (const std::exception& e) { ADD_FAILURE() << "Sync request exception: " << e.what(); } } -TEST_F(ClientTest, SyncCall) -{ +TEST_F(ClientTest, SyncCall) { // 重要:同步调用会阻塞当前线程等待 future, // 所以 io_context 必须在另一个线程跑,否则死锁。 auto work = boost::asio::make_work_guard(ioc); - std::thread ioc_thread([&] - { - ioc.run(); - }); + std::thread ioc_thread([&] { ioc.run(); }); - try - { + try { // 使用同步生成的 API auto res = client->echo_get_sync("sync_world", 999); EXPECT_EQ(res.result(), http::status::ok); std::string body = res.body(); EXPECT_TRUE(body.find("sync_world") != std::string::npos); - } - catch (const std::exception& e) - { + } catch (const std::exception& e) { ADD_FAILURE() << "Sync request exception: " << e.what(); } @@ -500,33 +443,27 @@ TEST_F(ClientTest, SyncCall) if (ioc_thread.joinable()) ioc_thread.join(); } -TEST(EasyModeTest, SyncRequestWithoutManualContext) -{ +TEST(EasyModeTest, SyncRequestWithoutManualContext) { // 不需要手动创建 ioc, work_guard, thread - auto client = std::make_shared(); // 使用默认构造 + auto client = std::make_shared(); // 使用默认构造 - try - { + try { // 直接调用同步接口 auto res = client->echo_get_sync("easy_mode", 1); EXPECT_EQ(res.result(), http::status::ok); EXPECT_TRUE(res.body().find("easy_mode") != std::string::npos); - } - catch (const std::exception& e) - { + } catch (const std::exception& e) { ADD_FAILURE() << "Exception: " << e.what(); } } -TEST(EasyModeTest, AsyncRequest) -{ +TEST(EasyModeTest, AsyncRequest) { auto client = std::make_shared(); std::promise done; auto future = done.get_future(); - client->echo_get("async_easy", 2, [&](auto ec, auto res) - { + client->echo_get("async_easy", 2, [&](auto ec, auto res) { EXPECT_FALSE(ec); done.set_value(); }); @@ -536,49 +473,45 @@ TEST(EasyModeTest, AsyncRequest) future.wait(); } -TEST(HttpClientLocalTest, SyncRequestWithUnrunExternalIoContextTimesOut) -{ +TEST(HttpClientLocalTest, SyncRequestWithUnrunExternalIoContextTimesOut) { boost::asio::io_context ioc; HttpClient client(ioc); client.set_base_url("http://127.0.0.1:9"); client.set_timeout(std::chrono::seconds(0)); auto start = std::chrono::steady_clock::now(); - EXPECT_THROW( - client.request_sync(http::verb::get, "/", {}, "", {}), - boost::system::system_error); + EXPECT_THROW(client.request_sync(http::verb::get, "/", {}, "", {}), + boost::system::system_error); auto elapsed = std::chrono::steady_clock::now() - start; EXPECT_LT(elapsed, std::chrono::seconds(2)); } -TEST(HttpClientLocalTest, SyncRequestTimeoutClosesStalledConnection) -{ +TEST(HttpClientLocalTest, SyncRequestTimeoutClosesStalledConnection) { boost::asio::io_context server_ioc; - tcp::acceptor acceptor(server_ioc, {boost::asio::ip::make_address("127.0.0.1"), 0}); + tcp::acceptor acceptor(server_ioc, + {boost::asio::ip::make_address("127.0.0.1"), 0}); const auto endpoint = acceptor.local_endpoint(); std::atomic accepted{false}; std::atomic client_closed{false}; auto server_socket = std::make_shared(server_ioc); auto read_buffer = std::make_shared>(); auto read_until_close = std::make_shared>(); - *read_until_close = [server_socket, read_buffer, read_until_close, &client_closed]() - { - server_socket->async_read_some(boost::asio::buffer(*read_buffer), - [read_until_close, &client_closed](boost::system::error_code read_ec, - std::size_t) - { - if (read_ec) - { - client_closed = true; - return; - } - (*read_until_close)(); - }); + *read_until_close = [server_socket, read_buffer, read_until_close, + &client_closed]() { + server_socket->async_read_some( + boost::asio::buffer(*read_buffer), + [read_until_close, &client_closed](boost::system::error_code read_ec, + std::size_t) { + if (read_ec) { + client_closed = true; + return; + } + (*read_until_close)(); + }); }; - acceptor.async_accept(*server_socket, [&](boost::system::error_code ec) - { + acceptor.async_accept(*server_socket, [&](boost::system::error_code ec) { ASSERT_FALSE(ec) << ec.message(); accepted = true; (*read_until_close)(); @@ -594,10 +527,10 @@ TEST(HttpClientLocalTest, SyncRequestTimeoutClosesStalledConnection) client.set_base_url("http://127.0.0.1:" + std::to_string(endpoint.port())); client.set_timeout(std::chrono::seconds(1)); - EXPECT_THROW(client.request_sync(http::verb::get, "/", {}, "", {}), boost::system::system_error); + EXPECT_THROW(client.request_sync(http::verb::get, "/", {}, "", {}), + boost::system::system_error); - for (int i = 0; i < 100 && (!accepted || !client_closed); ++i) - { + for (int i = 0; i < 100 && (!accepted || !client_closed); ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); } @@ -611,13 +544,11 @@ TEST(HttpClientLocalTest, SyncRequestTimeoutClosesStalledConnection) EXPECT_TRUE(client_closed); } - // ========================================== // WebSocket 测试 // ========================================== -TEST(HttpClientStreamTest, StreamsRequestAndResponseWithBoundedBuffers) -{ +TEST(HttpClientStreamTest, StreamsRequestAndResponseWithBoundedBuffers) { LocalHttpEchoServer server; net::io_context ioc; auto client = std::make_shared(ioc); @@ -631,62 +562,64 @@ TEST(HttpClientStreamTest, StreamsRequestAndResponseWithBoundedBuffers) HttpClientStream::RequestHead head{http::verb::post, "/stream", 11}; head.content_length(payload->size()); - *read_next = [client, received, read_buffer, read_next, &completed] - { - client->async_read_some(net::buffer(*read_buffer), - [client, received, read_buffer, read_next, &completed](beast::error_code ec, std::size_t n, bool done) - { - ASSERT_FALSE(ec) << ec.message(); *received += n; - if (!done) return (*read_next)(); - completed = true; - }); - }; - *write_next = [client, payload, offset, write_next, read_next] - { - if (*offset == payload->size()) - { - return client->async_finish_request([client, read_next](beast::error_code ec) - { - ASSERT_FALSE(ec) << ec.message(); - client->async_read_response_head([read_next](beast::error_code head_ec, HttpClientStream::ResponseHead head) - { - ASSERT_FALSE(head_ec) << head_ec.message(); EXPECT_EQ(head.result(), http::status::ok); (*read_next)(); + *read_next = [client, received, read_buffer, read_next, &completed] { + client->async_read_some( + net::buffer(*read_buffer), + [client, received, read_buffer, read_next, &completed]( + beast::error_code ec, std::size_t n, bool done) { + ASSERT_FALSE(ec) << ec.message(); + *received += n; + if (!done) return (*read_next)(); + completed = true; }); - }); + }; + *write_next = [client, payload, offset, write_next, read_next] { + if (*offset == payload->size()) { + return client->async_finish_request( + [client, read_next](beast::error_code ec) { + ASSERT_FALSE(ec) << ec.message(); + client->async_read_response_head( + [read_next](beast::error_code head_ec, + HttpClientStream::ResponseHead head) { + ASSERT_FALSE(head_ec) << head_ec.message(); + EXPECT_EQ(head.result(), http::status::ok); + (*read_next)(); + }); + }); } - const auto size = std::min(32 * 1024, payload->size() - *offset); + const auto size = + std::min(32 * 1024, payload->size() - *offset); const auto chunk = net::buffer(payload->data() + *offset, size); - client->async_write_some(chunk, [offset, size, write_next](beast::error_code ec) - { - ASSERT_FALSE(ec) << ec.message(); *offset += size; (*write_next)(); - }); + client->async_write_some(chunk, + [offset, size, write_next](beast::error_code ec) { + ASSERT_FALSE(ec) << ec.message(); + *offset += size; + (*write_next)(); + }); }; client->async_start(server.base_url() + "/stream", std::move(head), - [write_next](beast::error_code ec) { ASSERT_FALSE(ec) << ec.message(); (*write_next)(); }); + [write_next](beast::error_code ec) { + ASSERT_FALSE(ec) << ec.message(); + (*write_next)(); + }); ioc.run(); EXPECT_TRUE(completed); EXPECT_EQ(*received, payload->size()); } -class WebsocketTest : public ::testing::Test -{ -protected: +class WebsocketTest : public ::testing::Test { + protected: boost::asio::io_context ioc; std::shared_ptr ws_client; - void SetUp() override - { - ws_client = std::make_shared(ioc); - } + void SetUp() override { ws_client = std::make_shared(ioc); } - void TearDown() override - { + void TearDown() override { if (ws_client) ws_client->close(); } }; -TEST_F(WebsocketTest, WssEchoAndWriteQueue) -{ +TEST_F(WebsocketTest, WssEchoAndWriteQueue) { LocalWebSocketEchoServer server; std::string url = server.url(); @@ -700,63 +633,55 @@ TEST_F(WebsocketTest, WssEchoAndWriteQueue) // 创建定时器,但先不 async_wait,后面逻辑控制 boost::asio::steady_timer timer(ioc, std::chrono::seconds(15)); - ws_client->set_on_message([&](const std::string& msg) - { + ws_client->set_on_message([&](const std::string& msg) { // 过滤欢迎消息 if (msg.find("Request served by") != std::string::npos) return; received_count++; // spdlog::debug("Msg: {}", msg); - if (received_count >= message_count) - { + if (received_count >= message_count) { ws_client->close(); } }); - ws_client->set_on_close([&]() - { + ws_client->set_on_close([&]() { closed_gracefully = true; // 关键:连接关闭后,取消定时器,ioc.run() 就会立即返回 timer.cancel(); }); - ws_client->set_on_error([&](boost::beast::error_code ec) - { + ws_client->set_on_error([&](boost::beast::error_code ec) { // 忽略操作取消(通常是 close() 导致的 pending read 取消) if (ec == boost::asio::error::operation_aborted) return; spdlog::error("WS Error: {}", ec.message()); has_error = true; - timer.cancel(); // 发生错误也停止测试 + timer.cancel(); // 发生错误也停止测试 }); - ws_client->connect(url, [&](boost::beast::error_code ec) - { - if (ec) - { + ws_client->connect(url, [&](boost::beast::error_code ec) { + if (ec) { ADD_FAILURE() << "WS Connect Failed: " << ec.message(); timer.cancel(); return; } - for (int i = 0; i < message_count; ++i) - { + for (int i = 0; i < message_count; ++i) { ws_client->send("Msg-" + std::to_string(i)); } }); // 启动超时计时 - timer.async_wait([&](boost::system::error_code ec) - { - if (ec == boost::asio::error::operation_aborted) - { + timer.async_wait([&](boost::system::error_code ec) { + if (ec == boost::asio::error::operation_aborted) { // 定时器被取消,说明测试正常结束或提前出错 return; } // 定时器真的触发了 -> 超时 ws_client->close(); - ADD_FAILURE() << "Test Timed Out! Received: " << received_count << "/" << message_count; + ADD_FAILURE() << "Test Timed Out! Received: " << received_count << "/" + << message_count; }); ioc.run(); @@ -766,29 +691,29 @@ TEST_F(WebsocketTest, WssEchoAndWriteQueue) EXPECT_TRUE(closed_gracefully) << "on_close should be triggered"; } -TEST_F(WebsocketTest, FrameHandlerPreservesBinaryTypeAndPayload) -{ +TEST_F(WebsocketTest, FrameHandlerPreservesBinaryTypeAndPayload) { LocalWebSocketEchoServer server; bool connected = false; bool received = false; boost::asio::steady_timer timer(ioc, std::chrono::seconds(5)); - ws_client->set_on_frame([&](const khttpd::framework::WebsocketFrame& frame) - { + ws_client->set_on_frame([&](const khttpd::framework::WebsocketFrame& frame) { if (frame.type != khttpd::framework::WebsocketFrameType::binary) return; received = frame.payload == std::string("\x00\x01\xff", 3); ws_client->close(); timer.cancel(); }); - ws_client->connect(server.url(), [&](boost::beast::error_code ec) - { + ws_client->connect(server.url(), [&](boost::beast::error_code ec) { ASSERT_FALSE(ec) << ec.message(); connected = true; - ws_client->send({khttpd::framework::WebsocketFrameType::binary, std::string("\x00\x01\xff", 3)}); + ws_client->send({khttpd::framework::WebsocketFrameType::binary, + std::string("\x00\x01\xff", 3)}); }); - timer.async_wait([&](boost::system::error_code ec) - { - if (!ec) { ws_client->close(); ADD_FAILURE() << "Frame echo timed out"; } + timer.async_wait([&](boost::system::error_code ec) { + if (!ec) { + ws_client->close(); + ADD_FAILURE() << "Frame echo timed out"; + } }); ioc.run(); @@ -796,14 +721,11 @@ TEST_F(WebsocketTest, FrameHandlerPreservesBinaryTypeAndPayload) EXPECT_TRUE(received); } -TEST_F(WebsocketTest, ConnectFailure) -{ +TEST_F(WebsocketTest, ConnectFailure) { // 测试连接不可达端口 bool failed = false; - ws_client->connect("ws://localhost:59999", [&](boost::beast::error_code ec) - { - if (ec) - { + ws_client->connect("ws://localhost:59999", [&](boost::beast::error_code ec) { + if (ec) { failed = true; } }); @@ -812,10 +734,10 @@ TEST_F(WebsocketTest, ConnectFailure) EXPECT_TRUE(failed); } -TEST_F(WebsocketTest, CloseBeforeHandshakeSuppressesConnectCallback) -{ +TEST_F(WebsocketTest, CloseBeforeHandshakeSuppressesConnectCallback) { boost::asio::io_context server_ioc; - tcp::acceptor acceptor(server_ioc, {boost::asio::ip::make_address("127.0.0.1"), 0}); + tcp::acceptor acceptor(server_ioc, + {boost::asio::ip::make_address("127.0.0.1"), 0}); const auto endpoint = acceptor.local_endpoint(); auto server_socket = std::make_shared(server_ioc); @@ -823,10 +745,8 @@ TEST_F(WebsocketTest, CloseBeforeHandshakeSuppressesConnectCallback) std::thread server_thread([&] { server_ioc.run(); }); std::atomic connect_calls{0}; - ws_client->connect("ws://127.0.0.1:" + std::to_string(endpoint.port()), [&](boost::beast::error_code) - { - ++connect_calls; - }); + ws_client->connect("ws://127.0.0.1:" + std::to_string(endpoint.port()), + [&](boost::beast::error_code) { ++connect_calls; }); ws_client->close(); boost::asio::steady_timer timer(ioc, std::chrono::milliseconds(300)); @@ -839,10 +759,10 @@ TEST_F(WebsocketTest, CloseBeforeHandshakeSuppressesConnectCallback) EXPECT_EQ(connect_calls.load(), 0); } -TEST(WebsocketClientLifecycleTest, DestructorSuppressesPendingConnectCallback) -{ +TEST(WebsocketClientLifecycleTest, DestructorSuppressesPendingConnectCallback) { boost::asio::io_context server_ioc; - tcp::acceptor acceptor(server_ioc, {boost::asio::ip::make_address("127.0.0.1"), 0}); + tcp::acceptor acceptor(server_ioc, + {boost::asio::ip::make_address("127.0.0.1"), 0}); const auto endpoint = acceptor.local_endpoint(); auto server_socket = std::make_shared(server_ioc); @@ -853,10 +773,8 @@ TEST(WebsocketClientLifecycleTest, DestructorSuppressesPendingConnectCallback) std::atomic connect_calls{0}; { auto client = std::make_shared(client_ioc); - client->connect("ws://127.0.0.1:" + std::to_string(endpoint.port()), [&](boost::beast::error_code) - { - ++connect_calls; - }); + client->connect("ws://127.0.0.1:" + std::to_string(endpoint.port()), + [&](boost::beast::error_code) { ++connect_calls; }); } boost::asio::steady_timer timer(client_ioc, std::chrono::milliseconds(300)); @@ -869,24 +787,25 @@ TEST(WebsocketClientLifecycleTest, DestructorSuppressesPendingConnectCallback) EXPECT_EQ(connect_calls.load(), 0); } -TEST_F(ClientTest, ThreadPoolVerify) -{ - spdlog::debug("Pool Size: {}", khttpd::framework::IoContextPool::instance().get_thread_count()); +TEST_F(ClientTest, ThreadPoolVerify) { + spdlog::debug( + "Pool Size: {}", + khttpd::framework::IoContextPool::instance().get_thread_count()); std::promise p1, p2; auto f1 = p1.get_future(); auto f2 = p2.get_future(); // 发起两个请求 - client->echo_get("A", 1, [&](auto, auto) - { - spdlog::debug("Req 1 processed on thread hash: {}", std::hash{}(std::this_thread::get_id())); + client->echo_get("A", 1, [&](auto, auto) { + spdlog::debug("Req 1 processed on thread hash: {}", + std::hash{}(std::this_thread::get_id())); p1.set_value(); }); - client->echo_get("B", 2, [&](auto, auto) - { - spdlog::debug("Req 2 processed on thread hash: {}", std::hash{}(std::this_thread::get_id())); + client->echo_get("B", 2, [&](auto, auto) { + spdlog::debug("Req 2 processed on thread hash: {}", + std::hash{}(std::this_thread::get_id())); p2.set_value(); }); @@ -898,26 +817,22 @@ TEST_F(ClientTest, ThreadPoolVerify) // 3. Oat++-style API Client Tests // ========================================== -// Define API client using KHTTPD_API_CLIENT (single host, endpoints use API_CALL) +// Define API client using KHTTPD_API_CLIENT (single host, endpoints use +// API_CALL) KHTTPD_API_CLIENT(EchoClient, "http://127.0.0.1:1") - API_CALL(http::verb::get, "/get", get_echo, - QUERY(std::string, msg, "msg")) - API_CALL(http::verb::post, "/post", post_echo, - BODY(boost::json::object, body)) +API_CALL(http::verb::get, "/get", get_echo, QUERY(std::string, msg, "msg")) +API_CALL(http::verb::post, "/post", post_echo, BODY(boost::json::object, body)) KHTTPD_API_CLIENT_END() // Define API client using KHTTPD_API_CLIENT_POOL (multi-host with weights) KHTTPD_API_CLIENT_POOL(MultiHostClient, - KHTTPD_HOST("http://127.0.0.1:1", 3) - KHTTPD_HOST("http://127.0.0.1:1", 1) -) - API_CALL(http::verb::get, "/get", get_echo, - QUERY(std::string, msg, "msg")) + KHTTPD_HOST("http://127.0.0.1:1", 3) + KHTTPD_HOST("http://127.0.0.1:1", 1)) +API_CALL(http::verb::get, "/get", get_echo, QUERY(std::string, msg, "msg")) KHTTPD_API_CLIENT_END() // Test verb_from_string -TEST(ApiMacrosTest, VerbFromString) -{ +TEST(ApiMacrosTest, VerbFromString) { ASSERT_EQ(verb_from_string("GET"), http::verb::get); ASSERT_EQ(verb_from_string("get"), http::verb::get); ASSERT_EQ(verb_from_string("POST"), http::verb::post); @@ -927,12 +842,11 @@ TEST(ApiMacrosTest, VerbFromString) ASSERT_EQ(verb_from_string("PATCH"), http::verb::patch); ASSERT_EQ(verb_from_string("HEAD"), http::verb::head); ASSERT_EQ(verb_from_string("OPTIONS"), http::verb::options); - ASSERT_EQ(verb_from_string("UNKNOWN"), http::verb::get); // fallback + ASSERT_EQ(verb_from_string("UNKNOWN"), http::verb::get); // fallback } // Test single-host KHTTPD_API_CLIENT -TEST_F(ClientTest, OatppStyleSingleHost) -{ +TEST_F(ClientTest, OatppStyleSingleHost) { auto echo = std::make_shared(); echo->set_base_url(local_http_echo_server().base_url()); echo->set_timeout(std::chrono::seconds(10)); @@ -954,8 +868,7 @@ TEST_F(ClientTest, OatppStyleSingleHost) } // Test sync version -TEST_F(ClientTest, OatppStyleSync) -{ +TEST_F(ClientTest, OatppStyleSync) { auto echo = std::make_shared(); echo->set_base_url(local_http_echo_server().base_url()); echo->set_timeout(std::chrono::seconds(10)); @@ -970,11 +883,10 @@ TEST_F(ClientTest, OatppStyleSync) } // Test multi-host pool -TEST(ApiMacrosTest, HostPoolWeighted) -{ +TEST(ApiMacrosTest, HostPoolWeighted) { std::vector hosts = { - {"http://host-a.com", 3}, - {"http://host-b.com", 1}, + {"http://host-a.com", 3}, + {"http://host-b.com", 1}, }; HostPool pool(hosts); @@ -994,31 +906,27 @@ TEST(ApiMacrosTest, HostPoolWeighted) } } -TEST(ApiMacrosTest, HostPoolPickIsThreadSafe) -{ +TEST(ApiMacrosTest, HostPoolPickIsThreadSafe) { std::vector hosts = { - {"http://host-a.com", 3}, - {"http://host-b.com", 1}, + {"http://host-a.com", 3}, + {"http://host-b.com", 1}, }; HostPool pool(hosts); std::atomic picks{0}; std::vector threads; - for (int t = 0; t < 8; ++t) - { - threads.emplace_back([&]() - { - for (int i = 0; i < 1000; ++i) - { + for (int t = 0; t < 8; ++t) { + threads.emplace_back([&]() { + for (int i = 0; i < 1000; ++i) { const auto& picked = pool.pick(); - ASSERT_TRUE(picked == "http://host-a.com" || picked == "http://host-b.com"); + ASSERT_TRUE(picked == "http://host-a.com" || + picked == "http://host-b.com"); picks++; } }); } - for (auto& thread : threads) - { + for (auto& thread : threads) { thread.join(); } @@ -1026,8 +934,7 @@ TEST(ApiMacrosTest, HostPoolPickIsThreadSafe) } // Test multi-host API client -TEST_F(ClientTest, MultiHostClientPool) -{ +TEST_F(ClientTest, MultiHostClientPool) { auto mc = std::make_shared(); mc->set_base_url(local_http_echo_server().base_url()); mc->set_timeout(std::chrono::seconds(10)); diff --git a/framework/tests/concurrency_benchmark.cpp b/framework/tests/concurrency_benchmark.cpp index 64681ee..be00217 100644 --- a/framework/tests/concurrency_benchmark.cpp +++ b/framework/tests/concurrency_benchmark.cpp @@ -1,14 +1,11 @@ -#include "framework/context/http_context.hpp" -#include "framework/server.hpp" #include "framework/tests/concurrency_benchmark.hpp" +#include #include #include #include #include #include - -#include #include #include #include @@ -19,6 +16,9 @@ #include #include +#include "framework/context/http_context.hpp" +#include "framework/server.hpp" + namespace beast = boost::beast; namespace http = beast::http; namespace net = boost::asio; @@ -26,148 +26,147 @@ using tcp = net::ip::tcp; namespace fs = boost::filesystem; namespace khttpd_fw = khttpd::framework; -namespace -{ - struct Options - { - int concurrency = 32; - int requests = 2000; - int server_threads = 4; - int warmup = 100; - }; - - struct TempWebRoot - { - fs::path path; - - TempWebRoot() - : path(fs::temp_directory_path() / fs::unique_path("khttpd-concurrency-benchmark-%%%%-%%%%-%%%%")) - { - fs::create_directories(path); - std::ofstream((path / "index.html").string()) << "ok"; - } +namespace { +struct Options { + int concurrency = 32; + int requests = 2000; + int server_threads = 4; + int warmup = 100; +}; + +struct TempWebRoot { + fs::path path; + + TempWebRoot() + : path(fs::temp_directory_path() / + fs::unique_path("khttpd-concurrency-benchmark-%%%%-%%%%-%%%%")) { + fs::create_directories(path); + std::ofstream((path / "index.html").string()) << "ok"; + } - ~TempWebRoot() - { - boost::system::error_code ignored; - fs::remove_all(path, ignored); - } - }; - - int parse_positive_value(const char* name, const char* value) - { - try - { - const int parsed = std::stoi(value); - if (parsed <= 0) throw std::invalid_argument("not positive"); - return parsed; - } - catch (const std::exception&) - { - throw std::runtime_error(std::string(name) + " must be a positive integer"); - } + ~TempWebRoot() { + boost::system::error_code ignored; + fs::remove_all(path, ignored); + } +}; + +int parse_positive_value(const char* name, const char* value) { + try { + const int parsed = std::stoi(value); + if (parsed <= 0) throw std::invalid_argument("not positive"); + return parsed; + } catch (const std::exception&) { + throw std::runtime_error(std::string(name) + " must be a positive integer"); } +} - Options parse_options(int argc, char** argv) - { - Options options; - for (int i = 1; i < argc; ++i) - { - const std::string argument = argv[i]; - if (argument == "--help") - { - std::cout << "Usage: concurrency_benchmark [--concurrency N] [--requests N] [--server-threads N] [--warmup N]\n"; - std::exit(0); - } - if (i + 1 >= argc) throw std::runtime_error("missing value for " + argument); - const int value = parse_positive_value(argument.c_str(), argv[++i]); - if (argument == "--concurrency") options.concurrency = value; - else if (argument == "--requests") options.requests = value; - else if (argument == "--server-threads") options.server_threads = value; - else if (argument == "--warmup") options.warmup = value; - else throw std::runtime_error("unknown argument: " + argument); +Options parse_options(int argc, char** argv) { + Options options; + for (int i = 1; i < argc; ++i) { + const std::string argument = argv[i]; + if (argument == "--help") { + std::cout << "Usage: concurrency_benchmark [--concurrency N] [--requests " + "N] [--server-threads N] [--warmup N]\n"; + std::exit(0); } - return options; + if (i + 1 >= argc) + throw std::runtime_error("missing value for " + argument); + const int value = parse_positive_value(argument.c_str(), argv[++i]); + if (argument == "--concurrency") + options.concurrency = value; + else if (argument == "--requests") + options.requests = value; + else if (argument == "--server-threads") + options.server_threads = value; + else if (argument == "--warmup") + options.warmup = value; + else + throw std::runtime_error("unknown argument: " + argument); } + return options; +} - bool request_once(unsigned short port, int request_id, std::uint64_t& latency_us) - { - const auto started_at = std::chrono::steady_clock::now(); - try - { - net::io_context ioc; - beast::tcp_stream stream(ioc); - stream.expires_after(std::chrono::seconds(10)); - stream.connect(tcp::endpoint(net::ip::address_v4::loopback(), port)); - - http::request request{http::verb::get, "/benchmark?id=" + std::to_string(request_id), 11}; - request.set(http::field::host, "127.0.0.1"); - request.set(http::field::user_agent, "khttpd-concurrency-benchmark"); - request.keep_alive(false); - http::write(stream, request); - - beast::flat_buffer buffer; - http::response response; - http::read(stream, buffer, response); - beast::error_code ignored; - stream.socket().shutdown(tcp::socket::shutdown_both, ignored); - - latency_us = static_cast(std::chrono::duration_cast( - std::chrono::steady_clock::now() - started_at).count()); - return response.result() == http::status::ok && response.body() == "pong"; - } - catch (...) - { - latency_us = static_cast(std::chrono::duration_cast( - std::chrono::steady_clock::now() - started_at).count()); - return false; - } +bool request_once(unsigned short port, int request_id, + std::uint64_t& latency_us) { + const auto started_at = std::chrono::steady_clock::now(); + try { + net::io_context ioc; + beast::tcp_stream stream(ioc); + stream.expires_after(std::chrono::seconds(10)); + stream.connect(tcp::endpoint(net::ip::address_v4::loopback(), port)); + + http::request request{ + http::verb::get, "/benchmark?id=" + std::to_string(request_id), 11}; + request.set(http::field::host, "127.0.0.1"); + request.set(http::field::user_agent, "khttpd-concurrency-benchmark"); + request.keep_alive(false); + http::write(stream, request); + + beast::flat_buffer buffer; + http::response response; + http::read(stream, buffer, response); + beast::error_code ignored; + stream.socket().shutdown(tcp::socket::shutdown_both, ignored); + + latency_us = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now() - started_at) + .count()); + return response.result() == http::status::ok && response.body() == "pong"; + } catch (...) { + latency_us = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now() - started_at) + .count()); + return false; } +} - void run_requests(unsigned short port, int requests, int concurrency, std::vector& latencies, - std::atomic& successes, std::atomic& failures) - { - std::atomic next_request{0}; - std::vector> worker_latencies(static_cast(concurrency)); - std::vector workers; - workers.reserve(static_cast(concurrency)); - for (int worker = 0; worker < concurrency; ++worker) - { - workers.emplace_back([&, worker]() - { - auto& worker_samples = worker_latencies[static_cast(worker)]; - while (true) - { - const int request_id = next_request.fetch_add(1, std::memory_order_relaxed); - if (request_id >= requests) return; - std::uint64_t latency_us = 0; - if (request_once(port, request_id, latency_us)) successes.fetch_add(1, std::memory_order_relaxed); - else failures.fetch_add(1, std::memory_order_relaxed); - worker_samples.push_back(latency_us); - } - }); - } - for (auto& worker : workers) worker.join(); - for (auto& samples : worker_latencies) - { - latencies.insert(latencies.end(), samples.begin(), samples.end()); - } +void run_requests(unsigned short port, int requests, int concurrency, + std::vector& latencies, + std::atomic& successes, + std::atomic& failures) { + std::atomic next_request{0}; + std::vector> worker_latencies( + static_cast(concurrency)); + std::vector workers; + workers.reserve(static_cast(concurrency)); + for (int worker = 0; worker < concurrency; ++worker) { + workers.emplace_back([&, worker]() { + auto& worker_samples = worker_latencies[static_cast(worker)]; + while (true) { + const int request_id = + next_request.fetch_add(1, std::memory_order_relaxed); + if (request_id >= requests) return; + std::uint64_t latency_us = 0; + if (request_once(port, request_id, latency_us)) + successes.fetch_add(1, std::memory_order_relaxed); + else + failures.fetch_add(1, std::memory_order_relaxed); + worker_samples.push_back(latency_us); + } + }); + } + for (auto& worker : workers) worker.join(); + for (auto& samples : worker_latencies) { + latencies.insert(latencies.end(), samples.begin(), samples.end()); } } +} // namespace -int main(int argc, char** argv) -{ - try - { +int main(int argc, char** argv) { + try { const Options options = parse_options(argc, argv); TempWebRoot web_root; - auto server = std::make_shared(tcp::endpoint(tcp::v4(), 0), web_root.path.string(), options.server_threads); - server->get_http_router().get("/benchmark", [](khttpd_fw::HttpContext& context) - { - context.set_status(http::status::ok); - context.set_content_type("text/plain"); - context.set_body("pong"); - }); + auto server = std::make_shared( + tcp::endpoint(tcp::v4(), 0), web_root.path.string(), + options.server_threads); + server->get_http_router().get("/benchmark", + [](khttpd_fw::HttpContext& context) { + context.set_status(http::status::ok); + context.set_content_type("text/plain"); + context.set_body("pong"); + }); const auto port = server->local_endpoint().port(); std::thread server_thread([server]() { server->run(); }); std::this_thread::sleep_for(std::chrono::milliseconds(100)); @@ -175,26 +174,30 @@ int main(int argc, char** argv) std::vector warmup_latencies; std::atomic warmup_successes{0}; std::atomic warmup_failures{0}; - run_requests(port, options.warmup, options.concurrency, warmup_latencies, warmup_successes, warmup_failures); + run_requests(port, options.warmup, options.concurrency, warmup_latencies, + warmup_successes, warmup_failures); std::vector latencies; latencies.reserve(static_cast(options.requests)); std::atomic successes{0}; std::atomic failures{0}; const auto started_at = std::chrono::steady_clock::now(); - run_requests(port, options.requests, options.concurrency, latencies, successes, failures); - const auto elapsed_us = static_cast(std::chrono::duration_cast( - std::chrono::steady_clock::now() - started_at).count()); + run_requests(port, options.requests, options.concurrency, latencies, + successes, failures); + const auto elapsed_us = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now() - started_at) + .count()); server->stop(); server_thread.join(); - const khttpd_fw::benchmark::Stats stats(std::move(latencies), elapsed_us, successes.load(), failures.load()); - std::cout << khttpd_fw::benchmark::format_report(stats, options.server_threads, options.concurrency, options.requests); + const khttpd_fw::benchmark::Stats stats(std::move(latencies), elapsed_us, + successes.load(), failures.load()); + std::cout << khttpd_fw::benchmark::format_report( + stats, options.server_threads, options.concurrency, options.requests); return stats.failures() == 0 ? 0 : 1; - } - catch (const std::exception& error) - { + } catch (const std::exception& error) { std::cerr << "concurrency benchmark failed: " << error.what() << "\n"; return 2; } diff --git a/framework/tests/concurrency_benchmark.hpp b/framework/tests/concurrency_benchmark.hpp index 63543c6..3a9be64 100644 --- a/framework/tests/concurrency_benchmark.hpp +++ b/framework/tests/concurrency_benchmark.hpp @@ -3,81 +3,86 @@ #include #include #include -#include #include +#include #include #include #include #include -namespace khttpd::framework::benchmark -{ - class Stats - { - public: - Stats(std::vector latencies_us, - std::uint64_t elapsed_us, - std::size_t successes, - std::size_t failures) - : latencies_us_(std::move(latencies_us)), elapsed_us_(elapsed_us), successes_(successes), failures_(failures) - { - std::sort(latencies_us_.begin(), latencies_us_.end()); - } +namespace khttpd::framework::benchmark { +class Stats { + public: + Stats(std::vector latencies_us, std::uint64_t elapsed_us, + std::size_t successes, std::size_t failures) + : latencies_us_(std::move(latencies_us)), + elapsed_us_(elapsed_us), + successes_(successes), + failures_(failures) { + std::sort(latencies_us_.begin(), latencies_us_.end()); + } - double percentile(double fraction) const - { - if (latencies_us_.empty()) return 0.0; - const auto bounded_fraction = std::max(0.0, std::min(1.0, fraction)); - const auto index = static_cast(bounded_fraction * (latencies_us_.size() - 1) + 0.5); - return static_cast(latencies_us_[index]); - } + double percentile(double fraction) const { + if (latencies_us_.empty()) return 0.0; + const auto bounded_fraction = std::max(0.0, std::min(1.0, fraction)); + const auto index = static_cast( + bounded_fraction * (latencies_us_.size() - 1) + 0.5); + return static_cast(latencies_us_[index]); + } - double requests_per_second() const - { - if (elapsed_us_ == 0) return 0.0; - return static_cast(successes_) * 1000000.0 / static_cast(elapsed_us_); - } + double requests_per_second() const { + if (elapsed_us_ == 0) return 0.0; + return static_cast(successes_) * 1000000.0 / + static_cast(elapsed_us_); + } - double average_latency_us() const - { - if (latencies_us_.empty()) return 0.0; - const auto total = std::accumulate(latencies_us_.begin(), latencies_us_.end(), std::uint64_t{0}); - return static_cast(total) / static_cast(latencies_us_.size()); - } + double average_latency_us() const { + if (latencies_us_.empty()) return 0.0; + const auto total = std::accumulate(latencies_us_.begin(), + latencies_us_.end(), std::uint64_t{0}); + return static_cast(total) / + static_cast(latencies_us_.size()); + } - std::size_t successes() const { return successes_; } - std::size_t failures() const { return failures_; } + std::size_t successes() const { return successes_; } + std::size_t failures() const { return failures_; } - private: - std::vector latencies_us_; - std::uint64_t elapsed_us_; - std::size_t successes_; - std::size_t failures_; - }; + private: + std::vector latencies_us_; + std::uint64_t elapsed_us_; + std::size_t successes_; + std::size_t failures_; +}; - inline std::string format_report(const Stats& stats, int server_threads, int concurrency, int requests) - { - const auto total = stats.successes() + stats.failures(); - const auto success_rate = total == 0 ? 0.0 : static_cast(stats.successes()) * 100.0 / static_cast(total); - std::ostringstream report; - report << "========================================\n" - << "khttpd HTTP 并发基准测试\n" - << "========================================\n" - << "测试配置\n" - << " 服务端线程数 : " << server_threads << "\n" - << " 并发客户端数 : " << concurrency << "\n" - << " 请求总数 : " << requests << "\n" - << "----------------------------------------\n" - << "测试结果\n" - << " 成功请求数 : " << stats.successes() << "\n" - << " 失败请求数 : " << stats.failures() << "\n" - << " 成功率 : " << std::fixed << std::setprecision(2) << success_rate << "%\n" - << " 吞吐量 : " << std::setprecision(2) << stats.requests_per_second() << " req/s\n" - << " 平均延迟 : " << std::setprecision(2) << stats.average_latency_us() << " us\n" - << " p50 延迟 : " << stats.percentile(0.50) << " us\n" - << " p95 延迟 : " << stats.percentile(0.95) << " us\n" - << " p99 延迟 : " << stats.percentile(0.99) << " us\n" - << "========================================\n"; - return report.str(); - } +inline std::string format_report(const Stats& stats, int server_threads, + int concurrency, int requests) { + const auto total = stats.successes() + stats.failures(); + const auto success_rate = total == 0 + ? 0.0 + : static_cast(stats.successes()) * + 100.0 / static_cast(total); + std::ostringstream report; + report << "========================================\n" + << "khttpd HTTP 并发基准测试\n" + << "========================================\n" + << "测试配置\n" + << " 服务端线程数 : " << server_threads << "\n" + << " 并发客户端数 : " << concurrency << "\n" + << " 请求总数 : " << requests << "\n" + << "----------------------------------------\n" + << "测试结果\n" + << " 成功请求数 : " << stats.successes() << "\n" + << " 失败请求数 : " << stats.failures() << "\n" + << " 成功率 : " << std::fixed << std::setprecision(2) + << success_rate << "%\n" + << " 吞吐量 : " << std::setprecision(2) + << stats.requests_per_second() << " req/s\n" + << " 平均延迟 : " << std::setprecision(2) + << stats.average_latency_us() << " us\n" + << " p50 延迟 : " << stats.percentile(0.50) << " us\n" + << " p95 延迟 : " << stats.percentile(0.95) << " us\n" + << " p99 延迟 : " << stats.percentile(0.99) << " us\n" + << "========================================\n"; + return report.str(); } +} // namespace khttpd::framework::benchmark diff --git a/framework/tests/concurrency_benchmark_test.cpp b/framework/tests/concurrency_benchmark_test.cpp index a2ef87b..a1fb69f 100644 --- a/framework/tests/concurrency_benchmark_test.cpp +++ b/framework/tests/concurrency_benchmark_test.cpp @@ -2,9 +2,9 @@ #include -TEST(ConcurrencyBenchmarkStatsTest, CalculatesPercentilesAndThroughput) -{ - const khttpd::framework::benchmark::Stats stats({1, 2, 3, 4, 5}, 1000000, 4, 1); +TEST(ConcurrencyBenchmarkStatsTest, CalculatesPercentilesAndThroughput) { + const khttpd::framework::benchmark::Stats stats({1, 2, 3, 4, 5}, 1000000, 4, + 1); EXPECT_DOUBLE_EQ(stats.percentile(0.50), 3.0); EXPECT_DOUBLE_EQ(stats.percentile(0.95), 5.0); @@ -12,10 +12,11 @@ TEST(ConcurrencyBenchmarkStatsTest, CalculatesPercentilesAndThroughput) EXPECT_DOUBLE_EQ(stats.requests_per_second(), 4.0); } -TEST(ConcurrencyBenchmarkStatsTest, FormatsReadableChineseReport) -{ - const khttpd::framework::benchmark::Stats stats({100, 200, 300}, 1000000, 3, 0); - const auto report = khttpd::framework::benchmark::format_report(stats, 4, 8, 80); +TEST(ConcurrencyBenchmarkStatsTest, FormatsReadableChineseReport) { + const khttpd::framework::benchmark::Stats stats({100, 200, 300}, 1000000, 3, + 0); + const auto report = + khttpd::framework::benchmark::format_report(stats, 4, 8, 80); EXPECT_NE(report.find("khttpd HTTP 并发基准测试"), std::string::npos); EXPECT_NE(report.find("并发客户端数"), std::string::npos); diff --git a/framework/tests/context_test.cpp b/framework/tests/context_test.cpp index 107aedd..ec91d0c 100644 --- a/framework/tests/context_test.cpp +++ b/framework/tests/context_test.cpp @@ -1,6 +1,8 @@ -#include "framework/context/http_context.hpp" #include -#include // For empty body requests + +#include // For empty body requests + +#include "framework/context/http_context.hpp" namespace beast = boost::beast; namespace http = beast::http; @@ -9,15 +11,11 @@ namespace khttpd_fw = khttpd::framework; // Helper function to create a request for testing template -http::request make_request( - http::verb method, - const std::string& target, - int version = 11, - const std::string& body_str = "") -{ +http::request make_request(http::verb method, const std::string& target, + int version = 11, + const std::string& body_str = "") { http::request req(method, target, version); - if (!body_str.empty()) - { + if (!body_str.empty()) { req.body() = body_str; req.prepare_payload(); } @@ -25,16 +23,14 @@ http::request make_request( } // Helper to create HttpContext with dummy response -khttpd_fw::HttpContext create_context( - http::request& req, - http::response& res) -{ +khttpd_fw::HttpContext create_context(http::request& req, + http::response& res) { return khttpd_fw::HttpContext(req, res); } -TEST(HttpContextTest, PathAndMethod) -{ - http::request req = make_request(http::verb::get, "/users/123?q=test"); +TEST(HttpContextTest, PathAndMethod) { + http::request req = + make_request(http::verb::get, "/users/123?q=test"); http::response res; khttpd_fw::HttpContext ctx = create_context(req, res); @@ -42,9 +38,9 @@ TEST(HttpContextTest, PathAndMethod) ASSERT_EQ(ctx.method(), http::verb::get); } -TEST(HttpContextTest, QueryParameters) -{ - http::request req = make_request(http::verb::get, "/search?query=boost+beast&page=2"); +TEST(HttpContextTest, QueryParameters) { + http::request req = + make_request(http::verb::get, "/search?query=boost+beast&page=2"); http::response res; khttpd_fw::HttpContext ctx = create_context(req, res); @@ -57,8 +53,7 @@ TEST(HttpContextTest, QueryParameters) ASSERT_FALSE(ctx.get_query_param("non_existent").has_value()); } -TEST(HttpContextTest, Headers) -{ +TEST(HttpContextTest, Headers) { http::request req = make_request(http::verb::get, "/"); req.set(http::field::user_agent, "Test-Agent/1.0"); req.set("X-Custom-Header", "CustomValue"); @@ -74,10 +69,10 @@ TEST(HttpContextTest, Headers) ASSERT_FALSE(ctx.get_header("Non-Existent-Header").has_value()); } -TEST(HttpContextTest, JsonBody) -{ +TEST(HttpContextTest, JsonBody) { std::string json_str = R"({"name": "Alice", "age": 30})"; - http::request req = make_request(http::verb::post, "/api/json", 11, json_str); + http::request req = + make_request(http::verb::post, "/api/json", 11, json_str); req.set(http::field::content_type, "application/json"); http::response res; khttpd_fw::HttpContext ctx = create_context(req, res); @@ -89,10 +84,11 @@ TEST(HttpContextTest, JsonBody) ASSERT_EQ(ctx.get_json().value().at("age").as_int64(), 30); } -TEST(HttpContextTest, InvalidJsonBody) -{ - std::string invalid_json_str = R"({"name": "Alice", "age": })"; // Malformed JSON - http::request req = make_request(http::verb::post, "/api/json", 11, invalid_json_str); +TEST(HttpContextTest, InvalidJsonBody) { + std::string invalid_json_str = + R"({"name": "Alice", "age": })"; // Malformed JSON + http::request req = + make_request(http::verb::post, "/api/json", 11, invalid_json_str); req.set(http::field::content_type, "application/json"); http::response res; khttpd_fw::HttpContext ctx = create_context(req, res); @@ -100,10 +96,10 @@ TEST(HttpContextTest, InvalidJsonBody) ASSERT_FALSE(ctx.get_json().has_value()); } -TEST(HttpContextTest, FormUrlEncodedBody) -{ +TEST(HttpContextTest, FormUrlEncodedBody) { std::string form_str = "param1=value1¶m2=value%202"; - http::request req = make_request(http::verb::post, "/api/form", 11, form_str); + http::request req = + make_request(http::verb::post, "/api/form", 11, form_str); req.set(http::field::content_type, "application/x-www-form-urlencoded"); http::response res; khttpd_fw::HttpContext ctx = create_context(req, res); @@ -115,26 +111,33 @@ TEST(HttpContextTest, FormUrlEncodedBody) ASSERT_FALSE(ctx.get_form_param("non_existent").has_value()); } -TEST(HttpContextTest, MultipartFormData) -{ +TEST(HttpContextTest, MultipartFormData) { std::string boundary = "----------WebKitFormBoundary12345"; std::string multipart_body = - "--" + boundary + "\r\n" - "Content-Disposition: form-data; name=\"description\"\r\n\r\n" - "This is a test description.\r\n" - "--" + boundary + "\r\n" - "Content-Disposition: form-data; name=\"file\"; filename=\"my_test.txt\"\r\n" - "Content-Type: text/plain\r\n\r\n" - "Hello, world!\r\n" - "--" + boundary + "--\r\n"; - - http::request req = make_request(http::verb::post, "/api/upload", 11, multipart_body); - req.set(http::field::content_type, "multipart/form-data; boundary=" + boundary); + "--" + boundary + + "\r\n" + "Content-Disposition: form-data; name=\"description\"\r\n\r\n" + "This is a test description.\r\n" + "--" + + boundary + + "\r\n" + "Content-Disposition: form-data; name=\"file\"; " + "filename=\"my_test.txt\"\r\n" + "Content-Type: text/plain\r\n\r\n" + "Hello, world!\r\n" + "--" + + boundary + "--\r\n"; + + http::request req = + make_request(http::verb::post, "/api/upload", 11, multipart_body); + req.set(http::field::content_type, + "multipart/form-data; boundary=" + boundary); http::response res; khttpd_fw::HttpContext ctx = create_context(req, res); ASSERT_TRUE(ctx.get_multipart_field("description").has_value()); - ASSERT_EQ(ctx.get_multipart_field("description").value(), "This is a test description."); + ASSERT_EQ(ctx.get_multipart_field("description").value(), + "This is a test description."); const auto* files = ctx.get_uploaded_files("file"); ASSERT_NE(files, nullptr); @@ -148,8 +151,7 @@ TEST(HttpContextTest, MultipartFormData) } // Test response setters -TEST(HttpContextTest, ResponseSetters) -{ +TEST(HttpContextTest, ResponseSetters) { http::request req = make_request(http::verb::get, "/"); http::response res; khttpd_fw::HttpContext ctx = create_context(req, res); @@ -168,11 +170,12 @@ TEST(HttpContextTest, ResponseSetters) ASSERT_EQ(actual_res["X-Framework-Version"], "1.0"); } -TEST(HttpContextTest, Cookies) -{ +TEST(HttpContextTest, Cookies) { http::request req = make_request(http::verb::get, "/"); req.set(http::field::cookie, "session_id=12345; user=alice"); - req.insert(http::field::cookie, "theme=dark; user=bob"); // Multiple headers, multiple values for 'user' + req.insert( + http::field::cookie, + "theme=dark; user=bob"); // Multiple headers, multiple values for 'user' http::response res; khttpd_fw::HttpContext ctx = create_context(req, res); @@ -193,8 +196,7 @@ TEST(HttpContextTest, Cookies) ASSERT_TRUE(ctx.get_cookies("non_existent").empty()); } -TEST(HttpContextTest, SetCookie) -{ +TEST(HttpContextTest, SetCookie) { http::request req = make_request(http::verb::get, "/"); http::response res; khttpd_fw::HttpContext ctx = create_context(req, res); @@ -212,36 +214,35 @@ TEST(HttpContextTest, SetCookie) auto& actual_res = ctx.get_response(); auto range = actual_res.equal_range(http::field::set_cookie); std::vector cookies; - for(auto it = range.first; it != range.second; ++it) { - cookies.emplace_back(it->value()); + for (auto it = range.first; it != range.second; ++it) { + cookies.emplace_back(it->value()); } ASSERT_EQ(cookies.size(), 2); - + bool found_foo = false; bool found_user = false; for (const auto& c : cookies) { - if (c.find("foo=bar") != std::string::npos) { - found_foo = true; - } - if (c.find("user=123") != std::string::npos) { - found_user = true; - ASSERT_NE(c.find("Max-Age=3600"), std::string::npos); - ASSERT_NE(c.find("HttpOnly"), std::string::npos); - ASSERT_NE(c.find("Secure"), std::string::npos); - ASSERT_NE(c.find("Path=/api"), std::string::npos); - ASSERT_NE(c.find("Domain=example.com"), std::string::npos); - ASSERT_NE(c.find("SameSite=Strict"), std::string::npos); - } + if (c.find("foo=bar") != std::string::npos) { + found_foo = true; + } + if (c.find("user=123") != std::string::npos) { + found_user = true; + ASSERT_NE(c.find("Max-Age=3600"), std::string::npos); + ASSERT_NE(c.find("HttpOnly"), std::string::npos); + ASSERT_NE(c.find("Secure"), std::string::npos); + ASSERT_NE(c.find("Path=/api"), std::string::npos); + ASSERT_NE(c.find("Domain=example.com"), std::string::npos); + ASSERT_NE(c.find("SameSite=Strict"), std::string::npos); + } } ASSERT_TRUE(found_foo); ASSERT_TRUE(found_user); } // Test set_body_json and set_body_from -TEST(HttpContextTest, SetBodyJson) -{ +TEST(HttpContextTest, SetBodyJson) { http::request req = make_request(http::verb::get, "/"); http::response res; khttpd_fw::HttpContext ctx(req, res); @@ -257,8 +258,7 @@ TEST(HttpContextTest, SetBodyJson) ASSERT_EQ(actual_res.body(), R"({"status":"ok","count":42})"); } -TEST(HttpContextTest, SetBodyFrom) -{ +TEST(HttpContextTest, SetBodyFrom) { http::request req = make_request(http::verb::get, "/"); http::response res; khttpd_fw::HttpContext ctx(req, res); @@ -276,8 +276,7 @@ TEST(HttpContextTest, SetBodyFrom) } // Test path params set/get directly -TEST(HttpContextTest, PathParamsWithoutDispatch) -{ +TEST(HttpContextTest, PathParamsWithoutDispatch) { http::request req = make_request(http::verb::get, "/"); http::response res; khttpd_fw::HttpContext ctx(req, res); @@ -293,8 +292,7 @@ TEST(HttpContextTest, PathParamsWithoutDispatch) } // Test context attributes -TEST(HttpContextTest, Attributes) -{ +TEST(HttpContextTest, Attributes) { http::request req = make_request(http::verb::get, "/"); http::response res; khttpd_fw::HttpContext ctx(req, res); @@ -320,8 +318,7 @@ TEST(HttpContextTest, Attributes) } // Test header case insensitivity -TEST(HttpContextTest, HeaderCaseInsensitive) -{ +TEST(HttpContextTest, HeaderCaseInsensitive) { http::request req = make_request(http::verb::get, "/"); req.set("X-Custom-Header", "value1"); http::response res; @@ -333,8 +330,7 @@ TEST(HttpContextTest, HeaderCaseInsensitive) } // Test set_cookie rejects invalid key characters -TEST(HttpContextTest, InvalidCookieKeyChars) -{ +TEST(HttpContextTest, InvalidCookieKeyChars) { http::request req = make_request(http::verb::get, "/"); http::response res; khttpd_fw::HttpContext ctx(req, res); @@ -345,13 +341,14 @@ TEST(HttpContextTest, InvalidCookieKeyChars) auto& actual_res = ctx.get_response(); auto range = actual_res.equal_range(http::field::set_cookie); int count = 0; - for(auto it = range.first; it != range.second; ++it) { count++; } - ASSERT_EQ(count, 0); // No cookies should be set + for (auto it = range.first; it != range.second; ++it) { + count++; + } + ASSERT_EQ(count, 0); // No cookies should be set } // Test set_cookie rejects invalid value characters -TEST(HttpContextTest, InvalidCookieValueChars) -{ +TEST(HttpContextTest, InvalidCookieValueChars) { http::request req = make_request(http::verb::get, "/"); http::response res; khttpd_fw::HttpContext ctx(req, res); @@ -362,13 +359,14 @@ TEST(HttpContextTest, InvalidCookieValueChars) auto& actual_res = ctx.get_response(); auto range = actual_res.equal_range(http::field::set_cookie); int count = 0; - for(auto it = range.first; it != range.second; ++it) { count++; } - ASSERT_EQ(count, 0); // No cookies should be set + for (auto it = range.first; it != range.second; ++it) { + count++; + } + ASSERT_EQ(count, 0); // No cookies should be set } // Test empty path -TEST(HttpContextTest, EmptyPath) -{ +TEST(HttpContextTest, EmptyPath) { http::request req = make_request(http::verb::get, ""); http::response res; khttpd_fw::HttpContext ctx(req, res); @@ -377,10 +375,10 @@ TEST(HttpContextTest, EmptyPath) } // Test JSON array body -TEST(HttpContextTest, JsonArrayBody) -{ +TEST(HttpContextTest, JsonArrayBody) { std::string json_str = R"([1, "two", {"key": "value"}])"; - http::request req = make_request(http::verb::post, "/api/array", 11, json_str); + http::request req = + make_request(http::verb::post, "/api/array", 11, json_str); req.set(http::field::content_type, "application/json"); http::response res; khttpd_fw::HttpContext ctx(req, res); diff --git a/framework/tests/cronjob_test.cpp b/framework/tests/cronjob_test.cpp index b72196c..a9d2b7c 100644 --- a/framework/tests/cronjob_test.cpp +++ b/framework/tests/cronjob_test.cpp @@ -1,22 +1,24 @@ #include -#include -#include -#include + #include #include +#include +#include +#include // 包含你之前的头文件 -#include "cron/CronJob.hpp" -#include "io_context_pool.hpp" - #include -#include -#include -#include + #include #include +#include +#include +#include #include +#include "cron/CronJob.hpp" +#include "io_context_pool.hpp" + // 引入你的头文件 #include "cron/CronScheduler.hpp" #include "io_context_pool.hpp" @@ -25,17 +27,12 @@ using namespace std::chrono_literals; using namespace khttpd::framework; // --- 测试用的辅助类 --- -class TestableCronJob : public CronJob -{ -public: - TestableCronJob(const std::string& expr) - : CronJob(expr), run_count_(0) - { - } +class TestableCronJob : public CronJob { + public: + TestableCronJob(const std::string& expr) : CronJob(expr), run_count_(0) {} // 实现 run 方法 - void run() override - { + void run() override { // 1. 增加计数 run_count_++; @@ -49,21 +46,16 @@ class TestableCronJob : public CronJob // 辅助方法:等待任务执行 n 次 // 返回 true 表示在超时前完成了任务,false 表示超时 - bool wait_for_runs(int expected_count, std::chrono::milliseconds timeout) - { + bool wait_for_runs(int expected_count, std::chrono::milliseconds timeout) { std::unique_lock lock(mutex_); - return cv_.wait_for(lock, timeout, [this, expected_count]() - { + return cv_.wait_for(lock, timeout, [this, expected_count]() { return run_count_ >= expected_count; }); } - int get_run_count() const - { - return run_count_; - } + int get_run_count() const { return run_count_; } -private: + private: std::atomic run_count_; std::mutex mutex_; std::condition_variable cv_; @@ -71,37 +63,33 @@ class TestableCronJob : public CronJob // --- 测试套件 --- -class CronJobTest : public ::testing::Test -{ -protected: - static void SetUpTestSuite() - { +class CronJobTest : public ::testing::Test { + protected: + static void SetUpTestSuite() { // 确保 IoContextPool 至少有一个线程在运行 // 注意:单例模式下,这个池会在所有测试间共享 IoContextPool::instance(1); } - static void TearDownTestSuite() - { + static void TearDownTestSuite() { // 测试结束后停止池(可选,视具体需求而定) // IoContextPool::instance().stop(); } }; // 测试 1: 验证无效的 Cron 表达式会抛出异常 -TEST_F(CronJobTest, ThrowsOnInvalidExpression) -{ +TEST_F(CronJobTest, ThrowsOnInvalidExpression) { // 这是一个错误的表达式 (只有 5 个字段,或者是乱码) std::string invalid_expr = "invalid cron string"; - EXPECT_THROW({ - auto job = std::make_shared(invalid_expr); - }, std::runtime_error); // 这里的异常类型取决于 croncpp 具体抛出什么,通常是 std::runtime_error 或 croncpp::cron_exception + EXPECT_THROW( + { auto job = std::make_shared(invalid_expr); }, + std::runtime_error); // 这里的异常类型取决于 croncpp 具体抛出什么,通常是 + // std::runtime_error 或 croncpp::cron_exception } // 测试 2: 验证任务是否能被调度和执行 -TEST_F(CronJobTest, RunsScheduleCorrectly) -{ +TEST_F(CronJobTest, RunsScheduleCorrectly) { // 设置为每秒执行一次 ("* * * * * *") // 注意:croncpp 能够处理秒级 auto job = std::make_shared("* * * * * *"); @@ -119,8 +107,7 @@ TEST_F(CronJobTest, RunsScheduleCorrectly) } // 测试 3: 验证 Stop 后不再执行 -TEST_F(CronJobTest, StopPreventsFurtherExecution) -{ +TEST_F(CronJobTest, StopPreventsFurtherExecution) { auto job = std::make_shared("* * * * * *"); job->start(); @@ -142,8 +129,7 @@ TEST_F(CronJobTest, StopPreventsFurtherExecution) } // 测试 4: 多个任务并发 -TEST_F(CronJobTest, MultipleJobs) -{ +TEST_F(CronJobTest, MultipleJobs) { auto job1 = std::make_shared("* * * * * *"); auto job2 = std::make_shared("* * * * * *"); @@ -158,109 +144,83 @@ TEST_F(CronJobTest, MultipleJobs) job2->stop(); } - // --- 辅助类:用于线程安全地计数和等待 --- -class AsyncCounter -{ -public: - void tick() - { +class AsyncCounter { + public: + void tick() { run_count_++; cv_.notify_all(); } - int get_count() const - { - return run_count_; - } + int get_count() const { return run_count_; } // 等待至少达到 expected_count 次执行 // 返回 true 表示成功,false 表示超时 - bool wait_for_at_least(int expected_count, std::chrono::milliseconds timeout) - { + bool wait_for_at_least(int expected_count, + std::chrono::milliseconds timeout) { std::unique_lock lock(mtx_); - return cv_.wait_for(lock, timeout, [this, expected_count]() - { + return cv_.wait_for(lock, timeout, [this, expected_count]() { return run_count_ >= expected_count; }); } // 等待指定的时间,确认在此期间计数器是否变化(用于验证 Stop 和 Delay) // 如果计数器在 timeout 内没有增加,返回 true - bool ensure_no_execution_for(std::chrono::milliseconds duration) - { + bool ensure_no_execution_for(std::chrono::milliseconds duration) { int initial = run_count_; std::unique_lock lock(mtx_); - // wait_for 返回 false 表示超时(即条件一直不满足),这意味着没有达到 initial + 1 - // 所以如果 wait_for 返回 false,说明没有执行,是我们想要的结果 - bool triggered = cv_.wait_for(lock, duration, [this, initial]() - { - return run_count_ > initial; - }); + // wait_for 返回 false 表示超时(即条件一直不满足),这意味着没有达到 + // initial + 1 所以如果 wait_for 返回 false,说明没有执行,是我们想要的结果 + bool triggered = cv_.wait_for( + lock, duration, [this, initial]() { return run_count_ > initial; }); return !triggered; } -private: + private: std::atomic run_count_{0}; std::mutex mtx_; std::condition_variable cv_; }; // --- 测试套件 --- -class CronSchedulerTest : public ::testing::Test -{ -protected: - static void SetUpTestSuite() - { +class CronSchedulerTest : public ::testing::Test { + protected: + static void SetUpTestSuite() { // 初始化线程池,使用 2 个线程以支持并发测试 IoContextPool::instance(2); } - static void TearDownTestSuite() - { - IoContextPool::instance().stop(); - } + static void TearDownTestSuite() { IoContextPool::instance().stop(); } - void SetUp() override - { - CronScheduler::instance().stop_all(); - } + void SetUp() override { CronScheduler::instance().stop_all(); } - void TearDown() override - { - CronScheduler::instance().stop_all(); - } + void TearDown() override { CronScheduler::instance().stop_all(); } }; // 测试 1: 验证通过 Scheduler 调度的基础 Lambda 任务能正常运行 -TEST_F(CronSchedulerTest, ScheduleBasic) -{ +TEST_F(CronSchedulerTest, ScheduleBasic) { auto counter = std::make_shared(); // 每秒执行一次 // 注意:持有返回的 job 指针,否则测试函数结束时如果 pool 还在跑,任务也会跑 - auto job = CronScheduler::instance().schedule("* * * * * *", [counter]() - { - counter->tick(); - }); + auto job = CronScheduler::instance().schedule( + "* * * * * *", [counter]() { counter->tick(); }); // 等待至少执行 1 次,超时时间 2.5 秒 - ASSERT_TRUE(counter->wait_for_at_least(1, 2500ms)) << "Job failed to run within timeout"; + ASSERT_TRUE(counter->wait_for_at_least(1, 2500ms)) + << "Job failed to run within timeout"; // 停止任务 job->stop(); } // 测试 2: 验证手动停止 (Stop) 功能,并测试之前的竞态条件修复 -TEST_F(CronSchedulerTest, ScheduleStop) -{ +TEST_F(CronSchedulerTest, ScheduleStop) { auto counter = std::make_shared(); // 极高频任务(每秒) - auto job = CronScheduler::instance().schedule("* * * * * *", [counter]() - { - counter->tick(); - }); + auto job = CronScheduler::instance().schedule( + "* * * * * *", [counter]() { counter->tick(); }); // 1. 确保它跑起来了 ASSERT_TRUE(counter->wait_for_at_least(1, 2000ms)); @@ -276,12 +236,11 @@ TEST_F(CronSchedulerTest, ScheduleStop) std::this_thread::sleep_for(2000ms); EXPECT_EQ(counter->get_count(), count_after_stop) - << "Job continued running after stop() was called"; + << "Job continued running after stop() was called"; } // 测试 3: 验证延迟启动 (Delayed Start) -TEST_F(CronSchedulerTest, ScheduleDelay) -{ +TEST_F(CronSchedulerTest, ScheduleDelay) { auto counter = std::make_shared(); // 定义延迟时间:2秒 @@ -289,10 +248,7 @@ TEST_F(CronSchedulerTest, ScheduleDelay) // 调度:每秒执行一次,但先延迟 2 秒 auto job = CronScheduler::instance().schedule( - "* * * * * *", - [counter]() { counter->tick(); }, - delay_time - ); + "* * * * * *", [counter]() { counter->tick(); }, delay_time); // 阶段 A: 验证在延迟期间(比如前 1 秒内),任务没有运行 // Cron 是每秒一次,如果没有延迟,1秒内肯定会跑。 @@ -302,20 +258,21 @@ TEST_F(CronSchedulerTest, ScheduleDelay) // 阶段 B: 验证延迟结束后,任务开始运行 // 现在已经过了 1s,再等 2.5s (总共 3.5s),应该能覆盖 2s 延迟 + 1s 触发 ASSERT_TRUE(counter->wait_for_at_least(1, 2500ms)) - << "Job failed to start after delay"; + << "Job failed to start after delay"; job->stop(); } // 测试 4: 多个任务并发 -TEST_F(CronSchedulerTest, MultipleTasks) -{ +TEST_F(CronSchedulerTest, MultipleTasks) { auto counter1 = std::make_shared(); auto counter2 = std::make_shared(); - auto job1 = CronScheduler::instance().schedule("* * * * * *", [counter1]() { counter1->tick(); }); + auto job1 = CronScheduler::instance().schedule( + "* * * * * *", [counter1]() { counter1->tick(); }); // job2 延迟 1 秒开始 - auto job2 = CronScheduler::instance().schedule("* * * * * *", [counter2]() { counter2->tick(); }, 1000ms); + auto job2 = CronScheduler::instance().schedule( + "* * * * * *", [counter2]() { counter2->tick(); }, 1000ms); // 验证 job1 跑了 EXPECT_TRUE(counter1->wait_for_at_least(1, 2000ms)); @@ -328,17 +285,15 @@ TEST_F(CronSchedulerTest, MultipleTasks) } // 测试 5: 验证错误的表达式不会导致 Crash,而是抛出异常 -TEST_F(CronSchedulerTest, InvalidExpression) -{ - EXPECT_THROW({ - CronScheduler::instance().schedule("invalid cron", [](){}); - }, std::exception); +TEST_F(CronSchedulerTest, InvalidExpression) { + EXPECT_THROW({ CronScheduler::instance().schedule("invalid cron", []() {}); }, + std::exception); } -TEST_F(CronSchedulerTest, UnscheduleStopsAndRemovesJob) -{ +TEST_F(CronSchedulerTest, UnscheduleStopsAndRemovesJob) { auto counter = std::make_shared(); - auto job = CronScheduler::instance().schedule("* * * * * *", [counter]() { counter->tick(); }); + auto job = CronScheduler::instance().schedule( + "* * * * * *", [counter]() { counter->tick(); }); EXPECT_EQ(CronScheduler::instance().job_count(), 1u); @@ -349,8 +304,7 @@ TEST_F(CronSchedulerTest, UnscheduleStopsAndRemovesJob) EXPECT_TRUE(counter->ensure_no_execution_for(1200ms)); } -TEST_F(CronSchedulerTest, PruneStoppedRemovesManuallyStoppedJobs) -{ +TEST_F(CronSchedulerTest, PruneStoppedRemovesManuallyStoppedJobs) { auto job1 = CronScheduler::instance().schedule("* * * * * *", []() {}); auto job2 = CronScheduler::instance().schedule("* * * * * *", []() {}); diff --git a/framework/tests/di_container_test.cpp b/framework/tests/di_container_test.cpp index 64f0846..d90e475 100644 --- a/framework/tests/di_container_test.cpp +++ b/framework/tests/di_container_test.cpp @@ -1,7 +1,9 @@ -#include "gtest/gtest.h" #include "di/di_container.hpp" + +#include // For tracking construction/destruction #include -#include // For tracking construction/destruction + +#include "gtest/gtest.h" using namespace khttpd::framework; @@ -14,86 +16,71 @@ std::atomic s_MainComponent_count{0}; // 测试用组件类 // =========================================== -class DependencyA : public ComponentBase -{ -public: +class DependencyA : public ComponentBase { + public: bool constructed = false; - DependencyA() - { + DependencyA() { constructed = true; s_DependencyA_count++; // spdlog::debug("DependencyA constructed. Count: {}", s_DependencyA_count); } - ~DependencyA() override - { + ~DependencyA() override { s_DependencyA_count--; // spdlog::debug("DependencyA destructed. Count: {}", s_DependencyA_count); } }; -class DependencyB : public ComponentBase -{ -public: +class DependencyB : public ComponentBase { + public: bool constructed = false; std::shared_ptr depA; - explicit DependencyB(std::shared_ptr a) : depA(a) - { + explicit DependencyB(std::shared_ptr a) : depA(a) { constructed = true; s_DependencyB_count++; // spdlog::debug("DependencyB constructed. Count: {}", s_DependencyB_count); } - ~DependencyB() override - { + ~DependencyB() override { s_DependencyB_count--; // spdlog::debug("DependencyB destructed. Count: {}", s_DependencyB_count); } - [[nodiscard]] std::shared_ptr getDepA() const - { - return depA; - } + [[nodiscard]] std::shared_ptr getDepA() const { return depA; } }; -class MainComponent : public ComponentBase -{ -public: +class MainComponent : public ComponentBase { + public: bool constructed = false; std::shared_ptr depB; - explicit MainComponent(std::shared_ptr b) : depB(b) - { + explicit MainComponent(std::shared_ptr b) : depB(b) { constructed = true; s_MainComponent_count++; - // spdlog::debug("MainComponent constructed. Count: {}", s_MainComponent_count); + // spdlog::debug("MainComponent constructed. Count: {}", + // s_MainComponent_count); } - ~MainComponent() override - { + ~MainComponent() override { s_MainComponent_count--; - // spdlog::debug("MainComponent destructed. Count: {}", s_MainComponent_count); + // spdlog::debug("MainComponent destructed. Count: {}", + // s_MainComponent_count); } - [[nodiscard]] std::shared_ptr getDepB() const - { - return depB; - } + [[nodiscard]] std::shared_ptr getDepB() const { return depB; } }; // =========================================== // GTest 测试夹具 (Test Fixture) // =========================================== -class DIContainerTest : public ::testing::Test -{ -protected: +class DIContainerTest : public ::testing::Test { + protected: DI_Container& container = DI_Container::instance(); - void SetUp() override - { + void SetUp() override { // 在每个测试开始前清理容器,确保测试独立 container.clear(); // 重置计数器 @@ -102,15 +89,17 @@ class DIContainerTest : public ::testing::Test s_MainComponent_count = 0; } - void TearDown() override - { + void TearDown() override { // 确保所有 shared_ptr 在测试结束时被释放,从而触发析构 // GTest会自动在测试结束后销毁局部变量,导致shared_ptr引用计数归零 // 但这里为了明确性,我们可以在需要时再次clear - container.clear(); // 确保单例缓存也被清空,以便析构 - ASSERT_EQ(s_DependencyA_count, 0) << "DependencyA instances not fully destructed."; - ASSERT_EQ(s_DependencyB_count, 0) << "DependencyB instances not fully destructed."; - ASSERT_EQ(s_MainComponent_count, 0) << "MainComponent instances not fully destructed."; + container.clear(); // 确保单例缓存也被清空,以便析构 + ASSERT_EQ(s_DependencyA_count, 0) + << "DependencyA instances not fully destructed."; + ASSERT_EQ(s_DependencyB_count, 0) + << "DependencyB instances not fully destructed."; + ASSERT_EQ(s_MainComponent_count, 0) + << "MainComponent instances not fully destructed."; } }; @@ -119,33 +108,29 @@ class DIContainerTest : public ::testing::Test // =========================================== // 测试 DI_Container 自身是否是单例 -TEST_F(DIContainerTest, IsSingleton) -{ +TEST_F(DIContainerTest, IsSingleton) { DI_Container& container1 = DI_Container::instance(); DI_Container& container2 = DI_Container::instance(); ASSERT_EQ(&container1, &container2); } // 测试注册和解析没有依赖的组件 -TEST_F(DIContainerTest, RegisterAndResolveNoDependencies) -{ +TEST_F(DIContainerTest, RegisterAndResolveNoDependencies) { container.register_component(); auto a = container.resolve(); ASSERT_NE(nullptr, a); ASSERT_TRUE(a->constructed); - ASSERT_EQ(s_DependencyA_count, 1); // 应该只创建了一个实例 + ASSERT_EQ(s_DependencyA_count, 1); // 应该只创建了一个实例 } // 测试解析一个未注册的组件是否抛出异常 -TEST_F(DIContainerTest, ResolveNonExistentComponentThrows) -{ +TEST_F(DIContainerTest, ResolveNonExistentComponentThrows) { ASSERT_THROW(container.resolve(), std::runtime_error); } // 测试依赖解析是否工作 -TEST_F(DIContainerTest, DependencyResolutionWorks) -{ +TEST_F(DIContainerTest, DependencyResolutionWorks) { container.register_component(); container.register_component(); @@ -155,7 +140,8 @@ TEST_F(DIContainerTest, DependencyResolutionWorks) ASSERT_NE(nullptr, b->getDepA()); ASSERT_TRUE(b->getDepA()->constructed); - // 验证 DependencyA 也是单例,即 b 内部的 DependencyA 和直接解析的 DependencyA 是同一个实例 + // 验证 DependencyA 也是单例,即 b 内部的 DependencyA 和直接解析的 DependencyA + // 是同一个实例 ASSERT_EQ(b->getDepA().get(), container.resolve().get()); ASSERT_EQ(s_DependencyA_count, 1); @@ -163,8 +149,7 @@ TEST_F(DIContainerTest, DependencyResolutionWorks) } // 测试嵌套依赖解析是否工作 -TEST_F(DIContainerTest, NestedDependencyResolutionWorks) -{ +TEST_F(DIContainerTest, NestedDependencyResolutionWorks) { container.register_component(); container.register_component(); container.register_component(); @@ -181,7 +166,8 @@ TEST_F(DIContainerTest, NestedDependencyResolutionWorks) // 验证所有依赖都是单例,即它们都是同一个实例 ASSERT_EQ(mainComp->getDepB().get(), container.resolve().get()); - ASSERT_EQ(mainComp->getDepB()->getDepA().get(), container.resolve().get()); + ASSERT_EQ(mainComp->getDepB()->getDepA().get(), + container.resolve().get()); ASSERT_EQ(s_DependencyA_count, 1); ASSERT_EQ(s_DependencyB_count, 1); @@ -189,22 +175,21 @@ TEST_F(DIContainerTest, NestedDependencyResolutionWorks) } // 测试组件是否被正确地作为单例管理 -TEST_F(DIContainerTest, ComponentIsSingleton) -{ +TEST_F(DIContainerTest, ComponentIsSingleton) { container.register_component(); auto a1 = container.resolve(); auto a2 = container.resolve(); - ASSERT_EQ(a1.get(), a2.get()); // 验证地址相同 - ASSERT_EQ(s_DependencyA_count, 1); // 应该只构造了一次 + ASSERT_EQ(a1.get(), a2.get()); // 验证地址相同 + ASSERT_EQ(s_DependencyA_count, 1); // 应该只构造了一次 } // 测试重新注册组件(覆盖)是否正常(应该给出警告但不抛异常) -TEST_F(DIContainerTest, OverwriteRegistrationWarning) -{ +TEST_F(DIContainerTest, OverwriteRegistrationWarning) { container.register_component(); - // GTest usually captures or redirects warning logs; this only verifies it does not crash. + // GTest usually captures or redirects warning logs; this only verifies it + // does not crash. container.register_component(); // 验证仍能解析 @@ -213,8 +198,7 @@ TEST_F(DIContainerTest, OverwriteRegistrationWarning) } // 测试当一个组件的依赖未注册时是否抛出异常 -TEST_F(DIContainerTest, ResolveThrowsForMissingNestedDependency) -{ +TEST_F(DIContainerTest, ResolveThrowsForMissingNestedDependency) { // 只注册 DependencyB,不注册 DependencyA container.register_component(); @@ -223,28 +207,26 @@ TEST_F(DIContainerTest, ResolveThrowsForMissingNestedDependency) } // 测试共享指针的生命周期管理 -TEST_F(DIContainerTest, SharedPtrManagesLifetime) -{ +TEST_F(DIContainerTest, SharedPtrManagesLifetime) { container.register_component(); std::shared_ptr a_ptr; { auto temp_ptr = container.resolve(); - a_ptr = temp_ptr; // 拷贝 shared_ptr,引用计数增加 + a_ptr = temp_ptr; // 拷贝 shared_ptr,引用计数增加 ASSERT_EQ(s_DependencyA_count, 1); - } // temp_ptr 超出作用域,引用计数减1,但 a_ptr 仍持有,所以不会析构 + } // temp_ptr 超出作用域,引用计数减1,但 a_ptr 仍持有,所以不会析构 - ASSERT_EQ(s_DependencyA_count, 1); // 实例仍然存在 + ASSERT_EQ(s_DependencyA_count, 1); // 实例仍然存在 - container.clear(); // 清空容器缓存,不再持有引用 + container.clear(); // 清空容器缓存,不再持有引用 // 此时,如果 a_ptr 是唯一持有者,则应该析构 // GTest的TearDown会再次clear,所以在此之前,a_ptr 可能会是唯一持有者并析构 // 或者在a_ptr自己超出作用域后析构 } // 确保在嵌套解析中,所有依赖只被构造一次 -TEST_F(DIContainerTest, NestedResolutionOnlyConstructsOnce) -{ +TEST_F(DIContainerTest, NestedResolutionOnlyConstructsOnce) { container.register_component(); container.register_component(); container.register_component(); @@ -260,7 +242,7 @@ TEST_F(DIContainerTest, NestedResolutionOnlyConstructsOnce) ASSERT_EQ(s_DependencyA_count, 1); ASSERT_EQ(s_DependencyB_count, 1); ASSERT_EQ(s_MainComponent_count, 1); - ASSERT_EQ(mainComp1.get(), mainComp2.get()); // 验证是同一个实例 + ASSERT_EQ(mainComp1.get(), mainComp2.get()); // 验证是同一个实例 // 直接解析依赖,也应该返回已存在的单例 auto a = container.resolve(); @@ -269,22 +251,19 @@ TEST_F(DIContainerTest, NestedResolutionOnlyConstructsOnce) } // Test circular dependency detection -class CircularA : public ComponentBase -{ -public: +class CircularA : public ComponentBase { + public: explicit CircularA(std::shared_ptr b) : depB(b) {} std::shared_ptr depB; }; -class CircularB : public ComponentBase -{ -public: +class CircularB : public ComponentBase { + public: explicit CircularB(std::shared_ptr a) : depA(a) {} std::shared_ptr depA; }; -TEST_F(DIContainerTest, CircularDependencyDetection) -{ +TEST_F(DIContainerTest, CircularDependencyDetection) { container.register_component(); container.register_component(); @@ -293,8 +272,7 @@ TEST_F(DIContainerTest, CircularDependencyDetection) } // Test resolve after clear throws -TEST_F(DIContainerTest, ResolveAfterClear) -{ +TEST_F(DIContainerTest, ResolveAfterClear) { container.register_component(); auto a = container.resolve(); ASSERT_NE(a, nullptr); diff --git a/framework/tests/http_client_stream_edge_test.cpp b/framework/tests/http_client_stream_edge_test.cpp index 6ad06f5..d5e5055 100644 --- a/framework/tests/http_client_stream_edge_test.cpp +++ b/framework/tests/http_client_stream_edge_test.cpp @@ -1,21 +1,21 @@ -#include "framework/client/http_client_stream.hpp" -#include "framework/client/http_client.hpp" - #include + +#include #include #include -#include #include +#include "framework/client/http_client.hpp" +#include "framework/client/http_client_stream.hpp" + namespace client = khttpd::framework::client; namespace http = boost::beast::http; namespace net = boost::asio; namespace ssl = net::ssl; using tcp = net::ip::tcp; -namespace -{ - constexpr char test_certificate[] = R"PEM(-----BEGIN CERTIFICATE----- +namespace { +constexpr char test_certificate[] = R"PEM(-----BEGIN CERTIFICATE----- MIIDCTCCAfGgAwIBAgIUThDTQ+lb7kmBiXurHn9Wy6vf+54wDQYJKoZIhvcNAQEL BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDgwOTE0NDcwN1oXDTI2MDgx MDE0NDcwN1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF @@ -35,7 +35,7 @@ alJ1edM4GDkgOW+q/iIrLSLq7UFl+JgQYD+jHXF3gDgjqK/riDjWYxO3TabwjcBz d+xV3kUBYw3hrnwi7A== -----END CERTIFICATE-----)PEM"; - constexpr char test_private_key[] = R"PEM(-----BEGIN PRIVATE KEY----- +constexpr char test_private_key[] = R"PEM(-----BEGIN PRIVATE KEY----- MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCRlrIk5aHK+z08 FhvGD2lmezU6hWSvkazsOYO7NlKmwpbQytY4wLK9BU7HI7CyMndXjHcrho5kyEvM F7SZ7CJD7YsNMPRup+DaYd/zHXGTABxYzbmqI2jrI0OIXMKvEWAxFyTh6OWv5gZK @@ -63,22 +63,22 @@ lL7kS/l+M8K6Q2ghxEPLQzPD+gQvlUT+zjpoP9jHK3gHq3Im4bayFvmaqX+t0Piy CQmqJob1eraZfRUHk7QnsOO0TF+s6UWf1bO3rYZBLdr15sWcAkC7e0GhfGEhSFzW C6fgselIUr+XYmAQoT5Tqw== -----END PRIVATE KEY-----)PEM"; -} +} // namespace -TEST(HttpClientStreamEdgeTest, StreamsRequestAndResponseOverTls) -{ +TEST(HttpClientStreamEdgeTest, StreamsRequestAndResponseOverTls) { net::io_context server_ioc; ssl::context server_context(ssl::context::tls_server); server_context.use_certificate_chain(net::buffer(test_certificate)); - server_context.use_private_key(net::buffer(test_private_key), ssl::context::file_format::pem); + server_context.use_private_key(net::buffer(test_private_key), + ssl::context::file_format::pem); tcp::acceptor acceptor(server_ioc, {net::ip::address_v4::loopback(), 0}); const auto port = acceptor.local_endpoint().port(); const std::string payload(4097, 't'); - std::thread server([&] - { + std::thread server([&] { tcp::socket socket(server_ioc); acceptor.accept(socket); - boost::beast::ssl_stream tls(std::move(socket), server_context); + boost::beast::ssl_stream tls(std::move(socket), + server_context); tls.handshake(ssl::stream_base::server); boost::beast::flat_buffer buffer; http::request_parser parser; @@ -106,55 +106,52 @@ TEST(HttpClientStreamEdgeTest, StreamsRequestAndResponseOverTls) client::HttpClientStream::RequestHead head{http::verb::post, "/ignored", 11}; head.content_length(payload.size()); - *read_next = [stream, received, read_buffer, read_next, &completed] - { - stream->async_read_some(net::buffer(*read_buffer), - [stream, received, read_buffer, read_next, &completed] - (boost::system::error_code ec, std::size_t size, bool done) - { - ASSERT_FALSE(ec) << ec.message(); - received->append(read_buffer->data(), size); - if (!done) return (*read_next)(); - completed = true; - }); + *read_next = [stream, received, read_buffer, read_next, &completed] { + stream->async_read_some( + net::buffer(*read_buffer), + [stream, received, read_buffer, read_next, &completed]( + boost::system::error_code ec, std::size_t size, bool done) { + ASSERT_FALSE(ec) << ec.message(); + received->append(read_buffer->data(), size); + if (!done) return (*read_next)(); + completed = true; + }); }; - *write_next = [stream, &payload, write_offset, write_next, read_next] - { + *write_next = [stream, &payload, write_offset, write_next, read_next] { if (*write_offset == payload.size()) - return stream->async_finish_request([stream, read_next](boost::system::error_code ec) - { - ASSERT_FALSE(ec) << ec.message(); - stream->async_read_response_head([read_next] - (boost::system::error_code head_ec, client::HttpClientStream::ResponseHead head) - { - ASSERT_FALSE(head_ec) << head_ec.message(); - EXPECT_EQ(head.result(), http::status::ok); - (*read_next)(); - }); - }); + return stream->async_finish_request( + [stream, read_next](boost::system::error_code ec) { + ASSERT_FALSE(ec) << ec.message(); + stream->async_read_response_head( + [read_next](boost::system::error_code head_ec, + client::HttpClientStream::ResponseHead head) { + ASSERT_FALSE(head_ec) << head_ec.message(); + EXPECT_EQ(head.result(), http::status::ok); + (*read_next)(); + }); + }); const auto size = std::min(7, payload.size() - *write_offset); - stream->async_write_some(net::buffer(payload.data() + *write_offset, size), - [write_offset, size, write_next](boost::system::error_code ec) - { + stream->async_write_some( + net::buffer(payload.data() + *write_offset, size), + [write_offset, size, write_next](boost::system::error_code ec) { + ASSERT_FALSE(ec) << ec.message(); + *write_offset += size; + (*write_next)(); + }); + }; + stream->async_start( + "https://127.0.0.1:" + std::to_string(port) + "/upload?part=1", + std::move(head), [write_next](boost::system::error_code ec) { ASSERT_FALSE(ec) << ec.message(); - *write_offset += size; (*write_next)(); }); - }; - stream->async_start("https://127.0.0.1:" + std::to_string(port) + "/upload?part=1", - std::move(head), [write_next](boost::system::error_code ec) - { - ASSERT_FALSE(ec) << ec.message(); - (*write_next)(); - }); ioc.run(); server.join(); EXPECT_TRUE(completed); EXPECT_EQ(*received, payload); } -TEST(HttpClientStreamEdgeTest, RejectsMalformedUrl) -{ +TEST(HttpClientStreamEdgeTest, RejectsMalformedUrl) { net::io_context ioc; auto stream = std::make_shared(ioc); client::HttpClientStream::RequestHead head{http::verb::get, "/", 11}; @@ -162,46 +159,46 @@ TEST(HttpClientStreamEdgeTest, RejectsMalformedUrl) stream->async_start("not a url", std::move(head), [&](boost::system::error_code ec) { result = ec; }); ioc.run(); - EXPECT_EQ(result, make_error_code(boost::system::errc::operation_not_supported)); + EXPECT_EQ(result, + make_error_code(boost::system::errc::operation_not_supported)); } -TEST(HttpClientStreamEdgeTest, WriteBeforeStartIsAborted) -{ +TEST(HttpClientStreamEdgeTest, WriteBeforeStartIsAborted) { net::io_context ioc; auto stream = std::make_shared(ioc); const std::array data{'x'}; boost::system::error_code result; - stream->async_write_some(net::buffer(data), [&](boost::system::error_code ec) { result = ec; }); + stream->async_write_some(net::buffer(data), + [&](boost::system::error_code ec) { result = ec; }); ioc.run(); EXPECT_EQ(result, net::error::operation_aborted); } -TEST(HttpClientStreamEdgeTest, FinishBeforeStartIsAborted) -{ +TEST(HttpClientStreamEdgeTest, FinishBeforeStartIsAborted) { net::io_context ioc; auto stream = std::make_shared(ioc); boost::system::error_code result; - stream->async_finish_request([&](boost::system::error_code ec) { result = ec; }); + stream->async_finish_request( + [&](boost::system::error_code ec) { result = ec; }); ioc.run(); EXPECT_EQ(result, net::error::operation_aborted); } -TEST(HttpClientStreamEdgeTest, HeadSkipsBodyAfterConsecutiveInformationalResponses) -{ +TEST(HttpClientStreamEdgeTest, + HeadSkipsBodyAfterConsecutiveInformationalResponses) { net::io_context server_ioc; tcp::acceptor acceptor(server_ioc, {net::ip::address_v4::loopback(), 0}); const auto port = acceptor.local_endpoint().port(); - std::thread server([&] - { + std::thread server([&] { tcp::socket socket(server_ioc); acceptor.accept(socket); boost::beast::flat_buffer request_buffer; http::request request; http::read(socket, request_buffer, request); const std::string wire = - "HTTP/1.1 100 Continue\r\n\r\n" - "HTTP/1.1 103 Early Hints\r\nLink: ; rel=preload\r\n\r\n" - "HTTP/1.1 200 OK\r\nContent-Length: 1234\r\nConnection: close\r\n\r\n"; + "HTTP/1.1 100 Continue\r\n\r\n" + "HTTP/1.1 103 Early Hints\r\nLink: ; rel=preload\r\n\r\n" + "HTTP/1.1 200 OK\r\nContent-Length: 1234\r\nConnection: close\r\n\r\n"; net::write(socket, net::buffer(wire)); }); @@ -209,64 +206,67 @@ TEST(HttpClientStreamEdgeTest, HeadSkipsBodyAfterConsecutiveInformationalRespons auto stream = std::make_shared(ioc); client::HttpClientStream::RequestHead request{http::verb::head, "/", 11}; bool done = false; - stream->async_start("http://127.0.0.1:" + std::to_string(port) + "/", std::move(request), - [stream, &done](boost::system::error_code ec) - { - ASSERT_FALSE(ec) << ec.message(); - stream->async_finish_request([stream, &done](boost::system::error_code finish_ec) - { - ASSERT_FALSE(finish_ec) << finish_ec.message(); - stream->async_read_response_head([stream, &done] - (boost::system::error_code head_ec, client::HttpClientStream::ResponseHead head) - { - ASSERT_FALSE(head_ec) << head_ec.message(); - EXPECT_EQ(head.result(), http::status::ok); - EXPECT_EQ(head[http::field::content_length], "1234"); - std::array body{}; - stream->async_read_some(net::buffer(body), [&done] - (boost::system::error_code read_ec, std::size_t size, bool body_done) - { - EXPECT_FALSE(read_ec); - EXPECT_EQ(size, 0u); - EXPECT_TRUE(body_done); - done = true; - }); - }); + stream->async_start( + "http://127.0.0.1:" + std::to_string(port) + "/", std::move(request), + [stream, &done](boost::system::error_code ec) { + ASSERT_FALSE(ec) << ec.message(); + stream->async_finish_request( + [stream, &done](boost::system::error_code finish_ec) { + ASSERT_FALSE(finish_ec) << finish_ec.message(); + stream->async_read_response_head( + [stream, &done](boost::system::error_code head_ec, + client::HttpClientStream::ResponseHead head) { + ASSERT_FALSE(head_ec) << head_ec.message(); + EXPECT_EQ(head.result(), http::status::ok); + EXPECT_EQ(head[http::field::content_length], "1234"); + std::array body{}; + stream->async_read_some( + net::buffer(body), + [&done](boost::system::error_code read_ec, + std::size_t size, bool body_done) { + EXPECT_FALSE(read_ec); + EXPECT_EQ(size, 0u); + EXPECT_TRUE(body_done); + done = true; + }); + }); + }); }); - }); ioc.run(); server.join(); EXPECT_TRUE(done); } -TEST(HttpClientStreamEdgeTest, BufferedClientAlsoHandlesHeadAndInformationalResponses) -{ +TEST(HttpClientStreamEdgeTest, + BufferedClientAlsoHandlesHeadAndInformationalResponses) { net::io_context server_ioc; tcp::acceptor acceptor(server_ioc, {net::ip::address_v4::loopback(), 0}); const auto port = acceptor.local_endpoint().port(); - std::thread server([&] - { + std::thread server([&] { tcp::socket socket(server_ioc); acceptor.accept(socket); boost::beast::flat_buffer request_buffer; http::request request; http::read(socket, request_buffer, request); - const std::string wire = "HTTP/1.1 103 Early Hints\r\n\r\n" - "HTTP/1.1 200 OK\r\nContent-Length: 99\r\nConnection: close\r\n\r\n"; + const std::string wire = + "HTTP/1.1 103 Early Hints\r\n\r\n" + "HTTP/1.1 200 OK\r\nContent-Length: 99\r\nConnection: close\r\n\r\n"; net::write(socket, net::buffer(wire)); }); net::io_context ioc; auto http_client = std::make_shared(ioc); bool done = false; - http_client->request(http::verb::head, "http://127.0.0.1:" + std::to_string(port) + "/", - {}, {}, {}, [&](boost::system::error_code ec, http::response response) - { - EXPECT_FALSE(ec) << ec.message(); - EXPECT_EQ(response.result(), http::status::ok); - EXPECT_EQ(response[http::field::content_length], "99"); - EXPECT_TRUE(response.body().empty()); - done = true; - }); + http_client->request(http::verb::head, + "http://127.0.0.1:" + std::to_string(port) + "/", {}, {}, + {}, + [&](boost::system::error_code ec, + http::response response) { + EXPECT_FALSE(ec) << ec.message(); + EXPECT_EQ(response.result(), http::status::ok); + EXPECT_EQ(response[http::field::content_length], "99"); + EXPECT_TRUE(response.body().empty()); + done = true; + }); ioc.run(); server.join(); EXPECT_TRUE(done); diff --git a/framework/tests/http_context_multipart_edge_test.cpp b/framework/tests/http_context_multipart_edge_test.cpp index 68b4be5..31483ce 100644 --- a/framework/tests/http_context_multipart_edge_test.cpp +++ b/framework/tests/http_context_multipart_edge_test.cpp @@ -1,47 +1,48 @@ -#include "framework/context/http_context.hpp" #include +#include "framework/context/http_context.hpp" + namespace beast = boost::beast; namespace http = beast::http; namespace khttpd_fw = khttpd::framework; template -http::request make_request( - http::verb method, - const std::string& target, - int version = 11, - const std::string& body_str = "") -{ +http::request make_request(http::verb method, const std::string& target, + int version = 11, + const std::string& body_str = "") { http::request req(method, target, version); - if (!body_str.empty()) - { + if (!body_str.empty()) { req.body() = body_str; req.prepare_payload(); } return req; } -class MultipartEdgeTest : public ::testing::Test -{ -}; +class MultipartEdgeTest : public ::testing::Test {}; // Test multiple files uploaded under the same form field name -TEST_F(MultipartEdgeTest, MultipleFilesInSameField) -{ +TEST_F(MultipartEdgeTest, MultipleFilesInSameField) { std::string boundary = "----------Boundary123"; - std::string multipart_body = - "--" + boundary + "\r\n" - "Content-Disposition: form-data; name=\"files\"; filename=\"photo1.jpg\"\r\n" - "Content-Type: image/jpeg\r\n\r\n" - "IMAGE_DATA_1\r\n" - "--" + boundary + "\r\n" - "Content-Disposition: form-data; name=\"files\"; filename=\"photo2.png\"\r\n" - "Content-Type: image/png\r\n\r\n" - "IMAGE_DATA_2\r\n" - "--" + boundary + "--\r\n"; - - http::request req = make_request(http::verb::post, "/upload", 11, multipart_body); - req.set(http::field::content_type, "multipart/form-data; boundary=" + boundary); + std::string multipart_body = "--" + boundary + + "\r\n" + "Content-Disposition: form-data; " + "name=\"files\"; filename=\"photo1.jpg\"\r\n" + "Content-Type: image/jpeg\r\n\r\n" + "IMAGE_DATA_1\r\n" + "--" + + boundary + + "\r\n" + "Content-Disposition: form-data; " + "name=\"files\"; filename=\"photo2.png\"\r\n" + "Content-Type: image/png\r\n\r\n" + "IMAGE_DATA_2\r\n" + "--" + + boundary + "--\r\n"; + + http::request req = + make_request(http::verb::post, "/upload", 11, multipart_body); + req.set(http::field::content_type, + "multipart/form-data; boundary=" + boundary); http::response res; khttpd_fw::HttpContext ctx(req, res); @@ -59,20 +60,25 @@ TEST_F(MultipartEdgeTest, MultipleFilesInSameField) } // Test that same form field name results in last value winning (overwrite) -TEST_F(MultipartEdgeTest, FormFieldOverwrite) -{ +TEST_F(MultipartEdgeTest, FormFieldOverwrite) { std::string boundary = "----------Boundary456"; std::string multipart_body = - "--" + boundary + "\r\n" - "Content-Disposition: form-data; name=\"username\"\r\n\r\n" - "first_value\r\n" - "--" + boundary + "\r\n" - "Content-Disposition: form-data; name=\"username\"\r\n\r\n" - "second_value\r\n" - "--" + boundary + "--\r\n"; - - http::request req = make_request(http::verb::post, "/form", 11, multipart_body); - req.set(http::field::content_type, "multipart/form-data; boundary=" + boundary); + "--" + boundary + + "\r\n" + "Content-Disposition: form-data; name=\"username\"\r\n\r\n" + "first_value\r\n" + "--" + + boundary + + "\r\n" + "Content-Disposition: form-data; name=\"username\"\r\n\r\n" + "second_value\r\n" + "--" + + boundary + "--\r\n"; + + http::request req = + make_request(http::verb::post, "/form", 11, multipart_body); + req.set(http::field::content_type, + "multipart/form-data; boundary=" + boundary); http::response res; khttpd_fw::HttpContext ctx(req, res); @@ -82,14 +88,15 @@ TEST_F(MultipartEdgeTest, FormFieldOverwrite) } // Test multipart body without proper boundary - should not crash -TEST_F(MultipartEdgeTest, MissingBoundary) -{ +TEST_F(MultipartEdgeTest, MissingBoundary) { // Body that claims to be multipart but has no proper boundary markers std::string boundary = "----------NonExistent"; std::string body = "This is not a valid multipart body at all"; - http::request req = make_request(http::verb::post, "/upload", 11, body); - req.set(http::field::content_type, "multipart/form-data; boundary=" + boundary); + http::request req = + make_request(http::verb::post, "/upload", 11, body); + req.set(http::field::content_type, + "multipart/form-data; boundary=" + boundary); http::response res; khttpd_fw::HttpContext ctx(req, res); @@ -100,17 +107,20 @@ TEST_F(MultipartEdgeTest, MissingBoundary) ASSERT_FALSE(ctx.get_multipart_field("any_field").has_value()); } -TEST_F(MultipartEdgeTest, QuotedBoundary) -{ +TEST_F(MultipartEdgeTest, QuotedBoundary) { std::string boundary = "----QuotedBoundary"; std::string multipart_body = - "--" + boundary + "\r\n" - "Content-Disposition: form-data; name=\"field\"\r\n\r\n" - "quoted boundary works\r\n" - "--" + boundary + "--\r\n"; - - http::request req = make_request(http::verb::post, "/form", 11, multipart_body); - req.set(http::field::content_type, "multipart/form-data; boundary=\"" + boundary + "\""); + "--" + boundary + + "\r\n" + "Content-Disposition: form-data; name=\"field\"\r\n\r\n" + "quoted boundary works\r\n" + "--" + + boundary + "--\r\n"; + + http::request req = + make_request(http::verb::post, "/form", 11, multipart_body); + req.set(http::field::content_type, + "multipart/form-data; boundary=\"" + boundary + "\""); http::response res; khttpd_fw::HttpContext ctx(req, res); @@ -118,19 +128,23 @@ TEST_F(MultipartEdgeTest, QuotedBoundary) ASSERT_EQ(ctx.get_multipart_field("field").value(), "quoted boundary works"); } -TEST_F(MultipartEdgeTest, BoundaryTextInsideFileDataIsPreserved) -{ +TEST_F(MultipartEdgeTest, BoundaryTextInsideFileDataIsPreserved) { std::string boundary = "----BoundaryInData"; std::string file_data = "line one --" + boundary + " is not a delimiter"; std::string multipart_body = - "--" + boundary + "\r\n" - "Content-Disposition: form-data; name=\"file\"; filename=\"data.txt\"\r\n" - "Content-Type: text/plain\r\n\r\n" + - file_data + "\r\n" - "--" + boundary + "--\r\n"; - - http::request req = make_request(http::verb::post, "/upload", 11, multipart_body); - req.set(http::field::content_type, "multipart/form-data; boundary=" + boundary); + "--" + boundary + + "\r\n" + "Content-Disposition: form-data; name=\"file\"; filename=\"data.txt\"\r\n" + "Content-Type: text/plain\r\n\r\n" + + file_data + + "\r\n" + "--" + + boundary + "--\r\n"; + + http::request req = + make_request(http::verb::post, "/upload", 11, multipart_body); + req.set(http::field::content_type, + "multipart/form-data; boundary=" + boundary); http::response res; khttpd_fw::HttpContext ctx(req, res); diff --git a/framework/tests/http_proxy_session_edge_test.cpp b/framework/tests/http_proxy_session_edge_test.cpp index 153a1c5..d63e692 100644 --- a/framework/tests/http_proxy_session_edge_test.cpp +++ b/framework/tests/http_proxy_session_edge_test.cpp @@ -1,74 +1,77 @@ -#include "framework/client/http_proxy_session.hpp" - #include + #include +#include "framework/client/http_proxy_session.hpp" + namespace fw = khttpd::framework; namespace client = khttpd::framework::client; namespace http = boost::beast::http; namespace net = boost::asio; -namespace -{ - class ObservableRequestStream final : public fw::HttpRequestStream - { - public: - bool read = false; - bool cancelled = false; +namespace { +class ObservableRequestStream final : public fw::HttpRequestStream { + public: + bool read = false; + bool cancelled = false; - void async_read_some(net::mutable_buffer, ReadCallback callback) override - { - read = true; - callback({}, 0, true); - } - void cancel() override { cancelled = true; } - }; + void async_read_some(net::mutable_buffer, ReadCallback callback) override { + read = true; + callback({}, 0, true); + } + void cancel() override { cancelled = true; } +}; - class ObservableResponseStream final : public fw::HttpResponseStream - { - public: - bool started = false; - bool cancelled = false; +class ObservableResponseStream final : public fw::HttpResponseStream { + public: + bool started = false; + bool cancelled = false; - void async_start(ResponseHead, Callback callback) override - { - started = true; - callback({}); - } - void async_write_some(net::const_buffer, Callback callback) override { callback({}); } - void async_finish(Callback callback) override { callback({}); } - void cancel() override { cancelled = true; } - }; -} + void async_start(ResponseHead, Callback callback) override { + started = true; + callback({}); + } + void async_write_some(net::const_buffer, Callback callback) override { + callback({}); + } + void async_finish(Callback callback) override { callback({}); } + void cancel() override { cancelled = true; } +}; +} // namespace -TEST(HttpProxySessionEdgeTest, UnsupportedUpstreamSchemeCancelsBothDownstreamSides) -{ +TEST(HttpProxySessionEdgeTest, + UnsupportedUpstreamSchemeCancelsBothDownstreamSides) { net::io_context ioc; auto inbound = std::make_shared(); auto downstream = std::make_shared(); - auto proxy = std::make_shared(ioc, inbound, downstream, 17); + auto proxy = + std::make_shared(ioc, inbound, downstream, 17); client::HttpClientStream::RequestHead head{http::verb::post, "/", 11}; boost::system::error_code result; bool completed = false; proxy->start("ftp://unsupported.test/upload", std::move(head), - [&](boost::system::error_code ec) { result = ec; completed = true; }); + [&](boost::system::error_code ec) { + result = ec; + completed = true; + }); ioc.run(); EXPECT_TRUE(completed); - EXPECT_EQ(result, make_error_code(boost::system::errc::operation_not_supported)); + EXPECT_EQ(result, + make_error_code(boost::system::errc::operation_not_supported)); EXPECT_TRUE(inbound->cancelled); EXPECT_TRUE(downstream->cancelled); EXPECT_FALSE(inbound->read); EXPECT_FALSE(downstream->started); } -TEST(HttpProxySessionEdgeTest, ExplicitCancelCancelsInboundAndDownstream) -{ +TEST(HttpProxySessionEdgeTest, ExplicitCancelCancelsInboundAndDownstream) { net::io_context ioc; auto inbound = std::make_shared(); auto downstream = std::make_shared(); - auto proxy = std::make_shared(ioc, inbound, downstream); + auto proxy = + std::make_shared(ioc, inbound, downstream); proxy->cancel(); diff --git a/framework/tests/http_session_test_harness.hpp b/framework/tests/http_session_test_harness.hpp index acdb96f..3d883d8 100644 --- a/framework/tests/http_session_test_harness.hpp +++ b/framework/tests/http_session_test_harness.hpp @@ -1,78 +1,75 @@ #ifndef KHTTPD_FRAMEWORK_TESTS_HTTP_SESSION_TEST_HARNESS_HPP #define KHTTPD_FRAMEWORK_TESTS_HTTP_SESSION_TEST_HARNESS_HPP -#include "framework/router/http_router.hpp" -#include "framework/router/websocket_router.hpp" -#include "framework/session/http_session.hpp" - #include #include #include #include #include -namespace khttpd::framework::tests -{ - namespace beast = boost::beast; - namespace http = beast::http; - namespace net = boost::asio; - namespace fs = boost::filesystem; - using tcp = net::ip::tcp; +#include "framework/router/http_router.hpp" +#include "framework/router/websocket_router.hpp" +#include "framework/session/http_session.hpp" + +namespace khttpd::framework::tests { +namespace beast = boost::beast; +namespace http = beast::http; +namespace net = boost::asio; +namespace fs = boost::filesystem; +using tcp = net::ip::tcp; - struct TempWebRoot - { - fs::path path = fs::temp_directory_path() / fs::unique_path("khttpd-http-test-%%%%-%%%%"); +struct TempWebRoot { + fs::path path = + fs::temp_directory_path() / fs::unique_path("khttpd-http-test-%%%%-%%%%"); - TempWebRoot() { fs::create_directories(path); } - ~TempWebRoot() - { - boost::system::error_code ec; - fs::remove_all(path, ec); - } - }; + TempWebRoot() { fs::create_directories(path); } + ~TempWebRoot() { + boost::system::error_code ec; + fs::remove_all(path, ec); + } +}; - template - http::response round_trip(HttpRouter& router, WebsocketRouter& websocket_router, - const fs::path& web_root, - http::request request, - std::uint64_t max_buffered_body = - HttpSession::default_max_buffered_request_body_size) - { - net::io_context server_ioc; - auto guard = net::make_work_guard(server_ioc); - tcp::acceptor acceptor(server_ioc, {net::ip::make_address("127.0.0.1"), 0}); - const auto endpoint = acceptor.local_endpoint(); - const auto canonical_web_root = fs::canonical(web_root); +template +http::response round_trip( + HttpRouter& router, WebsocketRouter& websocket_router, + const fs::path& web_root, http::request request, + std::uint64_t max_buffered_body = + HttpSession::default_max_buffered_request_body_size) { + net::io_context server_ioc; + auto guard = net::make_work_guard(server_ioc); + tcp::acceptor acceptor(server_ioc, {net::ip::make_address("127.0.0.1"), 0}); + const auto endpoint = acceptor.local_endpoint(); + const auto canonical_web_root = fs::canonical(web_root); - acceptor.async_accept([&](beast::error_code ec, tcp::socket socket) - { - if (ec) return; - std::make_shared(std::move(socket), router, websocket_router, - web_root.string(), canonical_web_root, - max_buffered_body)->run(); - }); - std::thread server_thread([&] { server_ioc.run(); }); + acceptor.async_accept([&](beast::error_code ec, tcp::socket socket) { + if (ec) return; + std::make_shared(std::move(socket), router, websocket_router, + web_root.string(), canonical_web_root, + max_buffered_body) + ->run(); + }); + std::thread server_thread([&] { server_ioc.run(); }); - net::io_context client_ioc; - tcp::socket client(client_ioc); - client.connect(endpoint); - beast::error_code ec; - http::write(client, request, ec); - if (ec) throw boost::system::system_error(ec); + net::io_context client_ioc; + tcp::socket client(client_ioc); + client.connect(endpoint); + beast::error_code ec; + http::write(client, request, ec); + if (ec) throw boost::system::system_error(ec); - beast::flat_buffer buffer; - http::response_parser parser; - http::read(client, buffer, parser, ec); - if (ec) throw boost::system::system_error(ec); - auto response = parser.release(); + beast::flat_buffer buffer; + http::response_parser parser; + http::read(client, buffer, parser, ec); + if (ec) throw boost::system::system_error(ec); + auto response = parser.release(); - beast::error_code ignored; - client.close(ignored); - guard.reset(); - server_ioc.stop(); - server_thread.join(); - return response; - } + beast::error_code ignored; + client.close(ignored); + guard.reset(); + server_ioc.stop(); + server_thread.join(); + return response; } +} // namespace khttpd::framework::tests #endif diff --git a/framework/tests/http_stream_edge_test.cpp b/framework/tests/http_stream_edge_test.cpp index 3b375f0..dac2273 100644 --- a/framework/tests/http_stream_edge_test.cpp +++ b/framework/tests/http_stream_edge_test.cpp @@ -1,23 +1,24 @@ -#include "http_session_test_harness.hpp" - #include + #include +#include "http_session_test_harness.hpp" + namespace fw = khttpd::framework; namespace test = khttpd::framework::tests; namespace http = boost::beast::http; namespace net = boost::asio; -namespace -{ - void install_counting_stream(fw::HttpRouter& router, std::size_t read_buffer_size = 5) - { - router.stream("/stream", http::verb::post, - [read_buffer_size](fw::HttpContext& ctx, std::shared_ptr request, - std::shared_ptr, fw::HttpStreamComplete complete) - { - struct State - { +namespace { +void install_counting_stream(fw::HttpRouter& router, + std::size_t read_buffer_size = 5) { + router.stream( + "/stream", http::verb::post, + [read_buffer_size](fw::HttpContext& ctx, + std::shared_ptr request, + std::shared_ptr, + fw::HttpStreamComplete complete) { + struct State { std::vector buffer; std::size_t total = 0; fw::HttpContext* ctx; @@ -30,88 +31,91 @@ namespace state->ctx = &ctx; state->request = std::move(request); state->complete = std::move(complete); - state->read = [state] - { - state->request->async_read_some(net::buffer(state->buffer), - [state](boost::system::error_code ec, std::size_t size, bool done) - { - ASSERT_FALSE(ec) << ec.message(); - state->total += size; - if (!done) return state->read(); - state->ctx->set_body(std::to_string(state->total)); - state->complete(); - }); + state->read = [state] { + state->request->async_read_some( + net::buffer(state->buffer), [state](boost::system::error_code ec, + std::size_t size, bool done) { + ASSERT_FALSE(ec) << ec.message(); + state->total += size; + if (!done) return state->read(); + state->ctx->set_body(std::to_string(state->total)); + state->complete(); + }); }; state->read(); }); - } +} - http::response send_stream(std::string body, bool chunked, - std::uint64_t buffered_limit = 0) - { - test::TempWebRoot web_root; - fw::HttpRouter router; - fw::WebsocketRouter websocket_router; - install_counting_stream(router); - http::request request{http::verb::post, "/stream", 11}; - request.body() = std::move(body); - if (chunked) request.chunked(true); else request.prepare_payload(); - request.keep_alive(false); - return test::round_trip(router, websocket_router, web_root.path, std::move(request), buffered_limit); - } +http::response send_stream( + std::string body, bool chunked, std::uint64_t buffered_limit = 0) { + test::TempWebRoot web_root; + fw::HttpRouter router; + fw::WebsocketRouter websocket_router; + install_counting_stream(router); + http::request request{http::verb::post, "/stream", 11}; + request.body() = std::move(body); + if (chunked) + request.chunked(true); + else + request.prepare_payload(); + request.keep_alive(false); + return test::round_trip(router, websocket_router, web_root.path, + std::move(request), buffered_limit); } +} // namespace -TEST(HttpStreamEdgeTest, EmptyContentLengthBodyCompletesWithoutReadData) -{ +TEST(HttpStreamEdgeTest, EmptyContentLengthBodyCompletesWithoutReadData) { const auto response = send_stream({}, false); EXPECT_EQ(response.result(), http::status::ok); EXPECT_EQ(response.body(), "0"); } -TEST(HttpStreamEdgeTest, StreamRouteBypassesZeroBufferedBodyLimit) -{ +TEST(HttpStreamEdgeTest, StreamRouteBypassesZeroBufferedBodyLimit) { const auto response = send_stream(std::string(4097, 's'), false, 0); EXPECT_EQ(response.result(), http::status::ok); EXPECT_EQ(response.body(), "4097"); } -TEST(HttpStreamEdgeTest, ChunkedBodyCrossingManySmallReadsPreservesByteCount) -{ +TEST(HttpStreamEdgeTest, ChunkedBodyCrossingManySmallReadsPreservesByteCount) { const auto response = send_stream(std::string(103, 'c'), true, 1); EXPECT_EQ(response.result(), http::status::ok); EXPECT_EQ(response.body(), "103"); } -TEST(HttpStreamEdgeTest, CancellingRequestBodyKeepsResponseChannelUsable) -{ +TEST(HttpStreamEdgeTest, CancellingRequestBodyKeepsResponseChannelUsable) { test::TempWebRoot web_root; fw::HttpRouter router; fw::WebsocketRouter websocket_router; - router.stream("/early", http::verb::post, - [](fw::HttpContext&, std::shared_ptr, - std::shared_ptr response, fw::HttpStreamComplete) - { - response->cancel_request_body(); - fw::HttpResponseStream::ResponseHead head{http::status::forbidden, 11}; - head.content_length(6); - head.keep_alive(false); - response->async_start(std::move(head), [response](boost::system::error_code ec) - { - ASSERT_FALSE(ec) << ec.message(); - static constexpr char denied[] = "denied"; - response->async_write_some(net::buffer(denied, 6), [response](boost::system::error_code write_ec) - { - ASSERT_FALSE(write_ec) << write_ec.message(); - response->async_finish([](boost::system::error_code finish_ec) - { ASSERT_FALSE(finish_ec) << finish_ec.message(); }); - }); + router.stream( + "/early", http::verb::post, + [](fw::HttpContext&, std::shared_ptr, + std::shared_ptr response, + fw::HttpStreamComplete) { + response->cancel_request_body(); + fw::HttpResponseStream::ResponseHead head{http::status::forbidden, 11}; + head.content_length(6); + head.keep_alive(false); + response->async_start( + std::move(head), [response](boost::system::error_code ec) { + ASSERT_FALSE(ec) << ec.message(); + static constexpr char denied[] = "denied"; + response->async_write_some( + net::buffer(denied, 6), + [response](boost::system::error_code write_ec) { + ASSERT_FALSE(write_ec) << write_ec.message(); + response->async_finish( + [](boost::system::error_code finish_ec) { + ASSERT_FALSE(finish_ec) << finish_ec.message(); + }); + }); + }); }); - }); http::request request{http::verb::post, "/early", 11}; request.body().assign(4096, 'x'); request.prepare_payload(); request.keep_alive(false); - const auto response = test::round_trip(router, websocket_router, web_root.path, std::move(request), 0); + const auto response = test::round_trip(router, websocket_router, + web_root.path, std::move(request), 0); EXPECT_EQ(response.result(), http::status::forbidden); EXPECT_EQ(response.body(), "denied"); } diff --git a/framework/tests/interceptor_test.cpp b/framework/tests/interceptor_test.cpp index 4455a12..a3c9094 100644 --- a/framework/tests/interceptor_test.cpp +++ b/framework/tests/interceptor_test.cpp @@ -1,20 +1,21 @@ -#include -#include "framework/router/http_router.hpp" -#include "framework/context/http_context.hpp" #include "framework/interceptor/interceptor.hpp" + +#include + #include #include +#include "framework/context/http_context.hpp" +#include "framework/router/http_router.hpp" + using namespace khttpd::framework; // Mock objects for HttpContext // Since HttpContext constructor requires Request and Response objects // We need to construct them. -class InterceptorTest : public ::testing::Test -{ -protected: - void SetUp() override - { +class InterceptorTest : public ::testing::Test { + protected: + void SetUp() override { req.version(11); req.method(boost::beast::http::verb::get); req.target("/test"); @@ -32,38 +33,31 @@ class InterceptorTest : public ::testing::Test }; // Test implementation of Interceptor -class TestInterceptor : public Interceptor -{ -public: +class TestInterceptor : public Interceptor { + public: InterceptorResult handle_request_result = InterceptorResult::Continue; bool handle_request_called = false; bool handle_response_called = false; std::string request_attribute_key; std::string request_attribute_value; - InterceptorResult handle_request(HttpContext& ctx) override - { + InterceptorResult handle_request(HttpContext& ctx) override { handle_request_called = true; - if (!request_attribute_key.empty()) - { + if (!request_attribute_key.empty()) { ctx.set_attribute(request_attribute_key, request_attribute_value); } return handle_request_result; } - void handle_response(HttpContext& ctx) override - { + void handle_response(HttpContext& ctx) override { handle_response_called = true; // Modify response header to prove it ran ctx.set_header("X-Test-Interceptor", "Executed"); } }; -TEST_F(InterceptorTest, DefaultMethodsDoNothing) -{ - class DefaultInterceptor : public Interceptor - { - }; +TEST_F(InterceptorTest, DefaultMethodsDoNothing) { + class DefaultInterceptor : public Interceptor {}; auto interceptor = std::make_shared(); // Should return Continue by default @@ -73,8 +67,7 @@ TEST_F(InterceptorTest, DefaultMethodsDoNothing) interceptor->handle_response(*ctx); } -TEST_F(InterceptorTest, PreInterceptorPassThrough) -{ +TEST_F(InterceptorTest, PreInterceptorPassThrough) { auto interceptor = std::make_shared(); router->add_interceptor(interceptor); @@ -85,8 +78,7 @@ TEST_F(InterceptorTest, PreInterceptorPassThrough) EXPECT_TRUE(interceptor->handle_request_called); } -TEST_F(InterceptorTest, PreInterceptorStop) -{ +TEST_F(InterceptorTest, PreInterceptorStop) { auto interceptor = std::make_shared(); interceptor->handle_request_result = InterceptorResult::Stop; router->add_interceptor(interceptor); @@ -98,8 +90,7 @@ TEST_F(InterceptorTest, PreInterceptorStop) EXPECT_TRUE(interceptor->handle_request_called); } -TEST_F(InterceptorTest, MultipleInterceptorsSequence) -{ +TEST_F(InterceptorTest, MultipleInterceptorsSequence) { auto i1 = std::make_shared(); auto i2 = std::make_shared(); @@ -113,8 +104,7 @@ TEST_F(InterceptorTest, MultipleInterceptorsSequence) EXPECT_TRUE(i2->handle_request_called); } -TEST_F(InterceptorTest, MultipleInterceptorsStopMiddle) -{ +TEST_F(InterceptorTest, MultipleInterceptorsStopMiddle) { auto i1 = std::make_shared(); auto i2 = std::make_shared(); auto i3 = std::make_shared(); @@ -130,11 +120,10 @@ TEST_F(InterceptorTest, MultipleInterceptorsStopMiddle) EXPECT_EQ(result, InterceptorResult::Stop); EXPECT_TRUE(i1->handle_request_called); EXPECT_TRUE(i2->handle_request_called); - EXPECT_FALSE(i3->handle_request_called); // Should not be called + EXPECT_FALSE(i3->handle_request_called); // Should not be called } -TEST_F(InterceptorTest, PostInterceptorsRun) -{ +TEST_F(InterceptorTest, PostInterceptorsRun) { auto i1 = std::make_shared(); router->add_interceptor(i1); @@ -146,29 +135,22 @@ TEST_F(InterceptorTest, PostInterceptorsRun) EXPECT_EQ(header->value(), "Executed"); } -TEST_F(InterceptorTest, PostInterceptorsOrder) -{ - // Post interceptors typically run in reverse order or same order depending on implementation. - // In HttpRouter::run_post_interceptors implementation: - // for (auto it = interceptors_.rbegin(); it != interceptors_.rend(); ++it) - // It runs in reverse order of addition. +TEST_F(InterceptorTest, PostInterceptorsOrder) { + // Post interceptors typically run in reverse order or same order depending on + // implementation. In HttpRouter::run_post_interceptors implementation: for + // (auto it = interceptors_.rbegin(); it != interceptors_.rend(); ++it) It + // runs in reverse order of addition. std::vector order; - class OrderingInterceptor : public Interceptor - { - public: + class OrderingInterceptor : public Interceptor { + public: int id; std::vector& order_ref; - OrderingInterceptor(int i, std::vector& o) : id(i), order_ref(o) - { - } + OrderingInterceptor(int i, std::vector& o) : id(i), order_ref(o) {} - void handle_response(HttpContext& ctx) override - { - order_ref.push_back(id); - } + void handle_response(HttpContext& ctx) override { order_ref.push_back(id); } }; auto i1 = std::make_shared(1, order); @@ -185,8 +167,7 @@ TEST_F(InterceptorTest, PostInterceptorsOrder) EXPECT_EQ(order[1], 1); } -TEST_F(InterceptorTest, ContextDataPassing) -{ +TEST_F(InterceptorTest, ContextDataPassing) { auto i1 = std::make_shared(); i1->request_attribute_key = "user_id"; i1->request_attribute_value = "12345"; diff --git a/framework/tests/openapi_test.cpp b/framework/tests/openapi_test.cpp index a5f8d3b..a782d0a 100644 --- a/framework/tests/openapi_test.cpp +++ b/framework/tests/openapi_test.cpp @@ -1,8 +1,9 @@ -#include "framework/router/http_router.hpp" -#include "framework/router/http_result.hpp" #include "framework/router/openapi.hpp" #include + +#include "framework/router/http_result.hpp" +#include "framework/router/http_router.hpp" #if defined(__clang__) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" @@ -13,13 +14,13 @@ #pragma GCC diagnostic ignored "-Wvariadic-macros" #pragma GCC diagnostic ignored "-Wpedantic" #endif -#include -#include #include -#include +#include +#include #include #include +#include #include #include #include @@ -29,50 +30,42 @@ namespace fw = khttpd::framework; namespace http = boost::beast::http; -struct OpenApiCreatePetRequest -{ +struct OpenApiCreatePetRequest { std::string name; int age; std::optional nickname; }; -struct OpenApiPetResponse -{ +struct OpenApiPetResponse { int id; std::string name; std::vector tags; }; -struct OpenApiOpaquePayload -{ +struct OpenApiOpaquePayload { int value; }; -struct OpenApiMessageRequest -{ +struct OpenApiMessageRequest { std::string message; }; -struct OpenApiMessageResponse -{ +struct OpenApiMessageResponse { std::string message; }; -struct OpenApiDescribedFieldsRequest -{ +struct OpenApiDescribedFieldsRequest { std::string token; std::vector permissions; }; OpenApiOpaquePayload tag_invoke(boost::json::value_to_tag, - const boost::json::value& value) -{ + const boost::json::value& value) { return {boost::json::value_to(value.as_object().at("value"))}; } void tag_invoke(boost::json::value_from_tag, boost::json::value& value, - const OpenApiOpaquePayload& payload) -{ + const OpenApiOpaquePayload& payload) { value = {{"value", payload.value}}; } @@ -82,9 +75,11 @@ BOOST_DESCRIBE_STRUCT(OpenApiMessageRequest, (), (message)) BOOST_DESCRIBE_STRUCT(OpenApiMessageResponse, (), (message)) BOOST_DESCRIBE_STRUCT(OpenApiDescribedFieldsRequest, (), (token, permissions)) -KHTTPD_OPENAPI_FIELD_DOCUMENTATION(OpenApiDescribedFieldsRequest, - KHTTPD_OPENAPI_FIELD(token, "API token to authorize.") - KHTTPD_OPENAPI_FIELD(permissions, "Permissions required by the caller.")) +KHTTPD_OPENAPI_FIELD_DOCUMENTATION( + OpenApiDescribedFieldsRequest, + KHTTPD_OPENAPI_FIELD(token, "API token to authorize.") + KHTTPD_OPENAPI_FIELD(permissions, + "Permissions required by the caller.")) #if defined(__clang__) #pragma clang diagnostic pop @@ -92,32 +87,32 @@ KHTTPD_OPENAPI_FIELD_DOCUMENTATION(OpenApiDescribedFieldsRequest, #pragma GCC diagnostic pop #endif -namespace -{ - const boost::json::object& operation_at(const boost::json::object& document, - const std::string& path, - const std::string& method) - { - return document.at("paths").as_object().at(path).as_object().at(method).as_object(); - } +namespace { +const boost::json::object& operation_at(const boost::json::object& document, + const std::string& path, + const std::string& method) { + return document.at("paths") + .as_object() + .at(path) + .as_object() + .at(method) + .as_object(); +} - std::string test_output_path(const std::string& filename) - { - const auto* directory = std::getenv("TEST_TMPDIR"); - return std::string(directory ? directory : "/tmp") + "/" + filename; - } +std::string test_output_path(const std::string& filename) { + const auto* directory = std::getenv("TEST_TMPDIR"); + return std::string(directory ? directory : "/tmp") + "/" + filename; +} - std::string read_file(const std::string& path) - { - std::ifstream input(path, std::ios::binary); - std::ostringstream contents; - contents << input.rdbuf(); - return contents.str(); - } +std::string read_file(const std::string& path) { + std::ifstream input(path, std::ios::binary); + std::ostringstream contents; + contents << input.rdbuf(); + return contents.str(); } +} // namespace -TEST(OpenApiTest, IncludesLegacyMethodsAndPathParameters) -{ +TEST(OpenApiTest, IncludesLegacyMethodsAndPathParameters) { fw::HttpRouter router; router.get("/users/:id", [](fw::HttpContext&) {}); router.post("/users/:id", [](fw::HttpContext&) {}); @@ -127,11 +122,13 @@ TEST(OpenApiTest, IncludesLegacyMethodsAndPathParameters) EXPECT_EQ(document.at("openapi"), "3.1.0"); EXPECT_EQ(document.at("info").as_object().at("title"), "Accounts API"); EXPECT_EQ(document.at("info").as_object().at("version"), "9.2.0"); - const auto& path = document.at("paths").as_object().at("/users/{id}").as_object(); + const auto& path = + document.at("paths").as_object().at("/users/{id}").as_object(); EXPECT_TRUE(path.contains("get")); EXPECT_TRUE(path.contains("post")); - const auto& parameters = path.at("get").as_object().at("parameters").as_array(); + const auto& parameters = + path.at("get").as_object().at("parameters").as_array(); ASSERT_EQ(parameters.size(), 1U); const auto& parameter = parameters.front().as_object(); EXPECT_EQ(parameter.at("name"), "id"); @@ -141,18 +138,17 @@ TEST(OpenApiTest, IncludesLegacyMethodsAndPathParameters) EXPECT_EQ(parameter.at("x-khttpd-greedy"), true); } -TEST(OpenApiTest, NormalizesColonAndBracePathParameters) -{ +TEST(OpenApiTest, NormalizesColonAndBracePathParameters) { fw::HttpRouter router; router.get("/teams/{team_id}/users/:user_id", [](fw::HttpContext&) {}); const auto document = fw::generate_openapi(router); - const auto& operation = operation_at(document, "/teams/{team_id}/users/{user_id}", "get"); + const auto& operation = + operation_at(document, "/teams/{team_id}/users/{user_id}", "get"); const auto& parameters = operation.at("parameters").as_array(); ASSERT_EQ(parameters.size(), 2U); - for (std::size_t index = 0; index < parameters.size(); ++index) - { + for (std::size_t index = 0; index < parameters.size(); ++index) { const auto& parameter = parameters.at(index).as_object(); EXPECT_EQ(parameter.at("in"), "path"); EXPECT_EQ(parameter.at("required"), true); @@ -162,63 +158,78 @@ TEST(OpenApiTest, NormalizesColonAndBracePathParameters) EXPECT_EQ(parameters.at(1).as_object().at("name"), "user_id"); } -TEST(OpenApiTest, DocumentsTypedRouteParameterDescriptors) -{ +TEST(OpenApiTest, DocumentsTypedRouteParameterDescriptors) { fw::HttpRouter router; fw::RouteDocumentation documentation; documentation.summary = "Update a pet"; documentation.description = "Updates a pet using bound route parameters."; - router.put("/pets/{pet_id}", - [](const int pet_id, const OpenApiCreatePetRequest& request, const bool notify, - const std::optional& tag) - { - return OpenApiPetResponse{pet_id, notify ? request.name : tag.value_or(request.name), {}}; - }, - documentation, - fw::PathParam{"pet_id"}, - fw::Body{}, - fw::QueryParam{"notify", false}, - fw::QueryParam>{"tag"}); + router.put( + "/pets/{pet_id}", + [](const int pet_id, const OpenApiCreatePetRequest& request, + const bool notify, const std::optional& tag) { + return OpenApiPetResponse{ + pet_id, notify ? request.name : tag.value_or(request.name), {}}; + }, + documentation, fw::PathParam{"pet_id"}, + fw::Body{}, + fw::QueryParam{"notify", false}, + fw::QueryParam>{"tag"}); const auto document = fw::generate_openapi(router); const auto& operation = operation_at(document, "/pets/{pet_id}", "put"); EXPECT_EQ(operation.at("summary"), "Update a pet"); - EXPECT_EQ(operation.at("description"), "Updates a pet using bound route parameters."); + EXPECT_EQ(operation.at("description"), + "Updates a pet using bound route parameters."); const auto& parameters = operation.at("parameters").as_array(); ASSERT_EQ(parameters.size(), 3U); EXPECT_EQ(parameters.at(0).as_object().at("name"), "pet_id"); EXPECT_EQ(parameters.at(0).as_object().at("in"), "path"); EXPECT_EQ(parameters.at(0).as_object().at("required"), true); - EXPECT_EQ(parameters.at(0).as_object().at("schema").as_object().at("type"), "integer"); + EXPECT_EQ(parameters.at(0).as_object().at("schema").as_object().at("type"), + "integer"); EXPECT_EQ(parameters.at(1).as_object().at("name"), "notify"); EXPECT_EQ(parameters.at(1).as_object().at("in"), "query"); EXPECT_EQ(parameters.at(1).as_object().at("required"), false); - EXPECT_EQ(parameters.at(1).as_object().at("schema").as_object().at("type"), "boolean"); - EXPECT_EQ(parameters.at(1).as_object().at("schema").as_object().at("default"), false); + EXPECT_EQ(parameters.at(1).as_object().at("schema").as_object().at("type"), + "boolean"); + EXPECT_EQ(parameters.at(1).as_object().at("schema").as_object().at("default"), + false); EXPECT_EQ(parameters.at(2).as_object().at("name"), "tag"); EXPECT_EQ(parameters.at(2).as_object().at("in"), "query"); EXPECT_EQ(parameters.at(2).as_object().at("required"), false); - EXPECT_EQ(parameters.at(2).as_object().at("schema").as_object().at("type"), "string"); - - const auto& request_schema = operation.at("requestBody").as_object().at("content").as_object() - .at("application/json").as_object().at("schema").as_object(); + EXPECT_EQ(parameters.at(2).as_object().at("schema").as_object().at("type"), + "string"); + + const auto& request_schema = operation.at("requestBody") + .as_object() + .at("content") + .as_object() + .at("application/json") + .as_object() + .at("schema") + .as_object(); EXPECT_EQ(request_schema.at("type"), "object"); EXPECT_TRUE(request_schema.at("properties").as_object().contains("name")); } -TEST(OpenApiTest, IncludesTypedDescribeSchemas) -{ +TEST(OpenApiTest, IncludesTypedDescribeSchemas) { fw::HttpRouter router; - router.post("/pets", [](const OpenApiCreatePetRequest& request) - { - return fw::HttpResult::created({7, request.name, {"new"}}); + router.post("/pets", [](const OpenApiCreatePetRequest& request) { + return fw::HttpResult::created( + {7, request.name, {"new"}}); }); const auto document = fw::generate_openapi(router); const auto& operation = operation_at(document, "/pets", "post"); - const auto& request_schema = operation.at("requestBody").as_object() - .at("content").as_object().at("application/json").as_object().at("schema").as_object(); + const auto& request_schema = operation.at("requestBody") + .as_object() + .at("content") + .as_object() + .at("application/json") + .as_object() + .at("schema") + .as_object(); EXPECT_EQ(request_schema.at("type"), "object"); const auto& request_properties = request_schema.at("properties").as_object(); EXPECT_EQ(request_properties.at("name").as_object().at("type"), "string"); @@ -229,67 +240,133 @@ TEST(OpenApiTest, IncludesTypedDescribeSchemas) EXPECT_EQ(required[0], "name"); EXPECT_EQ(required[1], "age"); - const auto& response_schema = operation.at("responses").as_object().at("200").as_object() - .at("content").as_object().at("application/json").as_object().at("schema").as_object(); + const auto& response_schema = operation.at("responses") + .as_object() + .at("200") + .as_object() + .at("content") + .as_object() + .at("application/json") + .as_object() + .at("schema") + .as_object(); EXPECT_EQ(response_schema.at("type"), "object"); - EXPECT_EQ(response_schema.at("properties").as_object().at("id").as_object().at("type"), "integer"); - const auto& tags = response_schema.at("properties").as_object().at("tags").as_object(); + EXPECT_EQ(response_schema.at("properties") + .as_object() + .at("id") + .as_object() + .at("type"), + "integer"); + const auto& tags = + response_schema.at("properties").as_object().at("tags").as_object(); EXPECT_EQ(tags.at("type"), "array"); EXPECT_EQ(tags.at("items").as_object().at("type"), "string"); } -TEST(OpenApiTest, IncludesFieldDescriptionsForTypedDtos) -{ +TEST(OpenApiTest, IncludesFieldDescriptionsForTypedDtos) { fw::HttpRouter router; - router.post("/field-descriptions", [](const OpenApiDescribedFieldsRequest&) { return true; }); + router.post("/field-descriptions", + [](const OpenApiDescribedFieldsRequest&) { return true; }); const auto document = fw::generate_openapi(router); - const auto& request_schema = operation_at(document, "/field-descriptions", "post") - .at("requestBody").as_object().at("content").as_object().at("application/json").as_object() - .at("schema").as_object(); + const auto& request_schema = + operation_at(document, "/field-descriptions", "post") + .at("requestBody") + .as_object() + .at("content") + .as_object() + .at("application/json") + .as_object() + .at("schema") + .as_object(); const auto& properties = request_schema.at("properties").as_object(); - EXPECT_EQ(properties.at("token").as_object().at("description"), "API token to authorize."); - EXPECT_EQ(properties.at("permissions").as_object().at("description"), "Permissions required by the caller."); + EXPECT_EQ(properties.at("token").as_object().at("description"), + "API token to authorize."); + EXPECT_EQ(properties.at("permissions").as_object().at("description"), + "Permissions required by the caller."); } -TEST(OpenApiTest, IncludesExplicitSchemasForDocumentedLegacyRoutes) -{ +TEST(OpenApiTest, IncludesExplicitSchemasForDocumentedLegacyRoutes) { fw::HttpRouter router; - router.post("/authorize", [](fw::HttpContext&) {}, - {"Authorize", "Checks an API token.", {}, - boost::json::object{{"type", "object"}, - {"properties", boost::json::object{ - {"token", boost::json::object{{"type", "string"}}}, - {"permissions", boost::json::object{{"type", "array"}, - {"items", boost::json::object{{"type", "string"}}}}}}}, - {"required", boost::json::array{"token", "permissions"}}}, - boost::json::object{{"type", "object"}, - {"properties", boost::json::object{ - {"active", boost::json::object{{"type", "boolean"}}}, - {"authorized", boost::json::object{{"type", "boolean"}}}}}}}); + router.post( + "/authorize", [](fw::HttpContext&) {}, + {"Authorize", + "Checks an API token.", + {}, + boost::json::object{ + {"type", "object"}, + {"properties", + boost::json::object{ + {"token", boost::json::object{{"type", "string"}}}, + {"permissions", + boost::json::object{ + {"type", "array"}, + {"items", boost::json::object{{"type", "string"}}}}}}}, + {"required", boost::json::array{"token", "permissions"}}}, + boost::json::object{ + {"type", "object"}, + {"properties", + boost::json::object{ + {"active", boost::json::object{{"type", "boolean"}}}, + {"authorized", boost::json::object{{"type", "boolean"}}}}}}}); const auto document = fw::generate_openapi(router); const auto& operation = operation_at(document, "/authorize", "post"); - const auto& request = operation.at("requestBody").as_object().at("content").as_object() - .at("application/json").as_object().at("schema").as_object(); - EXPECT_EQ(request.at("properties").as_object().at("token").as_object().at("type"), "string"); + const auto& request = operation.at("requestBody") + .as_object() + .at("content") + .as_object() + .at("application/json") + .as_object() + .at("schema") + .as_object(); + EXPECT_EQ( + request.at("properties").as_object().at("token").as_object().at("type"), + "string"); EXPECT_EQ(request.at("required").as_array()[1], "permissions"); - const auto& response = operation.at("responses").as_object().at("200").as_object() - .at("content").as_object().at("application/json").as_object().at("schema").as_object(); - EXPECT_EQ(response.at("properties").as_object().at("authorized").as_object().at("type"), "boolean"); + const auto& response = operation.at("responses") + .as_object() + .at("200") + .as_object() + .at("content") + .as_object() + .at("application/json") + .as_object() + .at("schema") + .as_object(); + EXPECT_EQ(response.at("properties") + .as_object() + .at("authorized") + .as_object() + .at("type"), + "boolean"); } -TEST(OpenApiTest, EmitsPropertiesForUnreflectedObjectSchemas) -{ +TEST(OpenApiTest, EmitsPropertiesForUnreflectedObjectSchemas) { fw::HttpRouter router; - router.post("/opaque", [](const OpenApiOpaquePayload& request) { return request; }); + router.post("/opaque", + [](const OpenApiOpaquePayload& request) { return request; }); const auto document = fw::generate_openapi(router); const auto& operation = operation_at(document, "/opaque", "post"); - const auto& request_schema = operation.at("requestBody").as_object() - .at("content").as_object().at("application/json").as_object().at("schema").as_object(); - const auto& response_schema = operation.at("responses").as_object().at("200").as_object() - .at("content").as_object().at("application/json").as_object().at("schema").as_object(); + const auto& request_schema = operation.at("requestBody") + .as_object() + .at("content") + .as_object() + .at("application/json") + .as_object() + .at("schema") + .as_object(); + const auto& response_schema = operation.at("responses") + .as_object() + .at("200") + .as_object() + .at("content") + .as_object() + .at("application/json") + .as_object() + .at("schema") + .as_object(); EXPECT_EQ(request_schema.at("type"), "object"); EXPECT_TRUE(request_schema.at("properties").as_object().empty()); @@ -297,52 +374,62 @@ TEST(OpenApiTest, EmitsPropertiesForUnreflectedObjectSchemas) EXPECT_TRUE(response_schema.at("properties").as_object().empty()); } -TEST(OpenApiTest, DocumentsAsyncAndStreamSkeletons) -{ +TEST(OpenApiTest, DocumentsAsyncAndStreamSkeletons) { fw::HttpRouter router; - router.async_route("/jobs/:job_id", http::verb::post, - [](fw::HttpContext&, fw::HttpAsyncComplete complete) { complete(); }); + router.async_route( + "/jobs/:job_id", http::verb::post, + [](fw::HttpContext&, fw::HttpAsyncComplete complete) { complete(); }); router.stream("/downloads/:path", http::verb::get, [](fw::HttpContext&, std::shared_ptr, - std::shared_ptr, fw::HttpStreamComplete complete) - { - complete(); - }); + std::shared_ptr, + fw::HttpStreamComplete complete) { complete(); }); const auto document = fw::generate_openapi(router); - EXPECT_TRUE(document.at("paths").as_object().at("/jobs/{job_id}").as_object().contains("post")); - EXPECT_TRUE(document.at("paths").as_object().at("/downloads/{path}").as_object().contains("get")); - EXPECT_TRUE(operation_at(document, "/jobs/{job_id}", "post").contains("responses")); - EXPECT_TRUE(operation_at(document, "/downloads/{path}", "get").contains("responses")); + EXPECT_TRUE(document.at("paths") + .as_object() + .at("/jobs/{job_id}") + .as_object() + .contains("post")); + EXPECT_TRUE(document.at("paths") + .as_object() + .at("/downloads/{path}") + .as_object() + .contains("get")); + EXPECT_TRUE( + operation_at(document, "/jobs/{job_id}", "post").contains("responses")); + EXPECT_TRUE( + operation_at(document, "/downloads/{path}", "get").contains("responses")); } -TEST(OpenApiTest, ProducesDeterministicOutput) -{ +TEST(OpenApiTest, ProducesDeterministicOutput) { fw::HttpRouter router; router.get("/z-last", [](fw::HttpContext&) {}); router.post("/a-first", [](fw::HttpContext&) {}); router.get("/a-first", [](fw::HttpContext&) {}); - const auto first = boost::json::serialize(fw::generate_openapi(router, {"Stable", "1"})); - const auto second = boost::json::serialize(fw::generate_openapi(router, {"Stable", "1"})); + const auto first = + boost::json::serialize(fw::generate_openapi(router, {"Stable", "1"})); + const auto second = + boost::json::serialize(fw::generate_openapi(router, {"Stable", "1"})); EXPECT_EQ(first, second); EXPECT_LT(first.find("/a-first"), first.find("/z-last")); EXPECT_LT(first.find("\"get\""), first.find("\"post\"")); } -TEST(OpenApiTest, IncludesRouteSummaryAndDescription) -{ +TEST(OpenApiTest, IncludesRouteSummaryAndDescription) { fw::HttpRouter router; router.post("/messages", [](fw::HttpContext&) {}); - router.document_route("/messages", http::verb::post, - {"Send a message", "Accepts a message and returns its delivery result."}); + router.document_route( + "/messages", http::verb::post, + {"Send a message", "Accepts a message and returns its delivery result."}); const auto document = fw::generate_openapi(router); const auto& operation = operation_at(document, "/messages", "post"); EXPECT_EQ(operation.at("summary"), "Send a message"); - EXPECT_EQ(operation.at("description"), "Accepts a message and returns its delivery result."); + EXPECT_EQ(operation.at("description"), + "Accepts a message and returns its delivery result."); fw::install_openapi_routes(router); http::request request(http::verb::get, "/docs", 11); @@ -350,16 +437,19 @@ TEST(OpenApiTest, IncludesRouteSummaryAndDescription) fw::HttpContext context(request, response); ASSERT_TRUE(router.dispatch(context)); EXPECT_NE(response.body().find("Send a message"), std::string::npos); - EXPECT_NE(response.body().find("Accepts a message and returns its delivery result."), std::string::npos); + EXPECT_NE(response.body().find( + "Accepts a message and returns its delivery result."), + std::string::npos); } -TEST(OpenApiTest, IncludesDocumentedRequestHeaders) -{ +TEST(OpenApiTest, IncludesDocumentedRequestHeaders) { fw::HttpRouter router; - router.post("/secure", [](fw::HttpContext&) {}, - {"Secure operation", "Requires an API access token.", - {{"Authorization", "Bearer access token.", true}, - {"X-Request-Id", "Optional caller correlation identifier.", false}}}); + router.post( + "/secure", [](fw::HttpContext&) {}, + {"Secure operation", + "Requires an API access token.", + {{"Authorization", "Bearer access token.", true}, + {"X-Request-Id", "Optional caller correlation identifier.", false}}}); const auto document = fw::generate_openapi(router); const auto& operation = operation_at(document, "/secure", "post"); @@ -367,9 +457,11 @@ TEST(OpenApiTest, IncludesDocumentedRequestHeaders) ASSERT_EQ(parameters.size(), 2U); EXPECT_EQ(parameters[0].as_object().at("name"), "Authorization"); EXPECT_EQ(parameters[0].as_object().at("in"), "header"); - EXPECT_EQ(parameters[0].as_object().at("description"), "Bearer access token."); + EXPECT_EQ(parameters[0].as_object().at("description"), + "Bearer access token."); EXPECT_EQ(parameters[0].as_object().at("required"), true); - EXPECT_EQ(parameters[0].as_object().at("schema").as_object().at("type"), "string"); + EXPECT_EQ(parameters[0].as_object().at("schema").as_object().at("type"), + "string"); EXPECT_EQ(parameters[1].as_object().at("name"), "X-Request-Id"); EXPECT_EQ(parameters[1].as_object().at("required"), false); @@ -383,8 +475,7 @@ TEST(OpenApiTest, IncludesDocumentedRequestHeaders) EXPECT_NE(response.body().find("data-header-name"), std::string::npos); } -TEST(OpenApiTest, DocumentsRouteAtRegistration) -{ +TEST(OpenApiTest, DocumentsRouteAtRegistration) { fw::HttpRouter router; router.get("/status", [](fw::HttpContext&) {}, {"Service status", "Returns the current service status."}); @@ -395,17 +486,16 @@ TEST(OpenApiTest, DocumentsRouteAtRegistration) EXPECT_EQ(operation.at("description"), "Returns the current service status."); } -TEST(OpenApiTest, ServesHiddenRuntimeDocument) -{ +TEST(OpenApiTest, ServesHiddenRuntimeDocument) { fw::HttpRouter router; - router.get("/health", [](fw::HttpContext& context) - { + router.get("/health", [](fw::HttpContext& context) { context.set_status(http::status::ok); context.set_body("ok"); }); fw::install_openapi_routes(router, {"Runtime API", "2.0"}); - http::request request(http::verb::get, "/openapi.json", 11); + http::request request(http::verb::get, "/openapi.json", + 11); http::response response; fw::HttpContext context(request, response); @@ -428,16 +518,15 @@ TEST(OpenApiTest, ServesHiddenRuntimeDocument) EXPECT_NE(docs_response.body().find("/openapi.json"), std::string::npos); EXPECT_NE(docs_response.body().find("/health"), std::string::npos); EXPECT_NE(docs_response.body().find("message"), std::string::npos); - EXPECT_NE(body.find("required"), std::string::npos); + EXPECT_NE(body.find("message"), + std::string::npos); + EXPECT_NE(body.find("required"), + std::string::npos); EXPECT_NE(body.find("data-path=\"/messages/{channel}\""), std::string::npos); EXPECT_NE(body.find(">Send request"), std::string::npos); EXPECT_NE(body.find(">Copy cURL"), std::string::npos); - EXPECT_NE(body.find("curl -i -X POST '/messages/{channel}'"), std::string::npos); - EXPECT_NE(body.find("--data-binary '{"message":""}'"), + EXPECT_NE(body.find("curl -i -X POST '/messages/{channel}'"), std::string::npos); + EXPECT_NE( + body.find("--data-binary '{"message":""}'"), + std::string::npos); const std::string policy(response["Content-Security-Policy"]); std::smatch nonce_match; - ASSERT_TRUE(std::regex_search(policy, nonce_match, - std::regex("script-src 'nonce-([a-f0-9]{32})'"))); + ASSERT_TRUE(std::regex_search( + policy, nonce_match, std::regex("script-src 'nonce-([a-f0-9]{32})'"))); EXPECT_EQ(policy.find("script-src 'unsafe-inline'"), std::string::npos); - EXPECT_NE(body.find("", "<2>"}); + fw::install_openapi_routes(router, + {"API ", "<2>"}); http::request request(http::verb::get, "/docs", 11); http::response response; @@ -573,9 +665,11 @@ TEST(OpenApiSecurityTest, EscapesDynamicDocumentationContent) EXPECT_EQ(response.body().find("