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

            Line data    Source code
       1              : #ifndef THINGER_HTTP_SERVER_REVERSE_PROXY_HPP
       2              : #define THINGER_HTTP_SERVER_REVERSE_PROXY_HPP
       3              : 
       4              : #include "../common/http_request.hpp"
       5              : #include "../common/http_response.hpp"
       6              : #include "../../util/types.hpp"
       7              : #include <boost/asio/ssl/context.hpp>
       8              : #include <chrono>
       9              : #include <cstdint>
      10              : #include <functional>
      11              : #include <map>
      12              : #include <memory>
      13              : #include <mutex>
      14              : #include <optional>
      15              : #include <string>
      16              : #include <string_view>
      17              : #include <tuple>
      18              : 
      19              : namespace thinger::http {
      20              : 
      21              : class request;
      22              : class response;
      23              : 
      24              : // TLS settings to reach a target over HTTPS (or WSS)
      25              : struct proxy_tls_options {
      26              :     // Verify the certificate of the target: its chain, against the trusted CAs, and that it
      27              :     // was issued for `server_name`. Only disable it for testing.
      28              :     bool verify = true;
      29              : 
      30              :     // Name sent with SNI and verified in the certificate (a host name or an IP address);
      31              :     // the target host if empty
      32              :     std::string server_name;
      33              : 
      34              :     // CAs trusted to verify the target instead of the system ones: a PEM file, or PEM text.
      35              :     // A target with a certificate from a private CA is only accepted when its CA is set here.
      36              :     std::string ca_file;
      37              :     std::string ca_pem;
      38              : };
      39              : 
      40              : // Where a reverse proxy forwards a request: a TCP address, over TLS or not, or a Unix
      41              : // socket, with a base path prepended to the forwarded path
      42              : struct proxy_target {
      43              :     enum class transport { tcp, tls, unix_socket };
      44              : 
      45              :     transport type = transport::tcp;
      46              : 
      47              :     // TCP and TLS targets: host name or IP address (IPv6 without brackets), and port
      48              :     std::string host;
      49              :     uint16_t port = 80;
      50              : 
      51              :     // Unix socket targets
      52              :     std::string socket_path;
      53              : 
      54              :     // Prepended to the path forwarded (e.g. "/api"); empty for none
      55              :     std::string base_path;
      56              : 
      57              :     // TLS targets
      58              :     proxy_tls_options tls;
      59              : 
      60              :     // Target from a URL: "http://host[:port][/base]", "https://..." (TLS), or "ws://" and
      61              :     // "wss://" alike. Empty if the URL is not valid (or has a query or fragment).
      62              :     static std::optional<proxy_target> from_url(std::string_view url);
      63              : 
      64              :     // Unix socket target
      65              :     static proxy_target unix_socket(std::string path, std::string base_path = {});
      66              : 
      67              :     // Host header for the target: its host, with the port unless it is the default one
      68              :     // ("localhost" for Unix sockets)
      69              :     std::string authority() const;
      70              : };
      71              : 
      72              : // Forwarding headers added to the requests sent to the target
      73              : enum class forwarding_headers {
      74              :     x_forwarded,    // X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host
      75              :     forwarded,      // Forwarded (RFC 7239): for=...;proto=...;host=...
      76              :     none            // none added
      77              : };
      78              : 
      79              : // How a request is forwarded
      80              : struct proxy_options {
      81              :     // Path sent to the target (after the target base path), e.g. "/" + req["path"], as it is
      82              :     // to be sent (percent-encoded). The path of the request, as received, if empty. The query
      83              :     // of the request is appended to a path without one.
      84              :     std::string path;
      85              : 
      86              :     // Host header sent: the target authority by default, `host` if set, or the Host of the
      87              :     // request if `preserve_host` (the original host is also in the forwarding headers)
      88              :     bool preserve_host = false;
      89              :     std::string host;
      90              : 
      91              :     // Headers reporting the client to the target. Those received from a trusted proxy (see
      92              :     // http_application::set_trusted_proxies, the header it sets) are kept and extended; from
      93              :     // any other peer, the forwarding headers are dropped, as the client could forge them.
      94              :     forwarding_headers forwarding = forwarding_headers::x_forwarded;
      95              : 
      96              :     // Time to connect to the target (TLS handshake included); 504 if exceeded
      97           21 :     std::chrono::milliseconds connect_timeout{10000};
      98              : 
      99              :     // Maximum time without any data exchanged once connected (waiting for the response
     100              :     // included): 504 if the response has not started, or the connection to the client is
     101              :     // closed if it has. Zero disables it (e.g. for event streams). It covers the exchange
     102              :     // only: a tunnel has its own, tunnel_idle_timeout.
     103           12 :     std::chrono::milliseconds idle_timeout{60000};
     104              : 
     105              :     // Maximum time for the whole exchange, connection included; zero for no limit
     106           21 :     std::chrono::milliseconds total_timeout{0};
     107              : 
     108              :     // Maximum request body forwarded (413 if larger); zero for no limit. The maximum body
     109              :     // size of the route (route::max_body_size, or the application one) applies too, as to
     110              :     // any body read: the smaller one wins. The body is never held whole: it is streamed to
     111              :     // the target as it arrives.
     112              :     size_t max_body_size = 0;
     113              : 
     114              :     // Forward protocol upgrades (WebSocket...) as a tunnel between the client and the target
     115              :     bool allow_upgrade = true;
     116              : 
     117              :     // Default of tunnel_idle_timeout: above the 60 s after which a thinger-http WebSocket
     118              :     // pings a quiet peer, so a quiet WebSocket is not cut before its keepalive sends anything,
     119              :     // and equal to its WebSocket and connection idle timeouts (120 s). nginx (60 s), AWS ALB
     120              :     // (60 s) and Cloudflare (100 s) close idle tunnels earlier
     121              :     static constexpr std::chrono::milliseconds DEFAULT_TUNNEL_IDLE_TIMEOUT{120000};
     122              : 
     123              :     // Maximum time a tunnel (see allow_upgrade) stays open with nothing forwarded in either
     124              :     // direction: then the connections to the client and to the target are closed. Any byte
     125              :     // forwarded counts, so WebSocket data, pings and pongs keep it open, while a peer that
     126              :     // stops reading lets it end. Independent of idle_timeout: zero disables it.
     127              :     std::chrono::milliseconds tunnel_idle_timeout = DEFAULT_TUNNEL_IDLE_TIMEOUT;
     128              : 
     129              :     // Change the request sent to the target before it is sent: its method, path (get_uri())
     130              :     // and headers, e.g. to add credentials or remove the Authorization of the client. The
     131              :     // body framing headers are set by the proxy afterwards.
     132              :     sync_callback<void(request& incoming, http_request& outgoing)> on_request;
     133              : 
     134              :     // Change the response of the target before it is sent to the client: its status and
     135              :     // headers (the framing headers are set by the proxy afterwards). Also called for the
     136              :     // response of a protocol upgrade (101).
     137              :     sync_callback<void(request& incoming, http_response& response)> on_response;
     138              : };
     139              : 
     140              : // What happened to a forwarded request
     141              : struct proxy_result {
     142              :     // Status sent to the client: the target's, or the error of the proxy (502, 504...);
     143              :     // 0 if nothing could be sent (e.g. the response was already sent)
     144              :     int status = 0;
     145              : 
     146              :     // Whether the response came from the target (then `status` is its status)
     147              :     bool from_target = false;
     148              : 
     149              :     // Whether the connection became a tunnel to the target (protocol upgrade)
     150              :     bool upgraded = false;
     151              : 
     152              :     // Whether the exchange was complete: the request body sent, and the response received
     153              :     // and sent whole to the client
     154              :     bool complete = false;
     155              : 
     156              :     // Body bytes forwarded: of the request to the target, and of the response to the client
     157              :     size_t request_bytes = 0;
     158              :     size_t response_bytes = 0;
     159              : 
     160              :     // Why the exchange failed, empty if it did not
     161              :     std::string error;
     162              : };
     163              : 
     164              : // Reverse proxy: forwards the request of a route handler to a target (TCP, TLS or Unix
     165              : // socket), and its response back, streaming both bodies with flow control, or turns the
     166              : // connection into a tunnel to the target on a protocol upgrade (WebSocket). Use it from a
     167              : // coroutine handler that has not read the body:
     168              : //
     169              : //     http::reverse_proxy proxy;
     170              : //     auto target = *http::proxy_target::from_url("http://127.0.0.1:3000");
     171              : //     server.host("app.example.com").set_not_found_handler(
     172              : //         [&](http::request& req, http::response& res) -> thinger::awaitable<void> {
     173              : //             co_await proxy.forward(req, res, target);
     174              : //         });
     175              : //
     176              : // It is thread-safe: one proxy can serve every route and thread. The request is forwarded
     177              : // without hop-by-hop headers and with forwarding headers; the body is framed again by the
     178              : // proxy, from what the server parser accepted. The response of the target is parsed
     179              : // strictly: a response that cannot be framed safely is answered with 502. The connection
     180              : // to the target is used for a single request.
     181              : class reverse_proxy {
     182              : public:
     183          447 :     reverse_proxy() = default;
     184              : 
     185              :     // Proxy with the options used by forward() calls that do not give their own
     186           69 :     explicit reverse_proxy(proxy_options defaults) : defaults_(std::move(defaults)) {}
     187              : 
     188              :     // Forward the request to `target` and send its response (errors through the error
     189              :     // formatter: 502 if the target cannot be reached or its response is invalid, 504 on a
     190              :     // timeout). Call it before reading the request body, with the response not sent yet.
     191              :     awaitable<proxy_result> forward(request& req, response& res, const proxy_target& target);
     192              :     awaitable<proxy_result> forward(request& req, response& res, const proxy_target& target,
     193              :                                     const proxy_options& options);
     194              : 
     195              :     // Options used by default (set them before forwarding)
     196              :     proxy_options& options() { return defaults_; }
     197              :     const proxy_options& options() const { return defaults_; }
     198              : 
     199              :     // TLS context for targets with the given settings (shared by the targets with the same
     200              :     // CAs and verification); throws if the CAs cannot be loaded
     201              :     std::shared_ptr<boost::asio::ssl::context> tls_context(const proxy_tls_options& tls);
     202              : 
     203              : private:
     204              :     proxy_options defaults_;
     205              : 
     206              :     std::mutex tls_mutex_;
     207              :     std::map<std::tuple<bool, std::string, std::string>, std::shared_ptr<boost::asio::ssl::context>> tls_contexts_;
     208              : };
     209              : 
     210              : } // namespace thinger::http
     211              : 
     212              : #endif // THINGER_HTTP_SERVER_REVERSE_PROXY_HPP
        

Generated by: LCOV version 2.0-1