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
|