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

            Line data    Source code
       1              : #ifndef THINGER_HTTP_VIRTUAL_HOST_HPP
       2              : #define THINGER_HTTP_VIRTUAL_HOST_HPP
       3              : 
       4              : #include "routing/route_handler.hpp"
       5              : #include "routing/route.hpp"
       6              : #include "routing/route_registrar.hpp"
       7              : #include "file_server/static_files.hpp"
       8              : #include <algorithm>
       9              : #include <chrono>
      10              : #include <cstdint>
      11              : #include <filesystem>
      12              : #include <memory>
      13              : #include <optional>
      14              : #include <regex>
      15              : #include <string>
      16              : #include <utility>
      17              : #include <vector>
      18              : 
      19              : namespace thinger::http {
      20              : 
      21              : class route_group;
      22              : 
      23              : // Set of routes served for a host name (see http_application::host). The application
      24              : // itself is the default virtual host: its routes answer the requests for hosts not registered.
      25              : class virtual_host : public route_registrar<virtual_host> {
      26              : public:
      27              :     // Default host ("*")
      28              :     virtual_host();
      29              : 
      30              :     // Host registered by name: an exact name ("api.example.com"), or a pattern where "*"
      31              :     // matches one label ("*.example.com") and ":name" or ":name(regex)" (a regex without
      32              :     // capture groups) a label available as a request parameter (":tenant.example.com" ->
      33              :     // req["tenant"]). Case-insensitive.
      34              :     explicit virtual_host(const std::string& name);
      35              : 
      36              :     // Host matched by a regular expression on the whole host name (case-insensitive);
      37              :     // its capture groups are available through request::get_host_matches()
      38              :     virtual_host(const std::string& name, const std::regex& pattern);
      39              : 
      40        34098 :     virtual ~virtual_host() = default;
      41              :     virtual_host(const virtual_host&) = delete;
      42              :     virtual_host& operator=(const virtual_host&) = delete;
      43              : 
      44              :     // Route registration: get, post, put, del, patch, head and options (see route_registrar)
      45              : 
      46              :     // Group of routes sharing a path prefix, tags and metadata (see route_group)
      47              :     route_group group(const std::string& prefix);
      48              : 
      49              :     // Shared JSON schema (OpenAPI component) that route schemas can reference with
      50              :     // {"$ref": "#/components/schemas/<name>"}. Register it before the routes using it.
      51              :     void schema_component(const std::string& name, nlohmann::json schema);
      52              : 
      53              :     // Static files of `directory` served under `url_prefix` (GET, and HEAD through it), as
      54              :     // response::send_file does: with ETag and Last-Modified, answering conditional requests
      55              :     // with 304 and single byte ranges with 206, or their precompressed siblings (see
      56              :     // static_options). Paths resolving outside the directory (through symlinks) are
      57              :     // answered with 403. A route is chosen once per request, so with static_options::fallthrough
      58              :     // a path without a file goes to the not-found handler, not to another serve_static().
      59              :     void serve_static(const std::string& url_prefix,
      60              :                       const std::string& directory,
      61              :                       const std::string& fallback = "index.html");
      62              :     void serve_static(const std::string& url_prefix,
      63              :                       const std::string& directory,
      64              :                       static_options options);
      65              : 
      66              :     // Fallback handler, called instead of answering 404/405 when no route matches: takes
      67              :     // (request&, response&) or (response&), synchronous or coroutine. nullptr (or an empty
      68              :     // std::function) removes it.
      69              :     template<request_handler_callable F>
      70          848 :     void set_not_found_handler(F&& handler) {
      71          848 :         settings_->router.set_fallback_handler(std::forward<F>(handler));
      72          848 :     }
      73            3 :     void set_not_found_handler(std::nullptr_t) { settings_->router.set_fallback_handler(nullptr); }
      74              : 
      75              :     // Limits of the requests served by this host: the default of its routes (a group or a
      76              :     // route can set its own, see route::max_body_size, route::timeout, route::request_timeout,
      77              :     // route::decode_limits), and the limits of its not-found handler (e.g. a reverse proxy for
      78              :     // the whole host). Unset, the application ones apply (http_application::set_max_body_size,
      79              :     // set_body_limits; no timeout; the request deadline of the connection, see
      80              :     // connection_options::request_timeout). Precedence: route > group > host > application. On the
      81              :     // application itself, they are those of the default host only. Set before serving.
      82           18 :     virtual_host& max_body_size(std::uint64_t size) {
      83           18 :         settings_->max_body_size = size;
      84           18 :         return *this;
      85              :     }
      86              : 
      87           18 :     virtual_host& timeout(std::chrono::milliseconds timeout,
      88              :                           http_response::status status = http_response::status::gateway_timeout) {
      89           18 :         settings_->timeout = std::max(timeout, std::chrono::milliseconds{0});
      90           18 :         settings_->timeout_status = status;
      91           18 :         return *this;
      92              :     }
      93              : 
      94           12 :     virtual_host& request_timeout(std::chrono::milliseconds timeout) {
      95           12 :         settings_->request_timeout = std::max(timeout, std::chrono::milliseconds{0});
      96           12 :         return *this;
      97              :     }
      98              : 
      99            9 :     virtual_host& decode_limits(body_limits limits) {
     100            9 :         settings_->decode_limits = limits;
     101            9 :         return *this;
     102              :     }
     103              : 
     104              :     // Limits set on the host (nullopt if unset)
     105        26824 :     const std::optional<std::uint64_t>& get_max_body_size() const { return settings_->max_body_size; }
     106        26899 :     const std::optional<std::chrono::milliseconds>& get_timeout() const { return settings_->timeout; }
     107           30 :     http_response::status get_timeout_status() const { return settings_->timeout_status; }
     108        27112 :     const std::optional<std::chrono::milliseconds>& get_request_timeout() const { return settings_->request_timeout; }
     109          444 :     const std::optional<body_limits>& get_decode_limits() const { return settings_->decode_limits; }
     110              : 
     111              :     // Name the host was registered with ("*" for the default host)
     112           30 :     const std::string& name() const { return settings_->name; }
     113              : 
     114              :     // Whether the host is matched by a pattern instead of its exact name
     115            9 :     bool is_pattern() const { return settings_->pattern.has_value(); }
     116              : 
     117              :     // Match a host name (lowercase, without port) against the pattern; `matches` gets the
     118              :     // whole host and the captures. Always false for exact and default hosts.
     119              :     bool matches(const std::string& host, std::smatch& matches) const;
     120              : 
     121              :     // Names of the pattern captures, in order (empty for unnamed ones, such as "*")
     122           63 :     const std::vector<std::string>& get_parameters() const { return settings_->parameters; }
     123              : 
     124              :     // Access to router for advanced use cases (e.g. an openapi_generator for this host)
     125        39851 :     route_handler& router() { return settings_->router; }
     126            2 :     const route_handler& router() const { return settings_->router; }
     127              : 
     128              :     // Whether a host name is a pattern for virtual_host(name): it has "*" or ":name"
     129              :     static bool is_host_pattern(const std::string& name);
     130              : 
     131              :     // Host name of a Host header value: lowercase, without port nor trailing dot
     132              :     // ("API.Example.com:8080" -> "api.example.com", "[::1]:80" -> "[::1]")
     133              :     static std::string normalize_host(std::string_view host);
     134              :     // The same name in its own case, without a copy (part of `host`)
     135              :     static std::string_view host_name(std::string_view host);
     136              : 
     137              : private:
     138              :     friend class route_registrar<virtual_host>;
     139              :     // The application keeps a host of its own sharing the settings of its default host (itself)
     140              :     friend class http_application;
     141              : 
     142              :     // What a request served by the host reads: shared, so the requests of a server destroyed
     143              :     // while they run still find it (see http_application)
     144              :     struct settings {
     145              :         route_handler router;
     146              :         std::string name;
     147              :         std::optional<std::regex> pattern;
     148              :         std::vector<std::string> parameters;
     149              : 
     150              :         // Limits of the host (unset: the application ones)
     151              :         std::optional<std::uint64_t> max_body_size;
     152              :         std::optional<std::chrono::milliseconds> timeout;
     153              :         http_response::status timeout_status = http_response::status::gateway_timeout;
     154              :         std::optional<std::chrono::milliseconds> request_timeout;
     155              :         std::optional<body_limits> decode_limits;
     156              :     };
     157              : 
     158        11193 :     explicit virtual_host(std::shared_ptr<settings> shared) : settings_(std::move(shared)) {}
     159              : 
     160        31756 :     route& make_route(method http_method, const std::string& path) {
     161        31756 :         return settings_->router[http_method][path];
     162              :     }
     163              : 
     164              :     std::shared_ptr<settings> settings_ = std::make_shared<settings>();
     165              : };
     166              : 
     167              : // Routes registered through a group get its path prefix, and inherit its tags, metadata
     168              : // and limits (a route can still override a metadata key with its own meta(), or a limit).
     169              : // They are applied as each route is registered: set them on the group first.
     170              : class route_group : public route_registrar<route_group> {
     171              : public:
     172          122 :     route_group(virtual_host& host, std::string prefix)
     173          122 :         : host_(host), prefix_(std::move(prefix)) {}
     174              : 
     175           41 :     route_group& tag(const std::string& name) {
     176           41 :         tags_.push_back(name);
     177           41 :         return *this;
     178              :     }
     179              : 
     180            6 :     route_group& meta(const std::string& key, nlohmann::json value) {
     181            6 :         metadata_[key] = std::move(value);
     182            6 :         return *this;
     183              :     }
     184              : 
     185              :     // Limits of the routes registered afterwards (see route::max_body_size, route::timeout,
     186              :     // route::request_timeout and route::decode_limits)
     187           12 :     route_group& max_body_size(std::uint64_t size) {
     188           12 :         max_body_size_ = size;
     189           12 :         return *this;
     190              :     }
     191              : 
     192            3 :     route_group& timeout(std::chrono::milliseconds timeout,
     193              :                          http_response::status status = http_response::status::gateway_timeout) {
     194            3 :         timeout_ = {timeout, status};
     195            3 :         return *this;
     196              :     }
     197              : 
     198           24 :     route_group& request_timeout(std::chrono::milliseconds timeout) {
     199           24 :         request_timeout_ = timeout;
     200           24 :         return *this;
     201              :     }
     202              : 
     203            3 :     route_group& decode_limits(body_limits limits) {
     204            3 :         decode_limits_ = limits;
     205            3 :         return *this;
     206              :     }
     207              : 
     208              :     // Nested group: prefix appended, tags, metadata and limits inherited
     209            9 :     route_group group(const std::string& prefix) const {
     210            9 :         route_group nested(*this);
     211            9 :         nested.prefix_ += prefix;
     212            9 :         return nested;
     213            0 :     }
     214              : 
     215              :     const std::string& prefix() const { return prefix_; }
     216              : 
     217              : private:
     218              :     friend class route_registrar<route_group>;
     219          352 :     route& make_route(method http_method, const std::string& path) {
     220          352 :         auto& r = host_.router()[http_method][prefix_ + path];
     221          352 :         r.tags(tags_);
     222          370 :         for (const auto& [key, value] : metadata_.items()) {
     223           18 :             r.meta(key, value);
     224          352 :         }
     225          352 :         if (max_body_size_) r.max_body_size(*max_body_size_);
     226          352 :         if (timeout_) r.timeout(timeout_->first, timeout_->second);
     227          352 :         if (request_timeout_) r.request_timeout(*request_timeout_);
     228          352 :         if (decode_limits_) r.decode_limits(*decode_limits_);
     229          352 :         return r;
     230              :     }
     231              : 
     232              :     virtual_host& host_;
     233              :     std::string prefix_;
     234              :     std::vector<std::string> tags_;
     235              :     nlohmann::json metadata_ = nlohmann::json::object();
     236              :     std::optional<std::uint64_t> max_body_size_;
     237              :     std::optional<std::pair<std::chrono::milliseconds, http_response::status>> timeout_;
     238              :     std::optional<std::chrono::milliseconds> request_timeout_;
     239              :     std::optional<body_limits> decode_limits_;
     240              : };
     241              : 
     242          122 : inline route_group virtual_host::group(const std::string& prefix) {
     243          122 :     return route_group(*this, prefix);
     244              : }
     245              : 
     246              : } // namespace thinger::http
     247              : 
     248              : #endif // THINGER_HTTP_VIRTUAL_HOST_HPP
        

Generated by: LCOV version 2.0-1