From ef1e544ff1cd367d5e7330c5dff6a9ad3fe0a405 Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 24 Dec 2024 22:26:40 +0100 Subject: [PATCH] Hello libcode-seafire-server --- .editorconfig | 17 + .gitattributes | 1 + .gitea/workflows/on-push.yaml | 24 ++ .gitignore | 32 ++ LICENSE | 31 ++ README.md | 21 ++ build/.gitignore | 4 + build/bootstrap.build | 7 + build/export.build | 6 + build/root.build | 16 + buildfile | 5 + code/seafire/server/.gitignore | 9 + code/seafire/server/buildfile | 68 ++++ code/seafire/server/common-error.hxx | 52 +++ code/seafire/server/configuration.hxx | 21 ++ code/seafire/server/counter.hxx | 33 ++ code/seafire/server/diagnostics.cxx | 41 +++ code/seafire/server/diagnostics.hxx | 26 ++ code/seafire/server/error-handler.cxx | 12 + code/seafire/server/error-handler.hxx | 38 ++ code/seafire/server/middleware.cxx | 44 +++ code/seafire/server/middleware.hxx | 84 +++++ code/seafire/server/parameters.cxx | 45 +++ code/seafire/server/parameters.hxx | 62 ++++ code/seafire/server/query-parameter.hxx | 64 ++++ code/seafire/server/query-parameters.cxx | 61 ++++ code/seafire/server/query-parameters.hxx | 30 ++ code/seafire/server/request-handler.cxx | 13 + code/seafire/server/request-handler.hxx | 82 +++++ code/seafire/server/request.cxx | 35 ++ code/seafire/server/request.hxx | 83 +++++ code/seafire/server/request.txx | 32 ++ code/seafire/server/response.cxx | 146 ++++++++ code/seafire/server/response.hxx | 209 +++++++++++ code/seafire/server/server.cxx | 331 +++++++++++++++++ code/seafire/server/server.hxx | 75 ++++ code/seafire/server/session.cxx | 154 ++++++++ code/seafire/server/session.hxx | 130 +++++++ code/seafire/server/supervisor.cxx | 88 +++++ code/seafire/server/supervisor.hxx | 92 +++++ code/seafire/server/transaction.cxx | 435 +++++++++++++++++++++++ code/seafire/server/transaction.hxx | 216 +++++++++++ code/seafire/server/version.hxx.in | 37 ++ manifest | 15 + repositories.manifest | 19 + tests/.gitignore | 8 + tests/build/.gitignore | 4 + tests/build/bootstrap.build | 5 + tests/build/root.build | 16 + tests/buildfile | 1 + 50 files changed, 3080 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitea/workflows/on-push.yaml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 build/.gitignore create mode 100644 build/bootstrap.build create mode 100644 build/export.build create mode 100644 build/root.build create mode 100644 buildfile create mode 100644 code/seafire/server/.gitignore create mode 100644 code/seafire/server/buildfile create mode 100644 code/seafire/server/common-error.hxx create mode 100644 code/seafire/server/configuration.hxx create mode 100644 code/seafire/server/counter.hxx create mode 100644 code/seafire/server/diagnostics.cxx create mode 100644 code/seafire/server/diagnostics.hxx create mode 100644 code/seafire/server/error-handler.cxx create mode 100644 code/seafire/server/error-handler.hxx create mode 100644 code/seafire/server/middleware.cxx create mode 100644 code/seafire/server/middleware.hxx create mode 100644 code/seafire/server/parameters.cxx create mode 100644 code/seafire/server/parameters.hxx create mode 100644 code/seafire/server/query-parameter.hxx create mode 100644 code/seafire/server/query-parameters.cxx create mode 100644 code/seafire/server/query-parameters.hxx create mode 100644 code/seafire/server/request-handler.cxx create mode 100644 code/seafire/server/request-handler.hxx create mode 100644 code/seafire/server/request.cxx create mode 100644 code/seafire/server/request.hxx create mode 100644 code/seafire/server/request.txx create mode 100644 code/seafire/server/response.cxx create mode 100644 code/seafire/server/response.hxx create mode 100644 code/seafire/server/server.cxx create mode 100644 code/seafire/server/server.hxx create mode 100644 code/seafire/server/session.cxx create mode 100644 code/seafire/server/session.hxx create mode 100644 code/seafire/server/supervisor.cxx create mode 100644 code/seafire/server/supervisor.hxx create mode 100644 code/seafire/server/transaction.cxx create mode 100644 code/seafire/server/transaction.hxx create mode 100644 code/seafire/server/version.hxx.in create mode 100644 manifest create mode 100644 repositories.manifest create mode 100644 tests/.gitignore create mode 100644 tests/build/.gitignore create mode 100644 tests/build/bootstrap.build create mode 100644 tests/build/root.build create mode 100644 tests/buildfile 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..23a9e11 --- /dev/null +++ b/.gitea/workflows/on-push.yaml @@ -0,0 +1,24 @@ +name: on-push +on: [push] + +jobs: + build-and-test: + runs-on: linux + container: code.helloryan.se/infra/buildenv/cxx-amd64-fedora-40:latest + volumes: + - /build + steps: + - name: Clone repository + uses: actions/checkout@v3 + - name: Authenticate + run: | + git config unset http.https://code.helloryan.se/.extraheader + echo "${{ secrets.NETRC }}" >> ~/.netrc + - name: Initialize + run: | + bpkg create -d /build cc config.cc.coptions="-Wall -Werror" + bdep init -A /build + - name: Build + run: b + - name: Test + run: b test 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..39a9732 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# libcode-seafire-server + +![Build status](https://code.helloryan.se/code/libcode-seafire-server/actions/workflows/on-push.yaml/badge.svg) + +## Requirements + +None, other than a modern C++-compiler. + +## Building + +See the wiki, https://code.helloryan.se/code/wiki/wiki/Build-Instructions, for +build instructions. + +## Contact + +Please report bugs and issues by sending an e-mail to: ryan@helloryan.se. + +## Contributing + +Please send an e-mail to ryan@helloryan.se to request an account and +write-access to the libcode-seafire-server repository. 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..6cfb189 --- /dev/null +++ b/build/bootstrap.build @@ -0,0 +1,7 @@ +project = libcode-seafire-server + +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..678703e --- /dev/null +++ b/build/export.build @@ -0,0 +1,6 @@ +$out_root/ +{ + include code/seafire/server/ +} + +export $out_root/code/seafire/server/$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..bbe185e --- /dev/null +++ b/buildfile @@ -0,0 +1,5 @@ +./: {code/ tests/} doc{README.md} legal{LICENSE} manifest + +# Don't install tests. +# +tests/: install = false diff --git a/code/seafire/server/.gitignore b/code/seafire/server/.gitignore new file mode 100644 index 0000000..b1ed0e0 --- /dev/null +++ b/code/seafire/server/.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/code/seafire/server/buildfile b/code/seafire/server/buildfile new file mode 100644 index 0000000..0eb0a70 --- /dev/null +++ b/code/seafire/server/buildfile @@ -0,0 +1,68 @@ +intf_libs = # Interface dependencies. +impl_libs = # Implementation dependencies. + +import intf_libs =+ libasio%lib{asio} +import intf_libs =+ libcode-uri%lib{code-uri} +import intf_libs =+ libcode-seafire-common%lib{code-seafire-common} +import intf_libs =+ libcode-seafire-protocol%lib{code-seafire-protocol} + +./: lib{code-seafire-server}: libul{code-seafire-server} + +libul{code-seafire-server}: {hxx ixx txx cxx}{** -**.test... -version} \ + {hxx }{ version} + +libul{code-seafire-server}: $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{code-seafire-server}: 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" +cxx.coptions =+ -fvisibility=hidden + +# Export options. +# +lib{code-seafire-server}: +{ + 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{code-seafire-server}: bin.lib.version = "-$version.project_id" +else + lib{code-seafire-server}: bin.lib.version = "-$version.major.$version.minor" + +# Install into the code/seafire/server/ subdirectory of, say, /usr/include/ +# recreating subdirectories. +# +{hxx ixx txx}{*}: +{ + install = include/code/seafire/server/ + install.subdirs = true +} diff --git a/code/seafire/server/common-error.hxx b/code/seafire/server/common-error.hxx new file mode 100644 index 0000000..e8b66d7 --- /dev/null +++ b/code/seafire/server/common-error.hxx @@ -0,0 +1,52 @@ +#ifndef code__seafire__server__common_error_hxx_ +#define code__seafire__server__common_error_hxx_ + +namespace code::seafire::server +{ + + enum class common_error_t + { + // RCF 7231. + // + bad_request, + payment_required, + forbidden, + not_found, + method_not_allowed, + not_acceptable, + request_timeout, + conflict, + gone, + length_required, + payload_too_large, + uri_too_long, + unsupported_media_type, + expectation_failed, + upgrade_required, + + internal_server_error, + not_implemented, + bad_gateway, + service_unavailable, + gateway_timeout, + http_version_not_supported, + + // RFC 7232. + // + not_modified, + precondition_failed, + + // RFC 7235. + // + unauthorized, + proxy_auth_required, + + // Unofficial. + // + enhance_your_calm + + }; + +} // namespace code::seafire::server + +#endif diff --git a/code/seafire/server/configuration.hxx b/code/seafire/server/configuration.hxx new file mode 100644 index 0000000..64dbc30 --- /dev/null +++ b/code/seafire/server/configuration.hxx @@ -0,0 +1,21 @@ +#ifndef code__seafire__server__configuration_hxx_ +#define code__seafire__server__configuration_hxx_ + +#include + +namespace code::seafire::server +{ + + /// Holds server configuration parameters. + /// + struct configuration_t + { + /// The timeout of a transaction. + /// + std::chrono::seconds request_timeout; + + }; + +} // namespace code::seafire::server + +#endif diff --git a/code/seafire/server/counter.hxx b/code/seafire/server/counter.hxx new file mode 100644 index 0000000..c5e7dc3 --- /dev/null +++ b/code/seafire/server/counter.hxx @@ -0,0 +1,33 @@ +#ifndef code__seafire__server__counter_hxx_ +#define code__seafire__server__counter_hxx_ + +#include +#include + +namespace code::seafire::server +{ + + template + class atomic_t + : public std::atomic + { + public: + atomic_t() + : std::atomic{0} + {} + + atomic_t(atomic_t const& other) + : std::atomic{other.load()} + {} + + atomic_t(atomic_t&& other) + : std::atomic{other.load()} + {} + + }; + + using counter_t = atomic_t; + +} // namespace code::seafire::server + +#endif diff --git a/code/seafire/server/diagnostics.cxx b/code/seafire/server/diagnostics.cxx new file mode 100644 index 0000000..596172d --- /dev/null +++ b/code/seafire/server/diagnostics.cxx @@ -0,0 +1,41 @@ +#include + +namespace code::seafire::server +{ + + common::diagnostics_t::category_t const& + server_category() + { + static common::diagnostics_t::category_t category{"server"}; + return category; + } + + common::diagnostics_t::category_t const& + supervisor_category() + { + static common::diagnostics_t::category_t category{"supervisor"}; + return category; + } + + common::diagnostics_t::category_t const& + session_category() + { + static common::diagnostics_t::category_t category{"session"}; + return category; + } + + common::diagnostics_t::category_t const& + transaction_category() + { + static common::diagnostics_t::category_t category{"transaction"}; + return category; + } + + common::diagnostics_t::category_t const& + request_category() + { + static common::diagnostics_t::category_t category{"request"}; + return category; + } + +} // namespace code::seafire::server diff --git a/code/seafire/server/diagnostics.hxx b/code/seafire/server/diagnostics.hxx new file mode 100644 index 0000000..803b04a --- /dev/null +++ b/code/seafire/server/diagnostics.hxx @@ -0,0 +1,26 @@ +#ifndef code__seafire__server__diagnostics_hxx_ +#define code__seafire__server__diagnostics_hxx_ + +#include + +namespace code::seafire::server +{ + + common::diagnostics_t::category_t const& + server_category(); + + common::diagnostics_t::category_t const& + supervisor_category(); + + common::diagnostics_t::category_t const& + session_category(); + + common::diagnostics_t::category_t const& + transaction_category(); + + common::diagnostics_t::category_t const& + request_category(); + +} // namespace code::seafire::server + +#endif diff --git a/code/seafire/server/error-handler.cxx b/code/seafire/server/error-handler.cxx new file mode 100644 index 0000000..74e141e --- /dev/null +++ b/code/seafire/server/error-handler.cxx @@ -0,0 +1,12 @@ +#include + +namespace code::seafire::server +{ + + error_handler_t:: + error_handler_t() = default; + + error_handler_t:: + ~error_handler_t() noexcept = default; + +} // namespace code::seafire::serverk diff --git a/code/seafire/server/error-handler.hxx b/code/seafire/server/error-handler.hxx new file mode 100644 index 0000000..5cf5c58 --- /dev/null +++ b/code/seafire/server/error-handler.hxx @@ -0,0 +1,38 @@ +#ifndef code__seafire__server__error_handler_hxx_ +#define code__seafire__server__error_handler_hxx_ + +#include + +namespace code::seafire::server +{ + + class request_t; + class response_t; + + class error_handler_t + { + public: + virtual + void + on_error(request_t&, response_t&, common_error_t) = 0; + + virtual + void + on_exception(request_t&, response_t&) noexcept = 0; + + protected: + error_handler_t(); + + error_handler_t(error_handler_t const&) = delete; + error_handler_t(error_handler_t&&) = delete; + + ~error_handler_t() noexcept; + + error_handler_t& operator=(error_handler_t const&) = delete; + error_handler_t& operator=(error_handler_t&&) = delete; + + }; + +} + +#endif diff --git a/code/seafire/server/middleware.cxx b/code/seafire/server/middleware.cxx new file mode 100644 index 0000000..145dbe5 --- /dev/null +++ b/code/seafire/server/middleware.cxx @@ -0,0 +1,44 @@ +#include + +namespace code::seafire::server +{ + + void + middleware_t:: + invoke(request_t& req, response_t& res, request_handler_t const& next) const + { + handler_->invoke(req, res, next); + } + + middleware_t:: + middleware_t(std::shared_ptr handler) + : handler_{handler} + {} + + request_handler_t + make_middleware(std::vector const& chain, + request_handler_t handler) + { + struct handler_t + { + middleware_t middleware; + request_handler_t next; + + void + operator()(request_t& req, response_t& res) const + { + middleware.invoke(req, res, next); + } + + }; + + request_handler_t next{std::move(handler)}; + + for (auto it = chain.rbegin(); it != chain.rend(); ++it) { + next = handler_t{*it, next}; + } + + return next; + } + +} // namespace code::seafire::server diff --git a/code/seafire/server/middleware.hxx b/code/seafire/server/middleware.hxx new file mode 100644 index 0000000..b650a95 --- /dev/null +++ b/code/seafire/server/middleware.hxx @@ -0,0 +1,84 @@ +#ifndef code__seafire__server__middleware_hxx_ +#define code__seafire__server__middleware_hxx_ + +#include + +#include +#include + +namespace code::seafire::server +{ + + /// Implements middleware functionality. + /// + class middleware_t + { + public: + template + middleware_t(T handler) + : handler_{std::make_shared>(std::move(handler))} + {} + + void + invoke(request_t&, response_t&, request_handler_t const&) const; + + template + friend + middleware_t + make_middleware(Args&&...); + + private: + struct concept_t + { + virtual + ~concept_t() noexcept = default; + + virtual + void + invoke(request_t&, response_t&, request_handler_t const&) const = 0; + + }; + + template + struct container_t + : concept_t + { + template + container_t(Args&&... args) + : handler{std::forward(args)...} + {} + + void + invoke(request_t& req, + response_t& res, + request_handler_t const& next) const override + { + handler(req, res, next); + } + + T handler; + + }; + + explicit + middleware_t(std::shared_ptr); + + std::shared_ptr handler_; + + }; + + template + middleware_t + make_middleware(Args&&... args) + { + return middleware_t{ + std::make_shared>(std::forward(args)...) + }; + } + + request_handler_t + make_middleware(std::vector const&, request_handler_t); + +} // namespace code::seafire::server + +#endif diff --git a/code/seafire/server/parameters.cxx b/code/seafire/server/parameters.cxx new file mode 100644 index 0000000..fc4fed6 --- /dev/null +++ b/code/seafire/server/parameters.cxx @@ -0,0 +1,45 @@ +#include + +namespace code::seafire::server +{ + + std::optional + string_parameter_t:: + try_parse(std::optional const& input) + { + return input; + } + + std::optional + int_parameter_t:: + try_parse(std::optional const& input) + { + try { + if (input) { + return std::stoll(*input); + } + + return std::nullopt; + } + catch (...) { + return std::nullopt; + } + } + + std::optional + uint_parameter_t:: + try_parse(std::optional const& input) + { + try { + if (input) { + return std::stoull(*input); + } + + return std::nullopt; + } + catch (...) { + return std::nullopt; + } + } + +} // namespace code::seafire::server diff --git a/code/seafire/server/parameters.hxx b/code/seafire/server/parameters.hxx new file mode 100644 index 0000000..118e992 --- /dev/null +++ b/code/seafire/server/parameters.hxx @@ -0,0 +1,62 @@ +#ifndef code__seafire__server__parameters_hxx_ +#define code__seafire__server__parameters_hxx_ + +#include +#include +#include +#include +#include + +namespace code::seafire::server +{ + + template + struct parameter_name_t + { + constexpr parameter_name_t(const char (&str)[N]) + { + std::copy_n(str, N, name); + } + + operator std::string const() const + { + return name; + } + + char name[N]; + + }; + + struct string_parameter_t + { + using value_type = std::string; + + static + std::optional + try_parse(std::optional const&); + + }; + + struct int_parameter_t + { + using value_type = std::int64_t; + + static + std::optional + try_parse(std::optional const&); + + }; + + struct uint_parameter_t + { + using value_type = std::uint64_t; + + static + std::optional + try_parse(std::optional const&); + + }; + +} // namespace code::seafire::server + +#endif diff --git a/code/seafire/server/query-parameter.hxx b/code/seafire/server/query-parameter.hxx new file mode 100644 index 0000000..5e4e646 --- /dev/null +++ b/code/seafire/server/query-parameter.hxx @@ -0,0 +1,64 @@ +#ifndef code__seafire__server__query_parameter_hxx_ +#define code__seafire__server__query_parameter_hxx_ + +#include +#include +#include + +#include + +namespace code::seafire::server +{ + + template + class query_parameter_t + { + public: + using parameter_type = ParameterType; + using value_type = typename parameter_type::value_type; + + static + std::string const& + name() + { + static std::string const name{Name}; + return name; + } + + query_parameter_t(std::optional value) + : value_{std::move(value)} + {} + + std::optional const& + value() const + { + return value_; + } + + operator std::optional const&() const + { + return value(); + } + + std::optional const* + operator->() const + { + return &value(); + } + + static + query_parameter_t + fetch(request_t& r) + { + auto value = r.extensions().use().get(name()); + return std::optional{parameter_type::try_parse(value)}; + } + + private: + std::optional value_; + + }; + +} // namespace code::seafire::server + +#endif diff --git a/code/seafire/server/query-parameters.cxx b/code/seafire/server/query-parameters.cxx new file mode 100644 index 0000000..72bae49 --- /dev/null +++ b/code/seafire/server/query-parameters.cxx @@ -0,0 +1,61 @@ +#include + +namespace code::seafire::server +{ + + query_parameters_t:: + query_parameters_t(std::map keys) + : keys_{std::move(keys)} + {} + + std::optional + query_parameters_t:: + get(std::string const& key) const + { + if (auto it = keys_.find(key); it != keys_.end()) + return it->second; + + return std::nullopt; + } + + std::optional + query_parameters_t:: + try_parse(std::string const& q) + { + std::map keys; + + for (auto it = q.begin(); it != q.end();) { + auto k = it; + + while (it != q.end() && *it != '=' && *it != '&') { + ++it; + } + + auto kend = it; + + if (it != q.end() && *it == '=') { + ++it; // skips '=' + } + + auto v = it; + + while (it != q.end() && *it != '&') { + ++it; + } + + auto vend = it; + + if (it != q.end() && *it == '&') { + ++it; // skips '&' + } + + std::string key{k, kend}; + std::string value{v, vend}; + + keys.emplace(std::move(key), std::move(value)); + } + + return keys; + } + +} // namespace code::seafire::server diff --git a/code/seafire/server/query-parameters.hxx b/code/seafire/server/query-parameters.hxx new file mode 100644 index 0000000..ffc4535 --- /dev/null +++ b/code/seafire/server/query-parameters.hxx @@ -0,0 +1,30 @@ +#ifndef code__seafire__server__query_parameters_hxx_ +#define code__seafire__server__query_parameters_hxx_ + +#include +#include +#include + +namespace code::seafire::server +{ + + class query_parameters_t + { + public: + query_parameters_t(std::map); + + std::optional + get(std::string const&) const; + + static + std::optional + try_parse(std::string const&); + + private: + std::map keys_; + + }; + +} // namespace code::seafire::server + +#endif diff --git a/code/seafire/server/request-handler.cxx b/code/seafire/server/request-handler.cxx new file mode 100644 index 0000000..7668846 --- /dev/null +++ b/code/seafire/server/request-handler.cxx @@ -0,0 +1,13 @@ +#include + +namespace code::seafire::server +{ + + void + request_handler_t:: + invoke(request_t& req, response_t& res) const + { + handler_->invoke(req, res); + } + +} // namespace code::seafire::server diff --git a/code/seafire/server/request-handler.hxx b/code/seafire/server/request-handler.hxx new file mode 100644 index 0000000..b092d1a --- /dev/null +++ b/code/seafire/server/request-handler.hxx @@ -0,0 +1,82 @@ +#ifndef code__seafire__server__request_handler_hxx_ +#define code__seafire__server__request_handler_hxx_ + +#include + +namespace code::seafire::server +{ + + class request_t; + class response_t; + + class request_handler_t + { + public: + template + request_handler_t(Handler handler) + : handler_{ + std::make_shared>( + std::move(handler) + ) + } + {} + + void + invoke(request_t& req, response_t& res) const; + + template + friend + request_handler_t + make_request_handler(Args&&...); + + private: + struct concept_t + { + virtual + ~concept_t() noexcept = default; + + virtual + void + invoke(request_t&, response_t&) const = 0; + + }; + + template + struct container_t + : concept_t + { + template + container_t(Args&&... args) + : handler_{std::forward(args)...} + {} + + void + invoke(request_t& req, response_t& res) const override + { + handler_(req, res); + } + + Handler handler_; + }; + + request_handler_t(std::shared_ptr handler) + : handler_{std::move(handler)} + {} + + // Request handlers are shared for performance reasons and hence const. + // + std::shared_ptr handler_; + + }; + + template + request_handler_t + make_request_handler(Args&&... args) + { + using container_t = request_handler_t::container_t; + return std::make_shared(std::forward(args)...); + } + +} // namespace code::seafire::server + +#endif diff --git a/code/seafire/server/request.cxx b/code/seafire/server/request.cxx new file mode 100644 index 0000000..b8aeedc --- /dev/null +++ b/code/seafire/server/request.cxx @@ -0,0 +1,35 @@ +#include + +#include + +namespace code::seafire::server +{ + + protocol::request_t const& + request_t:: + get_message() const + { + return get_request(); + } + + std::istream& + request_t:: + content() + { + return get_request_content(); + } + + common::extension_context_t& + request_t:: + extensions() + { + return get_request_extensions(); + } + + request_t:: + request_t() = default; + + request_t:: + ~request_t() noexcept = default; + +} // namespace code::seafire::server diff --git a/code/seafire/server/request.hxx b/code/seafire/server/request.hxx new file mode 100644 index 0000000..23b95f1 --- /dev/null +++ b/code/seafire/server/request.hxx @@ -0,0 +1,83 @@ +#ifndef code__seafire__server__request_hxx_ +#define code__seafire__server__request_hxx_ + +#include + +#include +#include + +#include + +#include + +namespace code::seafire::server +{ + + /// Represents a server-side request. + /// + class request_t + { + public: + protocol::request_t const& + get_message() const; + + std::istream& + content(); + + // fixme: replace with non-virtual function and virtual + // get_request_allocator function. + // + virtual + common::allocator_t& + memory() = 0; + + common::extension_context_t& + extensions(); + + protected: + request_t(); + + request_t(request_t const&) = delete; + request_t(request_t&&) = delete; + + ~request_t() noexcept; + + request_t& operator=(request_t const&) = delete; + request_t& operator=(request_t&&) = delete; + + private: + virtual + protocol::request_t const& + get_request() const = 0; + + virtual + std::istream& + get_request_content() = 0; + + virtual + common::extension_context_t& + get_request_extensions() = 0; + + }; + + template + bool + has(request_t const&); + + template + bool + has_quick(request_t const&); + + template + auto + get(request_t const&); + + template + auto + get(request_t const&, std::error_code&); + +} // namespace code::seafire::server + +#include + +#endif diff --git a/code/seafire/server/request.txx b/code/seafire/server/request.txx new file mode 100644 index 0000000..f1637ad --- /dev/null +++ b/code/seafire/server/request.txx @@ -0,0 +1,32 @@ +namespace code::seafire::server +{ + + template + bool + has(request_t const& r) + { + return protocol::has
(r.get_message()); + } + + template + bool + has_quick(request_t const& r) + { + return protocol::has_quick
(r.get_message()); + } + + template + auto + get(request_t const& r) + { + return protocol::get
(r.get_message()); + } + + template + auto + get(request_t const& r, std::error_code& ec) + { + return protocol::get
(r.get_message(), ec); + } + +} // namespace code::seafire::server diff --git a/code/seafire/server/response.cxx b/code/seafire/server/response.cxx new file mode 100644 index 0000000..7fdc2c5 --- /dev/null +++ b/code/seafire/server/response.cxx @@ -0,0 +1,146 @@ +#include + +namespace code::seafire::server +{ + + /// Access the response message. + /// + protocol::response_t& + response_t:: + get_message() + { + return get_response(); + } + + /// Access the response message. + /// + protocol::response_t const& + response_t:: + get_message() const + { + return get_response(); + } + + /// Send response. + /// + void + response_t:: + send(protocol::status_code_t s) + { + send(s, common::io::const_buffers_t{}); + } + + /// Send response. + /// + void + response_t:: + send(protocol::status_code_t s, common::io::const_buffer_t const& content) + { + send(s, common::io::const_buffers_t{content}); + } + + /// Send response. + /// + void + response_t:: + send(protocol::status_code_t s, common::io::const_buffers_t const& content) + { + do_send_response(s, content); + } + + /// Send response. + /// + void + response_t:: + send(protocol::status_code_t s, stream_t const& content) + { + send(s, content.rdbuf()->data()); + } + + /// Send error. + /// + void + response_t:: + send(common_error_t error) + { + do_send_error(error); + } + + /// Allocate a new stream. + /// + response_t::stream_t + response_t:: + allocate_stream() + { + return stream_t{&memory().alloc_emplace()}; + } + + /// Access response extensions. + /// + common::extension_context_t& + response_t:: + extensions() + { + return get_response_extensions(); + } + + void + response_t:: + invoke_finalizer(finalizer_t* f, request_t& r) + { + if (f) { + f->invoke(r, *this); + } + } + + response_t:: + response_t() = default; + + response_t:: + ~response_t() noexcept = default; + + asio::streambuf* + response_t::stream_t:: + rdbuf() const + { + return rdbuf_; + } + + std::size_t + response_t::stream_t:: + size() const + { + return rdbuf_->data().size(); + } + + response_t::stream_t:: + stream_t(asio::streambuf* rdbuf) + : std::iostream{rdbuf ? rdbuf : throw std::invalid_argument{"buf"}}, + rdbuf_{rdbuf} + {} + + /// Construct a new finalizer. + /// + response_t::finalizer_t:: + finalizer_t(response_t& r, function_t f) + : response_{r}, f_{f} + { + response_.register_finalizer(this); + } + + /// Destroy this finalizer. + /// + response_t::finalizer_t:: + ~finalizer_t() noexcept + { + response_.deregister_finalizer(this); + } + + void + response_t::finalizer_t:: + invoke(request_t& req, response_t& res) + { + f_(req, res); + } + +} // namespace code::seafire::server diff --git a/code/seafire/server/response.hxx b/code/seafire/server/response.hxx new file mode 100644 index 0000000..bcd6d67 --- /dev/null +++ b/code/seafire/server/response.hxx @@ -0,0 +1,209 @@ +#ifndef code__seafire__server__response_hxx_ +#define code__seafire__server__response_hxx_ + +#include + +#include +#include + +#include +#include + +#include + +#include +#include + +namespace code::seafire::server +{ + + class request_t; + + /// Represents a server-side response. + /// + class response_t + { + public: + class stream_t; + class finalizer_t; + + friend finalizer_t; + + protocol::response_t& + get_message(); + + protocol::response_t const& + get_message() const; + + void + send(protocol::status_code_t); + + void + send(protocol::status_code_t, common::io::const_buffer_t const&); + + void + send(protocol::status_code_t, common::io::const_buffers_t const&); + + void + send(protocol::status_code_t, stream_t const&); + + void + send(common_error_t); + + stream_t + allocate_stream(); + + // fixme: replace with non-virtual function and virtual + // get_response_allocator function. + // + virtual + common::allocator_t& + memory() = 0; + + common::extension_context_t& + extensions(); + + protected: + response_t(); + + response_t(response_t const&) = delete; + response_t(response_t&&) = delete; + + ~response_t() noexcept; + + virtual + void + register_finalizer(finalizer_t*) = 0; + + virtual + void + deregister_finalizer(finalizer_t*) = 0; + + void + invoke_finalizer(finalizer_t*, request_t&); + + response_t& operator=(response_t const&) = delete; + response_t& operator=(response_t&&) = delete; + + private: + virtual + protocol::response_t& + get_response() = 0; + + virtual + protocol::response_t const& + get_response() const = 0; + + virtual + common::extension_context_t& + get_response_extensions() = 0; + + virtual + void + do_send_response(protocol::status_code_t, + common::io::const_buffers_t const&) = 0; + + virtual + void + do_send_error(common_error_t) = 0; + + }; + + template + bool + has(response_t const& r) + { + return protocol::has
(r.get_message()); + } + + template + bool + has_quick(response_t const& r) + { + return protocol::has_quick
(r.get_message()); + } + + template + auto + get(response_t const& r) + { + return protocol::get
(r.get_message()); + } + + template + auto + get(response_t const& r, std::error_code& ec) + { + return protocol::get
(r.get_message(), ec); + } + + template + void + set(response_t& r, Args&&... args) + { + protocol::set
(r.get_message(), std::forward(args)...); + } + + template + void + erase(response_t& r) + { + protocol::erase
(r.get_message()); + } + + template + void + set_if_not_set(response_t& r, Args&&... args) + { + protocol::set_if_not_set
(r.get_message(), std::forward(args)...); + } + + class response_t::stream_t + : public std::iostream + { + public: + asio::streambuf* + rdbuf() const; + + std::size_t + size() const; + + private: + friend response_t; + + explicit + stream_t(asio::streambuf*); + + asio::streambuf* rdbuf_; + + }; + + class response_t::finalizer_t + { + public: + using function_t = std::function; + + finalizer_t(response_t&, function_t); + + finalizer_t(finalizer_t const&) = delete; + finalizer_t(finalizer_t&&) = delete; + + ~finalizer_t() noexcept; + + finalizer_t& operator=(finalizer_t const&) = delete; + finalizer_t& operator=(finalizer_t&&) = delete; + + private: + friend response_t; + + void + invoke(request_t&, response_t&); + + response_t& response_; + function_t f_; + + }; + +} // namespace code::seafire::server + +#endif diff --git a/code/seafire/server/server.cxx b/code/seafire/server/server.cxx new file mode 100644 index 0000000..f688904 --- /dev/null +++ b/code/seafire/server/server.cxx @@ -0,0 +1,331 @@ +#include +#include +#include + +#include + +#include +#include + +#include + +namespace code::seafire::server +{ + + /// Construct a new HTTP server using the specified acceptors + /// and request handler. + /// + server_t:: + server_t(common::diagnostics_t& diagnostics, + configuration_t configuration, + acceptor_set_t acceptors, + request_handler_t handler) + : diagnostics_{diagnostics}, + configuration_{configuration}, + acceptors_{std::move(acceptors)}, + handler_{handler}, + supervisor_{diagnostics_, *this, handler_} + {} + + void + server_t:: + start() + { + trace() << "starting seafire server..."; + + // fixme: add tracing. + // + + for (auto const& j : acceptors_) { + init_accept(*j); + } + } + + void + server_t:: + stop(bool quick) + { + trace() << "seafire server stop requested..."; + + if (quick) { + supervisor_.stop_all(); + } + } + + void + server_t:: + on_error(request_t& req, response_t& res, common_error_t error) + { + trace() << "on_error()..."; + + namespace rfc7231 = protocol::rfc7231; + + switch (error) { + case common_error_t::bad_request: { + static std::string const message{"Bad request\n"}; + + set(res, "text", "plain"); + res.send(400, common::io::buffer(message)); + return; + } + + case common_error_t::payment_required: { + static std::string const message{"Payment required\n"}; + + set(res, "text", "plain"); + res.send(402, common::io::buffer(message)); + return; + } + + case common_error_t::forbidden: { + static std::string const message{"Forbidden\n"}; + + set(res, "text", "plain"); + res.send(403, common::io::buffer(message)); + return; + } + + case common_error_t::not_found: { + static std::string const message{"Not found\n"}; + + set(res, "text", "plain"); + res.send(404, common::io::buffer(message)); + return; + } + + case common_error_t::method_not_allowed: { + static std::string const message{"Method not allowed\n"}; + + set(res, "text", "plain"); + res.send(405, common::io::buffer(message)); + return; + } + + case common_error_t::not_acceptable: { + static std::string const message{"Not acceptable\n"}; + + set(res, "text", "plain"); + res.send(406, common::io::buffer(message)); + return; + } + + case common_error_t::request_timeout: { + static std::string const message{"Request timeout\n"}; + + set(res, "text", "plain"); + res.send(408, common::io::buffer(message)); + return; + } + + case common_error_t::conflict: { + static std::string const message{"Conflict\n"}; + + set(res, "text", "plain"); + res.send(409, common::io::buffer(message)); + return; + } + + case common_error_t::gone: { + static std::string const message{"Gone\n"}; + + set(res, "text", "plain"); + res.send(410, common::io::buffer(message)); + return; + } + + case common_error_t::length_required: { + static std::string const message{"Length required\n"}; + + set(res, "text", "plain"); + res.send(411, common::io::buffer(message)); + return; + } + + case common_error_t::payload_too_large: { + static std::string const message{"Payload too large\n"}; + + set(res, "text", "plain"); + res.send(413, common::io::buffer(message)); + return; + } + + case common_error_t::uri_too_long: { + static std::string const message{"Target URI too long\n"}; + + set(res, "text", "plain"); + res.send(414, common::io::buffer(message)); + return; + } + + case common_error_t::unsupported_media_type: { + static std::string const message{"Unsupported media type\n"}; + + set(res, "text", "plain"); + res.send(415, common::io::buffer(message)); + return; + } + + case common_error_t::expectation_failed: { + static std::string const message{"Expectation failed\n"}; + + set(res, "text", "plain"); + res.send(417, common::io::buffer(message)); + return; + } + + case common_error_t::upgrade_required: { + static std::string const message{"Upgrade required\n"}; + + set(res, "text", "plain"); + res.send(426, common::io::buffer(message)); + return; + } + + + case common_error_t::internal_server_error: { + static std::string const message{"Internal server error\n"}; + + set(res, "text", "plain"); + res.send(500, common::io::buffer(message)); + return; + } + + case common_error_t::not_implemented: { + static std::string const message{"Not implemented\n"}; + + set(res, "text", "plain"); + res.send(501, common::io::buffer(message)); + return; + } + + case common_error_t::bad_gateway: { + static std::string const message{"Bad gateway\n"}; + + set(res, "text", "plain"); + res.send(502, common::io::buffer(message)); + return; + } + + case common_error_t::service_unavailable: { + static std::string const message{"Service unavailable\n"}; + + set(res, "text", "plain"); + res.send(503, common::io::buffer(message)); + return; + } + + case common_error_t::gateway_timeout: { + static std::string const message{"Gateway timeout\n"}; + + set(res, "text", "plain"); + res.send(504, common::io::buffer(message)); + return; + } + + case common_error_t::http_version_not_supported: { + static std::string const message{"HTTP version not supported\n"}; + + set(res, "text", "plain"); + res.send(505, common::io::buffer(message)); + return; + } + + // rfc 7232 + // + + case common_error_t::not_modified: { + static std::string const message{"Not modified\n"}; + + set(res, "text", "plain"); + res.send(304, common::io::buffer(message)); + return; + } + + case common_error_t::precondition_failed: { + static std::string const message{"Precondition failed\n"}; + + set(res, "text", "plain"); + res.send(412, common::io::buffer(message)); + return; + } + + // rfc 7235 + // + + case common_error_t::unauthorized: { + static std::string const message{"Unauthorized\n"}; + + set(res, "text", "plain"); + res.send(401, common::io::buffer(message)); + return; + } + + case common_error_t::proxy_auth_required: { + static std::string const message{"Proxy authentication required\n"}; + + set(res, "text", "plain"); + res.send(407, common::io::buffer(message)); + return; + } + + // Unofficial. + // + + case common_error_t::enhance_your_calm: { + static std::string const message{"Enhance your calm\n"}; + + set(res, "text", "plain"); + res.send(420, common::io::buffer(message)); + return; + } + } + + throw std::invalid_argument{"invalid common error"}; + } + + void + server_t:: + on_exception(request_t&, response_t&) noexcept + { + trace() << "on_exception()..."; + } + + common::diagnostics_t::proxy_t + server_t:: + trace() + { + return diagnostics_ << server_category(); + } + + void + server_t:: + init_accept(common::io::acceptor_t& acceptor) + { + trace() << "init_accept()..."; + + auto bound = [this, &acceptor](std::error_code const& ec) + { + on_accept(acceptor, ec); + }; + + supervisor_.async_accept(acceptor, bound); + } + + void + server_t:: + on_accept(common::io::acceptor_t& acceptor, std::error_code const& ec) + { + trace() << "on_accept()..."; + + if (!ec) { + // no error. + // + init_accept(acceptor); + } + else if (ec != asio::error::operation_aborted) { + // fixme: handle error accordingly. + // + } + } + +} // namespace code::seafire::server diff --git a/code/seafire/server/server.hxx b/code/seafire/server/server.hxx new file mode 100644 index 0000000..426849e --- /dev/null +++ b/code/seafire/server/server.hxx @@ -0,0 +1,75 @@ +#ifndef code__seafire__server__hxx_ +#define code__seafire__server__hxx_ + +#include +#include +#include +#include +#include + +#include + +#include + +namespace code::seafire::server +{ + + class request_t; + class response_t; + class transaction_t; + + class server_t + : public error_handler_t + { + public: + /// Acceptor set type. + /// + using acceptor_set_t = std::set>; + + server_t(common::diagnostics_t&, + configuration_t, + acceptor_set_t, + request_handler_t); + + server_t(server_t const&) = delete; + server_t(server_t&&) = delete; + + void + start(); + + void + stop(bool = false); + + server_t& operator=(server_t const&) = delete; + server_t& operator=(server_t&&) = delete; + + protected: + void + on_error(request_t&, response_t&, common_error_t) override; + + void + on_exception(request_t&, response_t&) noexcept override; + + // fixme: void shutdown_stream(); + + private: + common::diagnostics_t::proxy_t + trace(); + + void + init_accept(common::io::acceptor_t&); + + void + on_accept(common::io::acceptor_t&, std::error_code const&); + + common::diagnostics_t& diagnostics_; + configuration_t configuration_; + acceptor_set_t const acceptors_; + request_handler_t handler_; + supervisor_t supervisor_; + + }; + +} // namespace code::seafire::server + +#endif diff --git a/code/seafire/server/session.cxx b/code/seafire/server/session.cxx new file mode 100644 index 0000000..1182485 --- /dev/null +++ b/code/seafire/server/session.cxx @@ -0,0 +1,154 @@ +#include + +#include +#include +#include + +#include + +#include + +namespace code::seafire::server +{ + + session_t:: + ~session_t() noexcept + { + trace() << "~session_t()..."; + } + + supervisor_t& + session_t:: + owner() + { + return owner_; + } + + session_t::stats_t + session_t:: + stats() const + { + return stats_; + } + + void + session_t:: + start() + { + trace() << "start()..."; + init_transaction(); + } + + void + session_t:: + stop() + { + // fixme: should we really ignore the error code at this point? + // + + std::error_code ignored_ec; + stream_->close(ignored_ec); + } + + session_t:: + session_t(common::diagnostics_t& diagnostics, + error_handler_t& error_handler, + supervisor_t& owner, + std::unique_ptr stream, + request_handler_t& handler) + : diagnostics_{diagnostics}, + error_handler_{error_handler}, + owner_{owner}, + stream_{std::move(stream)}, + handler_{handler}, + connection_{*stream_, 1024*1024} // FIXME: magic number. + { + trace() << "session()..."; + } + + void + session_t:: + init_transaction() + { + trace() << "init_transaction()"; + + auto self = shared_from_this(); + + auto bound = [this, self](std::error_code const& ec, + transaction_t::result_t result) + { + on_tx_complete(ec, result == transaction_t::result_t::complete_closed); + }; + + // fixme: This will potentially destroy the previous transaction. + // Should we detect this and throw? Shouldn't ever happen... + // + current_tx_ = make_transaction(diagnostics_, + std::chrono::seconds{0}, + error_handler_, + connection_, + handler_, + bound); + + current_tx_->start(); + } + + void + session_t:: + on_tx_complete(std::error_code const& ec, bool close) + { + trace() << "on_tx_complete()..."; + + auto self = shared_from_this(); + + current_tx_.reset(); + + if (!ec) { + if (close) { + trace() << "on_tx_complete(): close requested..."; + + // Initiate a graceful close of this session/connection. + // + init_close(); + } + else { + trace() << "on_tx_complete(): keep-alive requested..."; + + // We're apparently not done, initiate a new transaction. + // + init_transaction(); + } + } + else { + trace() << "on_tx_complete(): error: " << ec; + + // If an error occurred, we just remove ourself from the + // supervisor which will eventually close the connection + // when we are destroyed. + // + owner().remove(self); + } + } + + void + session_t:: + init_close() + { + auto self = shared_from_this(); + + auto bound = [this, self]() + { + owner().remove(self); + }; + + stream_->async_graceful_close(bound); + } + + common::diagnostics_t::proxy_t + session_t:: + trace() + { + return diagnostics_ << session_category(); + } + +} // namespace code::seafire::server diff --git a/code/seafire/server/session.hxx b/code/seafire/server/session.hxx new file mode 100644 index 0000000..8061057 --- /dev/null +++ b/code/seafire/server/session.hxx @@ -0,0 +1,130 @@ +#ifndef code__seafire__server__session_hxx_ +#define code__seafire__server__session_hxx_ + +#include + +#include + +#include +#include +#include + +#include + +#include + +#include +#include + +namespace code::seafire::server +{ + + class supervisor_t; + class transaction_t; + + class session_t + : public std::enable_shared_from_this + { + public: + class info_t; + + struct stats_t + { + /// The total number of transactions handled by this session. + /// + counter_t transaction_counter; + + }; + + virtual + ~session_t() noexcept; + + supervisor_t& + owner(); + + stats_t + stats() const; + + void + start(); + + void + stop(); + + public: + friend + std::shared_ptr + make_shared_session(common::diagnostics_t&, + error_handler_t&, + supervisor_t&, + std::unique_ptr, + request_handler_t&); + + session_t(common::diagnostics_t&, + error_handler_t&, + supervisor_t&, + std::unique_ptr, + request_handler_t&); + + session_t(session_t const&) = delete; + session_t(session_t&&) = delete; + + void + init_transaction(); + + void + on_tx_complete(std::error_code const&, bool); + + void + init_close(); + + void + init_close_read(); + + void + on_close_read(std::error_code const&); + + session_t& operator=(session_t const&) = delete; + session_t& operator=(session_t&&) = delete; + + private: + common::diagnostics_t::proxy_t + trace(); + + std::mutex protector_; + + common::diagnostics_t& diagnostics_; + error_handler_t& error_handler_; + supervisor_t& owner_; + std::unique_ptr stream_; + request_handler_t& handler_; + protocol::connection_t connection_; + stats_t stats_; + std::shared_ptr current_tx_; + + char throwaway_[1]; + + }; + + class session_t::info_t + { + }; + + inline + std::shared_ptr + make_shared_session(common::diagnostics_t& diagnostics, + error_handler_t& error_handler, + supervisor_t& supervisor, + std::unique_ptr stream, + request_handler_t& request_handler) + { + return std::make_shared(diagnostics, + error_handler, + supervisor, + std::move(stream), + request_handler); + } + +} // namespace code::seafire::server + +#endif diff --git a/code/seafire/server/supervisor.cxx b/code/seafire/server/supervisor.cxx new file mode 100644 index 0000000..f86c09e --- /dev/null +++ b/code/seafire/server/supervisor.cxx @@ -0,0 +1,88 @@ +#include + +#include + +namespace code::seafire::server +{ + + supervisor_t:: + supervisor_t(common::diagnostics_t& diagnostics, + error_handler_t& error_handler, + request_handler_t& handler) + : diagnostics_{diagnostics}, + error_handler_{error_handler}, + handler_{handler} + {} + + void + supervisor_t:: + async_accept(common::io::acceptor_t& acceptor, accept_handler_t handler) + { + trace() << "async_accept()..."; + + auto bound = [this, handler](std::error_code const& ec, + std::unique_ptr stream) + { + handler(ec); + + if (!ec) { + this->start(std::move(stream)); + } + }; + + acceptor.async_accept(bound); + } + + void + supervisor_t:: + start(std::unique_ptr stream) + { + trace() << "start()..."; + + if (!stream) { + throw std::invalid_argument{"stream"}; + } + + auto session = make_shared_session(diagnostics_, + error_handler_, + *this, + std::move(stream), + handler_); + + { + std::lock_guard lock{protector_}; + sessions_.emplace(session); + } + + session->start(); + } + + void + supervisor_t:: + stop_all() + { + trace() << "stop_all()..."; + + std::lock_guard lock{protector_}; + + for (auto const& j : sessions_) { + j->stop(); + } + } + + common::diagnostics_t::proxy_t + supervisor_t:: + trace() const + { + return diagnostics_ << supervisor_category(); + } + + void + supervisor_t:: + remove(session_ptr_t session) + { + std::lock_guard lock{protector_}; + sessions_.erase(session); + } + +} // namespace code::seafire::server diff --git a/code/seafire/server/supervisor.hxx b/code/seafire/server/supervisor.hxx new file mode 100644 index 0000000..4f0c815 --- /dev/null +++ b/code/seafire/server/supervisor.hxx @@ -0,0 +1,92 @@ +#ifndef code__seafire__server__supervisor_hxx_ +#define code__seafire__server__supervisor_hxx_ + +#include + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace code::seafire::server +{ + + /// Implements a session supervisor. + /// + class supervisor_t + { + friend session_t; + + using session_ptr_t = std::shared_ptr; + + public: + class info_t; + + using accept_handler_t = std::function; + + /// Tracks supervisor statistics counters. + /// + struct counters_t + { + /// Tracks the total number of sessions. + /// + counter_t total_session_count; + + /// Tracks the total number of currently active sessions. + /// + counter_t active_session_count; + + }; + + supervisor_t(common::diagnostics_t&, + error_handler_t&, + request_handler_t&); + + supervisor_t(supervisor_t const&) = delete; + supervisor_t(supervisor_t&&) = delete; + + void + async_accept(common::io::acceptor_t&, accept_handler_t); + + void + start(std::unique_ptr); + + void + stop_all(); + + supervisor_t& operator=(supervisor_t const&) = delete; + supervisor_t& operator=(supervisor_t&&) = delete; + + private: + common::diagnostics_t::proxy_t + trace() const; + + void + remove(session_ptr_t); + + std::mutex protector_; + + common::diagnostics_t& diagnostics_; + error_handler_t& error_handler_; + request_handler_t& handler_; + std::set sessions_; + counters_t stats_; + + }; + + class supervisor_t::info_t + { + public: + }; + +} // namespace code::seafire::server + +#endif diff --git a/code/seafire/server/transaction.cxx b/code/seafire/server/transaction.cxx new file mode 100644 index 0000000..96b548f --- /dev/null +++ b/code/seafire/server/transaction.cxx @@ -0,0 +1,435 @@ +#include "code/seafire/protocol/status-code.hxx" +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include + +namespace code::seafire::server +{ + + // fixme: determine which member functions need to lock the mutex. + // + + transaction_t:: + ~transaction_t() noexcept(false) + { + trace() << "~transaction_t()..."; + } + + error_handler_t& + transaction_t:: + get_error_handler() + { + return error_handler_; + } + + protocol::connection_t& + transaction_t:: + connection() + { + return connection_; + } + + asio::any_io_executor const& + transaction_t:: + get_executor() + { + return connection().get_executor(); + } + + void + transaction_t:: + start() + { + init_read(); + } + + void + transaction_t:: + cancel() + { + // fixme: connection().cancel(); + } + + common::allocator_t& + transaction_t:: + memory() + { + return allocator_; + } + + protocol::request_t const& + transaction_t:: + get_request() const + { + return request_; + } + + std::istream& + transaction_t:: + get_request_content() + { + return request_content_stream_; + } + + common::extension_context_t& + transaction_t:: + get_request_extensions() + { + return request_extensions_; + } + + void + transaction_t:: + register_finalizer(finalizer_t* f) + { + if (f == nullptr) { + throw std::invalid_argument{"invalid finalizer"}; + } + + std::lock_guard lock{protector_}; + finalizers_.emplace_back(f); + } + + void + transaction_t:: + deregister_finalizer(finalizer_t* f) + { + if (f == nullptr) { + throw std::invalid_argument{"invalid finalizer"}; + } + + std::lock_guard lock{protector_}; + std::erase(finalizers_, f); + } + + protocol::response_t& + transaction_t:: + get_response() + { + return response_; + } + + protocol::response_t const& + transaction_t:: + get_response() const + { + return response_; + } + + common::extension_context_t& + transaction_t:: + get_response_extensions() + { + return response_extensions_; + } + + void + transaction_t:: + do_send_response(protocol::status_code_t s, + common::io::const_buffers_t const& content) + { + trace() << "do_send_response()..."; + + std::lock_guard lock{protector_}; + + auto self = shared_from_this(); + + invoke_finalizers(); + finalize_response(s, asio::buffer_size(content)); + init_write(content); + } + + void + transaction_t:: + do_send_error(common_error_t error) + { + auto self = shared_from_this(); + + auto bound = [this, self, error] + { + error_handler_.on_error(*this, *this, error); + }; + + asio::post(get_executor(), bound); + } + + transaction_t:: + transaction_t(common::diagnostics_t& diagnostics, + std::chrono::seconds request_timeout, + error_handler_t& error_handler, + protocol::connection_t& connection, + request_handler_t& handler, + completion_handler_t on_completion) + : diagnostics_{diagnostics}, + request_timeout_{request_timeout}, + error_handler_{error_handler}, + connection_{connection}, + handler_{handler}, + on_completion_{on_completion}, + request_timeout_timer_{connection_.get_executor()}, + request_content_{10}, // fixme: make configurable buffer max, + //request_content_{1024 * 1024 * 32}, // fixme: make configurable buffer max, + request_content_stream_{&request_content_} + { + trace() << "transaction_t()..."; + } + + common::diagnostics_t::proxy_t + transaction_t:: + trace() + { + return diagnostics_ << transaction_category(); + } + + bool + transaction_t:: + keep_alive() + { + auto c = get(get_request()); + + if (c && c->close()) { + return false; + } + + if (get_request().version() == protocol::http_1_0 && c) { + return c->keep_alive(); + } + + if (get_request().version() == protocol::http_1_1) { + return true; + } + + return false; + } + + void + transaction_t:: + init_read() + { + trace() << "init_read()..."; + + auto self = shared_from_this(); + prepare_response(); + + if (request_timeout_ > std::chrono::seconds{0}) { + auto on_timeout = [this, self](std::error_code const& ec) + { + on_read_timeout(ec); + }; + + request_timeout_timer_.expires_after(request_timeout_); + request_timeout_timer_.async_wait(on_timeout); + } + + auto on_read = [this, self](std::error_code const& ec) + { + this->on_read(ec); + }; + + connection().async_read(request_, request_content_, on_read); + } + + void + transaction_t:: + on_read_timeout(std::error_code const& ec) + { + auto self = shared_from_this(); + + std::lock_guard lock{protector_}; + + if (!ec) { + // fixme: add diagnostics... + // + connection_.cancel(); + send(common_error_t::request_timeout); + } + else if (ec != asio::error::operation_aborted) { + // error occurred, restart timer. + // + request_timeout_timer_.expires_after(request_timeout_); + + auto on_timeout = [this, self](std::error_code const& ec) + { + on_read_timeout(ec); + }; + + request_timeout_timer_.async_wait(on_timeout); + } + } + + void + transaction_t:: + on_read(std::error_code const& ec) + { + // Attempt to cancel the request timeout handler, if active. + // + if (request_timeout_ > std::chrono::seconds{0}) { + if (request_timeout_timer_.cancel() < 1) { + // Timeout already happened. + // + return; + } + } + + // fixme: add tracing. + // + + if (ec) { + // fixme: Send error response based on ec. + // + send(common_error_t::internal_server_error); + return; + } + + // handle Expect: 100-continue + + if (auto expect = request_.headers().get_one("expect"); expect) { + if (request_.version() == protocol::http_1_1 && *expect == "100-continue") { + static std::string const response{ + "HTTP/1.1 100 Continue\r\n\r\n" + }; + + connection().get_stream().write(common::io::buffer(response)); + } + } + + init_dispatch(); + } + + void + transaction_t:: + prepare_response() + { + using protocol::rfc7231::date_t; + using protocol::rfc7231::product_t; + using protocol::rfc7231::products_t; + using protocol::rfc7231::server_t; + + using protocol::set; + + set(get_response(), products_t{ + product_t{"Seafire", LIBCODE_SEAFIRE_SERVER_VERSION_STR} + }); + set(get_response(), std::chrono::system_clock::now()); + + // we always respond with HTTP/1.1 since that is the highest version we support. + // + get_response().set_version(protocol::http_1_1); + } + + void + transaction_t:: + init_dispatch() + { + trace() << "init_dispatch()..."; + + try { + handler_.invoke(*this, *this); + } + catch (...) { + trace() << "handler threw exception, dispatching to exception handler..."; + + try { + get_error_handler().on_exception(*this, *this); + } + catch (...) { + trace() << "exception handler threw, we're out of luck..."; + + // Fuck, we're out of luck. + // + send(common_error_t::internal_server_error); + } + } + } + + void + transaction_t:: + invoke_finalizers() + { + for (auto const& j : finalizers_) { + invoke_finalizer(j, *this); + } + } + + void + transaction_t:: + finalize_response(protocol::status_code_t const& s, + std::size_t content_length) + { + get_response().set_status(s); + + namespace rfc7230 = protocol::rfc7230; + namespace rfc7231 = protocol::rfc7231; + + if (get_request().version() == protocol::http_1_0) { + if (keep_alive()) { + set(get_response(), "keep-alive"); + } + else { + set(get_response(), "close"); + } + } + else if (get_request().version() == protocol::http_1_1) { + if (keep_alive()) { + erase(get_response()); + } + else { + set(get_response(), "close"); + } + } + + // Make sure content-type is always set. + // + if (!has(get_response())) { + set(get_response(), protocol::media_type_t{"application", "octet-stream"}); + } + + // Always set content length to the actual content length. + // + set(get_response(), content_length); + } + + void + transaction_t:: + init_write(common::io::const_buffers_t const& content) + { + trace() << "init write...()"; + + auto self = shared_from_this(); + std::lock_guard lock{protector_}; + + auto bound = [this, self](std::error_code const& ec) + { + on_write(ec); + }; + + connection().async_write(get_response(), content, bound); + } + + void + transaction_t:: + on_write(std::error_code const& ec) + { + trace() << "on_write()..."; + + auto self = shared_from_this(); + std::lock_guard lock{protector_}; + + auto bound = [self, ec, close = !keep_alive(), cb = on_completion_]() + { + cb(ec, close ? complete_closed : complete); + }; + + asio::post(get_executor(), bound); + } + +} // namespace code::seafire::server diff --git a/code/seafire/server/transaction.hxx b/code/seafire/server/transaction.hxx new file mode 100644 index 0000000..db9bb46 --- /dev/null +++ b/code/seafire/server/transaction.hxx @@ -0,0 +1,216 @@ +#ifndef code__seafire__server__transaction_hxx_ +#define code__seafire__server__transaction_hxx_ + +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include + +namespace code::seafire::server +{ + + class transaction_t + : public std::enable_shared_from_this, + request_t, + response_t + { + public: + struct configuration_t; + + /// The result of this transaction. + /// + enum result_t + { + /// Indicates a completed transation. + /// + complete, + + /// Indicates a completed transation and that the + /// underlying connection should be closed. + complete_closed + + }; + + /// Completion handler type. + /// + using completion_handler_t = std::function; + + virtual + ~transaction_t() noexcept(false); + + error_handler_t& + get_error_handler(); + + protocol::connection_t& + connection(); + + asio::any_io_executor const& + get_executor(); + + void + start(); + + void + cancel(); + + // ================= + // Common interface. + // + + common::allocator_t& + memory() override; + + // ================== + // Request interface. + // + // fixme: make these private. + // + + protocol::request_t const& + get_request() const override; + + std::istream& + get_request_content() override; + + common::extension_context_t& + get_request_extensions() override; + + // =================== + // Response interface. + // + // fixme: make these private. + // + + void + register_finalizer(finalizer_t* f) override; + + void + deregister_finalizer(finalizer_t* f) override; + + protocol::response_t& + get_response() override; + + protocol::response_t const& + get_response() const override; + + common::extension_context_t& + get_response_extensions() override; + + void + do_send_response(protocol::status_code_t, + common::io::const_buffers_t const&) override; + + void + do_send_error(common_error_t error) override; + + protected: + template + friend + std::shared_ptr + make_transaction(Args&&...); + + protected: + /// fixme: replace request_timeout with a configuration_t object. + /// + transaction_t(common::diagnostics_t&, + std::chrono::seconds request_timeout, + error_handler_t&, + protocol::connection_t&, + request_handler_t&, + completion_handler_t); + + private: + common::diagnostics_t::proxy_t + trace(); + + bool + keep_alive(); + + void + init_read(); + + void + on_read_timeout(std::error_code const&); + + void + on_read(std::error_code const&); + + void + prepare_response(); + + void + init_dispatch(); + + void + invoke_finalizers(); + + void + finalize_response(protocol::status_code_t const&, + std::size_t); + + void + init_write(common::io::const_buffers_t const&); + + void + on_write(std::error_code const&); + + std::recursive_mutex protector_; + + common::diagnostics_t& diagnostics_; + std::chrono::seconds request_timeout_; + error_handler_t& error_handler_; + common::allocator_t allocator_; + common::extension_context_t request_extensions_; + common::extension_context_t response_extensions_; + protocol::connection_t& connection_; + request_handler_t& handler_; + completion_handler_t on_completion_; + asio::steady_timer request_timeout_timer_; + protocol::request_t request_; + asio::streambuf request_content_; + std::istream request_content_stream_; + protocol::response_t response_; + std::vector finalizers_; + + }; + + /// Holds transaction configuration parameters. + /// + struct transaction_t::configuration_t + { + /// Holds the request timeout (in seconds). + /// + /// Request timeout is completely disabled if this is 0. + /// + std::chrono::seconds request_timeout; + + }; + + template + std::shared_ptr + make_transaction(Args&&... args) + { + return std::shared_ptr{ + new transaction_t{ + std::forward(args)... + } + }; + } + +} // namespace code::seafire::server + +#endif diff --git a/code/seafire/server/version.hxx.in b/code/seafire/server/version.hxx.in new file mode 100644 index 0000000..b055ec9 --- /dev/null +++ b/code/seafire/server/version.hxx.in @@ -0,0 +1,37 @@ +#ifndef code__seafire__server__version_hxx_ +#define code__seafire__server__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 LIBCODE_SEAFIRE_SERVER_VERSION $libcode_seafire_server.version.project_number$ULL +#define LIBCODE_SEAFIRE_SERVER_VERSION_STR "$libcode_seafire_server.version.project$" +#define LIBCODE_SEAFIRE_SERVER_VERSION_ID "$libcode_seafire_server.version.project_id$" +#define LIBCODE_SEAFIRE_SERVER_VERSION_FULL "$libcode_seafire_server.version$" + +#define LIBCODE_SEAFIRE_SERVER_VERSION_MAJOR $libcode_seafire_server.version.major$ +#define LIBCODE_SEAFIRE_SERVER_VERSION_MINOR $libcode_seafire_server.version.minor$ +#define LIBCODE_SEAFIRE_SERVER_VERSION_PATCH $libcode_seafire_server.version.patch$ + +#define LIBCODE_SEAFIRE_SERVER_PRE_RELEASE $libcode_seafire_server.version.pre_release$ + +#define LIBCODE_SEAFIRE_SERVER_SNAPSHOT_SN $libcode_seafire_server.version.snapshot_sn$ULL +#define LIBCODE_SEAFIRE_SERVER_SNAPSHOT_ID "$libcode_seafire_server.version.snapshot_id$" + +#endif diff --git a/manifest b/manifest new file mode 100644 index 0000000..3199c64 --- /dev/null +++ b/manifest @@ -0,0 +1,15 @@ +: 1 +name: libcode-seafire-server +version: 0.1.0-a.0.z +language: c++ +summary: libcode-seafire-server C++ library +license: BSD-4-Clause +description-file: README.md +url: https://helloryan.se/code/ +email: ryan@helloryan.se +depends: * build2 >= 0.17.0 +depends: * bpkg >= 0.17.0 +depends: libasio ^1.29.0 +depends: libcode-uri ^0.1.0- +depends: libcode-seafire-common ^0.1.0- +depends: libcode-seafire-protocol ^0.1.0- diff --git a/repositories.manifest b/repositories.manifest new file mode 100644 index 0000000..36c2756 --- /dev/null +++ b/repositories.manifest @@ -0,0 +1,19 @@ +: 1 +summary: libcode-seafire-server 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/code/libcode-uri.git##HEAD + +: +role: prerequisite +location: https://code.helloryan.se/code/libcode-seafire-common.git##HEAD + +: +role: prerequisite +location: https://code.helloryan.se/code/libcode-seafire-protocol.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/}