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
|