Line data Source code
1 : //
2 : // Copyright (c) 2026 Vinnie Falco (vinnie dot falco at gmail dot com)
3 : //
4 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
5 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6 : //
7 : // Official repository: https://github.com/cppalliance/http
8 : //
9 :
10 : #include <boost/http/server/flat_router.hpp>
11 : #include <boost/http/server/basic_router.hpp>
12 : #include <boost/http/detail/except.hpp>
13 :
14 : #include "src/server/detail/router_base.hpp"
15 : #include "src/server/detail/pct_decode.hpp"
16 : #include "src/server/detail/route_match.hpp"
17 :
18 : #include <algorithm>
19 :
20 : namespace boost {
21 : namespace http {
22 :
23 : //------------------------------------------------
24 :
25 : struct flat_router::impl
26 : {
27 : using entry = detail::router_base::entry;
28 : using layer = detail::router_base::layer;
29 : using handler = detail::router_base::handler;
30 : using matcher = detail::router_base::matcher;
31 : using opt_flags = detail::router_base::opt_flags;
32 : using handler_ptr = detail::router_base::handler_ptr;
33 : using options_handler_ptr = detail::router_base::options_handler_ptr;
34 : using match_result = route_params_base::match_result;
35 :
36 : std::vector<entry> entries;
37 : std::vector<matcher> matchers;
38 :
39 : std::uint64_t global_methods_ = 0;
40 : std::vector<std::string> global_custom_verbs_;
41 : std::string global_allow_header_;
42 : options_handler_ptr options_handler_;
43 :
44 : // RAII scope tracker sets matcher's skip_ when scope ends
45 : struct scope_tracker
46 : {
47 : std::vector<matcher>& matchers_;
48 : std::vector<entry>& entries_;
49 : std::size_t matcher_idx_;
50 :
51 150 : scope_tracker(
52 : std::vector<matcher>& m,
53 : std::vector<entry>& e,
54 : std::size_t idx)
55 150 : : matchers_(m)
56 150 : , entries_(e)
57 150 : , matcher_idx_(idx)
58 : {
59 150 : }
60 :
61 150 : ~scope_tracker()
62 : {
63 150 : matchers_[matcher_idx_].skip_ = entries_.size();
64 150 : }
65 : };
66 :
67 : // Build Allow header string from bitmask and custom verbs
68 : static std::string
69 165 : build_allow_header(
70 : std::uint64_t methods,
71 : std::vector<std::string> const& custom)
72 : {
73 165 : if(methods == ~0ULL)
74 34 : return "DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT";
75 :
76 148 : std::string result;
77 : // Methods in alphabetical order
78 : static constexpr std::pair<http::method, char const*> known[] = {
79 : {http::method::acl, "ACL"},
80 : {http::method::bind, "BIND"},
81 : {http::method::checkout, "CHECKOUT"},
82 : {http::method::connect, "CONNECT"},
83 : {http::method::copy, "COPY"},
84 : {http::method::delete_, "DELETE"},
85 : {http::method::get, "GET"},
86 : {http::method::head, "HEAD"},
87 : {http::method::link, "LINK"},
88 : {http::method::lock, "LOCK"},
89 : {http::method::merge, "MERGE"},
90 : {http::method::mkactivity, "MKACTIVITY"},
91 : {http::method::mkcalendar, "MKCALENDAR"},
92 : {http::method::mkcol, "MKCOL"},
93 : {http::method::move, "MOVE"},
94 : {http::method::msearch, "M-SEARCH"},
95 : {http::method::notify, "NOTIFY"},
96 : {http::method::options, "OPTIONS"},
97 : {http::method::patch, "PATCH"},
98 : {http::method::post, "POST"},
99 : {http::method::propfind, "PROPFIND"},
100 : {http::method::proppatch, "PROPPATCH"},
101 : {http::method::purge, "PURGE"},
102 : {http::method::put, "PUT"},
103 : {http::method::rebind, "REBIND"},
104 : {http::method::report, "REPORT"},
105 : {http::method::search, "SEARCH"},
106 : {http::method::subscribe, "SUBSCRIBE"},
107 : {http::method::trace, "TRACE"},
108 : {http::method::unbind, "UNBIND"},
109 : {http::method::unlink, "UNLINK"},
110 : {http::method::unlock, "UNLOCK"},
111 : {http::method::unsubscribe, "UNSUBSCRIBE"},
112 : };
113 5032 : for(auto const& [m, name] : known)
114 : {
115 4884 : if(methods & (1ULL << static_cast<unsigned>(m)))
116 : {
117 111 : if(!result.empty())
118 7 : result += ", ";
119 111 : result += name;
120 : }
121 : }
122 : // Append custom verbs
123 152 : for(auto const& v : custom)
124 : {
125 4 : if(!result.empty())
126 0 : result += ", ";
127 4 : result += v;
128 : }
129 148 : return result;
130 148 : }
131 :
132 : static opt_flags
133 132 : compute_effective_opts(
134 : opt_flags parent,
135 : opt_flags child)
136 : {
137 132 : opt_flags result = parent;
138 :
139 : // case_sensitive: bits 1-2 (2=true, 4=false)
140 132 : if(child & 2)
141 4 : result = (result & ~6) | 2;
142 128 : else if(child & 4)
143 2 : result = (result & ~6) | 4;
144 :
145 : // strict: bits 3-4 (8=true, 16=false)
146 132 : if(child & 8)
147 1 : result = (result & ~24) | 8;
148 131 : else if(child & 16)
149 1 : result = (result & ~24) | 16;
150 :
151 132 : return result;
152 : }
153 :
154 : void
155 98 : flatten(detail::router_base::impl& src)
156 : {
157 98 : flatten_recursive(src, opt_flags{}, 0);
158 98 : build_allow_headers();
159 98 : }
160 :
161 : void
162 98 : build_allow_headers()
163 : {
164 : // Build per-matcher Allow header strings
165 248 : for(auto& m : matchers)
166 : {
167 150 : if(m.end_)
168 65 : m.allow_header_ = build_allow_header(
169 65 : m.allowed_methods_, m.custom_verbs_);
170 : }
171 :
172 : // Deduplicate global custom verbs and build global Allow header
173 98 : std::sort(global_custom_verbs_.begin(), global_custom_verbs_.end());
174 196 : global_custom_verbs_.erase(
175 98 : std::unique(global_custom_verbs_.begin(), global_custom_verbs_.end()),
176 98 : global_custom_verbs_.end());
177 98 : global_allow_header_ = build_allow_header(
178 98 : global_methods_, global_custom_verbs_);
179 98 : }
180 :
181 : void
182 132 : flatten_recursive(
183 : detail::router_base::impl& src,
184 : opt_flags parent_opts,
185 : std::uint32_t depth)
186 : {
187 132 : opt_flags eff = compute_effective_opts(parent_opts, src.opt);
188 :
189 282 : for(auto& layer : src.layers)
190 : {
191 : // Move matcher, set flat router fields
192 150 : std::size_t matcher_idx = matchers.size();
193 150 : matchers.emplace_back(std::move(layer.match));
194 150 : auto& m = matchers.back();
195 150 : m.first_entry_ = entries.size();
196 150 : m.effective_opts_ = eff;
197 150 : m.depth_ = depth;
198 : // m.skip_ set by scope_tracker dtor
199 :
200 150 : scope_tracker scope(matchers, entries, matcher_idx);
201 :
202 312 : for(auto& e : layer.entries)
203 : {
204 162 : if(e.h->kind == detail::router_base::is_router)
205 : {
206 : // Recurse into nested router
207 34 : auto* nested = e.h->get_router();
208 34 : if(nested && nested->impl_)
209 34 : flatten_recursive(*nested->impl_, eff, depth + 1);
210 : }
211 : else
212 : {
213 : // Collect methods for OPTIONS (only for end routes)
214 128 : if(m.end_)
215 : {
216 : // Per-matcher collection
217 68 : if(e.all)
218 8 : m.allowed_methods_ = ~0ULL;
219 60 : else if(e.verb != http::method::unknown)
220 58 : m.allowed_methods_ |= (1ULL << static_cast<unsigned>(e.verb));
221 2 : else if(!e.verb_str.empty())
222 2 : m.custom_verbs_.push_back(e.verb_str);
223 :
224 : // Global collection (for OPTIONS *)
225 68 : if(e.all)
226 8 : global_methods_ = ~0ULL;
227 60 : else if(e.verb != http::method::unknown)
228 58 : global_methods_ |= (1ULL << static_cast<unsigned>(e.verb));
229 2 : else if(!e.verb_str.empty())
230 2 : global_custom_verbs_.push_back(e.verb_str);
231 : }
232 :
233 : // Set matcher_idx, then move entire entry
234 128 : e.matcher_idx = matcher_idx;
235 128 : entries.emplace_back(std::move(e));
236 : }
237 : }
238 : // ~scope_tracker sets matchers[matcher_idx].skip_
239 150 : }
240 132 : }
241 :
242 : // Restore path to a given base_path length
243 : static void
244 30 : restore_path(
245 : route_params_base& p,
246 : std::size_t base_len)
247 : {
248 30 : p.base_path = { p.decoded_path_.data(), base_len };
249 : // Account for the addedSlash_ when computing path length
250 30 : auto const path_len = p.decoded_path_.size() - (p.addedSlash_ ? 1 : 0);
251 30 : if(base_len < path_len)
252 29 : p.path = { p.decoded_path_.data() + base_len,
253 : path_len - base_len };
254 : else
255 2 : p.path = { p.decoded_path_.data() +
256 1 : p.decoded_path_.size() - 1, 1 }; // soft slash
257 30 : }
258 :
259 : route_task
260 107 : dispatch_loop(route_params_base& p, bool is_options) const
261 : {
262 : // All checks happen BEFORE co_await to minimize coroutine launches.
263 : // Avoid touching p.ep_ (expensive atomic on Windows) - use p.kind_ for mode checks.
264 :
265 : std::size_t last_matched = SIZE_MAX;
266 : std::uint32_t current_depth = 0;
267 :
268 : // Collect methods from all matching end-route matchers for OPTIONS
269 : std::uint64_t options_methods = 0;
270 : std::vector<std::string> options_custom_verbs;
271 :
272 : // Stack of base_path lengths at each depth level.
273 : // path_stack[d] = base_path.size() before any matcher at depth d was tried.
274 : std::size_t path_stack[detail::router_base::max_path_depth];
275 : path_stack[0] = 0;
276 :
277 : // Track which matcher index is matched at each depth level.
278 : // matched_at_depth[d] = matcher index that successfully matched at depth d.
279 : std::size_t matched_at_depth[detail::router_base::max_path_depth];
280 : for(std::size_t d = 0; d < detail::router_base::max_path_depth; ++d)
281 : matched_at_depth[d] = SIZE_MAX;
282 :
283 : for(std::size_t i = 0; i < entries.size(); )
284 : {
285 : auto const& e = entries[i];
286 : auto const& m = matchers[e.matcher_idx];
287 : auto const target_depth = m.depth_;
288 :
289 : //--------------------------------------------------
290 : // Pre-invoke checks (no coroutine yet)
291 : //--------------------------------------------------
292 :
293 : // For nested routes: verify ancestors at depths 0..target_depth-1 are matched.
294 : // For siblings: if moving to same depth with different matcher, restore path.
295 : bool ancestors_ok = true;
296 :
297 : // Check if we need to match new ancestor matchers for this entry.
298 : // We iterate through matchers from last_matched+1 to e.matcher_idx,
299 : // but ONLY process those that are at depths we need (ancestors or self).
300 : std::size_t start_idx = (last_matched == SIZE_MAX) ? 0 : last_matched + 1;
301 :
302 : for(std::size_t check_idx = start_idx;
303 : check_idx <= e.matcher_idx && ancestors_ok;
304 : ++check_idx)
305 : {
306 : auto const& cm = matchers[check_idx];
307 :
308 : // Only check matchers that are:
309 : // 1. Ancestors (depth < target_depth) that we haven't matched yet, or
310 : // 2. The entry's own matcher
311 : bool is_needed_ancestor = (cm.depth_ < target_depth) &&
312 : (matched_at_depth[cm.depth_] == SIZE_MAX);
313 : bool is_self = (check_idx == e.matcher_idx);
314 :
315 : if(!is_needed_ancestor && !is_self)
316 : continue;
317 :
318 : // Restore path if moving to same or shallower depth
319 : if(cm.depth_ <= current_depth && current_depth > 0)
320 : {
321 : restore_path(p, path_stack[cm.depth_]);
322 : }
323 :
324 : // In error/exception mode, skip end routes
325 : if(cm.end_ && p.kind_ != detail::router_base::is_plain)
326 : {
327 : i = cm.skip_;
328 : ancestors_ok = false;
329 : break;
330 : }
331 :
332 : // Apply effective_opts for this matcher
333 : p.case_sensitive = (cm.effective_opts_ & 2) != 0;
334 : p.strict = (cm.effective_opts_ & 8) != 0;
335 :
336 : // Save path state before trying this matcher
337 : if(cm.depth_ < detail::router_base::max_path_depth)
338 : path_stack[cm.depth_] = p.base_path.size();
339 :
340 : match_result mr;
341 : if(!cm(p, mr))
342 : {
343 : // Clear matched_at_depth for this depth and deeper
344 : for(std::size_t d = cm.depth_; d < detail::router_base::max_path_depth; ++d)
345 : matched_at_depth[d] = SIZE_MAX;
346 : i = cm.skip_;
347 : ancestors_ok = false;
348 : break;
349 : }
350 :
351 : // Copy captured params to route_params_base
352 : if(!mr.params_.empty())
353 : {
354 : for(auto& param : mr.params_)
355 : p.params.push_back(std::move(param));
356 : }
357 :
358 : // Mark this depth as matched
359 : if(cm.depth_ < detail::router_base::max_path_depth)
360 : matched_at_depth[cm.depth_] = check_idx;
361 :
362 : last_matched = check_idx;
363 : current_depth = cm.depth_ + 1;
364 :
365 : // Save state for next depth level
366 : if(current_depth < detail::router_base::max_path_depth)
367 : path_stack[current_depth] = p.base_path.size();
368 : }
369 :
370 : if(!ancestors_ok)
371 : continue;
372 :
373 : // Collect methods from matching end-route matchers for OPTIONS
374 : if(is_options && m.end_)
375 : {
376 : options_methods |= m.allowed_methods_;
377 : for(auto const& v : m.custom_verbs_)
378 : options_custom_verbs.push_back(v);
379 : }
380 :
381 : // Check method match (only for end routes)
382 : if(m.end_ && !e.match_method(
383 : const_cast<route_params_base&>(p)))
384 : {
385 : ++i;
386 : continue;
387 : }
388 :
389 : // Check kind match (cheap char comparison)
390 : if(e.h->kind != p.kind_)
391 : {
392 : ++i;
393 : continue;
394 : }
395 :
396 : //--------------------------------------------------
397 : // Invoke handler (coroutine starts here)
398 : //--------------------------------------------------
399 :
400 : route_result rv;
401 : try
402 : {
403 : rv = co_await e.h->invoke(
404 : const_cast<route_params_base&>(p));
405 : }
406 : catch(...)
407 : {
408 : // Only touch ep_ when actually catching
409 : p.ep_ = std::current_exception();
410 : p.kind_ = detail::router_base::is_exception;
411 : ++i;
412 : continue;
413 : }
414 :
415 : //--------------------------------------------------
416 : // Handle result
417 : //
418 : // Coroutines invert control - handler does the send.
419 : // route_what::done = handler completed request
420 : // route_what::next = continue to next handler
421 : // route_what::next_route = skip to next route
422 : // route_what::close = close connection
423 : // route_what::error = enter error mode
424 : //--------------------------------------------------
425 :
426 : if(rv.what() == route_what::next)
427 : {
428 : ++i;
429 : continue;
430 : }
431 :
432 : if(rv.what() == route_what::next_route)
433 : {
434 : // next_route only valid for end routes, not middleware
435 : if(!m.end_)
436 : // VFALCO this is a logic error
437 : co_return route_error(error::invalid_route_result);
438 : i = m.skip_;
439 : continue;
440 : }
441 :
442 : if(rv.what() == route_what::done ||
443 : rv.what() == route_what::close)
444 : {
445 : // Handler completed or requested close
446 : co_return rv;
447 : }
448 :
449 : // Error - transition to error mode
450 : p.ec_ = rv.error();
451 : p.kind_ = detail::router_base::is_error;
452 :
453 : if(m.end_)
454 : {
455 : // End routes don't have error handlers
456 : i = m.skip_;
457 : continue;
458 : }
459 :
460 : ++i;
461 : }
462 :
463 : // Final state
464 : if(p.kind_ == detail::router_base::is_exception)
465 : co_return route_error(error::unhandled_exception);
466 : if(p.kind_ == detail::router_base::is_error)
467 : co_return route_error(p.ec_);
468 :
469 : // OPTIONS fallback: path matched but no explicit OPTIONS handler
470 : if(is_options && options_methods != 0 && options_handler_)
471 : {
472 : // Build Allow header from collected methods
473 : std::string allow = build_allow_header(options_methods, options_custom_verbs);
474 : co_return co_await options_handler_->invoke(p, allow);
475 : }
476 :
477 : co_return route_next; // no handler matched
478 214 : }
479 : };
480 :
481 : //------------------------------------------------
482 :
483 98 : flat_router::
484 : flat_router(
485 98 : detail::router_base&& src)
486 98 : : impl_(std::make_shared<impl>())
487 : {
488 98 : impl_->flatten(*src.impl_);
489 98 : impl_->options_handler_ = std::move(src.options_handler_);
490 98 : }
491 :
492 : route_task
493 104 : flat_router::
494 : dispatch(
495 : http::method verb,
496 : urls::url_view const& url,
497 : route_params_base& p) const
498 : {
499 104 : if(verb == http::method::unknown)
500 1 : detail::throw_invalid_argument();
501 :
502 : // Handle OPTIONS * before normal dispatch
503 107 : if(verb == http::method::options &&
504 107 : url.encoded_path() == "*")
505 : {
506 1 : if(impl_->options_handler_)
507 : {
508 1 : return impl_->options_handler_->invoke(
509 1 : p, impl_->global_allow_header_);
510 : }
511 : // No handler, let it fall through to 404
512 : }
513 :
514 : // Initialize params
515 102 : p.kind_ = detail::router_base::is_plain;
516 102 : p.verb_ = verb;
517 102 : p.verb_str_.clear();
518 102 : p.ec_.clear();
519 102 : p.ep_ = nullptr;
520 102 : p.params.clear();
521 102 : p.decoded_path_ = detail::pct_decode_path(url.encoded_path());
522 102 : if(p.decoded_path_.empty() || p.decoded_path_.back() != '/')
523 : {
524 69 : p.decoded_path_.push_back('/');
525 69 : p.addedSlash_ = true;
526 : }
527 : else
528 : {
529 33 : p.addedSlash_ = false;
530 : }
531 : // Set path views after potential reallocation from push_back
532 : // Exclude added trailing slash from visible path, but keep "/" if empty
533 102 : p.base_path = { p.decoded_path_.data(), 0 };
534 102 : auto const subtract = (p.addedSlash_ && p.decoded_path_.size() > 1) ? 1 : 0;
535 102 : p.path = { p.decoded_path_.data(), p.decoded_path_.size() - subtract };
536 :
537 102 : return impl_->dispatch_loop(p, verb == http::method::options);
538 : }
539 :
540 : route_task
541 6 : flat_router::
542 : dispatch(
543 : std::string_view verb,
544 : urls::url_view const& url,
545 : route_params_base& p) const
546 : {
547 6 : if(verb.empty())
548 1 : detail::throw_invalid_argument();
549 :
550 5 : auto const method = http::string_to_method(verb);
551 5 : bool const is_options = (method == http::method::options);
552 :
553 : // Handle OPTIONS * before normal dispatch
554 5 : if(is_options && url.encoded_path() == "*")
555 : {
556 0 : if(impl_->options_handler_)
557 : {
558 0 : return impl_->options_handler_->invoke(
559 0 : p, impl_->global_allow_header_);
560 : }
561 : // No handler, let it fall through to 404
562 : }
563 :
564 : // Initialize params
565 5 : p.kind_ = detail::router_base::is_plain;
566 5 : p.verb_ = method;
567 5 : if(p.verb_ == http::method::unknown)
568 4 : p.verb_str_ = verb;
569 : else
570 1 : p.verb_str_.clear();
571 5 : p.ec_.clear();
572 5 : p.ep_ = nullptr;
573 5 : p.params.clear();
574 5 : p.decoded_path_ = detail::pct_decode_path(url.encoded_path());
575 5 : if(p.decoded_path_.empty() || p.decoded_path_.back() != '/')
576 : {
577 0 : p.decoded_path_.push_back('/');
578 0 : p.addedSlash_ = true;
579 : }
580 : else
581 : {
582 5 : p.addedSlash_ = false;
583 : }
584 : // Set path views after potential reallocation from push_back
585 : // Exclude added trailing slash from visible path, but keep "/" if empty
586 5 : p.base_path = { p.decoded_path_.data(), 0 };
587 5 : auto const subtract = (p.addedSlash_ && p.decoded_path_.size() > 1) ? 1 : 0;
588 5 : p.path = { p.decoded_path_.data(), p.decoded_path_.size() - subtract };
589 :
590 5 : return impl_->dispatch_loop(p, is_options);
591 : }
592 :
593 : } // http
594 : } // boost
595 :
|