TLA Line data Source code
1 : //
2 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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 "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 HIT 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 MIS 0 : result += ", ";
88 HIT 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 MIS 0 : return;
141 :
142 : // Per-matcher collection
143 HIT 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 MIS 0 : matchers.pop_back();
186 : }
187 : else
188 : {
189 HIT 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 MIS 0 : throw_invalid_argument();
405 HIT 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 MIS 0 : return;
429 :
430 HIT 50 : sub.impl_->finalize_pending();
431 :
432 50 : if(pattern.empty())
433 MIS 0 : pattern = "/";
434 :
435 : // Create parent matcher for the mount point
436 HIT 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 MIS 0 : throw_invalid_argument();
441 HIT 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 MIS 0 : impl_->global_custom_verbs_.push_back(std::move(v));
495 HIT 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 MIS 0 : impl_->options_handler_ = std::move(sub.impl_->options_handler_);
500 :
501 HIT 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 MIS 0 : throw_invalid_argument();
513 :
514 HIT 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 MIS 0 : throw_invalid_argument();
537 :
538 HIT 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 MIS 0 : router_base::
582 : finalize_pending()
583 : {
584 0 : if(impl_)
585 0 : impl_->finalize_pending();
586 0 : }
587 :
588 : void
589 HIT 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 MIS 0 : if(impl_->options_handler_)
669 : {
670 0 : return impl_->options_handler_->invoke(
671 0 : p, impl_->global_allow_header_);
672 : }
673 : }
674 :
675 : // Initialize params
676 HIT 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 MIS 0 : pv.decoded_path_.push_back('/');
690 0 : pv.addedSlash_ = true;
691 : }
692 : else
693 : {
694 HIT 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
|