/src/glaze/include/glaze/net/http_router.hpp
Line | Count | Source |
1 | | // Glaze Library |
2 | | // For the license information refer to glaze.hpp |
3 | | |
4 | | #pragma once |
5 | | |
6 | | #include <algorithm> |
7 | | #include <functional> |
8 | | #include <future> |
9 | | #include <iostream> |
10 | | #include <memory> |
11 | | #include <optional> |
12 | | #include <source_location> |
13 | | #include <string> |
14 | | #include <string_view> |
15 | | #include <unordered_map> |
16 | | #include <vector> |
17 | | |
18 | | #include "glaze/json/generic.hpp" |
19 | | #include "glaze/net/http.hpp" |
20 | | #include "glaze/net/http_headers.hpp" |
21 | | #include "glaze/net/url.hpp" |
22 | | #include "glaze/util/key_transformers.hpp" |
23 | | |
24 | | // To deconflict Windows.h, which defines a DELETE macro that collides with the |
25 | | // http_method::DELETE used in the route helpers below. http.hpp's own undef is |
26 | | // include-guarded, so it does not re-run when a later include (e.g. asio via |
27 | | // http_client.hpp) pulls in Windows.h before this header is parsed. |
28 | | #ifdef DELETE |
29 | | #undef DELETE |
30 | | #endif |
31 | | |
32 | | namespace glz |
33 | | { |
34 | | namespace detail |
35 | | { |
36 | | struct request_line |
37 | | { |
38 | | http_method method; |
39 | | std::string_view target; |
40 | | bool is_http11; |
41 | | }; |
42 | | } |
43 | | |
44 | | // Request context object |
45 | | struct request |
46 | | { |
47 | | http_method method{}; |
48 | | std::string target{}; // Full request target (path + query string) |
49 | | std::string path{}; // Path component only (without query string) |
50 | | std::unordered_map<std::string, std::string> params{}; // Path parameters (e.g., :id) |
51 | | std::unordered_map<std::string, std::string> query{}; // Query parameters (e.g., ?limit=10) |
52 | | glz::http_headers headers{}; |
53 | | std::string body{}; |
54 | | std::string remote_ip{}; |
55 | | uint16_t remote_port{}; |
56 | | }; |
57 | | |
58 | | // Serialized to the response body when response::body<Opts>(value) fails |
59 | | // to write `value`. Declared at namespace scope so glaze's reflection can |
60 | | // name the type; function-local types are not reflectable on GCC. |
61 | | struct glz_write_error |
62 | | { |
63 | | std::string_view error{"glaze write failure"}; |
64 | | uint32_t code{}; |
65 | | std::string_view message{}; |
66 | | }; |
67 | | |
68 | | // Response builder |
69 | | struct response |
70 | | { |
71 | | enum header_flag : uint8_t { |
72 | | has_content_length = 1, |
73 | | has_date = 2, |
74 | | has_server = 4, |
75 | | has_connection = 8, |
76 | | }; |
77 | | |
78 | | int status_code = 200; |
79 | | glz::http_headers response_headers{}; |
80 | | std::string response_body{}; |
81 | | uint8_t user_headers_set{}; |
82 | | |
83 | | inline response& status(int code) |
84 | 0 | { |
85 | 0 | status_code = code; |
86 | 0 | return *this; |
87 | 0 | } |
88 | | |
89 | | // Replaces any existing field with this name. |
90 | | // |
91 | | // A field-name or field-value carrying CR or LF would terminate the field on |
92 | | // the wire, letting attacker-influenced data inject extra headers or a body |
93 | | // (CWE-113); header_field_has_crlf carries the full rationale. Rejecting it |
94 | | // here keeps it out of the container and, crucially, skips the |
95 | | // mark_user_supplied bookkeeping below - see that function for why the order |
96 | | // of these two lines is load-bearing. The wire serializers keep an |
97 | | // independent drop as a backstop for fields that bypass this setter. |
98 | | inline response& header(std::string_view name, std::string_view value) |
99 | 0 | { |
100 | 0 | if (header_field_has_crlf(name, value)) [[unlikely]] { |
101 | 0 | return *this; |
102 | 0 | } |
103 | 0 |
|
104 | 0 | mark_user_supplied(name); |
105 | 0 | response_headers.set(std::string(name), std::string(value)); |
106 | 0 | return *this; |
107 | 0 | } |
108 | | |
109 | | // Appends instead of replacing, for names that can repeat like Set-Cookie. |
110 | | // Content-Length and Transfer-Encoding are replaced regardless: a second one leaves |
111 | | // the body length ambiguous and opens response smuggling (RFC 9112 6.3). |
112 | | inline response& add_header(std::string_view name, std::string_view value) |
113 | 0 | { |
114 | 0 | if (header_field_has_crlf(name, value)) [[unlikely]] { |
115 | 0 | return *this; |
116 | 0 | } |
117 | 0 |
|
118 | 0 | mark_user_supplied(name); |
119 | 0 |
|
120 | 0 | if (header_field_frames_body(name)) [[unlikely]] { |
121 | 0 | response_headers.set(std::string(name), std::string(value)); |
122 | 0 | } |
123 | 0 | else { |
124 | 0 | response_headers.add(std::string(name), std::string(value)); |
125 | 0 | } |
126 | 0 | return *this; |
127 | 0 | } |
128 | | |
129 | | inline response& body(std::string_view content) |
130 | 0 | { |
131 | 0 | response_body.assign(content.data(), content.size()); |
132 | 0 | return *this; |
133 | 0 | } |
134 | | |
135 | | // Use glz::opts for format deduction and serialization |
136 | | // my_response.res<Opts>(value); |
137 | | template <auto Opts, class T> |
138 | | response& body(T&& value) |
139 | | { |
140 | | if constexpr (Opts.format == JSON) { |
141 | | content_type("application/json"); |
142 | | } |
143 | | else if constexpr (Opts.format == BEVE) { |
144 | | content_type("application/beve"); |
145 | | } |
146 | | auto ec = glz::write<Opts>(std::forward<T>(value), response_body); |
147 | | if (ec) { |
148 | | // The body may have been partially written before the error |
149 | | // fired; clear it so the response is never a mix of a partial |
150 | | // success payload and the error report. |
151 | | response_body.clear(); |
152 | | |
153 | | // Serialize a structured glz_write_error object instead of the |
154 | | // legacy hardcoded placeholder. The original error_code and |
155 | | // any custom_error_message are surfaced so callers can debug |
156 | | // failed writes without having to reproduce them locally. |
157 | | glz_write_error info{ |
158 | | .code = uint32_t(ec.ec), |
159 | | .message = ec.custom_error_message, |
160 | | }; |
161 | | if (auto write_ec = glz::write_json(info, response_body); write_ec) { |
162 | | // Re-erroring on a tiny struct is essentially impossible, |
163 | | // but keep a safe static fallback so the response body is |
164 | | // never left empty after a failed write attempt. |
165 | | response_body = R"({"error":"glaze write failure"})"; |
166 | | } |
167 | | } |
168 | | return *this; |
169 | | } |
170 | | |
171 | | // Reset response for reuse, preserving allocated capacity |
172 | | void clear() |
173 | 0 | { |
174 | 0 | status_code = 200; |
175 | 0 | response_headers.clear(); |
176 | 0 | response_body.clear(); // preserves capacity |
177 | 0 | user_headers_set = 0; |
178 | 0 | } |
179 | | |
180 | 0 | inline response& content_type(std::string_view type) { return header("Content-Type", type); } |
181 | | |
182 | | // JSON response helper using Glaze |
183 | | template <class T = glz::generic> |
184 | | response& json(T&& value) |
185 | | { |
186 | | content_type("application/json"); |
187 | | auto ec = glz::write_json(std::forward<T>(value), response_body); |
188 | | if (ec) { |
189 | | response_body = R"({"error":"glz::write_json error"})"; // rare that this would ever happen |
190 | | } |
191 | | return *this; |
192 | | } |
193 | | |
194 | | private: |
195 | | // Records that the handler supplied one of the headers the wire serializer |
196 | | // would otherwise generate, so the serializer leaves it alone. Only reached |
197 | | // once the field has passed the CR/LF check: a field dropped there must not |
198 | | // set its flag, or a rejected Content-Length or Connection would suppress |
199 | | // the auto-generated counterpart and leave the response unframed. |
200 | | void mark_user_supplied(std::string_view name) noexcept |
201 | 0 | { |
202 | 0 | if (glz::striequal(name, "content-length")) |
203 | 0 | user_headers_set |= has_content_length; |
204 | 0 | else if (glz::striequal(name, "date")) |
205 | 0 | user_headers_set |= has_date; |
206 | 0 | else if (glz::striequal(name, "server")) |
207 | 0 | user_headers_set |= has_server; |
208 | 0 | else if (glz::striequal(name, "connection")) |
209 | 0 | user_headers_set |= has_connection; |
210 | 0 | } |
211 | | }; |
212 | | |
213 | | using handler = std::function<void(const request&, response&)>; |
214 | | using async_handler = std::function<std::future<void>(const request&, response&)>; |
215 | | using error_handler = std::function<void(std::error_code, std::source_location)>; |
216 | | |
217 | | // Forward declarations for streaming and WebSocket support. |
218 | | // Full definitions live in glaze/net/http_server.hpp and glaze/net/websocket_connection.hpp. |
219 | | // Forward declarations are sufficient here because streaming_handler holds streaming_response |
220 | | // by reference and websocket_handler holds websocket_server through std::shared_ptr. |
221 | | struct streaming_response; |
222 | | struct websocket_server; |
223 | | |
224 | | // Streaming handler signature. Streaming routes take over the connection lifecycle |
225 | | // (no keep-alive) and write chunked responses through streaming_response. |
226 | | using streaming_handler = std::function<void(request&, streaming_response&)>; |
227 | | |
228 | | // WebSocket handler value: a websocket_server instance is bound to a path. The HTTP |
229 | | // server detects the upgrade handshake (Upgrade: websocket) and dispatches to the |
230 | | // matching server entry. |
231 | | using websocket_handler = std::shared_ptr<websocket_server>; |
232 | | |
233 | | /** |
234 | | * @brief Parameter constraint for route validation |
235 | | * |
236 | | * Defines validation rules for route parameters using a validation function. |
237 | | */ |
238 | | struct param_constraint |
239 | | { |
240 | | /** |
241 | | * @brief Human-readable description of the constraint |
242 | | * |
243 | | * Used for OpenAPI parameter descriptions and debugging output. |
244 | | */ |
245 | | std::string description{}; |
246 | | |
247 | | /** |
248 | | * @brief Validation function for parameter values |
249 | | * |
250 | | * This function is used to validate parameter values. It should return true if the parameter value is valid, |
251 | | * false otherwise. |
252 | | */ |
253 | | std::function<bool(std::string_view)> validation = [](std::string_view) { return true; }; |
254 | | }; |
255 | | |
256 | | /** |
257 | | * @brief An entry for a registered route. |
258 | | */ |
259 | | struct route_spec |
260 | | { |
261 | | std::string description{}; |
262 | | std::vector<std::string> tags{}; |
263 | | std::unordered_map<std::string, param_constraint> constraints{}; |
264 | | |
265 | | // Type information for schema generation |
266 | | std::optional<std::string> request_body_schema{}; |
267 | | std::optional<std::string> response_schema{}; |
268 | | std::optional<std::string> request_body_type_name{}; |
269 | | std::optional<std::string> response_type_name{}; |
270 | | }; |
271 | | |
272 | | /** |
273 | | * @brief Generic radix-tree route table parameterized over the stored handler type. |
274 | | * |
275 | | * route_table provides path matching with support for static, parameterized (":param"), |
276 | | * and wildcard ("*name") segments. It is used as a building block by basic_http_router |
277 | | * to store normal routes (typed by Handler), streaming routes (streaming_handler), and |
278 | | * WebSocket routes (websocket_handler). |
279 | | * |
280 | | * @tparam H The stored handler type. Must be default-constructible (the empty value |
281 | | * returned by match() when no route matches is H{}). |
282 | | */ |
283 | | template <class H> |
284 | | struct route_table |
285 | | { |
286 | | /** |
287 | | * @brief Stored route entry: handler plus optional spec for schema/constraints. |
288 | | */ |
289 | | struct route_entry |
290 | | { |
291 | | H handle{}; |
292 | | route_spec spec{}; |
293 | | }; |
294 | | |
295 | | /** |
296 | | * @brief Node in the radix tree routing structure |
297 | | * |
298 | | * Each node represents a segment of a path, which can be a static string, |
299 | | * a parameter (prefixed with ":"), or a wildcard (prefixed with "*"). |
300 | | */ |
301 | | struct radix_node |
302 | | { |
303 | | std::string segment; |
304 | | bool is_parameter = false; |
305 | | bool is_wildcard = false; |
306 | | std::string parameter_name; |
307 | | std::unordered_map<std::string, std::unique_ptr<radix_node>> static_children; |
308 | | std::unique_ptr<radix_node> parameter_child; |
309 | | std::unique_ptr<radix_node> wildcard_child; |
310 | | std::unordered_map<http_method, H> handlers; |
311 | | std::unordered_map<http_method, std::unordered_map<std::string, param_constraint>> constraints; |
312 | | bool is_endpoint = false; |
313 | | std::string full_path; |
314 | | |
315 | | std::string to_string() const |
316 | | { |
317 | | std::string result; |
318 | | result.reserve(80 + segment.size() + full_path.size()); |
319 | | |
320 | | result.append("Node["); |
321 | | result.append(is_parameter ? "PARAM:" : (is_wildcard ? "WILD:" : "")); |
322 | | result.append(segment); |
323 | | result.append(", endpoint="); |
324 | | result.append(is_endpoint ? "true" : "false"); |
325 | | result.append(", children="); |
326 | | result.append(std::to_string(static_children.size())); |
327 | | result.append(parameter_child ? "+param" : ""); |
328 | | result.append(wildcard_child ? "+wild" : ""); |
329 | | result.append(", full_path="); |
330 | | result.append(full_path); |
331 | | result.append("]"); |
332 | | return result; |
333 | | } |
334 | | }; |
335 | | |
336 | | /** |
337 | | * @brief Map of registered routes, keyed by full path then HTTP method. |
338 | | * |
339 | | * Public for backward compatibility and to support introspection (e.g., the |
340 | | * OpenAPI generator iterates this map). |
341 | | */ |
342 | | std::unordered_map<std::string, std::unordered_map<http_method, route_entry>> routes; |
343 | | |
344 | | /** |
345 | | * @brief Split a path into segments |
346 | | * |
347 | | * Splits a path like "/users/:id/profile" into ["users", ":id", "profile"]. |
348 | | * This is the canonical implementation; basic_http_router::split_path is a |
349 | | * thin backward-compatible wrapper that delegates here. |
350 | | * |
351 | | * @param path The path to split |
352 | | * @return Vector of path segments |
353 | | */ |
354 | | static std::vector<std::string> split_path(std::string_view path) |
355 | | { |
356 | | std::vector<std::string> segments; |
357 | | segments.reserve(std::count(path.begin(), path.end(), '/') + 1); |
358 | | |
359 | | size_t start = 0; |
360 | | while (start < path.size()) { |
361 | | if (path[start] == '/') { |
362 | | start++; |
363 | | continue; |
364 | | } |
365 | | |
366 | | size_t end = path.find('/', start); |
367 | | if (end == std::string::npos) end = path.size(); |
368 | | |
369 | | segments.push_back(std::string(path.substr(start, end - start))); |
370 | | start = end; |
371 | | } |
372 | | |
373 | | return segments; |
374 | | } |
375 | | |
376 | | /** |
377 | | * @brief Detect a ".." path-traversal component in a decoded capture. |
378 | | * |
379 | | * Returns true when `path` contains a ".." segment delimited by a |
380 | | * separator or a string boundary. Percent-decoding happens after the |
381 | | * target is split on literal '/', so a "%2e%2e%2f" sequence only becomes |
382 | | * a "../" here; a capture carrying such a segment can climb out of a base |
383 | | * directory once a handler resolves it as a filesystem path. |
384 | | * |
385 | | * Both '/' and '\\' are treated as separators: on Windows the backslash |
386 | | * is a path separator too, so a "%2e%2e%5c" ("..\") capture traverses the |
387 | | * same way and an encoded backslash would otherwise slip past a '/'-only |
388 | | * check. A backslash is a legal byte in a POSIX filename, but a capture |
389 | | * bound for a filesystem join is exactly the case guarded here, so the |
390 | | * traversal reading wins over the rare literal-backslash name. |
391 | | */ |
392 | | static bool has_dot_dot_segment(std::string_view path) noexcept |
393 | | { |
394 | | size_t start = 0; |
395 | | while (true) { |
396 | | const size_t sep = path.find_first_of("/\\", start); |
397 | | const size_t seg_len = (sep == std::string_view::npos ? path.size() : sep) - start; |
398 | | if (seg_len == 2 && path[start] == '.' && path[start + 1] == '.') { |
399 | | return true; |
400 | | } |
401 | | if (sep == std::string_view::npos) { |
402 | | return false; |
403 | | } |
404 | | start = sep + 1; |
405 | | } |
406 | | } |
407 | | |
408 | | /** |
409 | | * @brief Register a route in the table. |
410 | | * |
411 | | * @param method The HTTP method (GET, POST, etc.) |
412 | | * @param path The route path, may contain ":param" or trailing "*name" wildcards. |
413 | | * @param handle The handler to associate with the route. |
414 | | * @param spec Optional spec for the route. |
415 | | */ |
416 | | void add(http_method method, std::string_view path, H handle, const route_spec& spec = {}) |
417 | | { |
418 | | try { |
419 | | // Install in the radix tree first. add_route can throw on conflict |
420 | | // (duplicate :param or *wildcard names at the same position); if it |
421 | | // throws we want the routes map to stay in sync with the tree, not |
422 | | // to silently retain an entry that iteration sees but match() never |
423 | | // reaches. Pass handle by value so we still own a copy to move into |
424 | | // the routes entry below. |
425 | | add_route(method, path, handle, spec.constraints); |
426 | | |
427 | | auto& entry = routes[std::string(path)][method]; |
428 | | entry.handle = std::move(handle); |
429 | | entry.spec = spec; |
430 | | } |
431 | | catch (const std::exception& e) { |
432 | | std::fprintf(stderr, "Error adding route '%.*s': %s\n", static_cast<int>(path.length()), path.data(), |
433 | | e.what()); |
434 | | } |
435 | | } |
436 | | |
437 | | /** |
438 | | * @brief Match a target against the registered routes. |
439 | | * |
440 | | * @param method The HTTP method of the request. |
441 | | * @param target The request target (may include a query string). |
442 | | * @return Pair of (matched handler, extracted path parameters). The handler is |
443 | | * a default-constructed H if no route matched. |
444 | | */ |
445 | | std::pair<H, std::unordered_map<std::string, std::string>> match(http_method method, |
446 | | std::string_view target) const |
447 | | { |
448 | | std::unordered_map<std::string, std::string> params; |
449 | | H result{}; |
450 | | |
451 | | // Strip query string from target for matching |
452 | | const auto [path, query_string] = split_target(target); |
453 | | |
454 | | // First try direct lookup for non-parameterized routes (optimization) |
455 | | auto direct_it = direct_routes.find(std::string(path)); |
456 | | if (direct_it != direct_routes.end()) { |
457 | | auto method_it = direct_it->second.find(method); |
458 | | if (method_it != direct_it->second.end()) { |
459 | | return {method_it->second, params}; |
460 | | } |
461 | | } |
462 | | |
463 | | std::vector<std::string> segments = split_path(path); |
464 | | match_node(&root, segments, 0, method, params, result); |
465 | | |
466 | | return {result, params}; |
467 | | } |
468 | | |
469 | | /** |
470 | | * @brief Print the entire tree structure for debugging. |
471 | | * |
472 | | * Prints only the tree contents. The caller is responsible for any header |
473 | | * (basic_http_router::print_tree groups three of these under labelled sections). |
474 | | */ |
475 | | void print_tree() const { print_node(&root, 0); } |
476 | | |
477 | | private: |
478 | | mutable radix_node root; |
479 | | std::unordered_map<std::string, std::unordered_map<http_method, H>> direct_routes; |
480 | | |
481 | | void add_route(http_method method, std::string_view path, H handle, |
482 | | const std::unordered_map<std::string, param_constraint>& constraints = {}) |
483 | | { |
484 | | std::string path_str(path); |
485 | | |
486 | | if (path_str.find(':') == std::string::npos && path_str.find('*') == std::string::npos) { |
487 | | direct_routes[path_str][method] = handle; |
488 | | return; |
489 | | } |
490 | | |
491 | | std::vector<std::string> segments = split_path(path); |
492 | | |
493 | | radix_node* current = &root; |
494 | | |
495 | | for (size_t i = 0; i < segments.size(); ++i) { |
496 | | const std::string& segment = segments[i]; |
497 | | |
498 | | if (segment.empty()) continue; |
499 | | |
500 | | if (segment[0] == ':') { |
501 | | std::string param_name = segment.substr(1); |
502 | | |
503 | | if (!current->parameter_child) { |
504 | | current->parameter_child = std::make_unique<radix_node>(); |
505 | | current->parameter_child->is_parameter = true; |
506 | | current->parameter_child->parameter_name = param_name; |
507 | | current->parameter_child->segment = segment; |
508 | | current->parameter_child->full_path = current->full_path + "/" + segment; |
509 | | } |
510 | | else if (current->parameter_child->parameter_name != param_name) { |
511 | | throw std::runtime_error("Route conflict: different parameter names at same position: :" + |
512 | | current->parameter_child->parameter_name + " vs :" + param_name); |
513 | | } |
514 | | |
515 | | current = current->parameter_child.get(); |
516 | | } |
517 | | else if (segment[0] == '*') { |
518 | | std::string wildcard_name = segment.substr(1); |
519 | | |
520 | | if (i != segments.size() - 1) { |
521 | | throw std::runtime_error("Wildcard must be the last segment in route: " + path_str); |
522 | | } |
523 | | |
524 | | if (!current->wildcard_child) { |
525 | | current->wildcard_child = std::make_unique<radix_node>(); |
526 | | current->wildcard_child->is_wildcard = true; |
527 | | current->wildcard_child->parameter_name = wildcard_name; |
528 | | current->wildcard_child->segment = segment; |
529 | | current->wildcard_child->full_path = current->full_path + "/" + segment; |
530 | | } |
531 | | else if (current->wildcard_child->parameter_name != wildcard_name) { |
532 | | throw std::runtime_error("Route conflict: different wildcard names at same position: *" + |
533 | | current->wildcard_child->parameter_name + " vs *" + wildcard_name); |
534 | | } |
535 | | |
536 | | current = current->wildcard_child.get(); |
537 | | break; |
538 | | } |
539 | | else { |
540 | | if (current->static_children.find(segment) == current->static_children.end()) { |
541 | | current->static_children[segment] = std::make_unique<radix_node>(); |
542 | | current->static_children[segment]->segment = segment; |
543 | | current->static_children[segment]->full_path = current->full_path + "/" + segment; |
544 | | } |
545 | | |
546 | | current = current->static_children[segment].get(); |
547 | | } |
548 | | } |
549 | | |
550 | | current->is_endpoint = true; |
551 | | current->handlers[method] = handle; |
552 | | |
553 | | if (!constraints.empty()) { |
554 | | current->constraints[method] = constraints; |
555 | | } |
556 | | } |
557 | | |
558 | | bool match_node(radix_node* node, const std::vector<std::string>& segments, size_t index, http_method method, |
559 | | std::unordered_map<std::string, std::string>& params, H& result) const |
560 | | { |
561 | | if (index == segments.size()) { |
562 | | if (node->is_endpoint) { |
563 | | auto it = node->handlers.find(method); |
564 | | if (it != node->handlers.end()) { |
565 | | bool constraints_passed = true; |
566 | | auto constraints_it = node->constraints.find(method); |
567 | | if (constraints_it != node->constraints.end()) { |
568 | | for (const auto& [param_name, constraint] : constraints_it->second) { |
569 | | auto param_it = params.find(param_name); |
570 | | if (param_it != params.end()) { |
571 | | const std::string& value = param_it->second; |
572 | | if (!constraint.validation(value)) { |
573 | | constraints_passed = false; |
574 | | break; |
575 | | } |
576 | | } |
577 | | } |
578 | | } |
579 | | |
580 | | if (constraints_passed) { |
581 | | result = it->second; |
582 | | return true; |
583 | | } |
584 | | return false; |
585 | | } |
586 | | } |
587 | | return false; |
588 | | } |
589 | | |
590 | | const std::string& segment = segments[index]; |
591 | | |
592 | | auto static_it = node->static_children.find(segment); |
593 | | if (static_it != node->static_children.end()) { |
594 | | if (match_node(static_it->second.get(), segments, index + 1, method, params, result)) { |
595 | | return true; |
596 | | } |
597 | | } |
598 | | |
599 | | if (node->parameter_child) { |
600 | | std::string decoded = url_decode(segment); |
601 | | |
602 | | // A ":param" captures a single segment; refuse a decoded ".." |
603 | | // component so the value cannot escape a base directory when a |
604 | | // handler treats it as a path. |
605 | | if (!has_dot_dot_segment(decoded)) { |
606 | | std::string param_name = node->parameter_child->parameter_name; |
607 | | params[param_name] = std::move(decoded); |
608 | | |
609 | | if (match_node(node->parameter_child.get(), segments, index + 1, method, params, result)) { |
610 | | return true; |
611 | | } |
612 | | |
613 | | params.erase(param_name); |
614 | | } |
615 | | } |
616 | | |
617 | | if (node->wildcard_child) { |
618 | | std::string full_capture; |
619 | | for (size_t i = index; i < segments.size(); i++) { |
620 | | if (i > index) full_capture += "/"; |
621 | | full_capture += url_decode(segments[i]); |
622 | | } |
623 | | |
624 | | // The capture is joined from decoded segments, so a "%2e%2e%2f" in |
625 | | // the request only resolves to a ".." here; refuse it so a mount |
626 | | // like "/files/*path" cannot be walked outside its base directory. |
627 | | if (has_dot_dot_segment(full_capture)) { |
628 | | return false; |
629 | | } |
630 | | |
631 | | const auto& wildcard_name = node->wildcard_child->parameter_name; |
632 | | params[wildcard_name] = full_capture; |
633 | | |
634 | | if (node->wildcard_child->is_endpoint) { |
635 | | auto it = node->wildcard_child->handlers.find(method); |
636 | | if (it != node->wildcard_child->handlers.end()) { |
637 | | bool constraints_passed = true; |
638 | | auto constraints_it = node->wildcard_child->constraints.find(method); |
639 | | if (constraints_it != node->wildcard_child->constraints.end()) { |
640 | | for (const auto& [param_name, constraint] : constraints_it->second) { |
641 | | auto param_it = params.find(param_name); |
642 | | if (param_it != params.end()) { |
643 | | const std::string& value = param_it->second; |
644 | | if (!constraint.validation(value)) { |
645 | | constraints_passed = false; |
646 | | break; |
647 | | } |
648 | | } |
649 | | } |
650 | | } |
651 | | |
652 | | if (constraints_passed) { |
653 | | result = it->second; |
654 | | return true; |
655 | | } |
656 | | } |
657 | | } |
658 | | |
659 | | // Wildcard match failed (not an endpoint, wrong method, or |
660 | | // constraint failure). Mirror the parameter-branch behavior and |
661 | | // erase the capture so the caller's params map reflects only the |
662 | | // matched route's parameters. |
663 | | params.erase(wildcard_name); |
664 | | } |
665 | | |
666 | | return false; |
667 | | } |
668 | | |
669 | | void print_node(const radix_node* node, int depth) const |
670 | | { |
671 | | if (!node) return; |
672 | | |
673 | | std::string indent(depth * 2, ' '); |
674 | | std::cout << indent << node->to_string() << "\n"; |
675 | | |
676 | | if (node->is_endpoint) { |
677 | | std::cout << indent << " Handlers: "; |
678 | | for (const auto& [method, _] : node->handlers) { |
679 | | std::cout << to_string(method) << " "; |
680 | | } |
681 | | std::cout << "\n"; |
682 | | |
683 | | for (const auto& [method, method_constraints] : node->constraints) { |
684 | | std::cout << indent << " Constraints for " << to_string(method) << ":\n"; |
685 | | for (const auto& [param, constraint] : method_constraints) { |
686 | | std::cout << indent << " " << param << ": (" << constraint.description << ")\n"; |
687 | | } |
688 | | } |
689 | | } |
690 | | |
691 | | for (const auto& [segment, child] : node->static_children) { |
692 | | print_node(child.get(), depth + 1); |
693 | | } |
694 | | |
695 | | if (node->parameter_child) { |
696 | | print_node(node->parameter_child.get(), depth + 1); |
697 | | } |
698 | | |
699 | | if (node->wildcard_child) { |
700 | | print_node(node->wildcard_child.get(), depth + 1); |
701 | | } |
702 | | } |
703 | | }; |
704 | | |
705 | | /** |
706 | | * @brief Match a value against a pattern with advanced pattern matching features |
707 | | * |
708 | | * Supports: |
709 | | * - Wildcards (*) for matching any number of characters |
710 | | * - Question marks (?) for matching a single character |
711 | | * - Character classes ([a-z], [^0-9]) |
712 | | * - Anchors (^ for start of string, $ for end of string) |
713 | | * - Escape sequences with backslash |
714 | | * |
715 | | * @param value The string to match |
716 | | * @param pattern The pattern to match against |
717 | | * @return true if the value matches the pattern, false otherwise |
718 | | */ |
719 | | inline bool match_pattern(std::string_view value, std::string_view pattern) |
720 | 0 | { |
721 | 0 | enum struct State { Literal, Escape, CharClass }; |
722 | 0 |
|
723 | 0 | if (pattern.empty()) return true; // Empty pattern matches anything |
724 | 0 |
|
725 | 0 | size_t v_pos = 0; |
726 | 0 | size_t p_pos = 0; |
727 | 0 |
|
728 | 0 | // For backtracking when we encounter * |
729 | 0 | std::optional<size_t> backtrack_pattern; |
730 | 0 | std::optional<size_t> backtrack_value; |
731 | 0 |
|
732 | 0 | // For character classes |
733 | 0 | State state = State::Literal; |
734 | 0 | bool negate_class = false; |
735 | 0 | bool char_class_match = false; |
736 | 0 |
|
737 | 0 | while (v_pos < value.size() || p_pos < pattern.size()) { |
738 | 0 | // Pattern exhausted but value remains |
739 | 0 | if (p_pos >= pattern.size()) { |
740 | 0 | if (backtrack_pattern) { |
741 | 0 | p_pos = *backtrack_pattern; |
742 | 0 | v_pos = ++(*backtrack_value); |
743 | 0 | continue; |
744 | 0 | } |
745 | 0 | return false; |
746 | 0 | } |
747 | 0 |
|
748 | 0 | // Value exhausted but pattern remains |
749 | 0 | if (v_pos >= value.size()) { |
750 | 0 | if (p_pos < pattern.size() && pattern[p_pos] == '*' && p_pos == pattern.size() - 1) return true; |
751 | 0 |
|
752 | 0 | if (backtrack_pattern) { |
753 | 0 | p_pos = *backtrack_pattern; |
754 | 0 | v_pos = ++(*backtrack_value); |
755 | 0 | continue; |
756 | 0 | } |
757 | 0 | return false; |
758 | 0 | } |
759 | 0 |
|
760 | 0 | switch (state) { |
761 | 0 | case State::Literal: |
762 | 0 | if (pattern[p_pos] == '\\') { |
763 | 0 | state = State::Escape; |
764 | 0 | p_pos++; |
765 | 0 | continue; |
766 | 0 | } |
767 | 0 | else if (pattern[p_pos] == '[') { |
768 | 0 | state = State::CharClass; |
769 | 0 | char_class_match = false; |
770 | 0 | p_pos++; |
771 | 0 |
|
772 | 0 | if (p_pos < pattern.size() && pattern[p_pos] == '^') { |
773 | 0 | negate_class = true; |
774 | 0 | p_pos++; |
775 | 0 | } |
776 | 0 | else { |
777 | 0 | negate_class = false; |
778 | 0 | } |
779 | 0 | continue; |
780 | 0 | } |
781 | 0 | else if (pattern[p_pos] == '*') { |
782 | 0 | backtrack_pattern = p_pos; |
783 | 0 | backtrack_value = v_pos; |
784 | 0 | p_pos++; |
785 | 0 | continue; |
786 | 0 | } |
787 | 0 | else if (pattern[p_pos] == '?') { |
788 | 0 | p_pos++; |
789 | 0 | v_pos++; |
790 | 0 | continue; |
791 | 0 | } |
792 | 0 | else if (pattern[p_pos] == '^' && p_pos == 0) { |
793 | 0 | p_pos++; |
794 | 0 | continue; |
795 | 0 | } |
796 | 0 | else if (pattern[p_pos] == '$' && p_pos == pattern.size() - 1) { |
797 | 0 | return v_pos == value.size(); |
798 | 0 | } |
799 | 0 | else { |
800 | 0 | if (pattern[p_pos] != value[v_pos]) { |
801 | 0 | if (backtrack_pattern) { |
802 | 0 | p_pos = *backtrack_pattern; |
803 | 0 | v_pos = ++(*backtrack_value); |
804 | 0 | continue; |
805 | 0 | } |
806 | 0 | return false; |
807 | 0 | } |
808 | 0 | p_pos++; |
809 | 0 | v_pos++; |
810 | 0 | } |
811 | 0 | break; |
812 | 0 |
|
813 | 0 | case State::Escape: |
814 | 0 | if (p_pos >= pattern.size() || pattern[p_pos] != value[v_pos]) { |
815 | 0 | if (backtrack_pattern) { |
816 | 0 | p_pos = *backtrack_pattern; |
817 | 0 | v_pos = ++(*backtrack_value); |
818 | 0 | state = State::Literal; |
819 | 0 | continue; |
820 | 0 | } |
821 | 0 | return false; |
822 | 0 | } |
823 | 0 | p_pos++; |
824 | 0 | v_pos++; |
825 | 0 | state = State::Literal; |
826 | 0 | break; |
827 | 0 |
|
828 | 0 | case State::CharClass: |
829 | 0 | if (pattern[p_pos] == ']') { |
830 | 0 | p_pos++; |
831 | 0 | if (negate_class) { |
832 | 0 | if (char_class_match) { |
833 | 0 | return false; |
834 | 0 | } |
835 | 0 | v_pos++; |
836 | 0 | } |
837 | 0 | else { |
838 | 0 | if (!char_class_match) { |
839 | 0 | return false; |
840 | 0 | } |
841 | 0 | v_pos++; |
842 | 0 | } |
843 | 0 | state = State::Literal; |
844 | 0 | continue; |
845 | 0 | } |
846 | 0 | else if (p_pos + 2 < pattern.size() && pattern[p_pos + 1] == '-') { |
847 | 0 | char start = pattern[p_pos]; |
848 | 0 | char end = pattern[p_pos + 2]; |
849 | 0 | if (value[v_pos] >= start && value[v_pos] <= end) { |
850 | 0 | char_class_match = true; |
851 | 0 | } |
852 | 0 | p_pos += 3; |
853 | 0 | } |
854 | 0 | else { |
855 | 0 | if (pattern[p_pos] == value[v_pos]) { |
856 | 0 | char_class_match = true; |
857 | 0 | } |
858 | 0 | p_pos++; |
859 | 0 | } |
860 | 0 | break; |
861 | 0 | } |
862 | 0 | } |
863 | 0 |
|
864 | 0 | return v_pos == value.size() && p_pos == pattern.size(); |
865 | 0 | } |
866 | | |
867 | | /** |
868 | | * @brief HTTP router based on a radix tree for efficient path matching |
869 | | * |
870 | | * @tparam Handler The type of the handler that gets invoked upon a route match. |
871 | | * Must be invocable with (const request&, response&). |
872 | | * Note: Middleware registered via use() must also be this type. |
873 | | * |
874 | | * The basic_http_router class provides fast route matching for HTTP requests using a radix tree |
875 | | * data structure. It supports static routes, parameterized routes (e.g., "/users/:id"), |
876 | | * wildcard routes, and parameter validation via constraints. |
877 | | * |
878 | | * In addition to normal request/response handlers, basic_http_router also stores |
879 | | * streaming routes (registered via stream_get/stream_post/stream) and WebSocket |
880 | | * routes (registered via websocket()). All three kinds share the same radix-tree |
881 | | * matching logic, so streaming and WebSocket routes also support ":param" path |
882 | | * parameters. |
883 | | * |
884 | | * Note: http_server::mount() only accepts the default http_router (basic_http_router<>). |
885 | | * Custom handler routers are intended for standalone use or with custom server implementations. |
886 | | */ |
887 | | template <class Handler = std::function<void(const request&, response&)>> |
888 | | requires std::invocable<Handler, const request&, response&> |
889 | | struct basic_http_router |
890 | | { |
891 | | /** |
892 | | * @brief Function type for request handlers |
893 | | */ |
894 | | using handler = Handler; |
895 | | |
896 | | /** |
897 | | * @brief A compile-time boolean indicating whether asynchronous request handlers are enabled. |
898 | | */ |
899 | | static constexpr bool is_async_enabled = |
900 | | std::is_constructible_v<Handler, std::function<void(const request&, response&)>>; |
901 | | |
902 | | /** |
903 | | * @brief Function type for asynchronous request handlers |
904 | | */ |
905 | | using async_handler = std::function<std::future<void>(const request&, response&)>; |
906 | | |
907 | | /** |
908 | | * @brief Underlying route table type for normal routes. |
909 | | */ |
910 | | using normal_route_table = route_table<Handler>; |
911 | | |
912 | | /** |
913 | | * @brief Backward-compatible alias for the normal route entry type. |
914 | | */ |
915 | | using route_entry = typename normal_route_table::route_entry; |
916 | | |
917 | | basic_http_router() = default; |
918 | | |
919 | | /** |
920 | | * @brief Backward-compatible static helper. Equivalent to route_table<Handler>::split_path. |
921 | | */ |
922 | | static std::vector<std::string> split_path(std::string_view path) { return normal_route_table::split_path(path); } |
923 | | |
924 | | /** |
925 | | * @brief Register a route with the router |
926 | | * |
927 | | * @param method The HTTP method (GET, POST, etc.) |
928 | | * @param path The route path, can include parameters (":param") and wildcards ("*param") |
929 | | * @param handle The handler function to call when this route matches |
930 | | * @param spec Optional spec for the route. |
931 | | * @return Reference to this router for method chaining |
932 | | * @throws std::runtime_error if there's a route conflict |
933 | | */ |
934 | | basic_http_router& route(http_method method, std::string_view path, handler handle, const route_spec& spec = {}) |
935 | | { |
936 | | normal_routes.add(method, path, std::move(handle), spec); |
937 | | return *this; |
938 | | } |
939 | | |
940 | | /** |
941 | | * @brief Register a GET route |
942 | | */ |
943 | | basic_http_router& get(std::string_view path, handler handle, const route_spec& spec = {}) |
944 | | { |
945 | | return route(http_method::GET, path, std::move(handle), spec); |
946 | | } |
947 | | |
948 | | /** |
949 | | * @brief Register a POST route |
950 | | */ |
951 | | basic_http_router& post(std::string_view path, handler handle, const route_spec& spec = {}) |
952 | | { |
953 | | return route(http_method::POST, path, std::move(handle), spec); |
954 | | } |
955 | | |
956 | | /** |
957 | | * @brief Register a PUT route |
958 | | */ |
959 | | basic_http_router& put(std::string_view path, handler handle, const route_spec& spec = {}) |
960 | | { |
961 | | return route(http_method::PUT, path, std::move(handle), spec); |
962 | | } |
963 | | |
964 | | /** |
965 | | * @brief Register a DELETE route |
966 | | */ |
967 | | basic_http_router& del(std::string_view path, handler handle, const route_spec& spec = {}) |
968 | | { |
969 | | return route(http_method::DELETE, path, std::move(handle), spec); |
970 | | } |
971 | | |
972 | | /** |
973 | | * @brief Register a PATCH route |
974 | | */ |
975 | | basic_http_router& patch(std::string_view path, handler handle, const route_spec& spec = {}) |
976 | | { |
977 | | return route(http_method::PATCH, path, std::move(handle), spec); |
978 | | } |
979 | | |
980 | | /** |
981 | | * @brief Register an asynchronous route |
982 | | */ |
983 | | basic_http_router& route_async(http_method method, std::string_view path, async_handler handle, |
984 | | const route_spec& spec = {}) |
985 | | requires is_async_enabled |
986 | | { |
987 | | return route( |
988 | | method, path, |
989 | | [handle = std::move(handle)](const request& req, response& res) { |
990 | | auto future = handle(req, res); |
991 | | future.get(); |
992 | | }, |
993 | | spec); |
994 | | } |
995 | | |
996 | | /** |
997 | | * @brief Register an asynchronous GET route |
998 | | */ |
999 | | basic_http_router& get_async(std::string_view path, async_handler handle, const route_spec& spec = {}) |
1000 | | requires is_async_enabled |
1001 | | { |
1002 | | return route_async(http_method::GET, path, std::move(handle), spec); |
1003 | | } |
1004 | | |
1005 | | /** |
1006 | | * @brief Register an asynchronous POST route |
1007 | | */ |
1008 | | basic_http_router& post_async(std::string_view path, async_handler handle, const route_spec& spec = {}) |
1009 | | requires is_async_enabled |
1010 | | { |
1011 | | return route_async(http_method::POST, path, std::move(handle), spec); |
1012 | | } |
1013 | | |
1014 | | /** |
1015 | | * @brief Register an asynchronous PUT route |
1016 | | */ |
1017 | | basic_http_router& put_async(std::string_view path, async_handler handle, const route_spec& spec = {}) |
1018 | | requires is_async_enabled |
1019 | | { |
1020 | | return route_async(http_method::PUT, path, std::move(handle), spec); |
1021 | | } |
1022 | | |
1023 | | /** |
1024 | | * @brief Register an asynchronous DELETE route |
1025 | | */ |
1026 | | basic_http_router& del_async(std::string_view path, async_handler handle, const route_spec& spec = {}) |
1027 | | requires is_async_enabled |
1028 | | { |
1029 | | return route_async(http_method::DELETE, path, std::move(handle), spec); |
1030 | | } |
1031 | | |
1032 | | /** |
1033 | | * @brief Register an asynchronous PATCH route |
1034 | | */ |
1035 | | basic_http_router& patch_async(std::string_view path, async_handler handle, const route_spec& spec = {}) |
1036 | | requires is_async_enabled |
1037 | | { |
1038 | | return route_async(http_method::PATCH, path, std::move(handle), spec); |
1039 | | } |
1040 | | |
1041 | | /** |
1042 | | * @brief Register a streaming route. Streaming handlers take over the connection |
1043 | | * lifecycle and write chunked responses through streaming_response. |
1044 | | * |
1045 | | * Supports the same path-parameter syntax as normal routes (":param" and "*name"). |
1046 | | */ |
1047 | | basic_http_router& stream(http_method method, std::string_view path, streaming_handler handle, |
1048 | | const route_spec& spec = {}) |
1049 | | { |
1050 | | streaming_routes.add(method, path, std::move(handle), spec); |
1051 | | return *this; |
1052 | | } |
1053 | | |
1054 | | /** |
1055 | | * @brief Register a streaming GET route. |
1056 | | */ |
1057 | | basic_http_router& stream_get(std::string_view path, streaming_handler handle, const route_spec& spec = {}) |
1058 | | { |
1059 | | return stream(http_method::GET, path, std::move(handle), spec); |
1060 | | } |
1061 | | |
1062 | | /** |
1063 | | * @brief Register a streaming POST route. |
1064 | | */ |
1065 | | basic_http_router& stream_post(std::string_view path, streaming_handler handle, const route_spec& spec = {}) |
1066 | | { |
1067 | | return stream(http_method::POST, path, std::move(handle), spec); |
1068 | | } |
1069 | | |
1070 | | /** |
1071 | | * @brief Register a WebSocket handler for a path. |
1072 | | * |
1073 | | * Supports the same path-parameter syntax as normal routes. The HTTP server |
1074 | | * detects the upgrade handshake (Upgrade: websocket) and dispatches to the |
1075 | | * matching server, populating request.params from the path. |
1076 | | */ |
1077 | | basic_http_router& websocket(std::string_view path, websocket_handler server, const route_spec& spec = {}) |
1078 | | { |
1079 | | websocket_routes.add(http_method::GET, path, std::move(server), spec); |
1080 | | return *this; |
1081 | | } |
1082 | | |
1083 | | /** |
1084 | | * @brief Register middleware to be executed before route handlers |
1085 | | * |
1086 | | * Middleware functions are executed in the order they are registered. |
1087 | | * |
1088 | | * @param middleware The middleware function |
1089 | | * @return Reference to this router for method chaining |
1090 | | */ |
1091 | | basic_http_router& use(handler middleware) |
1092 | | { |
1093 | | middlewares.push_back(std::move(middleware)); |
1094 | | return *this; |
1095 | | } |
1096 | | |
1097 | | /** |
1098 | | * @brief Match a normal request against registered routes |
1099 | | * |
1100 | | * @param method The HTTP method of the request |
1101 | | * @param target The target path of the request (may include query string) |
1102 | | * @return A pair containing the matched handler and extracted parameters |
1103 | | */ |
1104 | | std::pair<handler, std::unordered_map<std::string, std::string>> match(http_method method, |
1105 | | std::string_view target) const |
1106 | | { |
1107 | | return normal_routes.match(method, target); |
1108 | | } |
1109 | | |
1110 | | /** |
1111 | | * @brief Match a streaming request against registered streaming routes. |
1112 | | */ |
1113 | | std::pair<streaming_handler, std::unordered_map<std::string, std::string>> match_streaming( |
1114 | | http_method method, std::string_view target) const |
1115 | | { |
1116 | | return streaming_routes.match(method, target); |
1117 | | } |
1118 | | |
1119 | | /** |
1120 | | * @brief Match a WebSocket upgrade request against registered WebSocket routes. |
1121 | | * |
1122 | | * WebSocket upgrades are HTTP GET requests by definition, so the lookup uses |
1123 | | * http_method::GET internally. |
1124 | | */ |
1125 | | std::pair<websocket_handler, std::unordered_map<std::string, std::string>> match_websocket( |
1126 | | std::string_view target) const |
1127 | | { |
1128 | | return websocket_routes.match(http_method::GET, target); |
1129 | | } |
1130 | | |
1131 | | /** |
1132 | | * @brief Print the router structure for debugging. |
1133 | | */ |
1134 | | void print_tree() const |
1135 | | { |
1136 | | std::cout << "[normal routes]\n"; |
1137 | | normal_routes.print_tree(); |
1138 | | std::cout << "[streaming routes]\n"; |
1139 | | streaming_routes.print_tree(); |
1140 | | std::cout << "[websocket routes]\n"; |
1141 | | websocket_routes.print_tree(); |
1142 | | } |
1143 | | |
1144 | | /** |
1145 | | * @brief Storage for normal request/response routes. |
1146 | | * |
1147 | | * Iterate `router.normal_routes.routes` for path -> method -> entry inspection |
1148 | | * (used by the OpenAPI generator and mount()). |
1149 | | * |
1150 | | * Migration note: prior to the streaming/WebSocket unification, this field was |
1151 | | * exposed as `router.routes`. Code that iterated `router.routes` should now |
1152 | | * iterate `router.normal_routes.routes` (or use the `routes()` accessor below). |
1153 | | */ |
1154 | | normal_route_table normal_routes; |
1155 | | |
1156 | | /** |
1157 | | * @brief Backward-compatible accessor for the registered normal routes map. |
1158 | | * |
1159 | | * Equivalent to `normal_routes.routes`. Provided as a function (not a |
1160 | | * reference data member) because a reference member would be copied verbatim |
1161 | | * by the implicitly-defined move constructor and dangle into the moved-from |
1162 | | * source. |
1163 | | */ |
1164 | | auto& routes() noexcept { return normal_routes.routes; } |
1165 | | const auto& routes() const noexcept { return normal_routes.routes; } |
1166 | | |
1167 | | /** |
1168 | | * @brief Storage for streaming routes. |
1169 | | */ |
1170 | | route_table<streaming_handler> streaming_routes; |
1171 | | |
1172 | | /** |
1173 | | * @brief Storage for WebSocket routes. |
1174 | | */ |
1175 | | route_table<websocket_handler> websocket_routes; |
1176 | | |
1177 | | /** |
1178 | | * @brief Vector of middleware handlers |
1179 | | */ |
1180 | | std::vector<handler> middlewares; |
1181 | | }; |
1182 | | |
1183 | | /** |
1184 | | * @brief Default HTTP router using std::function handlers |
1185 | | * |
1186 | | * This is a type alias for backward compatibility. Use basic_http_router<Handler> |
1187 | | * if you need to customize the handler type for coroutines, different futures |
1188 | | * implementations, or callback-based architectures. |
1189 | | */ |
1190 | | using http_router = basic_http_router<>; |
1191 | | } |