src/server/any_router.cpp

91.6% Lines (218/238) 94.1% Functions (16/17)
src/server/any_router.cpp
Line TLA Hits Source Code
1 //
2 // Copyright (c) 2025 Vinnie Falco ([email protected])
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 "src/server/detail/any_router.hpp"
11 #include <boost/http/server/detail/router_base.hpp>
12 #include <boost/http/detail/except.hpp>
13 #include <boost/http/error.hpp>
14 #include <boost/url/grammar/ci_string.hpp>
15 #include <boost/url/grammar/hexdig_chars.hpp>
16 #include "src/server/detail/pct_decode.hpp"
17
18 #include <algorithm>
19
20 namespace boost {
21 namespace http {
22 namespace detail {
23
24 //------------------------------------------------
25 //
26 // impl helpers
27 //
28 //------------------------------------------------
29
30 std::string
31 187 router_base::impl::
32 build_allow_header(
33 std::uint64_t methods,
34 std::vector<std::string> const& custom)
35 {
36 187 if(methods == ~0ULL)
37 34 return "DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT";
38
39 170 std::string result;
40 static constexpr std::pair<http::method, char const*> known[] = {
41 {http::method::acl, "ACL"},
42 {http::method::bind, "BIND"},
43 {http::method::checkout, "CHECKOUT"},
44 {http::method::connect, "CONNECT"},
45 {http::method::copy, "COPY"},
46 {http::method::delete_, "DELETE"},
47 {http::method::get, "GET"},
48 {http::method::head, "HEAD"},
49 {http::method::link, "LINK"},
50 {http::method::lock, "LOCK"},
51 {http::method::merge, "MERGE"},
52 {http::method::mkactivity, "MKACTIVITY"},
53 {http::method::mkcalendar, "MKCALENDAR"},
54 {http::method::mkcol, "MKCOL"},
55 {http::method::move, "MOVE"},
56 {http::method::msearch, "M-SEARCH"},
57 {http::method::notify, "NOTIFY"},
58 {http::method::options, "OPTIONS"},
59 {http::method::patch, "PATCH"},
60 {http::method::post, "POST"},
61 {http::method::propfind, "PROPFIND"},
62 {http::method::proppatch, "PROPPATCH"},
63 {http::method::purge, "PURGE"},
64 {http::method::put, "PUT"},
65 {http::method::rebind, "REBIND"},
66 {http::method::report, "REPORT"},
67 {http::method::search, "SEARCH"},
68 {http::method::subscribe, "SUBSCRIBE"},
69 {http::method::trace, "TRACE"},
70 {http::method::unbind, "UNBIND"},
71 {http::method::unlink, "UNLINK"},
72 {http::method::unlock, "UNLOCK"},
73 {http::method::unsubscribe, "UNSUBSCRIBE"},
74 };
75 5780 for(auto const& [m, name] : known)
76 {
77 5610 if(methods & (1ULL << static_cast<unsigned>(m)))
78 {
79 128 if(!result.empty())
80 8 result += ", ";
81 128 result += name;
82 }
83 }
84 174 for(auto const& v : custom)
85 {
86 4 if(!result.empty())
87 result += ", ";
88 4 result += v;
89 }
90 170 return result;
91 170 }
92
93 router_base::opt_flags
94 495 router_base::impl::
95 compute_effective_opts(
96 opt_flags parent,
97 opt_flags child)
98 {
99 495 opt_flags result = parent;
100
101 // case_sensitive: bits 1-2 (2=true, 4=false)
102 495 if(child & 2)
103 4 result = (result & ~6) | 2;
104 491 else if(child & 4)
105 4 result = (result & ~6) | 4;
106
107 // strict: bits 3-4 (8=true, 16=false)
108 495 if(child & 8)
109 1 result = (result & ~24) | 8;
110 494 else if(child & 16)
111 1 result = (result & ~24) | 16;
112
113 495 return result;
114 }
115
116 void
117 34 router_base::impl::
118 restore_path(
119 route_params& p,
120 std::size_t base_len)
121 {
122 34 auto& pv = *route_params_access{p};
123 34 p.base_path = { pv.decoded_path_.data(), base_len };
124 34 auto const path_len = pv.decoded_path_.size() - (pv.addedSlash_ ? 1 : 0);
125 34 if(base_len < path_len)
126 33 p.path = { pv.decoded_path_.data() + base_len,
127 path_len - base_len };
128 else
129 2 p.path = { pv.decoded_path_.data() +
130 1 pv.decoded_path_.size() - 1, 1 }; // soft slash
131 34 }
132
133 void
134 68 router_base::impl::
135 update_allow_for_entry(
136 matcher& m,
137 entry const& e)
138 {
139 68 if(!m.end_)
140 return;
141
142 // Per-matcher collection
143 68 if(e.all)
144 8 m.allowed_methods_ = ~0ULL;
145 60 else if(e.verb != http::method::unknown)
146 58 m.allowed_methods_ |= (1ULL << static_cast<unsigned>(e.verb));
147 2 else if(!e.verb_str.empty())
148 2 m.custom_verbs_.push_back(e.verb_str);
149
150 // Rebuild per-matcher Allow header eagerly
151 68 m.allow_header_ = build_allow_header(
152 68 m.allowed_methods_, m.custom_verbs_);
153
154 // Global collection (for OPTIONS *)
155 68 if(e.all)
156 8 global_methods_ = ~0ULL;
157 60 else if(e.verb != http::method::unknown)
158 58 global_methods_ |= (1ULL << static_cast<unsigned>(e.verb));
159 2 else if(!e.verb_str.empty())
160 2 global_custom_verbs_.push_back(e.verb_str);
161 }
162
163 void
164 117 router_base::impl::
165 rebuild_global_allow_header()
166 {
167 117 std::sort(global_custom_verbs_.begin(), global_custom_verbs_.end());
168 234 global_custom_verbs_.erase(
169 117 std::unique(global_custom_verbs_.begin(), global_custom_verbs_.end()),
170 117 global_custom_verbs_.end());
171 117 global_allow_header_ = build_allow_header(
172 117 global_methods_, global_custom_verbs_);
173 117 }
174
175 void
176 341 router_base::impl::
177 finalize_pending()
178 {
179 341 if(pending_route_ == SIZE_MAX)
180 277 return;
181 64 auto& m = matchers[pending_route_];
182 64 if(entries.size() == m.first_entry_)
183 {
184 // empty route, remove it
185 matchers.pop_back();
186 }
187 else
188 {
189 64 m.skip_ = entries.size();
190 }
191 64 pending_route_ = SIZE_MAX;
192 }
193
194 //------------------------------------------------
195 //
196 // dispatch
197 //
198 //------------------------------------------------
199
200 route_task
201 112 router_base::impl::
202 dispatch_loop(route_params& p, bool is_options) const
203 {
204 auto& pv = *route_params_access{p};
205
206 std::size_t last_matched = SIZE_MAX;
207 std::uint32_t current_depth = 0;
208
209 std::uint64_t options_methods = 0;
210 std::vector<std::string> options_custom_verbs;
211
212 std::size_t path_stack[router_base::max_path_depth];
213 path_stack[0] = 0;
214
215 std::size_t matched_at_depth[router_base::max_path_depth];
216 for(std::size_t d = 0; d < router_base::max_path_depth; ++d)
217 matched_at_depth[d] = SIZE_MAX;
218
219 for(std::size_t i = 0; i < entries.size(); )
220 {
221 auto const& e = entries[i];
222 auto const& m = matchers[e.matcher_idx];
223 auto const target_depth = m.depth_;
224
225 bool ancestors_ok = true;
226
227 std::size_t start_idx = (last_matched == SIZE_MAX) ? 0 : last_matched + 1;
228
229 for(std::size_t check_idx = start_idx;
230 check_idx <= e.matcher_idx && ancestors_ok;
231 ++check_idx)
232 {
233 auto const& cm = matchers[check_idx];
234
235 bool is_needed_ancestor = (cm.depth_ < target_depth) &&
236 (matched_at_depth[cm.depth_] == SIZE_MAX);
237 bool is_self = (check_idx == e.matcher_idx);
238
239 if(!is_needed_ancestor && !is_self)
240 continue;
241
242 if(cm.depth_ <= current_depth && current_depth > 0)
243 {
244 restore_path(p, path_stack[cm.depth_]);
245 }
246
247 if(cm.end_ && pv.kind_ != router_base::is_plain)
248 {
249 i = cm.skip_;
250 ancestors_ok = false;
251 break;
252 }
253
254 pv.case_sensitive = (cm.effective_opts_ & 2) != 0;
255 pv.strict = (cm.effective_opts_ & 8) != 0;
256
257 if(cm.depth_ < router_base::max_path_depth)
258 path_stack[cm.depth_] = p.base_path.size();
259
260 match_result mr;
261 if(!cm(p, mr))
262 {
263 for(std::size_t d = cm.depth_; d < router_base::max_path_depth; ++d)
264 matched_at_depth[d] = SIZE_MAX;
265 i = cm.skip_;
266 ancestors_ok = false;
267 break;
268 }
269
270 if(!mr.params_.empty())
271 {
272 for(auto& param : mr.params_)
273 p.params.push_back(std::move(param));
274 }
275
276 if(cm.depth_ < router_base::max_path_depth)
277 matched_at_depth[cm.depth_] = check_idx;
278
279 last_matched = check_idx;
280 current_depth = cm.depth_ + 1;
281
282 if(current_depth < router_base::max_path_depth)
283 path_stack[current_depth] = p.base_path.size();
284 }
285
286 if(!ancestors_ok)
287 continue;
288
289 // Collect methods from matching end-route matchers for OPTIONS
290 if(is_options && m.end_)
291 {
292 options_methods |= m.allowed_methods_;
293 for(auto const& v : m.custom_verbs_)
294 options_custom_verbs.push_back(v);
295 }
296
297 if(m.end_ && !e.match_method(
298 const_cast<route_params&>(p)))
299 {
300 ++i;
301 continue;
302 }
303
304 if(e.h->kind != pv.kind_)
305 {
306 ++i;
307 continue;
308 }
309
310 //--------------------------------------------------
311 // Invoke handler
312 //--------------------------------------------------
313
314 route_result rv;
315 try
316 {
317 rv = co_await e.h->invoke(
318 const_cast<route_params&>(p));
319 }
320 catch(...)
321 {
322 pv.ep_ = std::current_exception();
323 pv.kind_ = router_base::is_exception;
324 ++i;
325 continue;
326 }
327
328 if(rv.what() == route_what::next)
329 {
330 ++i;
331 continue;
332 }
333
334 if(rv.what() == route_what::next_route)
335 {
336 if(!m.end_)
337 co_return route_error(error::invalid_route_result);
338 i = m.skip_;
339 continue;
340 }
341
342 if(rv.what() == route_what::done ||
343 rv.what() == route_what::close)
344 {
345 co_return rv;
346 }
347
348 // Error - transition to error mode
349 pv.ec_ = rv.error();
350 pv.kind_ = router_base::is_error;
351
352 if(m.end_)
353 {
354 i = m.skip_;
355 continue;
356 }
357
358 ++i;
359 }
360
361 if(pv.kind_ == router_base::is_exception)
362 co_return route_error(error::unhandled_exception);
363 if(pv.kind_ == router_base::is_error)
364 co_return route_error(pv.ec_);
365
366 // OPTIONS fallback
367 if(is_options && options_methods != 0 && options_handler_)
368 {
369 std::string allow = build_allow_header(options_methods, options_custom_verbs);
370 co_return co_await options_handler_->invoke(p, allow);
371 }
372
373 co_return route_next;
374 224 }
375
376 //------------------------------------------------
377 //
378 // router_base
379 //
380 //------------------------------------------------
381
382 168 router_base::
383 router_base(
384 168 opt_flags opt)
385 168 : impl_(std::make_shared<impl>(opt))
386 {
387 168 }
388
389 void
390 65 router_base::
391 add_middleware(
392 std::string_view pattern,
393 handlers hn)
394 {
395 65 impl_->finalize_pending();
396
397 65 if(pattern.empty())
398 34 pattern = "/";
399
400 65 auto const matcher_idx = impl_->matchers.size();
401 65 impl_->matchers.emplace_back(pattern, false);
402 65 auto& m = impl_->matchers.back();
403 65 if(m.error())
404 throw_invalid_argument();
405 65 m.first_entry_ = impl_->entries.size();
406 65 m.effective_opts_ = impl::compute_effective_opts(0, impl_->opt_);
407 65 m.own_opts_ = impl_->opt_;
408 65 m.depth_ = 0;
409
410 139 for(std::size_t i = 0; i < hn.n; ++i)
411 {
412 74 impl_->entries.emplace_back(std::move(hn.p[i]));
413 74 impl_->entries.back().matcher_idx = matcher_idx;
414 }
415
416 65 m.skip_ = impl_->entries.size();
417 65 }
418
419 void
420 50 router_base::
421 inline_router(
422 std::string_view pattern,
423 router_base&& sub)
424 {
425 50 impl_->finalize_pending();
426
427 50 if(!sub.impl_)
428 return;
429
430 50 sub.impl_->finalize_pending();
431
432 50 if(pattern.empty())
433 pattern = "/";
434
435 // Create parent matcher for the mount point
436 50 auto const parent_matcher_idx = impl_->matchers.size();
437 50 impl_->matchers.emplace_back(pattern, false);
438 50 auto& parent_m = impl_->matchers.back();
439 50 if(parent_m.error())
440 throw_invalid_argument();
441 50 parent_m.first_entry_ = impl_->entries.size();
442
443 50 auto parent_eff = impl::compute_effective_opts(0, impl_->opt_);
444 50 parent_m.effective_opts_ = parent_eff;
445 50 parent_m.own_opts_ = impl_->opt_;
446 50 parent_m.depth_ = 0;
447
448 // Check nesting depth
449 50 std::size_t max_sub_depth = 0;
450 332 for(auto const& sm : sub.impl_->matchers)
451 564 max_sub_depth = (std::max)(max_sub_depth,
452 282 static_cast<std::size_t>(sm.depth_));
453 50 if(max_sub_depth + 1 >= max_path_depth)
454 1 throw_length_error(
455 "router nesting depth exceeds max_path_depth");
456
457 // Compute offsets for re-indexing
458 49 auto const matcher_offset = impl_->matchers.size();
459 49 auto const entry_offset = impl_->entries.size();
460
461 // Recompute effective_opts for inlined matchers using depth stack
462 49 auto sub_root_eff = impl::compute_effective_opts(
463 49 parent_eff, sub.impl_->opt_);
464 opt_flags eff_stack[max_path_depth];
465 49 eff_stack[0] = sub_root_eff;
466
467 // Inline sub's matchers
468 315 for(auto& sm : sub.impl_->matchers)
469 {
470 266 auto d = sm.depth_;
471 266 opt_flags parent = (d > 0) ? eff_stack[d - 1] : parent_eff;
472 266 eff_stack[d] = impl::compute_effective_opts(parent, sm.own_opts_);
473 266 sm.effective_opts_ = eff_stack[d];
474 266 sm.depth_ += 1; // increase by 1 (parent is at depth 0)
475 266 sm.first_entry_ += entry_offset;
476 266 sm.skip_ += entry_offset;
477 266 impl_->matchers.push_back(std::move(sm));
478 }
479
480 // Inline sub's entries
481 98 for(auto& se : sub.impl_->entries)
482 {
483 49 se.matcher_idx += matcher_offset;
484 49 impl_->entries.push_back(std::move(se));
485 }
486
487 // Set parent matcher's skip
488 // Need to re-fetch since vector may have reallocated
489 49 impl_->matchers[parent_matcher_idx].skip_ = impl_->entries.size();
490
491 // Merge global methods
492 49 impl_->global_methods_ |= sub.impl_->global_methods_;
493 49 for(auto& v : sub.impl_->global_custom_verbs_)
494 impl_->global_custom_verbs_.push_back(std::move(v));
495 49 impl_->rebuild_global_allow_header();
496
497 // Move options handler if sub has one and parent doesn't
498 49 if(sub.impl_->options_handler_ && !impl_->options_handler_)
499 impl_->options_handler_ = std::move(sub.impl_->options_handler_);
500
501 49 sub.impl_.reset();
502 }
503
504 std::size_t
505 77 router_base::
506 new_route(
507 std::string_view pattern)
508 {
509 77 impl_->finalize_pending();
510
511 77 if(pattern.empty())
512 throw_invalid_argument();
513
514 77 auto const idx = impl_->matchers.size();
515 77 impl_->matchers.emplace_back(pattern, true);
516 77 auto& m = impl_->matchers.back();
517 77 if(m.error())
518 12 throw_invalid_argument();
519 65 m.first_entry_ = impl_->entries.size();
520 65 m.effective_opts_ = impl::compute_effective_opts(0, impl_->opt_);
521 65 m.own_opts_ = impl_->opt_;
522 65 m.depth_ = 0;
523
524 65 impl_->pending_route_ = idx;
525 65 return idx;
526 }
527
528 void
529 58 router_base::
530 add_to_route(
531 std::size_t idx,
532 http::method verb,
533 handlers hn)
534 {
535 58 if(verb == http::method::unknown)
536 throw_invalid_argument();
537
538 58 auto& m = impl_->matchers[idx];
539 116 for(std::size_t i = 0; i < hn.n; ++i)
540 {
541 58 impl_->entries.emplace_back(verb, std::move(hn.p[i]));
542 58 impl_->entries.back().matcher_idx = idx;
543 58 impl_->update_allow_for_entry(m, impl_->entries.back());
544 }
545 58 impl_->rebuild_global_allow_header();
546 58 }
547
548 void
549 10 router_base::
550 add_to_route(
551 std::size_t idx,
552 std::string_view verb,
553 handlers hn)
554 {
555 10 auto& m = impl_->matchers[idx];
556
557 10 if(verb.empty())
558 {
559 // all methods
560 16 for(std::size_t i = 0; i < hn.n; ++i)
561 {
562 8 impl_->entries.emplace_back(std::move(hn.p[i]));
563 8 impl_->entries.back().matcher_idx = idx;
564 8 impl_->update_allow_for_entry(m, impl_->entries.back());
565 }
566 }
567 else
568 {
569 // specific method string
570 4 for(std::size_t i = 0; i < hn.n; ++i)
571 {
572 2 impl_->entries.emplace_back(verb, std::move(hn.p[i]));
573 2 impl_->entries.back().matcher_idx = idx;
574 2 impl_->update_allow_for_entry(m, impl_->entries.back());
575 }
576 }
577 10 impl_->rebuild_global_allow_header();
578 10 }
579
580 void
581 router_base::
582 finalize_pending()
583 {
584 if(impl_)
585 impl_->finalize_pending();
586 }
587
588 void
589 4 router_base::
590 set_options_handler_impl(
591 options_handler_ptr p)
592 {
593 4 impl_->options_handler_ = std::move(p);
594 4 }
595
596 //------------------------------------------------
597 //
598 // dispatch
599 //
600 //------------------------------------------------
601
602 route_task
603 109 router_base::
604 dispatch(
605 http::method verb,
606 urls::url_view const& url,
607 route_params& p) const
608 {
609 109 if(verb == http::method::unknown)
610 1 throw_invalid_argument();
611
612 108 impl_->ensure_finalized();
613
614 // Handle OPTIONS * before normal dispatch
615 113 if(verb == http::method::options &&
616 113 url.encoded_path() == "*")
617 {
618 1 if(impl_->options_handler_)
619 {
620 1 return impl_->options_handler_->invoke(
621 1 p, impl_->global_allow_header_);
622 }
623 }
624
625 // Initialize params
626 107 auto& pv = *route_params_access{p};
627 107 pv.kind_ = is_plain;
628 107 pv.verb_ = verb;
629 107 pv.verb_str_.clear();
630 107 pv.ec_.clear();
631 107 pv.ep_ = nullptr;
632 107 p.params.clear();
633 107 pv.decoded_path_ = pct_decode_path(url.encoded_path());
634 107 if(pv.decoded_path_.empty() || pv.decoded_path_.back() != '/')
635 {
636 70 pv.decoded_path_.push_back('/');
637 70 pv.addedSlash_ = true;
638 }
639 else
640 {
641 37 pv.addedSlash_ = false;
642 }
643 107 p.base_path = { pv.decoded_path_.data(), 0 };
644 107 auto const subtract = (pv.addedSlash_ && pv.decoded_path_.size() > 1) ? 1 : 0;
645 107 p.path = { pv.decoded_path_.data(), pv.decoded_path_.size() - subtract };
646
647 107 return impl_->dispatch_loop(p, verb == http::method::options);
648 }
649
650 route_task
651 6 router_base::
652 dispatch(
653 std::string_view verb,
654 urls::url_view const& url,
655 route_params& p) const
656 {
657 6 if(verb.empty())
658 1 throw_invalid_argument();
659
660 5 impl_->ensure_finalized();
661
662 5 auto const method = http::string_to_method(verb);
663 5 bool const is_options = (method == http::method::options);
664
665 // Handle OPTIONS * before normal dispatch
666 5 if(is_options && url.encoded_path() == "*")
667 {
668 if(impl_->options_handler_)
669 {
670 return impl_->options_handler_->invoke(
671 p, impl_->global_allow_header_);
672 }
673 }
674
675 // Initialize params
676 5 auto& pv = *route_params_access{p};
677 5 pv.kind_ = is_plain;
678 5 pv.verb_ = method;
679 5 if(pv.verb_ == http::method::unknown)
680 4 pv.verb_str_ = verb;
681 else
682 1 pv.verb_str_.clear();
683 5 pv.ec_.clear();
684 5 pv.ep_ = nullptr;
685 5 p.params.clear();
686 5 pv.decoded_path_ = pct_decode_path(url.encoded_path());
687 5 if(pv.decoded_path_.empty() || pv.decoded_path_.back() != '/')
688 {
689 pv.decoded_path_.push_back('/');
690 pv.addedSlash_ = true;
691 }
692 else
693 {
694 5 pv.addedSlash_ = false;
695 }
696 5 p.base_path = { pv.decoded_path_.data(), 0 };
697 5 auto const subtract = (pv.addedSlash_ && pv.decoded_path_.size() > 1) ? 1 : 0;
698 5 p.path = { pv.decoded_path_.data(), pv.decoded_path_.size() - subtract };
699
700 5 return impl_->dispatch_loop(p, is_options);
701 }
702
703 } // detail
704 } // http
705 } // boost
706