LCOV - code coverage report
Current view: top level - http/server - virtual_host.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 98.8 % 84 83
Test Date: 2026-11-10 10:38:07 Functions: 100.0 % 15 15

            Line data    Source code
       1              : #include "virtual_host.hpp"
       2              : #include "request.hpp"
       3              : #include "response.hpp"
       4              : #include "file_server/static_files.hpp"
       5              : #include "../../util/ascii.hpp"
       6              : #include <algorithm>
       7              : 
       8              : namespace thinger::http {
       9              : 
      10              : namespace {
      11              : 
      12           54 :     bool is_identifier_start(char c) {
      13           54 :         return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
      14              :     }
      15              : 
      16          126 :     bool is_identifier_char(char c) {
      17          126 :         return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
      18              :     }
      19              : 
      20              :     // Convert a host pattern to a regex: "*" -> one label, ":name" -> one label captured
      21              :     // as `name`, ":name(regex)" -> custom capture; everything else is matched literally
      22           45 :     std::string host_pattern_to_regex(const std::string& pattern, std::vector<std::string>& parameters) {
      23           45 :         static const std::string special = ".^$|()[]{}*+?\\";
      24           45 :         static const std::string label = "([^.]+)";
      25           45 :         std::string regex;
      26              : 
      27          636 :         for (size_t i = 0; i < pattern.size(); ++i) {
      28          591 :             char c = pattern[i];
      29          591 :             if (c == '*') {
      30           30 :                 regex += label;
      31           30 :                 parameters.emplace_back();
      32          561 :             } else if (c == ':' && i + 1 < pattern.size() && is_identifier_start(pattern[i + 1])) {
      33           18 :                 size_t end = i + 1;
      34          126 :                 while (end < pattern.size() && is_identifier_char(pattern[end])) ++end;
      35           18 :                 parameters.push_back(pattern.substr(i + 1, end - i - 1));
      36              : 
      37           18 :                 if (end < pattern.size() && pattern[end] == '(') {
      38              :                     // custom regex up to the matching parenthesis
      39            3 :                     size_t close = end;
      40           48 :                     for (int depth = 0; close < pattern.size(); ++close) {
      41           48 :                         if (pattern[close] == '(') depth++;
      42           45 :                         else if (pattern[close] == ')' && --depth == 0) break;
      43              :                     }
      44            3 :                     regex += "(" + pattern.substr(end + 1, close - end - 1) + ")";
      45            3 :                     end = close + 1;
      46              :                 } else {
      47           15 :                     regex += label;
      48              :                 }
      49           18 :                 i = end - 1;
      50              :             } else {
      51          543 :                 if (special.find(c) != std::string::npos) regex += '\\';
      52          543 :                 regex += c;
      53              :             }
      54              :         }
      55           45 :         return regex;
      56            0 :     }
      57              : 
      58              : }
      59              : 
      60        11193 : virtual_host::virtual_host() {
      61        11193 :     settings_->name = "*";
      62        11193 : }
      63              : 
      64          240 : virtual_host::virtual_host(const std::string& name) {
      65          240 :     auto& host = *settings_;
      66          240 :     host.name = name;
      67          240 :     ::thinger::util::ascii::to_lower(host.name);
      68          240 :     if (is_host_pattern(host.name)) {
      69           45 :         host.pattern = std::regex(host_pattern_to_regex(host.name, host.parameters), std::regex::icase);
      70              :     }
      71          240 : }
      72              : 
      73           27 : virtual_host::virtual_host(const std::string& name, const std::regex& pattern) {
      74           27 :     settings_->name = name;
      75           27 :     settings_->pattern = pattern;
      76           27 :     settings_->parameters.resize(pattern.mark_count());
      77           27 : }
      78              : 
      79          501 : bool virtual_host::is_host_pattern(const std::string& name) {
      80         7266 :     for (size_t i = 0; i < name.size(); ++i) {
      81         6861 :         if (name[i] == '*') return true;
      82         6795 :         if (name[i] == ':' && i + 1 < name.size() && is_identifier_start(name[i + 1])) return true;
      83              :     }
      84          405 :     return false;
      85              : }
      86              : 
      87          474 : std::string virtual_host::normalize_host(std::string_view host) {
      88          474 :     std::string result(host_name(host));
      89          474 :     ::thinger::util::ascii::to_lower(result);
      90          474 :     return result;
      91              : }
      92              : 
      93         4334 : std::string_view virtual_host::host_name(std::string_view host) {
      94              :     // IPv6 literal, with an optional port after the closing bracket
      95         4334 :     bool literal = host.starts_with('[');
      96         4334 :     auto end = literal ? host.find(']') : host.find(':');
      97         4334 :     if (literal && end != std::string_view::npos) ++end;
      98         4334 :     host = host.substr(0, end);
      99         4334 :     constexpr std::string_view spaces = " \t\r\n\v\f";
     100         4334 :     host.remove_prefix(std::min(host.find_first_not_of(spaces), host.size()));
     101         4334 :     host = host.substr(0, host.find_last_not_of(spaces) + 1);
     102         4334 :     if (host.ends_with('.')) host.remove_suffix(1);
     103         4334 :     return host;
     104              : }
     105              : 
     106           99 : bool virtual_host::matches(const std::string& host, std::smatch& matches) const {
     107           99 :     return settings_->pattern && std::regex_match(host, matches, *settings_->pattern);
     108              : }
     109              : 
     110           19 : void virtual_host::schema_component(const std::string& name, nlohmann::json schema) {
     111           19 :     settings_->router.add_schema_component(name, std::move(schema));
     112           19 : }
     113              : 
     114              : // Static file serving
     115          270 : void virtual_host::serve_static(const std::string& url_prefix,
     116              :                                 const std::string& directory,
     117              :                                 const std::string& fallback) {
     118          540 :     serve_static(url_prefix, directory, static_options{.fallback = fallback});
     119          270 : }
     120              : 
     121          426 : void virtual_host::serve_static(const std::string& url_prefix,
     122              :                                 const std::string& directory,
     123              :                                 static_options options) {
     124              :     namespace fs = std::filesystem;
     125              : 
     126              :     // No double slash for the prefix "/"
     127          426 :     std::string route = url_prefix;
     128          426 :     if (!route.empty() && route.back() == '/') route.pop_back();
     129          426 :     route += "/:path(.*)";
     130              : 
     131              :     // HEAD requests reach it too, unless a HEAD route matches them
     132          426 :     if (!options.fallthrough) {
     133          690 :         get(route, [directory = fs::path(directory), options = std::move(options)](request& req, response& res) {
     134         1206 :             file_server::serve(req, res, directory, req["path"], options);
     135          948 :         }).hidden();
     136          345 :         return;
     137              :     }
     138              :     // A coroutine only to wait for the not-found handler. The router outlives it: it owns the route
     139          288 :     get(route, [router = &settings_->router, directory = fs::path(directory), options = std::move(options)](
     140              :                    request& req, response& res) -> thinger::awaitable<void> {
     141              :         if (file_server::serve(req, res, directory, req["path"], options)) co_return;
     142              :         // As if no route had matched it (the middlewares ran once, and do not run again)
     143              :         req.set_matched_route(nullptr);
     144              :         co_await router->handle_unmatched(req, res);
     145          333 :     }).hidden();
     146          426 : }
     147              : 
     148              : } // namespace thinger::http
        

Generated by: LCOV version 2.0-1