/* This file is part of libhttpserver Copyright (C) 2011-2026 Sebastiano Merlino This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #if !defined (_HTTPSERVER_HPP_INSIDE_) && !defined (HTTPSERVER_COMPILATION) #error "Only or can be included directly." #endif /** * @file hook_context.hpp * @brief Per-phase context structs passed to hook callables. * * Each `*_ctx` type carries exactly the * information the matching phase publishes; lifetime is the hook * callback invocation -- pointers in these structs MUST NOT be * captured past the callback's return. */ #ifndef SRC_HTTPSERVER_HOOK_CONTEXT_HPP_ #define SRC_HTTPSERVER_HOOK_CONTEXT_HPP_ #include #include #include #include #include #include #include #include #include #include "httpserver/http_method.hpp" namespace httpserver { // Forward declarations -- the hook contexts reference http_request and // http_response only by pointer/reference, so we avoid pulling either // header into this public surface (no transitive // backend leak). http_resource is referenced for the route-resolved // phase via an optional pointer; same forward-decl strategy. class http_request; class http_response; class http_resource; /** * @brief Libhttpserver-defined peer address (IPv4 or IPv6) plus port. * * Kept libhttpserver-native so the public hook surface carries no * `` or MHD types. `bytes` is in * network byte order: the first four bytes carry an IPv4 address with * the rest zero; all sixteen bytes are used for IPv6. `port` is in * host byte order. */ struct peer_address { enum class family : std::uint8_t { unspec = 0, ipv4 = 1, ipv6 = 2 }; family fam = family::unspec; std::array bytes{}; std::uint16_t port = 0; // Returns a printable representation of the address (no port). // Defined out-of-line in src/peer_address.cpp. (Placement note: this // struct is logically a context type, but its to_string() body lives in // a separate TU to keep both hook_handle.cpp and peer_address.cpp under // the per-file LOC ceiling. A future task could consolidate into a // dedicated hook_context.cpp if other context types grow out-of-line // bodies.) [[nodiscard]] std::string to_string() const; }; /** * @brief Light pointer-and-bag view of a matched route. * * Used by `route_resolved_ctx` and `before_handler_ctx`. * `path_template` is a `string_view` into per-request storage; it is * valid **only for the duration of the hook call**. Do NOT capture or * store the view beyond the hook's return — the backing string is owned * by the per-request `connection_context` object and is destroyed when the * request completes. If you need the value after the hook returns, copy * it into a `std::string` inside the hook body. * `methods` carries the method bits the matched entry serves; `is_prefix` * flags prefix-match registrations (`register_prefix` / single-resource). */ struct route_descriptor { std::string_view path_template; method_set methods{}; bool is_prefix = false; }; // ---- Phase context structs --------------------------------------------- // // Each phase has a dedicated context struct so the per-phase add_hook // overload is distinguishable from the other ten at the type level, and // so adding fields to one phase does not perturb the others' ABI. // // The fields below are deliberately POD-shaped // (references / scalars / spans / string_views / optionals of POD). /// @brief Context for the `connection_opened` phase: new TCP/TLS connection. struct connection_open_ctx { peer_address peer{}; }; /// @brief Context for the `connection_closed` phase: connection torn down. struct connection_close_ctx { peer_address peer{}; }; /** * @brief Context for the `accept_decision` phase. * * Observation-only; the handler returns `void`. Accept/deny * is decided by the policy callback, not by the hook. * * @note `accepted` mirrors the policy callback's MHD_YES/MHD_NO return. * @note `reason` is set when the connection is rejected: * - `"denied"` — the peer hit the deny list (and is not allow-listed). * - `"not-on-allow-list"` — default policy REJECT and the peer is not * on the allow list. * - `std::nullopt` — the connection was accepted. * @note The `string_view`'s referent is a string literal with static * storage duration; capturing it past the hook return is safe. If a * heap-owned copy is needed, use `std::string(*ctx.reason)`. */ struct accept_ctx { peer_address peer{}; bool accepted = true; std::optional reason{}; }; /** * @brief Context for the `request_received` phase. * * Fires after the `http_request` is fully populated from MHD's * headers and BEFORE any body bytes are read. The request pointer is * mutable so a hook may adjust per-request state before the upload * starts. Short-circuit-capable: returning * `hook_action::respond_with(r)` aborts the upload -- the body is * never read and the resource handler is never invoked. * * @security The mutable `request` pointer gives hooks FULL write access * to the live request object (headers, method, body state). A buggy or * compromised hook can tamper with the request in ways that bypass * downstream security checks (auth, validation). Prefer an immutable * view if the hook only needs to observe, and audit hooks registered * for this phase carefully. */ struct request_received_ctx { http_request* request = nullptr; // mutable: hook may set context std::chrono::steady_clock::time_point received_at{}; }; /** * @brief Context for the `body_chunk` phase. * * Fires once per chunk MHD delivers to the upload callback, BEFORE * the bytes are appended to the request body or fed to any in-flight * post-processor. Short-circuit-capable. * * @attention This phase is invoked from arbitrary MHD worker threads * at arbitrary granularity — on slow networks chunks may be a single * byte. Hooks MUST be cheap (no blocking I/O, no per-chunk heap * allocation in the hot path) — a slow hook back-pressures the * connection's upload. * @note `chunk` aliases MHD-owned memory; it is only valid for the * duration of the hook call. Copy into owned storage if the data must * outlive this firing. * @note `offset` is the number of body bytes already buffered before * this chunk (first firing has `offset==0`, next has * `offset==chunk0.size()`, etc.). * @note Short-circuit: returning `hook_action::respond_with(r)` aborts * the upload at the next MHD callback; the resource handler is never * invoked. Any in-flight post-processor is destroyed and its buffer * freed at the short-circuit point. */ struct body_chunk_ctx { http_request* request = nullptr; std::span chunk{}; std::uint64_t offset = 0; // Currently always false: no firing site ever sets it. End-of-body // is signalled by MHD's zero-size upload callback, not this flag; // it is reserved for future use. Do not rely on it to detect the // last chunk. bool is_final = false; }; /** * @brief Context for the `route_resolved` phase. * * Observation-only. `matched` is empty for the 404 path (no route * matched); otherwise it describes the matched registration. */ struct route_resolved_ctx { const http_request* request = nullptr; std::optional matched{}; const http_resource* resource = nullptr; // nullable; nullptr for lambda routes }; /** * @brief Context for the `before_handler` phase. * * Short-circuit-capable. Fires after route resolution (the matched * resource is known) and BEFORE both `is_allowed` and the resource * handler invocation. Returning `hook_action::respond_with(r)` skips * both checks and goes straight to response materialisation. Also * the phase used by the `method_not_allowed_handler` and * `auth_handler` v1 aliases. * * @note `method` is the wire method decoded by `answer_to_connection`. * The 405-alias hook consults this against * `resource->get_allowed_methods()` to decide whether to * short-circuit with 405 + Allow header. * @note `resource` is the resolved `http_resource` pointer; `nullptr` * for lambda-route registrations without a stable `http_resource*`. * The hook fires only for route hits. * * @security The mutable `request` pointer gives hooks FULL write access * to the live request object. This is intentional (e.g., the auth hook * may annotate the request), but a buggy or compromised hook can tamper * in ways that bypass downstream security checks. Audit hooks * registered for this phase carefully. */ struct before_handler_ctx { http_request* request = nullptr; std::optional matched{}; http_method method = http_method::count_; const http_resource* resource = nullptr; }; /** * @brief Context for the `handler_exception` phase. * * Short-circuit-capable. Fires when an exception escapes the resource * handler, before the `internal_error_handler` v1 alias is consulted. */ struct handler_exception_ctx { const http_request* request = nullptr; std::exception_ptr exception{}; /** * Human-readable description of the exception. For `std::exception` * throws this is `e.what()`; for non-std throws this is the sentinel * string @c "unknown exception". * * @security This value originates from application exception text and * MAY contain internal detail (DB connection strings, file paths, * user-supplied input). Hook implementations and internal_error_handler * callbacks MUST NOT forward this value into HTTP response bodies * without sanitization (CWE-209: Information Exposure Through an Error * Message). * * @note The view is valid only for the synchronous duration of the hook * call chain — it aliases the live exception object's storage. Do NOT * store it past the hook's return. */ std::string_view message{}; }; /** * @brief Context for the `after_handler` phase. * * Short-circuit-capable; the `response` pointer is mutable so hooks * may rewrite headers / status without replacing the body. */ struct after_handler_ctx { const http_request* request = nullptr; http_response* response = nullptr; // mutable: hook may mutate }; /** * @brief Context for the `response_sent` phase. * * Observation point fired immediately after `MHD_queue_response` and * BEFORE `MHD_destroy_response`. Carries the data users have been * asking for (issues #281 and #69): `status`, `bytes_queued`, * `elapsed`. The `log_access` v1 alias is wired through this phase. * * @note `status` is the HTTP status code passed to * `MHD_queue_response`. * @note `bytes_queued` is `http_response::body_->size()`. For deferred * or pipe bodies `size()` returns 0 because the final length is not * yet known at queue time; fall back to the Content-Length header for * streamed bodies. * @note `elapsed` is `steady_clock::now()` at the fire site minus * `connection_context::start_time` (captured on the first invocation of * `answer_to_connection`). Granularity is nanoseconds. * @note `elapsed` is `nanoseconds::zero()` when only the `log_access` * alias slot fires (no `add_hook(response_sent, ...)` hooks registered). * The alias lambda does not read this field; the zero avoids a * gratuitous `steady_clock::now()` call on that code path. * @attention The `response` pointer is non-null at the fire site. * Hooks MUST NOT capture it past their return — the `http_response` * is destroyed in `~connection_context` immediately after * `request_completed` fires. */ struct response_sent_ctx { const http_request* request = nullptr; const http_response* response = nullptr; int status = 0; std::size_t bytes_queued = 0; std::chrono::nanoseconds elapsed{}; }; /** * @brief Context for the `request_completed` phase. * * Unconditional final hook. Fires BEFORE the per-request state is * destroyed so the ctx pointers remain backed by live storage for the * duration of the hook call. Hooks MUST NOT capture `request` or * `resp` past their return. * * @note `resp` is NULLABLE. On early-failure paths (e.g., a * `request_received` hook returning `respond_with(413)`), * `conn->response` is populated and `resp` points into it. On paths * where MHD terminates the request before any response object is * built, `resp` is `nullptr`. * @note `succeeded` maps from `MHD_RequestTerminationCode`: * `MHD_REQUEST_TERMINATED_COMPLETED_OK` → `true`; everything else → * `false`. A user-policy rejection that produced a complete response * on the wire (e.g., a 413 from a `request_received` short-circuit) * reports `succeeded == true` because MHD drove the request to * ordinary completion. * @note `duration` is `steady_clock::now()` at the fire site minus * `connection_context::start_time`; mirrors `response_sent_ctx::elapsed`. * On degenerate paths where `answer_to_connection` never ran (e.g., * a port scan), `start_time` is epoch and `duration` is set to * `nanoseconds{-1}` as a sentinel so hook authors can distinguish * this case from a real (but very slow) request. * @attention Hooks MUST NOT capture `request` or `resp` past their * return — both are destroyed in `~connection_context` immediately after * this fire. */ struct request_completed_ctx { const http_request* request = nullptr; const http_response* resp = nullptr; bool succeeded = false; std::chrono::steady_clock::duration duration{}; }; } // namespace httpserver #endif // SRC_HTTPSERVER_HOOK_CONTEXT_HPP_