From d300f0ba818829dd0c2c9ec7def05bf207a2be18 Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Sat, 17 Jan 2026 09:17:44 -0800 Subject: [PATCH 01/12] Call run_async --- src/server/route_handler.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server/route_handler.cpp b/src/server/route_handler.cpp index 184c08a0..b75e8fc8 100644 --- a/src/server/route_handler.cpp +++ b/src/server/route_handler.cpp @@ -9,7 +9,7 @@ #include #include -#include +#include namespace boost { namespace http { @@ -59,7 +59,7 @@ spawn( return this->suspend( [ex = this->ex, t = std::move(t)](resumer resume) mutable { - capy::async_run(ex)(std::move(t), + capy::run_async(ex, [resume](route_result rv) { resume(rv); @@ -67,7 +67,7 @@ spawn( [resume](std::exception_ptr ep) { resume(ep); - }); + })(std::move(t)); }); } From 6e6ffb56bcf704d31bc39018fcfc324e92d5060b Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Fri, 16 Jan 2026 04:59:01 -0800 Subject: [PATCH 02/12] Add new router and flat router --- .gitignore | 21 + CMakePresets.json | 16 - doc/modules/ROOT/pages/server/router.adoc | 17 +- include/boost/http.hpp | 1 - include/boost/http/server/basic_router.hpp | 1246 ------------- .../boost/http/server/detail/router_base.hpp | 112 ++ include/boost/http/server/flat_router.hpp | 94 + include/boost/http/server/router.hpp | 847 ++++++++- include/boost/http/server/router_types.hpp | 114 +- src/server/basic_router.cpp | 903 --------- src/server/detail/pct_decode.cpp | 153 ++ src/server/detail/pct_decode.hpp | 42 + src/server/detail/route_match.cpp | 99 + src/server/detail/route_match.hpp | 59 + src/server/{ => detail}/route_rule.hpp | 84 +- src/server/detail/router_base.cpp | 187 ++ src/server/detail/router_base.hpp | 127 ++ src/server/detail/stable_string.hpp | 102 ++ src/server/flat_router.cpp | 424 +++++ src/server/route_rule.cpp | 16 - test/unit/server/basic_router.cpp | 1624 ----------------- test/unit/server/flat_router.cpp | 32 + test/unit/server/route_handler.cpp | 30 +- test/unit/server/router.cpp | 462 +++++ test/unit/server/router_types.cpp | 16 - 25 files changed, 2874 insertions(+), 3954 deletions(-) delete mode 100644 CMakePresets.json delete mode 100644 include/boost/http/server/basic_router.hpp create mode 100644 include/boost/http/server/detail/router_base.hpp create mode 100644 include/boost/http/server/flat_router.hpp delete mode 100644 src/server/basic_router.cpp create mode 100644 src/server/detail/pct_decode.cpp create mode 100644 src/server/detail/pct_decode.hpp create mode 100644 src/server/detail/route_match.cpp create mode 100644 src/server/detail/route_match.hpp rename src/server/{ => detail}/route_rule.hpp (83%) create mode 100644 src/server/detail/router_base.cpp create mode 100644 src/server/detail/router_base.hpp create mode 100644 src/server/detail/stable_string.hpp create mode 100644 src/server/flat_router.cpp delete mode 100644 src/server/route_rule.cpp delete mode 100644 test/unit/server/basic_router.cpp create mode 100644 test/unit/server/flat_router.cpp create mode 100644 test/unit/server/router.cpp diff --git a/.gitignore b/.gitignore index 06c484e0..a9ff6e47 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,24 @@ !/build/Jamfile /out/ CMakeUserPresets.json + +# CMake artifacts (if accidentally run from source dir) +#CMakeCache.txt +#CMakeFiles/ +#cmake_install.cmake +#CTestTestfile.cmake +#DartConfiguration.tcl +#build.ninja +#.ninja_deps +#.ninja_log +#*.lib +#Dependencies/ +#test/**/cmake_install.cmake +#test/**/CTestTestfile.cmake +#test/**/test_suite/ +#test/**/*.cmake + +# Build artifacts +#*.exe +#*.pdb +#*.dll diff --git a/CMakePresets.json b/CMakePresets.json deleted file mode 100644 index ca4abfb1..00000000 --- a/CMakePresets.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "version": 8, - "configurePresets": [ - { - "name": "Custom configure preset", - "displayName": "Custom configure preset", - "description": "Sets Ninja generator, build and install directory", - "generator": "Ninja", - "binaryDir": "${sourceDir}/out/build/${presetName}", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug", - "CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}" - } - } - ] -} \ No newline at end of file diff --git a/doc/modules/ROOT/pages/server/router.adoc b/doc/modules/ROOT/pages/server/router.adoc index ed181fc7..761be744 100644 --- a/doc/modules/ROOT/pages/server/router.adoc +++ b/doc/modules/ROOT/pages/server/router.adoc @@ -45,9 +45,9 @@ appropriate handlers in order. using namespace boost::http; -basic_router router; +router r; -router.add(method::get, "/hello", +r.add(method::get, "/hello", [](route_params& p) { p.status(status::ok); @@ -232,7 +232,7 @@ Configure matching behavior when constructing the router: [source,cpp] ---- -basic_router router( +router r( router_options() .case_sensitive(true) // Paths are case-sensitive .strict(true)); // Trailing slash matters @@ -265,10 +265,10 @@ using namespace boost::http; int main() { - basic_router router; + router r; // Health check endpoint - router.add(method::get, "/health", + r.add(method::get, "/health", [](route_params& p) { p.status(status::ok); @@ -277,7 +277,7 @@ int main() }); // API routes - router.route("/api/echo") + r.route("/api/echo") .add(method::post, [](route_params& p) { @@ -292,9 +292,10 @@ int main() return route::send; }); - // Dispatch a request + // Dispatch a request using flat_router + flat_router fr(std::move(r)); route_params p; - auto rv = router.dispatch( + auto rv = co_await fr.dispatch( method::get, urls::url_view("/health"), p); diff --git a/include/boost/http.hpp b/include/boost/http.hpp index 46cd02d1..abbebbcf 100644 --- a/include/boost/http.hpp +++ b/include/boost/http.hpp @@ -44,7 +44,6 @@ #include #include -#include #include #include #include diff --git a/include/boost/http/server/basic_router.hpp b/include/boost/http/server/basic_router.hpp deleted file mode 100644 index c7c2c06b..00000000 --- a/include/boost/http/server/basic_router.hpp +++ /dev/null @@ -1,1246 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/http -// - -#ifndef BOOST_HTTP_SERVER_BASIC_ROUTER_HPP -#define BOOST_HTTP_SERVER_BASIC_ROUTER_HPP - -#include -#include -#include -#include -#include // VFALCO fix -#include -#include -#include -#include -#include -#include - -namespace boost { -namespace http { - -template -class basic_router; - -/** Configuration options for HTTP routers. -*/ -struct router_options -{ - /** Constructor. - - Routers constructed with default options inherit the values of - @ref case_sensitive and @ref strict from the parent router. - If there is no parent, both default to `false`. - The value of @ref merge_params always defaults to `false` - and is never inherited. - */ - router_options() = default; - - /** Set whether to merge parameters from parent routers. - - This setting controls whether route parameters defined on parent - routers are made available in nested routers. It is not inherited - and always defaults to `false`. - - @par Example - @code - router r( router_options() - .merge_params( true ) - .case_sensitive( true ) - .strict( false ) ); - @endcode - - @param value `true` to merge parameters from parent routers. - - @return A reference to `*this` for chaining. - */ - router_options& - merge_params( - bool value) noexcept - { - v_ = (v_ & ~1) | (value ? 1 : 0); - return *this; - } - - /** Set whether pattern matching is case-sensitive. - - When this option is not set explicitly, the value is inherited - from the parent router or defaults to `false` if there is no parent. - - @par Example - @code - router r( router_options() - .case_sensitive( true ) - .strict( true ) ); - @endcode - - @param value `true` to perform case-sensitive path matching. - - @return A reference to `*this` for chaining. - */ - router_options& - case_sensitive( - bool value) noexcept - { - if(value) - v_ = (v_ & ~6) | 2; - else - v_ = (v_ & ~6) | 4; - return *this; - } - - /** Set whether pattern matching is strict. - - When this option is not set explicitly, the value is inherited - from the parent router or defaults to `false` if there is no parent. - Strict matching treats a trailing slash as significant: - the pattern `"/api"` matches `"/api"` but not `"/api/"`. - When strict matching is disabled, these paths are treated - as equivalent. - - @par Example - @code - router r( router_options() - .strict( true ) - .case_sensitive( false ) ); - @endcode - - @param value `true` to enable strict path matching. - - @return A reference to `*this` for chaining. - */ - router_options& - strict( - bool value) noexcept - { - if(value) - v_ = (v_ & ~24) | 8; - else - v_ = (v_ & ~24) | 16; - return *this; - } - -private: - template friend class basic_router; - unsigned int v_ = 0; -}; - -//----------------------------------------------- - -//namespace detail { - -class any_router; - -//----------------------------------------------- - -// implementation for all routers -class any_router -{ -private: - template - friend class http::basic_router; - using opt_flags = unsigned int; - - struct BOOST_HTTP_DECL any_handler - { - virtual ~any_handler() = default; - virtual std::size_t count() const noexcept = 0; - virtual route_result invoke( - route_params_base&) const = 0; - }; - - using handler_ptr = std::unique_ptr; - - struct handler_list - { - std::size_t n; - handler_ptr* p; - }; - - using match_result = route_params_base::match_result; - struct matcher; - struct layer; - struct impl; - - BOOST_HTTP_DECL ~any_router(); - BOOST_HTTP_DECL any_router(opt_flags); - BOOST_HTTP_DECL any_router(any_router&&) noexcept; - BOOST_HTTP_DECL any_router(any_router const&) noexcept; - BOOST_HTTP_DECL any_router& operator=(any_router&&) noexcept; - BOOST_HTTP_DECL any_router& operator=(any_router const&) noexcept; - BOOST_HTTP_DECL std::size_t count() const noexcept; - BOOST_HTTP_DECL layer& new_layer(core::string_view pattern); - BOOST_HTTP_DECL void add_impl(core::string_view, handler_list const&); - BOOST_HTTP_DECL void add_impl(layer&, - http::method, handler_list const&); - BOOST_HTTP_DECL void add_impl(layer&, - core::string_view, handler_list const&); - BOOST_HTTP_DECL route_result resume_impl( - route_params_base&, route_result ec) const; - BOOST_HTTP_DECL route_result dispatch_impl(http::method, - core::string_view, urls::url_view const&, - route_params_base&) const; - BOOST_HTTP_DECL route_result dispatch_impl( - route_params_base&) const; - route_result do_dispatch(route_params_base&) const; - - impl* impl_ = nullptr; -}; - -//} // detail - -//----------------------------------------------- - -/** A container for HTTP route handlers. - - `basic_router` objects store and dispatch route handlers based on the - HTTP method and path of an incoming request. Routes are added with a - path pattern, method, and an associated handler, and the router is then - used to dispatch the appropriate handler. - - Patterns used to create route definitions have percent-decoding applied - when handlers are mounted. A literal "%2F" in the pattern string is - indistinguishable from a literal '/'. For example, "/x%2Fz" is the - same as "/x/z" when used as a pattern. - - @par Example - @code - using router_type = basic_router; - router_type router; - router.get( "/hello", - []( route_params& p ) - { - p.res.status( status::ok ); - p.res.set_body( "Hello, world!" ); - return route::send; - } ); - @endcode - - Router objects are lightweight, shared references to their contents. - Copies of a router obtained through construction, conversion, or - assignment do not create new instances; they all refer to the same - underlying data. - - @par Handlers - - Regular handlers are invoked for matching routes and have this - equivalent signature: - @code - route_result handler( Params& p ) - @endcode - - The return value is a @ref route_result used to indicate the desired - action through @ref route enum values, or to indicate that a failure - occurred. Failures are represented by error codes for which - `system::error_code::failed()` returns `true`. - - When a failing error code is produced and remains unhandled, the - router enters error-dispatching mode. In this mode, only error - handlers are invoked. Error handlers are registered globally or - for specific paths and execute in the order of registration whenever - a failing error code is present in the response. - - Error handlers have this equivalent signature: - @code - route_result error_handler( Params& p, system::error_code ec ) - @endcode - - Each error handler may return any failing @ref system::error_code, - which is equivalent to calling: - @code - p.next( ec ); // with ec.failed() == true - @endcode - - Returning @ref route::next indicates that control should proceed to - the next matching error handler. Returning a different failing code - replaces the current error and continues dispatch in error mode using - that new code. Error handlers are invoked until one returns a result - other than @ref route::next. - - Exception handlers have this equivalent signature: - @code - route_result exception_handler( Params& p, E ex ) - @endcode - - Where `E` is the type of exception caught. Handlers installed for an - exception of type `E` will also be called when the exception type is - a derived class of `E`. Exception handlers are invoked in the order - of registration whenever an exception is present in the request. - - The prefix match is not strict: middleware attached to `"/api"` - will also match `"/api/users"` and `"/api/data"`. When registered - before route handlers for the same prefix, middleware runs before - those routes. This is analogous to `app.use( path, ... )` in - Express.js. - - @par Thread Safety - - Member functions marked `const` such as @ref dispatch and @ref resume - may be called concurrently on routers that refer to the same data. - Modification of routers through calls to non-`const` member functions - is not thread-safe and must not be performed concurrently with any - other member function. - - @par Constraints - - `Params` must be publicly derived from @ref route_params_base. - - @tparam Params The type of the parameters object passed to handlers. -*/ -template -class basic_router : public /*detail::*/any_router -{ - // Params must be publicly derived from route_params_base - BOOST_CORE_STATIC_ASSERT( - detail::derived_from::value); - - // 0 = unrecognized - // 1 = normal handler (Params&) - // 2 = error handler (Params&, error_code) - // 4 = basic_router - - template - struct handler_type - : std::integral_constant - { - }; - - // route_result( Params& ) const - template - struct handler_type()( - std::declval())), - route_result>::value - >::type> : std::integral_constant {}; - - // route_result( Params&, system::error_code const& ) const - template - struct handler_type()( - std::declval(), - std::declval())), - route_result>::value - >::type> : std::integral_constant {}; - - // basic_router - template - struct handler_type::value && - std::is_convertible::value && - std::is_constructible>::value - >::type> : std::integral_constant {}; - - template - struct handler_check : std::true_type {}; - - template - struct handler_check - : std::conditional< - ( (handler_type::value & Mask) != 0 ), - handler_check, - std::false_type - >::type {}; - - // exception handler (Params&, E) - - template - struct except_type : std::false_type {}; - - template - struct except_type{} && ( - mp11::mp_size::arg_types>{} == 2) && - std::is_convertible::arg_types>>::value - >::type> - : std::true_type - { - // type of exception - using type = typename std::decay::arg_types>>::type; - }; - - template using except_types = - mp11::mp_all< except_type... >; - -public: - /** The type of params used in handlers. - */ - using params_type = Params; - - /** A fluent interface for defining handlers on a specific route. - - This type represents a single route within the router and - provides a chainable API for registering handlers associated - with particular HTTP methods or for all methods collectively. - - Typical usage registers one or more handlers for a route: - @code - router.route( "/users/:id" ) - .get( show_user ) - .put( update_user ) - .all( log_access ); - @endcode - - Each call appends handlers in registration order. - */ - class fluent_route; - - /** Constructor. - - Creates an empty router with the specified configuration. - Routers constructed with default options inherit the values - of @ref router_options::case_sensitive and - @ref router_options::strict from the parent router, or default - to `false` if there is no parent. The value of - @ref router_options::merge_params defaults to `false` and - is never inherited. - - @param options The configuration options to use. - */ - explicit - basic_router( - router_options options = {}) - : any_router(options.v_) - { - } - - /** Construct a router from another router with compatible types. - - This constructs a router that shares the same underlying routing - state as another router whose params type is a base class of `Params`. - - The resulting router participates in shared ownership of the - implementation; copying the router does not duplicate routes or - handlers, and changes visible through one router are visible - through all routers that share the same underlying state. - - @par Constraints - - `Params` must be derived from `OtherParams`. - - @param other The router to construct from. - - @tparam OtherParams The params type of the source router. - */ - template< - class OtherParams, - class = typename std::enable_if< - detail::derived_from::value>::type - > - basic_router( - basic_router const& other) noexcept - : any_router(other) - { - } - - /** Add middleware handlers for a path prefix. - - Each handler registered with this function participates in the - routing and error-dispatch process for requests whose path begins - with the specified prefix, as described in the @ref basic_router - class documentation. Handlers execute in the order they are added - and may return @ref route::next to transfer control to the - subsequent handler in the chain. - - @par Example - @code - router.use( "/api", - []( route_params& p ) - { - if( ! authenticate( p ) ) - { - p.res.status( 401 ); - p.res.set_body( "Unauthorized" ); - return route::send; - } - return route::next; - }, - []( route_params& p ) - { - p.res.set_header( "X-Powered-By", "MyServer" ); - return route::next; - } ); - @endcode - - @par Preconditions - - @p pattern must be a valid path prefix; it may be empty to - indicate the root scope. - - @param pattern The pattern to match. - - @param h1 The first handler to add. - - @param hn Additional handlers to add, invoked after @p h1 in - registration order. - */ - template - void use( - core::string_view pattern, - H1&& h1, HN... hn) - { - // If you get a compile error on this line it means that - // one or more of the provided types is not a valid handler, - // error handler, or router. - BOOST_CORE_STATIC_ASSERT(handler_check<7, H1, HN...>::value); - add_impl(pattern, make_handler_list( - std::forward

(h1), std::forward(hn)...)); - } - - /** Add global middleware handlers. - - Each handler registered with this function participates in the - routing and error-dispatch process as described in the - @ref basic_router class documentation. Handlers execute in the - order they are added and may return @ref route::next to transfer - control to the next handler in the chain. - - This is equivalent to writing: - @code - use( "/", h1, hn... ); - @endcode - - @par Example - @code - router.use( - []( Params& p ) - { - p.res.erase( "X-Powered-By" ); - return route::next; - } ); - @endcode - - @par Constraints - - @p h1 must not be convertible to @ref core::string_view. - - @param h1 The first handler to add. - - @param hn Additional handlers to add, invoked after @p h1 in - registration order. - */ - template::value>::type> - void use(H1&& h1, HN&&... hn) - { - // If you get a compile error on this line it means that - // one or more of the provided types is not a valid handler, - // error handler, or router. - BOOST_CORE_STATIC_ASSERT(handler_check<7, H1, HN...>::value); - use(core::string_view(), - std::forward

(h1), std::forward(hn)...); - } - - /** Add exception handlers for a route pattern. - - Registers one or more exception handlers that will be invoked - when an exception is thrown during request processing for routes - matching the specified pattern. - - Handlers are invoked in the order provided until one handles - the exception. - - @par Example - @code - app.except( "/api*", - []( route_params& p, std::exception const& ex ) - { - p.res.set_status( 500 ); - return route::send; - } ); - @endcode - - @param pattern The route pattern to match, or empty to match - all routes. - - @param h1 The first exception handler. - - @param hn Additional exception handlers. - */ - template - void except( - core::string_view pattern, - H1&& h1, HN... hn) - { - // If you get a compile error on this line it means that one or - // more of the provided types is not a valid exception handler - BOOST_CORE_STATIC_ASSERT(except_types::value); - add_impl(pattern, make_except_list( - std::forward

(h1), std::forward(hn)...)); - } - - /** Add global exception handlers. - - Registers one or more exception handlers that will be invoked - when an exception is thrown during request processing for any - route. - - Equivalent to calling `except( "", h1, hn... )`. - - @par Example - @code - app.except( - []( route_params& p, std::exception const& ex ) - { - p.res.set_status( 500 ); - return route::send; - } ); - @endcode - - @param h1 The first exception handler. - - @param hn Additional exception handlers. - */ - template::value>::type> - void except(H1&& h1, HN&&... hn) - { - // If you get a compile error on this line it means that one or - // more of the provided types is not a valid exception handler - BOOST_CORE_STATIC_ASSERT(except_types::value); - except(core::string_view(), - std::forward

(h1), std::forward(hn)...); - } - - /** Add handlers for all HTTP methods matching a path pattern. - - This registers regular handlers for the specified path pattern, - participating in dispatch as described in the @ref basic_router - class documentation. Handlers run when the route matches, - regardless of HTTP method, and execute in registration order. - Error handlers and routers cannot be passed here. A new route - object is created even if the pattern already exists. - - @par Example - @code - router.route( "/status" ) - .add( method::head, check_headers ) - .add( method::get, send_status ) - .all( log_access ); - @endcode - - @par Preconditions - - @p pattern must be a valid path pattern; it must not be empty. - - @param pattern The path pattern to match. - - @param h1 The first handler to add. - - @param hn Additional handlers to add, invoked after @p h1 in - registration order. - */ - template - void all( - core::string_view pattern, - H1&& h1, HN&&... hn) - { - // If you get a compile error on this line it means that - // one or more of the provided types is not a valid handler. - // Error handlers and routers cannot be passed here. - BOOST_CORE_STATIC_ASSERT(handler_check<1, H1, HN...>::value); - this->route(pattern).all( - std::forward

(h1), std::forward(hn)...); - } - - /** Add route handlers for a method and pattern. - - This registers regular handlers for the specified HTTP verb and - path pattern, participating in dispatch as described in the - @ref basic_router class documentation. Error handlers and - routers cannot be passed here. - - @param verb The known HTTP method to match. - - @param pattern The path pattern to match. - - @param h1 The first handler to add. - - @param hn Additional handlers to add, invoked after @p h1 in - registration order. - */ - template - void add( - http::method verb, - core::string_view pattern, - H1&& h1, HN&&... hn) - { - // If you get a compile error on this line it means that - // one or more of the provided types is not a valid handler. - // Error handlers and routers cannot be passed here. - BOOST_CORE_STATIC_ASSERT(handler_check<1, H1, HN...>::value); - this->route(pattern).add(verb, - std::forward

(h1), std::forward(hn)...); - } - - /** Add route handlers for a method string and pattern. - - This registers regular handlers for the specified HTTP verb and - path pattern, participating in dispatch as described in the - @ref basic_router class documentation. Error handlers and - routers cannot be passed here. - - @param verb The HTTP method string to match. - - @param pattern The path pattern to match. - - @param h1 The first handler to add. - - @param hn Additional handlers to add, invoked after @p h1 in - registration order. - */ - template - void add( - core::string_view verb, - core::string_view pattern, - H1&& h1, HN&&... hn) - { - // If you get a compile error on this line it means that - // one or more of the provided types is not a valid handler. - // Error handlers and routers cannot be passed here. - BOOST_CORE_STATIC_ASSERT(handler_check<1, H1, HN...>::value); - this->route(pattern).add(verb, - std::forward

(h1), std::forward(hn)...); - } - - /** Return a fluent route for the specified path pattern. - - Adds a new route to the router for the given pattern. - A new route object is always created, even if another - route with the same pattern already exists. The returned - @ref fluent_route reference allows method-specific handler - registration (such as GET or POST) or catch-all handlers - with @ref fluent_route::all. - - @param pattern The path expression to match against request - targets. This may include parameters or wildcards following - the router's pattern syntax. May not be empty. - - @return A fluent route interface for chaining handler - registrations. - */ - auto - route( - core::string_view pattern) -> fluent_route - { - return fluent_route(*this, pattern); - } - - //-------------------------------------------- - - /** Dispatch a request to the appropriate handler. - - This runs the routing and error-dispatch logic for the given HTTP - method and target URL, as described in the @ref basic_router class - documentation. - - @par Thread Safety - - This function may be called concurrently on the same object along - with other `const` member functions. Each concurrent invocation - must use distinct params objects. - - @param verb The HTTP method to match. This must not be - @ref http::method::unknown. - - @param url The full request target used for route matching. - - @param p The params to pass to handlers. - - @return The @ref route_result describing how routing completed. - - @throws std::invalid_argument If @p verb is - @ref http::method::unknown. - */ - auto - dispatch( - http::method verb, - urls::url_view const& url, - Params& p) const -> - route_result - { - if(verb == http::method::unknown) - detail::throw_invalid_argument(); - return dispatch_impl(verb, - core::string_view(), url, p); - } - - /** Dispatch a request using a method string. - - This runs the routing and error-dispatch logic for the given HTTP - method string and target URL, as described in the @ref basic_router - class documentation. This overload is intended for method tokens - that are not represented by @ref http::method. - - @par Thread Safety - - This function may be called concurrently on the same object along - with other `const` member functions. Each concurrent invocation - must use distinct params objects. - - @param verb The HTTP method string to match. This must not - be empty. - - @param url The full request target used for route matching. - - @param p The params to pass to handlers. - - @return The @ref route_result describing how routing completed. - - @throws std::invalid_argument If @p verb is empty. - */ - auto - dispatch( - core::string_view verb, - urls::url_view const& url, - Params& p) -> - route_result - { - // verb cannot be empty - if(verb.empty()) - detail::throw_invalid_argument(); - return dispatch_impl( - http::method::unknown, - verb, url, p); - } - - /** Resume dispatch after a suspension. - - This continues routing after a previous call to @ref dispatch - returned @ref route::suspend. It recreates the routing state and - resumes as if the handler that suspended had instead returned - the specified @p rv from its body. The regular routing and - error-dispatch logic then proceeds as described in the - @ref basic_router class documentation. For example, if @p rv is - @ref route::next, the next matching handlers are invoked. - - @par Thread Safety - - This function may be called concurrently on the same object along - with other `const` member functions. Each concurrent invocation - must use distinct params objects. - - @param p The params to pass to handlers. - - @param rv The @ref route_result to resume with, as if returned - by the suspended handler. - - @return The @ref route_result describing how routing completed. - */ - auto - resume( - Params& p, - route_result const& rv) const -> - route_result - { - return resume_impl(p, rv); - } - -private: - // used to avoid a race when modifying p.resume_ - struct set_resume - { - std::size_t& resume; - bool cancel_ = true; - - ~set_resume() - { - if(cancel_) - resume = 0; - } - - set_resume( - route_params_base& p) noexcept - : resume(p.resume_) - { - resume = p.pos_; - } - - void apply() noexcept - { - cancel_ = false; - } - }; - - // wrapper for route handlers - template< - class H, - class Ty = handler_type::type > > - struct handler_impl : any_handler - { - typename std::decay::type h; - - template - explicit handler_impl(Args&&... args) - : h(std::forward(args)...) - { - } - - std::size_t - count() const noexcept override - { - return count(Ty{}); - } - - route_result - invoke( - route_params_base& p) const override - { - return invoke(static_cast(p), Ty{}); - } - - private: - std::size_t count( - std::integral_constant) = delete; - - std::size_t count( - std::integral_constant) const noexcept - { - return 1; - } - - std::size_t count( - std::integral_constant) const noexcept - { - return 1; - } - - std::size_t count( - std::integral_constant) const noexcept - { - return 1 + h.count(); - } - - route_result invoke(Params&, - std::integral_constant) const = delete; - - // ( Params& ) - route_result invoke(Params& p, - std::integral_constant) const - { - route_params_base& p_(p); - if( p_.ec_.failed() || - p_.ep_) - return http::route::next; - set_resume u(p_); - auto rv = h(p); - if(rv == http::route::suspend) - { - u.apply(); - return rv; - } - return rv; - } - - // ( Params&, error_code ) - route_result - invoke(Params& p, - std::integral_constant) const - { - route_params_base& p_(p); - if(! p_.ec_.failed()) - return http::route::next; - set_resume u(p_); - auto rv = h(p, p_.ec_); - if(rv == http::route::suspend) - { - u.apply(); - return rv; - } - return rv; - } - - // any_router - route_result invoke(Params& p, - std::integral_constant) const - { - route_params_base& p_(p); - if( p_.resume_ > 0 || - ( ! p_.ec_.failed() && - ! p_.ep_)) - return h.dispatch_impl(p); - return http::route::next; - } - }; - - template::type>::type> - struct except_impl : any_handler - { - typename std::decay::type h; - - template - explicit except_impl(Args&&... args) - : h(std::forward(args)...) - { - } - - std::size_t - count() const noexcept override - { - return 1; - } - - route_result - invoke(Params& p) const override - { - #ifndef BOOST_NO_EXCEPTIONS - route_params_base& p_(p); - if(! p_.ep_) - return http::route::next; - try - { - std::rethrow_exception(p_.ep_); - } - catch(E const& ex) - { -#ifdef __APPLE__ - // The Apple linker has a bug whereby it can erroneously - // deduplicate functions with identical codegen even though the - // unwind tables are different. Here we force different codegen - // depending on the type of E. - static volatile int apple_linker_bug_workaround_ = 0; - if(apple_linker_bug_workaround_) - throw ex; -#endif // APPLE - set_resume u(p_); - // VFALCO What if h throws? - auto rv = h(p, ex); - if(rv == http::route::suspend) - { - u.apply(); - return rv; - } - return rv; - } - catch(...) - { - BOOST_ASSERT(p_.ep_); - return http::route::next; - } - #else - (void)p; - return http::route::next; - #endif - } - }; - - template - struct handler_list_impl : handler_list - { - template - explicit handler_list_impl(HN&&... hn) - { - n = sizeof...(HN); - p = v; - assign<0>(std::forward(hn)...); - } - - // exception handlers - template - explicit handler_list_impl(int, HN&&... hn) - { - n = sizeof...(HN); - p = v; - assign<0>(0, std::forward(hn)...); - } - - private: - template - void assign(H1&& h1, HN&&... hn) - { - v[I] = handler_ptr(new handler_impl

( - std::forward

(h1))); - assign(std::forward(hn)...); - } - - // exception handlers - template - void assign(int, H1&& h1, HN&&... hn) - { - v[I] = handler_ptr(new except_impl

( - std::forward

(h1))); - assign(0, std::forward(hn)...); - } - - template - void assign(int = 0) - { - } - - handler_ptr v[N]; - }; - - template - static auto - make_handler_list(HN&&... hn) -> - handler_list_impl - { - return handler_list_impl( - std::forward(hn)...); - } - - template - static auto - make_except_list(HN&&... hn) -> - handler_list_impl - { - return handler_list_impl( - 0, std::forward(hn)...); - } - - void append(layer& e, - http::method verb, - handler_list const& handlers) - { - add_impl(e, verb, handlers); - } -}; - -//----------------------------------------------- - -template -class basic_router:: - fluent_route -{ -public: - fluent_route(fluent_route const&) = default; - - /** Add handlers that apply to all HTTP methods. - - This registers regular handlers that run for any request matching - the route's pattern, regardless of HTTP method. Handlers are - appended to the route's handler sequence and are invoked in - registration order whenever a preceding handler returns - @ref route::next. Error handlers and routers cannot be passed here. - - This function returns a @ref fluent_route, allowing additional - method registrations to be chained. For example: - @code - router.route( "/resource" ) - .all( log_request ) - .add( method::get, show_resource ) - .add( method::post, update_resource ); - @endcode - - @param h1 The first handler to add. - - @param hn Additional handlers to add, invoked after @p h1 in - registration order. - - @return A reference to `*this` for chained registrations. - */ - template - auto all( - H1&& h1, HN&&... hn) -> - fluent_route - { - // If you get a compile error on this line it means that - // one or more of the provided types is not a valid handler. - // Error handlers and routers cannot be passed here. - BOOST_CORE_STATIC_ASSERT(handler_check<1, H1, HN...>::value); - owner_.add_impl(e_, core::string_view(), make_handler_list( - std::forward

(h1), std::forward(hn)...)); - return *this; - } - - /** Add handlers for a specific HTTP method. - - This registers regular handlers for the given method on the - current route, participating in dispatch as described in the - @ref basic_router class documentation. Handlers are appended - to the route's handler sequence and invoked in registration - order whenever a preceding handler returns @ref route::next. - Error handlers and routers cannot be passed here. - - @param verb The HTTP method to match. - - @param h1 The first handler to add. - - @param hn Additional handlers to add, invoked after @p h1 in - registration order. - - @return A reference to `*this` for chained registrations. - */ - template - auto add( - http::method verb, - H1&& h1, HN&&... hn) -> - fluent_route - { - // If you get a compile error on this line it means that - // one or more of the provided types is not a valid handler. - // Error handlers and routers cannot be passed here. - BOOST_CORE_STATIC_ASSERT(handler_check<1, H1, HN...>::value); - owner_.add_impl(e_, verb, make_handler_list( - std::forward

(h1), std::forward(hn)...)); - return *this; - } - - /** Add handlers for a method string. - - This registers regular handlers for the given HTTP method string - on the current route, participating in dispatch as described in - the @ref basic_router class documentation. This overload is - intended for methods not represented by @ref http::method. - Handlers are appended to the route's handler sequence and invoked - in registration order whenever a preceding handler returns - @ref route::next. Error handlers and routers cannot be passed here. - - @param verb The HTTP method string to match. - - @param h1 The first handler to add. - - @param hn Additional handlers to add, invoked after @p h1 in - registration order. - - @return A reference to `*this` for chained registrations. - */ - template - auto add( - core::string_view verb, - H1&& h1, HN&&... hn) -> - fluent_route - { - // If you get a compile error on this line it means that - // one or more of the provided types is not a valid handler. - // Error handlers and routers cannot be passed here. - BOOST_CORE_STATIC_ASSERT(handler_check<1, H1, HN...>::value); - owner_.add_impl(e_, verb, make_handler_list( - std::forward

(h1), std::forward(hn)...)); - return *this; - } - -private: - friend class basic_router; - fluent_route( - basic_router& owner, - core::string_view pattern) - : e_(owner.new_layer(pattern)) - , owner_(owner) - { - } - - layer& e_; - basic_router& owner_; -}; - -} // http -} // boost - -#endif \ No newline at end of file diff --git a/include/boost/http/server/detail/router_base.hpp b/include/boost/http/server/detail/router_base.hpp new file mode 100644 index 00000000..6b024348 --- /dev/null +++ b/include/boost/http/server/detail/router_base.hpp @@ -0,0 +1,112 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#ifndef BOOST_HTTP_SERVER_DETAIL_ROUTER_BASE_HPP +#define BOOST_HTTP_SERVER_DETAIL_ROUTER_BASE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost { +namespace http { + +template +class router; +class flat_router; + +namespace detail { + +// implementation for all routers +class BOOST_HTTP_DECL + router_base +{ + struct impl; + impl* impl_; + + friend class http::flat_router; + +protected: + using opt_flags = unsigned int; + + enum + { + is_invalid = 0, + is_plain = 1, + is_error = 2, + is_router = 4, + is_exception = 8 + }; + + struct BOOST_HTTP_DECL + handler + { + char const kind; + explicit handler(char kind_) noexcept : kind(kind_) {} + virtual ~handler() = default; + virtual auto invoke(route_params_base&) const -> + capy::task = 0; + + // Returns the nested router if this handler wraps one, nullptr otherwise. + // Used by flat_router::flatten() to recurse into nested routers. + virtual router_base* get_router() noexcept { return nullptr; } + }; + + using handler_ptr = std::unique_ptr; + + struct handlers + { + std::size_t n; + handler_ptr* p; + }; + +protected: + using match_result = route_params_base::match_result; + struct matcher; + struct entry; + struct layer; + + ~router_base(); + router_base(opt_flags); + router_base(router_base&&) noexcept; + router_base& operator=(router_base&&) noexcept; + layer& new_layer(std::string_view pattern); + std::size_t new_layer_idx(std::string_view pattern); + layer& get_layer(std::size_t idx); + void add_impl(std::string_view, handlers); + void add_impl(layer&, http::method, handlers); + void add_impl(layer&, std::string_view, handlers); + void set_nested_depth(std::size_t parent_depth); + +public: + /** Maximum nesting depth for routers. + + This limit applies to nested routers added via use(). + Exceeding this limit throws std::length_error at insertion time. + */ + static constexpr std::size_t max_path_depth = 16; +}; + +template +concept returns_route_task = std::same_as< + std::invoke_result_t, + capy::task>; + +} // detail +} // http +} // boost + +#endif diff --git a/include/boost/http/server/flat_router.hpp b/include/boost/http/server/flat_router.hpp new file mode 100644 index 00000000..4fb78429 --- /dev/null +++ b/include/boost/http/server/flat_router.hpp @@ -0,0 +1,94 @@ +// +// Copyright (c) 2026 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#ifndef BOOST_HTTP_SERVER_FLAT_ROUTER_HPP +#define BOOST_HTTP_SERVER_FLAT_ROUTER_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost { +namespace http { + +/** A flattened router optimized for dispatch performance. + + `flat_router` is constructed from a @ref router by flattening + its nested structure into contiguous arrays. This eliminates + pointer chasing during dispatch and improves cache locality. + + The dispatch algorithm uses fixed-size arrays sized by + `detail::router_base::max_path_depth`. Since this limit is + enforced when routers are nested, dispatch is guaranteed + not to overflow. +*/ +class BOOST_HTTP_DECL + flat_router +{ + struct impl; + impl* impl_; + +public: + ~flat_router(); + + flat_router( + detail::router_base&&); + + /** Dispatch a request using a known HTTP method. + + @param verb The HTTP method to match. Must not be + @ref http::method::unknown. + + @param url The full request target used for route matching. + + @param p The params to pass to handlers. + + @return A task yielding the @ref route_result describing + how routing completed. + + @throws std::invalid_argument If @p verb is + @ref http::method::unknown. + */ + capy::task + dispatch( + http::method verb, + urls::url_view const& url, + route_params_base& p) const; + + /** Dispatch a request using a method string. + + @param verb The HTTP method string to match. Must not be empty. + + @param url The full request target used for route matching. + + @param p The params to pass to handlers. + + @return A task yielding the @ref route_result describing + how routing completed. + + @throws std::invalid_argument If @p verb is empty. + */ + capy::task + dispatch( + std::string_view verb, + urls::url_view const& url, + route_params_base& p) const; +}; + +} // http +} // boost + +#endif diff --git a/include/boost/http/server/router.hpp b/include/boost/http/server/router.hpp index 5fd9fe23..47800dac 100644 --- a/include/boost/http/server/router.hpp +++ b/include/boost/http/server/router.hpp @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) @@ -11,25 +11,850 @@ #define BOOST_HTTP_SERVER_ROUTER_HPP #include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include namespace boost { namespace http { -struct route_params; +template class router; -/** A router for HTTP servers +/** Configuration options for HTTP routers. +*/ +struct router_options +{ + /** Constructor. + + Routers constructed with default options inherit the values of + @ref case_sensitive and @ref strict from the parent router. + If there is no parent, both default to `false`. + The value of @ref merge_params always defaults to `false` + and is never inherited. + */ + router_options() = default; + + /** Set whether to merge parameters from parent routers. + + This setting controls whether route parameters defined on parent + routers are made available in nested routers. It is not inherited + and always defaults to `false`. + + @par Example + @code + router r( router_options() + .merge_params( true ) + .case_sensitive( true ) + .strict( false ) ); + @endcode + + @param value `true` to merge parameters from parent routers. + + @return A reference to `*this` for chaining. + */ + router_options& + merge_params( + bool value) noexcept + { + v_ = (v_ & ~1) | (value ? 1 : 0); + return *this; + } + + /** Set whether pattern matching is case-sensitive. + + When this option is not set explicitly, the value is inherited + from the parent router or defaults to `false` if there is no parent. + + @par Example + @code + router r( router_options() + .case_sensitive( true ) + .strict( true ) ); + @endcode + + @param value `true` to perform case-sensitive path matching. + + @return A reference to `*this` for chaining. + */ + router_options& + case_sensitive( + bool value) noexcept + { + if(value) + v_ = (v_ & ~6) | 2; + else + v_ = (v_ & ~6) | 4; + return *this; + } + + /** Set whether pattern matching is strict. + + When this option is not set explicitly, the value is inherited + from the parent router or defaults to `false` if there is no parent. + Strict matching treats a trailing slash as significant: + the pattern `"/api"` matches `"/api"` but not `"/api/"`. + When strict matching is disabled, these paths are treated + as equivalent. + + @par Example + @code + router r( router_options() + .strict( true ) + .case_sensitive( false ) ); + @endcode + + @param value `true` to enable strict path matching. + + @return A reference to `*this` for chaining. + */ + router_options& + strict( + bool value) noexcept + { + if(value) + v_ = (v_ & ~24) | 8; + else + v_ = (v_ & ~24) | 16; + return *this; + } + +private: + template friend class router; + unsigned int v_ = 0; +}; + +//----------------------------------------------- + +/** A container for HTTP route handlers. + + `router` objects store and dispatch route handlers based on the + HTTP method and path of an incoming request. Routes are added with a + path pattern, method, and an associated handler, and the router is then + used to dispatch the appropriate handler. + + Patterns used to create route definitions have percent-decoding applied + when handlers are mounted. A literal "%2F" in the pattern string is + indistinguishable from a literal '/'. For example, "/x%2Fz" is the + same as "/x/z" when used as a pattern. + + @par Example + @code + using router_type = router; + router_type router; + router.get( "/hello", + []( route_params& p ) + { + p.res.status( status::ok ); + p.res.set_body( "Hello, world!" ); + return route::send; + } ); + @endcode + + Router objects are lightweight, shared references to their contents. + Copies of a router obtained through construction, conversion, or + assignment do not create new instances; they all refer to the same + underlying data. + + @par Handlers + + Regular handlers are invoked for matching routes and have this + equivalent signature: + @code + route_result handler( Params& p ) + @endcode + + The return value is a @ref route_result used to indicate the desired + action through @ref route enum values, or to indicate that a failure + occurred. Failures are represented by error codes for which + `system::error_code::failed()` returns `true`. + + When a failing error code is produced and remains unhandled, the + router enters error-dispatching mode. In this mode, only error + handlers are invoked. Error handlers are registered globally or + for specific paths and execute in the order of registration whenever + a failing error code is present in the response. + + Error handlers have this equivalent signature: + @code + route_result error_handler( Params& p, system::error_code ec ) + @endcode + + Each error handler may return any failing @ref system::error_code, + which is equivalent to calling: + @code + p.next( ec ); // with ec.failed() == true + @endcode + + Returning @ref route::next indicates that control should proceed to + the next matching error handler. Returning a different failing code + replaces the current error and continues dispatch in error mode using + that new code. Error handlers are invoked until one returns a result + other than @ref route::next. + + Exception handlers have this equivalent signature: + @code + route_result exception_handler( Params& p, E ex ) + @endcode + + Where `E` is the type of exception caught. Handlers installed for an + exception of type `E` will also be called when the exception type is + a derived class of `E`. Exception handlers are invoked in the order + of registration whenever an exception is present in the request. + + The prefix match is not strict: middleware attached to `"/api"` + will also match `"/api/users"` and `"/api/data"`. When registered + before route handlers for the same prefix, middleware runs before + those routes. This is analogous to `app.use( path, ... )` in + Express.js. + + @par Thread Safety - This is a specialization of `basic_router` using - `route_params` as the handler parameter type. + Member functions marked `const` such as @ref dispatch and @ref resume + may be called concurrently on routers that refer to the same data. + Modification of routers through calls to non-`const` member functions + is not thread-safe and must not be performed concurrently with any + other member function. - @see - @ref basic_router, - @ref route_params + @par Nesting Depth + + Routers may be nested to a maximum depth of `max_path_depth` (16 levels). + Exceeding this limit throws `std::length_error` when the nested router + is added via @ref use. This limit ensures that @ref flat_router dispatch + never overflows its fixed-size tracking arrays. + + @par Constraints + + `Params` must be publicly derived from @ref route_params_base. + + @tparam Params The type of the parameters object passed to handlers. */ -using router = basic_router; +template +class router : public detail::router_base +{ + static_assert(std::derived_from); + + template + static inline constexpr char handler_kind = + []() -> char + { + if constexpr (detail::returns_route_task) + { + return is_plain; + } + else if constexpr (detail::returns_route_task< + T, P&, system::error_code>) + { + return is_error; + } + else if constexpr( + std::is_base_of_v && + std::is_convertible_v && + std::is_constructible_v>) + { + return is_router; + } + else if constexpr (detail::returns_route_task< + T, P&, std::exception_ptr>) + { + return is_exception; + } + else + { + return is_invalid; + } + }(); + + template + static inline constexpr bool handler_crvals = + ((!std::is_lvalue_reference_v || + std::is_const_v> || + std::is_function_v>) && ...); + + template + static inline constexpr bool handler_check = + (((handler_kind & Mask) != 0) && ...); + + template + struct handler_impl : handler + { + std::decay_t h; + + template + explicit handler_impl(H_ h_) + : handler(handler_kind) + , h(std::forward(h_)) + { + } + + auto invoke(route_params_base& rp) const -> + capy::task override + { + if constexpr (detail::returns_route_task) + { + return h(static_cast(rp)); + } + else if constexpr (detail::returns_route_task< + H, P&, system::error_code>) + { + return h(static_cast(rp), rp.ec_); + } + else if constexpr (detail::returns_route_task< + H, P&, std::exception_ptr>) + { + return h(static_cast(rp), rp.ep_); + } + else + { + // impossible with flat router + std::terminate(); + } + } + + detail::router_base* + get_router() noexcept override + { + if constexpr (std::is_base_of_v< + detail::router_base, std::decay_t>) + return &h; + else + return nullptr; + } + }; + + template + static handler_ptr make_handler(H&& h) + { + return std::make_unique>(std::forward(h)); + } + + template + struct handlers_impl : handlers + { + handler_ptr v[N]; + + template + explicit handlers_impl(HN&&... hn) + { + p = v; + n = sizeof...(HN); + assign<0>(std::forward(hn)...); + } + + private: + template + void assign(H1&& h1, HN&&... hn) + { + v[I] = make_handler(std::forward

(h1)); + assign(std::forward(hn)...); + } + + template + void assign(int = 0) + { + } + }; + + template + static auto make_handlers(HN&&... hn) + { + return handlers_impl( + std::forward(hn)...); + } + +public: + /** The type of params used in handlers. + */ + using params_type = P; + + /** A fluent interface for defining handlers on a specific route. + + This type represents a single route within the router and + provides a chainable API for registering handlers associated + with particular HTTP methods or for all methods collectively. + + Typical usage registers one or more handlers for a route: + @code + router.route( "/users/:id" ) + .get( show_user ) + .put( update_user ) + .all( log_access ); + @endcode + + Each call appends handlers in registration order. + */ + class fluent_route; + + router(router const&) = delete; + router& operator=(router const&) = delete; + + /** Constructor. + + Creates an empty router with the specified configuration. + Routers constructed with default options inherit the values + of @ref router_options::case_sensitive and + @ref router_options::strict from the parent router, or default + to `false` if there is no parent. The value of + @ref router_options::merge_params defaults to `false` and + is never inherited. + + @param options The configuration options to use. + */ + explicit + router( + router_options options = {}) + : router_base(options.v_) + { + } + + /** Construct a router from another router with compatible types. + + This constructs a router that shares the same underlying routing + state as another router whose params type is a base class of `Params`. + + The resulting router participates in shared ownership of the + implementation; copying the router does not duplicate routes or + handlers, and changes visible through one router are visible + through all routers that share the same underlying state. + + @par Constraints + + `Params` must be derived from `OtherParams`. + + @param other The router to construct from. + + @tparam OtherParams The params type of the source router. + */ + template + requires std::derived_from + router( + router&& other) noexcept + : router_base(std::move(other)) + { + } + + /** Add middleware handlers for a path prefix. + + Each handler registered with this function participates in the + routing and error-dispatch process for requests whose path begins + with the specified prefix, as described in the @ref router + class documentation. Handlers execute in the order they are added + and may return @ref route::next to transfer control to the + subsequent handler in the chain. + + @par Example + @code + router.use( "/api", + []( route_params& p ) + { + if( ! authenticate( p ) ) + { + p.res.status( 401 ); + p.res.set_body( "Unauthorized" ); + return route::send; + } + return route::next; + }, + []( route_params& p ) + { + p.res.set_header( "X-Powered-By", "MyServer" ); + return route::next; + } ); + @endcode + + @par Preconditions + + @p pattern must be a valid path prefix; it may be empty to + indicate the root scope. + + @param pattern The pattern to match. + + @param h1 The first handler to add. + + @param hn Additional handlers to add, invoked after @p h1 in + registration order. + */ + template + void use( + std::string_view pattern, + H1&& h1, HN&&... hn) + { + static_assert(handler_crvals, + "pass handlers by value or std::move()"); + static_assert(handler_check<7, H1, HN...>, + "cannot use exception handlers here"); + add_impl(pattern, make_handlers( + std::forward

(h1), std::forward(hn)...)); + } + + /** Add global middleware handlers. + + Each handler registered with this function participates in the + routing and error-dispatch process as described in the + @ref router class documentation. Handlers execute in the + order they are added and may return @ref route::next to transfer + control to the next handler in the chain. + + This is equivalent to writing: + @code + use( "/", h1, hn... ); + @endcode + + @par Example + @code + router.use( + []( Params& p ) + { + p.res.erase( "X-Powered-By" ); + return route::next; + } ); + @endcode + + @par Constraints + + @p h1 must not be convertible to @ref std::string_view. + + @param h1 The first handler to add. + + @param hn Additional handlers to add, invoked after @p h1 in + registration order. + */ + template + void use(H1&& h1, HN&&... hn) + requires (!std::convertible_to) + { + static_assert(handler_crvals, + "pass handlers by value or std::move()"); + static_assert(handler_check<7, H1, HN...>, + "cannot use exception handlers here"); + use(std::string_view(), + std::forward

(h1), std::forward(hn)...); + } + + /** Add exception handlers for a route pattern. + + Registers one or more exception handlers that will be invoked + when an exception is thrown during request processing for routes + matching the specified pattern. + + Handlers are invoked in the order provided until one handles + the exception. + + @par Example + @code + app.except( "/api*", + []( route_params& p, std::exception const& ex ) + { + p.res.set_status( 500 ); + return route::send; + } ); + @endcode + + @param pattern The route pattern to match, or empty to match + all routes. + + @param h1 The first exception handler. + + @param hn Additional exception handlers. + */ + template + void except( + std::string_view pattern, + H1&& h1, HN&&... hn) + { + static_assert(handler_crvals, + "pass handlers by value or std::move()"); + static_assert(handler_check<8, H1, HN...>, + "only exception handlers are allowed here"); + add_impl(pattern, make_handlers( + std::forward

(h1), std::forward(hn)...)); + } + + /** Add global exception handlers. + + Registers one or more exception handlers that will be invoked + when an exception is thrown during request processing for any + route. + + Equivalent to calling `except( "", h1, hn... )`. + + @par Example + @code + app.except( + []( route_params& p, std::exception const& ex ) + { + p.res.set_status( 500 ); + return route::send; + } ); + @endcode + + @param h1 The first exception handler. + + @param hn Additional exception handlers. + */ + template + void except(H1&& h1, HN&&... hn) + requires (!std::convertible_to) + { + static_assert(handler_crvals, + "pass handlers by value or std::move()"); + static_assert(handler_check<8, H1, HN...>, + "only exception handlers are allowed here"); + except(std::string_view(), + std::forward

(h1), std::forward(hn)...); + } + + /** Add handlers for all HTTP methods matching a path pattern. + + This registers regular handlers for the specified path pattern, + participating in dispatch as described in the @ref router + class documentation. Handlers run when the route matches, + regardless of HTTP method, and execute in registration order. + Error handlers and routers cannot be passed here. A new route + object is created even if the pattern already exists. + + @par Example + @code + router.route( "/status" ) + .add( method::head, check_headers ) + .add( method::get, send_status ) + .all( log_access ); + @endcode + + @par Preconditions + + @p pattern must be a valid path pattern; it must not be empty. + + @param pattern The path pattern to match. + + @param h1 The first handler to add. + + @param hn Additional handlers to add, invoked after @p h1 in + registration order. + */ + template + void all( + std::string_view pattern, + H1&& h1, HN&&... hn) + { + static_assert(handler_crvals, + "pass handlers by value or std::move()"); + static_assert(handler_check<1, H1, HN...>, + "only normal route handlers are allowed here"); + this->route(pattern).all( + std::forward

(h1), std::forward(hn)...); + } + + /** Add route handlers for a method and pattern. + + This registers regular handlers for the specified HTTP verb and + path pattern, participating in dispatch as described in the + @ref router class documentation. Error handlers and + routers cannot be passed here. + + @param verb The known HTTP method to match. + + @param pattern The path pattern to match. + + @param h1 The first handler to add. + + @param hn Additional handlers to add, invoked after @p h1 in + registration order. + */ + template + void add( + http::method verb, + std::string_view pattern, + H1&& h1, HN&&... hn) + { + static_assert(handler_crvals, + "pass handlers by value or std::move()"); + static_assert(handler_check<1, H1, HN...>, + "only normal route handlers are allowed here"); + this->route(pattern).add(verb, + std::forward

(h1), std::forward(hn)...); + } + + /** Add route handlers for a method string and pattern. + + This registers regular handlers for the specified HTTP verb and + path pattern, participating in dispatch as described in the + @ref router class documentation. Error handlers and + routers cannot be passed here. + + @param verb The HTTP method string to match. + + @param pattern The path pattern to match. + + @param h1 The first handler to add. + + @param hn Additional handlers to add, invoked after @p h1 in + registration order. + */ + template + void add( + std::string_view verb, + std::string_view pattern, + H1&& h1, HN&&... hn) + { + static_assert(handler_crvals, + "pass handlers by value or std::move()"); + static_assert(handler_check<1, H1, HN...>, + "only normal route handlers are allowed here"); + this->route(pattern).add(verb, + std::forward

(h1), std::forward(hn)...); + } + + /** Return a fluent route for the specified path pattern. + + Adds a new route to the router for the given pattern. + A new route object is always created, even if another + route with the same pattern already exists. The returned + @ref fluent_route reference allows method-specific handler + registration (such as GET or POST) or catch-all handlers + with @ref fluent_route::all. + + @param pattern The path expression to match against request + targets. This may include parameters or wildcards following + the router's pattern syntax. May not be empty. + + @return A fluent route interface for chaining handler + registrations. + */ + auto + route( + std::string_view pattern) -> fluent_route + { + return fluent_route(*this, pattern); + } +}; + +template +class router

:: + fluent_route +{ +public: + fluent_route(fluent_route const&) = default; + + /** Add handlers that apply to all HTTP methods. + + This registers regular handlers that run for any request matching + the route's pattern, regardless of HTTP method. Handlers are + appended to the route's handler sequence and are invoked in + registration order whenever a preceding handler returns + @ref route::next. Error handlers and routers cannot be passed here. + + This function returns a @ref fluent_route, allowing additional + method registrations to be chained. For example: + @code + router.route( "/resource" ) + .all( log_request ) + .add( method::get, show_resource ) + .add( method::post, update_resource ); + @endcode + + @param h1 The first handler to add. + + @param hn Additional handlers to add, invoked after @p h1 in + registration order. + + @return A reference to `*this` for chained registrations. + */ + template + auto all( + H1&& h1, HN&&... hn) -> + fluent_route + { + static_assert(handler_check<1, H1, HN...>); + owner_.add_impl(owner_.get_layer(layer_idx_), std::string_view{}, + owner_.make_handlers( + std::forward

(h1), std::forward(hn)...)); + return *this; + } + + /** Add handlers for a specific HTTP method. + + This registers regular handlers for the given method on the + current route, participating in dispatch as described in the + @ref router class documentation. Handlers are appended + to the route's handler sequence and invoked in registration + order whenever a preceding handler returns @ref route::next. + Error handlers and routers cannot be passed here. + + @param verb The HTTP method to match. + + @param h1 The first handler to add. + + @param hn Additional handlers to add, invoked after @p h1 in + registration order. + + @return A reference to `*this` for chained registrations. + */ + template + auto add( + http::method verb, + H1&& h1, HN&&... hn) -> + fluent_route + { + static_assert(handler_check<1, H1, HN...>); + owner_.add_impl(owner_.get_layer(layer_idx_), verb, owner_.make_handlers( + std::forward

(h1), std::forward(hn)...)); + return *this; + } + + /** Add handlers for a method string. + + This registers regular handlers for the given HTTP method string + on the current route, participating in dispatch as described in + the @ref router class documentation. This overload is + intended for methods not represented by @ref http::method. + Handlers are appended to the route's handler sequence and invoked + in registration order whenever a preceding handler returns + @ref route::next. Error handlers and routers cannot be passed here. + + @param verb The HTTP method string to match. + + @param h1 The first handler to add. + + @param hn Additional handlers to add, invoked after @p h1 in + registration order. + + @return A reference to `*this` for chained registrations. + */ + template + auto add( + std::string_view verb, + H1&& h1, HN&&... hn) -> + fluent_route + { + static_assert(handler_check<1, H1, HN...>); + owner_.add_impl(owner_.get_layer(layer_idx_), verb, owner_.make_handlers( + std::forward

(h1), std::forward(hn)...)); + return *this; + } + +private: + friend class router; + fluent_route( + router& owner, + std::string_view pattern) + : layer_idx_(owner.new_layer_idx(pattern)) + , owner_(owner) + { + } + + std::size_t layer_idx_; + router& owner_; +}; } // http } // boost -#endif \ No newline at end of file +#endif diff --git a/include/boost/http/server/router_types.hpp b/include/boost/http/server/router_types.hpp index d60fc780..5b3f8b1b 100644 --- a/include/boost/http/server/router_types.hpp +++ b/include/boost/http/server/router_types.hpp @@ -63,7 +63,7 @@ enum class route When the handler returns this value, the router is placed into a suspended state which can later be reactivated by invoking - @ref basic_router::resume. Depending on the implementation, + @ref router::resume. Depending on the implementation, this might detach the handler from the session until it is resumed. */ @@ -76,7 +76,7 @@ enum class route until one returns @ref send. If none do, the caller proceeds to evaluate the next matching route. - This value is returned by @ref basic_router::dispatch if no + This value is returned by @ref router::dispatch if no handlers in any route handle the request. */ next, @@ -307,17 +307,34 @@ operator()(F&& f) -> //------------------------------------------------ namespace detail { -class any_router; +class router_base; } // detail -template -class basic_router; +template class router; + +struct route_params_base_privates +{ + struct match_result; + + std::string verb_str_; + std::string decoded_path_; + system::error_code ec_; + std::exception_ptr ep_; + std::size_t pos_ = 0; + std::size_t resume_ = 0; + http::method verb_ = + http::method::unknown; + bool addedSlash_ = false; + bool case_sensitive = false; + bool strict = false; + char kind_ = 0; // dispatch mode, initialized by flat_router::dispatch() +}; /** Base class for request objects This is a required public base for any `Request` - type used with @ref basic_router. + type used with @ref router. */ -class route_params_base +class route_params_base : public route_params_base_privates { public: /** Return true if the request method matches `m` @@ -347,27 +364,82 @@ class route_params_base */ core::string_view path; + struct match_result; + private: - friend class /*detail::*/any_router; template - friend class basic_router; - struct match_result; + friend class router; + friend struct route_params_access; + route_params_base& operator=( route_params_base const&) = delete; +}; - std::string verb_str_; - std::string decoded_path_; - system::error_code ec_; - std::exception_ptr ep_; - std::size_t pos_ = 0; - std::size_t resume_ = 0; - http::method verb_ = - http::method::unknown; - bool addedSlash_ = false; - bool case_sensitive = false; - bool strict = false; +struct route_params_base:: + match_result +{ + void adjust_path( + route_params_base& p, + std::size_t n) + { + n_ = n; + if(n_ == 0) + return; + p.base_path = { + p.base_path.data(), + p.base_path.size() + n_ }; + if(n_ < p.path.size()) + { + p.path.remove_prefix(n_); + } + else + { + // append a soft slash + p.path = { p.decoded_path_.data() + + p.decoded_path_.size() - 1, 1}; + BOOST_ASSERT(p.path == "/"); + } + } + + void restore_path( + route_params_base& p) + { + if( n_ > 0 && + p.addedSlash_ && + p.path.data() == + p.decoded_path_.data() + + p.decoded_path_.size() - 1) + { + // remove soft slash + p.path = { + p.base_path.data() + + p.base_path.size(), 0 }; + } + p.base_path.remove_suffix(n_); + p.path = { + p.path.data() - n_, + p.path.size() + n_ }; + } + +private: + std::size_t n_ = 0; // chars moved from path to base_path +}; + + +namespace detail { + +struct route_params_access +{ + route_params_base& rp; + + route_params_base_privates* operator->() const noexcept + { + return &rp; + } }; +} // detail + } // http } // boost diff --git a/src/server/basic_router.cpp b/src/server/basic_router.cpp deleted file mode 100644 index 9b1b051d..00000000 --- a/src/server/basic_router.cpp +++ /dev/null @@ -1,903 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/http -// - -#include "src/server/route_rule.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace boost { -namespace http { - -//namespace detail { - -// VFALCO Temporarily here until Boost.URL merges the fix -static -bool -ci_is_equal( - core::string_view s0, - core::string_view s1) noexcept -{ - auto n = s0.size(); - if(s1.size() != n) - return false; - auto p1 = s0.data(); - auto p2 = s1.data(); - char a, b; - // fast loop - while(n--) - { - a = *p1++; - b = *p2++; - if(a != b) - goto slow; - } - return true; - do - { - a = *p1++; - b = *p2++; - slow: - if( grammar::to_lower(a) != - grammar::to_lower(b)) - return false; - } - while(n--); - return true; -} - - -//------------------------------------------------ -/* - -pattern target path(use) path(get) -------------------------------------------------- -/ / / -/ /api /api -/api /api / /api -/api /api/ / /api/ -/api /api/ / no-match strict -/api /api/v0 /v0 no-match -/api/ /api / /api -/api/ /api / no-match strict -/api/ /api/ / /api/ -/api/ /api/v0 /v0 no-match - -*/ - -//------------------------------------------------ - -/* -static -void -make_lower(std::string& s) -{ - for(auto& c : s) - c = grammar::to_lower(c); -} -*/ - -// decode all percent escapes -static -std::string -pct_decode( - urls::pct_string_view s) -{ - std::string result; - core::string_view sv(s); - result.reserve(s.size()); - auto it = sv.data(); - auto const end = it + sv.size(); - for(;;) - { - if(it == end) - break; - if(*it != '%') - { - result.push_back(*it++); - continue; - } - ++it; -#if 0 - // pct_string_view can never have invalid pct-encodings - if(it == end) - goto invalid; -#endif - auto d0 = urls::grammar::hexdig_value(*it++); -#if 0 - // pct_string_view can never have invalid pct-encodings - if( d0 < 0 || - it == end) - goto invalid; -#endif - auto d1 = urls::grammar::hexdig_value(*it++); -#if 0 - // pct_string_view can never have invalid pct-encodings - if(d1 < 0) - goto invalid; -#endif - result.push_back(d0 * 16 + d1); - } - return result; -#if 0 -invalid: - // can't get here, as received a pct_string_view - detail::throw_invalid_argument(); -#endif -} - -// decode all percent escapes except slashes '/' and '\' -static -std::string -pct_decode_path( - urls::pct_string_view s) -{ - std::string result; - core::string_view sv(s); - result.reserve(s.size()); - auto it = sv.data(); - auto const end = it + sv.size(); - for(;;) - { - if(it == end) - break; - if(*it != '%') - { - result.push_back(*it++); - continue; - } - ++it; -#if 0 - // pct_string_view can never have invalid pct-encodings - if(it == end) - goto invalid; -#endif - auto d0 = urls::grammar::hexdig_value(*it++); -#if 0 - // pct_string_view can never have invalid pct-encodings - if( d0 < 0 || - it == end) - goto invalid; -#endif - auto d1 = urls::grammar::hexdig_value(*it++); -#if 0 - // pct_string_view can never have invalid pct-encodings - if(d1 < 0) - goto invalid; -#endif - char c = d0 * 16 + d1; - if( c != '/' && - c != '\\') - { - result.push_back(c); - continue; - } - result.append(it - 3, 3); - } - return result; -#if 0 -invalid: - // can't get here, as received a pct_string_view - detail::throw_invalid_argument(); -#endif -} - -//------------------------------------------------ - -//} // detail - -struct route_params_base:: - match_result -{ - void adjust_path( - route_params_base& p, - std::size_t n) - { - n_ = n; - if(n_ == 0) - return; - p.base_path = { - p.base_path.data(), - p.base_path.size() + n_ }; - if(n_ < p.path.size()) - { - p.path.remove_prefix(n_); - } - else - { - // append a soft slash - p.path = { p.decoded_path_.data() + - p.decoded_path_.size() - 1, 1}; - BOOST_ASSERT(p.path == "/"); - } - } - - void restore_path( - route_params_base& p) - { - if( n_ > 0 && - p.addedSlash_ && - p.path.data() == - p.decoded_path_.data() + - p.decoded_path_.size() - 1) - { - // remove soft slash - p.path = { - p.base_path.data() + - p.base_path.size(), 0 }; - } - p.base_path.remove_suffix(n_); - p.path = { - p.path.data() - n_, - p.path.size() + n_ }; - } - -private: - std::size_t n_ = 0; // chars moved from path to base_path -}; - -//------------------------------------------------ - -//namespace detail { - -// Matches a path against a pattern -struct any_router::matcher -{ - bool const end; // false for middleware - - matcher( - core::string_view pat, - bool end_) - : end(end_) - , decoded_pat_( - [&pat] - { - auto s = pct_decode(pat); - if( s.size() > 1 - && s.back() == '/') - s.pop_back(); - return s; - }()) - , slash_(pat == "/") - { - if(! slash_) - pv_ = grammar::parse( - decoded_pat_, path_rule).value(); - } - - /** Return true if p.path is a match - */ - bool operator()( - route_params_base& p, - match_result& mr) const - { - BOOST_ASSERT(! p.path.empty()); - if( slash_ && ( - ! end || - p.path == "/")) - { - // params = {}; - mr.adjust_path(p, 0); - return true; - } - auto it = p.path.data(); - auto pit = pv_.segs.begin(); - auto const end_ = it + p.path.size(); - auto const pend = pv_.segs.end(); - while(it != end_ && pit != pend) - { - // prefix has to match - auto s = core::string_view(it, end_); - if(! p.case_sensitive) - { - if(pit->prefix.size() > s.size()) - return false; - s = s.substr(0, pit->prefix.size()); - //if(! grammar::ci_is_equal(s, pit->prefix)) - if(! ci_is_equal(s, pit->prefix)) - return false; - } - else - { - if(! s.starts_with(pit->prefix)) - return false; - } - it += pit->prefix.size(); - ++pit; - } - if(end) - { - // require full match - if( it != end_ || - pit != pend) - return false; - } - else if(pit != pend) - { - return false; - } - // number of matching characters - auto const n = it - p.path.data(); - mr.adjust_path(p, n); - return true; - } - -private: - stable_string decoded_pat_; - path_rule_t::value_type pv_; - bool slash_; -}; - -//------------------------------------------------ - -struct any_router::layer -{ - struct entry - { - handler_ptr handler; - - // only for end routes - http::method verb = - http::method::unknown; - std::string verb_str; - bool all; - - explicit entry( - handler_ptr h) noexcept - : handler(std::move(h)) - , all(true) - { - } - - entry( - http::method verb_, - handler_ptr h) noexcept - : handler(std::move(h)) - , verb(verb_) - , all(false) - { - BOOST_ASSERT(verb != - http::method::unknown); - } - - entry( - core::string_view verb_str_, - handler_ptr h) noexcept - : handler(std::move(h)) - , verb(http::string_to_method(verb_str_)) - , all(false) - { - if(verb != http::method::unknown) - return; - verb_str = verb_str_; - } - - bool match_method( - route_params_base const& p) const noexcept - { - if(all) - return true; - if(verb != http::method::unknown) - return p.verb_ == verb; - if(p.verb_ != http::method::unknown) - return false; - return p.verb_str_ == verb_str; - } - }; - - matcher match; - std::vector entries; - - // middleware layer - layer( - core::string_view pat, - handler_list handlers) - : match(pat, false) - { - entries.reserve(handlers.n); - for(std::size_t i = 0; i < handlers.n; ++i) - entries.emplace_back(std::move(handlers.p[i])); - } - - // route layer - explicit layer( - core::string_view pat) - : match(pat, true) - { - } - - std::size_t count() const noexcept - { - std::size_t n = 0; - for(auto const& e : entries) - n += e.handler->count(); - return n; - } -}; - -//------------------------------------------------ - -struct any_router::impl -{ - std::atomic refs{1}; - std::vector layers; - opt_flags opt; - - explicit impl( - opt_flags opt_) noexcept - : opt(opt_) - { - } -}; - -//------------------------------------------------ - -any_router:: -~any_router() -{ - if(! impl_) - return; - if(--impl_->refs == 0) - delete impl_; -} - -any_router:: -any_router( - opt_flags opt) - : impl_(new impl(opt)) -{ -} - -any_router:: -any_router(any_router&& other) noexcept - :impl_(other.impl_) -{ - other.impl_ = nullptr; -} - -any_router:: -any_router(any_router const& other) noexcept -{ - impl_ = other.impl_; - ++impl_->refs; -} - -any_router& -any_router:: -operator=(any_router&& other) noexcept -{ - auto p = impl_; - impl_ = other.impl_; - other.impl_ = nullptr; - if(p && --p->refs == 0) - delete p; - return *this; -} - -any_router& -any_router:: -operator=(any_router const& other) noexcept -{ - auto p = impl_; - impl_ = other.impl_; - ++impl_->refs; - if(p && --p->refs == 0) - delete p; - return *this; -} - -//------------------------------------------------ - -std::size_t -any_router:: -count() const noexcept -{ - std::size_t n = 0; - for(auto const& i : impl_->layers) - for(auto const& e : i.entries) - n += e.handler->count(); - return n; -} - -auto -any_router:: -new_layer( - core::string_view pattern) -> layer& -{ - // the pattern must not be empty - if(pattern.empty()) - detail::throw_invalid_argument(); - // delete the last route if it is empty, - // this happens if they call route() without - // adding anything - if(! impl_->layers.empty() && - impl_->layers.back().entries.empty()) - impl_->layers.pop_back(); - impl_->layers.emplace_back(pattern); - return impl_->layers.back(); -}; - -void -any_router:: -add_impl( - core::string_view pattern, - handler_list const& handlers) -{ - if( pattern.empty()) - pattern = "/"; - impl_->layers.emplace_back( - pattern, std::move(handlers)); -} - -void -any_router:: -add_impl( - layer& e, - http::method verb, - handler_list const& handlers) -{ - // cannot be unknown - if(verb == http::method::unknown) - detail::throw_invalid_argument(); - - e.entries.reserve(e.entries.size() + handlers.n); - for(std::size_t i = 0; i < handlers.n; ++i) - e.entries.emplace_back(verb, - std::move(handlers.p[i])); -} - -void -any_router:: -add_impl( - layer& e, - core::string_view verb_str, - handler_list const& handlers) -{ - e.entries.reserve(e.entries.size() + handlers.n); - - if(verb_str.empty()) - { - // all - for(std::size_t i = 0; i < handlers.n; ++i) - e.entries.emplace_back( - std::move(handlers.p[i])); - return; - } - - // possibly custom string - for(std::size_t i = 0; i < handlers.n; ++i) - e.entries.emplace_back(verb_str, - std::move(handlers.p[i])); -} - -//------------------------------------------------ - -auto -any_router:: -resume_impl( - route_params_base& p, - route_result ec) const -> - route_result -{ - BOOST_ASSERT(p.resume_ > 0); - if( ec == route::send || - ec == route::close || - ec == route::complete) - return ec; - if(! is_route_result(ec)) - { - // must indicate failure - if(! ec.failed()) - detail::throw_invalid_argument(); - } - - // restore base_path and path - p.base_path = { p.decoded_path_.data(), 0 }; - p.path = p.decoded_path_; - if(p.addedSlash_) - p.path.remove_suffix(1); - - // resume_ was set in the handler's wrapper - BOOST_ASSERT(p.resume_ == p.pos_); - p.pos_ = 0; - p.ec_ = ec; - return do_dispatch(p); -} - -// top-level dispatch that gets called first -route_result -any_router:: -dispatch_impl( - http::method verb, - core::string_view verb_str, - urls::url_view const& url, - route_params_base& p) const -{ - p.verb_str_.clear(); - p.decoded_path_.clear(); - p.ec_.clear(); - p.ep_ = nullptr; - p.pos_ = 0; - p.resume_ = 0; - p.addedSlash_ = false; - p.case_sensitive = false; - p.strict = false; - - if(verb == http::method::unknown) - { - BOOST_ASSERT(! verb_str.empty()); - verb = http::string_to_method(verb_str); - if(verb == http::method::unknown) - p.verb_str_ = verb_str; - } - else - { - BOOST_ASSERT(verb_str.empty()); - } - p.verb_ = verb; - - // VFALCO use reusing-StringToken - p.decoded_path_ = - pct_decode_path(url.encoded_path()); - BOOST_ASSERT(! p.decoded_path_.empty()); - p.base_path = { p.decoded_path_.data(), 0 }; - p.path = p.decoded_path_; - if(p.decoded_path_.back() != '/') - { - p.decoded_path_.push_back('/'); - p.addedSlash_ = true; - } - - // we cannot do anything after do_dispatch returns, - // other than return the route_result, or else we - // could race with the suspended operation trying to resume. - auto rv = do_dispatch(p); - if(rv == route::suspend) - return rv; - if(p.ep_) - { - p.ep_ = nullptr; - return error::unhandled_exception; - } - if( p.ec_.failed()) - p.ec_ = {}; - return rv; -} - -// recursive dispatch -route_result -any_router:: -dispatch_impl( - route_params_base& p) const -{ - // options are recursive and need to be restored on - // exception or when returning to a calling router. - struct option_saver - { - option_saver( - route_params_base& p) noexcept - : p_(&p) - , case_sensitive_(p.case_sensitive) - , strict_(p.strict) - { - } - - ~option_saver() - { - if(! p_) - return; - p_->case_sensitive = case_sensitive_; - p_->strict = strict_; - }; - - void cancel() noexcept - { - p_ = nullptr; - } - - private: - route_params_base* p_; - bool case_sensitive_; - bool strict_; - }; - - option_saver restore_options(p); - - // inherit or apply options - if((impl_->opt & 2) != 0) - p.case_sensitive = true; - else if((impl_->opt & 4) != 0) - p.case_sensitive = false; - - if((impl_->opt & 8) != 0) - p.strict = true; - else if((impl_->opt & 16) != 0) - p.strict = false; - - match_result mr; - for(auto const& i : impl_->layers) - { - if(p.resume_ > 0) - { - auto const n = i.count(); // handlers in layer - if(p.pos_ + n < p.resume_) - { - p.pos_ += n; // skip layer - continue; - } - // repeat match to recreate the stack - bool is_match = i.match(p, mr); - BOOST_ASSERT(is_match); - (void)is_match; - } - else - { - if(i.match.end && p.ec_.failed()) - { - // routes can't have error handlers - p.pos_ += i.count(); // skip layer - continue; - } - if(! i.match(p, mr)) - { - // not a match - p.pos_ += i.count(); // skip layer - continue; - } - } - for(auto it = i.entries.begin(); - it != i.entries.end(); ++it) - { - auto const& e(*it); - if(p.resume_) - { - auto const n = e.handler->count(); - if(p.pos_ + n < p.resume_) - { - p.pos_ += n; // skip entry - continue; - } - BOOST_ASSERT(e.match_method(p)); - } - else if(i.match.end) - { - // check verb for match - if(! e.match_method(p)) - { - p.pos_ += e.handler->count(); // skip entry - continue; - } - } - - route_result rv; - // increment before invoke - ++p.pos_; - if(p.pos_ != p.resume_) - { - // call the handler - #ifdef BOOST_NO_EXCEPTIONS - rv = e.handler->invoke(p); - #else - try - { - rv = e.handler->invoke(p); - if(p.ec_.failed()) - p.ep_ = {}; // transition to error mode - } - catch(...) - { - if(p.ec_.failed()) - p.ec_ = {}; // transition to except mode - p.ep_ = std::current_exception(); - rv = route::next; - } - #endif - - // p.pos_ can be incremented further - // inside the above call to invoke. - if(rv == route::suspend) - { - // It is essential that we return immediately, without - // doing anything after route::suspend is returned, - // otherwise we could race with the suspended operation - // attempting to call resume(). - restore_options.cancel(); - return rv; - } - } - else - { - // a subrouter never suspends on its own - BOOST_ASSERT(e.handler->count() == 1); - // can't suspend on resume - if(p.ec_ == route::suspend) - detail::throw_invalid_argument(); - // do resume - p.resume_ = 0; - rv = p.ec_; - p.ec_ = {}; - } - if( rv == route::send || - rv == route::complete || - rv == route::close) - { - if( p.ec_.failed()) - p.ec_ = {}; - if( p.ep_) - p.ep_ = nullptr; - return rv; - } - if(rv == route::next) - continue; // next entry - if(rv == route::next_route) - { - // middleware can't return next_route - if(! i.match.end) - detail::throw_invalid_argument(); - while(++it != i.entries.end()) - p.pos_ += it->handler->count(); - break; // skip remaining entries - } - // we must handle all route enums - BOOST_ASSERT(! is_route_result(rv)); - if(! rv.failed()) - { - // handler must return non-successful error_code - detail::throw_invalid_argument(); - } - // error handling mode - p.ec_ = rv; - if(! i.match.end) - continue; // next entry - // routes don't have error handlers - while(++it != i.entries.end()) - p.pos_ += it->handler->count(); - break; // skip remaining entries - } - - mr.restore_path(p); - } - - return route::next; -} - -route_result -any_router:: -do_dispatch( - route_params_base& p) const -{ - auto rv = dispatch_impl(p); - BOOST_ASSERT(is_route_result(rv)); - BOOST_ASSERT(rv != route::next_route); - if(rv != route::next) - { - // when rv == route::suspend we must return immediately, - // without attempting to perform any additional operations. - return rv; - } - if(! p.ec_.failed()) - { - // unhandled route - return route::next; - } - // error condition - return p.ec_; -} - -//} // detail - -} // http -} // boost diff --git a/src/server/detail/pct_decode.cpp b/src/server/detail/pct_decode.cpp new file mode 100644 index 00000000..665ea3eb --- /dev/null +++ b/src/server/detail/pct_decode.cpp @@ -0,0 +1,153 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#include "src/server/detail/pct_decode.hpp" + +namespace boost { +namespace http { +namespace detail { + +bool +ci_is_equal( + core::string_view s0, + core::string_view s1) noexcept +{ + auto n = s0.size(); + if(s1.size() != n) + return false; + auto p1 = s0.data(); + auto p2 = s1.data(); + char a, b; + // fast loop + while(n--) + { + a = *p1++; + b = *p2++; + if(a != b) + goto slow; + } + return true; + do + { + a = *p1++; + b = *p2++; + slow: + if( grammar::to_lower(a) != + grammar::to_lower(b)) + return false; + } + while(n--); + return true; +} + +std::string +pct_decode( + urls::pct_string_view s) +{ + std::string result; + core::string_view sv(s); + result.reserve(s.size()); + auto it = sv.data(); + auto const end = it + sv.size(); + for(;;) + { + if(it == end) + break; + if(*it != '%') + { + result.push_back(*it++); + continue; + } + ++it; +#if 0 + // pct_string_view can never have invalid pct-encodings + if(it == end) + goto invalid; +#endif + auto d0 = urls::grammar::hexdig_value(*it++); +#if 0 + // pct_string_view can never have invalid pct-encodings + if( d0 < 0 || + it == end) + goto invalid; +#endif + auto d1 = urls::grammar::hexdig_value(*it++); +#if 0 + // pct_string_view can never have invalid pct-encodings + if(d1 < 0) + goto invalid; +#endif + result.push_back(d0 * 16 + d1); + } + return result; +#if 0 +invalid: + // can't get here, as received a pct_string_view + detail::throw_invalid_argument(); +#endif +} + +// decode all percent escapes except slashes '/' and '\' +std::string +pct_decode_path( + urls::pct_string_view s) +{ + std::string result; + core::string_view sv(s); + result.reserve(s.size()); + auto it = sv.data(); + auto const end = it + sv.size(); + for(;;) + { + if(it == end) + break; + if(*it != '%') + { + result.push_back(*it++); + continue; + } + ++it; +#if 0 + // pct_string_view can never have invalid pct-encodings + if(it == end) + goto invalid; +#endif + auto d0 = urls::grammar::hexdig_value(*it++); +#if 0 + // pct_string_view can never have invalid pct-encodings + if( d0 < 0 || + it == end) + goto invalid; +#endif + auto d1 = urls::grammar::hexdig_value(*it++); +#if 0 + // pct_string_view can never have invalid pct-encodings + if(d1 < 0) + goto invalid; +#endif + char c = d0 * 16 + d1; + if( c != '/' && + c != '\\') + { + result.push_back(c); + continue; + } + result.append(it - 3, 3); + } + return result; +#if 0 +invalid: + // can't get here, as received a pct_string_view + detail::throw_invalid_argument(); +#endif +} + +} // detail +} // http +} // boost diff --git a/src/server/detail/pct_decode.hpp b/src/server/detail/pct_decode.hpp new file mode 100644 index 00000000..c5c1c4af --- /dev/null +++ b/src/server/detail/pct_decode.hpp @@ -0,0 +1,42 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#ifndef BOOST_HTTP_SERVER_DETAIL_PCT_DECODE_HPP +#define BOOST_HTTP_SERVER_DETAIL_PCT_DECODE_HPP + +#include +#include +#include +#include +#include + +namespace boost { +namespace http { +namespace detail { + +bool +ci_is_equal( + core::string_view s0, + core::string_view s1) noexcept; + +// decode all percent escapes +std::string +pct_decode( + urls::pct_string_view s); + +// decode all percent escapes except slashes '/' and '\' +std::string +pct_decode_path( + urls::pct_string_view s); + +} // detail +} // http +} // boost + +#endif diff --git a/src/server/detail/route_match.cpp b/src/server/detail/route_match.cpp new file mode 100644 index 00000000..3f8f0a3f --- /dev/null +++ b/src/server/detail/route_match.cpp @@ -0,0 +1,99 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#include "src/server/detail/pct_decode.hpp" +#include "src/server/detail/route_match.hpp" + +namespace boost { +namespace http { +namespace detail { + +router_base:: +matcher:: +matcher( + std::string_view pat, + bool end_arg) + : decoded_pat_( + [&pat] + { + auto s = pct_decode(pat); + if( s.size() > 1 + && s.back() == '/') + s.pop_back(); + return s; + }()) + , end_(end_arg) + , slash_(pat == "/") +{ + if(! slash_) + pv_ = grammar::parse( + decoded_pat_, detail::path_rule).value(); +} + +bool +router_base:: +matcher:: +operator()( + route_params_base& p, + match_result& mr) const +{ + BOOST_ASSERT(! p.path.empty()); + if( slash_ && ( + ! end_ || + p.path == "/")) + { + // params = {}; + mr.adjust_path(p, 0); + return true; + } + auto it = p.path.data(); + auto pit = pv_.segs.begin(); + auto const path_end = it + p.path.size(); + auto const pend = pv_.segs.end(); + while(it != path_end && pit != pend) + { + // prefix has to match + auto s = core::string_view(it, path_end); + if(! p.case_sensitive) + { + if(pit->prefix.size() > s.size()) + return false; + s = s.substr(0, pit->prefix.size()); + //if(! grammar::ci_is_equal(s, pit->prefix)) + if(! ci_is_equal(s, pit->prefix)) + return false; + } + else + { + if(! s.starts_with(pit->prefix)) + return false; + } + it += pit->prefix.size(); + ++pit; + } + if(end_) + { + // require full match + if( it != path_end || + pit != pend) + return false; + } + else if(pit != pend) + { + return false; + } + // number of matching characters + auto const n = it - p.path.data(); + mr.adjust_path(p, n); + return true; +} + +} // detail +} // http +} // boost diff --git a/src/server/detail/route_match.hpp b/src/server/detail/route_match.hpp new file mode 100644 index 00000000..fc2a2e5c --- /dev/null +++ b/src/server/detail/route_match.hpp @@ -0,0 +1,59 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#ifndef BOOST_HTTP_SERVER_DETAIL_ROUTE_MATCH_HPP +#define BOOST_HTTP_SERVER_DETAIL_ROUTE_MATCH_HPP + +#include +#include +#include "src/server/detail/route_rule.hpp" +#include "src/server/detail/stable_string.hpp" + +namespace boost { +namespace http { +namespace detail { + +// Matches a path against a pattern +// Members ordered largest-to-smallest for optimal packing +struct router_base::matcher +{ + friend class http::flat_router; + + matcher(std::string_view pat, bool end_); + + // true if match + bool operator()( + route_params_base& p, + match_result& mr) const; + +private: + // 24 bytes (vector) + path_rule_t::value_type pv_; + + // 16 bytes (pointer + size) + stable_string decoded_pat_; + + // 8 bytes each + std::size_t first_entry_ = 0; // flat_router: first entry using this matcher + std::size_t skip_ = 0; // flat_router: entry index to jump to on failure + + // 4 bytes each + opt_flags effective_opts_ = 0; // flat_router: computed opts for this scope + std::uint32_t depth_ = 0; // flat_router: nesting level (0 = root) + + // 1 byte each + bool end_; // false for middleware + bool slash_; +}; + +} // detail +} // http +} // boost + +#endif diff --git a/src/server/route_rule.hpp b/src/server/detail/route_rule.hpp similarity index 83% rename from src/server/route_rule.hpp rename to src/server/detail/route_rule.hpp index d4845ecb..3b962896 100644 --- a/src/server/route_rule.hpp +++ b/src/server/detail/route_rule.hpp @@ -7,8 +7,8 @@ // Official repository: https://github.com/cppalliance/http // -#ifndef BOOST_HTTP_SERVER_ROUTE_RULE_HPP -#define BOOST_HTTP_SERVER_ROUTE_RULE_HPP +#ifndef BOOST_HTTP_SERVER_DETAIL_ROUTE_RULE_HPP +#define BOOST_HTTP_SERVER_DETAIL_ROUTE_RULE_HPP #include #include @@ -18,87 +18,14 @@ #include #include +#include "src/server/detail/stable_string.hpp" + namespace boost { namespace http { +namespace detail { namespace grammar = urls::grammar; -//------------------------------------------------ - -// avoids SBO -class stable_string -{ - char const* p_ = 0; - std::size_t n_ = 0; - -public: - ~stable_string() - { - if(p_) - delete[] p_; - } - - stable_string() = default; - - stable_string( - stable_string&& other) noexcept - : p_(other.p_) - , n_(other.n_) - { - other.p_ = nullptr; - other.n_ = 0; - } - - stable_string& operator=( - stable_string&& other) noexcept - { - auto p = p_; - auto n = n_; - p_ = other.p_; - n_ = other.n_; - other.p_ = p; - other.n_ = n; - return *this; - } - - explicit - stable_string( - core::string_view s) - : p_( - [&] - { - auto p =new char[s.size()]; - std::memcpy(p, s.data(), s.size()); - return p; - }()) - , n_(s.size()) - { - } - - stable_string( - char const* it, char const* end) - : stable_string(core::string_view(it, end)) - { - } - - char const* data() const noexcept - { - return p_; - } - - std::size_t size() const noexcept - { - return n_; - } - - operator core::string_view() const noexcept - { - return { data(), size() }; - } -}; - -//------------------------------------------------ - /** Rule for parsing a non-empty token of chars @par Requires @@ -371,6 +298,7 @@ struct route_match urls::segments_encoded_view path; }; +} // detail } // http } // boost diff --git a/src/server/detail/router_base.cpp b/src/server/detail/router_base.cpp new file mode 100644 index 00000000..724293a5 --- /dev/null +++ b/src/server/detail/router_base.cpp @@ -0,0 +1,187 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#include "src/server/detail/router_base.hpp" +#include +#include +#include +#include +#include +#include +#include "src/server/detail/route_match.hpp" +#include "src/server/detail/route_rule.hpp" + +/* + +pattern target path(use) path(get) +------------------------------------------------- +/ / / +/ /api /api +/api /api / /api +/api /api/ / /api/ +/api /api/ / no-match strict +/api /api/v0 /v0 no-match +/api/ /api / /api +/api/ /api / no-match strict +/api/ /api/ / /api/ +/api/ /api/v0 /v0 no-match + +*/ + +namespace boost { +namespace http { +namespace detail { + +router_base:: +~router_base() +{ + delete impl_; +} + +router_base:: +router_base( + opt_flags opt) + : impl_(new impl(opt)) +{ +} + +router_base:: +router_base( + router_base&& other) noexcept + :impl_(other.impl_) +{ + other.impl_ = nullptr; +} + +router_base& +router_base:: +operator=( + router_base&& other) noexcept +{ + delete impl_; + impl_ = other.impl_; + other.impl_ = 0; + return *this; +} + +auto +router_base:: +new_layer( + std::string_view pattern) -> layer& +{ + // the pattern must not be empty + if(pattern.empty()) + detail::throw_invalid_argument(); + // delete the last route if it is empty, + // this happens if they call route() without + // adding anything + if(! impl_->layers.empty() && + impl_->layers.back().entries.empty()) + impl_->layers.pop_back(); + impl_->layers.emplace_back(pattern); + return impl_->layers.back(); +}; + +std::size_t +router_base:: +new_layer_idx( + std::string_view pattern) +{ + new_layer(pattern); + return impl_->layers.size() - 1; +} + +auto +router_base:: +get_layer( + std::size_t idx) -> layer& +{ + return impl_->layers[idx]; +} + +void +router_base:: +add_impl( + std::string_view pattern, + handlers hn) +{ + if( pattern.empty()) + pattern = "/"; + impl_->layers.emplace_back( + pattern, hn); + + // Validate depth for any nested routers + auto& lay = impl_->layers.back(); + for(auto& entry : lay.entries) + if(entry.h->kind == is_router) + if(auto* r = entry.h->get_router()) + r->set_nested_depth(impl_->depth_); +} + +void +router_base:: +add_impl( + layer& l, + http::method verb, + handlers hn) +{ + // cannot be unknown + if(verb == http::method::unknown) + detail::throw_invalid_argument(); + + l.entries.reserve(l.entries.size() + hn.n); + for(std::size_t i = 0; i < hn.n; ++i) + l.entries.emplace_back(verb, + std::move(hn.p[i])); +} + +void +router_base:: +add_impl( + layer& l, + std::string_view verb_str, + handlers hn) +{ + l.entries.reserve(l.entries.size() + hn.n); + + if(verb_str.empty()) + { + // all + for(std::size_t i = 0; i < hn.n; ++i) + l.entries.emplace_back(std::move(hn.p[i])); + return; + } + + // possibly custom string + for(std::size_t i = 0; i < hn.n; ++i) + l.entries.emplace_back(verb_str, + std::move(hn.p[i])); +} + +void +router_base:: +set_nested_depth( + std::size_t parent_depth) +{ + std::size_t d = parent_depth + 1; + if(d >= max_path_depth) + detail::throw_length_error( + "router nesting depth exceeds max_path_depth"); + impl_->depth_ = d; + for(auto& layer : impl_->layers) + for(auto& entry : layer.entries) + if(entry.h->kind == is_router) + if(auto* r = entry.h->get_router()) + r->set_nested_depth(d); +} + +} // detail +} // http +} // boost + diff --git a/src/server/detail/router_base.hpp b/src/server/detail/router_base.hpp new file mode 100644 index 00000000..00d99f53 --- /dev/null +++ b/src/server/detail/router_base.hpp @@ -0,0 +1,127 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#ifndef BOOST_HTTP_SRC_SERVER_DETAIL_ROUTER_BASE_HPP +#define BOOST_HTTP_SRC_SERVER_DETAIL_ROUTER_BASE_HPP + +#include +#include "src/server/detail/route_match.hpp" + +namespace boost { +namespace http { +namespace detail { + +// An entry describes a single route handler. +// This can be an end route or a middleware. +// Members ordered largest-to-smallest for optimal packing. +struct router_base::entry +{ + // ~32 bytes (SSO string) + std::string verb_str; + + // 8 bytes each + handler_ptr h; + std::size_t matcher_idx = 0; // flat_router: index into matchers vector + + // 4 bytes + http::method verb = http::method::unknown; + + // 1 byte (+ 3 bytes padding) + bool all; + + // all + explicit entry( + handler_ptr h_) noexcept + : h(std::move(h_)) + , all(true) + { + } + + // verb match + entry( + http::method verb_, + handler_ptr h_) noexcept + : h(std::move(h_)) + , verb(verb_) + , all(false) + { + BOOST_ASSERT(verb != + http::method::unknown); + } + + // verb match + entry( + std::string_view verb_str_, + handler_ptr h_) noexcept + : h(std::move(h_)) + , verb(http::string_to_method(verb_str_)) + , all(false) + { + if(verb != http::method::unknown) + return; + verb_str = verb_str_; + } + + bool match_method( + route_params_base& rp) const noexcept + { + detail::route_params_access RP{rp}; + if(all) + return true; + if(verb != http::method::unknown) + return RP->verb_ == verb; + if(RP->verb_ != http::method::unknown) + return false; + return RP->verb_str_ == verb_str; + } +}; + +// A layer is a set of entries that match a route +struct router_base::layer +{ + matcher match; + std::vector entries; + + // middleware layer + layer( + std::string_view pat, + handlers hn) + : match(pat, false) + { + entries.reserve(hn.n); + for(std::size_t i = 0; i < hn.n; ++i) + entries.emplace_back(std::move(hn.p[i])); + } + + // route layer + explicit layer( + std::string_view pat) + : match(pat, true) + { + } +}; + +struct router_base::impl +{ + std::vector layers; + opt_flags opt; + std::size_t depth_ = 0; + + explicit impl( + opt_flags opt_) noexcept + : opt(opt_) + { + } +}; + +} // detail +} // http +} // boost + +#endif diff --git a/src/server/detail/stable_string.hpp b/src/server/detail/stable_string.hpp new file mode 100644 index 00000000..b4148d11 --- /dev/null +++ b/src/server/detail/stable_string.hpp @@ -0,0 +1,102 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#ifndef BOOST_HTTP_SERVER_DETAIL_STABLE_STRING_HPP +#define BOOST_HTTP_SERVER_DETAIL_STABLE_STRING_HPP + +#include +#include +#include + +namespace boost { +namespace http { +namespace detail { + +// avoids SBO +class stable_string +{ + char const* p_ = 0; + std::size_t n_ = 0; + +public: + ~stable_string() + { + if(p_) + delete[] p_; + } + + stable_string() = default; + + stable_string( + stable_string&& other) noexcept + : p_(other.p_) + , n_(other.n_) + { + other.p_ = nullptr; + other.n_ = 0; + } + + stable_string& operator=( + stable_string&& other) noexcept + { + auto p = p_; + auto n = n_; + p_ = other.p_; + n_ = other.n_; + other.p_ = p; + other.n_ = n; + return *this; + } + + explicit + stable_string( + std::string_view s) + : p_( + [&] + { + auto p =new char[s.size()]; + std::memcpy(p, s.data(), s.size()); + return p; + }()) + , n_(s.size()) + { + } + + stable_string( + char const* it, char const* end) + : stable_string(std::string_view(it, end)) + { + } + + char const* data() const noexcept + { + return p_; + } + + std::size_t size() const noexcept + { + return n_; + } + + operator core::string_view() const noexcept + { + return { data(), size() }; + } + + operator std::string_view() const noexcept + { + return { data(), size() }; + } +}; + +} // detail +} // http +} // boost + +#endif diff --git a/src/server/flat_router.cpp b/src/server/flat_router.cpp new file mode 100644 index 00000000..ca01718e --- /dev/null +++ b/src/server/flat_router.cpp @@ -0,0 +1,424 @@ +// +// Copyright (c) 2026 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#include +#include +#include + +#include "src/server/detail/router_base.hpp" +#include "src/server/detail/pct_decode.hpp" +#include "src/server/detail/route_match.hpp" + +#include + +namespace boost { +namespace http { + +//------------------------------------------------ + +struct flat_router::impl +{ + using entry = detail::router_base::entry; + using layer = detail::router_base::layer; + using handler = detail::router_base::handler; + using matcher = detail::router_base::matcher; + using opt_flags = detail::router_base::opt_flags; + using handler_ptr = detail::router_base::handler_ptr; + using match_result = route_params_base::match_result; + + std::vector entries; + std::vector matchers; + + // RAII scope tracker sets matcher's skip_ when scope ends + struct scope_tracker + { + std::vector& matchers_; + std::vector& entries_; + std::size_t matcher_idx_; + + scope_tracker( + std::vector& m, + std::vector& e, + std::size_t idx) + : matchers_(m) + , entries_(e) + , matcher_idx_(idx) + { + } + + ~scope_tracker() + { + matchers_[matcher_idx_].skip_ = entries_.size(); + } + }; + + static opt_flags + compute_effective_opts( + opt_flags parent, + opt_flags child) + { + opt_flags result = parent; + + // case_sensitive: bits 1-2 (2=true, 4=false) + if(child & 2) + result = (result & ~6) | 2; + else if(child & 4) + result = (result & ~6) | 4; + + // strict: bits 3-4 (8=true, 16=false) + if(child & 8) + result = (result & ~24) | 8; + else if(child & 16) + result = (result & ~24) | 16; + + return result; + } + + void + flatten(detail::router_base::impl& src) + { + flatten_recursive(src, opt_flags{}, 0); + } + + void + flatten_recursive( + detail::router_base::impl& src, + opt_flags parent_opts, + std::uint32_t depth) + { + opt_flags eff = compute_effective_opts(parent_opts, src.opt); + + for(auto& layer : src.layers) + { + // Move matcher, set flat router fields + std::size_t matcher_idx = matchers.size(); + matchers.emplace_back(std::move(layer.match)); + auto& m = matchers.back(); + m.first_entry_ = entries.size(); + m.effective_opts_ = eff; + m.depth_ = depth; + // m.skip_ set by scope_tracker dtor + + scope_tracker scope(matchers, entries, matcher_idx); + + for(auto& e : layer.entries) + { + if(e.h->kind == detail::router_base::is_router) + { + // Recurse into nested router + auto* nested = e.h->get_router(); + if(nested && nested->impl_) + flatten_recursive(*nested->impl_, eff, depth + 1); + } + else + { + // Set matcher_idx, then move entire entry + e.matcher_idx = matcher_idx; + entries.emplace_back(std::move(e)); + } + } + // ~scope_tracker sets matchers[matcher_idx].skip_ + } + } + + // Restore path to a given base_path length + static void + restore_path( + route_params_base& p, + std::size_t base_len) + { + p.base_path = { p.decoded_path_.data(), base_len }; + // Account for the addedSlash_ when computing path length + auto const path_len = p.decoded_path_.size() - (p.addedSlash_ ? 1 : 0); + if(base_len < path_len) + p.path = { p.decoded_path_.data() + base_len, + path_len - base_len }; + else + p.path = { p.decoded_path_.data() + + p.decoded_path_.size() - 1, 1 }; // soft slash + } + + capy::task + dispatch_loop(route_params_base& p) const + { + // All checks happen BEFORE co_await to minimize coroutine launches. + // Avoid touching p.ep_ (expensive atomic on Windows) - use p.kind_ for mode checks. + + std::size_t last_matched = SIZE_MAX; + std::uint32_t current_depth = 0; + + // Stack of base_path lengths at each depth level. + // path_stack[d] = base_path.size() before any matcher at depth d was tried. + std::size_t path_stack[detail::router_base::max_path_depth]; + path_stack[0] = 0; + + // Track which matcher index is matched at each depth level. + // matched_at_depth[d] = matcher index that successfully matched at depth d. + std::size_t matched_at_depth[detail::router_base::max_path_depth]; + for(std::size_t d = 0; d < detail::router_base::max_path_depth; ++d) + matched_at_depth[d] = SIZE_MAX; + + for(std::size_t i = 0; i < entries.size(); ) + { + auto const& e = entries[i]; + auto const& m = matchers[e.matcher_idx]; + auto const target_depth = m.depth_; + + //-------------------------------------------------- + // Pre-invoke checks (no coroutine yet) + //-------------------------------------------------- + + // For nested routes: verify ancestors at depths 0..target_depth-1 are matched. + // For siblings: if moving to same depth with different matcher, restore path. + bool ancestors_ok = true; + + // Check if we need to match new ancestor matchers for this entry. + // We iterate through matchers from last_matched+1 to e.matcher_idx, + // but ONLY process those that are at depths we need (ancestors or self). + std::size_t start_idx = (last_matched == SIZE_MAX) ? 0 : last_matched + 1; + + for(std::size_t check_idx = start_idx; + check_idx <= e.matcher_idx && ancestors_ok; + ++check_idx) + { + auto const& cm = matchers[check_idx]; + + // Only check matchers that are: + // 1. Ancestors (depth < target_depth) that we haven't matched yet, or + // 2. The entry's own matcher + bool is_needed_ancestor = (cm.depth_ < target_depth) && + (matched_at_depth[cm.depth_] == SIZE_MAX); + bool is_self = (check_idx == e.matcher_idx); + + if(!is_needed_ancestor && !is_self) + continue; + + // Restore path if moving to same or shallower depth + if(cm.depth_ <= current_depth && current_depth > 0) + { + restore_path(p, path_stack[cm.depth_]); + } + + // In error/exception mode, skip end routes + if(cm.end_ && p.kind_ != detail::router_base::is_plain) + { + i = cm.skip_; + ancestors_ok = false; + break; + } + + // Apply effective_opts for this matcher + p.case_sensitive = (cm.effective_opts_ & 2) != 0; + p.strict = (cm.effective_opts_ & 8) != 0; + + // Save path state before trying this matcher + if(cm.depth_ < detail::router_base::max_path_depth) + path_stack[cm.depth_] = p.base_path.size(); + + match_result mr; + if(!cm(p, mr)) + { + // Clear matched_at_depth for this depth and deeper + for(std::size_t d = cm.depth_; d < detail::router_base::max_path_depth; ++d) + matched_at_depth[d] = SIZE_MAX; + i = cm.skip_; + ancestors_ok = false; + break; + } + + // Mark this depth as matched + if(cm.depth_ < detail::router_base::max_path_depth) + matched_at_depth[cm.depth_] = check_idx; + + last_matched = check_idx; + current_depth = cm.depth_ + 1; + + // Save state for next depth level + if(current_depth < detail::router_base::max_path_depth) + path_stack[current_depth] = p.base_path.size(); + } + + if(!ancestors_ok) + continue; + + // Check method match (only for end routes) + if(m.end_ && !e.match_method( + const_cast(p))) + { + ++i; + continue; + } + + // Check kind match (cheap char comparison) + if(e.h->kind != p.kind_) + { + ++i; + continue; + } + + //-------------------------------------------------- + // Invoke handler (coroutine starts here) + //-------------------------------------------------- + + route_result rv; + try + { + rv = co_await e.h->invoke( + const_cast(p)); + } + catch(...) + { + // Only touch ep_ when actually catching + p.ep_ = std::current_exception(); + p.kind_ = detail::router_base::is_exception; + ++i; + continue; + } + + //-------------------------------------------------- + // Handle result + // + // Coroutines invert control - handler does the send. + // Success = !rv.failed() (handler completed request) + // route::next = continue to next handler + // route::next_route = skip to next route + // Failing error_code = enter error mode + //-------------------------------------------------- + + if(rv == route::next) + { + ++i; + continue; + } + + if(rv == route::next_route) + { + // next_route only valid for end routes, not middleware + if(!m.end_) + co_return make_error_code(std::errc::invalid_argument); + i = m.skip_; + continue; + } + + if(!rv.failed()) + { + // Success - handler completed the request + co_return rv; + } + + // Failing error_code - transition to error mode + p.ec_ = rv; + p.kind_ = detail::router_base::is_error; + + if(m.end_) + { + // End routes don't have error handlers + i = m.skip_; + continue; + } + + ++i; + } + + // Final state + if(p.kind_ == detail::router_base::is_exception) + co_return error::unhandled_exception; + if(p.kind_ == detail::router_base::is_error) + co_return p.ec_; + + co_return route::next; // no handler matched + } +}; + +//------------------------------------------------ + +flat_router:: +~flat_router() +{ + delete impl_; +} + +flat_router:: +flat_router( + detail::router_base&& src) + : impl_(new impl) +{ + impl_->flatten(*src.impl_); +} + +capy::task +flat_router:: +dispatch( + http::method verb, + urls::url_view const& url, + route_params_base& p) const +{ + if(verb == http::method::unknown) + detail::throw_invalid_argument(); + + // Initialize params + p.kind_ = detail::router_base::is_plain; + p.verb_ = verb; + p.verb_str_.clear(); + p.ec_.clear(); + p.ep_ = nullptr; + p.decoded_path_ = detail::pct_decode_path(url.encoded_path()); + p.base_path = { p.decoded_path_.data(), 0 }; + p.path = p.decoded_path_; + if(p.decoded_path_.back() != '/') + { + p.decoded_path_.push_back('/'); + p.addedSlash_ = true; + } + else + { + p.addedSlash_ = false; + } + + return impl_->dispatch_loop(p); +} + +capy::task +flat_router:: +dispatch( + std::string_view verb, + urls::url_view const& url, + route_params_base& p) const +{ + if(verb.empty()) + detail::throw_invalid_argument(); + + // Initialize params + p.kind_ = detail::router_base::is_plain; + p.verb_ = http::string_to_method(verb); + if(p.verb_ == http::method::unknown) + p.verb_str_ = verb; + else + p.verb_str_.clear(); + p.ec_.clear(); + p.ep_ = nullptr; + p.decoded_path_ = detail::pct_decode_path(url.encoded_path()); + p.base_path = { p.decoded_path_.data(), 0 }; + p.path = p.decoded_path_; + if(p.decoded_path_.back() != '/') + { + p.decoded_path_.push_back('/'); + p.addedSlash_ = true; + } + else + { + p.addedSlash_ = false; + } + + return impl_->dispatch_loop(p); +} + +} // http +} // boost + diff --git a/src/server/route_rule.cpp b/src/server/route_rule.cpp deleted file mode 100644 index 36d73737..00000000 --- a/src/server/route_rule.cpp +++ /dev/null @@ -1,16 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/http -// - -#include "src/server/route_rule.hpp" - -namespace boost { -namespace http { - -} // http -} // boost diff --git a/test/unit/server/basic_router.cpp b/test/unit/server/basic_router.cpp deleted file mode 100644 index 5b848e7f..00000000 --- a/test/unit/server/basic_router.cpp +++ /dev/null @@ -1,1624 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/http -// - -// Test that header file is self-contained. -#include - -#include - -#include "src/server/route_rule.hpp" - -#include "test_suite.hpp" - -namespace boost { -namespace http { - -struct basic_router_test -{ - void compileTimeTests() - { - struct params : route_params_base {}; - - BOOST_CORE_STATIC_ASSERT(std::is_copy_assignable>::value); - - struct h0 { void operator()(); }; - struct h1 { system::error_code operator()(); }; - struct h2 { system::error_code operator()(int); }; - struct h3 { system::error_code operator()(params&) const; }; - struct h4 { system::error_code operator()(params&, system::error_code) const; }; - struct h5 { void operator()(params&) {} }; - struct h6 { void operator()(params&, system::error_code) {} }; - struct h7 { system::error_code operator()(params&, int); }; - struct h8 { system::error_code operator()(params, int); }; - struct h9 { system::error_code operator()(params, system::error_code const&) const; }; - } - - using params = route_params_base; - - //-------------------------------------------- - - /** A handler for testing - */ - struct handler - { - ~handler() - { - if(alive_) - BOOST_TEST_EQ(called_, want_ != 0); - } - - explicit handler( - int want, system::error_code ec = - http::error::success) - : want_(want) - , ec_(ec) - { - } - - handler(handler&& other) - { - BOOST_ASSERT(other.alive_); - BOOST_ASSERT(! other.called_); - want_ = other.want_; - alive_ = true; - other.alive_ = false; - ec_ = other.ec_; - } - - route_result operator()(params&) const - { - called_ = true; - switch(want_) - { - default: - case 0: return route::close; - case 1: return route::send; - case 2: return route::next; - case 3: return ec_; - case 4: return route::next_route; - } - } - - private: - // 0 = not called - // 1 = called - // 2 = next - // 3 = error - // 4 = next_route - int want_; - bool alive_ = true; - bool mutable called_ = false; - system::error_code ec_; - }; - - // A handler which throws - template - struct throw_ex - { - ~throw_ex() - { - if(alive_) - BOOST_TEST(called_); - } - - throw_ex() = default; - - throw_ex(throw_ex&& other) - { - BOOST_ASSERT(other.alive_); - BOOST_ASSERT(! other.called_); - alive_ = true; - other.alive_ = false; - } - - route_result operator()(params&) const - { - called_ = true; - throw E("ex"); - } - - private: - bool alive_ = true; - bool mutable called_ = false; - }; - - /** An error handler for testing - */ - struct err_handler - { - ~err_handler() - { - if(alive_) - BOOST_TEST_EQ(called_, want_ != 0); - } - - err_handler( - int want, - system::error_code ec) - : want_(want) - , ec_(ec) - { - } - - err_handler(err_handler&& other) - { - BOOST_ASSERT(other.alive_); - BOOST_ASSERT(! other.called_); - want_ = other.want_; - alive_ = true; - other.alive_ = false; - ec_ = other.ec_; - } - - route_result operator()( - params&, system::error_code ec) const - { - called_ = true; - switch(want_) - { - default: - case 0: return route::close; - case 1: - BOOST_TEST(ec == ec_); - return route::send; - case 2: - BOOST_TEST(ec == ec_); - return route::next; - case 3: - BOOST_TEST(ec.failed()); - return ec_; - } - } - - private: - // 0 = not called - // 1 = called, expecting ec_ - // 2 = next, expecting ec_ - // 3 = change error - int want_; - bool alive_ = true; - bool mutable called_ = false; - system::error_code ec_; - }; - - /** An exception handler for testing - */ - template - struct ex_handler - { - ~ex_handler() - { - if(alive_) - BOOST_TEST_EQ(called_, want_ != 0); - } - - ex_handler(ex_handler const&) = delete; - - explicit ex_handler( - int want) - : want_(want) - { - } - - ex_handler(ex_handler&& other) - { - BOOST_ASSERT(other.alive_); - BOOST_ASSERT(! other.called_); - want_ = other.want_; - alive_ = true; - other.alive_ = false; - } - - route_result operator()( - params&, E const&) const - { - called_ = true; - switch(want_) - { - default: - case 0: return route::close; - case 1: return route::send; - case 2: return route::next; - } - } - - private: - // 0 = not called - // 1 = send - // 2 = next - int want_; - bool alive_ = true; - bool mutable called_ = false; - }; - - // handler to check base_url and path - struct path - { - ~path() - { - if(alive_) - BOOST_TEST(called_); - } - path(path&& other) - { - BOOST_ASSERT(other.alive_); - BOOST_ASSERT(! other.called_); - alive_ = true; - other.alive_ = false; - base_path_ = other.base_path_; - path_ = other.path_; - } - path( - core::string_view path = "/") - : path_(path) - { - } - path( - core::string_view base_path, - core::string_view path) - : base_path_(base_path) - , path_(path) - { - } - route_result operator()(params& req) const - { - called_ = true; - BOOST_TEST_EQ(req.base_path, base_path_); - BOOST_TEST_EQ(req.path, path_); - return route::next; - } - private: - bool alive_ = true; - bool mutable called_ = false; - core::string_view base_path_; - core::string_view path_; - }; - - //------------------------------------------- - - // must NOT be called - static handler skip() - { - return handler(0); - } - - // must be called - static handler send() - { - return handler(1); - } - - // must be called, returns route::next - static handler next() - { - return handler(2); - } - - // must be called, returns ec - static handler fail( - system::error_code ec) - { - return handler(3, ec); - } - - // must NOT be called - static err_handler err_skip() - { - return err_handler(0, - http::error::success); - } - - // must be called, expects ec, returns route::send - static err_handler err_send( - system::error_code ec) - { - return err_handler(1, ec); - } - - // must be called with `ec`, returns route::next - static err_handler err_next( - system::error_code ec) - { - return err_handler(2, ec); - } - - // must be called, returns a new error `ec` - static err_handler err_return( - system::error_code ec) - { - return err_handler(3, ec); - } - - struct none - { - }; - - // must NOT be called - static ex_handler ex_skip() - { - return ex_handler(0); - } - - // must be called, returns route::send - template - static ex_handler ex_send() - { - return ex_handler(1); - } - - // must be called, returns route::next - template - static ex_handler ex_next() - { - return ex_handler(2); - } - - using test_router = basic_router; - - void check( - test_router& r, - core::string_view url, - route_result rv0 = route::send) - { - params req; - auto rv = r.dispatch( - http::method::get, - urls::url_view(url), req); - if(BOOST_TEST_EQ(rv.message(), rv0.message())) - BOOST_TEST(rv == rv0); - } - - void check( - test_router& r, - http::method verb, - core::string_view url, - route_result rv0 = route::send) - { - params req; - auto rv = r.dispatch(verb, - urls::url_view(url), req); - if(BOOST_TEST_EQ(rv.message(), rv0.message())) - BOOST_TEST(rv == rv0); - } - - void check( - test_router& r, - core::string_view verb, - core::string_view url, - route_result rv0 = route::send) - { - params req; - auto rv = r.dispatch(verb, - urls::url_view(url), req); - if(BOOST_TEST_EQ(rv.message(), rv0.message())) - BOOST_TEST(rv == rv0); - } - - //-------------------------------------------- - - // special members - void testSpecial() - { - // default construction - { - test_router r; - check(r, "/", route::next); - } - - // copy construction - { - test_router r0; - r0.use(send()); - check(r0, "/"); - test_router r1(r0); - check(r1, "/"); - check(r0, "/"); - } - - // move assignment - { - test_router r0; - r0.use(send()); - check(r0, "/"); - test_router r1; - check(r1, "/", route::next); - r1 = std::move(r0); - check(r1, "/"); - } - - // copy assignment - { - test_router r0; - r0.use(send()); - check(r0, "/"); - test_router r1; - check(r1, "/", route::next); - r1 = r0; - check(r1, "/"); - check(r0, "/"); - } - - // options - { - // make sure this compiles - test_router r(router_options() - .case_sensitive(true) - .merge_params(true) - .strict(false)); - } - } - - void testUse() - { - system::error_code const er = - http::error::bad_connection; - system::error_code const er2 = - http::error::bad_expect; - - // pathless - { - test_router r; - r.use(send()); - check(r,"/"); - } - { - test_router r; - r.use( - path(), - send()); - check(r,"/"); - } - { - test_router r; - r.use( - send(), - skip()); - check(r,"/"); - } - { - test_router r; - r.use(send()); - r.use(skip()); - check(r,"/"); - } - { - test_router r; - r.use( - next(), - send()); - check(r,"/"); - } - { - test_router r; - r.use(next()); - r.use(send()); - check(r,"/"); - } - { - test_router r; - r.use(next()); - r.use(send()); - r.use(skip()); - check(r,"/"); - } - { - test_router r; - r.use( - next(), - send(), - skip()); - check(r,"/"); - } - - // pathless with errors - { - test_router r; - r.use(fail(er)); - check(r, "/", er); - } - { - test_router r; - r.use(next()); - r.use(err_skip()); - r.use(fail(er)); - r.use(skip()); - r.use(err_send(er)); - r.use(skip()); - r.use(err_skip()); - check(r,"/"); - } - { - test_router r; - r.use( - next(), - err_skip(), - fail(er), - skip(), - err_send(er), - skip(), - err_skip()); - check(r,"/"); - } - { - test_router r; - r.use(next()); - r.use(err_skip()); - r.use(fail(er)); - r.use(skip()); - r.use(err_return(er2)); - r.use(skip()); - r.use(err_next(er2)); - r.use(err_send(er2)); - check(r,"/"); - } - { - test_router r; - r.use( - next(), - err_skip(), - fail(er), - skip(), - err_return(er2), - skip(), - err_next(er2), - err_send(er2)); - check(r,"/"); - } - { - // cannot return success - test_router r; - r.use(fail(system::error_code())); - BOOST_TEST_THROWS(check(r,"/"), - std::invalid_argument); - } - { - // can't change failure to success - test_router r; - r.use( - fail(er), - err_return(system::error_code{})); - BOOST_TEST_THROWS(check(r,"/"), - std::invalid_argument); - } - - // pathless, returning route enums - { - test_router r; - r.use(fail(route::close)); - check(r,"/", route::close); - } - { - test_router r; - r.use(fail(route::complete)); - check(r,"/", route::complete); - } - { - test_router r; - r.use(fail(route::suspend)); - check(r,"/", route::suspend); - } - { - test_router r; - r.use(fail(route::next)); - check(r,"/", route::next); - } - { - // middleware can't return route::next_route - test_router r; - r.use(fail(route::next_route)); - BOOST_TEST_THROWS(check(r,"/", route::next), - std::invalid_argument); - } - { - test_router r; - r.use(fail(route::send)); - check(r,"/", route::send); - } - - // empty path - { - test_router r; - r.use("", send()); - check(r,"/"); - check(r,"/api"); - } - { - test_router r; - r.use("", - path("/api"), - send()); - check(r,"/api"); - } - - // prefix matching - { - test_router r; - r.use("/api", skip()); - check(r,"/", route::next); - } - { - test_router r; - r.use("/api", skip()); - check(r,"/", route::next); - check(r,"/a", route::next); - check(r,"/ap", route::next); - } - { - test_router r; - r.use("/api", send()); - check(r,"/api"); - check(r,"/api/"); - check(r,"/api/more"); - } - { - test_router r; - r.use("/api", - path("/api", "/more"), - send()); - check(r,"/api/more"); - } - { - test_router r; - r.use("/api/more", - path("/api/more", "/"), - send()); - check(r,"/api/more"); - } - { - test_router r; - r.use("/api", next()); - r.use("/api", send()); - check(r,"/api"); - check(r,"/api/"); - check(r,"/api/more"); - } - { - test_router r; - r.use("/api", - next(), - send()); - check(r,"/api"); - check(r,"/api/"); - check(r,"/api/more"); - } - { - test_router r; - r.use("/api", - next(), - send(), - err_skip(), - skip()); - check(r,"/api"); - check(r,"/api/"); - check(r,"/api/more"); - } - { - test_router r; - r.use("/api", skip()); - r.use("/", send()); - check(r,"/"); - } - { - test_router r; - r.use("/", next()); - r.use("/api", skip()); - r.use("/", send()); - check(r,"/"); - } - { - test_router r; - r.use("/x", next()); - r.use("/api", skip()); - r.use("/y", skip()); - r.use("/x", send()); - check(r,"/x"); - } - { - // no match - test_router r; - r.use("/x", skip()); - r.use("/api", skip()); - r.use("/y", skip()); - r.use("/x", skip()); - check(r,"/", route::next); - } - - // errors and matching - { - test_router r; - r.use("/x", skip()); - r.use("/api", skip()); - r.use("/y", fail(er)); - r.use("/y", skip()); - r.use("/x", err_skip()); - r.use("/y", err_return(er2)); - r.use("/z/", err_skip()); - r.use("/y", err_next(er2)); - r.use("/y", err_send(er2)); - r.use("/y", err_skip()); - r.use("/y", skip()); - check(r,"/y"); - } - { - test_router r; - r.use("/x", skip()); - r.use("/api", skip()); - r.use("/y", - fail(er), - skip()); - r.use("/x", err_skip()); - r.use("/y", err_return(er2)); - r.use("/z/", err_skip()); - r.use("/y", - err_next(er2), - err_send(er2), - err_skip(), - skip()); - check(r,"/y"); - } - - // case sensitivity - { - test_router r(router_options() - .case_sensitive(true)); - r.use("/x", skip()); - check(r, "/X", route::next); - } - { - test_router r(router_options() - .case_sensitive(false)); - r.use("/x", send()); - check(r, "/X"); - } - { - test_router r; - r.use("/x", send()); - check(r, "/X"); - } - } - - void testDispatch() - { - // dispatch - { - test_router r; - r.use(skip()); - BOOST_TEST_THROWS( - check(r, http::method::unknown, "/", route::next), - std::invalid_argument); - } - } - - void testRoute() - { - static auto const GET = http::method::get; - static auto const POST = http::method::post; - static system::error_code const er = - http::error::bad_connection; - static system::error_code const er2 = - http::error::bad_expect; - - // empty - { - test_router r; - check(r, "/", route::next); - check(r, GET, "/", route::next); - check(r, POST, "/", route::next); - check(r, "GET", "/", route::next); - check(r, "POST", "/", route::next); - BOOST_TEST_THROWS( - check(r, "", "/", route::next), - std::invalid_argument); - } - - // add - { - test_router r; - r.add(GET, "/", - path(), - send()); - check(r, GET, "/"); - check(r, "GET", "/"); - check(r, "get", "/", route::next); - check(r, POST, "/", route::next); - check(r, "POST", "/", route::next); - check(r, "post", "/", route::next); - } - { - test_router r; - r.add(POST, "/", send()); - check(r, POST, "/"); - check(r, "POST", "/"); - check(r, "Post", "/", route::next); - check(r, GET, "/", route::next); - check(r, "GET", "/", route::next); - check(r, "get", "/", route::next); - } - { - test_router r; - r.add(GET, "/x", skip()); - r.add(POST, "/y", skip()); - r.add(GET, "/y", - path("/y", "/"), - send()); - r.add(GET, "/z", skip()); - check(r, GET, "/y"); - } - { - test_router r; - r.add("HACK", "/", next()); - r.add("CRACK", "/", send()); - r.add(GET, "/", skip()); - check(r, "CRACK", "/"); - check(r, "crack", "/", route::next); - check(r, "HACK", "/", route::next); - } - - // route.add - { - test_router r; - r.route("/") - .add(GET, send()); - check(r, GET, "/"); - check(r, "GET", "/"); - check(r, "get", "/", route::next); - check(r, POST, "/", route::next); - check(r, "POST", "/", route::next); - check(r, "post", "/", route::next); - } - { - test_router r; - r.route("/") - .add(POST, send()); - check(r, POST, "/"); - check(r, "POST", "/"); - check(r, "Post", "/", route::next); - check(r, GET, "/", route::next); - check(r, "GET", "/", route::next); - check(r, "get", "/", route::next); - } - { - test_router r; - r.route("/x").add(GET, skip()); - r.route("/y") - .add(POST, skip()) - .add(GET, send()); - r.route("/z") - .add(GET, skip()); - check(r, GET, "/y"); - } - { - test_router r; - r.route("/") - .add("HACK", next()) - .add("CRACK", send()) - .add(GET, skip()); - check(r, "CRACK", "/"); - check(r, "crack", "/", route::next); - check(r, "HACK", "/", route::next); - } - - // mix with use - { - test_router r; - r.use(next()); - r.add(POST, "/x", skip()); - r.add(POST, "/y", skip()); - r.use("/z", next()); - r.add(POST, "/y", skip()); - r.add(POST, "/z", send()); - r.add(POST, "/z", skip()); - r.use(skip()); - check(r, POST, "/z"); - } - - // verb matching - { - test_router r; - r.add(GET, "/", send()); - check(r, GET, "/"); - check(r, POST, "/", route::next); - check(r, "GET", "/"); - check(r, "POST", "/", route::next); - check(r, "get", "/", route::next); - check(r, "Get", "/", route::next); - check(r, "gEt", "/", route::next); - check(r, "geT", "/", route::next); - check(r, "post", "/", route::next); - } - { - test_router r; - r.route("/") - .add(POST, skip()) - .add(GET, send()) - .add(GET, skip()) - .add(POST, skip()); - check(r, GET, "/"); - } - { - test_router r; - r.route("/") - .add(GET, skip()) - .add(POST, send()) - .add(POST, skip()) - .add(GET, skip()); - check(r, POST, "/"); - } - - // all - { - test_router r; - r.all("/x", skip()); - r.all("/y", send()); - r.all("/z", skip()); - check(r, GET, "/y"); - } - { - test_router r; - r.all("/y", next()); - r.all("/y", send()); - r.all("/z", skip()); - check(r, GET, "/y"); - } - { - test_router r; - r.add(GET, "/y", next()); - r.all("/y", send()); - r.all("/z", skip()); - check(r, GET, "/y"); - } - { - test_router r; - r.add(POST, "/y", skip()); - r.all("/y", send()); - r.all("/z", skip()); - check(r, GET, "/y"); - } - { - test_router r; - r.add(GET, "/x", skip()); - r.all("/y", send()); - r.use("/z", skip()); - check(r, GET, "/y"); - } - { - test_router r; - BOOST_TEST_THROWS( - r.all("", skip()), - std::invalid_argument); - } - - // error handling - { - test_router r; - r.use(err_skip()); - r.route("/") - .add(GET, skip()) - .add(POST, skip()) - .add("FAIL", fail(er)) - .add("HEAD", skip()); - check(r, "FAIL", "/", er); - } - { - test_router r; - r.use(err_skip()); - r.route("/") - .add(GET, skip()) - .add(POST, skip()) - .add("FAIL", fail(er)) - .add("HEAD", skip()); - r.use( - err_send(er), - err_skip()); - check(r, "FAIL", "/"); - } - { - test_router r; - r.route("/") - .add(GET, skip()) - .add(POST, fail(er)) - .add(POST, skip()); - r.use( - err_return(er2), - err_next(er2)); - r.use( - err_send(er2)); - check(r, POST, "/"); - } - - // request with known method, custom route - { - test_router r; - r.route("/") - .add("BEAUCOMP", skip()); - check(r, GET, "/", route::next); - } - - // clean up empty routes - { - test_router r; - r.route("/empty"); - r.route("/not-empty") - .add(GET, send()); - check(r, "/empty", route::next); - check(r, "/not-empty"); - } - - // bad verb - { - test_router r; - BOOST_TEST_THROWS( - r.route("/").add(http::method::unknown, skip()), - std::invalid_argument); - } - - // skip route on error - { - test_router r; - r.route("/") - .add(GET, next()) - .add(GET, fail(er)) - .add(GET, skip()) - .all(skip()) - .add(POST, skip()); - r.route("/") - .all(skip()); - r.use(err_send(er)); - check(r, GET, "/"); - } - - // skip route on next_route - { - test_router r; - r.route("/") - .add(GET, next()) - .add(GET, next()) - .add(GET, fail(route::next_route)) - .add(GET, skip()) - .add(GET, skip()) - .add(GET, skip()) - .add(GET, skip()) - .all(skip()); - r.route("/") - .add(GET, send()); - check(r, GET, "/"); - } - } - - void testSubRouter() - { - static auto const GET = http::method::get; - static auto const POST = http::method::post; - static system::error_code const er = - http::error::bad_connection; - static system::error_code const er2 = - http::error::bad_expect; - - // sub-middleware - { - test_router r; - r.use("/api", []{ - test_router r; - r.use("/v1", - skip()); - r.use("/v2", - path("/api/v2", "/"), - send()); - return r; }()); - check(r,"/api/v2"); - } - - // error handling - { - test_router r; - r.use("/api", []{ - test_router r; - r.use("/v1", - path("/api/v1", "/"), - fail(er)); // return er - return r; }()); - check(r,"/api/v1", er); - } - { - test_router r; - r.use("/api", []{ - test_router r; - r.use("/v1", - path("/api/v1", "/"), - fail(er)); - return r; }()); - r.use(err_next(er)); // next - check(r,"/api/v1", er); - } - { - test_router r; - r.use("/api", []{ - test_router r; - r.use("/v1", - path("/api/v1", "/"), - fail(er)); - return r; }()); - r.use( - skip()); - r.use([]{ - test_router r; - r.use( - skip(), - err_skip()); - return r; }()); - r.use("/api/v2", - err_skip()); - r.use( - err_next(er)); - check(r,"/api/v1", er); - } - { - test_router r; - r.use([]{ - test_router r; - r.use([]{ - test_router r; - r.use(fail(er)); - return r; }()); - r.use( - err_next(er), - skip()); - return r; }()); - r.use( - err_return(er2), - err_next(er2), - err_return(er)); - check(r, "/", er); - } - - // sub routes - { - test_router r; - r.use("/api", []{ - test_router r; - r.route("/user") - .add(POST, path("/api/user", "/")) - .add(GET, skip()) - .add(POST, send()); - return r; }()); - check(r, POST, "/api/user"); - } - - // nested options - { - test_router r(router_options() - .case_sensitive(true)); - r.use("/api", []{ - test_router r; - r.route("/USER") - .add(GET, skip()); - r.route("/user") - .add(GET, send()); - return r; }()); - check(r, "/api/user"); - } - { - test_router r(router_options() - .case_sensitive(true)); - r.use("/api", []{ - test_router r(router_options() - .case_sensitive(false)); - r.route("/USER") - .add(GET, send()); - r.route("/user") - .add(GET, skip()); - return r; }()); - check(r, "/api/user"); - } - { - test_router r; - r.use("/api", []{ - test_router r(router_options() - .case_sensitive(true)); - r.route("/USER") - .add(GET, send()); - r.route("/user") - .add(GET, skip()); - return r; }()); - check(r, "/api/USER"); - } - } - - void testErr() - { - static auto const GET = http::method::get; - static system::error_code const er = - http::error::bad_connection; - static system::error_code const er2 = - http::error::bad_content_length; - { - test_router r; - r.use(err_skip()); - check(r,"/", route::next); - } - { - test_router r; - r.use("", err_skip()); - check(r,"/", route::next); - } - { - test_router r; - r.use(send()); - r.use(err_skip()); - check(r,"/"); - } - { - test_router r; - r.use(fail(er)); - r.use(err_send(er)); - r.use(err_skip()); - check(r,"/", route::send); - } - { - test_router r; - r.use(fail(er)); - r.use("", err_send(er)); - r.use(err_skip()); - check(r,"/", route::send); - } - { - test_router r; - r.use(fail(er2)); - r.use(err_send(er2)); - r.use(err_skip()); - check(r,"/", route::send); - } - - // mount points - { - test_router r; - r.use("/api", fail(er)); - r.use("/api", err_send(er)); - r.use("/x", err_skip()); - check(r, "/api"); - } - { - test_router r; - r.use("/x", fail(er)); - r.use("/api", err_skip()); - r.use("/x", err_send(er)); - check(r, "/x/data"); - } - - // replacing errors - { - test_router r; - r.use(fail(er)); - r.use(err_return(er2)); - r.use(err_send(er2)); - check(r, "/"); - } - - { - test_router r; - r.use(fail(er)); - r.use(skip()); - r.use(err_send(er)); - check(r, "/"); - } - - // route-level vs. router-level - { - test_router r; - r.route("/").add(GET, fail(er)); - r.use(err_send(er)); - check(r, "/"); - } - - // subrouters - { - test_router r; - r.use("/api", []{ - test_router r; - r.use( - fail(er), - err_send(er), - err_skip()); - return r; }()); - r.use(err_skip()); - check(r, "/api"); - } - { - test_router r; - r.use("/api", []{ - test_router r; - r.use( - fail(er), - err_next(er), - skip()); - return r; }()); - r.use(err_send(er)); - r.use(err_skip()); - check(r, "/api"); - } - } - - void testExcept() - { -#ifndef BOOST_NO_EXCEPTIONS - { - test_router r; - r.except(ex_skip()); - check(r, "/", route::next); - } - { - test_router r; - r.use(throw_ex()); - check(r, "/", error::unhandled_exception); - } - { - test_router r; - r.except(ex_skip()); - r.use(throw_ex()); - check(r, "/", error::unhandled_exception); - } - { - test_router r; - r.except(ex_skip()); - r.use(throw_ex()); - r.except(ex_send()); - check(r, "/"); - } - { - test_router r; - r.except(ex_skip()); - r.use(throw_ex()); - r.except( - ex_skip(), - ex_next()); - check(r, "/", error::unhandled_exception); - } - { - test_router r; - r.except(ex_skip()); - r.use(throw_ex()); - r.except(ex_skip()); - check(r, "/", error::unhandled_exception); - } - { - test_router r; - r.except(ex_skip()); - r.use(throw_ex()); - r.except(ex_skip()); - r.except(ex_send()); - check(r, "/"); - } -#endif - } - - void testPath() - { - auto const path = []( - core::string_view pat, - core::string_view target, - core::string_view good) - { - test_router r; - r.use( pat, - [&](params& req) - { - BOOST_TEST_EQ(req.path, good); - return route::send; - }); - params req; - r.dispatch(http::method::get, - urls::url_view(target), req); - }; - - path("/", "/", "/"); - path("/", "/api", "/api"); - path("/api", "/api", "/"); - path("/api", "/api/", "/"); - path("/api", "/api/", "/"); - path("/api", "/api/v0", "/v0"); - path("/api/", "/api", "/"); - path("/api/", "/api", "/"); - path("/api/", "/api/", "/"); - path("/api/", "/api/v0", "/v0"); - } - - void testPctDecode() - { - static auto const GET = http::method::get; - - // slash - { - test_router r; - r.add(GET, "/auth/login", skip()); - check(r, "/auth%2flogin", route::next); - } - - // backslash - { - test_router r; - r.add(GET, "/auth\\login", skip()); - check(r, "/auth%5clogin", route::next); - } - - // unreserved - { - test_router r; - r.add(GET, "/a", send()); - check(r, "/%61"); - } - { - test_router r; - r.add(GET, "/%61", send()); - check(r, "/%61"); - } - { - test_router r; - r.add(GET, "/%61", send()); - check(r, "/a"); - } - } - - void testDetach() - { - static auto const GET = http::method::get; - { - test_router r; - r.use(next()); - r.use(fail(route::suspend)); - check(r,"/", route::suspend); - } - { - test_router r; - r.use(next()); - r.use(fail(route::suspend)); - params req; - auto rv1 = r.dispatch(GET, urls::url_view("/"), req); - BOOST_TEST(rv1 == route::suspend); - } - { - test_router r; - r.use(next()); - r.use(fail(route::suspend)); - r.use(next()); - r.use(fail(route::send)); - params req; - auto rv1 = r.dispatch(GET, urls::url_view("/"), req); - BOOST_TEST(rv1 == route::suspend); - auto rv2 = r.resume(req, route::next); - BOOST_TEST(rv2 == route::send); - } - { - test_router r; - r.use(next()); - r.use([]{ - test_router r; - r.use( - next(), - fail(route::suspend), - path(), - next()); - return r; }()); - r.use(send()); - params req; - auto rv1 = r.dispatch(GET, urls::url_view("/"), req); - BOOST_TEST(rv1 == route::suspend); - auto rv2 = r.resume(req, route::next); - BOOST_TEST(rv2 == route::send); - } - - // return values - { - test_router r; - r.use(fail(route::suspend)); - params req; - { - auto rv1 = r.dispatch(GET, urls::url_view("/"), req); - BOOST_TEST(rv1 == route::suspend); - auto rv2 = r.resume(req, route::send); - BOOST_TEST(rv2 == route::send); - } - { - auto rv1 = r.dispatch(GET, urls::url_view("/"), req); - BOOST_TEST(rv1 == route::suspend); - auto rv2 = r.resume(req, route::close); - BOOST_TEST(rv2 == route::close); - } - { - auto rv1 = r.dispatch(GET, urls::url_view("/"), req); - BOOST_TEST(rv1 == route::suspend); - auto rv2 = r.resume(req, route::complete); - BOOST_TEST(rv2 == route::complete); - } - { - auto rv1 = r.dispatch(GET, urls::url_view("/"), req); - BOOST_TEST(rv1 == route::suspend); - BOOST_TEST_THROWS(r.resume(req, system::error_code()), - std::invalid_argument); - } - } - - // path restoration - { - test_router r; - r.use(next()); - r.use("/api", []{ - test_router r; - r.use( - next(), - fail(route::suspend), - path("/api", "/"), - next()); - return r; }()); - r.use("/api", send()); - params req; - auto rv1 = r.dispatch(GET, urls::url_view("/api"), req); - BOOST_TEST(rv1 == route::suspend); - auto rv2 = r.resume(req, route::next); - BOOST_TEST(rv2 == route::send); - } - - // suspend on resume - { - test_router r; - r.use(fail(route::suspend)); - params req; - auto rv1 = r.dispatch(GET, urls::url_view("/"), req); - BOOST_TEST(rv1 == route::suspend); - BOOST_TEST_THROWS(r.resume(req, route::suspend), - std::invalid_argument); - } - - // invalid suspend - { - test_router r; - r.use(fail(route::suspend)); - params req; - auto rv1 = r.dispatch(GET, urls::url_view("/"), req); - BOOST_TEST(rv1 == route::suspend); - BOOST_TEST_THROWS(r.resume(req, system::error_code()), - std::invalid_argument); - } - } - - static route_result func( - params&) - { - return route::send; - } - - void testFuncPtr() - { - { - test_router r; - r.use(func); - } - } - - void run() - { - testSpecial(); - testUse(); - testDispatch(); - testRoute(); - testSubRouter(); - testErr(); - testExcept(); - testPath(); - testPctDecode(); - testDetach(); - testFuncPtr(); - } -}; - -TEST_SUITE( - basic_router_test, - "boost.http.server.basic_router"); - -} // http -} // boost diff --git a/test/unit/server/flat_router.cpp b/test/unit/server/flat_router.cpp new file mode 100644 index 00000000..c3006618 --- /dev/null +++ b/test/unit/server/flat_router.cpp @@ -0,0 +1,32 @@ +// +// Copyright (c) 2026 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +// Test that header file is self-contained. +#include + +#include "test_suite.hpp" + +namespace boost { +namespace http { + +struct flat_router_test +{ + void run() + { + // Header compilation test only. + // Functional tests are in router.cpp. + } +}; + +TEST_SUITE( + flat_router_test, + "boost.http.server.flat_router"); + +} // http +} // boost diff --git a/test/unit/server/route_handler.cpp b/test/unit/server/route_handler.cpp index c00ea024..457af4df 100644 --- a/test/unit/server/route_handler.cpp +++ b/test/unit/server/route_handler.cpp @@ -10,8 +10,10 @@ // Test that header file is self-contained. #include -#include +#include +#include #include +#include #include "test_suite.hpp" @@ -20,18 +22,18 @@ namespace http { struct route_handler_test { - using test_router = - basic_router; + using test_router = router; void check( test_router& r, http::method verb, core::string_view url, - route_result rv0 = route::send) + route_result rv0 = route_result{}) { + flat_router fr(std::move(r)); route_params p; - auto rv = r.dispatch( - verb, urls::url_view(url), p); + auto rv = capy::run_sync()(fr.dispatch( + verb, urls::url_view(url), p)); if(BOOST_TEST_EQ(rv.message(), rv0.message())) BOOST_TEST(rv == rv0); } @@ -52,40 +54,40 @@ struct route_handler_test test_router r; r.use( - [](route_params& p) + [](route_params& p) -> capy::task { // create session_token auto& st = p.session_data.try_emplace(); BOOST_TEST_EQ(st.valid, false); - return route::next; + co_return route::next; }); r.use("/user", - [](route_params& p) + [](route_params& p) -> capy::task { // make session token valid auto* st = p.session_data.find(); if(BOOST_TEST_NE(st, nullptr)) st->valid = true; - return route::next; + co_return route::next; }); r.route("/user/auth") .add(POST, - [](route_params& p) + [](route_params& p) -> capy::task { auto& st = p.session_data.get(); BOOST_TEST_EQ(st.valid, true); // create auth_token each time auto& at = p.route_data.emplace(); at.valid = true; - return route::next; + co_return route::next; }, - [](route_params& p) + [](route_params& p) -> capy::task { auto& at = p.route_data.get(); auto& st = p.session_data.get(); BOOST_TEST_EQ(at.valid, true); BOOST_TEST_EQ(st.valid, true); - return route::send; + co_return route_result{}; }); check(r, POST, urls::url_view("/user/auth")); } diff --git a/test/unit/server/router.cpp b/test/unit/server/router.cpp new file mode 100644 index 00000000..d027f22b --- /dev/null +++ b/test/unit/server/router.cpp @@ -0,0 +1,462 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +// Test that header file is self-contained. +#include +#include +#include + +#include + +#include "test_suite.hpp" + +namespace boost { +namespace http { + +struct router_test +{ + using params = route_params_base; + using test_router = router; + + //-------------------------------------------- + // Simple handlers - no destructor verification + //-------------------------------------------- + + // returns success (non-failing error_code) + static auto h_send(params&) -> capy::task + { co_return route_result{}; } + + // returns route::next + static auto h_next(params&) -> capy::task + { co_return route::next; } + + // returns route::next_route + static auto h_next_route(params&) -> capy::task + { co_return route::next_route; } + + // returns specified error + static auto h_fail(system::error_code ec) + { + return [ec](params&) -> capy::task + { co_return ec; }; + } + + // error handler returns success + static auto eh_send(system::error_code expect) + { + return [expect](params&, system::error_code ec) -> capy::task + { BOOST_TEST(ec == expect); co_return route_result{}; }; + } + + // error handler returns route::next + static auto eh_next(system::error_code expect) + { + return [expect](params&, system::error_code ec) -> capy::task + { BOOST_TEST(ec == expect); co_return route::next; }; + } + + // error handler returns a new error + static auto eh_return(system::error_code new_ec) + { + return [new_ec](params&, system::error_code) -> capy::task + { co_return new_ec; }; + } + + // exception handler returns success - return as lambda for template deduction + static auto exh_send() + { + return [](params&, std::exception_ptr) -> capy::task + { co_return route_result{}; }; + } + + // exception handler returns route::next + static auto exh_next() + { + return [](params&, std::exception_ptr) -> capy::task + { co_return route::next; }; + } + + // throws exception + static auto h_throw(params&) -> capy::task + { throw std::runtime_error("test"); co_return route::next; } + + // checks path then returns success + static auto h_path(core::string_view base, core::string_view path) + { + return [base, path](params& rp) -> capy::task + { + BOOST_TEST_EQ(rp.base_path, base); + BOOST_TEST_EQ(rp.path, path); + co_return route_result{}; + }; + } + + //-------------------------------------------- + // Helper to run dispatch and check result + //-------------------------------------------- + + static void check( + test_router& r, + core::string_view url, + route_result rv0 = route_result{}) + { + flat_router fr(std::move(r)); + params req; + auto rv = capy::run_sync()(fr.dispatch( + http::method::get, urls::url_view(url), req)); + BOOST_TEST_EQ(rv.message(), rv0.message()); + } + + static void check( + test_router& r, + http::method verb, + core::string_view url, + route_result rv0 = route_result{}) + { + flat_router fr(std::move(r)); + params req; + auto rv = capy::run_sync()(fr.dispatch( + verb, urls::url_view(url), req)); + BOOST_TEST_EQ(rv.message(), rv0.message()); + } + + static void check( + test_router& r, + core::string_view verb, + core::string_view url, + route_result rv0 = route_result{}) + { + flat_router fr(std::move(r)); + params req; + auto rv = capy::run_sync()(fr.dispatch( + verb, urls::url_view(url), req)); + BOOST_TEST_EQ(rv.message(), rv0.message()); + } + + //-------------------------------------------- + // Tests + //-------------------------------------------- + + void testUse() + { + // pathless middleware + { test_router r; r.use(h_send); check(r, "/"); } + { test_router r; r.use(h_next, h_send); check(r, "/"); } + { test_router r; r.use(h_next); r.use(h_send); check(r, "/"); } + + // path prefix matching + { test_router r; r.use("/api", h_next); check(r, "/", route::next); } + { test_router r; r.use("/api", h_send); check(r, "/api"); } + { test_router r; r.use("/api", h_send); check(r, "/api/"); } + { test_router r; r.use("/api", h_send); check(r, "/api/v1"); } + { test_router r; r.use("/api", h_next); r.use("/api", h_send); check(r, "/api"); } + + // multiple paths + { test_router r; r.use("/x", h_next); r.use("/y", h_send); check(r, "/y"); } + { test_router r; r.use("/x", h_next); r.use("/", h_send); check(r, "/"); } + + // path adjustments + { test_router r; r.use("/api", h_path("/api", "/")); check(r, "/api"); } + { test_router r; r.use("/api", h_path("/api", "/v1")); check(r, "/api/v1"); } + { test_router r; r.use("/api/v1", h_path("/api/v1", "/")); check(r, "/api/v1"); } + } + + void testRoute() + { + static auto const GET = http::method::get; + static auto const POST = http::method::post; + + // basic routing + { test_router r; r.add(GET, "/", h_send); check(r, GET, "/"); } + { test_router r; r.add(GET, "/", h_next); check(r, POST, "/", route::next); } + { test_router r; r.add(POST, "/", h_send); check(r, POST, "/"); } + + // verb matching - case sensitive + { test_router r; r.add(GET, "/", h_send); check(r, "GET", "/"); } + { test_router r; r.add(GET, "/", h_next); check(r, "get", "/", route::next); } + + // custom verb + { test_router r; r.add("CUSTOM", "/", h_send); check(r, "CUSTOM", "/"); } + { test_router r; r.add("CUSTOM", "/", h_next); check(r, "custom", "/", route::next); } + + // path matching + { test_router r; r.add(GET, "/x", h_next); r.add(GET, "/y", h_send); check(r, GET, "/y"); } + + // route().add() + { test_router r; r.route("/").add(GET, h_send); check(r, GET, "/"); } + { test_router r; r.route("/").add(GET, h_next).add(POST, h_send); check(r, POST, "/"); } + + // all() matches any method + { test_router r; r.all("/", h_send); check(r, GET, "/"); } + { test_router r; r.all("/", h_send); check(r, POST, "/"); } + { test_router r; r.all("/", h_send); check(r, "CUSTOM", "/"); } + + // route::next_route skips to next route + { test_router r; r.route("/").add(GET, h_next_route).add(GET, h_next); r.route("/").add(GET, h_send); check(r, GET, "/"); } + + // multiple handlers on same route + { test_router r; r.route("/").add(GET, h_next).add(GET, h_send); check(r, GET, "/"); } + } + + void testError() + { + system::error_code const er = http::error::bad_connection; + system::error_code const er2 = http::error::bad_expect; + + // error from handler + { test_router r; r.use(h_fail(er)); check(r, "/", er); } + + // error handler catches + { test_router r; r.use(h_fail(er), eh_send(er)); check(r, "/"); } + + // error handler next + { test_router r; r.use(h_fail(er), eh_next(er), eh_send(er)); check(r, "/"); } + + // error handler returns new error + { test_router r; r.use(h_fail(er), eh_return(er2), eh_send(er2)); check(r, "/"); } + + // error skips plain handlers + { test_router r; r.use(h_fail(er), h_next, eh_send(er)); check(r, "/"); } + + // error from route skips to middleware + { test_router r; r.route("/").add(http::method::get, h_fail(er)); r.use(eh_send(er)); check(r, http::method::get, "/"); } + + // error handler on different path not called + { test_router r; r.use("/x", h_fail(er)); r.use("/x", eh_send(er)); check(r, "/x"); } + } + + void testException() + { +#ifndef BOOST_NO_EXCEPTIONS + // unhandled exception + { test_router r; r.use(h_throw); check(r, "/", error::unhandled_exception); } + + // exception handler catches + { test_router r; r.use(h_throw); r.except(exh_send()); check(r, "/"); } + + // exception handler next + { test_router r; r.use(h_throw); r.except(exh_next(), exh_send()); check(r, "/"); } +#endif + } + + void testNested() + { + static auto const GET = http::method::get; + + // nested router middleware + { + test_router r; + r.use("/api", []{ + test_router r2; + r2.use("/v1", h_send); + return r2; + }()); + check(r, "/api/v1"); + } + + // nested router path adjustments + { + test_router r; + r.use("/api", []{ + test_router r2; + r2.use("/v1", h_path("/api/v1", "/")); + return r2; + }()); + check(r, "/api/v1"); + } + + // nested router with routes + { + test_router r; + r.use("/api", []{ + test_router r2; + r2.add(GET, "/user", h_send); + return r2; + }()); + check(r, GET, "/api/user"); + } + + // deeply nested + { + test_router r; + r.use("/a", []{ + test_router r2; + r2.use("/b", []{ + test_router r3; + r3.use("/c", h_send); + return r3; + }()); + return r2; + }()); + check(r, "/a/b/c"); + } + + // nested path restoration on sibling skip + { + test_router r; + r.use("/api", []{ + test_router r2; + r2.use("/v1", h_next); // doesn't match /api/v2 + return r2; + }()); + r.use("/api", h_send); // should match /api/v2 + check(r, "/api/v2"); + } + + // sibling routers - first doesn't match + { + test_router r; + r.use("/x", h_next); + r.use("/y", h_send); + check(r, "/y"); + } + + // error propagates from nested + { + system::error_code const er = http::error::bad_connection; + test_router r; + r.use("/api", [er]{ + test_router r2; + r2.use(h_fail(er)); + return r2; + }()); + r.use(eh_send(er)); + check(r, "/api"); + } + + // nesting beyond max_path_depth throws + { + // Build a chain of routers exceeding max_path_depth + auto make_deep = [](auto& self, std::size_t depth) -> test_router + { + test_router r; + if(depth > 0) + r.use("/x", self(self, depth - 1)); + else + r.use(h_send); + return r; + }; + + // max_path_depth levels should succeed + { + test_router r; + r.use("/a", make_deep(make_deep, + detail::router_base::max_path_depth - 2)); + // Should not throw + flat_router fr(std::move(r)); + (void)fr; + } + + // max_path_depth + 1 should throw + BOOST_TEST_THROWS( + [&]{ + test_router r; + r.use("/a", make_deep(make_deep, + detail::router_base::max_path_depth)); + }(), + std::length_error); + } + } + + void testOptions() + { + static auto const GET = http::method::get; + + // case_sensitive (default false) + { test_router r; r.use("/api", h_send); check(r, "/API"); } + { test_router r; r.add(GET, "/api", h_send); check(r, GET, "/API"); } + + // case_sensitive true + { + test_router r(router_options().case_sensitive(true)); + r.use("/api", h_next); + check(r, "/API", route::next); + } + + // nested inherits options + { + test_router r(router_options().case_sensitive(true)); + r.use("/api", []{ + test_router r2; // inherits case_sensitive + r2.use("/v1", h_next); + return r2; + }()); + check(r, "/api/V1", route::next); + } + + // nested can override options + { + test_router r(router_options().case_sensitive(true)); + r.use("/api", []{ + test_router r2(router_options().case_sensitive(false)); + r2.use("/v1", h_send); + return r2; + }()); + check(r, "/api/V1"); + } + } + + void testDispatch() + { + // unknown method throws + { + test_router r; + r.use(h_next); + flat_router fr(std::move(r)); + params req; + BOOST_TEST_THROWS( + capy::run_sync()(fr.dispatch( + http::method::unknown, urls::url_view("/"), req)), + std::invalid_argument); + } + + // empty verb string throws + { + test_router r; + r.use(h_next); + flat_router fr(std::move(r)); + params req; + BOOST_TEST_THROWS( + capy::run_sync()(fr.dispatch( + "", urls::url_view("/"), req)), + std::invalid_argument); + } + } + + void testPathDecoding() + { + static auto const GET = http::method::get; + + // percent-encoded characters + { test_router r; r.add(GET, "/a", h_send); check(r, GET, "/%61"); } + { test_router r; r.add(GET, "/%61", h_send); check(r, GET, "/a"); } + + // encoded slash doesn't match path separator + { test_router r; r.add(GET, "/auth/login", h_next); check(r, GET, "/auth%2flogin", route::next); } + } + + void run() + { + testUse(); + testRoute(); + testError(); + testException(); + testNested(); + testOptions(); + testDispatch(); + testPathDecoding(); + } +}; + +TEST_SUITE( + router_test, + "boost.http.server.router"); + +} // http +} // boost diff --git a/test/unit/server/router_types.cpp b/test/unit/server/router_types.cpp index a93d2fb0..25164228 100644 --- a/test/unit/server/router_types.cpp +++ b/test/unit/server/router_types.cpp @@ -50,22 +50,6 @@ struct router_types_test check(n, route::next_route); check(n, route::send); } - - basic_router r; - r.add(http::method::post, "/", - [](route_params_base& rp) -> - route_result - { - BOOST_TEST( rp.is_method(http::method::post)); - BOOST_TEST(! rp.is_method(http::method::get)); - BOOST_TEST(! rp.is_method("GET")); - BOOST_TEST(! rp.is_method("Post")); - BOOST_TEST( rp.is_method("POST")); - return route::send; - }); - route_params_base rp; - auto rv = r.dispatch("POST", "/", rp); - BOOST_TEST(rv = route::send); } }; From 49ff8c28d749508e7032bf23fb0be833feb2154f Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Sat, 17 Jan 2026 17:10:48 -0800 Subject: [PATCH 03/12] Trim CI targets --- .drone/drone.bat | 2 - .drone/drone.sh | 11 - .github/workflows/ci.yml | 1023 +++++++------------------------------- test/limits/Jamfile | 1 - 4 files changed, 179 insertions(+), 858 deletions(-) diff --git a/.drone/drone.bat b/.drone/drone.bat index f3c0ef48..b79bbea9 100755 --- a/.drone/drone.bat +++ b/.drone/drone.bat @@ -42,8 +42,6 @@ echo using zlib : : : ^off ^; >> !BOOST_ROOT!\project-config.jam REM Customizations cd -pushd !BOOST_ROOT!\libs -git clone https://github.com/cppalliance/buffers -b !BOOST_BRANCH! --depth 1 popd pushd !BOOST_ROOT!\libs git clone https://github.com/cppalliance/capy -b !BOOST_BRANCH! --depth 1 diff --git a/.drone/drone.sh b/.drone/drone.sh index 40c67ec6..6cb2a2f0 100755 --- a/.drone/drone.sh +++ b/.drone/drone.sh @@ -35,12 +35,6 @@ common_install () { . ./ci/common_install.sh - if [ ! -d "$BOOST_ROOT/libs/buffers" ]; then - pushd $BOOST_ROOT/libs - git clone https://github.com/cppalliance/buffers -b $BOOST_BRANCH --depth 1 - popd - fi - if [ ! -d "$BOOST_ROOT/libs/capy" ]; then pushd $BOOST_ROOT/libs git clone https://github.com/cppalliance/capy -b $BOOST_BRANCH --depth 1 @@ -121,11 +115,6 @@ cp -r $DRONE_BUILD_DIR/* libs/$SELF git submodule update --init --recursive # Customizations -if [ ! -d "$BOOST_ROOT/libs/buffers" ]; then - pushd $BOOST_ROOT/libs - git clone https://github.com/cppalliance/buffers -b $BOOST_BRANCH --depth 1 - popd -fi if [ ! -d "$BOOST_ROOT/libs/capy" ]; then pushd $BOOST_ROOT/libs git clone https://github.com/cppalliance/capy -b $BOOST_BRANCH --depth 1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48b506f4..2dca1ecb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,20 +1,18 @@ # -# Copyright (c) 2023 Christian Mazakas -# Copyright (c) 2023 Alan de Freitas -# Copyright (c) 2021-2023 Sam Darwin -# Copyright (c) 2020-2021 Peter Dimov -# Copyright (c) 2021 Andrey Semashev +# Copyright (c) 2026 Steve Gerbino # # Distributed under the Boost Software License, Version 1.0. (See accompanying # file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) # -# Official repository: https://github.com/CPPAlliance/http/ +# Official repository: https://github.com/cppalliance/http/ # name: CI on: pull_request: + branches: [ master, develop ] + workflow_dispatch: push: branches: - master @@ -38,18 +36,23 @@ env: TZ: "Europe/London" jobs: - runner-selection: - name: Runner Selection - runs-on: ${{ github.repository_owner == 'boostorg' && fromJSON('[ "self-hosted", "linux", "x64", "ubuntu-latest-aws" ]') || 'ubuntu-latest' }} - outputs: - labelmatrix: ${{ steps.aws_hosted_runners.outputs.labelmatrix }} - steps: - - name: AWS Hosted Runners - id: aws_hosted_runners - uses: cppalliance/aws-hosted-runners@v1.0.0 + # Self-hosted runner selection is disabled to allow re-running individual + # failed jobs from the GitHub Actions UI. When using dynamic runner selection, + # the runs-on value depends on this job's output, which isn't available when + # re-running a subset of jobs. + # + # runner-selection: + # name: Runner Selection + # runs-on: ${{ github.repository_owner == 'boostorg' && fromJSON('[ "self-hosted", "linux", "x64", "ubuntu-latest-aws" ]') || 'ubuntu-latest' }} + # outputs: + # labelmatrix: ${{ steps.aws_hosted_runners.outputs.labelmatrix }} + # steps: + # - name: AWS Hosted Runners + # id: aws_hosted_runners + # uses: cppalliance/aws-hosted-runners@v1.0.0 build: - needs: [ runner-selection ] + # needs: [ runner-selection ] defaults: run: shell: bash @@ -58,91 +61,36 @@ jobs: fail-fast: false matrix: include: - # Windows compilers - # + # Windows (3 configurations) - compiler: "msvc" version: "14.42" - cxxstd: "17,20" + cxxstd: "20" latest-cxxstd: "20" runs-on: "windows-2022" b2-toolset: "msvc-14.4" generator: "Visual Studio 17 2022" is-latest: true - name: "MSVC 14.42: C++17-20" + name: "MSVC 14.42: C++20" shared: false build-type: "Release" build-cmake: true - compiler: "msvc" version: "14.34" - cxxstd: "17,20" - latest-cxxstd: "20" - runs-on: "windows-2022" - b2-toolset: "msvc-14.3" - generator: "Visual Studio 17 2022" - is-latest: true - name: "MSVC 14.34: C++17-20" - shared: true - build-type: "Release" - build-cmake: true - - - compiler: "msvc" - version: "14.34" - cxxstd: "17,20" - latest-cxxstd: "20" - runs-on: "windows-2022" - b2-toolset: "msvc-14.3" - generator: "Visual Studio 17 2022" - is-latest: true - name: "MSVC 14.34: C++17-20 (x86)" - shared: false - x86: true - build-type: "Release" - - - compiler: "msvc" - version: "14.34" - cxxstd: "17,20" + cxxstd: "20" latest-cxxstd: "20" runs-on: "windows-2022" b2-toolset: "msvc-14.3" generator: "Visual Studio 17 2022" - is-latest: true - name: "MSVC 14.34: C++17-20" + name: "MSVC 14.34: C++20 (shared)" shared: true build-type: "Release" - build-cmake: true - - - compiler: "clang-cl" - version: "*" - cxx: "clang++-cl" - cc: "clang-cl" - runs-on: "windows-2022" - b2-toolset: "clang-win" - generator-toolset: "ClangCL" - is-latest: true - is-earliest: true - name: "Windows-Clang" - shared: false - build-type: "Release" - build-cmake: true - - - compiler: "mingw" - version: "*" - cxx: "g++" - cc: "gcc" - runs-on: "windows-2022" - b2-toolset: "gcc" - generator: "MinGW Makefiles" - is-latest: true - is-earliest: true - name: "MinGW (shared)" - shared: true - build-type: "Debug" - build-cmake: true - compiler: "mingw" version: "*" + cxxstd: "20" + latest-cxxstd: "20" cxx: "g++" cc: "gcc" runs-on: "windows-2022" @@ -150,124 +98,47 @@ jobs: generator: "MinGW Makefiles" is-latest: true is-earliest: true - name: "MinGW (static)" + name: "MinGW: C++20" shared: false build-type: "Release" build-cmake: true - # OSX compilers + # macOS (2 configurations) + # TODO: Re-enable when BSD/kqueue support is implemented # - - - compiler: "apple-clang" - version: "*" - cxx: "clang++" - cc: "clang" - runs-on: "macos-26" - b2-toolset: "clang" - is-latest: true - name: "Apple-Clang (macOS 26)" - shared: true - build-type: "Release" - build-cmake: true - - - compiler: "apple-clang" - version: "*" - cxx: "clang++" - cc: "clang" - runs-on: "macos-26" - b2-toolset: "clang" - is-latest: true - name: "Apple-Clang (macOS 26, ubsan)" - shared: false - build-type: "RelWithDebInfo" - ubsan: true - - - compiler: "apple-clang" - version: "*" - cxx: "clang++" - cc: "clang" - runs-on: "macos-26" - b2-toolset: "clang" - is-latest: true - name: "Apple-Clang (macOS 26, asan)" - shared: true - build-type: "RelWithDebInfo" - asan: true - - - compiler: "apple-clang" - version: "*" - cxx: "clang++" - cc: "clang" - runs-on: "macos-15" - b2-toolset: "clang" - name: "Apple-Clang (macOS 15)" - shared: false - build-type: "Release" - build-cmake: true - - - compiler: "apple-clang" - version: "*" - cxx: "clang++" - cc: "clang" - runs-on: "macos-14" - b2-toolset: "clang" - name: "Apple-Clang (macOS 14)" - shared: true - build-type: "Release" - build-cmake: true - - # Linux compilers + # - compiler: "apple-clang" + # version: "*" + # cxxstd: "20" + # latest-cxxstd: "20" + # cxx: "clang++" + # cc: "clang" + # runs-on: "macos-26" + # b2-toolset: "clang" + # is-latest: true + # name: "Apple-Clang (macOS 26, asan+ubsan): C++20" + # shared: true + # build-type: "RelWithDebInfo" + # asan: true + # ubsan: true # + # - compiler: "apple-clang" + # version: "*" + # cxxstd: "20" + # latest-cxxstd: "20" + # cxx: "clang++" + # cc: "clang" + # runs-on: "macos-14" + # b2-toolset: "clang" + # name: "Apple-Clang (macOS 14): C++20" + # shared: true + # build-type: "Release" + # build-cmake: true + + # Linux GCC (4 configurations) - compiler: "gcc" version: "15" - cxxstd: "17,20" - latest-cxxstd: "20" - cxx: "g++-15" - cc: "gcc-15" - runs-on: "ubuntu-latest" - container: "ubuntu:25.04" - b2-toolset: "gcc" - is-latest: true - name: "GCC 15: C++17-20" - shared: false - build-type: "Release" - build-cmake: true - - - compiler: "gcc" - version: "15" - cxxstd: "17,20" - latest-cxxstd: "20" - cxx: "g++-15" - cc: "gcc-15" - runs-on: "ubuntu-latest" - container: "ubuntu:25.04" - b2-toolset: "gcc" - is-latest: true - name: "GCC 15: C++17-20 (no zlib)" - shared: true - build-type: "Release" - build-cmake: true - - - compiler: "gcc" - version: "15" - cxxstd: "17,20" - latest-cxxstd: "20" - cxx: "g++-15" - cc: "gcc-15" - runs-on: "ubuntu-latest" - container: "ubuntu:25.04" - b2-toolset: "gcc" - is-latest: true - name: "GCC 15: C++17-20 (x86)" - shared: false - x86: true - build-type: "Release" - install: "gcc-15-multilib g++-15-multilib" - - - compiler: "gcc" - version: "15" - cxxstd: "17,20" + cxxstd: "20" latest-cxxstd: "20" cxx: "g++-15" cc: "gcc-15" @@ -275,31 +146,14 @@ jobs: container: "ubuntu:25.04" b2-toolset: "gcc" is-latest: true - name: "GCC 15: C++17-20" + name: "GCC 15: C++20" shared: true build-type: "Release" build-cmake: true - compiler: "gcc" version: "15" - cxxstd: "17,20" - latest-cxxstd: "20" - cxx: "g++-15" - cc: "gcc-15" - runs-on: "ubuntu-latest" - container: "ubuntu:25.04" - b2-toolset: "gcc" - is-latest: true - name: "GCC 15: C++17-20 (x86)" - shared: false - x86: true - build-type: "Release" - install: "gcc-15-multilib g++-15-multilib" - build-cmake: true - - - compiler: "gcc" - version: "15" - cxxstd: "17,20" + cxxstd: "20" latest-cxxstd: "20" cxx: "g++-15" cc: "gcc-15" @@ -307,94 +161,34 @@ jobs: container: "ubuntu:25.04" b2-toolset: "gcc" is-latest: true - name: "GCC 15: C++17-20 (asan)" + name: "GCC 15: C++20 (asan+ubsan)" shared: true asan: true - build-type: "RelWithDebInfo" - - - compiler: "gcc" - version: "15" - cxxstd: "17,20" - latest-cxxstd: "20" - cxx: "g++-15" - cc: "gcc-15" - runs-on: "ubuntu-latest" - container: "ubuntu:25.04" - b2-toolset: "gcc" - is-latest: true - name: "GCC 15: C++17-20 (asan, x86)" - shared: false - asan: true - x86: true - build-type: "RelWithDebInfo" - install: "gcc-15-multilib g++-15-multilib" - - - compiler: "gcc" - version: "15" - cxxstd: "17,20" - latest-cxxstd: "20" - cxx: "g++-15" - cc: "gcc-15" - runs-on: "ubuntu-latest" - container: "ubuntu:25.04" - b2-toolset: "gcc" - is-latest: true - name: "GCC 15: C++17-20 (ubsan)" - shared: true ubsan: true build-type: "RelWithDebInfo" - compiler: "gcc" - version: "15" - cxxstd: "17,20" + version: "12" + cxxstd: "20" latest-cxxstd: "20" - cxx: "g++-15" - cc: "gcc-15" + cxx: "g++-12" + cc: "gcc-12" runs-on: "ubuntu-latest" - container: "ubuntu:25.04" - b2-toolset: "gcc" - is-latest: true - name: "GCC 15: C++17-20 (ubsan, x86)" - shared: false - ubsan: true - x86: true - build-type: "RelWithDebInfo" - install: "gcc-15-multilib g++-15-multilib" - - - compiler: "gcc" - version: "14" - cxxstd: "17,20" - latest-cxxstd: "20" - cxx: "g++-14" - cc: "gcc-14" - runs-on: "ubuntu-24.04" - b2-toolset: "gcc" - name: "GCC 14: C++17-20" - shared: true - build-type: "Release" - - - compiler: "gcc" - version: "13" - cxxstd: "17,20" - latest-cxxstd: "20" - cxx: "g++-13" - cc: "gcc-13" - runs-on: "ubuntu-24.04" + container: "ubuntu:22.04" b2-toolset: "gcc" - name: "GCC 13: C++17-20" + name: "GCC 12: C++20" shared: true build-type: "Release" - compiler: "gcc" version: "13" - cxxstd: "17,20" + cxxstd: "20" latest-cxxstd: "20" cxx: "g++-13" cc: "gcc-13" runs-on: "ubuntu-24.04" b2-toolset: "gcc" - is-latest: true - name: "GCC 13: C++17-20 (coverage)" + name: "GCC 13: C++20 (coverage)" shared: false coverage: true build-type: "Debug" @@ -402,114 +196,11 @@ jobs: ccflags: "--coverage -fprofile-arcs -ftest-coverage" install: "lcov wget unzip" - - compiler: "gcc" - version: "12" - cxxstd: "17,20" - latest-cxxstd: "20" - cxx: "g++-12" - cc: "gcc-12" - runs-on: "ubuntu-latest" - container: "ubuntu:22.04" - b2-toolset: "gcc" - name: "GCC 12: C++17-20" - shared: true - build-type: "Release" - - - compiler: "gcc" - version: "11" - cxxstd: "17,20" - latest-cxxstd: "20" - cxx: "g++-11" - cc: "gcc-11" - runs-on: "ubuntu-latest" - container: "ubuntu:22.04" - b2-toolset: "gcc" - name: "GCC 11: C++17-20" - shared: false - build-type: "Release" - - - compiler: "gcc" - version: "10" - cxxstd: "14,17" - latest-cxxstd: "17" - cxx: "g++-10" - cc: "gcc-10" - runs-on: "ubuntu-latest" - container: "ubuntu:20.04" - b2-toolset: "gcc" - name: "GCC 10: C++14-17" - shared: true - build-type: "Release" - - - compiler: "gcc" - version: "9" - cxxstd: "14,17" - latest-cxxstd: "17" - cxx: "g++-9" - cc: "gcc-9" - runs-on: "ubuntu-latest" - container: "ubuntu:20.04" - b2-toolset: "gcc" - name: "GCC 9: C++14-17" - shared: false - build-type: "Release" - - - compiler: "gcc" - version: "8" - cxxstd: "14,17" - latest-cxxstd: "17" - cxx: "g++-8" - cc: "gcc-8" - runs-on: "ubuntu-latest" - container: "ubuntu:20.04" - b2-toolset: "gcc" - name: "GCC 8: C++14-17" - shared: true - build-type: "Release" - - - compiler: "gcc" - version: "7" - cxxstd: "14,17" - latest-cxxstd: "17" - cxx: "g++-7" - cc: "gcc-7" - runs-on: "ubuntu-latest" - container: "ubuntu:20.04" - b2-toolset: "gcc" - name: "GCC 7: C++14-17" - shared: false - build-type: "Release" - - - compiler: "gcc" - version: "6" - cxxstd: "11,14" - latest-cxxstd: "14" - cxx: "g++-6" - cc: "gcc-6" - runs-on: "ubuntu-latest" - container: "ubuntu:18.04" - b2-toolset: "gcc" - name: "GCC 6: C++11-14" - shared: true - build-type: "Release" - - - compiler: "gcc" - version: "5" - cxxstd: "11" - latest-cxxstd: "11" - cxx: "g++-5" - cc: "gcc-5" - runs-on: "ubuntu-latest" - container: "ubuntu:18.04" - b2-toolset: "gcc" - is-earliest: true - name: "GCC 5: C++11" - shared: false - build-type: "Release" + # Linux Clang (5 configurations) - compiler: "clang" version: "20" - cxxstd: "23,2c" + cxxstd: "20,23" latest-cxxstd: "23" cxx: "clang++-20" cc: "clang-20" @@ -517,27 +208,11 @@ jobs: container: "ubuntu:24.04" b2-toolset: "clang" is-latest: true - name: "Clang 20: C++23-2C" + name: "Clang 20: C++20-23" shared: true build-type: "Release" build-cmake: true - - compiler: "clang" - version: "20" - cxxstd: "23,2c" - latest-cxxstd: "23" - cxx: "clang++-20" - cc: "clang-20" - runs-on: "ubuntu-latest" - container: "ubuntu:24.04" - b2-toolset: "clang" - is-latest: true - name: "Clang 20: C++23-2C (x86)" - shared: false - x86: true - build-type: "Release" - install: "gcc-multilib g++-multilib" - - compiler: "clang" version: "20" cxxstd: "20" @@ -548,342 +223,63 @@ jobs: container: "ubuntu:24.04" b2-toolset: "clang" is-latest: true - name: "Clang 20: C++20 (time-trace)" - shared: true - time-trace: true - build-type: "Release" - cxxflags: "-ftime-trace" - ccflags: "-ftime-trace" - install: " wget unzip" - - - compiler: "clang" - version: "20" - cxxstd: "17,20" - latest-cxxstd: "20" - cxx: "clang++-20" - cc: "clang-20" - runs-on: "ubuntu-latest" - container: "ubuntu:24.04" - b2-toolset: "clang" - is-latest: true - name: "Clang 20: C++17-20 (asan)" + name: "Clang 20: C++20 (asan+ubsan)" shared: false asan: true - build-type: "RelWithDebInfo" - - - compiler: "clang" - version: "20" - cxxstd: "17,20" - latest-cxxstd: "20" - cxx: "clang++-20" - cc: "clang-20" - runs-on: "ubuntu-latest" - container: "ubuntu:24.04" - b2-toolset: "clang" - is-latest: true - name: "Clang 20: C++17-20 (asan, x86)" - shared: true - asan: true - x86: true - build-type: "RelWithDebInfo" - install: "gcc-multilib g++-multilib" - - - compiler: "clang" - version: "20" - cxxstd: "23,2c" - latest-cxxstd: "23" - cxx: "clang++-20" - cc: "clang-20" - runs-on: "ubuntu-latest" - container: "ubuntu:24.04" - b2-toolset: "clang" - is-latest: true - name: "Clang 20: C++23-2C (ubsan)" - shared: true - ubsan: true - build-type: "RelWithDebInfo" - - - compiler: "clang" - version: "20" - cxxstd: "23,2c" - latest-cxxstd: "23" - cxx: "clang++-20" - cc: "clang-20" - runs-on: "ubuntu-latest" - container: "ubuntu:24.04" - b2-toolset: "clang" - is-latest: true - name: "Clang 20: C++23-2C (ubsan, x86)" - shared: false ubsan: true - x86: true build-type: "RelWithDebInfo" - install: "gcc-multilib g++-multilib" - - - compiler: "clang" - version: "19" - cxxstd: "20,2b" - latest-cxxstd: "20" - cxx: "clang++-19" - cc: "clang-19" - runs-on: "ubuntu-24.04" - b2-toolset: "clang" - name: "Clang 19: C++20-2b" - shared: false - build-type: "Release" - - - compiler: "clang" - version: "18" - cxxstd: "20,2b" - latest-cxxstd: "20" - cxx: "clang++-18" - cc: "clang-18" - runs-on: "ubuntu-24.04" - b2-toolset: "clang" - name: "Clang 18: C++20-2b" - shared: true - build-type: "Release" - compiler: "clang" version: "17" - cxxstd: "17,20" + cxxstd: "20" latest-cxxstd: "20" cxx: "clang++-17" cc: "clang-17" runs-on: "ubuntu-24.04" b2-toolset: "clang" - name: "Clang 17: C++17-20" - shared: false - build-type: "Release" - - - compiler: "clang" - version: "16" - cxxstd: "17,20" - latest-cxxstd: "20" - cxx: "clang++-16" - cc: "clang-16" - runs-on: "ubuntu-24.04" - b2-toolset: "clang" - name: "Clang 16: C++17-20" - shared: true - build-type: "Release" - - - compiler: "clang" - version: "15" - cxxstd: "17,20" - latest-cxxstd: "20" - cxx: "clang++-15" - cc: "clang-15" - runs-on: "ubuntu-latest" - container: "ubuntu:22.04" - b2-toolset: "clang" - name: "Clang 15: C++17-20" - shared: false - build-type: "Release" - - - compiler: "clang" - version: "14" - cxxstd: "17,20" - latest-cxxstd: "20" - cxx: "clang++-14" - cc: "clang-14" - runs-on: "ubuntu-latest" - container: "ubuntu:22.04" - b2-toolset: "clang" - name: "Clang 14: C++17-20" - shared: true - build-type: "Release" - - - compiler: "clang" - version: "13" - cxxstd: "17,20" - latest-cxxstd: "20" - cxx: "clang++-13" - cc: "clang-13" - runs-on: "ubuntu-latest" - container: "ubuntu:22.04" - b2-toolset: "clang" - name: "Clang 13: C++17-20" - shared: false - build-type: "Release" - - - compiler: "clang" - version: "12" - cxxstd: "17,20" - latest-cxxstd: "20" - cxx: "clang++-12" - cc: "clang-12" - runs-on: "ubuntu-latest" - container: "ubuntu:22.04" - b2-toolset: "clang" - name: "Clang 12: C++17-20" - shared: true - build-type: "Release" - - - compiler: "clang" - version: "11" - cxxstd: "14,17" - latest-cxxstd: "17" - cxx: "clang++-11" - cc: "clang-11" - runs-on: "ubuntu-latest" - container: "ubuntu:22.04" - b2-toolset: "clang" - name: "Clang 11: C++14-17" - shared: false - build-type: "Release" - - - compiler: "clang" - version: "10" - cxxstd: "14,17" - latest-cxxstd: "17" - cxx: "clang++-10" - cc: "clang-10" - runs-on: "ubuntu-latest" - container: "ubuntu:20.04" - b2-toolset: "clang" - name: "Clang 10: C++14-17" - shared: true - build-type: "Release" - - - compiler: "clang" - version: "9" - cxxstd: "14,17" - latest-cxxstd: "17" - cxx: "clang++-9" - cc: "clang-9" - runs-on: "ubuntu-latest" - container: "ubuntu:20.04" - b2-toolset: "clang" - name: "Clang 9: C++14-17" - shared: false - build-type: "Release" - - - compiler: "clang" - version: "8" - cxxstd: "14,17" - latest-cxxstd: "17" - cxx: "clang++-8" - cc: "clang-8" - runs-on: "ubuntu-latest" - container: "ubuntu:20.04" - b2-toolset: "clang" - name: "Clang 8: C++14-17" - shared: true - build-type: "Release" - - - compiler: "clang" - version: "7" - cxxstd: "14,17" - latest-cxxstd: "17" - cxx: "clang++-7" - cc: "clang-7" - runs-on: "ubuntu-latest" - container: "ubuntu:20.04" - b2-toolset: "clang" - name: "Clang 7: C++14-17" + name: "Clang 17: C++20" shared: false build-type: "Release" - compiler: "clang" - version: "6" - cxxstd: "14,17" - latest-cxxstd: "17" - cxx: "clang++-6.0" - cc: "clang-6.0" - runs-on: "ubuntu-latest" - container: "ubuntu:20.04" - b2-toolset: "clang" - name: "Clang 6: C++14-17" - shared: true - build-type: "Release" - - - compiler: "clang" - version: "5" - cxxstd: "11,14" - latest-cxxstd: "14" - cxx: "clang++-5.0" - cc: "clang-5.0" - runs-on: "ubuntu-latest" - container: "ubuntu:18.04" - b2-toolset: "clang" - name: "Clang 5: C++11-14" - shared: false - build-type: "Release" - - - compiler: "clang" - version: "4" - cxxstd: "11,14" - latest-cxxstd: "14" - cxx: "clang++-4.0" - cc: "clang-4.0" - runs-on: "ubuntu-latest" - container: "ubuntu:18.04" - b2-toolset: "clang" - name: "Clang 4: C++11-14" - shared: true - build-type: "Release" - - - compiler: "clang" - version: "3.9" - cxxstd: "11" - latest-cxxstd: "11" - cxx: "clang++-3.9" - cc: "clang-3.9" + version: "20" + cxxstd: "20,23" + latest-cxxstd: "23" + cxx: "clang++-20" + cc: "clang-20" runs-on: "ubuntu-latest" - container: "ubuntu:18.04" + container: "ubuntu:24.04" b2-toolset: "clang" - is-earliest: true - name: "Clang 3.9: C++11" + is-latest: true + name: "Clang 20: C++20-23 (x86)" shared: false + x86: true build-type: "Release" + install: "gcc-multilib g++-multilib" name: ${{ matrix.name }} - runs-on: ${{ fromJSON(needs.runner-selection.outputs.labelmatrix)[matrix.runs-on] }} + # Skip self-hosted runner selection for now + # runs-on: ${{ fromJSON(needs.runner-selection.outputs.labelmatrix)[matrix.runs-on] }} + runs-on: ${{ matrix.runs-on }} container: image: ${{ matrix.container }} options: --privileged - volumes: - - /node20217:/node20217:rw,rshared - - ${{ startsWith(matrix.container, 'ubuntu:1') && '/node20217:/__e/node20:ro,rshared' || ' ' }} + timeout-minutes: 120 steps: - - name: install nodejs20glibc2.17 - if: ${{ startsWith( matrix.container, 'ubuntu:1' ) }} - run: | - apt-get update - apt-get -yqq install xz-utils curl - curl -LO https://archives.boost.io/misc/node/node-v20.9.0-linux-x64-glibc-217.tar.xz - tar -xf node-v20.9.0-linux-x64-glibc-217.tar.xz --strip-components 1 -C /node20217 - ldd /__e/node20/bin/node - - - name: Clone Boost.HTTP - uses: actions/checkout@v3 + - name: Clone Boost.Corosio + uses: actions/checkout@v4 with: path: http-root - - name: Clone Boost.Buffers - uses: actions/checkout@v3 - with: - path: buffers-root - repository: cppalliance/buffers - ref: develop - - - name: Clone Boost.Capy - uses: actions/checkout@v3 - with: - path: capy-root - repository: cppalliance/capy - ref: develop - - name: Setup C++ uses: alandefreitas/cpp-actions/setup-cpp@v1.9.0 id: setup-cpp with: compiler: ${{ matrix.compiler }} version: ${{ matrix.version }} - check-latest: ${{ matrix.compiler != 'clang' || matrix.version != '^3.8' }} + check-latest: true trace-commands: true - name: Install packages @@ -893,8 +289,15 @@ jobs: apt-get-add-architecture: ${{ matrix.x86 && 'i386' || '' }} apt-get: >- ${{ matrix.install }} - build-essential zlib1g-dev libbrotli-dev - ${{ matrix.x86 && 'zlib1g-dev:i386 libbrotli-dev:i386' || '' }} + build-essential + ${{ matrix.x86 && '' || '' }} + + - name: Clone Capy + uses: actions/checkout@v4 + with: + repository: cppalliance/capy + ref: ${{ (github.ref_name == 'master' && github.ref_name) || 'develop' }} + path: capy-root - name: Clone Boost uses: alandefreitas/cpp-actions/boost-clone@v1.9.0 @@ -903,45 +306,8 @@ jobs: branch: ${{ (github.ref_name == 'master' && github.ref_name) || 'develop' }} boost-dir: boost-source modules-exclude-paths: '' - scan-modules-dir: | - http-root - buffers-root - capy-root - scan-modules-ignore: | - http - buffers - capy - - - name: Install Packages (Windows) - uses: alandefreitas/cpp-actions/package-install@v1.9.0 - if: ${{ startsWith(matrix.runs-on, 'windows') }} - id: package-install-windows - with: - vcpkg: zlib brotli - vcpkg-dir: vcpkg-root - vcpkg-triplet: ${{ matrix.x86 && 'x86-windows-static' || 'x64-windows' }} - - - name: Patch user-config.jam (Windows) - id: patch-user-config - shell: bash - if: ${{ startsWith(matrix.runs-on, 'windows') }} - run: | - set -xe - home=$(pwd) - - triplet=${{ matrix.x86 && 'x86-windows-static' || 'x64-windows' }} - addrmdl=${{ matrix.x86 && '32' || '64' }} - - # This is temporary until we move capy/build/brotli.jam to boost/tools/build - echo "import-search ${home}/boost-root/libs/capy/build ;" | sed 's/\/d\//D:\//g' >> user-config.jam - - echo "using zlib : : \"${home}/vcpkg-root/installed/${triplet}/include\" \"${home}/vcpkg-root/installed/${triplet}/lib\" \"${home}/vcpkg-root/installed/${triplet}/bin\" zlib : ${addrmdl} ;" | sed 's/\/d\//D:\//g' >> user-config.jam - echo "using brotli : : \"${home}/vcpkg-root/installed/${triplet}/include\" \"${home}/vcpkg-root/installed/${triplet}/lib\" \"${home}/vcpkg-root/installed/${triplet}/bin\" : ${addrmdl} ;" | sed 's/\/d\//D:\//g' >> user-config.jam - - cat user-config.jam - - toolchain=$(echo "$GITHUB_WORKSPACE/vcpkg-root/scripts/buildsystems/vcpkg.cmake" | sed 's/\/d\//D:\//g' ) - echo "toolchain=${toolchain}" >> $GITHUB_OUTPUT + scan-modules-dir: http-root + scan-modules-ignore: http,capy - name: ASLR Fix if: ${{ startsWith(matrix.runs-on, 'ubuntu' )}} @@ -967,6 +333,7 @@ jobs: # Remove module from boost-source rm -r "boost-source/libs/$module" || true + rm -r "boost-source/libs/capy" || true # Copy cached boost-source to an isolated boost-root cp -r boost-source boost-root @@ -979,12 +346,13 @@ jobs: # Patch boost-root with workspace module cp -r "$workspace_root"/http-root "libs/$module" - cp -r "$workspace_root"/buffers-root libs/buffers - cp -r "$workspace_root"/capy-root libs/capy + + # Patch boost-root with capy dependency + cp -r "$workspace_root"/capy-root "libs/capy" - name: Boost B2 Workflow uses: alandefreitas/cpp-actions/b2-workflow@v1.9.0 - if: ${{ !matrix.coverage && !matrix.time-trace }} + if: ${{ !matrix.coverage }} env: ASAN_OPTIONS: ${{ ((matrix.compiler == 'apple-clang' || matrix.compiler == 'clang') && 'detect_invalid_pointer_pairs=0:strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1') || 'detect_invalid_pointer_pairs=2:strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1' }} with: @@ -998,15 +366,13 @@ jobs: asan: ${{ matrix.asan }} ubsan: ${{ matrix.ubsan }} shared: ${{ matrix.shared }} - rtti: ${{ (matrix.is-latest && 'on,off') || 'on' }} + rtti: on cxxflags: ${{ (matrix.asan && '-fsanitize-address-use-after-scope -fsanitize=pointer-subtract') || '' }} - user-config: ${{ (startsWith(matrix.runs-on, 'windows') && format('{0}/user-config.jam', steps.patch.outputs.workspace_root)) || '' }} stop-on-error: true - extra-args: ${{ (matrix.valgrind && 'testing.launcher=valgrind' || '' )}} - name: Boost CMake Workflow uses: alandefreitas/cpp-actions/cmake-workflow@v1.9.0 - if: ${{ matrix.coverage || matrix.time-trace || matrix.build-cmake || matrix.is-earliest }} + if: ${{ matrix.coverage || matrix.build-cmake || matrix.is-earliest }} with: source-dir: boost-root build-dir: __build_cmake_test__ @@ -1026,11 +392,9 @@ jobs: extra-args: | -D Boost_VERBOSE=ON -D BOOST_INCLUDE_LIBRARIES="${{ steps.patch.outputs.module }}" - export-compile-commands: ${{ matrix.time-trace }} package: false package-artifact: false ref-source-dir: boost-root/libs/http - toolchain: ${{ (startsWith(matrix.runs-on, 'windows') && steps.patch-user-config.outputs.toolchain) || '' }} - name: Set Path if: startsWith(matrix.runs-on, 'windows') && matrix.shared @@ -1041,50 +405,48 @@ jobs: run: | echo "LD_LIBRARY_PATH=$GITHUB_WORKSPACE/.local/lib:$LD_LIBRARY_PATH" >> "$GITHUB_ENV" -# - name: Find Package Integration Workflow -# uses: alandefreitas/cpp-actions/cmake-workflow@v1.9.0 -# if: ${{ matrix.build-cmake || matrix.is-earliest }} -# with: -# source-dir: boost-root/libs/${{ steps.patch.outputs.module }}/test/cmake_test -# build-dir: __build_cmake_install_test__ -# generator: ${{ matrix.generator }} -# generator-toolset: ${{ matrix.generator-toolset }} -# build-type: ${{ matrix.build-type }} -# cxxstd: ${{ matrix.latest-cxxstd }} -# cc: ${{ steps.setup-cpp.outputs.cc || matrix.cc }} -# ccflags: ${{ matrix.ccflags }} -# cxx: ${{ steps.setup-cpp.outputs.cxx || matrix.cxx }} -# cxxflags: ${{ matrix.cxxflags }} -# shared: ${{ matrix.shared }} -# install: false -# cmake-version: '>=3.20' -# extra-args: | -# -D BOOST_CI_INSTALL_TEST=ON -# -D CMAKE_PREFIX_PATH=${{ steps.patch.outputs.workspace_root }}/.local -# ref-source-dir: boost-root/libs/http -# trace-commands: true -# toolchain: ${{ (startsWith(matrix.runs-on, 'windows') && steps.patch-user-config.outputs.toolchain) || '' }} - -# - name: Subdirectory Integration Workflow -# uses: alandefreitas/cpp-actions/cmake-workflow@v1.9.0 -# if: ${{ matrix.build-cmake || matrix.is-earliest }} -# with: -# source-dir: boost-root/libs/${{ steps.patch.outputs.module }}/test/cmake_test -# build-dir: __build_cmake_subdir_test__ -# generator: ${{ matrix.generator }} -# generator-toolset: ${{ matrix.generator-toolset }} -# build-type: ${{ matrix.build-type }} -# cxxstd: ${{ matrix.latest-cxxstd }} -# cc: ${{ steps.setup-cpp.outputs.cc || matrix.cc }} -# ccflags: ${{ matrix.ccflags }} -# cxx: ${{ steps.setup-cpp.outputs.cxx || matrix.cxx }} -# cxxflags: ${{ matrix.cxxflags }} -# shared: ${{ matrix.shared }} -# install: false -# cmake-version: '>=3.20' -# extra-args: -D BOOST_CI_INSTALL_TEST=OFF -# ref-source-dir: boost-root/libs/http/test/cmake_test -# toolchain: ${{ (startsWith(matrix.runs-on, 'windows') && steps.patch-user-config.outputs.toolchain) || '' }} + - name: Find Package Integration Workflow + uses: alandefreitas/cpp-actions/cmake-workflow@v1.9.0 + if: ${{ matrix.build-cmake || matrix.is-earliest }} + with: + source-dir: boost-root/libs/${{ steps.patch.outputs.module }}/test/cmake_test + build-dir: __build_cmake_install_test__ + generator: ${{ matrix.generator }} + generator-toolset: ${{ matrix.generator-toolset }} + build-type: ${{ matrix.build-type }} + cxxstd: ${{ matrix.latest-cxxstd }} + cc: ${{ steps.setup-cpp.outputs.cc || matrix.cc }} + ccflags: ${{ matrix.ccflags }} + cxx: ${{ steps.setup-cpp.outputs.cxx || matrix.cxx }} + cxxflags: ${{ matrix.cxxflags }} + shared: ${{ matrix.shared }} + install: false + cmake-version: '>=3.15' + extra-args: | + -D BOOST_CI_INSTALL_TEST=ON + -D CMAKE_PREFIX_PATH=${{ steps.patch.outputs.workspace_root }}/.local + ref-source-dir: boost-root/libs/http + trace-commands: true + + - name: Subdirectory Integration Workflow + uses: alandefreitas/cpp-actions/cmake-workflow@v1.9.0 + if: ${{ matrix.build-cmake || matrix.is-earliest }} + with: + source-dir: boost-root/libs/${{ steps.patch.outputs.module }}/test/cmake_test + build-dir: __build_cmake_subdir_test__ + generator: ${{ matrix.generator }} + generator-toolset: ${{ matrix.generator-toolset }} + build-type: ${{ matrix.build-type }} + cxxstd: ${{ matrix.latest-cxxstd }} + cc: ${{ steps.setup-cpp.outputs.cc || matrix.cc }} + ccflags: ${{ matrix.ccflags }} + cxx: ${{ steps.setup-cpp.outputs.cxx || matrix.cxx }} + cxxflags: ${{ matrix.cxxflags }} + shared: ${{ matrix.shared }} + install: false + cmake-version: '>=3.15' + extra-args: -D BOOST_CI_INSTALL_TEST=OFF + ref-source-dir: boost-root/libs/http/test/cmake_test - name: Root Project CMake Workflow uses: alandefreitas/cpp-actions/cmake-workflow@v1.9.0 @@ -1108,15 +470,6 @@ jobs: package: false package-artifact: false ref-source-dir: boost-root - toolchain: ${{ (startsWith(matrix.runs-on, 'windows') && steps.patch-user-config.outputs.toolchain) || '' }} - - - name: FlameGraph - uses: alandefreitas/cpp-actions/flamegraph@v1.9.0 - if: matrix.time-trace - with: - source-dir: boost-root/libs/http - build-dir: boost-root/__build_cmake_test__ - github_token: ${{ secrets.GITHUB_TOKEN }} - name: Codecov if: ${{ matrix.coverage }} @@ -1147,18 +500,20 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY changelog: - needs: [ runner-selection ] + # needs: [ runner-selection ] defaults: run: shell: bash name: Changelog Summary - runs-on: ${{ fromJSON(needs.runner-selection.outputs.labelmatrix)['ubuntu-22.04'] }} + # Skip self-hosted runner selection for now + # runs-on: ${{ fromJSON(needs.runner-selection.outputs.labelmatrix)['ubuntu-22.04'] }} + runs-on: 'ubuntu-22.04' timeout-minutes: 120 steps: - - name: Clone Boost.HTTP - uses: actions/checkout@v3 + - name: Clone Boost.Corosio + uses: actions/checkout@v4 with: # Avoid the common API rate limit exceeded error in boostorg by including 100 latest commits in any case fetch-depth: 100 @@ -1172,16 +527,11 @@ jobs: tag-pattern: 'boost-.*\..*\..*' antora: - needs: [ runner-selection ] - strategy: - fail-fast: false - matrix: - include: - - { name: Windows, os: windows-latest } - - { name: Ubuntu, os: ubuntu-latest } - - { name: MacOS, os: macos-15 } - name: Antora Docs (${{ matrix.name }}) - runs-on: ${{ fromJSON(needs.runner-selection.outputs.labelmatrix)[matrix.os] }} + # needs: [ runner-selection ] + name: Antora Docs + # Skip self-hosted runner selection for now + # runs-on: ${{ fromJSON(needs.runner-selection.outputs.labelmatrix)['ubuntu-latest'] }} + runs-on: 'ubuntu-latest' defaults: run: shell: bash @@ -1191,24 +541,17 @@ jobs: with: apt-get: git cmake - - name: Clone Boost.HTTP - uses: actions/checkout@v3 + - name: Clone Boost.Corosio + uses: actions/checkout@v4 with: path: http-root - - name: Clone Boost.Buffers - uses: actions/checkout@v3 - with: - path: buffers-root - repository: cppalliance/buffers - ref: develop - - - name: Clone Boost.Capy - uses: actions/checkout@v3 + - name: Clone Capy + uses: actions/checkout@v4 with: - path: capy-root repository: cppalliance/capy - ref: develop + ref: ${{ (github.ref_name == 'master' && github.ref_name) || 'develop' }} + path: capy-root - name: Clone Boost uses: alandefreitas/cpp-actions/boost-clone@v1.9.0 @@ -1217,14 +560,8 @@ jobs: branch: ${{ (github.ref_name == 'master' && github.ref_name) || 'develop' }} boost-dir: boost-source modules-exclude-paths: '' - scan-modules-dir: | - http-root - buffers-root - capy-root - scan-modules-ignore: | - http - buffers - capy + scan-modules-dir: http-root + scan-modules-ignore: http,capy - name: Patch Boost id: patch @@ -1245,6 +582,7 @@ jobs: # Remove module from boost-source rm -r "boost-source/libs/$module" || true + rm -r "boost-source/libs/capy" || true # Copy cached boost-source to an isolated boost-root cp -r boost-source boost-root @@ -1257,30 +595,27 @@ jobs: # Patch boost-root with workspace module cp -r "$workspace_root"/http-root "libs/$module" - cp -r "$workspace_root"/buffers-root libs/buffers - cp -r "$workspace_root"/capy-root libs/capy + + # Patch boost-root with capy dependency + cp -r "$workspace_root"/capy-root "libs/capy" - uses: actions/setup-node@v4 with: node-version: 18 - - name: Setup Ninja - if: runner.os == 'Windows' - uses: seanmiddleditch/gha-setup-ninja@v5 - - name: Build Antora Docs env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | git config --global --add safe.directory "$(pwd)" - + BOOST_SRC_DIR="$(pwd)/boost-root" export BOOST_SRC_DIR cd boost-root/libs/http - + cd doc bash ./build_antora.sh - + # Antora returns zero even if it fails, so we check if the site directory exists if [ ! -d "build/site" ]; then echo "Antora build failed" @@ -1290,5 +625,5 @@ jobs: - name: Create Antora Docs Artifact uses: actions/upload-artifact@v4 with: - name: antora-docs-${{ matrix.name }} + name: antora-docs path: boost-root/libs/http/doc/build/site diff --git a/test/limits/Jamfile b/test/limits/Jamfile index 5916b905..95feb4b2 100644 --- a/test/limits/Jamfile +++ b/test/limits/Jamfile @@ -18,7 +18,6 @@ project . ../.. ../../../url/extra/test_suite - /boost//buffers /boost//capy /boost//url ; From ffcff423357964c50cd16ef071d593430ce85c7e Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Sun, 18 Jan 2026 10:23:52 -0800 Subject: [PATCH 04/12] Refactor execution --- include/boost/http/impl/parser.hpp | 8 ++++---- include/boost/http/impl/serializer.hpp | 2 +- include/boost/http/parser.hpp | 2 +- include/boost/http/serializer.hpp | 4 ++-- include/boost/http/server/route_handler.hpp | 2 +- include/boost/http/sink.hpp | 2 +- include/boost/http/source.hpp | 2 +- src/parser.cpp | 6 +++--- src/server/route_handler.cpp | 16 ++-------------- 9 files changed, 16 insertions(+), 28 deletions(-) diff --git a/include/boost/http/impl/parser.hpp b/include/boost/http/impl/parser.hpp index bfc3bcf1..7b04d80f 100644 --- a/include/boost/http/impl/parser.hpp +++ b/include/boost/http/impl/parser.hpp @@ -102,7 +102,7 @@ set_body( // Check ElasticBuffer type requirements static_assert( - capy::is_dynamic_buffer::value, + capy::is_DynamicBuffer::value, "Type requirements not met."); // body must not already be set @@ -114,7 +114,7 @@ set_body( detail::throw_logic_error(); auto& dyn = ws().emplace< - capy::any_dynamic_buffer_impl::type, buffers_N>>(std::forward(eb)); @@ -129,7 +129,7 @@ set_body( { // Check ElasticBuffer type requirements static_assert( - capy::is_dynamic_buffer::value, + capy::is_DynamicBuffer::value, "Type requirements not met."); // body must not already be set @@ -142,7 +142,7 @@ set_body( // Use dynamic_buffer_ref to provide reference semantics auto& dyn = ws().emplace< - capy::any_dynamic_buffer_impl< + capy::any_DynamicBuffer_impl< detail::dynamic_buffer_ref, buffers_N>>(eb.get()); diff --git a/include/boost/http/impl/serializer.hpp b/include/boost/http/impl/serializer.hpp index 76860d0b..9b38c404 100644 --- a/include/boost/http/impl/serializer.hpp +++ b/include/boost/http/impl/serializer.hpp @@ -121,7 +121,7 @@ start( ConstBufferSequence&& cbs) { static_assert( - capy::const_buffer_sequence, + capy::ConstBufferSequence, "ConstBufferSequence type requirements not met"); start_init(m); diff --git a/include/boost/http/parser.hpp b/include/boost/http/parser.hpp index eb2f2b7d..68278b2f 100644 --- a/include/boost/http/parser.hpp +++ b/include/boost/http/parser.hpp @@ -642,7 +642,7 @@ class parser BOOST_HTTP_DECL void - set_body_impl(capy::any_dynamic_buffer&) noexcept; + set_body_impl(capy::any_DynamicBuffer&) noexcept; BOOST_HTTP_DECL void diff --git a/include/boost/http/serializer.hpp b/include/boost/http/serializer.hpp index 8d04e51f..357614a2 100644 --- a/include/boost/http/serializer.hpp +++ b/include/boost/http/serializer.hpp @@ -246,7 +246,7 @@ class serializer @par Constraints @code - capy::const_buffer_sequence + capy::ConstBufferSequence @endcode @par Exception Safety @@ -275,7 +275,7 @@ class serializer template< class ConstBufferSequence, class = typename std::enable_if< - capy::const_buffer_sequence>::type + capy::ConstBufferSequence>::type > void start( diff --git a/include/boost/http/server/route_handler.hpp b/include/boost/http/server/route_handler.hpp index b5f0ba67..9f3c5b40 100644 --- a/include/boost/http/server/route_handler.hpp +++ b/include/boost/http/server/route_handler.hpp @@ -84,7 +84,7 @@ struct BOOST_HTTP_SYMBOL_VISIBLE /** Executor associated with the session. */ - capy::any_dispatcher ex; + capy::any_executor_ref ex; /** Destructor */ diff --git a/include/boost/http/sink.hpp b/include/boost/http/sink.hpp index 014625cb..90a8834d 100644 --- a/include/boost/http/sink.hpp +++ b/include/boost/http/sink.hpp @@ -108,7 +108,7 @@ struct BOOST_HTTP_SYMBOL_VISIBLE bool more) { static_assert( - capy::const_buffer_sequence, + capy::ConstBufferSequence, "Type requirements not met"); return write_impl(bs, more); diff --git a/include/boost/http/source.hpp b/include/boost/http/source.hpp index ba6e82be..3162a375 100644 --- a/include/boost/http/source.hpp +++ b/include/boost/http/source.hpp @@ -111,7 +111,7 @@ struct BOOST_HTTP_SYMBOL_VISIBLE read(MutableBufferSequence const& bs) { static_assert( - capy::mutable_buffer_sequence, + capy::MutableBufferSequence, "Type requirements not met"); return read_impl(bs); diff --git a/src/parser.cpp b/src/parser.cpp index da269bb4..4900637c 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -554,7 +554,7 @@ class parser::impl capy::const_buffer_pair cbp_; detail::filter* filter_; - capy::any_dynamic_buffer* eb_; + capy::any_DynamicBuffer* eb_; sink* sink_; state state_; @@ -1675,7 +1675,7 @@ class parser::impl void set_body( - capy::any_dynamic_buffer& eb) noexcept + capy::any_DynamicBuffer& eb) noexcept { eb_ = &eb; style_ = style::elastic; @@ -2051,7 +2051,7 @@ is_body_set() const noexcept void parser:: set_body_impl( - capy::any_dynamic_buffer& eb) noexcept + capy::any_DynamicBuffer& eb) noexcept { BOOST_ASSERT(impl_); impl_->set_body(eb); diff --git a/src/server/route_handler.cpp b/src/server/route_handler.cpp index b75e8fc8..11073e73 100644 --- a/src/server/route_handler.cpp +++ b/src/server/route_handler.cpp @@ -9,7 +9,7 @@ #include #include -#include +#include namespace boost { namespace http { @@ -56,19 +56,7 @@ spawn( capy::task t) -> route_result { - return this->suspend( - [ex = this->ex, t = std::move(t)](resumer resume) mutable - { - capy::run_async(ex, - [resume](route_result rv) - { - resume(rv); - }, - [resume](std::exception_ptr ep) - { - resume(ep); - })(std::move(t)); - }); + return capy::run_sync()(std::move(t)); } } // http From 97b9b8cd5cf2894e023cecc865debfed61980c10 Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Sun, 18 Jan 2026 11:24:01 -0800 Subject: [PATCH 05/12] Add bcrypt from Capy --- CMakeLists.txt | 7 + build/Jamfile | 7 + doc/modules/ROOT/nav.adoc | 2 + doc/modules/ROOT/pages/bcrypt.adoc | 290 ++++++++++++++++ include/boost/http.hpp | 1 + include/boost/http/bcrypt.hpp | 49 +++ include/boost/http/bcrypt/error.hpp | 92 +++++ include/boost/http/bcrypt/hash.hpp | 173 ++++++++++ include/boost/http/bcrypt/result.hpp | 123 +++++++ include/boost/http/bcrypt/version.hpp | 37 ++ src/bcrypt/base64.cpp | 183 ++++++++++ src/bcrypt/base64.hpp | 47 +++ src/bcrypt/blowfish.cpp | 477 ++++++++++++++++++++++++++ src/bcrypt/blowfish.hpp | 64 ++++ src/bcrypt/crypt.cpp | 206 +++++++++++ src/bcrypt/crypt.hpp | 79 +++++ src/bcrypt/error.cpp | 57 +++ src/bcrypt/hash.cpp | 222 ++++++++++++ src/bcrypt/random.cpp | 184 ++++++++++ src/bcrypt/random.hpp | 31 ++ test/unit/bcrypt.cpp | 290 ++++++++++++++++ test/unit/bcrypt/error.cpp | 11 + test/unit/bcrypt/hash.cpp | 11 + test/unit/bcrypt/result.cpp | 11 + test/unit/bcrypt/version.cpp | 11 + 25 files changed, 2665 insertions(+) create mode 100644 doc/modules/ROOT/pages/bcrypt.adoc create mode 100644 include/boost/http/bcrypt.hpp create mode 100644 include/boost/http/bcrypt/error.hpp create mode 100644 include/boost/http/bcrypt/hash.hpp create mode 100644 include/boost/http/bcrypt/result.hpp create mode 100644 include/boost/http/bcrypt/version.hpp create mode 100644 src/bcrypt/base64.cpp create mode 100644 src/bcrypt/base64.hpp create mode 100644 src/bcrypt/blowfish.cpp create mode 100644 src/bcrypt/blowfish.hpp create mode 100644 src/bcrypt/crypt.cpp create mode 100644 src/bcrypt/crypt.hpp create mode 100644 src/bcrypt/error.cpp create mode 100644 src/bcrypt/hash.cpp create mode 100644 src/bcrypt/random.cpp create mode 100644 src/bcrypt/random.hpp create mode 100644 test/unit/bcrypt.cpp create mode 100644 test/unit/bcrypt/error.cpp create mode 100644 test/unit/bcrypt/hash.cpp create mode 100644 test/unit/bcrypt/result.cpp create mode 100644 test/unit/bcrypt/version.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 64bdd68c..e6940f8f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -172,6 +172,13 @@ add_library(boost_http include/boost/http.hpp build/Jamfile ${BOOST_HTTP_HEADERS add_library(Boost::http ALIAS boost_http) boost_http_setup_properties(boost_http) +# bcrypt requires platform-specific libraries +if (WIN32) + target_link_libraries(boost_http PRIVATE bcrypt) +elseif (APPLE) + target_link_libraries(boost_http PRIVATE "-framework Security") +endif () + #------------------------------------------------- # # Tests diff --git a/build/Jamfile b/build/Jamfile index 22b0f23a..09acd3b7 100644 --- a/build/Jamfile +++ b/build/Jamfile @@ -36,6 +36,9 @@ alias http_sources : [ glob-tree-ex ./src : *.cpp ] ; explicit http_sources ; +# Windows CNG library for bcrypt random number generation. +lib bcrypt_sys : : bcrypt ; + lib boost_http : http_sources : requirements @@ -44,10 +47,14 @@ lib boost_http /boost//url ../ BOOST_HTTP_SOURCE + windows:bcrypt_sys + darwin:"-framework Security" : usage-requirements /boost//capy /boost/json//boost_json/off /boost//url + windows:bcrypt_sys + darwin:"-framework Security" ; boost-install boost_http ; diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index a97bbd4b..85541aeb 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -9,6 +9,8 @@ * xref:Message.adoc[] * Server ** xref:server/router.adoc[Router] +* Cryptography +** xref:bcrypt.adoc[BCrypt Password Hashing] // ** xref:server/middleware.adoc[Middleware] // ** xref:server/errors.adoc[Error Handling] // ** xref:server/params.adoc[Route Parameters] diff --git a/doc/modules/ROOT/pages/bcrypt.adoc b/doc/modules/ROOT/pages/bcrypt.adoc new file mode 100644 index 00000000..fe10b7f9 --- /dev/null +++ b/doc/modules/ROOT/pages/bcrypt.adoc @@ -0,0 +1,290 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + += BCrypt Password Hashing + +This page explains how to securely hash and verify passwords using bcrypt. + +NOTE: Code snippets assume `using namespace boost::http;` is in effect. + +== What is BCrypt? + +BCrypt is a password-hashing function designed by Niels Provos and David +Mazières. It incorporates: + +* A **salt** to protect against rainbow table attacks +* An **adaptive cost factor** that can be increased as hardware improves +* Built-in work factor that makes brute-force attacks expensive + +BCrypt is the recommended algorithm for password storage. + +== Quick Start + +[source,cpp] +---- +#include + +// Hash a password +bcrypt::result hash = bcrypt::hash("my_password", 12); + +// Store hash.str() in database... + +// Later, verify the password +system::error_code ec; +bool valid = bcrypt::compare("my_password", stored_hash, ec); + +if (ec) + // Hash was malformed +else if (valid) + // Password matches +else + // Password does not match +---- + +== Hashing Passwords + +The `hash` function generates a salted hash: + +[source,cpp] +---- +// Default cost factor (10) +bcrypt::result r1 = bcrypt::hash("password"); + +// Custom cost factor +bcrypt::result r2 = bcrypt::hash("password", 12); + +// Custom cost factor and version +bcrypt::result r3 = bcrypt::hash("password", 12, bcrypt::version::v2b); +---- + +=== Cost Factor + +The cost factor (rounds) determines how expensive hashing is. Each increment +doubles the work: + +[cols="1,2"] +|=== +| Cost | Approximate Time (modern CPU) + +| 10 +| ~100ms + +| 12 +| ~400ms + +| 14 +| ~1.6s + +| 16 +| ~6.4s +|=== + +**Guidelines:** + +* Minimum: 10 for new applications +* Recommended: 12 for most applications +* Maximum: 31 (impractically slow) +* Adjust based on your hardware and latency requirements + +=== Password Length Limit + +BCrypt only uses the first 72 bytes of a password. Longer passwords are +silently truncated. If you need to support longer passwords, pre-hash +with SHA-256: + +[source,cpp] +---- +// For passwords > 72 bytes +std::string pre_hash = sha256(long_password); +bcrypt::result r = bcrypt::hash(pre_hash, 12); +---- + +== Verifying Passwords + +The `compare` function extracts the salt from a stored hash, re-hashes +the input password, and compares: + +[source,cpp] +---- +system::error_code ec; +bool valid = bcrypt::compare(user_input, stored_hash, ec); + +if (ec == bcrypt::error::invalid_hash) +{ + // Hash string is malformed - data corruption or tampering + log_security_event("invalid hash format"); + return false; +} + +if (valid) + grant_access(); +else + reject_login(); +---- + +WARNING: Always check the error code. A false return value alone does not +distinguish between "wrong password" and "malformed hash". + +== Working with Salts + +You can generate and use salts separately: + +[source,cpp] +---- +// Generate a salt +bcrypt::result salt = bcrypt::gen_salt(12); + +// Hash with explicit salt +system::error_code ec; +bcrypt::result hash = bcrypt::hash("password", salt.str(), ec); +---- + +This is rarely needed since `hash()` generates a salt automatically. + +== The result Type + +`bcrypt::result` is a fixed-size buffer (no heap allocation): + +[source,cpp] +---- +bcrypt::result r = bcrypt::hash("password", 12); + +// Access the hash string +core::string_view sv = r.str(); // Or just use r (implicit conversion) +char const* cstr = r.c_str(); // Null-terminated + +// Check for valid result +if (r) + store(r.str()); +---- + +== Hash String Format + +A bcrypt hash string has this format: + +---- +$2b$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy +│ │ │ │ +│ │ │ └─ hash (31 chars) +│ │ └─ salt (22 chars) +│ └─ cost factor +└─ version +---- + +Total length: 60 characters. + +== Extracting the Cost Factor + +To check the cost factor of an existing hash: + +[source,cpp] +---- +system::error_code ec; +unsigned rounds = bcrypt::get_rounds(stored_hash, ec); + +if (ec) + // Invalid hash format +else if (rounds < 12) + // Consider re-hashing with higher cost +---- + +== Upgrading Cost Factor + +When a user logs in successfully, you can check if their hash needs upgrading: + +[source,cpp] +---- +system::error_code ec; +bool valid = bcrypt::compare(password, stored_hash, ec); + +if (valid && !ec) +{ + unsigned current_cost = bcrypt::get_rounds(stored_hash, ec); + + if (!ec && current_cost < 12) + { + // Re-hash with higher cost + bcrypt::result new_hash = bcrypt::hash(password, 12); + update_stored_hash(user_id, new_hash.str()); + } +} +---- + +== Error Handling + +BCrypt defines two error codes: + +[cols="1,3"] +|=== +| Error | Meaning + +| `bcrypt::error::invalid_salt` +| Salt string is malformed + +| `bcrypt::error::invalid_hash` +| Hash string is malformed +|=== + +These errors indicate either data corruption or malicious input. Log them +as security events. + +== Version Selection + +BCrypt has multiple version prefixes: + +[cols="1,3"] +|=== +| Version | Description + +| `version::v2a` +| Original specification + +| `version::v2b` +| Fixed handling of passwords > 255 chars (recommended) +|=== + +Use `v2b` for new hashes. All versions produce compatible hashes that can +be verified by any version. + +== Security Considerations + +**Do:** + +* Use cost factor 12 or higher +* Store the complete hash string (includes salt and cost) +* Compare in constant time (handled by `compare`) +* Log invalid hash errors as security events + +**Do Not:** + +* Store salts separately (they are embedded in the hash) +* Use bcrypt for general-purpose hashing (use SHA-256) +* Compare hashes with `==` (timing attacks) + +== Summary + +[cols="1,3"] +|=== +| Function | Purpose + +| `bcrypt::hash(password, rounds)` +| Hash a password with auto-generated salt + +| `bcrypt::hash(password, salt, ec)` +| Hash with explicit salt + +| `bcrypt::compare(password, hash, ec)` +| Verify a password against a hash + +| `bcrypt::gen_salt(rounds)` +| Generate a random salt + +| `bcrypt::get_rounds(hash, ec)` +| Extract cost factor from hash +|=== diff --git a/include/boost/http.hpp b/include/boost/http.hpp index abbebbcf..d736d7ab 100644 --- a/include/boost/http.hpp +++ b/include/boost/http.hpp @@ -10,6 +10,7 @@ #ifndef BOOST_HTTP_HPP #define BOOST_HTTP_HPP +#include #include #include #include diff --git a/include/boost/http/bcrypt.hpp b/include/boost/http/bcrypt.hpp new file mode 100644 index 00000000..6129c428 --- /dev/null +++ b/include/boost/http/bcrypt.hpp @@ -0,0 +1,49 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +/** @file + bcrypt password hashing library. + + This header includes all bcrypt-related functionality including + password hashing, verification, and salt generation. + + bcrypt is a password-hashing function designed by Niels Provos + and David Mazières based on the Blowfish cipher. It incorporates + a salt to protect against rainbow table attacks and an adaptive + cost parameter that can be increased as hardware improves. + + @code + #include + + // Hash a password + http::bcrypt::result r; + http::bcrypt::hash(r, "my_password", 12); + + // Store r.str() in database... + + // Verify later + system::error_code ec; + bool ok = boost::http::bcrypt::compare("my_password", stored_hash, ec); + if (ec) + handle_malformed_hash(); + else if (ok) + grant_access(); + @endcode +*/ + +#ifndef BOOST_HTTP_BCRYPT_HPP +#define BOOST_HTTP_BCRYPT_HPP + +#include +#include +#include +#include +#include + +#endif diff --git a/include/boost/http/bcrypt/error.hpp b/include/boost/http/bcrypt/error.hpp new file mode 100644 index 00000000..62695702 --- /dev/null +++ b/include/boost/http/bcrypt/error.hpp @@ -0,0 +1,92 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#ifndef BOOST_HTTP_BCRYPT_ERROR_HPP +#define BOOST_HTTP_BCRYPT_ERROR_HPP + +#include +#include +#include + +namespace boost { +namespace http { +namespace bcrypt { + +/** Error codes for bcrypt operations. + + These errors indicate malformed input from untrusted sources. +*/ +enum class error +{ + /// Success + ok = 0, + + /// Salt string is malformed + invalid_salt, + + /// Hash string is malformed + invalid_hash +}; + +} // bcrypt +} // http + +namespace system { +template<> +struct is_error_code_enum< + ::boost::http::bcrypt::error> +{ + static bool const value = true; +}; +} // system + +namespace http { +namespace bcrypt { + +namespace detail { + +struct BOOST_SYMBOL_VISIBLE + error_cat_type + : system::error_category +{ + BOOST_HTTP_DECL const char* name( + ) const noexcept override; + BOOST_HTTP_DECL std::string message( + int) const override; + BOOST_HTTP_DECL char const* message( + int, char*, std::size_t + ) const noexcept override; + BOOST_SYSTEM_CONSTEXPR error_cat_type() + : error_category(0xbc8f2a4e7c193d56) + { + } +}; + +BOOST_HTTP_DECL extern + error_cat_type error_cat; + +} // detail + +inline +BOOST_SYSTEM_CONSTEXPR +system::error_code +make_error_code( + error ev) noexcept +{ + return system::error_code{ + static_cast::type>(ev), + detail::error_cat}; +} + +} // bcrypt +} // http +} // boost + +#endif diff --git a/include/boost/http/bcrypt/hash.hpp b/include/boost/http/bcrypt/hash.hpp new file mode 100644 index 00000000..07375e08 --- /dev/null +++ b/include/boost/http/bcrypt/hash.hpp @@ -0,0 +1,173 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#ifndef BOOST_HTTP_BCRYPT_HASH_HPP +#define BOOST_HTTP_BCRYPT_HASH_HPP + +#include +#include +#include +#include +#include +#include + +namespace boost { +namespace http { +namespace bcrypt { + +/** Generate a random salt. + + Creates a bcrypt salt string suitable for use with + the hash() function. + + @par Preconditions + @code + rounds >= 4 && rounds <= 31 + @endcode + + @par Exception Safety + Strong guarantee. + + @par Complexity + Constant. + + @param rounds Cost factor. Each increment doubles the work. + Default is 10, which takes approximately 100ms on modern hardware. + + @param ver Hash version to use. + + @return A 29-character salt string. + + @throws std::invalid_argument if rounds is out of range. + @throws system_error on RNG failure. +*/ +BOOST_HTTP_DECL +result +gen_salt( + unsigned rounds = 10, + version ver = version::v2b); + +/** Hash a password with auto-generated salt. + + Generates a random salt and hashes the password. + + @par Preconditions + @code + rounds >= 4 && rounds <= 31 + @endcode + + @par Exception Safety + Strong guarantee. + + @par Complexity + O(2^rounds). + + @param password The password to hash. Only the first 72 bytes + are used (bcrypt limitation). + + @param rounds Cost factor. Each increment doubles the work. + + @param ver Hash version to use. + + @return A 60-character hash string. + + @throws std::invalid_argument if rounds is out of range. + @throws system_error on RNG failure. +*/ +BOOST_HTTP_DECL +result +hash( + core::string_view password, + unsigned rounds = 10, + version ver = version::v2b); + +/** Hash a password using a provided salt. + + Uses the given salt to hash the password. The salt should + be a string previously returned by gen_salt() or extracted + from a hash string. + + @par Exception Safety + Strong guarantee. + + @par Complexity + O(2^rounds). + + @param password The password to hash. + + @param salt The salt string (29 characters). + + @param ec Set to bcrypt::error::invalid_salt if the salt + is malformed. + + @return A 60-character hash string, or empty result on error. +*/ +BOOST_HTTP_DECL +result +hash( + core::string_view password, + core::string_view salt, + system::error_code& ec); + +/** Compare a password against a hash. + + Extracts the salt from the hash, re-hashes the password, + and compares the result. + + @par Exception Safety + Strong guarantee. + + @par Complexity + O(2^rounds). + + @param password The plaintext password to check. + + @param hash The hash string to compare against. + + @param ec Set to bcrypt::error::invalid_hash if the hash + is malformed. + + @return true if the password matches the hash, false if + it does not match OR if an error occurred. Always check + ec to distinguish between a mismatch and an error. +*/ +BOOST_HTTP_DECL +bool +compare( + core::string_view password, + core::string_view hash, + system::error_code& ec); + +/** Extract the cost factor from a hash string. + + @par Exception Safety + Strong guarantee. + + @par Complexity + Constant. + + @param hash The hash string to parse. + + @param ec Set to bcrypt::error::invalid_hash if the hash + is malformed. + + @return The cost factor (4-31) on success, or 0 if an + error occurred. +*/ +BOOST_HTTP_DECL +unsigned +get_rounds( + core::string_view hash, + system::error_code& ec); + +} // bcrypt +} // http +} // boost + +#endif diff --git a/include/boost/http/bcrypt/result.hpp b/include/boost/http/bcrypt/result.hpp new file mode 100644 index 00000000..01188f8a --- /dev/null +++ b/include/boost/http/bcrypt/result.hpp @@ -0,0 +1,123 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#ifndef BOOST_HTTP_BCRYPT_RESULT_HPP +#define BOOST_HTTP_BCRYPT_RESULT_HPP + +#include +#include +#include +#include +#include + +namespace boost { +namespace http { +namespace bcrypt { + +/** Fixed-size buffer for bcrypt hash output. + + Stores a bcrypt hash string (max 60 chars) in an + inline buffer with no heap allocation. + + @par Example + @code + bcrypt::result r = bcrypt::hash("password", 10); + core::string_view sv = r; // or r.str() + std::cout << r.c_str(); // null-terminated + @endcode +*/ +class result +{ + char buf_[61]; // 60 chars + null terminator + unsigned char size_; + +public: + /** Default constructor. + + Constructs an empty result. + */ + result() noexcept + : size_(0) + { + buf_[0] = '\0'; + } + + /** Return the hash as a string_view. + */ + core::string_view + str() const noexcept + { + return core::string_view(buf_, size_); + } + + /** Implicit conversion to string_view. + */ + operator core::string_view() const noexcept + { + return str(); + } + + /** Return null-terminated C string. + */ + char const* + c_str() const noexcept + { + return buf_; + } + + /** Return pointer to data. + */ + char const* + data() const noexcept + { + return buf_; + } + + /** Return size in bytes (excludes null terminator). + */ + std::size_t + size() const noexcept + { + return size_; + } + + /** Check if result is empty. + */ + bool + empty() const noexcept + { + return size_ == 0; + } + + /** Check if result contains valid data. + */ + explicit + operator bool() const noexcept + { + return size_ != 0; + } + +private: + friend BOOST_HTTP_DECL result gen_salt(unsigned, version); + friend BOOST_HTTP_DECL result hash(core::string_view, unsigned, version); + friend BOOST_HTTP_DECL result hash(core::string_view, core::string_view, system::error_code&); + + char* buf() noexcept { return buf_; } + void set_size(unsigned char n) noexcept + { + size_ = n; + buf_[n] = '\0'; + } +}; + +} // bcrypt +} // http +} // boost + +#endif diff --git a/include/boost/http/bcrypt/version.hpp b/include/boost/http/bcrypt/version.hpp new file mode 100644 index 00000000..814af28b --- /dev/null +++ b/include/boost/http/bcrypt/version.hpp @@ -0,0 +1,37 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#ifndef BOOST_HTTP_BCRYPT_VERSION_HPP +#define BOOST_HTTP_BCRYPT_VERSION_HPP + +#include + +namespace boost { +namespace http { +namespace bcrypt { + +/** bcrypt hash version prefix. + + The version determines which variant of bcrypt is used. + All versions produce compatible hashes. +*/ +enum class version +{ + /// $2a$ - Original specification + v2a, + + /// $2b$ - Fixed handling of passwords > 255 chars (recommended) + v2b +}; + +} // bcrypt +} // http +} // boost + +#endif diff --git a/src/bcrypt/base64.cpp b/src/bcrypt/base64.cpp new file mode 100644 index 00000000..cfbbd60a --- /dev/null +++ b/src/bcrypt/base64.cpp @@ -0,0 +1,183 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#include "base64.hpp" + +namespace boost { +namespace http { +namespace bcrypt { +namespace detail { + +namespace { + +// bcrypt's non-standard base64 alphabet +constexpr char encode_table[] = + "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + +// Decode table: maps ASCII char to 6-bit value, or 0xFF for invalid +constexpr std::uint8_t decode_table[256] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0-7 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 8-15 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 16-23 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 24-31 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 32-39 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01, // 40-47 (. /) + 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, // 48-55 (0-7) + 0x3E, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 56-63 (8-9) + 0xFF, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // 64-71 (A-G) + 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, // 72-79 (H-O) + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, // 80-87 (P-W) + 0x19, 0x1A, 0x1B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 88-95 (X-Z) + 0xFF, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, // 96-103 (a-g) + 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, // 104-111 (h-o) + 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, // 112-119 (p-w) + 0x33, 0x34, 0x35, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 120-127 (x-z) + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, +}; + +} // namespace + +std::size_t +base64_encode( + char* dest, + std::uint8_t const* src, + std::size_t n) +{ + char* out = dest; + + while (n >= 3) + { + std::uint32_t v = + (static_cast(src[0]) << 16) | + (static_cast(src[1]) << 8) | + static_cast(src[2]); + + *out++ = encode_table[(v >> 18) & 0x3F]; + *out++ = encode_table[(v >> 12) & 0x3F]; + *out++ = encode_table[(v >> 6) & 0x3F]; + *out++ = encode_table[v & 0x3F]; + + src += 3; + n -= 3; + } + + if (n == 2) + { + std::uint32_t v = + (static_cast(src[0]) << 16) | + (static_cast(src[1]) << 8); + + *out++ = encode_table[(v >> 18) & 0x3F]; + *out++ = encode_table[(v >> 12) & 0x3F]; + *out++ = encode_table[(v >> 6) & 0x3F]; + } + else if (n == 1) + { + std::uint32_t v = + static_cast(src[0]) << 16; + + *out++ = encode_table[(v >> 18) & 0x3F]; + *out++ = encode_table[(v >> 12) & 0x3F]; + } + + return static_cast(out - dest); +} + +int +base64_decode( + std::uint8_t* dest, + char const* src, + std::size_t n) +{ + std::uint8_t* out = dest; + std::size_t i = 0; + + while (i + 4 <= n) + { + std::uint8_t a = decode_table[static_cast(src[i])]; + std::uint8_t b = decode_table[static_cast(src[i + 1])]; + std::uint8_t c = decode_table[static_cast(src[i + 2])]; + std::uint8_t d = decode_table[static_cast(src[i + 3])]; + + if ((a | b | c | d) & 0x80) + return -1; + + std::uint32_t v = + (static_cast(a) << 18) | + (static_cast(b) << 12) | + (static_cast(c) << 6) | + static_cast(d); + + *out++ = static_cast(v >> 16); + *out++ = static_cast(v >> 8); + *out++ = static_cast(v); + + i += 4; + } + + // Handle remaining 2 or 3 characters + if (i + 3 == n) + { + std::uint8_t a = decode_table[static_cast(src[i])]; + std::uint8_t b = decode_table[static_cast(src[i + 1])]; + std::uint8_t c = decode_table[static_cast(src[i + 2])]; + + if ((a | b | c) & 0x80) + return -1; + + std::uint32_t v = + (static_cast(a) << 18) | + (static_cast(b) << 12) | + (static_cast(c) << 6); + + *out++ = static_cast(v >> 16); + *out++ = static_cast(v >> 8); + } + else if (i + 2 == n) + { + std::uint8_t a = decode_table[static_cast(src[i])]; + std::uint8_t b = decode_table[static_cast(src[i + 1])]; + + if ((a | b) & 0x80) + return -1; + + std::uint32_t v = + (static_cast(a) << 18) | + (static_cast(b) << 12); + + *out++ = static_cast(v >> 16); + } + else if (i + 1 == n) + { + // Single trailing character is invalid + return -1; + } + + return static_cast(out - dest); +} + +} // detail +} // bcrypt +} // http +} // boost diff --git a/src/bcrypt/base64.hpp b/src/bcrypt/base64.hpp new file mode 100644 index 00000000..4f4fa399 --- /dev/null +++ b/src/bcrypt/base64.hpp @@ -0,0 +1,47 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#ifndef BOOST_HTTP_SRC_BCRYPT_BASE64_HPP +#define BOOST_HTTP_SRC_BCRYPT_BASE64_HPP + +#include +#include + +namespace boost { +namespace http { +namespace bcrypt { +namespace detail { + +// bcrypt uses a non-standard base64 alphabet: +// ./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 + +// Encode binary data to bcrypt base64. +// Returns number of characters written to dest. +// dest must have space for ((n * 4) + 2) / 3 characters. +std::size_t +base64_encode( + char* dest, + std::uint8_t const* src, + std::size_t n); + +// Decode bcrypt base64 to binary. +// Returns number of bytes written to dest, or -1 on error. +// dest must have space for (n * 3) / 4 bytes. +int +base64_decode( + std::uint8_t* dest, + char const* src, + std::size_t n); + +} // detail +} // bcrypt +} // http +} // boost + +#endif diff --git a/src/bcrypt/blowfish.cpp b/src/bcrypt/blowfish.cpp new file mode 100644 index 00000000..1bce35dd --- /dev/null +++ b/src/bcrypt/blowfish.cpp @@ -0,0 +1,477 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// +// Based on the OpenBSD implementation by Niels Provos. +// + +#include "blowfish.hpp" +#include +#include + +namespace boost { +namespace http { +namespace bcrypt { +namespace detail { + +namespace { + +// Initial P-array (first 18 32-bit values of the fractional part of pi) +constexpr std::uint32_t P_init[18] = { + 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, + 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, + 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, + 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, + 0x9216d5d9, 0x8979fb1b +}; + +// Initial S-boxes (more fractional bits of pi) +constexpr std::uint32_t S_init[4][256] = { + { + 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, + 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, + 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, + 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, + 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, + 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, + 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, + 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, + 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, + 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, + 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, + 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, + 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, + 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, + 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, + 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, + 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, + 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, + 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, + 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, + 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, + 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, + 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, + 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, + 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, + 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, + 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, + 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, + 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, + 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, + 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, + 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, + 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, + 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, + 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, + 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, + 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, + 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, + 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, + 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, + 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, + 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, + 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, + 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, + 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, + 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, + 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, + 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, + 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, + 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, + 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, + 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, + 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, + 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, + 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, + 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af, + 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, + 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, + 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, + 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, + 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, + 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, + 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, + 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a + }, + { + 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, + 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, + 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, + 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, + 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, + 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, + 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, + 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, + 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, + 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, + 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, + 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, + 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, + 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, + 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, + 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, + 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, + 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, + 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, + 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, + 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, + 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, + 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, + 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, + 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, + 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, + 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, + 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, + 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, + 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, + 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, + 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, + 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, + 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, + 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, + 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, + 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, + 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, + 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, + 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, + 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, + 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, + 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, + 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, + 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, + 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, + 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, + 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, + 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, + 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, + 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, + 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, + 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, + 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, + 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, + 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, + 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, + 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, + 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, + 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, + 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, + 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7 + }, + { + 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, + 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, + 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, + 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, + 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, + 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, + 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, + 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, + 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, + 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, + 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, + 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, + 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, + 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, + 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, + 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, + 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, + 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, + 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, + 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, + 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, + 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, + 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, + 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, + 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, + 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, + 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, + 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, + 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, + 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, + 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, + 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, + 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, + 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, + 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, + 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, + 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, + 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, + 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, + 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, + 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, + 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, + 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, + 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, + 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, + 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, + 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, + 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, + 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, + 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, + 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, + 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, + 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, + 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, + 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, + 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62, + 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, + 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, + 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, + 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, + 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, + 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, + 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, + 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0 + }, + { + 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, + 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, + 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, + 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, + 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, + 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, + 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, + 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, + 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, + 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, + 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, + 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, + 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, + 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, + 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, + 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, + 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, + 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, + 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, + 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, + 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, + 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, + 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, + 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, + 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, + 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, + 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, + 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, + 0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, + 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, + 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, + 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, + 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, + 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, + 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, + 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, + 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, + 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, + 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, + 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, + 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, + 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, + 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, + 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525, + 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, + 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, + 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, + 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, + 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, + 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, + 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, + 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, + 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, + 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, + 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, + 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, + 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, + 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, + 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, + 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, + 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, + 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, + 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, + 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6 + } +}; + +} // namespace + +void blowfish_init(blowfish_ctx& ctx) +{ + std::memcpy(ctx.P, P_init, sizeof(P_init)); + std::memcpy(ctx.S, S_init, sizeof(S_init)); +} + +inline std::uint32_t F(blowfish_ctx const& ctx, std::uint32_t x) +{ + std::uint32_t a = ctx.S[0][(x >> 24) & 0xFF]; + std::uint32_t b = ctx.S[1][(x >> 16) & 0xFF]; + std::uint32_t c = ctx.S[2][(x >> 8) & 0xFF]; + std::uint32_t d = ctx.S[3][x & 0xFF]; + return ((a + b) ^ c) + d; +} + +void blowfish_encrypt( + blowfish_ctx const& ctx, + std::uint32_t& L, + std::uint32_t& R) +{ + for (int i = 0; i < 16; i += 2) + { + L ^= ctx.P[i]; + R ^= F(ctx, L); + R ^= ctx.P[i + 1]; + L ^= F(ctx, R); + } + L ^= ctx.P[16]; + R ^= ctx.P[17]; + std::swap(L, R); +} + +void blowfish_expand_key( + blowfish_ctx& ctx, + std::uint8_t const* key, + std::size_t key_len) +{ + std::size_t j = 0; + + // XOR key into P-array + for (int i = 0; i < 18; ++i) + { + std::uint32_t data = 0; + for (int k = 0; k < 4; ++k) + { + data = (data << 8) | key[j]; + j = (j + 1) % key_len; + } + ctx.P[i] ^= data; + } + + // Encrypt all zeros, replace P and S + std::uint32_t L = 0, R = 0; + + for (int i = 0; i < 18; i += 2) + { + blowfish_encrypt(ctx, L, R); + ctx.P[i] = L; + ctx.P[i + 1] = R; + } + + for (int i = 0; i < 4; ++i) + { + for (int k = 0; k < 256; k += 2) + { + blowfish_encrypt(ctx, L, R); + ctx.S[i][k] = L; + ctx.S[i][k + 1] = R; + } + } +} + +void blowfish_expand_key_salt( + blowfish_ctx& ctx, + std::uint8_t const* key, + std::size_t key_len, + std::uint8_t const* salt, + std::size_t salt_len) +{ + std::size_t j = 0; + + // XOR key into P-array + for (int i = 0; i < 18; ++i) + { + std::uint32_t data = 0; + for (int k = 0; k < 4; ++k) + { + data = (data << 8) | key[j]; + j = (j + 1) % key_len; + } + ctx.P[i] ^= data; + } + + // Encrypt with salt XOR + std::uint32_t L = 0, R = 0; + j = 0; + + for (int i = 0; i < 18; i += 2) + { + // XOR salt into L and R + L ^= (static_cast(salt[(j + 0) % salt_len]) << 24) | + (static_cast(salt[(j + 1) % salt_len]) << 16) | + (static_cast(salt[(j + 2) % salt_len]) << 8) | + static_cast(salt[(j + 3) % salt_len]); + j = (j + 4) % salt_len; + + R ^= (static_cast(salt[(j + 0) % salt_len]) << 24) | + (static_cast(salt[(j + 1) % salt_len]) << 16) | + (static_cast(salt[(j + 2) % salt_len]) << 8) | + static_cast(salt[(j + 3) % salt_len]); + j = (j + 4) % salt_len; + + blowfish_encrypt(ctx, L, R); + ctx.P[i] = L; + ctx.P[i + 1] = R; + } + + for (int i = 0; i < 4; ++i) + { + for (int k = 0; k < 256; k += 2) + { + L ^= (static_cast(salt[(j + 0) % salt_len]) << 24) | + (static_cast(salt[(j + 1) % salt_len]) << 16) | + (static_cast(salt[(j + 2) % salt_len]) << 8) | + static_cast(salt[(j + 3) % salt_len]); + j = (j + 4) % salt_len; + + R ^= (static_cast(salt[(j + 0) % salt_len]) << 24) | + (static_cast(salt[(j + 1) % salt_len]) << 16) | + (static_cast(salt[(j + 2) % salt_len]) << 8) | + static_cast(salt[(j + 3) % salt_len]); + j = (j + 4) % salt_len; + + blowfish_encrypt(ctx, L, R); + ctx.S[i][k] = L; + ctx.S[i][k + 1] = R; + } + } +} + +void blowfish_encrypt_ecb( + blowfish_ctx const& ctx, + std::uint8_t* data, + std::size_t len) +{ + for (std::size_t i = 0; i < len; i += 8) + { + std::uint32_t L = + (static_cast(data[i]) << 24) | + (static_cast(data[i + 1]) << 16) | + (static_cast(data[i + 2]) << 8) | + static_cast(data[i + 3]); + + std::uint32_t R = + (static_cast(data[i + 4]) << 24) | + (static_cast(data[i + 5]) << 16) | + (static_cast(data[i + 6]) << 8) | + static_cast(data[i + 7]); + + blowfish_encrypt(ctx, L, R); + + data[i] = static_cast(L >> 24); + data[i + 1] = static_cast(L >> 16); + data[i + 2] = static_cast(L >> 8); + data[i + 3] = static_cast(L); + data[i + 4] = static_cast(R >> 24); + data[i + 5] = static_cast(R >> 16); + data[i + 6] = static_cast(R >> 8); + data[i + 7] = static_cast(R); + } +} + +} // detail +} // bcrypt +} // http +} // boost diff --git a/src/bcrypt/blowfish.hpp b/src/bcrypt/blowfish.hpp new file mode 100644 index 00000000..7ac99501 --- /dev/null +++ b/src/bcrypt/blowfish.hpp @@ -0,0 +1,64 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// +// Based on the OpenBSD implementation by Niels Provos. +// + +#ifndef BOOST_HTTP_SRC_BCRYPT_BLOWFISH_HPP +#define BOOST_HTTP_SRC_BCRYPT_BLOWFISH_HPP + +#include +#include + +namespace boost { +namespace http { +namespace bcrypt { +namespace detail { + +// Blowfish context +struct blowfish_ctx +{ + std::uint32_t P[18]; + std::uint32_t S[4][256]; +}; + +// Initialize context with default P and S boxes +void blowfish_init(blowfish_ctx& ctx); + +// Expand key into context +void blowfish_expand_key( + blowfish_ctx& ctx, + std::uint8_t const* key, + std::size_t key_len); + +// Expand key with salt (for eksblowfish) +void blowfish_expand_key_salt( + blowfish_ctx& ctx, + std::uint8_t const* key, + std::size_t key_len, + std::uint8_t const* salt, + std::size_t salt_len); + +// Encrypt 8 bytes in place (big-endian) +void blowfish_encrypt( + blowfish_ctx const& ctx, + std::uint32_t& L, + std::uint32_t& R); + +// Encrypt data (must be multiple of 8 bytes) +void blowfish_encrypt_ecb( + blowfish_ctx const& ctx, + std::uint8_t* data, + std::size_t len); + +} // detail +} // bcrypt +} // http +} // boost + +#endif diff --git a/src/bcrypt/crypt.cpp b/src/bcrypt/crypt.cpp new file mode 100644 index 00000000..f1f7be3b --- /dev/null +++ b/src/bcrypt/crypt.cpp @@ -0,0 +1,206 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#include "crypt.hpp" +#include "base64.hpp" +#include "blowfish.hpp" +#include "random.hpp" +#include +#include + +namespace boost { +namespace http { +namespace bcrypt { +namespace detail { + +namespace { + +// "OrpheanBeholderScryDoubt" - magic string for bcrypt +constexpr std::uint8_t magic_text[24] = { + 'O', 'r', 'p', 'h', 'e', 'a', 'n', 'B', + 'e', 'h', 'o', 'l', 'd', 'e', 'r', 'S', + 'c', 'r', 'y', 'D', 'o', 'u', 'b', 't' +}; + +char const* version_prefix(version ver) +{ + switch (ver) + { + case version::v2a: return "$2a$"; + case version::v2b: return "$2b$"; + default: return "$2b$"; + } +} + +} // namespace + +void generate_salt_bytes(std::uint8_t* salt) +{ + fill_random(salt, BCRYPT_SALT_LEN); +} + +std::size_t format_salt( + char* output, + std::uint8_t const* salt_bytes, + unsigned rounds, + version ver) +{ + char* p = output; + + // Version prefix + char const* prefix = version_prefix(ver); + std::size_t prefix_len = 4; + std::memcpy(p, prefix, prefix_len); + p += prefix_len; + + // Rounds (2 digits, zero-padded) + *p++ = static_cast('0' + (rounds / 10)); + *p++ = static_cast('0' + (rounds % 10)); + *p++ = '$'; + + // Salt (22 base64 characters) + std::size_t encoded = base64_encode(p, salt_bytes, BCRYPT_SALT_LEN); + p += encoded; + + return static_cast(p - output); +} + +bool parse_salt( + core::string_view salt_str, + version& ver, + unsigned& rounds, + std::uint8_t* salt_bytes) +{ + // Minimum: "$2a$XX$" + 22 chars = 29 + if (salt_str.size() < 29) + return false; + + char const* s = salt_str.data(); + + // Check prefix + if (s[0] != '$' || s[1] != '2') + return false; + + // Parse version + if (s[2] == 'a' && s[3] == '$') + ver = version::v2a; + else if (s[2] == 'b' && s[3] == '$') + ver = version::v2b; + else if (s[2] == 'y' && s[3] == '$') + ver = version::v2b; // treat $2y$ as $2b$ + else + return false; + + // Parse rounds + if (s[4] < '0' || s[4] > '9') + return false; + if (s[5] < '0' || s[5] > '9') + return false; + + rounds = static_cast((s[4] - '0') * 10 + (s[5] - '0')); + if (rounds < 4 || rounds > 31) + return false; + + if (s[6] != '$') + return false; + + // Decode salt (22 base64 chars -> 16 bytes) + int decoded = base64_decode(salt_bytes, s + 7, 22); + if (decoded != 16) + return false; + + return true; +} + +void bcrypt_hash( + char const* password, + std::size_t password_len, + std::uint8_t const* salt, + unsigned rounds, + std::uint8_t* hash) +{ + blowfish_ctx ctx; + + // Truncate password to 72 bytes (bcrypt limit) + // Include null terminator in hash + std::size_t key_len = std::min(password_len, std::size_t(72)); + + // Create key with null terminator + std::uint8_t key[73]; + std::memcpy(key, password, key_len); + key[key_len] = 0; + key_len++; + + // Initialize with default P and S boxes + blowfish_init(ctx); + + // Expensive key setup (eksblowfish) + blowfish_expand_key_salt(ctx, key, key_len, salt, BCRYPT_SALT_LEN); + + // 2^rounds iterations + std::uint64_t iterations = 1ULL << rounds; + for (std::uint64_t i = 0; i < iterations; ++i) + { + blowfish_expand_key(ctx, key, key_len); + blowfish_expand_key(ctx, salt, BCRYPT_SALT_LEN); + } + + // Encrypt magic text 64 times + std::uint8_t ctext[24]; + std::memcpy(ctext, magic_text, 24); + + for (int i = 0; i < 64; ++i) + { + blowfish_encrypt_ecb(ctx, ctext, 24); + } + + // Copy result (only 23 bytes are used in the final encoding) + std::memcpy(hash, ctext, 24); + + // Clear sensitive data + std::memset(&ctx, 0, sizeof(ctx)); + std::memset(key, 0, sizeof(key)); +} + +std::size_t format_hash( + char* output, + std::uint8_t const* salt_bytes, + std::uint8_t const* hash_bytes, + unsigned rounds, + version ver) +{ + char* p = output; + + // Format salt portion (29 chars) + p += format_salt(p, salt_bytes, rounds, ver); + + // Encode hash (23 bytes -> 31 base64 chars) + // Note: bcrypt only uses 23 of the 24 hash bytes + p += base64_encode(p, hash_bytes, 23); + + return static_cast(p - output); +} + +bool secure_compare( + std::uint8_t const* a, + std::uint8_t const* b, + std::size_t len) +{ + volatile std::uint8_t result = 0; + for (std::size_t i = 0; i < len; ++i) + { + result = static_cast(result | (a[i] ^ b[i])); + } + return result == 0; +} + +} // detail +} // bcrypt +} // http +} // boost diff --git a/src/bcrypt/crypt.hpp b/src/bcrypt/crypt.hpp new file mode 100644 index 00000000..c93c9305 --- /dev/null +++ b/src/bcrypt/crypt.hpp @@ -0,0 +1,79 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#ifndef BOOST_HTTP_SRC_BCRYPT_CRYPT_HPP +#define BOOST_HTTP_SRC_BCRYPT_CRYPT_HPP + +#include +#include +#include + +namespace boost { +namespace http { +namespace bcrypt { +namespace detail { + +// bcrypt constants +constexpr std::size_t BCRYPT_SALT_LEN = 16; // 128-bit salt +constexpr std::size_t BCRYPT_HASH_LEN = 24; // 192-bit hash (23 bytes used) +constexpr std::size_t BCRYPT_SALT_OUTPUT_LEN = 29; // "$2b$XX$" + 22 base64 +constexpr std::size_t BCRYPT_HASH_OUTPUT_LEN = 60; // salt + 31 base64 + +// Generate random salt bytes +void generate_salt_bytes(std::uint8_t* salt); + +// Format salt string: "$2b$XX$<22 base64 chars>" +// Returns number of characters written (29) +std::size_t format_salt( + char* output, + std::uint8_t const* salt_bytes, + unsigned rounds, + version ver); + +// Parse salt string, extract version, rounds, and salt bytes +// Returns true on success +bool parse_salt( + core::string_view salt_str, + version& ver, + unsigned& rounds, + std::uint8_t* salt_bytes); + +// Core bcrypt hash function +// password: null-terminated password (max 72 bytes used) +// salt: 16 bytes +// rounds: cost factor (2^rounds iterations) +// hash: output buffer (24 bytes) +void bcrypt_hash( + char const* password, + std::size_t password_len, + std::uint8_t const* salt, + unsigned rounds, + std::uint8_t* hash); + +// Format complete hash string +// Returns number of characters written (60) +std::size_t format_hash( + char* output, + std::uint8_t const* salt_bytes, + std::uint8_t const* hash_bytes, + unsigned rounds, + version ver); + +// Constant-time comparison of hash bytes +bool secure_compare( + std::uint8_t const* a, + std::uint8_t const* b, + std::size_t len); + +} // detail +} // bcrypt +} // http +} // boost + +#endif diff --git a/src/bcrypt/error.cpp b/src/bcrypt/error.cpp new file mode 100644 index 00000000..91be41aa --- /dev/null +++ b/src/bcrypt/error.cpp @@ -0,0 +1,57 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#include + +namespace boost { +namespace http { +namespace bcrypt { +namespace detail { + +const char* +error_cat_type:: +name() const noexcept +{ + return "boost.http.bcrypt"; +} + +std::string +error_cat_type:: +message(int ev) const +{ + return message(ev, nullptr, 0); +} + +char const* +error_cat_type:: +message( + int ev, + char*, + std::size_t) const noexcept +{ + switch(static_cast(ev)) + { + case error::ok: return "success"; + case error::invalid_salt: return "invalid salt"; + case error::invalid_hash: return "invalid hash"; + default: + return "unknown"; + } +} + +#if defined(__cpp_constinit) && __cpp_constinit >= 201907L +constinit error_cat_type error_cat; +#else +error_cat_type error_cat; +#endif + +} // detail +} // bcrypt +} // http +} // boost diff --git a/src/bcrypt/hash.cpp b/src/bcrypt/hash.cpp new file mode 100644 index 00000000..2357416d --- /dev/null +++ b/src/bcrypt/hash.cpp @@ -0,0 +1,222 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#include +#include +#include "base64.hpp" +#include "crypt.hpp" + +namespace boost { +namespace http { +namespace bcrypt { + +result +gen_salt( + unsigned rounds, + version ver) +{ + // Validate preconditions + if (rounds < 4 || rounds > 31) + http::detail::throw_invalid_argument("bcrypt rounds must be 4-31"); + + // Generate random salt + std::uint8_t salt_bytes[detail::BCRYPT_SALT_LEN]; + detail::generate_salt_bytes(salt_bytes); + + // Format salt string + result r; + std::size_t len = detail::format_salt( + r.buf(), + salt_bytes, + rounds, + ver); + + r.set_size(static_cast(len)); + return r; +} + +result +hash( + core::string_view password, + unsigned rounds, + version ver) +{ + // Validate preconditions + if (rounds < 4 || rounds > 31) + http::detail::throw_invalid_argument("bcrypt rounds must be 4-31"); + + // Generate random salt + std::uint8_t salt_bytes[detail::BCRYPT_SALT_LEN]; + detail::generate_salt_bytes(salt_bytes); + + // Hash password + std::uint8_t hash_bytes[detail::BCRYPT_HASH_LEN]; + detail::bcrypt_hash( + password.data(), + password.size(), + salt_bytes, + rounds, + hash_bytes); + + // Format output + result r; + std::size_t len = detail::format_hash( + r.buf(), + salt_bytes, + hash_bytes, + rounds, + ver); + + r.set_size(static_cast(len)); + return r; +} + +result +hash( + core::string_view password, + core::string_view salt, + system::error_code& ec) +{ + ec = {}; + + // Parse salt + version ver; + unsigned rounds; + std::uint8_t salt_bytes[detail::BCRYPT_SALT_LEN]; + + if (!detail::parse_salt(salt, ver, rounds, salt_bytes)) + { + ec = make_error_code(error::invalid_salt); + return result{}; + } + + // Hash password + std::uint8_t hash_bytes[detail::BCRYPT_HASH_LEN]; + detail::bcrypt_hash( + password.data(), + password.size(), + salt_bytes, + rounds, + hash_bytes); + + // Format output + result r; + std::size_t len = detail::format_hash( + r.buf(), + salt_bytes, + hash_bytes, + rounds, + ver); + + r.set_size(static_cast(len)); + return r; +} + +bool +compare( + core::string_view password, + core::string_view hash_str, + system::error_code& ec) +{ + ec = {}; + + // Parse hash to extract salt + version ver; + unsigned rounds; + std::uint8_t salt_bytes[detail::BCRYPT_SALT_LEN]; + + if (!detail::parse_salt(hash_str, ver, rounds, salt_bytes)) + { + ec = make_error_code(error::invalid_hash); + return false; + } + + // Validate hash length + if (hash_str.size() != detail::BCRYPT_HASH_OUTPUT_LEN) + { + ec = make_error_code(error::invalid_hash); + return false; + } + + // Decode stored hash (31 base64 chars starting at position 29) + std::uint8_t stored_hash[detail::BCRYPT_HASH_LEN]; + int decoded = detail::base64_decode( + stored_hash, + hash_str.data() + 29, + 31); + + if (decoded < 0) + { + ec = make_error_code(error::invalid_hash); + return false; + } + + // Compute hash of provided password + std::uint8_t computed_hash[detail::BCRYPT_HASH_LEN]; + detail::bcrypt_hash( + password.data(), + password.size(), + salt_bytes, + rounds, + computed_hash); + + // Constant-time comparison (only first 23 bytes are used) + return detail::secure_compare(stored_hash, computed_hash, 23); +} + +unsigned +get_rounds( + core::string_view hash_str, + system::error_code& ec) +{ + ec = {}; + + // Minimum length check + if (hash_str.size() < 7) + { + ec = make_error_code(error::invalid_hash); + return 0; + } + + char const* s = hash_str.data(); + + // Check prefix + if (s[0] != '$' || s[1] != '2') + { + ec = make_error_code(error::invalid_hash); + return 0; + } + + // Check version character + if ((s[2] != 'a' && s[2] != 'b' && s[2] != 'y') || s[3] != '$') + { + ec = make_error_code(error::invalid_hash); + return 0; + } + + // Parse rounds + if (s[4] < '0' || s[4] > '9' || s[5] < '0' || s[5] > '9') + { + ec = make_error_code(error::invalid_hash); + return 0; + } + + unsigned rounds = static_cast((s[4] - '0') * 10 + (s[5] - '0')); + if (rounds < 4 || rounds > 31) + { + ec = make_error_code(error::invalid_hash); + return 0; + } + + return rounds; +} + +} // bcrypt +} // http +} // boost diff --git a/src/bcrypt/random.cpp b/src/bcrypt/random.cpp new file mode 100644 index 00000000..c52488f7 --- /dev/null +++ b/src/bcrypt/random.cpp @@ -0,0 +1,184 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#include "random.hpp" +#include +#include + +#if defined(_WIN32) +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# include +# include +# ifdef _MSC_VER +# pragma comment(lib, "bcrypt.lib") +# endif +#elif defined(__linux__) +# include +#elif defined(__APPLE__) +# include +#else +# include +# include +#endif + +namespace boost { +namespace http { +namespace bcrypt { +namespace detail { + +#if defined(_WIN32) + +namespace { + +class rng_provider +{ + BCRYPT_ALG_HANDLE h_ = nullptr; + +public: + rng_provider() + { + NTSTATUS status = BCryptOpenAlgorithmProvider( + &h_, + BCRYPT_RNG_ALGORITHM, + nullptr, + 0); + if (!BCRYPT_SUCCESS(status)) + h_ = nullptr; + } + + ~rng_provider() + { + if (h_) + BCryptCloseAlgorithmProvider(h_, 0); + } + + rng_provider(rng_provider const&) = delete; + rng_provider& operator=(rng_provider const&) = delete; + + bool generate(void* buf, std::size_t n) const + { + if (!h_) + return false; + NTSTATUS status = BCryptGenRandom( + h_, + static_cast(buf), + static_cast(n), + 0); + return BCRYPT_SUCCESS(status); + } +}; + +rng_provider& get_rng() +{ + static rng_provider rng; + return rng; +} + +} // namespace + +void +fill_random(void* buf, std::size_t n) +{ + if (!get_rng().generate(buf, n)) + { + http::detail::throw_system_error( + system::error_code( + static_cast(GetLastError()), + system::system_category())); + } +} + +#elif defined(__linux__) + +void +fill_random(void* buf, std::size_t n) +{ + auto* p = static_cast(buf); + while (n > 0) + { + ssize_t r = getrandom(p, n, 0); + if (r < 0) + { + if (errno == EINTR) + continue; + http::detail::throw_system_error( + system::error_code( + errno, + system::system_category())); + } + p += r; + n -= static_cast(r); + } +} + +#elif defined(__APPLE__) + +void +fill_random(void* buf, std::size_t n) +{ + int err = SecRandomCopyBytes(kSecRandomDefault, n, buf); + if (err != errSecSuccess) + { + http::detail::throw_system_error( + system::error_code( + err, + system::system_category())); + } +} + +#else + +// Fallback: /dev/urandom +void +fill_random(void* buf, std::size_t n) +{ + static int fd = -1; + if (fd < 0) + { + fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC); + if (fd < 0) + { + http::detail::throw_system_error( + system::error_code( + errno, + system::system_category())); + } + } + + auto* p = static_cast(buf); + while (n > 0) + { + ssize_t r = read(fd, p, n); + if (r < 0) + { + if (errno == EINTR) + continue; + http::detail::throw_system_error( + system::error_code( + errno, + system::system_category())); + } + if (r == 0) + { + http::detail::throw_runtime_error( + "unexpected EOF from /dev/urandom"); + } + p += r; + n -= static_cast(r); + } +} + +#endif + +} // detail +} // bcrypt +} // http +} // boost diff --git a/src/bcrypt/random.hpp b/src/bcrypt/random.hpp new file mode 100644 index 00000000..30dc0a6f --- /dev/null +++ b/src/bcrypt/random.hpp @@ -0,0 +1,31 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#ifndef BOOST_HTTP_SRC_BCRYPT_RANDOM_HPP +#define BOOST_HTTP_SRC_BCRYPT_RANDOM_HPP + +#include +#include + +namespace boost { +namespace http { +namespace bcrypt { +namespace detail { + +// Fill buffer with cryptographically secure random bytes. +// Throws system_error on failure. +void +fill_random(void* buf, std::size_t n); + +} // detail +} // bcrypt +} // http +} // boost + +#endif diff --git a/test/unit/bcrypt.cpp b/test/unit/bcrypt.cpp new file mode 100644 index 00000000..7ffd65f8 --- /dev/null +++ b/test/unit/bcrypt.cpp @@ -0,0 +1,290 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +#include + +#include "test_helpers.hpp" + +namespace boost { +namespace http { + +struct bcrypt_test +{ + void + test_error_code() + { + // Test error codes can be created + system::error_code ec1 = bcrypt::make_error_code(bcrypt::error::ok); + BOOST_TEST(! ec1.failed()); + + system::error_code ec2 = bcrypt::make_error_code(bcrypt::error::invalid_salt); + BOOST_TEST(ec2.failed()); + BOOST_TEST(ec2.message() == "invalid salt"); + + system::error_code ec3 = bcrypt::make_error_code(bcrypt::error::invalid_hash); + BOOST_TEST(ec3.failed()); + BOOST_TEST(ec3.message() == "invalid hash"); + } + + void + test_result() + { + // Default construction + bcrypt::result r; + BOOST_TEST(r.empty()); + BOOST_TEST(r.size() == 0); + BOOST_TEST(! r); + + // After hashing + r = bcrypt::hash("password", 4); + BOOST_TEST(! r.empty()); + BOOST_TEST(r.size() == 60); + BOOST_TEST(static_cast(r)); + BOOST_TEST(r.c_str()[60] == '\0'); + } + + void + test_gen_salt() + { + // Default rounds (10) + bcrypt::result r = bcrypt::gen_salt(); + BOOST_TEST(r.size() == 29); + BOOST_TEST(r.str().substr(0, 4) == "$2b$"); + BOOST_TEST(r.str().substr(4, 2) == "10"); + + // Custom rounds + r = bcrypt::gen_salt(12); + BOOST_TEST(r.str().substr(4, 2) == "12"); + + // Version 2a + r = bcrypt::gen_salt(10, bcrypt::version::v2a); + BOOST_TEST(r.str().substr(0, 4) == "$2a$"); + + // Different salts each time + bcrypt::result r1 = bcrypt::gen_salt(4); + bcrypt::result r2 = bcrypt::gen_salt(4); + BOOST_TEST(r1.str() != r2.str()); + } + + void + test_hash_with_rounds() + { + // Basic hash + bcrypt::result r = bcrypt::hash("password", 4); + BOOST_TEST(r.size() == 60); + BOOST_TEST(r.str().substr(0, 7) == "$2b$04$"); + + // Different passwords produce different hashes + bcrypt::result r1 = bcrypt::hash("password1", 4); + bcrypt::result r2 = bcrypt::hash("password2", 4); + BOOST_TEST(r1.str() != r2.str()); + + // Same password with different salts produces different hashes + r1 = bcrypt::hash("password", 4); + r2 = bcrypt::hash("password", 4); + BOOST_TEST(r1.str() != r2.str()); + } + + void + test_hash_with_salt() + { + bcrypt::result salt = bcrypt::gen_salt(4); + + // Generate salt, then hash + { + system::error_code ec; + bcrypt::result h = bcrypt::hash("password", salt.str(), ec); + BOOST_TEST(! ec.failed()); + BOOST_TEST(h.size() == 60); + } + + // Same password + salt = same hash + { + system::error_code ec1; + bcrypt::result hash1 = bcrypt::hash("password", salt.str(), ec1); + BOOST_TEST(! ec1.failed()); + + system::error_code ec2; + bcrypt::result hash2 = bcrypt::hash("password", salt.str(), ec2); + BOOST_TEST(! ec2.failed()); + + BOOST_TEST(hash1.str() == hash2.str()); + } + + // Invalid salt + { + system::error_code ec; + bcrypt::result h = bcrypt::hash("password", "invalid", ec); + BOOST_TEST(ec == bcrypt::error::invalid_salt); + BOOST_TEST(h.empty()); + } + + // Malformed salt + { + system::error_code ec; + bcrypt::result h = bcrypt::hash("password", "$2b$04$", ec); + BOOST_TEST(ec == bcrypt::error::invalid_salt); + BOOST_TEST(h.empty()); + } + } + + void + test_compare() + { + bcrypt::result r = bcrypt::hash("correct_password", 4); + + // Correct password + { + system::error_code ec; + bool match = bcrypt::compare("correct_password", r.str(), ec); + BOOST_TEST(! ec.failed()); + BOOST_TEST(match); + } + + // Wrong password + { + system::error_code ec; + bool match = bcrypt::compare("wrong_password", r.str(), ec); + BOOST_TEST(! ec.failed()); + BOOST_TEST(! match); + } + + // Empty password (should not match) + { + system::error_code ec; + bool match = bcrypt::compare("", r.str(), ec); + BOOST_TEST(! ec.failed()); + BOOST_TEST(! match); + } + + // Invalid hash + { + system::error_code ec; + bool match = bcrypt::compare("password", "invalid", ec); + BOOST_TEST(ec == bcrypt::error::invalid_hash); + BOOST_TEST(! match); + } + + // Malformed hash (wrong length) + { + system::error_code ec; + bool match = bcrypt::compare("password", "$2b$04$abcdefghij", ec); + BOOST_TEST(ec == bcrypt::error::invalid_hash); + BOOST_TEST(! match); + } + } + + void + test_get_rounds() + { + // Valid hash + { + system::error_code ec; + unsigned rounds = bcrypt::get_rounds( + "$2b$12$abcdefghijklmnopqrstuuxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", ec); + BOOST_TEST(! ec.failed()); + BOOST_TEST(rounds == 12); + } + + // Different versions + { + system::error_code ec; + unsigned rounds = bcrypt::get_rounds( + "$2a$10$abcdefghijklmnopqrstuuxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", ec); + BOOST_TEST(! ec.failed()); + BOOST_TEST(rounds == 10); + } + + // Invalid format + { + system::error_code ec; + unsigned rounds = bcrypt::get_rounds("invalid", ec); + BOOST_TEST(ec == bcrypt::error::invalid_hash); + BOOST_TEST(rounds == 0); + } + + // Missing prefix + { + system::error_code ec; + unsigned rounds = bcrypt::get_rounds("2b$10$abc", ec); + BOOST_TEST(ec == bcrypt::error::invalid_hash); + BOOST_TEST(rounds == 0); + } + } + + void + test_known_vectors() + { + // Test vectors verified against reference implementations + + // U*U with all-C salt + { + system::error_code ec; + BOOST_TEST(bcrypt::compare("U*U", + "$2a$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW", ec)); + BOOST_TEST(! ec.failed()); + } + + // Empty password + { + system::error_code ec; + BOOST_TEST(bcrypt::compare("", + "$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s.", ec)); + BOOST_TEST(! ec.failed()); + } + + // Test that wrong password fails + { + system::error_code ec; + BOOST_TEST(! bcrypt::compare("wrong", + "$2a$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW", ec)); + BOOST_TEST(! ec.failed()); + } + } + + void + test_password_truncation() + { + // bcrypt truncates passwords to 72 bytes + std::string long_pw(100, 'a'); + std::string truncated_pw(72, 'a'); + + bcrypt::result salt = bcrypt::gen_salt(4); + + system::error_code ec1; + bcrypt::result r1 = bcrypt::hash(long_pw, salt.str(), ec1); + BOOST_TEST(! ec1.failed()); + + system::error_code ec2; + bcrypt::result r2 = bcrypt::hash(truncated_pw, salt.str(), ec2); + BOOST_TEST(! ec2.failed()); + + // Both should produce the same hash + BOOST_TEST(r1.str() == r2.str()); + } + + void + run() + { + test_error_code(); + test_result(); + test_gen_salt(); + test_hash_with_rounds(); + test_hash_with_salt(); + test_compare(); + test_get_rounds(); + test_known_vectors(); + test_password_truncation(); + } +}; + +TEST_SUITE(bcrypt_test, "boost.http.bcrypt"); + +} // http +} // boost diff --git a/test/unit/bcrypt/error.cpp b/test/unit/bcrypt/error.cpp new file mode 100644 index 00000000..1a356ce9 --- /dev/null +++ b/test/unit/bcrypt/error.cpp @@ -0,0 +1,11 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +// Test that header file is self-contained. +#include diff --git a/test/unit/bcrypt/hash.cpp b/test/unit/bcrypt/hash.cpp new file mode 100644 index 00000000..6ae4124a --- /dev/null +++ b/test/unit/bcrypt/hash.cpp @@ -0,0 +1,11 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +// Test that header file is self-contained. +#include diff --git a/test/unit/bcrypt/result.cpp b/test/unit/bcrypt/result.cpp new file mode 100644 index 00000000..b1e35b55 --- /dev/null +++ b/test/unit/bcrypt/result.cpp @@ -0,0 +1,11 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +// Test that header file is self-contained. +#include diff --git a/test/unit/bcrypt/version.cpp b/test/unit/bcrypt/version.cpp new file mode 100644 index 00000000..1914d890 --- /dev/null +++ b/test/unit/bcrypt/version.cpp @@ -0,0 +1,11 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + +// Test that header file is self-contained. +#include From 4935c4e6ab8abf2efb2726b1d20d5ed09f48fa81 Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Sun, 18 Jan 2026 13:02:40 -0800 Subject: [PATCH 06/12] Update documentation --- doc/agent-guide.md | 24 + doc/modules/ROOT/nav.adoc | 30 +- doc/modules/ROOT/pages/1.primer.adoc | 115 ---- doc/modules/ROOT/pages/2.messages.adoc | 134 ----- doc/modules/ROOT/pages/Message.adoc | 3 - doc/modules/ROOT/pages/containers.adoc | 465 +++++++++++++++ doc/modules/ROOT/pages/http-protocol.adoc | 218 +++++++ .../ROOT/pages/http_protocol_basics.adoc | 1 - doc/modules/ROOT/pages/index.adoc | 155 +++-- doc/modules/ROOT/pages/message_bodies.adoc | 1 - doc/modules/ROOT/pages/parsing.adoc | 383 ++++++++++++ doc/modules/ROOT/pages/router.adoc | 548 ++++++++++++++++++ doc/modules/ROOT/pages/serializing.adoc | 395 +++++++++++++ doc/modules/ROOT/pages/server/router.adoc | 311 ---------- 14 files changed, 2148 insertions(+), 635 deletions(-) create mode 100644 doc/agent-guide.md delete mode 100644 doc/modules/ROOT/pages/1.primer.adoc delete mode 100644 doc/modules/ROOT/pages/2.messages.adoc delete mode 100644 doc/modules/ROOT/pages/Message.adoc create mode 100644 doc/modules/ROOT/pages/containers.adoc create mode 100644 doc/modules/ROOT/pages/http-protocol.adoc delete mode 100644 doc/modules/ROOT/pages/http_protocol_basics.adoc delete mode 100644 doc/modules/ROOT/pages/message_bodies.adoc create mode 100644 doc/modules/ROOT/pages/parsing.adoc create mode 100644 doc/modules/ROOT/pages/router.adoc create mode 100644 doc/modules/ROOT/pages/serializing.adoc delete mode 100644 doc/modules/ROOT/pages/server/router.adoc diff --git a/doc/agent-guide.md b/doc/agent-guide.md new file mode 100644 index 00000000..b54ff547 --- /dev/null +++ b/doc/agent-guide.md @@ -0,0 +1,24 @@ +--- +Boost.HTTP Library Information +--- + +* Research + - https://expressjs.com/en/guide/routing.html + - https://www.w3.org/Protocols/rfc2616/rfc2616-sec1.html + +* Introduction + - Audience should have some familiarity with TCP/IP Networking + +* First Section is Introduction to HTTP + - Sessions, request/response sequence, closing + - Security concerns + +* Second Section is Containers + - request, response, fields containers + - method verb, status codes, reason strings (obsolete) + +* Third Section is Parsing and Serialization + +* Fourth Section is HTTP Router + - Express JS style routing with boost::http::router + - Route handlers diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index 85541aeb..cf72daa8 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -1,23 +1,9 @@ -* xref:1.primer.adoc[] -* xref:2.messages.adoc[] -* xref:sans_io_philosophy.adoc[] -* xref:http_protocol_basics.adoc[] -// * xref:header_containers.adoc[] -* xref:message_bodies.adoc[] -* Serializing -* Parsing -* xref:Message.adoc[] -* Server -** xref:server/router.adoc[Router] -* Cryptography -** xref:bcrypt.adoc[BCrypt Password Hashing] -// ** xref:server/middleware.adoc[Middleware] -// ** xref:server/errors.adoc[Error Handling] -// ** xref:server/params.adoc[Route Parameters] -// ** xref:server/advanced.adoc[Advanced Topics] -// ** xref:server/cors.adoc[CORS] -* Design Requirements -** xref:design_requirements/serializer.adoc[Serializer] -** xref:design_requirements/parser.adoc[Parser] -// * xref:reference:boost/http.adoc[Reference] +* xref:index.adoc[Introduction] +* xref:http-protocol.adoc[Introduction to HTTP] +* xref:containers.adoc[Containers] +* xref:parsing.adoc[Parsing] +* xref:serializing.adoc[Serializing] +* xref:router.adoc[Router] +* xref:bcrypt.adoc[BCrypt Password Hashing] +* xref:sans_io_philosophy.adoc[Sans-I/O Philosophy] * xref:reference.adoc[Reference] diff --git a/doc/modules/ROOT/pages/1.primer.adoc b/doc/modules/ROOT/pages/1.primer.adoc deleted file mode 100644 index 427f5546..00000000 --- a/doc/modules/ROOT/pages/1.primer.adoc +++ /dev/null @@ -1,115 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/buffers -// - -= HTTP Primer - -HTTP is a stream-oriented protocol between two connected programs: one acting -as the client, the other as the server. While the connection is open, the client -sends an HTTP request, which the server reads and answers with an HTTP response. -These _messages_ are paired in order; each request has exactly one -corresponding response. This exchange of structured messages continues until -either peer closes the connection, whether normally or due to an error. - -HTTP messages consist of three parts: the start line, the headers, and the -message body. The start line differs between requests and responses, while -the headers and body share the same structure. Headers are made up of zero -or more fields, each expressed as a name–value pair. Both the start line and -the header fields use a line-oriented text format, with each line terminated -by a CRLF sequence (carriage return followed by line feed, i.e. bytes -`0x0D 0x0A`). The message body is a sequence of bytes of defined length, -with content determined by the semantics of the start line and headers. - -This diagram shows an actual HTTP request and HTTP response - -[cols="1a,1a"] -|=== -|HTTP Request|HTTP Response - -| -[source] ----- -GET /index.html HTTP/1.1\r\n -User-Agent: Boost\r\n -\r\n ----- -| -[source] ----- -HTTP/1.1 200 OK\r\n -Server: Boost.HTTP\r\n -Content-Length: 13\r\n -\r\n -Hello, world! ----- - -|=== - -More formally, the ABNF for HTTP messages is defined as follows: - -[cols="1a,4a"] -|=== -|Name|ABNF - -|message -|[literal] -HTTP-message = request-line / status-line - *( header-field CRLF ) - CRLF - [ message-body ] - -|request-line -|[literal] -request-line = method SP request-target SP HTTP-version CRLF - -|status-line -|[literal] -status-line = HTTP-version SP status-code SP reason-phrase CRLF - -|=== - - -Most HTTP header field values are domain-specific or application-defined, while -certain fields commonly recur. The library understands these fields and takes -appropriate action to ensure RFC compliance: - -[cols="1a,4a"] -|=== -|Field|Description - -a| -https://tools.ietf.org/html/rfc7230#section-6.1[*Connection*] + -https://tools.ietf.org/html/rfc7230#appendix-A.1.2[*Proxy-Connection*] - -|This field lets the sender specify control options for the current connection. -Typical values include close, keep-alive, and upgrade. - -|https://tools.ietf.org/html/rfc7230#section-3.3.2[*Content-Length*] -|When present, this field tells the recipient the exact size of the message -body, measured in bytes, that follows the header. - -|https://tools.ietf.org/html/rfc7230#section-3.3.1[*Transfer-Encoding*] -|This optional field specifies the sequence of transfer codings that have been, -or will be, applied to the content payload to produce the message body. - -The library supports the -chunked, -gzip, -deflate, and -brotli -encoding schemes, -in any valid combination. Encodings can be automatically applied or removed -as needed by the caller. - -|https://tools.ietf.org/html/rfc7230#section-6.7[*Upgrade*] -|The Upgrade header field provides a mechanism to transition from HTTP/1.1 to -another protocol on the same connection. For example, it is the mechanism used -by WebSocket's initial HTTP handshake to establish a WebSocket connection. - -|=== - diff --git a/doc/modules/ROOT/pages/2.messages.adoc b/doc/modules/ROOT/pages/2.messages.adoc deleted file mode 100644 index 2cb3c354..00000000 --- a/doc/modules/ROOT/pages/2.messages.adoc +++ /dev/null @@ -1,134 +0,0 @@ -// -// Copyright (c) 2023 Vinnie Falco (vinnie.falco@gmail.com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/http -// - -= Messages - -The library provides both modifiable containers and immutable views for -requests, responses, and standalone field sets such as trailers. These can -be used to store incoming messages or construct outgoing ones. Unlike other -libraries, such as its predecessor Boost.Beast, the message body is kept -separate. In other words, the containers and views offered by this library -do not include the body. - -NOTE: By omitting the body from its container and view types, the library avoids -the need for templates—unlike the message container in Boost.Beast. Experience -has shown that templated containers create poor ergonomics, a design flaw this -library corrects. - -The following table lists the types used to model containers and views: - -[cols="1a,4a"] -|=== -|Type|Description - -|cpp:fields[] -|A modifiable container of header fields. - -|cpp:fields_view[] -|A read-only view to a cpp:fields[] - -|cpp:message[] -|A modifiable container holding a start-line and header fields, with - accompanying metadata. - -|cpp:message_view[] -|A read-only view to a cpp:message[] - -|cpp:request[] -|A modifiable container holding a request-line and header fields, with - accompanying metadata. - -|cpp:request_view[] -|A read-only view to a cpp:request[] - -|cpp:response[] -|A modifiable container holding a status-line and header fields, with - accompanying metadata. - -|cpp:response_view[] -|A read-only view to a cpp:response[] - -|=== - -== Construction - -All containers maintain the following invariants: - -* The container’s contents are always stored in serialized form that is - syntactically valid. - -* Any modification that would produce a malformed field or start line - throws an exception, with strong exception safety guaranteed. - -To satisfy these invariants, default-constructed containers -initially consist of a start line: - -[source,cpp] ----- -request req; -response res; - -assert(req->buffer() == "GET / HTTP/1.1\r\n\r\n"); -assert(res->buffer() == "HTTP/1.1 200 OK\r\n\r\n"); ----- - -The `buffer` function runs in constant time, never throws exceptions, -and returns a cpp:boost::core::string_view[] representing the complete -serialized object. - -Each header field consists of a name and a value, both stored as strings, -with prescribed ABNF format: - -[source] ----- -field-name = token -field-value = *( field-content / obs-fold ) -field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] -field-vchar = VCHAR / obs-text - -obs-fold = CRLF 1*( SP / HTAB ) - ; obsolete line folding ----- - -Operations that create or modify fields throw an exception if the name or -value violates the syntactic requirements of the protocol. - -Although fields may be identified by comparing their names, the library -provides the field enumeration, which defines a wide set of constants for -well-known field names. Internally, containers maintain a lookup table so -that specifying fields by enumeration replaces costly string comparisons -with efficient integer comparisons. - -The following example builds an -https://tools.ietf.org/html/rfc7231#section-4.3.1[HTTP GET] -request: - -[cols="1a,1a"] -|=== -|Code|Serialized Result - -| -[source,cpp] ----- -request req( method::get, "/index.htm", version::http_1_1 ); -req.append( field::accept, "text/html" ); -req.append( "User-Agent", "Boost" ); ----- -| -[literal] -GET /index.htm HTTP/1.1\r\n -Accept: text/html\r\n -User-Agent: Boost\r\n -\r\n - -|=== - -NOTE: Field-specific syntax (e.g., for date values) is not fully validated by -this library. It is the application’s responsibility to follow the relevant -specifications to ensure correct behavior. diff --git a/doc/modules/ROOT/pages/Message.adoc b/doc/modules/ROOT/pages/Message.adoc deleted file mode 100644 index 8f7f9858..00000000 --- a/doc/modules/ROOT/pages/Message.adoc +++ /dev/null @@ -1,3 +0,0 @@ -= Message - -image::ClassHierarchy.svg[] diff --git a/doc/modules/ROOT/pages/containers.adoc b/doc/modules/ROOT/pages/containers.adoc new file mode 100644 index 00000000..51ed6e96 --- /dev/null +++ b/doc/modules/ROOT/pages/containers.adoc @@ -0,0 +1,465 @@ +// +// Copyright (c) 2023 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + += Containers + +The library provides modifiable containers for HTTP messages and standalone +field collections. Unlike some HTTP libraries, the message body is kept +separate—these containers hold only the start line and headers, avoiding +the template complexity that comes from parameterizing on body type. + +== Container Types + +[cols="1a,4a"] +|=== +|Type|Description + +|`fields` +|A modifiable container of header fields (name-value pairs). + +|`request` +|A modifiable container holding a request line and header fields. + +|`response` +|A modifiable container holding a status line and header fields. + +|`static_request` +|A request container using externally-provided storage. + +|`static_response` +|A response container using externally-provided storage. + +|=== + +All containers maintain this invariant: **contents are always valid HTTP**. +Operations that would produce malformed output throw an exception. This +means you can safely serialize any container at any time. + +== Working with Fields + +The `fields` class stores a collection of header fields. Use it when you +need headers independent of a full request or response—for example, when +building trailer fields for chunked encoding. + +=== Creating Fields + +[source,cpp] +---- +// Empty fields container +fields f; + +// With initial capacity (bytes) +fields f(1024); + +// Parse from a string +fields f( + "Content-Type: text/html\r\n" + "Cache-Control: no-cache\r\n" + "\r\n"); +---- + +=== Adding Fields + +[source,cpp] +---- +fields f; + +// Append with field constant (preferred - faster lookup) +f.append(field::content_type, "text/html"); +f.append(field::cache_control, "no-cache"); + +// Append with string name +f.append("X-Custom-Header", "custom-value"); +---- + +Multiple fields with the same name are allowed: + +[source,cpp] +---- +f.append(field::set_cookie, "session=abc123"); +f.append(field::set_cookie, "theme=dark"); +// Both Set-Cookie headers are preserved +---- + +=== Querying Fields + +[source,cpp] +---- +// Check if a field exists +if (f.exists(field::content_type)) { /* ... */ } + +// Get the value (throws if not found) +auto ct = f.at(field::content_type); + +// Get the value or a default +auto ct = f.value_or(field::content_type, "application/octet-stream"); + +// Count occurrences +std::size_t n = f.count(field::set_cookie); + +// Iterate all fields +for (auto const& fld : f) +{ + std::cout << fld.name << ": " << fld.value << "\n"; +} +---- + +=== Modifying Fields + +[source,cpp] +---- +// Replace all occurrences with a single value +f.set(field::content_type, "application/json"); + +// Erase all occurrences +f.erase(field::cache_control); + +// Erase a specific field by iterator +auto it = f.find(field::content_type); +if (it != f.end()) + f.erase(it); +---- + +== Working with Requests + +The `request` class represents a complete HTTP request (minus the body). + +=== Creating Requests + +[source,cpp] +---- +// Default: GET / HTTP/1.1 +request req; + +// With method and target +request req(method::post, "/api/users"); + +// With method, target, and version +request req(method::get, "/", version::http_1_0); + +// Parse from a string +request req( + "POST /api/data HTTP/1.1\r\n" + "Host: example.com\r\n" + "Content-Length: 13\r\n" + "\r\n"); +---- + +=== Request Properties + +[source,cpp] +---- +request req(method::get, "/api/users"); + +// Access start line components +method m = req.method(); // method::get +auto target = req.target(); // "/api/users" +version v = req.version(); // version::http_1_1 + +// For unknown methods +auto text = req.method_text(); // "GET" + +// Modify start line +req.set_method(method::post); +req.set_target("/api/users/123"); +req.set_version(version::http_1_0); + +// Or set all at once (more efficient) +req.set_start_line(method::put, "/api/users/123", version::http_1_1); +---- + +=== Request Headers + +Requests inherit all field operations from `fields_base`: + +[source,cpp] +---- +request req(method::get, "/"); +req.set(field::host, "example.com"); +req.set(field::accept, "application/json"); +req.append(field::accept_encoding, "gzip, deflate"); + +// Serialized form +std::cout << req.buffer(); +---- + +Output: + +---- +GET / HTTP/1.1 +Host: example.com +Accept: application/json +Accept-Encoding: gzip, deflate + +---- + +== Working with Responses + +The `response` class represents a complete HTTP response (minus the body). + +=== Creating Responses + +[source,cpp] +---- +// Default: HTTP/1.1 200 OK +response res; + +// With status code +response res(status::not_found); + +// With status and version +response res(status::created, version::http_1_0); + +// Parse from a string +response res( + "HTTP/1.1 404 Not Found\r\n" + "Content-Type: text/plain\r\n" + "\r\n"); +---- + +=== Response Properties + +[source,cpp] +---- +response res(status::ok); + +// Access start line components +status s = res.status(); // status::ok +unsigned code = res.status_int(); // 200 +auto reason = res.reason(); // "OK" +version v = res.version(); // version::http_1_1 + +// Modify status +res.set_status(status::created); + +// Set status with custom reason (rarely needed) +res.set_status(200, "All Good"); + +// Set version +res.set_version(version::http_1_0); + +// Set all at once +res.set_start_line(status::accepted, version::http_1_1); +---- + +=== Common Response Patterns + +[source,cpp] +---- +// JSON API response +response res(status::ok); +res.set(field::content_type, "application/json"); +res.set_content_length(json_body.size()); + +// Redirect +response res(status::moved_permanently); +res.set(field::location, "https://example.com/new-path"); + +// Error with body +response res(status::bad_request); +res.set(field::content_type, "text/plain"); +res.set_content_length(error_message.size()); +---- + +== Methods and Status Codes + +=== HTTP Methods + +The `method` enumeration provides constants for standard HTTP methods: + +[source,cpp] +---- +method::get // GET +method::head // HEAD +method::post // POST +method::put // PUT +method::delete_ // DELETE (underscore due to C++ keyword) +method::connect // CONNECT +method::options // OPTIONS +method::trace // TRACE +method::patch // PATCH +// ... plus WebDAV methods, etc. +---- + +Convert between strings and enum values: + +[source,cpp] +---- +// String to method +method m = string_to_method("POST"); // method::post + +// Method to string +auto s = to_string(method::get); // "GET" +---- + +For non-standard methods, use `method::unknown` and work with strings: + +[source,cpp] +---- +request req; +req.set_method("CUSTOM"); // method() returns method::unknown +auto text = req.method_text(); // "CUSTOM" +---- + +=== Status Codes + +The `status` enumeration covers standard HTTP status codes: + +[source,cpp] +---- +// Informational (1xx) +status::continue_ // 100 +status::switching_protocols // 101 + +// Successful (2xx) +status::ok // 200 +status::created // 201 +status::accepted // 202 +status::no_content // 204 + +// Redirection (3xx) +status::moved_permanently // 301 +status::found // 302 +status::not_modified // 304 + +// Client Error (4xx) +status::bad_request // 400 +status::unauthorized // 401 +status::forbidden // 403 +status::not_found // 404 + +// Server Error (5xx) +status::internal_server_error // 500 +status::not_implemented // 501 +status::bad_gateway // 502 +status::service_unavailable // 503 +---- + +Status codes have associated reason phrases: + +[source,cpp] +---- +auto reason = to_string(status::not_found); // "Not Found" +---- + +Classify status codes by category: + +[source,cpp] +---- +auto sc = status_class::successful; // 2xx +auto sc = to_status_class(status::ok); // status_class::successful +auto sc = to_status_class(404); // status_class::client_error +---- + +== The Field Enumeration + +The `field` enumeration defines constants for well-known HTTP headers. +Using these constants instead of strings enables: + +* Faster comparisons (integer vs string) +* Case-insensitive matching without overhead +* Compile-time checking of field names + +[source,cpp] +---- +// Standard fields (partial list) +field::accept +field::accept_encoding +field::authorization +field::cache_control +field::connection +field::content_length +field::content_type +field::cookie +field::host +field::location +field::server +field::set_cookie +field::transfer_encoding +field::user_agent +// ... many more +---- + +Convert between strings and enum: + +[source,cpp] +---- +// String to field (case-insensitive) +auto f = string_to_field("Content-Type"); // field::content_type + +// Field to string (canonical casing) +auto s = to_string(field::content_type); // "Content-Type" +---- + +== Memory Management + +=== Capacity Control + +Containers allocate memory dynamically as needed. You can control this: + +[source,cpp] +---- +// Pre-allocate capacity +request req(1024); // Reserve 1024 bytes + +// Reserve more capacity +req.reserve_bytes(4096); + +// Get current capacity +std::size_t cap = req.capacity_in_bytes(); + +// Set maximum allowed capacity +req.set_max_capacity_in_bytes(8192); // Throws if exceeded +---- + +=== Static Containers + +For performance-critical paths, use static containers that never allocate: + +[source,cpp] +---- +// Fixed buffer of 2048 bytes +std::array storage; +static_request req(storage.data(), storage.size()); + +// If capacity is exceeded, operations throw +---- + +Static containers are useful when: + +* You need guaranteed no-allocation paths +* You have a fixed upper bound on message size +* You're working in embedded or real-time contexts + +== Serialization + +Every container can produce its serialized form instantly: + +[source,cpp] +---- +request req(method::get, "/"); +req.set(field::host, "example.com"); + +// Get the complete serialized message (minus body) +core::string_view sv = req.buffer(); + +// Stream output +std::cout << req; +---- + +The `buffer()` function returns a view to internal storage. It runs in +constant time and never allocates. The view remains valid until the +container is modified. + +== Next Steps + +Now that you can build and inspect HTTP messages, learn how to parse +incoming messages from the network: + +* xref:parsing.adoc[Parsing] — parse request and response messages diff --git a/doc/modules/ROOT/pages/http-protocol.adoc b/doc/modules/ROOT/pages/http-protocol.adoc new file mode 100644 index 00000000..09f10668 --- /dev/null +++ b/doc/modules/ROOT/pages/http-protocol.adoc @@ -0,0 +1,218 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + += Introduction to HTTP + +This section covers the fundamentals of HTTP that you need to understand +before using the library. After reading this, you'll know how HTTP sessions +work, what constitutes a message, and what security pitfalls to avoid. + +== Sessions + +HTTP is a stream-oriented protocol between two connected programs: a client +and a server. While the connection remains open, the client sends HTTP requests +and the server sends HTTP responses. These messages are paired in order—each +request has exactly one corresponding response. + +[source] +---- +Client Server + | | + |-------- Request #1 ----------------->| + |<------- Response #1 -----------------| + | | + |-------- Request #2 ----------------->| + |<------- Response #2 -----------------| + | | + ˅ ˅ +---- + +An HTTP/1.1 session typically proceeds as follows: + +1. Client establishes a TCP connection to the server +2. Client sends a request +3. Server processes the request and sends a response +4. Steps 2-3 repeat until either party closes the connection + +=== Persistent Connections + +HTTP/1.1 connections are persistent by default. The same connection can be +reused for multiple request/response exchanges, avoiding the overhead of +establishing new TCP connections. + +A connection is closed when: + +* Either party sends `Connection: close` +* An error occurs during parsing or I/O +* A configurable idle timeout expires +* The underlying transport is terminated + +=== Pipelining + +HTTP/1.1 allows clients to send multiple requests without waiting for +responses (pipelining). Responses must arrive in the same order as requests. +While the protocol supports this, many implementations handle it poorly, +which is why this library parses one complete message at a time. + +== Messages + +HTTP messages consist of three parts: the start line, the headers, and +an optional message body. + +[cols="1a,1a"] +|=== +|HTTP Request|HTTP Response + +| +[source] +---- +GET /index.html HTTP/1.1 +User-Agent: Boost +Host: example.com + +---- +| +[source] +---- +HTTP/1.1 200 OK +Server: Boost.HTTP +Content-Length: 13 + +Hello, world! +---- + +|=== + +=== Start Line + +The start line differs between requests and responses: + +**Request line**: `method SP request-target SP HTTP-version CRLF` + +**Status line**: `HTTP-version SP status-code SP reason-phrase CRLF` + +The library validates start lines strictly. Invalid syntax is rejected +immediately rather than attempting recovery. + +=== Header Fields + +Headers are name-value pairs that provide metadata about the message. +Each header occupies one line, terminated by CRLF: + +[source] +---- +field-name: field-value +---- + +Important characteristics: + +* Field names are case-insensitive (`Content-Type` equals `content-type`) +* Field values have leading and trailing whitespace stripped +* The same field name may appear multiple times +* Order of fields with the same name is significant + +The library tracks several headers automatically and enforces their semantics: + +[cols="1a,4a"] +|=== +|Field|Description + +|*Connection* +|Controls whether the connection stays open. Values include `keep-alive` +and `close`. The library updates connection state based on this field. + +|*Content-Length* +|Specifies the exact size of the message body in bytes. When present, +the parser uses this to determine when the body ends. + +|*Transfer-Encoding* +|Indicates transformations applied to the message body. The library +supports `chunked`, `gzip`, `deflate`, and `brotli` encodings. + +|*Upgrade* +|Requests a protocol switch (e.g., to WebSocket). The library detects +this and makes the raw connection available for the new protocol. + +|=== + +=== Message Body + +The body is a sequence of bytes following the headers. Its length is +determined by: + +* `Content-Length` header (exact byte count) +* `Transfer-Encoding: chunked` (length encoded in stream) +* Connection close (for responses without length indication) + +The library handles body framing automatically during parsing and +serialization. You provide or consume the raw body bytes. + +== Security Considerations + +HTTP implementation bugs frequently lead to security vulnerabilities. +The library is designed to prevent common attacks by default. + +=== Request Smuggling + +Request smuggling exploits disagreements between servers about where +one request ends and the next begins. This happens when: + +* Multiple `Content-Length` headers have different values +* Both `Content-Length` and `Transfer-Encoding: chunked` are present +* Malformed chunk sizes are interpreted differently + +The library rejects ambiguous requests. When both `Content-Length` and +`Transfer-Encoding` appear, `Transfer-Encoding` takes precedence per +RFC 9110, and `Content-Length` is removed from the parsed headers. + +=== Header Injection + +Header injection attacks insert unexpected headers by including CRLF +sequences in field values. The library forbids CR, LF, and NUL characters +in header values—attempts to include them throw an exception. + +[source,cpp] +---- +// This throws - newlines not allowed in values +req.set(field::user_agent, "Bad\r\nInjected-Header: evil"); +---- + +=== Resource Exhaustion + +Attackers can exhaust server memory by sending: + +* Extremely long header lines +* Too many header fields +* Enormous message bodies + +The library provides configurable limits for all of these. When a limit +is exceeded, parsing fails with a specific error code. + +[source,cpp] +---- +// Configure limits via parser config +request_parser::config cfg; +cfg.headers.max_field_size = 8192; // Max bytes per header line +cfg.headers.max_fields = 100; // Max number of headers +cfg.body_limit = 1024 * 1024; // Max body size (1 MB) +---- + +=== Field Validation + +Field names must consist only of valid token characters. Field values +must not contain control characters except horizontal tab. The library +validates these constraints on every operation that creates or modifies +headers. + +== Next Steps + +Now that you understand HTTP message structure and session management, +learn how to work with the library's message containers: + +* xref:containers.adoc[Containers] — request, response, and fields types diff --git a/doc/modules/ROOT/pages/http_protocol_basics.adoc b/doc/modules/ROOT/pages/http_protocol_basics.adoc deleted file mode 100644 index 9153cbee..00000000 --- a/doc/modules/ROOT/pages/http_protocol_basics.adoc +++ /dev/null @@ -1 +0,0 @@ -= HTTP Protocol Basics diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc index 5ed994b5..7737b10b 100644 --- a/doc/modules/ROOT/pages/index.adoc +++ b/doc/modules/ROOT/pages/index.adoc @@ -9,65 +9,124 @@ = Boost.HTTP -This is a portable C++ library offering containers and algorithms for implementing -the HTTP/1.1 protocol. The format is widely used to deliver content on the Internet, -and this implementation adheres strictly to the -https://datatracker.ietf.org/doc/html/rfc9110[HTTP/1.1 RFC specification], -henceform referred to as the RFC. The library is distinguished by these -provided features: - -* Sans-I/O approach -* Requires only C++11 -* Works without exceptions -* Fast compilation, few templates -* Advanced handling of memory (RAM) - -== Sans-I/O - -While this library implements the HTTP protocol, it does so without performing -any actual network activity as the logic is completely isolated from the -underlying I/O operations. The implementation manages state, ensures RFC -compliance, and provides the application-level interface for building and -inspecting HTTP messages and their payloads, and it is necessary to use or -write the interfacing network implementation on top of HTTP. - -The companion library Boost.HTTP.IO uses Boost.HTTP to implement network -I/O using Boost.Asio. The -https://sans-io.readthedocs.io/[sans-I/O] website goes into more depth regarding -this innovative approach to designing protocol libraries. +HTTP powers the web, but implementing it correctly is surprisingly hard. Boost.HTTP +is a portable C++ library that provides containers and algorithms for the HTTP/1.1 +protocol, giving you RFC-compliant message handling without the usual implementation +headaches. + +== What This Library Does + +* Provides modifiable containers for HTTP requests and responses +* Parses incoming HTTP messages with configurable limits +* Serializes outgoing messages with automatic chunked encoding +* Handles content encodings (gzip, deflate, brotli) +* Offers an Express.js-style router for request dispatch +* Enforces RFC 9110 compliance to prevent common security issues + +== What This Library Does Not Do + +* Network I/O — this is a Sans-I/O library by design +* HTTP/2 or HTTP/3 protocol support +* TLS/SSL handling +* Cookie management or session state +* Full HTTP client/server implementation (see Boost.Beast2 for I/O) + +== Target Audience + +This library is for C++ developers who need precise control over HTTP message +handling. You should have: + +* Familiarity with TCP/IP networking concepts +* Understanding of the HTTP request/response model +* Experience with C++ move semantics and memory management + +== Design Philosophy + +The library follows a Sans-I/O architecture that separates protocol logic from +network operations. This design choice yields several benefits: + +**Reusability.** The same protocol code works with any I/O framework — Asio, +io_uring, or platform-specific APIs. Write the HTTP logic once, integrate +it anywhere. + +**Testability.** Tests run as pure function calls without sockets, timers, +or network delays. Coverage is higher, execution is faster, results are +deterministic. + +**Security.** The parser is strict by default. Malformed input that could +enable request smuggling or header injection is rejected immediately. == Requirements -* Requires Boost and a compiler supporting at least C++11 -* Link to a static or dynamically linked version of this library -* Supports `-fno-exceptions`, detected automatically +* C++11 compiler (see tested compilers below) +* Boost libraries (core, system, optional) +* Link to the static or dynamic library + +The library supports `-fno-exceptions` and detects this automatically. + +=== Tested Compilers + +* GCC: 5 to 14 (except 8.0.1) +* Clang: 3.9, 4 to 18 +* MSVC: 14.1 to 14.42 + +== Code Conventions + +Code examples in this documentation assume these declarations are in effect: + +[source,cpp] +---- +#include + +using namespace boost::http; +---- + +== Quick Example + +[source,cpp] +---- +#include + +using namespace boost::http; + +int main() +{ + // Build a request + request req(method::get, "/api/users"); + req.set(field::host, "example.com"); + req.set(field::accept, "application/json"); -== Tested Compilers + // Build a response + response res(status::ok); + res.set(field::content_type, "application/json"); + res.set(field::content_length, "42"); -Boost.HTTP is tested with the following compiler versions: + // Access the serialized form + std::cout << req.buffer(); + std::cout << res.buffer(); +} +---- -* gcc: 5 to 14 (except 8.0.1) -* clang: 3.9, 4 to 18 -* msvc: 14.1 to 14.42 +Output: -== Quality Assurance +---- +GET /api/users HTTP/1.1 +Host: example.com +Accept: application/json -The development infrastructure for the library includes these per-commit analyses: +HTTP/1.1 200 OK +Content-Type: application/json +Content-Length: 42 -* Coverage reports -* Compilation and tests on Drone.io and GitHub Actions +---- -== ABNF +== Next Steps -This documentation uses the Augmented -https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_form[Backus-Naur Form,window=blank_] -(ABNF) notation of -https://datatracker.ietf.org/doc/html/rfc5234[rfc5234,window=blank_] -to specify particular grammars used by algorithms and containers. -While a complete understanding of the notation is not a requirement for using -the library, it may help for an understanding of how valid components of HTTP -messages are defined. In particular, this is of interest to users who wish to -compose parsing algorithms using the combinators provided by the library. +* xref:http-protocol.adoc[Introduction to HTTP] — understand HTTP sessions and message flow +* xref:containers.adoc[Containers] — work with requests, responses, and fields +* xref:parsing.adoc[Parsing] — parse incoming HTTP messages +* xref:serializing.adoc[Serializing] — produce outgoing HTTP messages +* xref:router.adoc[Router] — dispatch requests to handlers == Acknowledgments diff --git a/doc/modules/ROOT/pages/message_bodies.adoc b/doc/modules/ROOT/pages/message_bodies.adoc deleted file mode 100644 index dcd89567..00000000 --- a/doc/modules/ROOT/pages/message_bodies.adoc +++ /dev/null @@ -1 +0,0 @@ -= Message Bodies diff --git a/doc/modules/ROOT/pages/parsing.adoc b/doc/modules/ROOT/pages/parsing.adoc new file mode 100644 index 00000000..aa37afe2 --- /dev/null +++ b/doc/modules/ROOT/pages/parsing.adoc @@ -0,0 +1,383 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + += Parsing + +The parser transforms raw bytes from the network into structured HTTP messages. +It handles the complexity of message framing, chunked transfer encoding, and +content decoding so you can focus on application logic. + +== Parser Types + +The library provides two parser types: + +[cols="1a,4a"] +|=== +|Type|Description + +|`request_parser` +|Parses HTTP requests. Use on the server side. + +|`response_parser` +|Parses HTTP responses. Use on the client side. + +|=== + +Both types share the same interface through the `parser` base class. The +difference is in start-line parsing: requests have a method and target, +responses have a status code. + +== Basic Usage + +Parsing follows a pull model. You provide input buffers, call `parse()`, +and check the result. Here's the typical flow: + +[source,cpp] +---- +// 1. Install parser service with configuration +capy::polystore ctx; +request_parser::config cfg; +install_parser_service(ctx, cfg); + +// 2. Create parser +request_parser pr(ctx); + +// 3. Prepare for a new stream +pr.reset(); + +// 4. Start parsing a message +pr.start(); + +// 5. Feed data and parse +auto buf = pr.prepare(); +std::size_t n = socket.read_some(buf); +pr.commit(n); + +system::error_code ec; +pr.parse(ec); + +// 6. Check result +if (pr.got_header()) +{ + // Headers are available + auto const& req = pr.get(); +} + +if (pr.is_complete()) +{ + // Entire message parsed + auto body = pr.body(); +} +---- + +== Configuration + +Parser behavior is controlled through configuration installed on a context: + +[source,cpp] +---- +capy::polystore ctx; + +request_parser::config cfg; + +// Header limits +cfg.headers.max_field_size = 8192; // Max bytes per header line +cfg.headers.max_fields = 100; // Max number of headers +cfg.headers.max_start_line = 8192; // Max start line length + +// Body limits +cfg.body_limit = 64 * 1024; // Default: 64KB + +// Content decoding +cfg.apply_gzip_decoder = true; // Enable gzip decompression +cfg.apply_deflate_decoder = true; // Enable deflate decompression +cfg.apply_brotli_decoder = false; // Requires separate service + +// Buffer settings +cfg.min_buffer = 4096; // Minimum internal buffer +cfg.max_prepare = SIZE_MAX; // Maximum prepare() result size + +install_parser_service(ctx, cfg); +---- + +== Parsing Headers + +The parser signals when headers are complete: + +[source,cpp] +---- +request_parser pr(ctx); +pr.reset(); +pr.start(); + +// Feed data until headers are complete +while (!pr.got_header()) +{ + auto buf = pr.prepare(); + std::size_t n = socket.read_some(buf); + pr.commit(n); + + system::error_code ec; + pr.parse(ec); + + if (ec && ec != condition::need_more_input) + throw system::system_error(ec); +} + +// Access the parsed request +auto const& req = pr.get(); +std::cout << req.method_text() << " " << req.target() << "\n"; + +for (auto const& f : req) + std::cout << f.name << ": " << f.value << "\n"; +---- + +== Parsing the Body + +After headers are parsed, you have several options for handling the body. + +=== In-Place Body + +The simplest approach reads the body into the parser's internal buffer: + +[source,cpp] +---- +// After headers are complete +while (!pr.is_complete()) +{ + auto buf = pr.prepare(); + std::size_t n = socket.read_some(buf); + pr.commit(n); + + system::error_code ec; + pr.parse(ec); + if (ec && ec != condition::need_more_input) + throw system::system_error(ec); +} + +// Access the complete body +core::string_view body = pr.body(); +---- + +This works well for small bodies that fit in the parser's buffer. + +=== Dynamic Buffer Body + +For larger bodies, attach an elastic buffer: + +[source,cpp] +---- +// After headers complete +std::string body_storage; +pr.set_body(capy::string_buffer(&body_storage)); + +// Continue parsing - body goes into body_storage +while (!pr.is_complete()) +{ + auto buf = pr.prepare(); + std::size_t n = socket.read_some(buf); + pr.commit(n); + + system::error_code ec; + pr.parse(ec); + if (ec && ec != condition::need_more_input) + throw system::system_error(ec); +} + +// Body is now in body_storage +std::cout << "Body size: " << body_storage.size() << "\n"; +---- + +=== Sink Body + +For streaming or when you need custom processing, use a sink: + +[source,cpp] +---- +// Write body directly to file +pr.set_body("upload.bin", file_mode::write_new); + +while (!pr.is_complete()) +{ + auto buf = pr.prepare(); + std::size_t n = socket.read_some(buf); + pr.commit(n); + + system::error_code ec; + pr.parse(ec); + if (ec && ec != condition::need_more_input) + throw system::system_error(ec); +} +---- + +=== Pull-Based Body + +For maximum control, pull body chunks manually: + +[source,cpp] +---- +while (!pr.is_complete()) +{ + auto buf = pr.prepare(); + std::size_t n = socket.read_some(buf); + pr.commit(n); + + system::error_code ec; + pr.parse(ec); + if (ec && ec != condition::need_more_input) + throw system::system_error(ec); + + // Process available body data + auto body_bufs = pr.pull_body(); + process(body_bufs); + pr.consume_body(capy::buffer_size(body_bufs)); +} +---- + +== Body Size Limits + +Override the default body limit for a specific message: + +[source,cpp] +---- +// After headers complete +if (req.exists(field::content_type)) +{ + auto ct = req.at(field::content_type); + if (ct.starts_with("multipart/form-data")) + { + // Allow larger uploads + pr.set_body_limit(100 * 1024 * 1024); // 100 MB + } +} +---- + +== Content Decoding + +When enabled in configuration, the parser automatically decompresses +gzip, deflate, and brotli encoded bodies: + +[source,cpp] +---- +request_parser::config cfg; +cfg.apply_gzip_decoder = true; +cfg.apply_deflate_decoder = true; +// For brotli, also install the brotli decode service +---- + +The `Content-Encoding` header is processed automatically. The body +you receive is the decoded content. + +== Handling Multiple Messages + +For persistent connections, parse multiple messages in sequence: + +[source,cpp] +---- +request_parser pr(ctx); +pr.reset(); // Once per connection + +while (connection_open) +{ + pr.start(); // Once per message + + // Parse this message + while (!pr.is_complete()) + { + auto buf = pr.prepare(); + std::size_t n = socket.read_some(buf); + if (n == 0) + { + pr.commit_eof(); + break; + } + pr.commit(n); + + system::error_code ec; + pr.parse(ec); + + if (ec == error::end_of_stream) + break; // Clean connection close + if (ec && ec != condition::need_more_input) + throw system::system_error(ec); + } + + // Process the request + handle_request(pr.get()); +} +---- + +== Error Handling + +The parser reports errors through `system::error_code`: + +[source,cpp] +---- +system::error_code ec; +pr.parse(ec); + +if (ec == condition::need_more_input) +{ + // Not an error - need more data +} +else if (ec == error::end_of_stream) +{ + // Clean EOF - no more messages +} +else if (ec) +{ + // Parse error + std::cerr << "Parse error: " << ec.message() << "\n"; +} +---- + +Common errors include: + +* Invalid start line syntax +* Invalid header syntax +* Header size exceeded +* Body size exceeded +* Incomplete chunked encoding + +== Custom Sinks + +Implement the `sink` interface to handle body data your way: + +[source,cpp] +---- +class my_sink : public sink +{ + std::vector& output_; + +public: + explicit my_sink(std::vector& out) + : output_(out) + { + } + +protected: + results on_write(capy::const_buffer b, bool more) override + { + auto p = static_cast(b.data()); + output_.insert(output_.end(), p, p + b.size()); + return { {}, b.size() }; + } +}; + +// Use it +std::vector body_data; +pr.set_body(body_data); +---- + +== Next Steps + +Now that you can parse incoming messages, learn how to produce outgoing +messages: + +* xref:serializing.adoc[Serializing] — produce HTTP messages for transmission diff --git a/doc/modules/ROOT/pages/router.adoc b/doc/modules/ROOT/pages/router.adoc new file mode 100644 index 00000000..d0158531 --- /dev/null +++ b/doc/modules/ROOT/pages/router.adoc @@ -0,0 +1,548 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + += Router + +The router is an Express.js-style request dispatcher for HTTP servers. You +register handlers for path patterns and HTTP methods, then dispatch incoming +requests. The router matches requests against registered routes and invokes +the appropriate handlers in order. + +Like the rest of this library, the router is Sans-I/O: it handles routing and +response generation without performing network operations. A separate I/O +layer manages connections and drives the protocol. + +== Quick Start + +[source,cpp] +---- +#include + +using namespace boost::http; + +int main() +{ + router r; + + r.add(method::get, "/hello", + [](route_params& p) + { + p.status(status::ok); + p.set_body("Hello, world!"); + return route::send; + }); + + r.add(method::get, "/users/:id", + [](route_params& p) + { + auto id = p.param("id"); + p.status(status::ok); + p.set_body("User: " + std::string(id)); + return route::send; + }); + + // Flatten for dispatch + flat_router fr(std::move(r)); + + // Dispatch a request + route_params params; + // ... populate params from parsed request ... + auto result = co_await fr.dispatch(method::get, url, params); +} +---- + +== Route Handlers + +A handler is any callable that accepts a reference to the params object and +returns a `route_result`: + +[source,cpp] +---- +route_result handler(route_params& p); +---- + +The return value tells the router what to do next: + +[cols="1,3"] +|=== +|Value |Meaning + +|`route::send` +|Response is ready. Send it to the client. + +|`route::next` +|Continue to the next handler in the chain. + +|`route::next_route` +|Skip remaining handlers in this route, try the next route. + +|`route::close` +|Close the connection after sending any response. + +|`route::complete` +|Request fully handled; no response to send. + +|`route::detach` +|Handler took ownership of the session (advanced). +|=== + +Most handlers return `route::send` when they produce a response, or +`route::next` when they perform setup work and defer to later handlers. + +== Adding Routes + +Use `add()` to register a handler for a specific HTTP method and path: + +[source,cpp] +---- +router.add(method::get, "/users", get_users); +router.add(method::post, "/users", create_user); +router.add(method::get, "/users/:id", get_user); +router.add(method::put, "/users/:id", update_user); +router.add(method::delete_, "/users/:id", delete_user); +---- + +Use `all()` to match any HTTP method: + +[source,cpp] +---- +router.all("/status", check_status); +---- + +== Path Patterns + +Route paths support named parameters and wildcards: + +[cols="1,2,2"] +|=== +|Pattern |Example URL |Matches + +|`/users` +|`/users` +|Exact match + +|`/users/:id` +|`/users/42` +|Named parameter `id` = `"42"` + +|`/users/:id/posts/:pid` +|`/users/42/posts/7` +|Multiple parameters + +|`/files/*` +|`/files/docs/readme.txt` +|Wildcard captures remainder +|=== + +Access captured parameters in handlers: + +[source,cpp] +---- +r.add(method::get, "/users/:id/posts/:pid", + [](route_params& p) + { + auto user_id = p.param("id"); + auto post_id = p.param("pid"); + // ... + return route::send; + }); +---- + +== Fluent Route Interface + +The `route()` method returns a fluent interface for registering multiple +handlers on the same path: + +[source,cpp] +---- +router.route("/users/:id") + .add(method::get, get_user) + .add(method::put, update_user) + .add(method::delete_, delete_user) + .all(log_access); +---- + +This is equivalent to calling `add()` separately for each method, but more +concise when a path has multiple handlers. + +== Handler Chaining + +Multiple handlers can be registered for the same route. They execute in +order until one returns something other than `route::next`: + +[source,cpp] +---- +router.add(method::get, "/admin", + [](route_params& p) + { + // Authentication check + if (!is_authenticated(p)) + { + p.status(status::unauthorized); + p.set_body("Unauthorized"); + return route::send; + } + return route::next; + }, + [](route_params& p) + { + // Authorization check + if (!is_admin(p)) + { + p.status(status::forbidden); + p.set_body("Forbidden"); + return route::send; + } + return route::next; + }, + [](route_params& p) + { + // Business logic + p.status(status::ok); + p.set_body("Admin panel"); + return route::send; + }); +---- + +This pattern separates concerns: authentication, authorization, and business +logic each have their own handler. + +== Middleware + +Use `use()` to add middleware that runs for all routes matching a prefix: + +[source,cpp] +---- +// Global middleware (runs for all routes) +router.use( + [](route_params& p) + { + p.res.set(field::server, "MyApp/1.0"); + return route::next; + }); + +// Path-specific middleware +router.use("/api", + [](route_params& p) + { + // Verify API key + if (!p.req.exists(field::authorization)) + { + p.status(status::unauthorized); + return route::send; + } + return route::next; + }); +---- + +Middleware registered with `use()` matches prefix patterns. Middleware +attached to `"/api"` runs for `"/api"`, `"/api/users"`, and `"/api/data"`. + +== Error Handlers + +Register error handlers to catch failures during request processing: + +[source,cpp] +---- +// Global error handler +router.use( + [](route_params& p, system::error_code ec) + { + p.status(status::internal_server_error); + p.set_body("Error: " + ec.message()); + return route::send; + }); + +// Path-specific error handler +router.use("/api", + [](route_params& p, system::error_code ec) + { + p.status(status::internal_server_error); + p.res.set(field::content_type, "application/json"); + p.set_body("{\"error\":\"" + ec.message() + "\"}"); + return route::send; + }); +---- + +Error handlers receive the error code that caused the failure. Return +`route::next` to pass to the next error handler. + +== Exception Handlers + +Register exception handlers with `except()`: + +[source,cpp] +---- +router.except( + [](route_params& p, std::exception_ptr ep) + { + try + { + std::rethrow_exception(ep); + } + catch (std::exception const& e) + { + p.status(status::internal_server_error); + p.set_body(e.what()); + } + return route::send; + }); +---- + +== Router Options + +Configure matching behavior when constructing the router: + +[source,cpp] +---- +router r( + router_options() + .case_sensitive(true) // Paths are case-sensitive + .strict(true)); // Trailing slash matters +---- + +[cols="1,1,3"] +|=== +|Option |Default |Description + +|`case_sensitive` +|`false` +|When true, `/Users` and `/users` are different routes. + +|`strict` +|`false` +|When true, `/api` and `/api/` are different routes. + +|`merge_params` +|`false` +|When true, inherit parameters from parent routers. +|=== + +== Nested Routers + +Mount routers within routers for modular organization: + +[source,cpp] +---- +// API routes +router api; +api.add(method::get, "/users", list_users); +api.add(method::get, "/posts", list_posts); + +// Admin routes +router admin; +admin.add(method::get, "/stats", show_stats); +admin.add(method::post, "/config", update_config); + +// Main router +router app; +app.use("/api", std::move(api)); +app.use("/admin", std::move(admin)); +---- + +Routes are composed: `/api/users` matches `list_users`, `/admin/stats` +matches `show_stats`. + +== Dispatching Requests + +Convert the router to a `flat_router` for efficient dispatch: + +[source,cpp] +---- +// Build routes +router r; +// ... add routes ... + +// Flatten for dispatch (do this once) +flat_router fr(std::move(r)); + +// Dispatch requests +route_params p; +p.url = parsed_url; +p.req = parsed_request; + +auto result = co_await fr.dispatch( + p.req.method(), + p.url, + p); + +switch (result) +{ +case route::send: + // p.res contains response to send + co_await send_response(p.res); + break; + +case route::next: + // No handler matched - send 404 + send_not_found(); + break; + +case route::close: + // Close connection + break; +} +---- + +The `flat_router` pre-processes routes into a structure optimized for +dispatch performance. Create it once after all routes are registered. + +== The route_params Object + +The standard `route_params` type contains everything handlers need: + +[source,cpp] +---- +struct route_params : route_params_base +{ + urls::url_view url; // Parsed request target + http::request req; // Request headers + http::response res; // Response to build + http::request_parser parser; // For body access + http::serializer serializer; // For response output + capy::datastore route_data; // Per-request storage + capy::datastore session_data;// Per-session storage + suspender suspend; // For async operations + capy::any_executor_ref ex; // Session executor +}; +---- + +Convenience methods simplify common operations: + +[source,cpp] +---- +r.add(method::post, "/data", + [](route_params& p) + { + // Set response status + p.status(status::created); + + // Set response body + p.set_body("Created"); + + return route::send; + }); +---- + +== Async Operations + +Handlers can perform async work using `suspend`: + +[source,cpp] +---- +r.add(method::get, "/slow", + [](route_params& p) + { + return p.suspend( + [](resumer resume) + { + // Called synchronously + schedule_async_work([resume]() + { + // Called later, on completion + resume(route::send); + }); + }); + }); +---- + +== Reading Request Bodies + +Use `read_body` for async body reading: + +[source,cpp] +---- +r.add(method::post, "/upload", + [](route_params& p) + { + return p.read_body( + capy::string_body_sink(), + [&p](std::string body) + { + // Body is now available + process_upload(body); + p.status(status::ok); + return route::send; + }); + }); +---- + +== Complete Example + +[source,cpp] +---- +#include + +using namespace boost::http; + +int main() +{ + router r; + + // Middleware + r.use([](route_params& p) + { + p.res.set(field::server, "MyApp/1.0"); + return route::next; + }); + + // Health check + r.add(method::get, "/health", + [](route_params& p) + { + p.status(status::ok); + p.set_body("OK"); + return route::send; + }); + + // API routes + r.route("/api/users") + .add(method::get, + [](route_params& p) + { + p.status(status::ok); + p.res.set(field::content_type, "application/json"); + p.set_body("[{\"id\":1},{\"id\":2}]"); + return route::send; + }) + .add(method::post, + [](route_params& p) + { + p.status(status::created); + return route::send; + }); + + r.add(method::get, "/api/users/:id", + [](route_params& p) + { + auto id = p.param("id"); + p.status(status::ok); + p.set_body("{\"id\":" + std::string(id) + "}"); + return route::send; + }); + + // Error handler + r.use([](route_params& p, system::error_code ec) + { + p.status(status::internal_server_error); + p.set_body(ec.message()); + return route::send; + }); + + // Flatten and dispatch + flat_router fr(std::move(r)); + + // ... integrate with your I/O layer ... +} +---- + +== Next Steps + +* xref:bcrypt.adoc[BCrypt] — secure password hashing for authentication +* xref:sans_io_philosophy.adoc[Sans-I/O Philosophy] — design rationale diff --git a/doc/modules/ROOT/pages/serializing.adoc b/doc/modules/ROOT/pages/serializing.adoc new file mode 100644 index 00000000..b94fb5cd --- /dev/null +++ b/doc/modules/ROOT/pages/serializing.adoc @@ -0,0 +1,395 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http +// + += Serializing + +The serializer transforms HTTP messages into bytes for transmission. It handles +chunked encoding, content compression, and the `Expect: 100-continue` handshake +automatically. + +== Basic Usage + +Serialization follows a push model. You provide a message, then pull output +buffers until complete: + +[source,cpp] +---- +// 1. Install serializer service with configuration +capy::polystore ctx; +serializer::config cfg; +install_serializer_service(ctx, cfg); + +// 2. Create serializer +serializer sr(ctx); + +// 3. Start with a message +response res(status::ok); +res.set(field::content_type, "text/plain"); +sr.start(res, "Hello, world!"); + +// 4. Pull output and write to socket +while (!sr.is_done()) +{ + auto result = sr.prepare(); + if (!result) + throw system::system_error(result.error()); + + socket.write(*result); + sr.consume(capy::buffer_size(*result)); +} +---- + +== Configuration + +Serializer behavior is controlled through configuration: + +[source,cpp] +---- +capy::polystore ctx; + +serializer::config cfg; + +// Content encoding (compression) +cfg.apply_gzip_encoder = true; // Enable gzip compression +cfg.apply_deflate_encoder = false; // Enable deflate compression +cfg.apply_brotli_encoder = false; // Requires separate service + +// Compression settings +cfg.zlib_comp_level = 6; // 0-9 (0=none, 9=best) +cfg.zlib_window_bits = 15; // 9-15 +cfg.zlib_mem_level = 8; // 1-9 + +// Brotli settings (if enabled) +cfg.brotli_comp_quality = 5; // 0-11 +cfg.brotli_comp_window = 18; // 10-24 + +// Buffer settings +cfg.payload_buffer = 8192; // Internal buffer size +cfg.max_type_erase = 1024; // Space for source storage + +install_serializer_service(ctx, cfg); +---- + +== Body Sources + +The serializer supports several ways to provide message body content. + +=== No Body + +For messages without a body (HEAD responses, 204 No Content, etc.): + +[source,cpp] +---- +response res(status::no_content); +sr.start(res); // No body argument +---- + +=== Buffer Sequence Body + +Provide the body as in-memory buffers: + +[source,cpp] +---- +response res(status::ok); +res.set(field::content_type, "text/plain"); +res.set_content_length(13); + +std::string body = "Hello, world!"; +sr.start(res, capy::buffer(body)); +---- + +Multiple buffers are supported: + +[source,cpp] +---- +std::string part1 = "Hello, "; +std::string part2 = "world!"; +std::array buffers = { + capy::buffer(part1), + capy::buffer(part2) +}; + +sr.start(res, buffers); +---- + +=== Source Body + +For large or dynamic bodies, use a source: + +[source,cpp] +---- +// From file +capy::file f("large_file.bin", capy::file_mode::scan); +res.set_payload_size(f.size()); +sr.start(res, std::move(f)); + +// Pull output +while (!sr.is_done()) +{ + auto result = sr.prepare(); + if (!result) + throw system::system_error(result.error()); + + socket.write(*result); + sr.consume(capy::buffer_size(*result)); +} +---- + +=== Stream Body + +For maximum flexibility, push body data incrementally: + +[source,cpp] +---- +response res(status::ok); +res.set(field::content_type, "application/octet-stream"); +// No content-length - will use chunked encoding + +auto stream = sr.start_stream(res); + +// Push body data as it becomes available +while (has_more_data()) +{ + // Get buffer to write into + auto buf = stream.prepare(); + std::size_t n = generate_data(buf); + stream.commit(n); + + // Output is available + auto result = sr.prepare(); + if (result) + { + socket.write(*result); + sr.consume(capy::buffer_size(*result)); + } +} + +// Signal end of body +stream.close(); + +// Flush remaining output +while (!sr.is_done()) +{ + auto result = sr.prepare(); + if (result) + { + socket.write(*result); + sr.consume(capy::buffer_size(*result)); + } +} +---- + +== Chunked Encoding + +The serializer uses chunked transfer encoding automatically when: + +* No `Content-Length` header is set +* The body size is unknown at start time + +[source,cpp] +---- +response res(status::ok); +res.set(field::content_type, "text/event-stream"); +// No Content-Length - chunked encoding will be used + +auto stream = sr.start_stream(res); + +// Send chunks as events occur +for (auto& event : events) +{ + auto buf = stream.prepare(); + auto n = format_event(event, buf); + stream.commit(n); + + // Flush to client + auto result = sr.prepare(); + socket.write(*result); + sr.consume(capy::buffer_size(*result)); +} + +stream.close(); +---- + +== Content Encoding + +When compression is enabled and the client accepts it, the serializer +compresses the body automatically: + +[source,cpp] +---- +// Enable in config +serializer::config cfg; +cfg.apply_gzip_encoder = true; + +// Check Accept-Encoding from request +if (request_accepts_gzip(req)) +{ + res.set(field::content_encoding, "gzip"); + // Body will be compressed +} + +sr.start(res, large_body); +---- + +== Expect: 100-continue + +The serializer handles the 100-continue handshake: + +[source,cpp] +---- +response res(status::ok); +// ... set headers ... +sr.start(res, body_source); + +while (!sr.is_done()) +{ + auto result = sr.prepare(); + + if (result.error() == error::expect_100_continue) + { + // Client wants confirmation before sending body + // Send 100 Continue response + response cont(status::continue_); + serializer sr100(ctx); + sr100.start(cont); + // ... write sr100 output ... + + // Continue with original response + continue; + } + + if (!result) + throw system::system_error(result.error()); + + socket.write(*result); + sr.consume(capy::buffer_size(*result)); +} +---- + +== Error Handling + +Serializer errors are reported through the result type: + +[source,cpp] +---- +auto result = sr.prepare(); + +if (!result) +{ + auto ec = result.error(); + + if (ec == error::expect_100_continue) + { + // Not an error - handle 100-continue + } + else if (ec == error::need_data) + { + // Stream body needs more input + } + else + { + // Real error (e.g., source read failure) + std::cerr << "Serialization error: " << ec.message() << "\n"; + } +} +---- + +== Custom Sources + +Implement the `source` interface for custom body generation: + +[source,cpp] +---- +class my_source : public source +{ + std::function generator_; + std::string current_; + std::size_t pos_ = 0; + bool done_ = false; + +public: + explicit my_source(std::function gen) + : generator_(std::move(gen)) + { + } + +protected: + results on_read(capy::mutable_buffer b) override + { + results rv; + + while (b.size() > 0 && !done_) + { + // Refill current buffer if empty + if (pos_ >= current_.size()) + { + current_ = generator_(); + pos_ = 0; + if (current_.empty()) + { + done_ = true; + rv.finished = true; + break; + } + } + + // Copy to output + auto avail = current_.size() - pos_; + auto n = std::min(b.size(), avail); + std::memcpy(b.data(), current_.data() + pos_, n); + pos_ += n; + rv.bytes += n; + b = capy::mutable_buffer( + static_cast(b.data()) + n, + b.size() - n); + } + + return rv; + } +}; +---- + +== Multiple Messages + +Reuse the serializer for multiple messages on the same connection: + +[source,cpp] +---- +serializer sr(ctx); + +for (auto& request : requests) +{ + // Process request, build response + response res = handle(request); + + // Serialize response + sr.start(res, response_body); + + while (!sr.is_done()) + { + auto result = sr.prepare(); + if (result) + { + socket.write(*result); + sr.consume(capy::buffer_size(*result)); + } + } + + // Reset for next message + sr.reset(); +} +---- + +== Next Steps + +With parsing and serialization covered, you can now build complete HTTP +processing pipelines. For server applications, the router provides +request dispatch: + +* xref:router.adoc[Router] — dispatch requests to handlers diff --git a/doc/modules/ROOT/pages/server/router.adoc b/doc/modules/ROOT/pages/server/router.adoc deleted file mode 100644 index 761be744..00000000 --- a/doc/modules/ROOT/pages/server/router.adoc +++ /dev/null @@ -1,311 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/http -// - -= Server - -This library provides an Express.js-style request dispatcher for HTTP servers. -The interface is Sans-I/O: it handles routing and response generation without -performing network operations. A separate I/O framework such as Boost.Beast2 -manages connections and drives the protocol. - -== Router - -cpp:router[] is a class template that implements request routing. It stores a -collection of routes, each with a path pattern, HTTP method, and one or more -handlers. Callers (typically a framework) use the router to dispatch an HTTP -request to a handler. - -=== Route Handler - -Route handlers have this signature: -[source,cpp] ----- -route_result handler( route_params& rp ); ----- - -After this chapter you can: dispatch HTTP requests to handlers based on method -and path, chain handlers together, and control request flow. - -== Overview - -The router is an Express.js-style request dispatcher. You register handlers -for path patterns and HTTP methods, then dispatch incoming requests. The -router matches the request against registered routes and invokes the -appropriate handlers in order. - -[source,cpp] ----- -#include - -using namespace boost::http; - -router r; - -r.add(method::get, "/hello", - [](route_params& p) - { - p.status(status::ok); - p.set_body("Hello, world!"); - return route::send; - }); ----- - -The library provides `route_params` as the standard parameters type. It -contains the request, response, URL, and other context needed by handlers. - -== Handlers - -A handler is any callable that accepts a reference to the params object and -returns a `route_result`: - -[source,cpp] ----- -route_result handler(route_params& p); ----- - -The return value tells the router what to do next: - -[cols="1,3"] -|=== -|Value |Meaning - -|`route::send` -|Response is ready. Send it to the client. - -|`route::next` -|Continue to the next handler in the chain. - -|`route::next_route` -|Skip remaining handlers in this route, try the next route. - -|`route::close` -|Close the connection after sending any response. - -|`route::complete` -|Request fully handled; no response to send. - -|`route::detach` -|Handler took ownership of the session (advanced). -|=== - -Most handlers return `route::send` when they produce a response, or -`route::next` when they perform setup work and defer to later handlers. - -== Adding Routes - -Use `add()` to register a handler for a specific HTTP method and path: - -[source,cpp] ----- -router.add(method::get, "/users", get_users); -router.add(method::post, "/users", create_user); -router.add(method::get, "/users/:id", get_user); -router.add(method::put, "/users/:id", update_user); -router.add(method::delete_, "/users/:id", delete_user); ----- - -Use `all()` to match any HTTP method: - -[source,cpp] ----- -router.all("/status", check_status); ----- - -== Fluent Route Interface - -The `route()` method returns a fluent interface for registering multiple -handlers on the same path: - -[source,cpp] ----- -router.route("/users/:id") - .add(method::get, get_user) - .add(method::put, update_user) - .add(method::delete_, delete_user) - .all(log_access); ----- - -This is equivalent to calling `add()` separately for each method, but more -concise when a path has multiple method handlers. - -== Dispatching Requests - -Call `dispatch()` to route a request: - -[source,cpp] ----- -route_params p; -// ... populate p.req, p.url from parsed request ... - -route_result rv = router.dispatch(method::get, p.url, p); - -if(rv == route::send) -{ - // p.res contains the response to send -} -else if(rv == route::next) -{ - // No handler matched; send 404 -} ----- - -The router tries each matching route in registration order. If a handler -returns `route::next`, the router continues to the next handler. If all -handlers return `route::next`, dispatch returns `route::next` to indicate -no handler produced a response. - -== Handler Chaining - -Multiple handlers can be registered for the same route. They execute in -order until one returns something other than `route::next`: - -[source,cpp] ----- -router.add(method::get, "/admin", - [](route_params& p) - { - // Authentication check - if(!is_authenticated(p)) - { - p.status(status::unauthorized); - p.set_body("Unauthorized"); - return route::send; - } - return route::next; - }, - [](route_params& p) - { - // Authorization check - if(!is_admin(p)) - { - p.status(status::forbidden); - p.set_body("Forbidden"); - return route::send; - } - return route::next; - }, - [](route_params& p) - { - // Actual handler - p.status(status::ok); - p.set_body("Admin panel"); - return route::send; - }); ----- - -This pattern separates concerns: authentication, authorization, and business -logic each have their own handler. - -== Path Patterns - -Route paths support named parameters and wildcards: - -[cols="1,2,2"] -|=== -|Pattern |Example URL |Matches - -|`/users` -|`/users` -|Exact match - -|`/users/:id` -|`/users/42` -|Named parameter `id` = `"42"` - -|`/files/*` -|`/files/docs/readme.txt` -|Wildcard suffix -|=== - -Path matching is case-insensitive by default. Use `router_options` to change -this behavior. - -== Router Options - -Configure matching behavior when constructing the router: - -[source,cpp] ----- -router r( - router_options() - .case_sensitive(true) // Paths are case-sensitive - .strict(true)); // Trailing slash matters ----- - -[cols="1,1,3"] -|=== -|Option |Default |Description - -|`case_sensitive` -|`false` -|When true, `/Users` and `/users` are different routes. - -|`strict` -|`false` -|When true, `/api` and `/api/` are different routes. - -|`merge_params` -|`false` -|When true, inherit parameters from parent routers. -|=== - -== Complete Example - -[source,cpp] ----- -#include - -using namespace boost::http; - -int main() -{ - router r; - - // Health check endpoint - r.add(method::get, "/health", - [](route_params& p) - { - p.status(status::ok); - p.set_body("OK"); - return route::send; - }); - - // API routes - r.route("/api/echo") - .add(method::post, - [](route_params& p) - { - p.status(status::ok); - // Echo back the request body - return route::send; - }) - .add(method::get, - [](route_params& p) - { - p.status(status::method_not_allowed); - return route::send; - }); - - // Dispatch a request using flat_router - flat_router fr(std::move(r)); - route_params p; - auto rv = co_await fr.dispatch( - method::get, - urls::url_view("/health"), - p); - - // rv == route::send, p.res contains "OK" -} ----- - -== See Also - -* xref:server/middleware.adoc[Middleware] - Path-based handler chains -* xref:server/errors.adoc[Error Handling] - Error and exception handlers -* xref:server/params.adoc[Route Parameters] - The `route_params` object From 685db0bbb4e53185f45deb307b2a7a06d253329e Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Sun, 18 Jan 2026 11:24:01 -0800 Subject: [PATCH 07/12] Add bcrypt from Capy --- doc/modules/ROOT/nav.adoc | 29 +++++++++++++++++++++-------- test/limits/CMakeLists.txt | 7 +++++++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index cf72daa8..d7c9236b 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -1,9 +1,22 @@ -* xref:index.adoc[Introduction] -* xref:http-protocol.adoc[Introduction to HTTP] -* xref:containers.adoc[Containers] -* xref:parsing.adoc[Parsing] -* xref:serializing.adoc[Serializing] -* xref:router.adoc[Router] -* xref:bcrypt.adoc[BCrypt Password Hashing] -* xref:sans_io_philosophy.adoc[Sans-I/O Philosophy] +* xref:1.primer.adoc[] +* xref:2.messages.adoc[] +* xref:sans_io_philosophy.adoc[] +* xref:http_protocol_basics.adoc[] +// * xref:header_containers.adoc[] +* xref:message_bodies.adoc[] +* Serializing +* Parsing +* xref:Message.adoc[] +* Server +** xref:server/router.adoc[Router] +** xref:server/bcrypt.adoc[BCrypt Password Hashing] +// ** xref:server/middleware.adoc[Middleware] +// ** xref:server/errors.adoc[Error Handling] +// ** xref:server/params.adoc[Route Parameters] +// ** xref:server/advanced.adoc[Advanced Topics] +// ** xref:server/cors.adoc[CORS] +* Design Requirements +** xref:design_requirements/serializer.adoc[Serializer] +** xref:design_requirements/parser.adoc[Parser] +// * xref:reference:boost/http.adoc[Reference] * xref:reference.adoc[Reference] diff --git a/test/limits/CMakeLists.txt b/test/limits/CMakeLists.txt index 7c2835cf..806ace39 100644 --- a/test/limits/CMakeLists.txt +++ b/test/limits/CMakeLists.txt @@ -35,5 +35,12 @@ target_link_libraries(boost_http_limits PRIVATE target_link_libraries(boost_http_limits INTERFACE Boost::http) +# bcrypt requires platform-specific libraries +if (WIN32) + target_link_libraries(boost_http_limits PRIVATE bcrypt) +elseif (APPLE) + target_link_libraries(boost_http_limits PRIVATE "-framework Security") +endif () + add_test(NAME boost_http_limits COMMAND boost_http_limits) add_dependencies(tests boost_http_limits) From f2534400127e5860858715bbe446ec70d5bb5f3e Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Sun, 18 Jan 2026 13:34:06 -0800 Subject: [PATCH 08/12] flat routers are shared --- include/boost/http/server/flat_router.hpp | 5 +- src/server/flat_router.cpp | 8 +-- test/unit/server/flat_router.cpp | 66 ++++++++++++++++++++++- 3 files changed, 67 insertions(+), 12 deletions(-) diff --git a/include/boost/http/server/flat_router.hpp b/include/boost/http/server/flat_router.hpp index 4fb78429..11106423 100644 --- a/include/boost/http/server/flat_router.hpp +++ b/include/boost/http/server/flat_router.hpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -39,11 +40,9 @@ class BOOST_HTTP_DECL flat_router { struct impl; - impl* impl_; + std::shared_ptr impl_; public: - ~flat_router(); - flat_router( detail::router_base&&); diff --git a/src/server/flat_router.cpp b/src/server/flat_router.cpp index ca01718e..e9c6a17e 100644 --- a/src/server/flat_router.cpp +++ b/src/server/flat_router.cpp @@ -338,16 +338,10 @@ struct flat_router::impl //------------------------------------------------ -flat_router:: -~flat_router() -{ - delete impl_; -} - flat_router:: flat_router( detail::router_base&& src) - : impl_(new impl) + : impl_(std::make_shared()) { impl_->flatten(*src.impl_); } diff --git a/test/unit/server/flat_router.cpp b/test/unit/server/flat_router.cpp index c3006618..6505b0af 100644 --- a/test/unit/server/flat_router.cpp +++ b/test/unit/server/flat_router.cpp @@ -10,6 +10,11 @@ // Test that header file is self-contained. #include +// Full functional tests are in beast2/test/unit/server/router.cpp + +#include +#include + #include "test_suite.hpp" namespace boost { @@ -17,10 +22,67 @@ namespace http { struct flat_router_test { + using params = route_params_base; + using test_router = router; + + void testCopyConstruction() + { + auto counter = std::make_shared(0); + test_router r; + r.all("/", [counter](params&) -> capy::task + { + ++(*counter); + co_return route_result{}; + }); + + flat_router fr1(std::move(r)); + flat_router fr2(fr1); + + params req; + capy::run_sync()(fr1.dispatch( + http::method::get, urls::url_view("/"), req)); + BOOST_TEST_EQ(*counter, 1); + + capy::run_sync()(fr2.dispatch( + http::method::get, urls::url_view("/"), req)); + BOOST_TEST_EQ(*counter, 2); + } + + void testCopyAssignment() + { + auto counter = std::make_shared(0); + test_router r; + r.all("/", [counter](params&) -> capy::task + { + ++(*counter); + co_return route_result{}; + }); + + flat_router fr1(std::move(r)); + + test_router r2; + r2.all("/", [](params&) -> capy::task + { + co_return route_result{}; + }); + flat_router fr2(std::move(r2)); + + fr2 = fr1; + + params req; + capy::run_sync()(fr1.dispatch( + http::method::get, urls::url_view("/"), req)); + BOOST_TEST_EQ(*counter, 1); + + capy::run_sync()(fr2.dispatch( + http::method::get, urls::url_view("/"), req)); + BOOST_TEST_EQ(*counter, 2); + } + void run() { - // Header compilation test only. - // Functional tests are in router.cpp. + testCopyConstruction(); + testCopyAssignment(); } }; From 00131e21b2a8d4012b8eb18af2dc392f6647fafc Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Sun, 18 Jan 2026 14:21:48 -0800 Subject: [PATCH 09/12] Tidy up router static handler checks --- include/boost/http/server/router.hpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/include/boost/http/server/router.hpp b/include/boost/http/server/router.hpp index 47800dac..0dae5efb 100644 --- a/include/boost/http/server/router.hpp +++ b/include/boost/http/server/router.hpp @@ -488,8 +488,10 @@ class router : public detail::router_base { static_assert(handler_crvals, "pass handlers by value or std::move()"); - static_assert(handler_check<7, H1, HN...>, + static_assert(! handler_check<8, H1, HN...>, "cannot use exception handlers here"); + static_assert(handler_check<7, H1, HN...>, + "invalid handler signature"); add_impl(pattern, make_handlers( std::forward

(h1), std::forward(hn)...)); } @@ -532,8 +534,10 @@ class router : public detail::router_base { static_assert(handler_crvals, "pass handlers by value or std::move()"); - static_assert(handler_check<7, H1, HN...>, + static_assert(! handler_check<8, H1, HN...>, "cannot use exception handlers here"); + static_assert(handler_check<7, H1, HN...>, + "invalid handler signature"); use(std::string_view(), std::forward

(h1), std::forward(hn)...); } From 82810c1552430edc78216b28476b70dde5faf5d8 Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Sun, 18 Jan 2026 15:11:09 -0800 Subject: [PATCH 10/12] Remove unused include --- include/boost/http/server/route_handler.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/include/boost/http/server/route_handler.hpp b/include/boost/http/server/route_handler.hpp index 9f3c5b40..fb7b4a45 100644 --- a/include/boost/http/server/route_handler.hpp +++ b/include/boost/http/server/route_handler.hpp @@ -14,7 +14,6 @@ #include #include #include -#include #include // VFALCO forward declare? #include // VFALCO forward declare? #include // VFALCO forward declare? From 392d58d8d1784336526c690508a0bc8d67bdfe3a Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Sun, 18 Jan 2026 16:45:34 -0800 Subject: [PATCH 11/12] dll-linkage false positive and build fixes --- include/boost/http/server/flat_router.hpp | 9 +++++++++ test/limits/Jamfile | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/include/boost/http/server/flat_router.hpp b/include/boost/http/server/flat_router.hpp index 11106423..2f28ddbf 100644 --- a/include/boost/http/server/flat_router.hpp +++ b/include/boost/http/server/flat_router.hpp @@ -25,6 +25,11 @@ namespace boost { namespace http { +#ifdef BOOST_MSVC +#pragma warning(push) +#pragma warning(disable: 4251) // shared_ptr needs dll-interface +#endif + /** A flattened router optimized for dispatch performance. `flat_router` is constructed from a @ref router by flattening @@ -87,6 +92,10 @@ class BOOST_HTTP_DECL route_params_base& p) const; }; +#ifdef BOOST_MSVC +#pragma warning(pop) +#endif + } // http } // boost diff --git a/test/limits/Jamfile b/test/limits/Jamfile index 95feb4b2..bb81d758 100644 --- a/test/limits/Jamfile +++ b/test/limits/Jamfile @@ -10,6 +10,9 @@ import testing ; +# Windows CNG library for bcrypt random number generation. +lib bcrypt_sys : : bcrypt ; + project : requirements $(c11-requires) @@ -28,4 +31,6 @@ run limits.cpp /boost/http//http_sources BOOST_HTTP_NO_LIB BOOST_HTTP_STATIC_LINK static + windows:bcrypt_sys + darwin:"-framework Security" ; From e92e356e47ad566a74c6c37861a8120a6b1c6426 Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Sun, 18 Jan 2026 18:11:08 -0800 Subject: [PATCH 12/12] Remove obsolete route results and trim old APIs --- include/boost/http/server/route_handler.hpp | 165 +---------------- include/boost/http/server/router_types.hpp | 191 +------------------- src/server/cors.cpp | 3 +- src/server/route_handler.cpp | 9 - src/server/router_types.cpp | 12 -- test/unit/server/router_types.cpp | 4 - 6 files changed, 4 insertions(+), 380 deletions(-) diff --git a/include/boost/http/server/route_handler.hpp b/include/boost/http/server/route_handler.hpp index fb7b4a45..d1ee0e11 100644 --- a/include/boost/http/server/route_handler.hpp +++ b/include/boost/http/server/route_handler.hpp @@ -20,7 +20,6 @@ #include // VFALCO forward declare? #include #include -#include #include namespace boost { @@ -75,16 +74,6 @@ struct BOOST_HTTP_SYMBOL_VISIBLE */ capy::datastore session_data; - /** The suspender for this session. - - This can be used to suepend from the router and resume routing later. - */ - suspender suspend; - - /** Executor associated with the session. - */ - capy::any_executor_ref ex; - /** Destructor */ BOOST_HTTP_DECL @@ -114,161 +103,9 @@ struct BOOST_HTTP_SYMBOL_VISIBLE route_params& set_body(std::string s); - /** Read the request body and receive a value. - - This function reads the entire request body into the specified sink. - When the read operation completes, the given callback is invoked with - the result. - - The @p callback parameter must be a function object with this - equivalent signature, where `T` is the type produced by the value sink: - @code - void ( T&& t ); - @endcode - - @par Example - @code - rp.read_body( - capy::string_body_sink(), - []( std::string s ) - { - // body read successfully - }); - @endcode - - If an error or an exception occurs during the read, it is propagated - through the router to the next error or exception handler. - - @param sink The body sink to read into. - @param callback The function to call when the read completes. - @return The route result, which must be returned immediately - from the route handler. - */ - template< - class ValueSink, - class Callback> - auto - read_body( - ValueSink&& sink, - Callback&& callback) -> - route_result; - -#ifdef BOOST_HTTP_HAS_CORO - - /** Spawn a coroutine for this route. - - This function is used to spawn a coroutine - for the route handler. The coroutine is - passed a reference to the route_params object, - and when it returns, the returned route_result - is returned from this function. - - @par Example - @code - auto handler = - []( route_params& rp ) -> route_result - { - return rp.spawn( - []( route_params& rp ) -> capy::task - { - co_return route_result::next; - }); - }; - @endcode - @param coro The coroutine to spawn. - @return The route result, which must be returned immediately - from the route handler. - */ - BOOST_HTTP_DECL - auto spawn( - capy::task coro) -> - route_result; - -#endif - -protected: - std::function finish_; + //http::route_task capy::task }; -//----------------------------------------------- - -template< - class ValueSink, - class Callback> -auto -route_params:: -read_body( - ValueSink&& sink, - Callback&& callback) -> - route_result -{ - using T = typename std::decay::type; - - struct on_finish - { - T& sink; - resumer resume; - typename std::decay::type cb; - - on_finish( - T& sink_, - resumer resume_, - Callback&& cb_) - : sink(sink_) - , resume(resume_) - , cb(std::forward(cb_)) - { - } - - void operator()() - { - resume(std::move(cb)(sink.release())); - } - }; - - return suspend( - [&](resumer resume) - { - finish_ = on_finish( - this->parser.set_body( - std::forward(sink)), - resume, - std::forward(callback)); - }); -} - -//----------------------------------------------- - -#ifdef BOOST_HTTP_HAS_CORO - -/** Create a route handler from a coroutine function - - This is a convenience function for creating - route handlers from coroutine functions. - - @par Signature - The coroutine function must have this signature: - @code - capy::task( route_params& rp ); - @endcode - - @param f The coroutine function to invoke. - @return A route handler object. -*/ -inline -auto -co_route(std::function< - capy::task(route_params&)> f) -{ - return - [f_ = std::move(f)]( route_params& rp ) - { - return rp.spawn(f_(rp)); - }; -} - -#endif - } // http } // boost diff --git a/include/boost/http/server/router_types.hpp b/include/boost/http/server/router_types.hpp index 5b3f8b1b..bee8c8cd 100644 --- a/include/boost/http/server/router_types.hpp +++ b/include/boost/http/server/router_types.hpp @@ -42,33 +42,6 @@ using route_result = system::error_code; */ enum class route { - /** The handler requests that the connection be closed. - - No further requests will be processed. The caller should - close the connection once the current response, if any, - has been sent. - */ - close = 1, - - /** The handler completed the request. - - The response has been fully transmitted, and no further - handlers or routes will be invoked. The caller should continue - by either reading the next request on a persistent connection - or closing the session if it is not keep-alive. - */ - complete, - - /** The handler is suspending the route. - - When the handler returns this value, the router is placed into - a suspended state which can later be reactivated by invoking - @ref router::resume. Depending on the implementation, - this might detach the handler from the session until it is - resumed. - */ - suspend, - /** The handler declined to process the request. The handler chose not to generate a response. The caller @@ -88,15 +61,7 @@ enum class route caller stops invoking handlers in this route and resumes evaluation with the next candidate route. */ - next_route, - - /** The request was handled. - - The route handler processed the request and prepared - the response serializer. The caller will send the response - before reading the next request or closing the connection. - */ - send + next_route }; //------------------------------------------------ @@ -152,160 +117,6 @@ inline bool is_route_result( //------------------------------------------------ -class resumer; - -/** Function to suspend a route handler from its session - - This holds an reference to an implementation - which suspends the router which dispatched the handler. -*/ -class suspender -{ -public: - /** Base class of the implementation - */ - struct BOOST_HTTP_SYMBOL_VISIBLE - owner - { - BOOST_HTTP_DECL - virtual resumer do_suspend(); - virtual void do_resume(route_result const&) = 0; - virtual void do_resume(std::exception_ptr) = 0; - }; - - suspender() = default; - suspender(suspender const&) = default; - suspender& operator=(suspender const&) = default; - - explicit - suspender( - owner& who) noexcept - : p_(&who) - { - } - - /** Suspend and invoke the given function - - The function will be invoked with this equivalent signature: - @code - void( resumer ); - @endcode - - @return A @ref route_result equal to @ref route::suspend - */ - template - route_result - operator()(F&& f); - -private: - friend resumer; - // Clang doesn't consider uninstantiated templates - // when checking for unused private fields. - owner* p_ - #if defined(__clang__) - __attribute__((unused)) - #endif - = nullptr; -}; - -//------------------------------------------------ - -/** Function to resume a suspended route. - - This holds a reference to an implementation which resumes the handler's - session. The resume function is typically obtained at the time the - route is suspended. -*/ -class resumer -{ -public: - /** Constructor - - Default constructed resume functions will - be empty. An exception is thrown when - attempting to invoke an empty object. - */ - resumer() = default; - - /** Constructor - - Copies of resume functions behave the same - as the original - */ - resumer(resumer const&) = default; - - /** Assignment - - Copies of resume functions behave the same - as the original - */ - resumer& operator=(resumer const&) = default; - - /** Constructor - */ - explicit - resumer( - suspender::owner& who) noexcept - : p_(&who) - { - } - - /** Resume the session - - When a session is resumed, routing continues as if the handler - had returned the @ref route_result contained in @p rv. - - @param rv The route result to resume with. - - @throw std::invalid_argument If the object is empty. - */ - void operator()( - route_result const& rv) const - { - if(! p_) - detail::throw_invalid_argument(); - p_->do_resume(rv); - } - - /** Resume the session with an exception - - When a session is resumed with an exception, the exception - is propagated through the router's error handling mechanism. - - @param ep The exception to propagate. - - @throw std::invalid_argument If the object is empty. - */ - void operator()( - std::exception_ptr ep) const - { - if(! p_) - detail::throw_invalid_argument(); - p_->do_resume(ep); - } - -private: - suspender::owner* p_ - #if defined(__clang__) - __attribute__((unused)) - #endif - = nullptr; -}; - -template -auto -suspender:: -operator()(F&& f) -> - route_result -{ - if(! p_) - detail::throw_logic_error(); - std::forward(f)(p_->do_suspend()); - return route::suspend; -} - -//------------------------------------------------ - namespace detail { class router_base; } // detail diff --git a/src/server/cors.cpp b/src/server/cors.cpp index eaa78150..1a2f923e 100644 --- a/src/server/cors.cpp +++ b/src/server/cors.cpp @@ -176,7 +176,8 @@ operator()( p.res.set_status(options_.result); p.res.set_content_length(0); p.serializer.start(p.res); - return route::send; + // VFALCO FIXME + return {};//route::send; } // actual response setOrigin(v, p, options_); diff --git a/src/server/route_handler.cpp b/src/server/route_handler.cpp index 11073e73..f8b60159 100644 --- a/src/server/route_handler.cpp +++ b/src/server/route_handler.cpp @@ -50,14 +50,5 @@ set_body(std::string s) return *this; } -auto -route_params:: -spawn( - capy::task t) -> - route_result -{ - return capy::run_sync()(std::move(t)); -} - } // http } // boost diff --git a/src/server/router_types.cpp b/src/server/router_types.cpp index 47707360..c2786cc7 100644 --- a/src/server/router_types.cpp +++ b/src/server/router_types.cpp @@ -40,12 +40,8 @@ message( { switch(static_cast(code)) { - case route::close: return "close"; - case route::complete: return "complete"; - case route::suspend: return "suspend"; case route::next: return "next"; case route::next_route: return "next_route"; - case route::send: return "send"; default: return "?"; } @@ -71,14 +67,6 @@ route_cat_type route_cat; } // detail -resumer -suspender:: -owner:: -do_suspend() -{ - detail::throw_logic_error(); -} - bool route_params_base:: is_method( diff --git a/test/unit/server/router_types.cpp b/test/unit/server/router_types.cpp index 25164228..373e1527 100644 --- a/test/unit/server/router_types.cpp +++ b/test/unit/server/router_types.cpp @@ -43,12 +43,8 @@ struct router_types_test { { char const* const n = "boost.http.route"; - check(n, route::close); - check(n, route::complete); - check(n, route::suspend); check(n, route::next); check(n, route::next_route); - check(n, route::send); } } };