LCOV - code coverage report
Current view: top level - http/server - http_application.hpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 100.0 % 1 1
Test Date: 2026-11-10 10:38:07 Functions: - 0 0

            Line data    Source code
       1              : #ifndef THINGER_HTTP_APPLICATION_HPP
       2              : #define THINGER_HTTP_APPLICATION_HPP
       3              : 
       4              : #include "virtual_host.hpp"
       5              : #include "error_format.hpp"
       6              : #include "trusted_proxies.hpp"
       7              : #include "openapi.hpp"
       8              : #include "body_decoders.hpp"
       9              : #include "response_sink.hpp"
      10              : #include "response_observer.hpp"
      11              : #include "request_context.hpp"
      12              : #include "../../util/types.hpp"
      13              : #include <boost/system/error_code.hpp>
      14              : #include <chrono>
      15              : #include <cstdint>
      16              : #include <functional>
      17              : #include <map>
      18              : #include <memory>
      19              : #include <optional>
      20              : #include <string>
      21              : #include <string_view>
      22              : #include <vector>
      23              : 
      24              : namespace thinger::http {
      25              : 
      26              : class request;
      27              : class response;
      28              : 
      29              : // Options for requests dispatched in memory (see http_application::dispatch)
      30              : struct dispatch_options {
      31              :     // Address of the peer, reported by request::get_peer_ip() and, unless it is a trusted
      32              :     // proxy (see set_trusted_proxies), by request::get_request_ip(). Empty by default:
      33              :     // never assume a trusted address such as 127.0.0.1 for internal calls. The scheme
      34              :     // reported by request::get_request_scheme() is the one of the request (https if its URL
      35              :     // is https://, see http_request::set_ssl()), unless this address is a trusted proxy.
      36              :     std::string remote_ip;
      37              : 
      38              :     // Maximum time to wait for the response, handler execution included; 504 Gateway
      39              :     // Timeout if exceeded. The scope of the request is then cancelled with
      40              :     // cancel_reason::timeout: its handler goes on, and sees it in request::cancelled(), unless
      41              :     // its route declared the timeout (see route::cancel_on), which interrupts it. A caller
      42              :     // cancelling the dispatch cancels the scope with cancel_reason::client_gone instead
      43         1350 :     std::chrono::milliseconds timeout{30000};
      44              : 
      45              :     // Initial values of the request context (see request::context()), seen by the
      46              :     // middlewares and the handler, e.g. a trace id; they may replace them. Empty by default:
      47              :     // nothing is carried over from the request being served by the caller.
      48              :     request_context context;
      49              : };
      50              : 
      51              : // Cross-origin requests (see http_application::enable_cors). The defaults are those of cors()
      52              : // in Express: any origin, without credentials, reflecting the headers a preflight asks for.
      53              : struct cors_options {
      54              :     // Origins allowed. {"*"} alone (the default) is any origin: every response says
      55              :     // "Access-Control-Allow-Origin: *", also to a request without Origin. Otherwise, the exact
      56              :     // origins allowed (ASCII case-insensitive; empty for none, "null" for that origin): the one
      57              :     // of a request listed is reflected as it came, and every response says "Vary: Origin",
      58              :     // allowed or not. "*" next to other origins is invalid.
      59              :     std::vector<std::string> origins{"*"};
      60              : 
      61              :     // Whether an origin is allowed, instead of the list, which is ignored then: reflected as
      62              :     // above if it returns true. Called once for each request with an Origin (repeated ones come
      63              :     // joined by ", "), on the thread of the request and before its middlewares, so it must not
      64              :     // block. If it throws, the request is answered 500, with no origin allowed.
      65              :     std::function<bool(std::string_view origin, const http_request& request)> origin_filter{};
      66              : 
      67              :     // Access-Control-Allow-Methods of a preflight
      68              :     std::vector<std::string> methods{"GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"};
      69              : 
      70              :     // Access-Control-Allow-Headers of a preflight: without a value (the default), what its
      71              :     // Access-Control-Request-Headers asks for, with "Vary: Access-Control-Request-Headers";
      72              :     // with a value, that list, whatever it asks for (not sent if empty). Being optional, it is
      73              :     // set as std::vector<std::string>{"Content-Type", "Authorization"}.
      74              :     std::optional<std::vector<std::string>> allowed_headers{};
      75              : 
      76              :     // Access-Control-Expose-Headers, on the responses to an origin allowed (not sent if empty)
      77              :     std::vector<std::string> exposed_headers{};
      78              : 
      79              :     // Access-Control-Max-Age of a preflight: not sent without a value (the default), and sent
      80              :     // when 0. Not negative.
      81              :     std::optional<std::chrono::seconds> max_age{};
      82              : 
      83              :     // Send "Access-Control-Allow-Credentials: true" to an origin allowed (cookies or
      84              :     // Authorization in a browser). Invalid with any origin ("*"), which browsers reject: use a
      85              :     // list or a filter.
      86              :     bool credentials = false;
      87              : 
      88              :     // Let a preflight go on as any request (route matching, middlewares and the OPTIONS route of
      89              :     // the application, or 404/405), with the preflight headers already on its response. By
      90              :     // default the application answers it with 204, before route matching and the middlewares.
      91              :     bool preflight_continue = false;
      92              : };
      93              : 
      94              : namespace detail {
      95              :     // What http_application and response share of CORS (cors.cpp): the options once checked and
      96              :     // computed (null, and logged, if invalid); what they mean for a request, which runs the
      97              :     // filter of the options and throws what it throws; and its headers, added to a message about
      98              :     // to be sent, unless the message has them
      99              :     struct cors_policy;
     100              :     struct cors_decision {
     101              :         std::string origin;             // granted: "*", as it came, or empty for none
     102              :         bool preflight = false;         // its response says what a preflight is told
     103              :         bool answer_preflight = false;  // the application answers it, not its routes
     104              :         std::string requested_headers;  // Access-Control-Request-Headers of a preflight, to reflect
     105              :     };
     106              :     std::shared_ptr<const cors_policy> make_cors_policy(cors_options options);
     107              :     cors_decision decide_cors(const cors_policy& policy, const http_request& request);
     108              :     void apply_cors(const cors_policy& policy, const cors_decision& decision, http_response& message);
     109              : }
     110              : 
     111              : // Middlewares run after route matching (request::get_matched_route() is set, or null
     112              : // if no route matched) and before the request body is read, so they cannot see it.
     113              : // They share the response object with the route handler.
     114              : 
     115              : // Asynchronous middleware: co_return true to continue the chain, or respond through
     116              : // the response and co_return false to stop it (the route handler is not called).
     117              : using async_middleware_function = std::function<thinger::awaitable<bool>(request&, response&)>;
     118              : 
     119              : // Synchronous middleware: call next() before returning to continue the chain, or
     120              : // respond through the response to stop it. next() must not be called asynchronously.
     121              : using middleware_function = sync_callback<void(request&, response&, std::function<void()>)>;
     122              : 
     123              : // The same, as a coroutine (as an async Express middleware): call next() before it
     124              : // completes to continue the chain
     125              : using coroutine_middleware_function = std::function<thinger::awaitable<void>(request&, response&, std::function<void()>)>;
     126              : 
     127              : // What a server serves: the virtual hosts and their routes, the middlewares, and how
     128              : // requests are processed (error format, trusted proxies, limits). The application is the
     129              : // default virtual host: the routes registered on it (see virtual_host for the registration
     130              : // methods) answer every host without a virtual host of its own. A server (see
     131              : // server_base) is an application served on listeners; an application can also be
     132              : // used on its own, to dispatch requests in memory, without a connection.
     133              : class http_application : public virtual_host {
     134              : public:
     135              :     http_application();
     136              : 
     137              :     // Ends the requests dispatched in memory that are still in flight (see dispatch()), without
     138              :     // waiting for their handlers. It may be called from any thread, or from one of those handlers
     139              :     ~http_application();
     140              : 
     141              :     // Virtual hosts: the routes registered on the application itself answer any host,
     142              :     // unless the Host header (case-insensitive, port ignored) matches a host registered
     143              :     // here: exact names first, then patterns in registration order. Register before serving
     144              :     // (listen()): hosts are matched without locks.
     145              :     // name: exact ("api.example.com"), "*" (this default host), or a pattern where "*" is
     146              :     // one label and ":name" a label available as a request parameter (see virtual_host)
     147              :     virtual_host& host(const std::string& name);
     148              : 
     149              :     // Host matched by a regular expression on the whole host name (case-insensitive);
     150              :     // its capture groups are available through request::get_host_matches()
     151              :     virtual_host& host_regex(const std::string& pattern);
     152              : 
     153              :     // OpenAPI document generated from the routes of the default host (configure title,
     154              :     // hooks... here). For another host: openapi_generator(app.host(name).router()).
     155              :     openapi_generator& openapi();
     156              : 
     157              :     // Serve the OpenAPI document as JSON at `path` (not served unless called)
     158              :     route& serve_openapi(const std::string& path = "/openapi.json");
     159              : 
     160              :     // Dispatch a request in memory, without a connection, through the same steps as a
     161              :     // network request: route matching, middlewares, body reading and validation, handler,
     162              :     // and the same 404/405 handling. Useful for tests, internal calls and gateways.
     163              :     // The body is taken from the request content (set_content); chunked requests are not
     164              :     // supported. WebSocket, SSE and connection takeover answer 501.
     165              :     //
     166              :     // A dispatch shares only the serving state with the application, never the application
     167              :     // itself, so destroying it (from any thread, or from a handler) ends the dispatches in
     168              :     // flight without waiting for them. One without a complete response ends at once, and this
     169              :     // throws boost::system::system_error(operation_aborted); a complete one is delivered. One
     170              :     // not started yet (this awaitable, awaited later) runs no middleware or handler. A handler
     171              :     // running is interrupted as at the shutdown of a server: its next cancellable await ends
     172              :     // with operation_aborted, whatever its route declared and even if its scope was cancelled
     173              :     // already (request::cancelled() keeps the first reason, cancel_reason::shutdown if none).
     174              :     // Its current step, and the awaits it isolated from cancellation, end on their own, and what
     175              :     // it sends afterwards is dropped. What it captured itself is the caller's to keep alive.
     176              :     //
     177              :     // With a response hook (see on_response()), the dispatch ends once the hook has run, so what
     178              :     // the hook captures can be destroyed as soon as this returns. The wait is limited by what is
     179              :     // left of dispatch_options::timeout: a handler still running then, even one that answered
     180              :     // already, gets its response returned without waiting more, and the hook runs when the
     181              :     // handler ends, as on a connection: what the hook captures must live as long as the handler.
     182              :     // Neither a caller cancelling the dispatch, nor destroying the application, waits for the
     183              :     // hook. Without a hook nothing is waited for.
     184              :     awaitable<std::shared_ptr<http_response>> dispatch(std::shared_ptr<http_request> request,
     185              :                                                        dispatch_options options = {});
     186              : 
     187              :     // Same, calling `callback` once with the response; runs on `executor`. With no error to
     188              :     // report, a dispatch ended by destroying the application gets 503 Service Unavailable
     189              :     void dispatch(const boost::asio::any_io_executor& executor,
     190              :                   std::shared_ptr<http_request> request,
     191              :                   sync_callback<void(std::shared_ptr<http_response>)> callback,
     192              :                   dispatch_options options = {});
     193              : 
     194              :     // Handle a request received by a server, answering through `sink`
     195              :     awaitable<void> handle(std::shared_ptr<request> req, std::shared_ptr<response_sink> sink);
     196              : 
     197              :     // Middleware (register before serving, executed in registration order)
     198              :     void use(async_middleware_function middleware);
     199              :     void use(middleware_function middleware);
     200              :     void use(coroutine_middleware_function middleware);
     201              : 
     202              :     // Basic Auth helpers
     203              :     using auth_verify_function = std::function<bool(const std::string& username, const std::string& password)>;
     204              : 
     205              :     // Add basic auth for a specific path prefix
     206              :     void set_basic_auth(const std::string& path_prefix,
     207              :                         const std::string& realm,
     208              :                         auth_verify_function verify);
     209              : 
     210              :     // Add basic auth with simple username/password
     211              :     void set_basic_auth(const std::string& path_prefix,
     212              :                         const std::string& realm,
     213              :                         const std::string& username,
     214              :                         const std::string& password);
     215              : 
     216              :     // Add basic auth with multiple users
     217              :     void set_basic_auth(const std::string& path_prefix,
     218              :                         const std::string& realm,
     219              :                         const std::map<std::string, std::string>& users);
     220              : 
     221              :     // Serve cross-origin requests from browsers (see cors_options; off by default). Its headers
     222              :     // go on every response of the application, whoever answers it (the handler, a middleware,
     223              :     // or the server itself: 400, 404, 405, 413, 415, 500, a route timeout...), on a connection or
     224              :     // dispatched in memory; a header set by the handler or a middleware stays instead. It is not
     225              :     // access control: a request from an origin not allowed still runs, only without the headers
     226              :     // a browser needs to read its response. A preflight (OPTIONS with a non-empty
     227              :     // Access-Control-Request-Method) is answered 204 by the application, without running the
     228              :     // middlewares, unless cors_options::preflight_continue. Returns `invalid_argument` for
     229              :     // options that cannot be applied, the reason in the log: nothing changes then. Set it before
     230              :     // serving (listen()): it is read without locks.
     231              :     boost::system::error_code enable_cors(cors_options options = {});
     232              : 
     233              :     // Back to no CORS headers, and OPTIONS routed as any other method
     234              :     void disable_cors();
     235              : 
     236              :     // Maximum request body size (8 MB by default), for the routes that do not set their own
     237              :     // (route::max_body_size) and the requests matching no route: every read of a body is
     238              :     // bounded by it (413 if larger), and an unread body larger than this is not discarded
     239              :     // after the response: the connection is closed instead
     240              :     void set_max_body_size(std::uint64_t size);
     241              :     std::uint64_t get_max_body_size() const;
     242              : 
     243              :     // Decoders of request bodies by media type, for the routes taking the body decoded:
     244              :     // JSON, MessagePack, CBOR, UBJSON, urlencoded forms, text and binary are built in (see
     245              :     // body_decoder_registry); remove or add decoders here. Set before serving.
     246              :     body_decoder_registry& body_decoders();
     247              : 
     248              :     // Body types accepted by the routes taking the body decoded that do not set their own
     249              :     // (route::accepts): names of decoders or media types; {"json"} by default. Others are
     250              :     // answered with 415 Unsupported Media Type. Set before serving.
     251              :     void accept_body_types(std::vector<std::string> types);
     252              :     const std::vector<std::string>& get_body_types() const;
     253              : 
     254              :     // Limits of decoded bodies (nesting depth), for the routes that do not set their own
     255              :     // (route::decode_limits); set before serving
     256              :     void set_body_limits(body_limits limits);
     257              :     const body_limits& get_body_limits() const;
     258              : 
     259              :     // Decoders, accepted types and limits together, e.g. for the openapi_generator of a
     260              :     // virtual host: openapi_generator(app.host(name).router(), &app.get_body_settings())
     261              :     const body_settings& get_body_settings() const;
     262              : 
     263              :     // Format of the error responses: those sent with response::error() and the ones the
     264              :     // server generates (400 invalid body or schema validation, 404, 405, 413, 415, 500...).
     265              :     // By default the message is sent as text/plain, and errors with details (schema
     266              :     // validation) as JSON {"error": {"message": ..., "context": [...]}}. Set it before
     267              :     // serving (listen()): it is read without locks, and may be called concurrently from the
     268              :     // worker threads.
     269              :     void set_error_formatter(error_formatter formatter);
     270              : 
     271              :     // Proxies (IPs or CIDR ranges: "10.0.0.1", "10.0.0.0/8", "fd00::/8") whose forwarding
     272              :     // header gives the client address reported by request::get_request_ip(), and the scheme
     273              :     // reported by request::get_request_scheme() (the X-Forwarded-Proto value aligned with the
     274              :     // X-Forwarded-For entry of the client, or its last value; the proto of the Forwarded
     275              :     // element of the client). The headers are ignored for any other peer, so only list proxies
     276              :     // that overwrite or append to them. None by default; set before serving (listen()): they
     277              :     // are read without locks. Returns false if an entry is invalid (then no proxy is trusted).
     278              :     bool set_trusted_proxies(const std::vector<std::string>& proxies,
     279              :                              forwarded_header header = forwarded_header::x_forwarded_for);
     280              : 
     281              :     // Called once per request, with its response (see response_info: status, bytes, duration,
     282              :     // and whether it completed, handed the connection over, or was aborted), once both the
     283              :     // response ended and the application is done with the request (its handler returned, or
     284              :     // its route timed out), e.g. for access logs, metrics or audit records. Every request the
     285              :     // application receives is reported, whoever answered it: the handler, a middleware, or the
     286              :     // server itself (400 invalid framing or body, 404, 405, 413, 415, 417, 500, a route
     287              :     // timeout...), on a connection or dispatched in memory. Not the requests a connection
     288              :     // cannot parse (400, 431), which never reach the application.
     289              :     //
     290              :     // Streamed responses (start_chunked(), start_stream()) are reported once their end is
     291              :     // written. Upgrades (WebSocket, SSE, take_over()) are reported once the response handing
     292              :     // the connection over is written, as taken_over: what happens on the connection afterwards
     293              :     // is not (see websocket_connection::on_close(), sse_connection::on_close()).
     294              :     //
     295              :     // A request dispatched in memory returns after the hook has run, unless the timeout of the
     296              :     // dispatch ends first, or the dispatch is cancelled (see dispatch()).
     297              :     //
     298              :     // It runs on the thread of the connection (for in-memory dispatch, the one ending the
     299              :     // request or its response), so it must not block: hand anything slow to another thread.
     300              :     // It may run synchronously within the call that ends the response, when the application
     301              :     // was already done (e.g. a late end_chunked() or abort_stream()), and, for a response that
     302              :     // never ended, from the destructor of what held it, as the connection being released (also
     303              :     // at the teardown of the io_context). The request is read-only there, and its connection
     304              :     // may be gone (its peer address is kept). What it throws is logged. Without a hook nothing
     305              :     // is allocated or measured. Set it before serving (listen()): it is read without locks.
     306              :     void on_response(response_hook hook);
     307              : 
     308              : protected:
     309              :     // What serves the requests of a connection: it keeps what they read (the hosts and their
     310              :     // routes, the middlewares and the settings) alive, not the application, so the requests
     311              :     // still running when a server is destroyed find it
     312              :     std::function<awaitable<void>(std::shared_ptr<request>)> connection_handler() const;
     313              : 
     314              : private:
     315              :     class state;
     316              :     std::shared_ptr<state> state_;
     317              : };
     318              : 
     319              : } // namespace thinger::http
     320              : 
     321              : #endif // THINGER_HTTP_APPLICATION_HPP
        

Generated by: LCOV version 2.0-1