commit 5ce247e0147195f4ca726832d977e48959838b97 Author: Ryan Date: Sat Oct 18 00:44:28 2025 +0200 Hello libart-seafire-routing diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ec9f3de --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +indent_size = 4 +max_line_length = off +trim_trailing_whitespace = false + +[*.yaml] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitea/workflows/on-push.yaml b/.gitea/workflows/on-push.yaml new file mode 100644 index 0000000..c4c2eb2 --- /dev/null +++ b/.gitea/workflows/on-push.yaml @@ -0,0 +1,31 @@ +name: on-push + +on: + push: + tags-ignore: + - '*' + branches: + - '**' + +jobs: + build-and-test: + runs-on: linux + container: code.helloryan.se/art/infra/buildenv/x86_64-fedora_42-unified:latest + volumes: + - /build + steps: + - name: Configure repository access + run: | + git config --global http.$GITHUB_SERVER_URL/.extraheader "Authorization: token ${{ secrets.ACT_RUNNER_TOKEN }}" + - name: Configure build directory + run: | + bpkg create -d /build cc config.cxx=clang++ config.cc.coptions="-Wall -Werror -Wno-unknown-pragmas" + - name: Build package + run: | + cd /build + bpkg build --yes --trust-yes $GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git##$GITHUB_SHA + - name: Test package + run: | + cd /build + b test + diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..8055483 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +patreon: helloryan diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c19e5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +.bdep/ +Doxyfile + +# Local default options files. +# +.build2/local/ + +# Compiler/linker output. +# +*.d +*.t +*.i +*.i.* +*.ii +*.ii.* +*.o +*.obj +*.gcm +*.pcm +*.ifc +*.so +*.dylib +*.dll +*.a +*.lib +*.exp +*.pdb +*.ilk +*.exe +*.exe.dlls/ +*.exe.manifest +*.pc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dfc745b --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +Copyright © 2024 Ryan. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. All advertising materials mentioning features or use of this software must + display the following acknowledgement: + + This product includes software developed by Ryan, http://helloryan.se/. + +4. Neither the name(s) of the copyright holder(s) nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +NO EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4120a94 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# libart-seafire-routing + +![Build badge](https://code.helloryan.se/art/libart-seafire-routing/actions/workflows/on-push.yaml/badge.svg) + +libart-seafire-routing is part of the Seafire HTTP/1.1 library for C++. + +## Dedication + +This project is dedicated to the memory of Sefyr. + +## Sponsorship + +You can sponsor the development of this project via Patreon. Read more +over at https://patreon.com/helloryan. diff --git a/art/seafire/routing/.gitignore b/art/seafire/routing/.gitignore new file mode 100644 index 0000000..b1ed0e0 --- /dev/null +++ b/art/seafire/routing/.gitignore @@ -0,0 +1,9 @@ +# Generated version header. +# +version.hxx + +# Unit test executables and Testscript output directories +# (can be symlinks). +# +*.test +test-*.test diff --git a/art/seafire/routing/builder.cxx b/art/seafire/routing/builder.cxx new file mode 100644 index 0000000..809e1b7 --- /dev/null +++ b/art/seafire/routing/builder.cxx @@ -0,0 +1,43 @@ +#include +#include + +#include + +namespace art::seafire::routing +{ + + builder_t:: + builder_t() + {} + + std::list const& + builder_t:: + virtual_hosts() const + { + return _vhosts; + } + + virtual_host_t& + builder_t:: + add_virtual_host(std::string vhost) + { + _vhosts.emplace_back(std::move(vhost)); + return _vhosts.back(); + } + + routing_table_t + builder_t:: + build() const + { + std::vector endpoints; + + for (auto const& vhost : _vhosts) { + for (auto const& r : vhost.routes()) { + flatten(endpoints, vhost.host(), vhost.middleware(), r); + } + } + + return routing_table_t{std::move(endpoints)}; + } + +} // namespace art::seafire::routing diff --git a/art/seafire/routing/builder.hxx b/art/seafire/routing/builder.hxx new file mode 100644 index 0000000..9c24162 --- /dev/null +++ b/art/seafire/routing/builder.hxx @@ -0,0 +1,42 @@ +#ifndef art__seafire__routing__builder_hxx_ +#define art__seafire__routing__builder_hxx_ + +#include +#include + +#include + +#include +#include + +namespace art::seafire::routing +{ + + class builder_t + { + public: + builder_t(); + + builder_t(builder_t const&) = delete; + builder_t(builder_t&&) = delete; + + std::list const& + virtual_hosts() const; + + virtual_host_t& + add_virtual_host(std::string); + + routing_table_t + build() const; + + builder_t& operator=(builder_t const&) = delete; + builder_t& operator=(builder_t&&) = delete; + + private: + std::list _vhosts; + + }; + +} // namespace art::seafire::routing + +#endif diff --git a/art/seafire/routing/buildfile b/art/seafire/routing/buildfile new file mode 100644 index 0000000..d9924bb --- /dev/null +++ b/art/seafire/routing/buildfile @@ -0,0 +1,67 @@ +intf_libs = # Interface dependencies. +impl_libs = # Implementation dependencies. + +import impl_libs =+ libart-uri%lib{art-uri} +import intf_libs =+ libart-seafire-common%lib{art-seafire-common} +import intf_libs =+ libart-seafire-protocol%lib{art-seafire-protocol} +import intf_libs =+ libart-seafire-server%lib{art-seafire-server} + +./: lib{art-seafire-routing}: libul{art-seafire-routing} + +libul{art-seafire-routing}: {hxx ixx txx cxx}{** -**.test... -version} \ + {hxx }{ version} + +libul{art-seafire-routing}: $impl_libs $intf_libs + +# Unit tests. +# +exe{*.test}: +{ + test = true + install = false +} + +for t: cxx{**.test...} +{ + d = $directory($t) + n = $name($t)... + + ./: $d/exe{$n}: $t $d/{hxx ixx txx}{+$n} $d/testscript{+$n} + $d/exe{$n}: libul{art-seafire-routing}: bin.whole = false +} + +hxx{version}: in{version} $src_root/manifest +{ + dist = true + clean = ($src_root != $out_root) +} + +# Build options. +# +cxx.poptions =+ "-I$out_root" "-I$src_root" + +# Export options. +# +lib{art-seafire-routing}: +{ + cxx.export.poptions = "-I$out_root" "-I$src_root" + cxx.export.libs = $intf_libs +} + +# For pre-releases use the complete version to make sure they cannot +# be used in place of another pre-release or the final version. See +# the version module for details on the version.* variable values. +# +if $version.pre_release + lib{art-seafire-routing}: bin.lib.version = "-$version.project_id" +else + lib{art-seafire-routing}: bin.lib.version = "-$version.major.$version.minor" + +# Install into the art/seafire/routing/ subdirectory of, say, /usr/include/ +# recreating subdirectories. +# +{hxx ixx txx}{*}: +{ + install = include/art/seafire/routing/ + install.subdirs = true +} diff --git a/art/seafire/routing/diagnostics.cxx b/art/seafire/routing/diagnostics.cxx new file mode 100644 index 0000000..e8bd694 --- /dev/null +++ b/art/seafire/routing/diagnostics.cxx @@ -0,0 +1,15 @@ +#include + +namespace art::seafire::routing +{ + + /// Returns a reference to the routing diagnostic category. + /// + common::diagnostics_t::category_t const& + routing_category() + { + static common::diagnostics_t::category_t category{"router"}; + return category; + } + +} // namespace art::seafire::routing diff --git a/art/seafire/routing/diagnostics.hxx b/art/seafire/routing/diagnostics.hxx new file mode 100644 index 0000000..0b1470e --- /dev/null +++ b/art/seafire/routing/diagnostics.hxx @@ -0,0 +1,14 @@ +#ifndef art__seafire__routing__diagnostics_hxx_ +#define art__seafire__routing__diagnostics_hxx_ + +#include + +namespace art::seafire::routing +{ + + common::diagnostics_t::category_t const& + routing_category(); + +} // namespace art::seafire::routing + +#endif diff --git a/art/seafire/routing/endpoint.cxx b/art/seafire/routing/endpoint.cxx new file mode 100644 index 0000000..5c04237 --- /dev/null +++ b/art/seafire/routing/endpoint.cxx @@ -0,0 +1,48 @@ +#include + +namespace art::seafire::routing +{ + + endpoint_t:: + endpoint_t(std::string host, + std::string path, + server::request_handler_t handler) + : host_{std::move(host)}, + path_{std::move(path)}, + handler_{std::move(handler)} + {} + + std::string const& + endpoint_t:: + host() const + { + return host_; + } + + std::string const& + endpoint_t:: + path() const + { + return path_; + } + + server::request_handler_t const& + endpoint_t:: + handler() const + { + return handler_; + } + + std::ostream& + to_stream(std::ostream& o, endpoint_t const& ep) + { + return o << ep.host() << ": " << ep.path(); + } + + std::ostream& + operator<<(std::ostream& o, endpoint_t const& ep) + { + return to_stream(o, ep); + } + +} // namespace art::seafire::routing diff --git a/art/seafire/routing/endpoint.hxx b/art/seafire/routing/endpoint.hxx new file mode 100644 index 0000000..fe0a428 --- /dev/null +++ b/art/seafire/routing/endpoint.hxx @@ -0,0 +1,41 @@ +#ifndef art__seafire__routing__endpoint_hxx_ +#define art__seafire__routing__endpoint_hxx_ + +#include + +#include +#include + +namespace art::seafire::routing +{ + + class endpoint_t + { + public: + endpoint_t(std::string, std::string, server::request_handler_t); + + std::string const& + host() const; + + std::string const& + path() const; + + server::request_handler_t const& + handler() const; + + private: + std::string host_; + std::string path_; + server::request_handler_t handler_; + + }; + + std::ostream& + to_stream(std::ostream&, endpoint_t const&); + + std::ostream& + operator<<(std::ostream&, endpoint_t const&); + +} // namespace art::seafire::routing + +#endif diff --git a/art/seafire/routing/flatten.cxx b/art/seafire/routing/flatten.cxx new file mode 100644 index 0000000..1555375 --- /dev/null +++ b/art/seafire/routing/flatten.cxx @@ -0,0 +1,81 @@ +#include + +namespace art::seafire::routing +{ + + static + std::string + join_path(std::vector const& segments) + { + std::string path{"/"}; + + bool first{true}; + + for (auto const& j : segments) { + if (j.empty()) { + continue; + } + + if (first) { + first = false; + } + else { + path += "/"; + } + + path += j; + } + + return path; + } + + void + flatten(std::vector& endpoints, + std::string const& vhost, + std::vector middlewares, + route_t const& route, + std::vector segments) + { + segments.emplace_back(route.path()); + + // append any middlewares if we have any. + // + for (auto const& m : route.middleware()) { + middlewares.emplace_back(m); + } + + // generate an endpoint for this route if we have a handler. + // + if (auto const& h = route.handler()) { + endpoints.emplace_back(vhost, join_path(segments), server::make_middleware(middlewares, *h)); + } + + // flatten any child routes. + // + for (auto const& child_route : route.children()) { + flatten(endpoints, + vhost, + middlewares, + child_route, + segments); + } + } + + void + flatten(std::vector& endpoints, + std::string const& vhost, + std::vector middlewares, + route_t const& r) + { + flatten(endpoints, vhost, middlewares, r, {}); + } + + std::vector + flatten(std::string const& vhost, route_t const& r) + { + std::vector endpoints; + flatten(endpoints, vhost, {}, r); + return endpoints; + } + +} // namespace art::seafire::routing diff --git a/art/seafire/routing/flatten.hxx b/art/seafire/routing/flatten.hxx new file mode 100644 index 0000000..480c036 --- /dev/null +++ b/art/seafire/routing/flatten.hxx @@ -0,0 +1,32 @@ +#ifndef art__seafire__routing__flatten_route_hxx_ +#define art__seafire__routing__flatten_route_hxx_ + +#include +#include + +#include + +#include + +namespace art::seafire::routing +{ + + void + flatten(std::vector& endpoints, + std::string const&, + std::vector, + route_t const& r, + std::vector); + + void + flatten(std::vector&, + std::string const&, + std::vector, + route_t const&); + + std::vector + flatten(std::string const&, route_t const&); + +} // namespace art::seafire::routing + +#endif diff --git a/art/seafire/routing/host-parameter.hxx b/art/seafire/routing/host-parameter.hxx new file mode 100644 index 0000000..0dcede3 --- /dev/null +++ b/art/seafire/routing/host-parameter.hxx @@ -0,0 +1,63 @@ +#ifndef art__seafire__routing__host_parameter_hxx_ +#define art__seafire__routing__host_parameter_hxx_ + +#include +#include + +#include + +#include + +namespace art::seafire::routing +{ + + template< + server::parameter_name_t Name, + typename ParameterType = server::string_parameter_t + > + class host_parameter_t + : public server::named_parameter_t + { + public: + using parameter_type = ParameterType; + using value_type = typename parameter_type::value_type; + + host_parameter_t(std::optional value) + : _value{std::move(value)} + {} + + using server::named_parameter_t::name; + + std::optional const& + value() const + { + return _value; + } + + operator std::optional const&() const + { + return value(); + } + + std::optional const* + operator->() const + { + return &value(); + } + + static + host_parameter_t + fetch(server::request_t& req) + { + auto v = req.extensions().use().get(name()); + return parameter_type::try_parse(v); + } + + private: + std::optional _value; + + }; + +} // namespace art::seafire::routing + +#endif diff --git a/art/seafire/routing/match.cxx b/art/seafire/routing/match.cxx new file mode 100644 index 0000000..a726a03 --- /dev/null +++ b/art/seafire/routing/match.cxx @@ -0,0 +1,115 @@ +#include + +#include +#include + +namespace art::seafire::routing +{ + + bool + match(std::string const& subject, + std::string const& pattern, + char delim, + parameters_t& params) + { + parameters_t tmp_params; + + // p/pend = pattern iterators. + // + auto p = pattern.begin(); + auto const pend = pattern.end(); + + // s/send = subject iterators. + // + auto s = subject.begin(); + auto const send = subject.end(); + + auto match_param = [&]() + { + // k/kend = local pattern iterators. + // + auto const k = p; + while (p != pend && '}' != *p) { + ++p; + } + auto const kend = p; + ++p; + + if (k == kend) { + throw std::invalid_argument{"empty parameter name"}; + } + + // a greedy parameter will eat the rest of the subject. + // + bool const greedy{'*' == *k && 1 == std::distance(k, kend)}; + + // v/vend = local subject iterators. + // + auto const v = s; + while (s != send && (greedy || delim != *s)) { + ++s; + } + auto const vend = s; + + tmp_params.map().emplace(std::string{k, kend}, std::string{v, vend}); + }; + + while (p != pend) { + if (*p == '{') { + ++p; + match_param(); + continue; + } + + if (s == send) { + break; + } + + if (*p != *s) { + return false; + } + + ++p; + ++s; + } + + if (p != pend || s != send) { + return false; + } + + params = std::move(tmp_params); + + return true; + } + + std::string + render(std::string const& pattern, parameters_t& params) + { + std::stringstream str; + + auto p = pattern.begin(); + auto pend = pattern.end(); + + while (p != pend) { + if (*p == '{') { + ++p; + + auto const k = p; + while (p != pend && '}' != *p) { + ++p; + } + auto const kend = p; + + str << params.get(std::string{k, kend}).value_or(""); + } + else { + str << *p; + } + + ++p; + } + + return str.str(); + } + +} // namespace art::seafire::routing diff --git a/art/seafire/routing/match.hxx b/art/seafire/routing/match.hxx new file mode 100644 index 0000000..625ae25 --- /dev/null +++ b/art/seafire/routing/match.hxx @@ -0,0 +1,22 @@ +#ifndef art__seafire__routing__match_hxx_ +#define art__seafire__routing__match_hxx_ + +#include + +#include + +namespace art::seafire::routing +{ + + bool + match(std::string const&, + std::string const&, + char, + parameters_t&); + + std::string + render(std::string const&, parameters_t&); + +} // namespace art::seafire::routing + +#endif diff --git a/art/seafire/routing/parameters.cxx b/art/seafire/routing/parameters.cxx new file mode 100644 index 0000000..be909a7 --- /dev/null +++ b/art/seafire/routing/parameters.cxx @@ -0,0 +1,30 @@ +#include + +namespace art::seafire::routing +{ + + parameters_t::map_type& + parameters_t:: + map() + { + return _values; + } + + parameters_t::map_type const& + parameters_t:: + map() const + { + return _values; + } + + std::optional + parameters_t:: + get(std::string const& key) const + { + if (auto it = map().find(key); it != map().end()) + return it->second; + + return std::nullopt; + } + +} // namespace art::seafire::routing diff --git a/art/seafire/routing/parameters.hxx b/art/seafire/routing/parameters.hxx new file mode 100644 index 0000000..757f5ab --- /dev/null +++ b/art/seafire/routing/parameters.hxx @@ -0,0 +1,56 @@ +#ifndef seafire_routing__parameters_hxx_ +#define seafire_routing__parameters_hxx_ + +#include +#include +#include +#include + +namespace art::seafire::routing +{ + + class parameters_t + { + public: + using map_type = std::map; + + parameters_t() = default; + + parameters_t(map_type values) + : _values{std::move(values)} + {} + + map_type& + map(); + + map_type const& + map() const; + + std::optional + get(std::string const&) const; + + private: + map_type _values; + + }; + + class host_parameters_t + : public parameters_t + { + public: + using parameters_t::parameters_t; + + }; + + class route_parameters_t + : public parameters_t + { + public: + using parameters_t::parameters_t; + + }; + +} // namespace art::seafire::routing + + +#endif diff --git a/art/seafire/routing/route-parameter.hxx b/art/seafire/routing/route-parameter.hxx new file mode 100644 index 0000000..dfb9486 --- /dev/null +++ b/art/seafire/routing/route-parameter.hxx @@ -0,0 +1,63 @@ +#ifndef art__seafire__routing__route_parameter_hxx_ +#define art__seafire__routing__route_parameter_hxx_ + +#include +#include + +#include + +#include + +namespace art::seafire::routing +{ + + template< + server::parameter_name_t Name, + typename ParameterType = server::string_parameter_t + > + class route_parameter_t + : public server::named_parameter_t + { + public: + using parameter_type = ParameterType; + using value_type = typename parameter_type::value_type; + + route_parameter_t(std::optional value) + : _value{std::move(value)} + {} + + using server::named_parameter_t::name; + + std::optional const& + value() const + { + return _value; + } + + operator std::optional const&() const + { + return value(); + } + + std::optional const* + operator->() const + { + return &value(); + } + + static + route_parameter_t + fetch(server::request_t& req) + { + auto v = req.extensions().use().get(name()); + return parameter_type::try_parse(v); + } + + private: + std::optional _value; + + }; + +} // namespace art::seafire::routing + +#endif diff --git a/art/seafire/routing/route.cxx b/art/seafire/routing/route.cxx new file mode 100644 index 0000000..74739d2 --- /dev/null +++ b/art/seafire/routing/route.cxx @@ -0,0 +1,131 @@ +#include + +#include + +namespace art::seafire::routing +{ + + static + std::string + validate_path(std::string path) + { + if (!path.empty()) { + if (path.front() == '/') { + throw std::invalid_argument{"route path must not begin with '/'"}; + } + + if (path.back() == '/') { + throw std::invalid_argument{"route path must not end with '/'"}; + } + } + + return path; + + } + + route_t:: + route_t() = default; + + route_t:: + route_t(std::string path) + : path_{validate_path(std::move(path))} + {} + + route_t:: + route_t(std::string path, server::request_handler_t handler) + : path_{validate_path(std::move(path))}, handler_{std::move(handler)} + {} + + std::string const& + route_t:: + path() const + { + return path_; + } + + std::vector const& + route_t:: + middleware() const + { + return middleware_; + } + + std::optional const& + route_t:: + handler() const + { + return handler_; + } + + std::list const& + route_t:: + children() const + { + return children_; + } + + void + route_t:: + use(server::middleware_t m) + { + middleware_.emplace_back(std::move(m)); + } + + route_t& + route_t:: + add_route() + { + children_.emplace_back(); + return children_.back(); + } + + route_t& + route_t:: + add_route(std::string path) + { + children_.emplace_back(std::move(path)); + return children_.back(); + } + + route_t& + route_t:: + add_route(std::string path, server::request_handler_t handler) + { + children_.emplace_back(std::move(path), std::move(handler)); + return children_.back(); + } + + std::ostream& + to_stream(std::ostream& o, route_t const& r, std::size_t indent) + { + if (indent > 0) { + o << std::string(indent, ' '); + o << "-> "; + } + + o << "route: '" << r.path() << '\''; + + if (!r.middleware().empty()) { + o << " (with middleware)"; + } + + if (!r.handler()) { + o << " (null handler)"; + } + + o << '\n'; + + for (auto const& child : r.children()) { + to_stream(o, child, indent + 2); + } + + return o; + } + + std::ostream& + operator<<(std::ostream& o, route_t const& route) + { + return to_stream(o, route, 0); + } + +} // namespace art::seafire::routing diff --git a/art/seafire/routing/route.hxx b/art/seafire/routing/route.hxx new file mode 100644 index 0000000..db608e8 --- /dev/null +++ b/art/seafire/routing/route.hxx @@ -0,0 +1,77 @@ +#ifndef art__seafire__routing__route_hxx_ +#define art__seafire__routing__route_hxx_ + +#include + +#include +#include + +#include +#include +#include +#include + +namespace art::seafire::routing +{ + + class route_t + { + public: + route_t(); + + explicit + route_t(std::string); + + route_t(std::string, server::request_handler_t); + + route_t(route_t const&) = delete; + route_t(route_t&&) = delete; + + std::string const& + path() const; + + std::vector const& + middleware() const; + + std::optional const& + handler() const; + + std::list const& + children() const; + + void + use(server::middleware_t); + + route_t& + add_route(); + + route_t& + add_route(std::string); + + route_t& + add_route(std::string, server::request_handler_t); + + route_t& operator=(route_t const&) = delete; + route_t& operator=(route_t&&) = delete; + + private: + std::string path_; + std::vector middleware_; + std::optional handler_; + + // must be std::list to prevent invalidation of references to + // individual routes. + // + std::list children_; + + }; + + std::ostream& + to_stream(std::ostream&, route_t const&, std::size_t); + + std::ostream& + operator<<(std::ostream&, route_t const&); + +} // namespace art::seafire::routing + +#endif diff --git a/art/seafire/routing/router.cxx b/art/seafire/routing/router.cxx new file mode 100644 index 0000000..565f636 --- /dev/null +++ b/art/seafire/routing/router.cxx @@ -0,0 +1,122 @@ +#include +#include + +#include + +namespace art::seafire::routing +{ + + static + std::string + normalize_path(std::string const& path) + { + std::stringstream ipath{path}; + std::vector segments; + + for (std::string segment; std::getline(ipath, segment, '/');) { + if (segment.empty()) { + continue; + } + + if (segment == ".") { + continue; + } + + if (segment == "..") { + if (!segments.empty()) { + segments.pop_back(); + } + continue; + } + + segments.push_back(segment); + } + + std::string normalized; + + for (auto const& j : segments) { + normalized += '/'; + normalized += j; + } + + return normalized.empty() ? "/" : normalized; + } + + router_t:: + router_t(common::diagnostics_t& diagnostics, routing_table_t table) + : diagnostics_{diagnostics}, rt_{std::move(table)} + {} + + routing_table_t const& + router_t:: + routing_table() const + { + return rt_; + } + + void + router_t:: + on_request(server::request_t& req, server::response_t& res) const + { + trace() << "on_request(...)"; + + auto host = get(req); + + if (!host) { + trace() << "host not present on request!"; + res.send(server::common_error_t::not_found); + return; + } + + auto hostname = host->hostname(); + auto path = normalize_path(req.get_message().target_uri().path_str()); + + trace() << "locating endpoint:\n" + << " -> hostname: " << hostname << '\n' + << " -> path : " << path + ; + + auto result = routing_table().find_route(hostname, path); + + if (!result) { + trace() << "endpoint for [" << path << "] not found"; + res.send(server::common_error_t::not_found); + return; + } + + auto trace_endpoint = [&result](common::diagnostics_t::proxy_t proxy) + { + proxy << "endpoint found!"; + + for (auto const& j : result->host_params.map()) + proxy << "\n -> host param : " << j.first << " = " << j.second; + + for (auto const& j : result->route_params.map()) + proxy << "\n -> route param: " << j.first << " = " << j.second; + + proxy << '\n'; + }; + + trace_endpoint(trace()); + + req.extensions().extend(&res.allocator().alloc(result->host_params)); + req.extensions().extend(&res.allocator().alloc(result->route_params)); + + result->handler.invoke(req, res); + } + + void + router_t:: + operator()(server::request_t& req, server::response_t& res) const + { + on_request(req, res); + } + + common::diagnostics_t::proxy_t + router_t:: + trace() const + { + return diagnostics_ << routing_category(); + } + +} // namespace art::seafire::routing diff --git a/art/seafire/routing/router.hxx b/art/seafire/routing/router.hxx new file mode 100644 index 0000000..8570459 --- /dev/null +++ b/art/seafire/routing/router.hxx @@ -0,0 +1,40 @@ +#ifndef art__seafire__routing__router_hxx_ +#define art__seafire__routing__router_hxx_ + +#include +#include + +#include + +#include +#include + +namespace art::seafire::routing +{ + + class router_t + { + public: + router_t(common::diagnostics_t&, routing_table_t); + + routing_table_t const& + routing_table() const; + + void + on_request(server::request_t&, server::response_t&) const; + + void + operator()(server::request_t&, server::response_t&) const; + + private: + common::diagnostics_t::proxy_t + trace() const; + + common::diagnostics_t& diagnostics_; + routing_table_t rt_; + + }; + +} // namespace art::seafire::routing + +#endif diff --git a/art/seafire/routing/routing-table.cxx b/art/seafire/routing/routing-table.cxx new file mode 100644 index 0000000..994674b --- /dev/null +++ b/art/seafire/routing/routing-table.cxx @@ -0,0 +1,45 @@ +#include + +#include + +namespace art::seafire::routing +{ + + routing_table_t:: + routing_table_t(std::vector endpoints) + : endpoints_{std::move(endpoints)} + {} + + std::vector const& + routing_table_t:: + endpoints() const + { + return endpoints_; + } + + std::optional + routing_table_t:: + find_route(std::string const& host, std::string const& path) const + { + for (auto const& e : endpoints()) { + host_parameters_t host_params; + + if (!match(host, e.host(), '.', host_params)) { + continue; + } + + route_parameters_t route_params; + + if (match(path, e.path(), '/', route_params)) { + return find_result_t{ + std::move(host_params), + std::move(route_params), + e.handler() + }; + } + } + + return std::nullopt; + } + +} // namespace art::seafire::routing diff --git a/art/seafire/routing/routing-table.hxx b/art/seafire/routing/routing-table.hxx new file mode 100644 index 0000000..48a3f07 --- /dev/null +++ b/art/seafire/routing/routing-table.hxx @@ -0,0 +1,50 @@ +#ifndef art__seafire__routing__routing_table_hxx_ +#define art__seafire__routing__routing_table_hxx_ + +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace art::seafire::routing +{ + + class routing_table_t + { + public: + struct find_result_t + { + host_parameters_t host_params; + route_parameters_t route_params; + + server::request_handler_t const& handler; + + }; + + explicit + routing_table_t(std::vector); + + std::vector const& + endpoints() const; + + std::optional + find_route(std::string const&, std::string const&) const; + + private: + static + bool + match_host(std::string const&, std::string const&); + + std::vector endpoints_; + + }; + +} // namespace art::seafire::routing + +#endif diff --git a/art/seafire/routing/version.hxx.in b/art/seafire/routing/version.hxx.in new file mode 100644 index 0000000..710fe16 --- /dev/null +++ b/art/seafire/routing/version.hxx.in @@ -0,0 +1,37 @@ +#ifndef art__seafire__routing__version_hxx_ +#define art__seafire__routing__version_hxx_ + +// The numeric version format is AAAAABBBBBCCCCCDDDE where: +// +// AAAAA - major version number +// BBBBB - minor version number +// CCCCC - bugfix version number +// DDD - alpha / beta (DDD + 500) version number +// E - final (0) / snapshot (1) +// +// When DDDE is not 0, 1 is subtracted from AAAAABBBBBCCCCC. For example: +// +// Version AAAAABBBBBCCCCCDDDE +// +// 0.1.0 0000000001000000000 +// 0.1.2 0000000001000020000 +// 1.2.3 0000100002000030000 +// 2.2.0-a.1 0000200001999990010 +// 3.0.0-b.2 0000299999999995020 +// 2.2.0-a.1.z 0000200001999990011 +// +#define LIBART_SEAFIRE_ROUTING_VERSION $libart_seafire_routing.version.project_number$ULL +#define LIBART_SEAFIRE_ROUTING_VERSION_STR "$libart_seafire_routing.version.project$" +#define LIBART_SEAFIRE_ROUTING_VERSION_ID "$libart_seafire_routing.version.project_id$" +#define LIBART_SEAFIRE_ROUTING_VERSION_FULL "$libart_seafire_routing.version$" + +#define LIBART_SEAFIRE_ROUTING_VERSION_MAJOR $libart_seafire_routing.version.major$ +#define LIBART_SEAFIRE_ROUTING_VERSION_MINOR $libart_seafire_routing.version.minor$ +#define LIBART_SEAFIRE_ROUTING_VERSION_PATCH $libart_seafire_routing.version.patch$ + +#define LIBART_SEAFIRE_ROUTING_PRE_RELEASE $libart_seafire_routing.version.pre_release$ + +#define LIBART_SEAFIRE_ROUTING_SNAPSHOT_SN $libart_seafire_routing.version.snapshot_sn$ULL +#define LIBART_SEAFIRE_ROUTING_SNAPSHOT_ID "$libart_seafire_routing.version.snapshot_id$" + +#endif diff --git a/art/seafire/routing/virtual-host.cxx b/art/seafire/routing/virtual-host.cxx new file mode 100644 index 0000000..c9e3e5d --- /dev/null +++ b/art/seafire/routing/virtual-host.cxx @@ -0,0 +1,100 @@ +#include + +namespace art::seafire::routing +{ + + static + std::string + validate_host(std::string host) + { + if (host.empty()) { + throw std::invalid_argument{"host must not be empty"}; + } + + if (host.front() == '.') { + throw std::invalid_argument{"host must not begin with '.'"}; + } + + if (host.back() == '.') { + throw std::invalid_argument{"host must not end with '.'"}; + } + + return host; + } + + virtual_host_t:: + virtual_host_t(std::string host) + : _host{validate_host(std::move(host))} + {} + + std::string const& + virtual_host_t:: + host() const + { + return _host; + } + + std::vector const& + virtual_host_t:: + middleware() const + { + return _middleware; + } + + std::list const& + virtual_host_t:: + routes() const + { + return _routes; + } + + void + virtual_host_t:: + use(server::middleware_t m) + { + _middleware.emplace_back(std::move(m)); + } + + route_t& + virtual_host_t:: + add_route() + { + _routes.emplace_back(); + return _routes.back(); + } + + route_t& + virtual_host_t:: + add_route(std::string path) + { + _routes.emplace_back(std::move(path)); + return _routes.back(); + } + + route_t& + virtual_host_t:: + add_route(std::string path, server::request_handler_t handler) + { + _routes.emplace_back(std::move(path), std::move(handler)); + return _routes.back(); + } + + std::ostream& + to_stream(std::ostream& o, virtual_host_t const& vhost) + { + o << "virtual host: '" << vhost.host() << "'\n"; + + for (auto const& r : vhost.routes()) { + to_stream(o, r, 2); + } + + return o; + } + + std::ostream& + operator<<(std::ostream& o, virtual_host_t const& vhost) + { + return to_stream(o, vhost); + } + +} // namespace art::seafire::routing diff --git a/art/seafire/routing/virtual-host.hxx b/art/seafire/routing/virtual-host.hxx new file mode 100644 index 0000000..00d7791 --- /dev/null +++ b/art/seafire/routing/virtual-host.hxx @@ -0,0 +1,65 @@ +#ifndef seafire_routing__virtual_host_hxx_ +#define seafire_routing__virtual_host_hxx_ + +#include + +#include +#include + +#include +#include +#include +#include + +namespace art::seafire::routing +{ + + class virtual_host_t + { + public: + explicit + virtual_host_t(std::string); + + virtual_host_t(virtual_host_t const&) = delete; + virtual_host_t(virtual_host_t&&) = delete; + + std::string const& + host() const; + + std::vector const& + middleware() const; + + std::list const& + routes() const; + + void + use(server::middleware_t); + + route_t& + add_route(); + + route_t& + add_route(std::string); + + route_t& + add_route(std::string, server::request_handler_t); + + virtual_host_t& operator=(virtual_host_t const&) = delete; + virtual_host_t& operator=(virtual_host_t&&) = delete; + + private: + std::string _host; + std::vector _middleware; + std::list _routes; + + }; + + std::ostream& + to_stream(std::ostream&, virtual_host_t const&); + + std::ostream& + operator<<(std::ostream&, virtual_host_t const&); + +} // namespace art::seafire::routing + +#endif diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 0000000..974e01d --- /dev/null +++ b/build/.gitignore @@ -0,0 +1,4 @@ +/config.build +/root/ +/bootstrap/ +build/ diff --git a/build/bootstrap.build b/build/bootstrap.build new file mode 100644 index 0000000..8634006 --- /dev/null +++ b/build/bootstrap.build @@ -0,0 +1,7 @@ +project = libart-seafire-routing + +using version +using config +using test +using install +using dist diff --git a/build/export.build b/build/export.build new file mode 100644 index 0000000..f1e753c --- /dev/null +++ b/build/export.build @@ -0,0 +1,6 @@ +$out_root/ +{ + include art/seafire/routing/ +} + +export $out_root/art/seafire/routing/$import.target diff --git a/build/root.build b/build/root.build new file mode 100644 index 0000000..21e0a2e --- /dev/null +++ b/build/root.build @@ -0,0 +1,16 @@ +# Uncomment to suppress warnings coming from external libraries. +# +#cxx.internal.scope = current + +cxx.std = latest + +using cxx + +hxx{*}: extension = hxx +ixx{*}: extension = ixx +txx{*}: extension = txx +cxx{*}: extension = cxx + +# The test target for cross-testing (running tests under Wine, etc). +# +test.target = $cxx.target diff --git a/buildfile b/buildfile new file mode 100644 index 0000000..bc07bdc --- /dev/null +++ b/buildfile @@ -0,0 +1,5 @@ +./: {art/ tests/} doc{README.md} legal{LICENSE} manifest + +# Don't install tests. +# +tests/: install = false diff --git a/manifest b/manifest new file mode 100644 index 0000000..e41bfaa --- /dev/null +++ b/manifest @@ -0,0 +1,15 @@ +: 1 +name: libart-seafire-routing +version: 0.1.0-a.0.z +language: c++ +summary: Seafire Routing C++ library +license: BSD-4-Clause +description-file: README.md +url: https://art.helloryan.se/ +email: art@helloryan.se +depends: * build2 >= 0.17.0 +depends: * bpkg >= 0.17.0 +depends: libart-uri ^0.1.0- +depends: libart-seafire-common ^0.1.0- +depends: libart-seafire-protocol ^0.1.0- +depends: libart-seafire-server ^0.1.0- diff --git a/repositories.manifest b/repositories.manifest new file mode 100644 index 0000000..57149f9 --- /dev/null +++ b/repositories.manifest @@ -0,0 +1,23 @@ +: 1 +summary: Seafire-Routing project repository + +: +role: prerequisite +location: https://pkg.cppget.org/1/beta +trust: 70:64:FE:E4:E0:F3:60:F1:B4:51:E1:FA:12:5C:E0:B3:DB:DF:96:33:39:B9:2E:E5:C2:68:63:4C:A6:47:39:43 + +: +role: prerequisite +location: https://code.helloryan.se/art/libart-uri.git##HEAD + +: +role: prerequisite +location: https://code.helloryan.se/art/libart-seafire-common.git##HEAD + +: +role: prerequisite +location: https://code.helloryan.se/art/libart-seafire-protocol.git##HEAD + +: +role: prerequisite +location: https://code.helloryan.se/art/libart-seafire-server.git##HEAD diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..662178d --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,8 @@ +# Test executables. +# +driver + +# Testscript output directories (can be symlinks). +# +test +test-* diff --git a/tests/build/.gitignore b/tests/build/.gitignore new file mode 100644 index 0000000..974e01d --- /dev/null +++ b/tests/build/.gitignore @@ -0,0 +1,4 @@ +/config.build +/root/ +/bootstrap/ +build/ diff --git a/tests/build/bootstrap.build b/tests/build/bootstrap.build new file mode 100644 index 0000000..a07b5ea --- /dev/null +++ b/tests/build/bootstrap.build @@ -0,0 +1,5 @@ +project = # Unnamed tests subproject. + +using config +using test +using dist diff --git a/tests/build/root.build b/tests/build/root.build new file mode 100644 index 0000000..a67b2fe --- /dev/null +++ b/tests/build/root.build @@ -0,0 +1,16 @@ +cxx.std = latest + +using cxx + +hxx{*}: extension = hxx +ixx{*}: extension = ixx +txx{*}: extension = txx +cxx{*}: extension = cxx + +# Every exe{} in this subproject is by default a test. +# +exe{*}: test = true + +# The test target for cross-testing (running tests under Wine, etc). +# +test.target = $cxx.target diff --git a/tests/buildfile b/tests/buildfile new file mode 100644 index 0000000..aeeab15 --- /dev/null +++ b/tests/buildfile @@ -0,0 +1 @@ +./: {*/ -build/}