From 1b02d309a7e606b7dc2251a390f9e66af90f55f3 Mon Sep 17 00:00:00 2001 From: turuslan Date: Tue, 11 Feb 2025 09:46:41 +0500 Subject: [PATCH 01/40] snp quic Signed-off-by: turuslan --- BUILD.md | 4 + CMakeLists.txt | 5 + src/TODO_qtils/asio_buffer.hpp | 33 + src/TODO_qtils/from_span.hpp | 30 + src/TODO_qtils/macro/forward.hpp | 11 + src/TODO_qtils/macro/make_shared.hpp | 16 + src/TODO_qtils/macro/move.hpp | 19 + src/TODO_qtils/macro/weak.hpp | 18 + src/TODO_qtils/map_entry.hpp | 84 +++ src/TODO_qtils/std_hash_of.hpp | 16 + src/jam/CMakeLists.txt | 2 + src/jam/coro/coro.hpp | 49 ++ src/jam/coro/future.hpp | 66 ++ src/jam/coro/handler.hpp | 33 + src/jam/coro/init.hpp | 74 +++ src/jam/coro/io_context_ptr.hpp | 17 + src/jam/coro/set_thread.hpp | 19 + src/jam/coro/spawn.hpp | 60 ++ src/jam/coro/weak.hpp | 33 + src/jam/coro/yield.hpp | 18 + src/jam/ed25519.hpp | 22 + src/jam/genesis_hash.hpp | 13 + src/jam/snp/CMakeLists.txt | 34 + src/jam/snp/connections/address.hpp | 21 + src/jam/snp/connections/alpn.cpp | 46 ++ src/jam/snp/connections/alpn.hpp | 36 ++ src/jam/snp/connections/config.hpp | 20 + src/jam/snp/connections/connection.cpp | 37 ++ src/jam/snp/connections/connection.hpp | 44 ++ src/jam/snp/connections/connection_id.hpp | 17 + .../snp/connections/connection_id_counter.hpp | 24 + src/jam/snp/connections/connection_info.hpp | 19 + src/jam/snp/connections/connection_ptr.hpp | 20 + src/jam/snp/connections/connections.cpp | 148 +++++ src/jam/snp/connections/connections.hpp | 90 +++ src/jam/snp/connections/controller.hpp | 26 + src/jam/snp/connections/dns_name.cpp | 47 ++ src/jam/snp/connections/dns_name.hpp | 35 ++ src/jam/snp/connections/error.hpp | 131 ++++ src/jam/snp/connections/key.hpp | 14 + src/jam/snp/connections/lsquic/controller.hpp | 36 ++ src/jam/snp/connections/lsquic/engine.cpp | 591 ++++++++++++++++++ src/jam/snp/connections/lsquic/engine.hpp | 195 ++++++ src/jam/snp/connections/lsquic/init.cpp | 23 + src/jam/snp/connections/lsquic/init.hpp | 13 + src/jam/snp/connections/lsquic/log.hpp | 22 + src/jam/snp/connections/message_size.hpp | 15 + src/jam/snp/connections/port.hpp | 13 + src/jam/snp/connections/prefer_key.cpp | 13 + src/jam/snp/connections/prefer_key.hpp | 17 + src/jam/snp/connections/protocol_id.cpp | 19 + src/jam/snp/connections/protocol_id.hpp | 53 ++ src/jam/snp/connections/stream.cpp | 109 ++++ src/jam/snp/connections/stream.hpp | 83 +++ src/jam/snp/connections/stream_ptr.hpp | 19 + src/jam/snp/connections/tls_certificate.cpp | 103 +++ src/jam/snp/connections/tls_certificate.hpp | 59 ++ src/jam/snp/example_chat.cpp | 240 +++++++ vcpkg-overlay/cppcodec.cmake | 4 + vcpkg-overlay/liblsquic/disable-asan.patch | 23 + .../liblsquic/fix-found-boringssl.patch | 53 ++ vcpkg-overlay/liblsquic/lsquic_conn_ssl.patch | 80 +++ vcpkg-overlay/liblsquic/portfile.cmake | 78 +++ vcpkg-overlay/liblsquic/vcpkg.json | 25 + vcpkg.json | 3 + 65 files changed, 3340 insertions(+) create mode 100644 BUILD.md create mode 100644 src/TODO_qtils/asio_buffer.hpp create mode 100644 src/TODO_qtils/from_span.hpp create mode 100644 src/TODO_qtils/macro/forward.hpp create mode 100644 src/TODO_qtils/macro/make_shared.hpp create mode 100644 src/TODO_qtils/macro/move.hpp create mode 100644 src/TODO_qtils/macro/weak.hpp create mode 100644 src/TODO_qtils/map_entry.hpp create mode 100644 src/TODO_qtils/std_hash_of.hpp create mode 100644 src/jam/coro/coro.hpp create mode 100644 src/jam/coro/future.hpp create mode 100644 src/jam/coro/handler.hpp create mode 100644 src/jam/coro/init.hpp create mode 100644 src/jam/coro/io_context_ptr.hpp create mode 100644 src/jam/coro/set_thread.hpp create mode 100644 src/jam/coro/spawn.hpp create mode 100644 src/jam/coro/weak.hpp create mode 100644 src/jam/coro/yield.hpp create mode 100644 src/jam/genesis_hash.hpp create mode 100644 src/jam/snp/CMakeLists.txt create mode 100644 src/jam/snp/connections/address.hpp create mode 100644 src/jam/snp/connections/alpn.cpp create mode 100644 src/jam/snp/connections/alpn.hpp create mode 100644 src/jam/snp/connections/config.hpp create mode 100644 src/jam/snp/connections/connection.cpp create mode 100644 src/jam/snp/connections/connection.hpp create mode 100644 src/jam/snp/connections/connection_id.hpp create mode 100644 src/jam/snp/connections/connection_id_counter.hpp create mode 100644 src/jam/snp/connections/connection_info.hpp create mode 100644 src/jam/snp/connections/connection_ptr.hpp create mode 100644 src/jam/snp/connections/connections.cpp create mode 100644 src/jam/snp/connections/connections.hpp create mode 100644 src/jam/snp/connections/controller.hpp create mode 100644 src/jam/snp/connections/dns_name.cpp create mode 100644 src/jam/snp/connections/dns_name.hpp create mode 100644 src/jam/snp/connections/error.hpp create mode 100644 src/jam/snp/connections/key.hpp create mode 100644 src/jam/snp/connections/lsquic/controller.hpp create mode 100644 src/jam/snp/connections/lsquic/engine.cpp create mode 100644 src/jam/snp/connections/lsquic/engine.hpp create mode 100644 src/jam/snp/connections/lsquic/init.cpp create mode 100644 src/jam/snp/connections/lsquic/init.hpp create mode 100644 src/jam/snp/connections/lsquic/log.hpp create mode 100644 src/jam/snp/connections/message_size.hpp create mode 100644 src/jam/snp/connections/port.hpp create mode 100644 src/jam/snp/connections/prefer_key.cpp create mode 100644 src/jam/snp/connections/prefer_key.hpp create mode 100644 src/jam/snp/connections/protocol_id.cpp create mode 100644 src/jam/snp/connections/protocol_id.hpp create mode 100644 src/jam/snp/connections/stream.cpp create mode 100644 src/jam/snp/connections/stream.hpp create mode 100644 src/jam/snp/connections/stream_ptr.hpp create mode 100644 src/jam/snp/connections/tls_certificate.cpp create mode 100644 src/jam/snp/connections/tls_certificate.hpp create mode 100644 src/jam/snp/example_chat.cpp create mode 100644 vcpkg-overlay/cppcodec.cmake create mode 100644 vcpkg-overlay/liblsquic/disable-asan.patch create mode 100644 vcpkg-overlay/liblsquic/fix-found-boringssl.patch create mode 100644 vcpkg-overlay/liblsquic/lsquic_conn_ssl.patch create mode 100644 vcpkg-overlay/liblsquic/portfile.cmake create mode 100644 vcpkg-overlay/liblsquic/vcpkg.json diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000..15e6fa2 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,4 @@ + +```bash +brew install nasm # vcpkg liblsquic +``` diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f50388..24e0716 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,8 +27,13 @@ pkg_check_modules(libb2 REQUIRED IMPORTED_TARGET GLOBAL libb2) find_package(Boost CONFIG REQUIRED) find_package(fmt CONFIG REQUIRED) find_package(jam_crust CONFIG REQUIRED) +find_package(lsquic CONFIG REQUIRED) +find_package(OpenSSL REQUIRED) find_package(scale CONFIG REQUIRED) find_package(schnorrkel_crust CONFIG REQUIRED) +find_package(ZLIB REQUIRED) + +include(vcpkg-overlay/cppcodec.cmake) add_library(headers INTERFACE) target_include_directories(headers INTERFACE diff --git a/src/TODO_qtils/asio_buffer.hpp b/src/TODO_qtils/asio_buffer.hpp new file mode 100644 index 0000000..a2ec4d2 --- /dev/null +++ b/src/TODO_qtils/asio_buffer.hpp @@ -0,0 +1,33 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +namespace qtils { + inline boost::asio::const_buffer asioBuffer(BytesIn s) { + return {s.data(), s.size()}; + } + + boost::asio::mutable_buffer asioBuffer(auto &&t) + requires(requires { BytesOut{t}; }) + { + BytesOut s{t}; + return {s.data(), s.size()}; + } + + inline BytesIn asioBuffer(const boost::asio::const_buffer &s) { + // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + return {reinterpret_cast(s.data()), s.size()}; + } + + inline BytesOut asioBuffer(const boost::asio::mutable_buffer &s) { + // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + return {reinterpret_cast(s.data()), s.size()}; + } +} // namespace qtils diff --git a/src/TODO_qtils/from_span.hpp b/src/TODO_qtils/from_span.hpp new file mode 100644 index 0000000..f19ef22 --- /dev/null +++ b/src/TODO_qtils/from_span.hpp @@ -0,0 +1,30 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +namespace qtils { + inline bool fromSpan(BytesOut out, BytesIn span) { + if (span.size() != out.size()) { + return false; + } + memcpy(out.data(), span.data(), out.size()); + return true; + } + + template + std::optional fromSpan(BytesIn span) { + T out; + if (not fromSpan(out, span)) { + return std::nullopt; + } + return out; + } +} // namespace qtils diff --git a/src/TODO_qtils/macro/forward.hpp b/src/TODO_qtils/macro/forward.hpp new file mode 100644 index 0000000..d03cec6 --- /dev/null +++ b/src/TODO_qtils/macro/forward.hpp @@ -0,0 +1,11 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#define FORWARD(x) std::forward(x) diff --git a/src/TODO_qtils/macro/make_shared.hpp b/src/TODO_qtils/macro/make_shared.hpp new file mode 100644 index 0000000..fb49925 --- /dev/null +++ b/src/TODO_qtils/macro/make_shared.hpp @@ -0,0 +1,16 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#define MAKE_SHARED_(x_, ...) \ + x_ { \ + std::make_shared(__VA_ARGS__) \ + } + +#define MAKE_SHARED_T(T, ...) std::make_shared(__VA_ARGS__) diff --git a/src/TODO_qtils/macro/move.hpp b/src/TODO_qtils/macro/move.hpp new file mode 100644 index 0000000..2fba319 --- /dev/null +++ b/src/TODO_qtils/macro/move.hpp @@ -0,0 +1,19 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#define MOVE(x) \ + x { \ + std::move(x) \ + } + +#define MOVE_(x) \ + x##_ { \ + std::move(x) \ + } diff --git a/src/TODO_qtils/macro/weak.hpp b/src/TODO_qtils/macro/weak.hpp new file mode 100644 index 0000000..34d7bd8 --- /dev/null +++ b/src/TODO_qtils/macro/weak.hpp @@ -0,0 +1,18 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#define WEAK_SELF \ + weak_self { \ + weak_from_this() \ + } + +#define WEAK_LOCK(name) \ + auto name = weak_##name.lock(); \ + if (not name) return diff --git a/src/TODO_qtils/map_entry.hpp b/src/TODO_qtils/map_entry.hpp new file mode 100644 index 0000000..b4c714b --- /dev/null +++ b/src/TODO_qtils/map_entry.hpp @@ -0,0 +1,84 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include + +namespace qtils { + template + struct MapEntry { + using I = typename M::iterator; + using K = typename M::key_type; + + MapEntry(M &map, const K &key) : map{map} { + if (auto it = map.find(key); it != map.end()) { + it_or_key = it; + } else { + it_or_key = key; + } + } + + bool has() const { + return std::holds_alternative(it_or_key); + } + operator bool() const { + return has(); + } + auto &operator*() { + if (not has()) { + throw std::logic_error{"MapEntry::operator*"}; + } + return std::get(it_or_key)->second; + } + auto *operator->() { + if (not has()) { + throw std::logic_error{"MapEntry::operator->"}; + } + return &std::get(it_or_key)->second; + } + void insert(M::mapped_type value) { + if (has()) { + throw std::logic_error{"MapEntry::insert"}; + } + it_or_key = + map.emplace(std::move(std::get(it_or_key)), std::move(value)) + .first; + } + void insert_or_assign(M::mapped_type value) { + if (not has()) { + insert(std::move(value)); + } else { + **this = std::move(value); + } + } + M::mapped_type remove() { + if (not has()) { + throw std::logic_error{"MapEntry::remove"}; + } + auto node = map.extract(std::get(it_or_key)); + it_or_key = std::move(node.key()); + return std::move(node.mapped()); + } + + // NOLINTNEXTLINE(cppcoreguidelines-avoid-const-or-ref-data-members) + M ↦ + std::variant it_or_key{}; + }; + + template + auto entry(std::map &map, const K &key) { + return MapEntry>{map, key}; + } + + template + auto entry(std::unordered_map &map, const K &key) { + return MapEntry>{map, key}; + } +} // namespace qtils diff --git a/src/TODO_qtils/std_hash_of.hpp b/src/TODO_qtils/std_hash_of.hpp new file mode 100644 index 0000000..f20aa01 --- /dev/null +++ b/src/TODO_qtils/std_hash_of.hpp @@ -0,0 +1,16 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace qtils { + template + size_t stdHashOf(const T &v) { + return std::hash()(v); + } +} // namespace qtils diff --git a/src/jam/CMakeLists.txt b/src/jam/CMakeLists.txt index 4fd9231..761be33 100644 --- a/src/jam/CMakeLists.txt +++ b/src/jam/CMakeLists.txt @@ -3,3 +3,5 @@ # All Rights Reserved # SPDX-License-Identifier: Apache-2.0 # + +add_subdirectory(snp) diff --git a/src/jam/coro/coro.hpp b/src/jam/coro/coro.hpp new file mode 100644 index 0000000..99c29a7 --- /dev/null +++ b/src/jam/coro/coro.hpp @@ -0,0 +1,49 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +namespace jam { + /** + * Return type for coroutine. + * + * Does not resume when: + * - called directly outside executor, returns coroutine. + * - `coroSpawn` called when not running inside executor, + * resumes on next executor tick. + * int main() { + * boost::asio::io_context io; + * coroSpawn(io, []() -> Coro { co_return; }); // suspended + * io.run_one(); // resumes + * // may complete before next statement + * } + * Resumes when: + * - `coroSpawn` when running inside specified executor. + * post(executor, [] { + * coroSpawn(executor, []() -> Coro { co_return; }) // resumes + * // may complete before next statement + * }) + * co_await coroSpawn([]() -> Coro { co_return; }) // resumes + * // may complete before next statement + * - `co_await` + * co_await foo() // resumes + * // may complete before next statement + * After resuming may complete before specified statement ends. + * + * Use `CORO_YIELD` explicitly to suspend coroutine until next executor tick. + */ + template + using Coro = boost::asio::awaitable; + + /** + * Return type for coroutine returning outcome. + */ + template + using CoroOutcome = Coro>; +} // namespace jam diff --git a/src/jam/coro/future.hpp b/src/jam/coro/future.hpp new file mode 100644 index 0000000..95b2735 --- /dev/null +++ b/src/jam/coro/future.hpp @@ -0,0 +1,66 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace jam { + template + class SharedFuture { + public: + using Self = std::shared_ptr>; + + SharedFuture(IoContextPtr io_context_ptr) : MOVE_(io_context_ptr) {} + + static Coro ready(Self self) { + SET_CORO_THREAD(self->io_context_ptr_); + co_return std::holds_alternative(self->state_); + } + + /** + * Resumes coroutine immediately or inside `set`. + */ + static Coro get(Self self) { + SET_CORO_THREAD(self->io_context_ptr_); + if (auto *value = std::get_if(&self->state_)) { + co_return *value; + } + auto &handlers = std::get(self->state_); + co_return co_await coroHandler([&](CoroHandler &&handler) { + handlers.emplace_back(std::move(handler)); + }); + } + + /** + * Set value and wake waiting coroutines. + * Coroutines may complete before `set` returns. + */ + static Coro set(Self self, T value) { + SET_CORO_THREAD(self->io_context_ptr_); + if (std::holds_alternative(self->state_)) { + throw std::logic_error{"SharedFuture::set must be called once"}; + } + auto handlers = std::move(std::get(self->state_)); + self->state_ = std::move(value); + auto &state_value = std::get(self->state_); + for (auto &handler : handlers) { + handler(state_value); + } + } + + private: + using Handlers = std::deque>; + + IoContextPtr io_context_ptr_; + std::variant state_; + }; +} // namespace jam diff --git a/src/jam/coro/handler.hpp b/src/jam/coro/handler.hpp new file mode 100644 index 0000000..97e8d4e --- /dev/null +++ b/src/jam/coro/handler.hpp @@ -0,0 +1,33 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +namespace jam { + template + using CoroHandler = std::conditional_t< + std::is_void_v, + boost::asio::detail::awaitable_handler::executor_type>, + boost::asio::detail::awaitable_handler::executor_type, + T>>; + + /** + * Create handler for coroutine. + * Coroutine may complete earlier than handler returns. + */ + template + Coro coroHandler(std::invocable &&> auto &&f) { + co_await [&](auto *frame) { + f(CoroHandler{frame->detach_thread()}); + return nullptr; + }; + abort(); + } +} // namespace jam diff --git a/src/jam/coro/init.hpp b/src/jam/coro/init.hpp new file mode 100644 index 0000000..b5cc123 --- /dev/null +++ b/src/jam/coro/init.hpp @@ -0,0 +1,74 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +namespace jam { + /** + * Async init flag. + * struct Foo { + * CoroOutcome init() { + * auto init = init_.init(); // dtor will fail incomplete init + * ... + * init.ready(); // init completed + * } + * CoroOutcome foo() { + * if (not co_await init_.ready()) // init failed + * ... // ready + * } + * CoroInit init_; + * } + */ + class CoroInit { + class Init { + public: + Init(CoroInit &self) : self_{self} {} + ~Init() { + self_.set(false); + } + void ready() { + self_.set(true); + } + + private: + CoroInit &self_; + }; + + public: + CoroInit(IoContextPtr io_context_ptr) + : MOVE_(io_context_ptr), MAKE_SHARED_(future_, io_context_ptr_) {} + + auto init() { + if (init_called_) { + throw std::logic_error{"Coro::init init must be called once"}; + } + init_called_ = true; + return Init{*this}; + } + + Coro ready() { + return future_->get(future_); + } + + private: + void set(bool ready) { + coroSpawn(*io_context_ptr_, [future{future_}, ready]() -> Coro { + if (not ready and co_await future->ready(future)) { + co_return; + } + co_await future->set(future, ready); + }); + } + + IoContextPtr io_context_ptr_; + std::shared_ptr> future_; + bool init_called_ = false; + }; +} // namespace jam diff --git a/src/jam/coro/io_context_ptr.hpp b/src/jam/coro/io_context_ptr.hpp new file mode 100644 index 0000000..d88248e --- /dev/null +++ b/src/jam/coro/io_context_ptr.hpp @@ -0,0 +1,17 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace boost::asio { + class io_context; +} // namespace boost::asio + +namespace jam { + using IoContextPtr = std::shared_ptr; +} // namespace jam diff --git a/src/jam/coro/set_thread.hpp b/src/jam/coro/set_thread.hpp new file mode 100644 index 0000000..9cb26ea --- /dev/null +++ b/src/jam/coro/set_thread.hpp @@ -0,0 +1,19 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include + +#define SET_CORO_THREAD(io_context_ptr) \ + ({ \ + if (not io_context_ptr->get_executor().running_in_this_thread()) { \ + co_await boost::asio::post(*io_context_ptr, boost::asio::use_awaitable); \ + } \ + }) diff --git a/src/jam/coro/spawn.hpp b/src/jam/coro/spawn.hpp new file mode 100644 index 0000000..532fee5 --- /dev/null +++ b/src/jam/coro/spawn.hpp @@ -0,0 +1,60 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace jam { + void coroSpawn(auto &&executor, Coro &&coro) { + boost::asio::co_spawn( + FORWARD(executor), std::move(coro), [](std::exception_ptr e) { + if (e != nullptr) { + std::rethrow_exception(e); + } + }); + } + + template + void coroSpawn(auto &&executor, Coro &&coro) { + coroSpawn(FORWARD(executor), [MOVE(coro)]() mutable -> Coro { + std::ignore = co_await std::move(coro); + }); + } + + /** + * Start coroutine on specified executor. + * Spawning on same executor would execute coroutine immediately, + * so coroutine may complete before `coroSpawn` returns. + * Prevents dangling lambda capture in `coroSpawn([capture] { ... })`. + * `co_spawn([capture] { ... })` doesn't work + * because lambda is destroyed after returning coroutine object. + * `co_spawn([](args){ ... }(capture))` + * works because arguments are stored in coroutine state. + */ + void coroSpawn(auto &&executor, auto f) { + coroSpawn(FORWARD(executor), [](decltype(f) f) -> Coro { + if constexpr (std::is_void_v) { + co_await f(); + } else { + std::ignore = co_await f(); + } + }(std::move(f))); + } + + /** + * `coroSpawn` with current coroutine executor. + */ + Coro coroSpawn(auto f) { + coroSpawn(co_await boost::asio::this_coro::executor, std::move(f)); + co_return; + } +} // namespace jam diff --git a/src/jam/coro/weak.hpp b/src/jam/coro/weak.hpp new file mode 100644 index 0000000..857cd63 --- /dev/null +++ b/src/jam/coro/weak.hpp @@ -0,0 +1,33 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#define _CORO_WEAK_AWAIT(tmp_weak, tmp_coro, auto_r, r, shared, coro, ...) \ + ({ \ + auto tmp_weak = std::weak_ptr{shared}; \ + auto tmp_coro = (coro); \ + shared.reset(); \ + auto_r co_await std::move(tmp_coro); \ + shared = tmp_weak.lock(); \ + if (not shared) co_return __VA_ARGS__; \ + r \ + }) +#define CORO_WEAK_AWAIT(shared, coro, ...) \ + _CORO_WEAK_AWAIT( \ + QTILS_UNIQUE_NAME(tmp_weak), QTILS_UNIQUE_NAME(tmp_coro), auto r =, r; \ + , shared, coro, __VA_ARGS__) + +#define CORO_WEAK_AWAIT_V(shared, coro, ...) \ + _CORO_WEAK_AWAIT(QTILS_UNIQUE_NAME(tmp_weak), \ + QTILS_UNIQUE_NAME(tmp_coro), \ + , \ + , \ + shared, \ + coro, \ + __VA_ARGS__) diff --git a/src/jam/coro/yield.hpp b/src/jam/coro/yield.hpp new file mode 100644 index 0000000..e8b4cbc --- /dev/null +++ b/src/jam/coro/yield.hpp @@ -0,0 +1,18 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +/** + * Thread switch operation always completes, so it can't leak `shared_ptr`. + */ +#define CORO_YIELD \ + co_await boost::asio::post(co_await boost::asio::this_coro::executor, \ + boost::asio::use_awaitable) diff --git a/src/jam/ed25519.hpp b/src/jam/ed25519.hpp index 5699d57..e51659a 100644 --- a/src/jam/ed25519.hpp +++ b/src/jam/ed25519.hpp @@ -4,10 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ +#pragma once + #include +#include #include namespace jam::ed25519 { + using Seed = qtils::BytesN; using Secret = qtils::BytesN; using Public = qtils::BytesN; using KeyPair = qtils::BytesN; @@ -34,4 +38,22 @@ namespace jam::ed25519 { message.size_bytes()); return res == ED25519_RESULT_OK; } + + inline KeyPair from_seed(const Seed &seed) { + KeyPair keypair; + ed25519_keypair_from_seed(keypair.data(), seed.data()); + return keypair; + } + + inline Public get_public(const KeyPair &keypair) { + return qtils::fromSpan( + std::span{keypair}.subspan(ED25519_SECRET_KEY_LENGTH)) + .value(); + } + + inline Public get_secret(const KeyPair &keypair) { + return qtils::fromSpan( + std::span{keypair}.first(ED25519_SECRET_KEY_LENGTH)) + .value(); + } } // namespace jam::ed25519 diff --git a/src/jam/genesis_hash.hpp b/src/jam/genesis_hash.hpp new file mode 100644 index 0000000..8c79800 --- /dev/null +++ b/src/jam/genesis_hash.hpp @@ -0,0 +1,13 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace jam { + using GenesisHash = qtils::BytesN<32>; +} // namespace jam diff --git a/src/jam/snp/CMakeLists.txt b/src/jam/snp/CMakeLists.txt new file mode 100644 index 0000000..070c8b5 --- /dev/null +++ b/src/jam/snp/CMakeLists.txt @@ -0,0 +1,34 @@ +# +# Copyright Quadrivium LLC +# All Rights Reserved +# SPDX-License-Identifier: Apache-2.0 +# + +add_library(snp + connections/alpn.cpp + connections/connection.cpp + connections/connections.cpp + connections/dns_name.cpp + connections/lsquic/engine.cpp + connections/lsquic/init.cpp + connections/prefer_key.cpp + connections/protocol_id.cpp + connections/stream.cpp + connections/tls_certificate.cpp +) +target_link_libraries(snp + fmt::fmt + headers + lsquic::lsquic + OpenSSL::SSL + schnorrkel_crust::schnorrkel_crust + ZLIB::ZLIB +) + +add_executable(example_chat + example_chat.cpp +) +target_link_libraries(example_chat + snp +) + diff --git a/src/jam/snp/connections/address.hpp b/src/jam/snp/connections/address.hpp new file mode 100644 index 0000000..f6eac19 --- /dev/null +++ b/src/jam/snp/connections/address.hpp @@ -0,0 +1,21 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +namespace jam::snp { + struct Address { + using Ip = qtils::BytesN<16>; + static constexpr Ip kLocal{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; + + Ip ip; + Port port; + Key key; + }; +} // namespace jam::snp diff --git a/src/jam/snp/connections/alpn.cpp b/src/jam/snp/connections/alpn.cpp new file mode 100644 index 0000000..93f4c07 --- /dev/null +++ b/src/jam/snp/connections/alpn.cpp @@ -0,0 +1,46 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include +#include +#include + +namespace jam::snp { + Alpn::Alpn(const GenesisHash &genesis) { + const auto protocol = + fmt::format("jamnp-s/{}/{:x}", kVersion, std::span{genesis}.first(4)); + bytes_.reserve(1 + protocol.size()); + bytes_.emplace_back(protocol.size()); + qtils::append(bytes_, qtils::str2byte(protocol)); + } + + outcome::result Alpn::set(ssl_ctx_st *ssl_ctx) { + if (SSL_CTX_set_alpn_protos(ssl_ctx, bytes_.data(), bytes_.size()) != 0) { + return OpenSslError::SSL_CTX_set_alpn_protos; + } + SSL_CTX_set_alpn_select_cb(ssl_ctx, select, this); + return outcome::success(); + } + + int Alpn::select(ssl_st *ssl, + const unsigned char **out, + unsigned char *outlen, + const unsigned char *in, + unsigned int inlen, + void *void_self) { + auto *self = static_cast(void_self); + uint8_t *out2 = nullptr; + int r = SSL_select_next_proto( + &out2, outlen, in, inlen, self->bytes_.data(), self->bytes_.size()); + *out = out2; + return r == OPENSSL_NPN_NEGOTIATED ? SSL_TLSEXT_ERR_OK + : SSL_TLSEXT_ERR_ALERT_FATAL; + } +} // namespace jam::snp diff --git a/src/jam/snp/connections/alpn.hpp b/src/jam/snp/connections/alpn.hpp new file mode 100644 index 0000000..ff8fa88 --- /dev/null +++ b/src/jam/snp/connections/alpn.hpp @@ -0,0 +1,36 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +struct ssl_ctx_st; +struct ssl_st; + +namespace jam::snp { + // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L30-L41 + class Alpn { + static constexpr auto kVersion = 0; + + public: + Alpn(const GenesisHash &genesis); + + outcome::result set(ssl_ctx_st *ssl_ctx); + + private: + static int select(ssl_st *ssl, + const unsigned char **out, + unsigned char *outlen, + const unsigned char *in, + unsigned int inlen, + void *void_self); + + qtils::Bytes bytes_; + }; +} // namespace jam::snp diff --git a/src/jam/snp/connections/config.hpp b/src/jam/snp/connections/config.hpp new file mode 100644 index 0000000..377ee2e --- /dev/null +++ b/src/jam/snp/connections/config.hpp @@ -0,0 +1,20 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +namespace jam::snp { + struct ConnectionsConfig { + // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L30-L35 + GenesisHash genesis; + ed25519::KeyPair keypair; + std::optional listen_port; + }; +} // namespace jam::snp diff --git a/src/jam/snp/connections/connection.cpp b/src/jam/snp/connections/connection.cpp new file mode 100644 index 0000000..4980cae --- /dev/null +++ b/src/jam/snp/connections/connection.cpp @@ -0,0 +1,37 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include +#include +#include + +namespace jam::snp { + using lsquic::Engine; + + Connection::Connection(IoContextPtr io_context_ptr, + lsquic::ConnCtx *conn_ctx, + ConnectionInfo info) + : MOVE_(io_context_ptr), MOVE_(conn_ctx), MOVE_(info) {} + + Connection::~Connection() { + boost::asio::dispatch(*io_context_ptr_, [conn_ctx{conn_ctx_}] { + Engine::destroyConnection(conn_ctx); + }); + } + + const ConnectionInfo &Connection::info() const { + return info_; + } + + StreamPtrCoroOutcome Connection::open(Self self, ProtocolId protocol_id) { + SET_CORO_THREAD(self->io_context_ptr_); + co_return co_await Engine::openStream(self->conn_ctx_, protocol_id); + } +} // namespace jam::snp diff --git a/src/jam/snp/connections/connection.hpp b/src/jam/snp/connections/connection.hpp new file mode 100644 index 0000000..0d15dd9 --- /dev/null +++ b/src/jam/snp/connections/connection.hpp @@ -0,0 +1,44 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace jam::snp::lsquic { + struct ConnCtx; + class Engine; +} // namespace jam::snp::lsquic + +namespace jam::snp { + class Connection { + friend lsquic::Engine; + + public: + using Self = std::shared_ptr; + + Connection(IoContextPtr io_context_ptr, + lsquic::ConnCtx *conn_ctx, + ConnectionInfo info); + ~Connection(); + + const ConnectionInfo &info() const; + + /** + * Open stream with specified `ProtocolId`. + */ + static StreamPtrCoroOutcome open(Self self, ProtocolId protocol_id); + + private: + IoContextPtr io_context_ptr_; + lsquic::ConnCtx *conn_ctx_; + ConnectionInfo info_; + }; +} // namespace jam::snp diff --git a/src/jam/snp/connections/connection_id.hpp b/src/jam/snp/connections/connection_id.hpp new file mode 100644 index 0000000..040e250 --- /dev/null +++ b/src/jam/snp/connections/connection_id.hpp @@ -0,0 +1,17 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace jam::snp { + /** + * Is not QUIC connection id. + * Used to distinguish connections with same peer key. + */ + using ConnectionId = uint64_t; +} // namespace jam::snp diff --git a/src/jam/snp/connections/connection_id_counter.hpp b/src/jam/snp/connections/connection_id_counter.hpp new file mode 100644 index 0000000..9576fee --- /dev/null +++ b/src/jam/snp/connections/connection_id_counter.hpp @@ -0,0 +1,24 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +namespace jam::snp { + class ConnectionIdCounter { + public: + ConnectionId make() { + return connection_id_->fetch_add(1); + } + + private: + using Atomic = std::atomic; + std::shared_ptr connection_id_ = std::make_shared(); + }; +} // namespace jam::snp diff --git a/src/jam/snp/connections/connection_info.hpp b/src/jam/snp/connections/connection_info.hpp new file mode 100644 index 0000000..96ab32c --- /dev/null +++ b/src/jam/snp/connections/connection_info.hpp @@ -0,0 +1,19 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +namespace jam::snp { + struct ConnectionInfo { + ConnectionId id; + Key key; + + bool operator==(const ConnectionInfo &) const = default; + }; +} // namespace jam::snp diff --git a/src/jam/snp/connections/connection_ptr.hpp b/src/jam/snp/connections/connection_ptr.hpp new file mode 100644 index 0000000..5f7723f --- /dev/null +++ b/src/jam/snp/connections/connection_ptr.hpp @@ -0,0 +1,20 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +namespace jam::snp { + class Connection; +} // namespace jam::snp + +namespace jam::snp { + using ConnectionPtr = std::shared_ptr; + using ConnectionPtrOutcome = outcome::result; + using ConnectionPtrCoroOutcome = CoroOutcome; +} // namespace jam::snp diff --git a/src/jam/snp/connections/connections.cpp b/src/jam/snp/connections/connections.cpp new file mode 100644 index 0000000..b5195c0 --- /dev/null +++ b/src/jam/snp/connections/connections.cpp @@ -0,0 +1,148 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace jam::snp { + inline void todoPreferConnection() { + // TODO(turuslan): how to deduplicate connections between two peers? + throw std::logic_error{"TODO: prefer connection"}; + } + + Connections::Connections(IoContextPtr io_context_ptr, + ConnectionsConfig config) + : MOVE_(io_context_ptr), + init_{io_context_ptr_}, + MOVE_(config), + key_{ed25519::get_public(config_.keypair)} {} + + CoroOutcome Connections::init( + Self self, std::weak_ptr controller) { + SET_CORO_THREAD(self->io_context_ptr_); + auto init = self->init_.init(); + self->controller_ = std::move(controller); + BOOST_OUTCOME_CO_TRY(auto certificate, TlsCertificate::make(self->config_)); + BOOST_OUTCOME_CO_TRY(self->client_, + lsquic::Engine::make(self->io_context_ptr_, + self->connection_id_counter_, + certificate, + std::nullopt, + self)); + if (self->config_.listen_port) { + BOOST_OUTCOME_CO_TRY(self->server_, + lsquic::Engine::make(self->io_context_ptr_, + self->connection_id_counter_, + certificate, + self->config_.listen_port, + self)); + } + init.ready(); + co_return outcome::success(); + } + + const Key &Connections::key() const { + return key_; + } + + ConnectionPtrCoroOutcome Connections::connect(Self self, Address address) { + SET_CORO_THREAD(self->io_context_ptr_); + if (not co_await self->init_.ready()) { + co_return ConnectionsError::CONNECTIONS_INIT; + } + auto state = qtils::entry(self->connections_, address.key); + if (not state) { + state.insert(MAKE_SHARED_T(Connecting, self->io_context_ptr_)); + co_await coroSpawn([self, address, state]() mutable -> Coro { + CORO_YIELD; + auto connection_result = CORO_WEAK_AWAIT( + self, self->client_->connect(self->client_, address)); + auto state = qtils::entry(self->connections_, address.key); + if (not state or not std::holds_alternative(*state)) { + todoPreferConnection(); + } + auto connecting = std::move(std::get(*state)); + if (connection_result) { + auto &connection = connection_result.value(); + *state = Connected{connection}; + if (auto controller = self->controller_.lock()) { + controller->onOpen(address.key); + } + } else { + state.remove(); + } + CORO_WEAK_AWAIT_V( + self, connecting->set(connecting, std::move(connection_result))); + }); + } else if (auto *connected = std::get_if(&*state)) { + co_return *connected; + } + auto connecting = std::get(*state); + self.reset(); + co_return co_await connecting->get(connecting); + } + + Coro Connections::serve(Self self, + ProtocolId protocol_id, + ServeProtocol serve) { + SET_CORO_THREAD(self->io_context_ptr_); + qtils::entry(self->protocols_, protocol_id).insert(std::move(serve)); + } + + void Connections::onConnectionAccept(ConnectionPtr connection) { + auto state = entry(connections_, connection->info().key); + if (state) { + todoPreferConnection(); + } + state.insert(Connected{connection}); + if (auto controller = controller_.lock()) { + controller->onOpen(connection->info().key); + } + } + + void Connections::onConnectionClose(ConnectionInfo connection_info) { + auto state = entry(connections_, connection_info.key); + if (not state or not std::holds_alternative(*state) + or std::get(*state)->info() != connection_info) { + todoPreferConnection(); + } + state.remove(); + if (auto controller = controller_.lock()) { + controller->onClose(connection_info.key); + } + } + + void Connections::onStreamAccept(ConnectionPtr connection, + ProtocolId protocol_id, + StreamPtr stream) { + coroSpawn(*io_context_ptr_, + [self{shared_from_this()}, + protocol_id, + MOVE(stream), + connection_info{connection->info()}]() mutable -> Coro { + auto serve = qtils::entry(self->protocols_, protocol_id); + if (not serve) { + co_return; + } + auto copy = *serve; + std::ignore = CORO_WEAK_AWAIT( + self, copy(connection_info, std::move(stream))); + }); + } +} // namespace jam::snp diff --git a/src/jam/snp/connections/connections.hpp b/src/jam/snp/connections/connections.hpp new file mode 100644 index 0000000..d1b2e96 --- /dev/null +++ b/src/jam/snp/connections/connections.hpp @@ -0,0 +1,90 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace jam::snp { + class Address; + class ConnectionsController; +} // namespace jam::snp + +namespace jam::snp::lsquic { + class Engine; +} // namespace jam::snp::lsquic + +namespace jam::snp { + /** + * Initiates and accepts connections with peers. + * Prevents duplicate connections with peers. + */ + class Connections : public std::enable_shared_from_this, + public lsquic::EngineController { + public: + using Self = std::shared_ptr; + + Connections(IoContextPtr io_context_ptr, ConnectionsConfig config); + + /** + * Set controller. + * Start quic server and client. + */ + static CoroOutcome init( + Self self, std::weak_ptr controller); + + const Key &key() const; + + /** + * Connect or return existing connection. + */ + static ConnectionPtrCoroOutcome connect(Self self, Address address); + + using ServeProtocol = + std::function(ConnectionInfo, StreamPtr)>; + /** + * Set callback to handle protocol on server side. + */ + static Coro serve(Self self, + ProtocolId protocol_id, + ServeProtocol serve); + + // EngineController + void onConnectionAccept(ConnectionPtr connection) override; + void onConnectionClose(ConnectionInfo connection_info) override; + void onStreamAccept(ConnectionPtr connection, + ProtocolId protocol_id, + StreamPtr stream) override; + + private: + using Connecting = std::shared_ptr>; + using Connected = ConnectionPtr; + + IoContextPtr io_context_ptr_; + CoroInit init_; + ConnectionsConfig config_; + Key key_; + std::weak_ptr controller_; + std::shared_ptr client_; + std::optional> server_; + std::unordered_map, + qtils::BytesStdHash> + connections_; + std::unordered_map protocols_; + ConnectionIdCounter connection_id_counter_; + }; +} // namespace jam::snp diff --git a/src/jam/snp/connections/controller.hpp b/src/jam/snp/connections/controller.hpp new file mode 100644 index 0000000..0db3e0a --- /dev/null +++ b/src/jam/snp/connections/controller.hpp @@ -0,0 +1,26 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace jam::snp { + class ConnectionsController { + public: + virtual ~ConnectionsController() = default; + + /** + * There is now some connection with peer. + */ + virtual void onOpen(Key key) {} + + /** + * There are no more connections with peer. + */ + virtual void onClose(Key key) {} + }; +} // namespace jam::snp diff --git a/src/jam/snp/connections/dns_name.cpp b/src/jam/snp/connections/dns_name.cpp new file mode 100644 index 0000000..6b66db9 --- /dev/null +++ b/src/jam/snp/connections/dns_name.cpp @@ -0,0 +1,47 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include + +namespace jam::snp::base32 { + constexpr auto kAlphabet = "abcdefghijklmnopqrstuvwxyz234567"; + + struct Config { + template + using codec_impl = cppcodec::detail::stream_codec; + + static CPPCODEC_ALWAYS_INLINE constexpr size_t alphabet_size() { + return 32; + } + static CPPCODEC_ALWAYS_INLINE constexpr char symbol( + cppcodec::detail::alphabet_index_t idx) { + return kAlphabet[idx]; + } + static CPPCODEC_ALWAYS_INLINE constexpr bool generates_padding() { + return false; + } + }; + + inline void encode(std::span out, qtils::BytesIn bytes) { + using codec = cppcodec::detail::codec>; + codec::encode(out.data(), out.size(), bytes); + } +} // namespace jam::snp::base32 + +namespace jam::snp { + DnsName::DnsName(const Key &key) { + chars[0] = 'e'; + base32::encode(std::span{chars}.subspan(1), key); + } + + outcome::result DnsName::set(x509_st *x509) const { + // TODO(turuslan): cert.alt = DnsName(key) + return outcome::success(); + } +} // namespace jam::snp diff --git a/src/jam/snp/connections/dns_name.hpp b/src/jam/snp/connections/dns_name.hpp new file mode 100644 index 0000000..53a6814 --- /dev/null +++ b/src/jam/snp/connections/dns_name.hpp @@ -0,0 +1,35 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +struct x509_st; + +namespace jam::snp { + // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L15-L16 + struct DnsName { + explicit DnsName(const Key &key); + + constexpr operator std::string_view() const { + return std::string_view{chars.data(), chars.size()}; + } + + /** + * Set `DnsName` as subject alternative name for certificate. + */ + outcome::result set(x509_st *x509) const; + + static constexpr size_t kSize = 53; + std::array chars; + }; + constexpr auto format_as(const DnsName &v) { + return v.operator std::string_view(); + } +} // namespace jam::snp diff --git a/src/jam/snp/connections/error.hpp b/src/jam/snp/connections/error.hpp new file mode 100644 index 0000000..c225a4a --- /dev/null +++ b/src/jam/snp/connections/error.hpp @@ -0,0 +1,131 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace jam::snp { + enum class OpenSslError { + EVP_PKEY_get_raw_public_key, + EVP_PKEY_new_raw_private_key, + SSL_CTX_set_alpn_protos, + SSL_CTX_use_certificate, + SSL_CTX_use_PrivateKey, + SSL_CTX_set_signing_algorithm_prefs, + SSL_CTX_set_verify_algorithm_prefs, + SSL_get_peer_certificate, + X509_gmtime_adj, + X509_set_pubkey, + X509_get_pubkey, + X509_sign, + }; + Q_ENUM_ERROR_CODE(OpenSslError) { + using E = decltype(e); + switch (e) { + case E::EVP_PKEY_get_raw_public_key: + return "EVP_PKEY_get_raw_public_key"; + case E::EVP_PKEY_new_raw_private_key: + return "EVP_PKEY_new_raw_private_key"; + case E::SSL_CTX_set_alpn_protos: + return "SSL_CTX_set_alpn_protos"; + case E::SSL_CTX_use_certificate: + return "SSL_CTX_use_certificate"; + case E::SSL_CTX_use_PrivateKey: + return "SSL_CTX_use_PrivateKey"; + case E::SSL_CTX_set_signing_algorithm_prefs: + return "SSL_CTX_set_signing_algorithm_prefs"; + case E::SSL_CTX_set_verify_algorithm_prefs: + return "SSL_CTX_set_verify_algorithm_prefs"; + case E::SSL_get_peer_certificate: + return "SSL_get_peer_certificate"; + case E::X509_gmtime_adj: + return "X509_gmtime_adj"; + case E::X509_set_pubkey: + return "X509_set_pubkey"; + case E::X509_get_pubkey: + return "X509_get_pubkey"; + case E::X509_sign: + return "X509_sign"; + } + } + + enum class LsQuicError { + lsquic_conn_make_stream, + lsquic_engine_connect, + lsquic_engine_new, + lsquic_global_init, + }; + Q_ENUM_ERROR_CODE(LsQuicError) { + using E = decltype(e); + switch (e) { + case E::lsquic_conn_make_stream: + return "lsquic_conn_make_stream"; + case E::lsquic_engine_connect: + return "lsquic_engine_connect"; + case E::lsquic_engine_new: + return "lsquic_engine_new"; + case E::lsquic_global_init: + return "lsquic_global_init"; + } + } + + enum class ConnectionsError { + CONNECTION_OPEN_CLOSED, + CONNECTION_OPEN_DUPLICATE, + CONNECTIONS_INIT, + ENGINE_CONNECT_ALREADY, + ENGINE_CONNECT_CLOSED, + ENGINE_CONNECT_KEY_MISMATCH, + ENGINE_OPEN_STREAM_ALREADY, + ENGINE_OPEN_STREAM_TOO_MANY, + HANDSHAKE_FAILED, + PROTOCOL_ID_MAKE_INVALID, + STREAM_READ_CLOSED, + STREAM_READ_DESTROYED, + STREAM_READ_PROTOCOL_ID_CLOSED, + STREAM_READ_TOO_BIG, + STREAM_WRITE_CLOSED, + STREAM_WRITE_DESTROYED, + }; + Q_ENUM_ERROR_CODE(ConnectionsError) { + using E = decltype(e); + switch (e) { + case E::CONNECTION_OPEN_CLOSED: + return "Connection::open closed"; + case E::CONNECTION_OPEN_DUPLICATE: + return "Connection::open duplicate"; + case E::CONNECTIONS_INIT: + return "Connections::init error"; + case E::ENGINE_CONNECT_ALREADY: + return "Engine::connect already"; + case E::ENGINE_CONNECT_CLOSED: + return "Engine::connect closed"; + case E::ENGINE_CONNECT_KEY_MISMATCH: + return "Engine::connect key mismatch"; + case E::ENGINE_OPEN_STREAM_ALREADY: + return "Engine::openStream already"; + case E::ENGINE_OPEN_STREAM_TOO_MANY: + return "Engine::openStream too many streams"; + case E::HANDSHAKE_FAILED: + return "handshake failed"; + case E::PROTOCOL_ID_MAKE_INVALID: + return "ProtocolId::make invalid"; + case E::STREAM_READ_CLOSED: + return "Stream::read closed"; + case E::STREAM_READ_DESTROYED: + return "Stream::read destroyed"; + case E::STREAM_READ_PROTOCOL_ID_CLOSED: + return "Stream::readProtocolId closed"; + case E::STREAM_READ_TOO_BIG: + return "Stream::read too big"; + case E::STREAM_WRITE_CLOSED: + return "Stream::write closed"; + case E::STREAM_WRITE_DESTROYED: + return "Stream::write destroyed"; + } + } +} // namespace jam::snp diff --git a/src/jam/snp/connections/key.hpp b/src/jam/snp/connections/key.hpp new file mode 100644 index 0000000..c801e4e --- /dev/null +++ b/src/jam/snp/connections/key.hpp @@ -0,0 +1,14 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace jam::snp { + // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L13-L14 + using Key = ed25519::Public; +} // namespace jam::snp diff --git a/src/jam/snp/connections/lsquic/controller.hpp b/src/jam/snp/connections/lsquic/controller.hpp new file mode 100644 index 0000000..4c310e4 --- /dev/null +++ b/src/jam/snp/connections/lsquic/controller.hpp @@ -0,0 +1,36 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include + +namespace jam::snp::lsquic { + class EngineController { + public: + virtual ~EngineController() = default; + + /** + * Connection was accepted. + */ + virtual void onConnectionAccept(ConnectionPtr connection) {} + + /** + * Connection was closed. + */ + virtual void onConnectionClose(ConnectionInfo connection_info) {} + + /** + * Stream was accepted. + */ + virtual void onStreamAccept(ConnectionPtr connection, + ProtocolId protocol_id, + StreamPtr stream) {} + }; +} // namespace jam::snp::lsquic diff --git a/src/jam/snp/connections/lsquic/engine.cpp b/src/jam/snp/connections/lsquic/engine.cpp new file mode 100644 index 0000000..7a36880 --- /dev/null +++ b/src/jam/snp/connections/lsquic/engine.cpp @@ -0,0 +1,591 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define SELF_FROM_VOID Engine *self = static_cast(void_self) + +// TODO(turuslan): unique streams +// TODO(turuslan): connection/stream close event lag + +namespace jam::snp::lsquic { + // TODO(turuslan): config + constexpr uint32_t kWindowSize = 64 << 10; + + template + T::Ls *to_ls(T *ptr) { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + return reinterpret_cast(ptr); + } + template + T *from_ls(typename T::Ls *ptr) { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + return reinterpret_cast(ptr); + } + + void tryDelete(auto *ptr) { + if (not ptr->canDelete()) { + return; + } + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + delete ptr; + } + + Socket::endpoint_type make_endpoint(const Address &address) { + auto ip = boost::asio::ip::make_address_v6(address.ip); + return Socket::endpoint_type{ip, address.port}; + } + + outcome::result> Engine::make( + IoContextPtr io_context_ptr, + ConnectionIdCounter connection_id_counter, + TlsCertificate certificate, + std::optional listen_port, + std::weak_ptr controller) { + OUTCOME_TRY(init()); + + uint32_t flags = 0; + if (listen_port) { + flags |= LSENG_SERVER; + } + + lsquic_engine_settings settings{}; + lsquic_engine_init_settings(&settings, flags); + settings.es_init_max_stream_data_bidi_remote = kWindowSize; + settings.es_init_max_stream_data_bidi_local = kWindowSize; + + static lsquic_stream_if stream_if{}; + stream_if.on_new_conn = on_new_conn; + stream_if.on_conn_closed = on_conn_closed; + stream_if.on_hsk_done = on_hsk_done; + stream_if.on_new_stream = on_new_stream; + stream_if.on_close = on_close; + stream_if.on_read = on_read; + stream_if.on_write = on_write; + + lsquic_engine_api api{}; + api.ea_settings = &settings; + + Socket socket{*io_context_ptr}; + boost::system::error_code ec; + socket.open(boost::asio::ip::udp::v6(), ec); + if (ec) { + return ec; + } + socket.non_blocking(true, ec); + if (ec) { + return ec; + } + if (listen_port) { + auto ip = boost::asio::ip::address_v6::any(); + socket.bind({ip, listen_port.value()}, ec); + if (ec) { + return ec; + } + } + auto socket_local_endpoint = socket.local_endpoint(ec); + if (ec) { + return ec; + } + auto self = std::make_shared(Private{}, + io_context_ptr, + std::move(connection_id_counter), + std::move(certificate), + std::move(socket), + socket_local_endpoint, + std::move(controller)); + + api.ea_stream_if = &stream_if; + api.ea_stream_if_ctx = self.get(); + api.ea_packets_out = ea_packets_out; + api.ea_packets_out_ctx = self.get(); + api.ea_get_ssl_ctx = ea_get_ssl_ctx; + + self->engine_ = lsquic_engine_new(flags, &api); + if (self->engine_ == nullptr) { + return LsQuicError::lsquic_engine_new; + } + + io_context_ptr->post([weak_self{std::weak_ptr{self}}] { + WEAK_LOCK(self); + self->readLoop(); + }); + + return self; + } + + Engine::Engine(Private, + IoContextPtr io_context_ptr, + ConnectionIdCounter connection_id_counter, + TlsCertificate &&certificate, + Socket &&socket, + Socket::endpoint_type socket_local_endpoint, + std::weak_ptr controller) + : MOVE_(io_context_ptr), + MOVE_(connection_id_counter), + MOVE_(certificate), + MOVE_(socket), + MOVE_(socket_local_endpoint), + MOVE_(controller), + timer_{*io_context_ptr_} {} + + Engine::~Engine() { + if (engine_ != nullptr) { + boost::asio::dispatch(*io_context_ptr_, [engine{engine_}] { + // will call `Engine::on_conn_closed`, `Engine::on_close`. + lsquic_engine_destroy(engine); + }); + } + } + + ConnectionPtrCoroOutcome Engine::connect(Self self, Address address) { + SET_CORO_THREAD(self->io_context_ptr_); + if (self->connecting_) { + co_return ConnectionsError::ENGINE_CONNECT_ALREADY; + } + co_return co_await coroHandler( + [&](CoroHandler &&handler) { + self->connecting_.emplace(Connecting{ + .address = address, + .handler = std::move(handler), + }); + // will call `Engine::ea_get_ssl_ctx`, `Engine::on_new_conn`. + lsquic_engine_connect(self->engine_, + N_LSQVER, + self->socket_local_endpoint_.data(), + make_endpoint(address).data(), + self.get(), + nullptr, + nullptr, + 0, + nullptr, + 0, + nullptr, + 0); + if (auto connecting = qtils::optionTake(self->connecting_)) { + connecting->handler(LsQuicError::lsquic_engine_connect); + } + self->wantProcess(); + }); + } + + void Engine::wantFlush(StreamCtx *stream_ctx) { + if (stream_ctx->want_flush) { + return; + } + stream_ctx->want_flush = true; + if (not stream_ctx->stream) { + return; + } + want_flush_.emplace_back(stream_ctx->stream.value()); + wantProcess(); + } + + void Engine::wantProcess() { + if (want_process_) { + return; + } + want_process_ = true; + boost::asio::post(*io_context_ptr_, [WEAK_SELF] { + WEAK_LOCK(self); + self->process(); + }); + } + + void Engine::process() { + want_process_ = false; + auto want_flush = std::exchange(want_flush_, {}); + for (auto &weak_stream : want_flush) { + auto stream = weak_stream.lock(); + if (not stream) { + continue; + } + if (not stream->stream_ctx_->ls_stream) { + continue; + } + stream->stream_ctx_->want_flush = false; + lsquic_stream_flush(stream->stream_ctx_->ls_stream.value()); + } + // will call `Engine::on_new_conn`, `Engine::on_conn_closed`, + // `Engine::on_new_stream`, `Engine::on_close`, `Engine::on_read`, + // `Engine::on_write`, `Engine::ea_packets_out`. + lsquic_engine_process_conns(engine_); + int us = 0; + if (not lsquic_engine_earliest_adv_tick(engine_, &us)) { + return; + } + timer_.expires_after(std::chrono::microseconds{us}); + auto cb = [WEAK_SELF](boost::system::error_code ec) { + WEAK_LOCK(self); + if (ec) { + return; + } + self->process(); + }; + timer_.async_wait(std::move(cb)); + } + + void Engine::readLoop() { + // https://github.com/cbodley/nexus/blob/d1d8486f713fd089917331239d755932c7c8ed8e/src/socket.cc#L293 + while (true) { + socklen_t len = socket_local_endpoint_.size(); + auto n = recvfrom(socket_.native_handle(), + reading_.buffer.data(), + reading_.buffer.size(), + 0, + reading_.remote_endpoint.data(), + &len); + if (n == -1) { + if (errno == EAGAIN or errno == EWOULDBLOCK) { + auto cb = [WEAK_SELF](boost::system::error_code ec) { + WEAK_LOCK(self); + if (ec) { + return; + } + self->readLoop(); + }; + socket_.async_wait(boost::asio::socket_base::wait_read, + std::move(cb)); + } + break; + } + // will call `Engine::on_hsk_done`, `Engine::ea_get_ssl_ctx`. + lsquic_engine_packet_in(engine_, + reading_.buffer.data(), + n, + socket_local_endpoint_.data(), + reading_.remote_endpoint.data(), + this, + 0); + } + process(); + } + + void Engine::destroyConnection(ConnCtx *conn_ctx) { + conn_ctx->connection.reset(); + if (conn_ctx->ls_conn) { + lsquic_conn_close(conn_ctx->ls_conn.value()); + } else { + tryDelete(conn_ctx); + } + } + + StreamPtrCoroOutcome Engine::openStream(ConnCtx *conn_ctx, + ProtocolId protocol_id) { + if (not conn_ctx->ls_conn) { + co_return ConnectionsError::CONNECTION_OPEN_CLOSED; + } + if (conn_ctx->open_stream) { + co_return ConnectionsError::ENGINE_OPEN_STREAM_ALREADY; + } + if (lsquic_conn_n_avail_streams(conn_ctx->ls_conn.value()) == 0) { + co_return ConnectionsError::ENGINE_OPEN_STREAM_TOO_MANY; + } + conn_ctx->open_stream = nullptr; + // will call `Engine::on_new_stream`. + lsquic_conn_make_stream(conn_ctx->ls_conn.value()); + auto stream = qtils::optionTake(conn_ctx->open_stream).value(); + if (stream == nullptr) { + co_return LsQuicError::lsquic_conn_make_stream; + } + // stream not weak, because no other owners yet + BOOST_OUTCOME_CO_TRY(co_await stream->writeProtocolId(protocol_id)); + co_return stream; + } + + void Engine::destroyStream(StreamCtx *stream_ctx) { + stream_ctx->stream.reset(); + if (stream_ctx->ls_stream) { + lsquic_stream_close(stream_ctx->ls_stream.value()); + } else { + tryDelete(stream_ctx); + } + } + + void Engine::streamAccept(StreamPtr &&stream) { + coroSpawn(*io_context_ptr_, + [weak_controller{controller_}, + MOVE(stream)]() mutable -> CoroOutcome { + // stream not weak, because no other owners yet + BOOST_OUTCOME_CO_TRY(auto protocol_id, + co_await stream->readProtocolId()); + if (auto controller = weak_controller.lock()) { + auto &connection = stream->connection_; + controller->onStreamAccept( + connection, protocol_id, std::move(stream)); + } + co_return outcome::success(); + }); + } + + void Engine::streamReadFin(StreamCtx *stream_ctx) { + if (not stream_ctx->ls_stream) { + return; + } + lsquic_stream_shutdown(stream_ctx->ls_stream.value(), SHUT_RD); + } + + void Engine::streamWriteFin(StreamCtx *stream_ctx) { + if (not stream_ctx->ls_stream) { + return; + } + lsquic_stream_shutdown(stream_ctx->ls_stream.value(), SHUT_WR); + } + + CoroOutcome Engine::streamReadRaw(StreamCtx *stream_ctx, + qtils::BytesOut message) { + if (stream_ctx->reading) { + throw std::logic_error{"Engine::streamReadRaw duplicate"}; + } + auto remaining = message; + while (not remaining.empty()) { + if (not stream_ctx->ls_stream) { + co_return ConnectionsError::STREAM_READ_CLOSED; + } + auto n = lsquic_stream_read( + stream_ctx->ls_stream.value(), remaining.data(), remaining.size()); + if (n == 0) { + if (remaining.size() == message.size()) { + co_return false; + } else { + co_return ConnectionsError::STREAM_READ_CLOSED; + } + } + if (n == -1) { + if (errno != EWOULDBLOCK) { + co_return ConnectionsError::STREAM_READ_CLOSED; + } + co_await coroHandler([&](CoroHandler &&handler) { + stream_ctx->reading.emplace(std::move(handler)); + lsquic_stream_wantread(stream_ctx->ls_stream.value(), 1); + }); + continue; + } + remaining = remaining.subspan(n); + } + co_return true; + } + + CoroOutcome Engine::streamWriteRaw(StreamCtx *stream_ctx, + qtils::BytesIn message) { + if (stream_ctx->writing) { + throw std::logic_error{"Engine::streamWriteRaw duplicate"}; + } + auto remaining = message; + while (not remaining.empty()) { + if (not stream_ctx->ls_stream) { + co_return ConnectionsError::STREAM_WRITE_CLOSED; + } + auto n = lsquic_stream_write( + stream_ctx->ls_stream.value(), remaining.data(), remaining.size()); + if (n < 0) { + co_return ConnectionsError::STREAM_WRITE_CLOSED; + } + if (n != 0) { + remaining = remaining.subspan(n); + auto self = stream_ctx->engine.lock(); + if (not self) { + co_return ConnectionsError::STREAM_WRITE_CLOSED; + } + self->wantFlush(stream_ctx); + } + if (remaining.empty()) { + break; + } + co_await coroHandler([&](CoroHandler &&handler) { + stream_ctx->writing.emplace(std::move(handler)); + lsquic_stream_wantwrite(stream_ctx->ls_stream.value(), 1); + }); + } + co_return outcome::success(); + } + + lsquic_conn_ctx_t *Engine::on_new_conn(void *void_self, + lsquic_conn_t *ls_conn) { + SELF_FROM_VOID; + auto connecting = qtils::optionTake(self->connecting_); + auto is_connecting = connecting.has_value(); + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + auto *conn_ctx = new ConnCtx{ + .engine = self->weak_from_this(), + .ls_conn = ls_conn, + .connecting = std::move(connecting), + }; + auto *ls_conn_ctx = to_ls(conn_ctx); + lsquic_conn_set_ctx(ls_conn, ls_conn_ctx); + if (not is_connecting) { + // lsquic doesn't call `on_hsk_done` for incoming connection + on_hsk_done(ls_conn, LSQ_HSK_OK); + } + return ls_conn_ctx; + } + + void Engine::on_conn_closed(lsquic_conn_t *ls_conn) { + auto *conn_ctx = from_ls(lsquic_conn_get_ctx(ls_conn)); + conn_ctx->ls_conn.reset(); + lsquic_conn_set_ctx(ls_conn, nullptr); + if (auto connecting = qtils::optionTake(conn_ctx->connecting)) { + connecting->handler(ConnectionsError::ENGINE_CONNECT_CLOSED); + } else if (auto self = conn_ctx->engine.lock()) { + if (auto controller = self->controller_.lock()) { + controller->onConnectionClose(conn_ctx->info.value()); + } + } + tryDelete(conn_ctx); + } + + void Engine::on_hsk_done(lsquic_conn_t *ls_conn, lsquic_hsk_status status) { + auto *conn_ctx = from_ls(lsquic_conn_get_ctx(ls_conn)); + auto self = conn_ctx->engine.lock(); + if (not self) { + return; + } + auto ok = status == LSQ_HSK_OK or status == LSQ_HSK_RESUMED_OK; + auto connecting = qtils::optionTake(conn_ctx->connecting); + auto connection_result = [&]() -> ConnectionPtrOutcome { + if (not ok) { + return ConnectionsError::HANDSHAKE_FAILED; + } + OUTCOME_TRY(key, TlsCertificate::get_key(lsquic_conn_ssl(ls_conn))); + if (connecting and key != connecting->address.key) { + return ConnectionsError::ENGINE_CONNECT_KEY_MISMATCH; + } + conn_ctx->info = ConnectionInfo{ + .id = self->connection_id_counter_.make(), + .key = key, + }; + auto connection = std::make_shared( + self->io_context_ptr_, conn_ctx, conn_ctx->info.value()); + conn_ctx->connection = connection; + return connection; + }(); + if (not connection_result) { + lsquic_conn_close(ls_conn); + } + if (connecting) { + connecting->handler(std::move(connection_result)); + } else if (connection_result) { + auto &connection = connection_result.value(); + if (auto controller = self->controller_.lock()) { + controller->onConnectionAccept(std::move(connection)); + } + } + } + + lsquic_stream_ctx_t *Engine::on_new_stream(void *void_self, + lsquic_stream_t *ls_stream) { + SELF_FROM_VOID; + auto *conn_ctx = + from_ls(lsquic_conn_get_ctx(lsquic_stream_conn(ls_stream))); + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + auto *stream_ctx = new StreamCtx{ + .engine = self->weak_from_this(), + .ls_stream = ls_stream, + }; + ConnectionPtr connection; + if (conn_ctx->connection) { + connection = conn_ctx->connection->lock(); + } + if (connection) { + auto stream = std::make_shared( + self->io_context_ptr_, connection, stream_ctx); + stream_ctx->stream = stream; + if (conn_ctx->open_stream) { + conn_ctx->open_stream.value() = stream; + } else { + self->streamAccept(std::move(stream)); + } + } else { + lsquic_stream_close(ls_stream); + } + return to_ls(stream_ctx); + } + + void Engine::on_close(lsquic_stream_t *ls_stream, + lsquic_stream_ctx_t *ls_stream_ctx) { + auto *stream_ctx = from_ls(ls_stream_ctx); + stream_ctx->ls_stream.reset(); + if (auto reading = qtils::optionTake(stream_ctx->reading)) { + reading.value()(); + } + if (auto writing = qtils::optionTake(stream_ctx->writing)) { + writing.value()(); + } + tryDelete(stream_ctx); + } + + void Engine::on_read(lsquic_stream_t *ls_stream, + lsquic_stream_ctx_t *ls_stream_ctx) { + lsquic_stream_wantread(ls_stream, 0); + auto *stream_ctx = from_ls(ls_stream_ctx); + if (auto reading = qtils::optionTake(stream_ctx->reading)) { + reading.value()(); + } + } + + void Engine::on_write(lsquic_stream_t *ls_stream, + lsquic_stream_ctx_t *ls_stream_ctx) { + lsquic_stream_wantwrite(ls_stream, 0); + auto *stream_ctx = from_ls(ls_stream_ctx); + if (auto writing = qtils::optionTake(stream_ctx->writing)) { + writing.value()(); + } + } + + ssl_ctx_st *Engine::ea_get_ssl_ctx(void *void_self, const sockaddr *) { + SELF_FROM_VOID; + return self->certificate_; + } + + int Engine::ea_packets_out(void *void_self, + const lsquic_out_spec *out_spec, + unsigned n_packets_out) { + SELF_FROM_VOID; + // https://github.com/cbodley/nexus/blob/d1d8486f713fd089917331239d755932c7c8ed8e/src/socket.cc#L218 + int r = 0; + for (auto &spec : std::span{out_spec, n_packets_out}) { + msghdr msg{}; + msg.msg_iov = spec.iov; + msg.msg_iovlen = spec.iovlen; + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) + msg.msg_name = const_cast(spec.dest_sa); + msg.msg_namelen = spec.dest_sa->sa_family == AF_INET + ? sizeof(sockaddr_in) + : sizeof(sockaddr_in6); + auto n = sendmsg(self->socket_.native_handle(), &msg, 0); + if (n == -1) { + if (errno == EAGAIN or errno == EWOULDBLOCK) { + auto cb = [weak_self{self->weak_from_this()}]( + boost::system::error_code ec) { + WEAK_LOCK(self); + if (ec) { + return; + } + // will call `Engine::ea_packets_out`. + lsquic_engine_send_unsent_packets(self->engine_); + }; + self->socket_.async_wait(Socket::wait_write, std::move(cb)); + } + break; + } + ++r; + } + return r; + } +} // namespace jam::snp::lsquic diff --git a/src/jam/snp/connections/lsquic/engine.hpp b/src/jam/snp/connections/lsquic/engine.hpp new file mode 100644 index 0000000..f233ed7 --- /dev/null +++ b/src/jam/snp/connections/lsquic/engine.hpp @@ -0,0 +1,195 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct sockaddr; + +namespace jam::snp { + class ConnectionsConfig; +} // namespace jam::snp + +namespace jam::snp::lsquic { + class Engine; + class EngineController; +} // namespace jam::snp::lsquic + +namespace jam::snp::lsquic { + using Socket = boost::asio::ip::udp::socket; + Socket::endpoint_type make_endpoint(const Address &address); + + /** + * Captures `Engine::connect` arguments. + */ + struct Connecting { + Address address; + CoroHandler handler; + }; + + /** + * `lsquic_conn_ctx_t`. + */ + struct ConnCtx { + using Ls = lsquic_conn_ctx_t; + + std::weak_ptr engine; + std::optional ls_conn; + std::optional> connection; + std::optional connecting; + std::optional info; + std::optional open_stream; + + bool canDelete() const { + return not ls_conn and not connection; + } + }; + + /** + * `lsquic_stream_ctx_t`. + */ + struct StreamCtx { + using Ls = lsquic_stream_ctx_t; + + std::weak_ptr engine; + std::optional ls_stream; + std::optional> stream; + std::optional> reading; + std::optional> writing; + bool want_flush = false; + + bool canDelete() const { + return not ls_stream and not stream; + } + }; + + class Engine : public std::enable_shared_from_this { + friend Connection; + friend Stream; + + struct Private {}; + + public: + using Self = std::shared_ptr; + + static outcome::result> make( + IoContextPtr io_context_ptr, + ConnectionIdCounter connection_id_counter, + TlsCertificate certificate, + std::optional listen_port, + std::weak_ptr controller); + Engine(Private, + IoContextPtr io_context_ptr, + ConnectionIdCounter connection_id_counter, + TlsCertificate &&certificate, + Socket &&socket, + Socket::endpoint_type socket_local_endpoint, + std::weak_ptr controller); + ~Engine(); + + static ConnectionPtrCoroOutcome connect(Self self, Address address); + + private: + struct Reading { + static constexpr size_t kMaxUdpPacketSize = 64 << 10; + qtils::BytesN buffer; + boost::asio::ip::udp::endpoint remote_endpoint; + }; + + void wantFlush(StreamCtx *stream_ctx); + void wantProcess(); + void process(); + void readLoop(); + static void destroyConnection(ConnCtx *conn_ctx); + static StreamPtrCoroOutcome openStream(ConnCtx *conn_ctx, + ProtocolId protocol_id); + static void destroyStream(StreamCtx *stream_ctx); + void streamAccept(StreamPtr &&stream); + static void streamReadFin(StreamCtx *stream_ctx); + static void streamWriteFin(StreamCtx *stream_ctx); + static CoroOutcome streamReadRaw(StreamCtx *stream_ctx, + qtils::BytesOut message); + static CoroOutcome streamWriteRaw(StreamCtx *stream_ctx, + qtils::BytesIn message); + + /** + * Called from `lsquic_engine_connect` (client), + * `lsquic_engine_process_conns` (server). + */ + static lsquic_conn_ctx_t *on_new_conn(void *void_self, + lsquic_conn_t *ls_conn); + /** + * Called from `lsquic_engine_process_conns`, `lsquic_engine_destroy`. + */ + static void on_conn_closed(lsquic_conn_t *ls_conn); + /** + * Called from `lsquic_engine_packet_in` (client), + * `on_new_conn` (server). + */ + static void on_hsk_done(lsquic_conn_t *ls_conn, lsquic_hsk_status status); + /** + * Called from `lsquic_conn_make_stream` (client), + * `lsquic_engine_process_conns` (server). + */ + static lsquic_stream_ctx_t *on_new_stream(void *void_self, + lsquic_stream_t *ls_stream); + /** + * Called from `lsquic_engine_process_conns`, `lsquic_engine_destroy`. + */ + static void on_close(lsquic_stream_t *ls_stream, + lsquic_stream_ctx_t *ls_stream_ctx); + /** + * Called from `lsquic_engine_process_conns`. + * `lsquic_stream_flush` doesn't work inside `on_read`. + */ + static void on_read(lsquic_stream_t *ls_stream, + lsquic_stream_ctx_t *ls_stream_ctx); + /** + * Called from `lsquic_engine_process_conns`. + */ + static void on_write(lsquic_stream_t *ls_stream, + lsquic_stream_ctx_t *ls_stream_ctx); + /** + * Called from `lsquic_engine_connect` (client), + * `lsquic_engine_packet_in` (server). + */ + static ssl_ctx_st *ea_get_ssl_ctx(void *void_self, const sockaddr *); + /** + * Called from `lsquic_engine_process_conns`, + * `lsquic_engine_send_unsent_packets`. + */ + static int ea_packets_out(void *void_self, + const lsquic_out_spec *out_spec, + unsigned n_packets_out); + + IoContextPtr io_context_ptr_; + ConnectionIdCounter connection_id_counter_; + TlsCertificate certificate_; + Socket socket_; + Socket::endpoint_type socket_local_endpoint_; + std::weak_ptr controller_; + boost::asio::steady_timer timer_; + lsquic_engine_t *engine_ = nullptr; + Reading reading_; + std::optional connecting_; + std::deque> want_flush_; + bool want_process_ = false; + }; +} // namespace jam::snp::lsquic diff --git a/src/jam/snp/connections/lsquic/init.cpp b/src/jam/snp/connections/lsquic/init.cpp new file mode 100644 index 0000000..9392c7d --- /dev/null +++ b/src/jam/snp/connections/lsquic/init.cpp @@ -0,0 +1,23 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include + +namespace jam::snp::lsquic { + outcome::result init() { + static auto ok = [] { + return lsquic_global_init(LSQUIC_GLOBAL_CLIENT | LSQUIC_GLOBAL_SERVER) + == 0; + }(); + if (not ok) { + return LsQuicError::lsquic_global_init; + } + return outcome::success(); + } +} // namespace jam::snp::lsquic diff --git a/src/jam/snp/connections/lsquic/init.hpp b/src/jam/snp/connections/lsquic/init.hpp new file mode 100644 index 0000000..b51dc58 --- /dev/null +++ b/src/jam/snp/connections/lsquic/init.hpp @@ -0,0 +1,13 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace jam::snp::lsquic { + outcome::result init(); +} // namespace jam::snp::lsquic diff --git a/src/jam/snp/connections/lsquic/log.hpp b/src/jam/snp/connections/lsquic/log.hpp new file mode 100644 index 0000000..c27c8ea --- /dev/null +++ b/src/jam/snp/connections/lsquic/log.hpp @@ -0,0 +1,22 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +namespace jam::snp::lsquic { + inline void log() { + static lsquic_logger_if log{ + +[](void *, const char *buf, size_t len) { + return (int)fwrite(buf, sizeof(char), len, stdout); + }, + }; + lsquic_logger_init(&log, nullptr, LLTS_HHMMSSMS); + lsquic_set_log_level("debug"); + } +} // namespace jam::snp::lsquic diff --git a/src/jam/snp/connections/message_size.hpp b/src/jam/snp/connections/message_size.hpp new file mode 100644 index 0000000..34837e2 --- /dev/null +++ b/src/jam/snp/connections/message_size.hpp @@ -0,0 +1,15 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace jam::snp { + // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L109-L111 + using MessageSize = uint32_t; + constexpr MessageSize kMessageSizeMax = UINT32_MAX; +} // namespace jam::snp diff --git a/src/jam/snp/connections/port.hpp b/src/jam/snp/connections/port.hpp new file mode 100644 index 0000000..8f93034 --- /dev/null +++ b/src/jam/snp/connections/port.hpp @@ -0,0 +1,13 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace jam::snp { + using Port = uint16_t; +} // namespace jam::snp diff --git a/src/jam/snp/connections/prefer_key.cpp b/src/jam/snp/connections/prefer_key.cpp new file mode 100644 index 0000000..e19a753 --- /dev/null +++ b/src/jam/snp/connections/prefer_key.cpp @@ -0,0 +1,13 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +namespace jam::snp { + bool prefer_key(const Key &a, const Key &b) { + return ((a[31] > 127) != (b[31] > 127)) != (a < b); + } +} // namespace jam::snp diff --git a/src/jam/snp/connections/prefer_key.hpp b/src/jam/snp/connections/prefer_key.hpp new file mode 100644 index 0000000..71f4e65 --- /dev/null +++ b/src/jam/snp/connections/prefer_key.hpp @@ -0,0 +1,17 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace jam::snp { + // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L52-L62 + /** + * Is first key preferred over second. + */ + bool prefer_key(const Key &a, const Key &b); +} // namespace jam::snp diff --git a/src/jam/snp/connections/protocol_id.cpp b/src/jam/snp/connections/protocol_id.cpp new file mode 100644 index 0000000..c8d331a --- /dev/null +++ b/src/jam/snp/connections/protocol_id.cpp @@ -0,0 +1,19 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include + +namespace jam::snp { + outcome::result ProtocolId::make(Id id, bool unique) { + ProtocolId protocol_id{id}; + if (unique != protocol_id.unique()) { + return ConnectionsError::PROTOCOL_ID_MAKE_INVALID; + } + return protocol_id; + } +} // namespace jam::snp diff --git a/src/jam/snp/connections/protocol_id.hpp b/src/jam/snp/connections/protocol_id.hpp new file mode 100644 index 0000000..e2ea148 --- /dev/null +++ b/src/jam/snp/connections/protocol_id.hpp @@ -0,0 +1,53 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +namespace jam::snp { + class Stream; +} // namespace jam::snp + +namespace jam::snp { + // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L87-L101 + class ProtocolId { + friend Stream; + + using Id = uint8_t; + + ProtocolId(Id id) : id_{id} {} + + public: + /** + * Construct protocol with specified `id`. + * Check expected `unique` consistency with `id` range. + */ + static outcome::result make(Id id, bool unique); + + auto &id() const { + return id_; + } + + bool unique() const { + return id() < 128; + } + + auto operator<=>(const ProtocolId &) const = default; + + private: + Id id_; + }; +} // namespace jam::snp + +template <> +struct std::hash { + size_t operator()(const jam::snp::ProtocolId &v) const { + return qtils::stdHashOf(v.id()); + } +}; diff --git a/src/jam/snp/connections/stream.cpp b/src/jam/snp/connections/stream.cpp new file mode 100644 index 0000000..c88403a --- /dev/null +++ b/src/jam/snp/connections/stream.cpp @@ -0,0 +1,109 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace jam::snp { + using lsquic::Engine; + + using ProtocolIdBytes = qtils::BytesN<1>; + using MessageSizeBytes = qtils::BytesN<4>; + + Stream::Stream(IoContextPtr io_context_ptr, + ConnectionPtr connection, + lsquic::StreamCtx *stream_ctx) + : MOVE_(io_context_ptr), MOVE_(connection), MOVE_(stream_ctx) {} + + Stream::~Stream() { + boost::asio::dispatch(*io_context_ptr_, [stream_ctx{stream_ctx_}] { + Engine::destroyStream(stream_ctx); + }); + } + + CoroOutcome Stream::read(Self self, + qtils::Bytes &buffer, + MessageSize max) { + SET_CORO_THREAD(self->io_context_ptr_); + MessageSizeBytes size_bytes; + BOOST_OUTCOME_CO_TRY( + auto read_size, + CORO_WEAK_AWAIT(self, + Engine::streamReadRaw(self->stream_ctx_, size_bytes), + ConnectionsError::STREAM_READ_DESTROYED)); + if (not read_size) { + co_return false; + } + auto size = boost::endian::load_little_u32(size_bytes.data()); + if (size > max) { + co_return ConnectionsError::STREAM_READ_TOO_BIG; + } + buffer.resize(size); + BOOST_OUTCOME_CO_TRY( + auto read_message, + CORO_WEAK_AWAIT(self, + Engine::streamReadRaw(self->stream_ctx_, buffer), + ConnectionsError::STREAM_READ_DESTROYED)); + if (not read_message) { + co_return ConnectionsError::STREAM_READ_CLOSED; + } + co_return true; + } + + Coro Stream::readFin(Self self) { + SET_CORO_THREAD(self->io_context_ptr_); + Engine::streamReadFin(self->stream_ctx_); + co_return; + } + + CoroOutcome Stream::write(Self self, qtils::BytesIn message) { + SET_CORO_THREAD(self->io_context_ptr_); + MessageSizeBytes size_bytes; + auto size = message.size(); + if (size > kMessageSizeMax) { + throw std::logic_error{"Stream::write max"}; + } + boost::endian::store_little_u32(size_bytes.data(), size); + BOOST_OUTCOME_CO_TRY( + CORO_WEAK_AWAIT(self, + Engine::streamWriteRaw(self->stream_ctx_, size_bytes), + ConnectionsError::STREAM_WRITE_DESTROYED)); + BOOST_OUTCOME_CO_TRY( + CORO_WEAK_AWAIT(self, + Engine::streamWriteRaw(self->stream_ctx_, message), + ConnectionsError::STREAM_WRITE_DESTROYED)); + co_return outcome::success(); + } + + Coro Stream::writeFin(Self self) { + SET_CORO_THREAD(self->io_context_ptr_); + Engine::streamWriteFin(self->stream_ctx_); + co_return; + } + + CoroOutcome Stream::readProtocolId() { + ProtocolIdBytes bytes; + BOOST_OUTCOME_CO_TRY(auto read, + co_await Engine::streamReadRaw(stream_ctx_, bytes)); + if (not read) { + co_return ConnectionsError::STREAM_READ_PROTOCOL_ID_CLOSED; + } + co_return ProtocolId{bytes[0]}; + } + + CoroOutcome Stream::writeProtocolId(ProtocolId protocol_id) { + ProtocolIdBytes bytes{protocol_id.id()}; + co_return co_await Engine::streamWriteRaw(stream_ctx_, bytes); + } +} // namespace jam::snp diff --git a/src/jam/snp/connections/stream.hpp b/src/jam/snp/connections/stream.hpp new file mode 100644 index 0000000..cdd1012 --- /dev/null +++ b/src/jam/snp/connections/stream.hpp @@ -0,0 +1,83 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace jam::snp::lsquic { + struct StreamCtx; + class Engine; +} // namespace jam::snp::lsquic + +namespace jam::snp { + class Stream { + friend lsquic::Engine; + + public: + using Self = std::shared_ptr; + + Stream(IoContextPtr io_context_ptr, + ConnectionPtr connection, + lsquic::StreamCtx *stream_ctx); + /** + * Will close stream and decrement `Connection` shared use count. + */ + ~Stream(); + + // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L109-L111 + /** + * Read whole size prefixed message, no more than `max` bytes. + * Returns `true` if message was read, or `false` if fin was received or + * stream was closed. + */ + static CoroOutcome read(Self self, + qtils::Bytes &buffer, + MessageSize max); + + /** + * Close reading side of stream. + */ + static Coro readFin(Self self); + + // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L109-L111 + /** + * Write while size prefixed message. + */ + static CoroOutcome write(Self self, qtils::BytesIn message); + + /** + * Write fin. + * Closes writing side of stream. + */ + static Coro writeFin(Self self); + + private: + /** + * Read protocol id (server). + */ + CoroOutcome readProtocolId(); + /** + * Write protocol id (client). + */ + CoroOutcome writeProtocolId(ProtocolId protocol_id); + + /** + * `Stream`, `Engine` operations executed on one `IoContextPtr` thread. + */ + IoContextPtr io_context_ptr_; + /** + * `Stream` keeps `Connection` shared use count alive. + */ + ConnectionPtr connection_; + lsquic::StreamCtx *stream_ctx_; + }; +} // namespace jam::snp diff --git a/src/jam/snp/connections/stream_ptr.hpp b/src/jam/snp/connections/stream_ptr.hpp new file mode 100644 index 0000000..0973863 --- /dev/null +++ b/src/jam/snp/connections/stream_ptr.hpp @@ -0,0 +1,19 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +namespace jam::snp { + class Stream; +} // namespace jam::snp + +namespace jam::snp { + using StreamPtr = std::shared_ptr; + using StreamPtrCoroOutcome = CoroOutcome; +} // namespace jam::snp diff --git a/src/jam/snp/connections/tls_certificate.cpp b/src/jam/snp/connections/tls_certificate.cpp new file mode 100644 index 0000000..3503206 --- /dev/null +++ b/src/jam/snp/connections/tls_certificate.cpp @@ -0,0 +1,103 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace jam::snp { + outcome::result set_relative_time(ASN1_TIME *o_time, auto delta) { + if (not X509_gmtime_adj( + o_time, + std::chrono::duration_cast(delta).count())) { + return OpenSslError::X509_gmtime_adj; + } + return outcome::success(); + } + + TlsCertificate::TlsCertificate(const ConnectionsConfig &config) + : MAKE_SHARED_(alpn_, config.genesis), + MAKE_SHARED_(context_, Context::tlsv13) {} + + outcome::result TlsCertificate::make( + const ConnectionsConfig &config) { + TlsCertificate self{config}; + OUTCOME_TRY(self.alpn_->set(self)); + self.context_->set_verify_mode(Context::verify_peer + | Context::verify_fail_if_no_peer_cert + | Context::verify_client_once); + self.context_->set_verify_callback(verify); + std::array prefs{SSL_SIGN_ED25519}; + if (not SSL_CTX_set_signing_algorithm_prefs( + self, prefs.data(), prefs.size())) { + return OpenSslError::SSL_CTX_set_signing_algorithm_prefs; + } + if (not SSL_CTX_set_verify_algorithm_prefs( + self, prefs.data(), prefs.size())) { + return OpenSslError::SSL_CTX_set_verify_algorithm_prefs; + } + auto secret = ed25519::get_secret(config.keypair); + // `EVP_PKEY_new_raw_private_key` requires seed, but in ed25519 secret=seed + bssl::UniquePtr pkey(EVP_PKEY_new_raw_private_key( + EVP_PKEY_ED25519, nullptr, secret.data(), secret.size())); + if (not pkey) { + return OpenSslError::EVP_PKEY_new_raw_private_key; + } + if (not SSL_CTX_use_PrivateKey(self, pkey.get())) { + return OpenSslError::SSL_CTX_use_PrivateKey; + } + + bssl::UniquePtr x509(X509_new()); + OUTCOME_TRY(set_relative_time(X509_getm_notBefore(x509.get()), + -std::chrono::days{1})); + OUTCOME_TRY(set_relative_time(X509_getm_notAfter(x509.get()), + std::chrono::years{1})); + if (not X509_set_pubkey(x509.get(), pkey.get())) { + return OpenSslError::X509_set_pubkey; + } + OUTCOME_TRY(DnsName{ed25519::get_public(config.keypair)}.set(x509.get())); + if (not X509_sign(x509.get(), pkey.get(), nullptr)) { + return OpenSslError::X509_sign; + } + if (not SSL_CTX_use_certificate(self, x509.get())) { + return OpenSslError::SSL_CTX_use_certificate; + } + return self; + } + + TlsCertificate::operator ssl_ctx_st *() const { + return context_->native_handle(); + } + + outcome::result TlsCertificate::get_key(ssl_st *ssl) { + bssl::UniquePtr x509(SSL_get_peer_certificate(ssl)); + if (not x509) { + return OpenSslError::SSL_get_peer_certificate; + } + bssl::UniquePtr pkey(X509_get_pubkey(x509.get())); + if (not pkey) { + return OpenSslError::X509_get_pubkey; + } + Key key; + size_t key_size = key.size(); + if (not EVP_PKEY_get_raw_public_key(pkey.get(), key.data(), &key_size)) { + return OpenSslError::EVP_PKEY_get_raw_public_key; + } + return key; + } + + bool TlsCertificate::verify(bool, boost::asio::ssl::verify_context &ctx) { + X509_STORE_CTX *store_ctx = ctx.native_handle(); + // TODO(turuslan): DnsName(key) == cert.alt + return true; + } +} // namespace jam::snp diff --git a/src/jam/snp/connections/tls_certificate.hpp b/src/jam/snp/connections/tls_certificate.hpp new file mode 100644 index 0000000..1e619fd --- /dev/null +++ b/src/jam/snp/connections/tls_certificate.hpp @@ -0,0 +1,59 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +struct ssl_st; + +namespace boost::asio::ssl { + class context; + class verify_context; +} // namespace boost::asio::ssl + +namespace jam::snp { + struct ConnectionsConfig; + class Alpn; +} // namespace jam::snp + +struct ssl_ctx_st; + +namespace jam::snp { + class TlsCertificate { + TlsCertificate(const ConnectionsConfig &config); + + public: + /** + * Generate self-signed tls certificate. + */ + static outcome::result make( + const ConnectionsConfig &config); + + /** + * Allows passing `*this` to openssl functions. + */ + operator ssl_ctx_st *() const; + + /** + * Get peer key from tls certificate. + */ + static outcome::result get_key(ssl_st *ssl); + + private: + using Context = boost::asio::ssl::context; + + static bool verify(bool, boost::asio::ssl::verify_context &ctx); + + /** + * Keeps `Alpn` alive for `SSL_CTX_set_alpn_select_cb`. + */ + std::shared_ptr alpn_; + std::shared_ptr context_; + }; +} // namespace jam::snp diff --git a/src/jam/snp/example_chat.cpp b/src/jam/snp/example_chat.cpp new file mode 100644 index 0000000..8058b89 --- /dev/null +++ b/src/jam/snp/example_chat.cpp @@ -0,0 +1,240 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using jam::Coro; +using jam::coroHandler; +using jam::CoroHandler; +using jam::CoroOutcome; +using jam::coroSpawn; +using jam::GenesisHash; +using jam::IoContextPtr; +using jam::ed25519::KeyPair; +using jam::snp::Address; +using jam::snp::ConnectionInfo; +using jam::snp::Connections; +using jam::snp::ConnectionsConfig; +using jam::snp::ConnectionsController; +using jam::snp::Key; +using jam::snp::Port; +using jam::snp::ProtocolId; +using jam::snp::StreamPtr; + +inline auto operator""_ed25519(const char *c, size_t s) { + auto seed = qtils::unhex({c, s}).value(); + return jam::ed25519::from_seed(seed); +} + +std::vector keys{ + "f8dfdb0f1103d9fb2905204ac32529d5f148761c4321b2865b0a40e15be75f57"_ed25519, + "96c891b8726cb18c781aefc082dbafcb827e16c8f18f22d461e83eabd618e780"_ed25519, + "619d5e68139f714ee8e7892ce5afd8fbe7a4172a675fea5c5a06fb94fe3d797d"_ed25519, + "8d0c5f498a763eaa8c04861cac06289784140b4bbfa814fef898f1f4095de4a3"_ed25519, +}; +Address server_address{ + Address::kLocal, + 10000, + jam::ed25519::get_public(keys[0]), +}; +ProtocolId protocol_id = ProtocolId::make(0, true).value(); + +size_t indexOfKey(const Key &key) { + auto it = std::ranges::find_if(keys, [&](const KeyPair &keypair) { + return jam::ed25519::get_public(keypair) == key; + }); + if (it == keys.end()) { + throw std::logic_error{"TODO: example"}; + } + return it - keys.begin(); +} + +struct ChatController : ConnectionsController { + static constexpr size_t kMaxMsg = 8; + + struct Writer { + StreamPtr stream; + std::deque queue; + bool writing = false; + }; + using WriterPtr = std::shared_ptr; + + std::map writers; + + static CoroOutcome write(WriterPtr writer, + size_t i_msg, + const std::string msg) { + qtils::Bytes buffer; + buffer.emplace_back(i_msg); + qtils::append(buffer, qtils::str2byte(msg)); + writer->queue.emplace_back(buffer); + if (writer->writing) { + co_return outcome::success(); + } + writer->writing = true; + while (not writer->queue.empty()) { + auto buffer = writer->queue.front(); + writer->queue.pop_front(); + BOOST_OUTCOME_CO_TRY( + co_await writer->stream->write(writer->stream, buffer)); + } + writer->writing = false; + co_return outcome::success(); + } + + void onOpen(Key key) override { + fmt::println("#{} (connected)", indexOfKey(key)); + } + + void onClose(Key key) override { + fmt::println("#{} (disconnected)", indexOfKey(key)); + } + + void print(size_t i_msg, std::string msg) { + fmt::println("#{} > {}", i_msg, msg); + } + + Coro broadcast(std::optional i_read, + size_t i_msg, + std::string msg) { + for (auto &[i_write, writer] : writers) { + if (i_write == i_read) { + continue; + } + co_await coroSpawn([this, i_write, writer, i_msg, msg]() -> Coro { + if (not co_await write(writer, i_msg, msg)) { + writers.erase(i_write); + } + }); + } + } + + Coro onRead(size_t i_read, size_t i_msg, std::string msg) { + print(i_msg, msg); + co_await broadcast(i_read, i_msg, msg); + } + + CoroOutcome add(ConnectionInfo info, StreamPtr stream) { + auto i_read = indexOfKey(info.key); + writers.emplace(i_read, std::make_shared(Writer{stream})); + qtils::Bytes buffer; + while (true) { + BOOST_OUTCOME_CO_TRY(auto read, + co_await stream->read(stream, buffer, 1 + kMaxMsg)); + if (not read) { + break; + } + if (buffer.size() < 1) { + break; + } + auto i_msg = buffer[0]; + co_await onRead( + i_read, i_msg, std::string{qtils::byte2str(buffer).substr(1)}); + } + co_await stream->readFin(stream); + co_return outcome::success(); + } +}; + +struct Input { + Input(IoContextPtr io_context_ptr) : fd_{*io_context_ptr, STDIN_FILENO} {} + + Coro> read() { + auto [ec, n] = co_await boost::asio::async_read_until( + fd_, buf_, "\n", boost::asio::as_tuple(boost::asio::use_awaitable)); + if (ec) { + co_return std::nullopt; + } + auto s = qtils::byte2str(qtils::asioBuffer(buf_.data())); + auto i = s.find("\n"); + if (i != s.npos) { + s = s.substr(0, i); + } + auto r = std::string{s}; + buf_.consume(buf_.size()); + co_return r; + } + + boost::asio::posix::stream_descriptor fd_; + boost::asio::streambuf buf_; +}; + +CoroOutcome co_main(IoContextPtr io_context_ptr, size_t arg_i) { + fmt::println("#{} (self)", arg_i); + + std::optional listen_port; + GenesisHash genesis; + ConnectionsConfig config{genesis, keys.at(arg_i)}; + auto is_server = arg_i == 0; + if (is_server) { + config.listen_port = server_address.port; + } + auto connections = std::make_shared(io_context_ptr, config); + auto chat = std::make_shared(); + BOOST_OUTCOME_CO_TRY(co_await connections->init(connections, chat)); + co_await coroSpawn([io_context_ptr, arg_i, chat]() -> Coro { + Input input{io_context_ptr}; + while (true) { + auto msg = co_await input.read(); + if (not msg) { + break; + } + msg->resize(std::min(msg->size(), ChatController::kMaxMsg)); + if (msg->empty()) { + continue; + } + co_await chat->broadcast(std::nullopt, arg_i, *msg); + } + io_context_ptr->stop(); + }); + if (not is_server) { + BOOST_OUTCOME_CO_TRY( + auto connection, + co_await connections->connect(connections, server_address)); + BOOST_OUTCOME_CO_TRY(auto stream, + co_await connection->open(connection, protocol_id)); + std::ignore = co_await chat->add(connection->info(), stream); + fmt::println("(disconnected)"); + io_context_ptr->stop(); + } else { + co_await connections->serve( + connections, + protocol_id, + [chat](ConnectionInfo info, StreamPtr stream) -> CoroOutcome { + co_return co_await chat->add(info, stream); + }); + std::optional> work_guard; + co_await coroHandler([&](CoroHandler &&handler) { + work_guard.emplace(std::move(handler)); + }); + } + co_return outcome::success(); +} + +int main(int argc, char **argv) { + setvbuf(stdout, nullptr, _IONBF, 0); + setvbuf(stderr, nullptr, _IONBF, 0); + + size_t arg_i = 0; + if (argc == 2) { + arg_i = std::atoi(argv[1]); + } + + auto io_context_ptr = std::make_shared(); + coroSpawn(*io_context_ptr, [io_context_ptr, arg_i]() -> Coro { + (co_await co_main(io_context_ptr, arg_i)).value(); + }); + io_context_ptr->run(); +} diff --git a/vcpkg-overlay/cppcodec.cmake b/vcpkg-overlay/cppcodec.cmake new file mode 100644 index 0000000..c21ba7c --- /dev/null +++ b/vcpkg-overlay/cppcodec.cmake @@ -0,0 +1,4 @@ + +find_path(CPPCODEC_INCLUDE_DIRS "cppcodec/base32_crockford.hpp") +add_library(cppcodec INTERFACE) +target_include_directories(cppcodec INTERFACE ${CPPCODEC_INCLUDE_DIRS}) diff --git a/vcpkg-overlay/liblsquic/disable-asan.patch b/vcpkg-overlay/liblsquic/disable-asan.patch new file mode 100644 index 0000000..2b05d0e --- /dev/null +++ b/vcpkg-overlay/liblsquic/disable-asan.patch @@ -0,0 +1,23 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 65c4776..5d4086a 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -60,12 +60,12 @@ ENDIF() + + IF(CMAKE_BUILD_TYPE STREQUAL "Debug") + SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -O0 -g3") +- IF(CMAKE_C_COMPILER MATCHES "clang" AND +- NOT "$ENV{TRAVIS}" MATCHES "^true$" AND +- NOT "$ENV{EXTRA_CFLAGS}" MATCHES "-fsanitize") +- SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -fsanitize=address") +- SET(LIBS ${LIBS} -fsanitize=address) +- ENDIF() ++ # IF(CMAKE_C_COMPILER MATCHES "clang" AND ++ # NOT "$ENV{TRAVIS}" MATCHES "^true$" AND ++ # NOT "$ENV{EXTRA_CFLAGS}" MATCHES "-fsanitize") ++ # SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -fsanitize=address") ++ # SET(LIBS ${LIBS} -fsanitize=address) ++ # ENDIF() + # Uncomment to enable cleartext protocol mode (no crypto): + #SET (MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -DLSQUIC_ENABLE_HANDSHAKE_DISABLE=1") + ELSE() diff --git a/vcpkg-overlay/liblsquic/fix-found-boringssl.patch b/vcpkg-overlay/liblsquic/fix-found-boringssl.patch new file mode 100644 index 0000000..a3a632c --- /dev/null +++ b/vcpkg-overlay/liblsquic/fix-found-boringssl.patch @@ -0,0 +1,53 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 5d4086a..e085a83 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -120,10 +120,12 @@ IF(CMAKE_BUILD_TYPE STREQUAL "Debug") + SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -Od") + #SET (MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -DFIU_ENABLE=1") + #SET(LIBS ${LIBS} fiu) ++ SET(LIB_NAME ssld cryptod) + ELSE() + SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -Ox") + # Comment out the following line to compile out debug messages: + #SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -DLSQUIC_LOWEST_LOG_LEVEL=LSQ_LOG_INFO") ++ SET(LIB_NAME ssl crypto) + ENDIF() + + ENDIF() #MSVC +@@ -191,7 +193,7 @@ IF (NOT DEFINED BORINGSSL_LIB AND DEFINED BORINGSSL_DIR) + ELSE() + + +- FOREACH(LIB_NAME ssl crypto) ++ FOREACH(LIB ${LIB_NAME}) + # If BORINGSSL_LIB is defined, try find each lib. Otherwise, user should define BORINGSSL_LIB_ssl, + # BORINGSSL_LIB_crypto and so on explicitly. For example, including boringssl and lsquic both via + # add_subdirectory: +@@ -201,20 +203,20 @@ ELSE() + # add_subdirectory(third_party/lsquic) + IF (DEFINED BORINGSSL_LIB) + IF (CMAKE_SYSTEM_NAME STREQUAL Windows) +- FIND_LIBRARY(BORINGSSL_LIB_${LIB_NAME} +- NAMES ${LIB_NAME} ++ FIND_LIBRARY(BORINGSSL_LIB_${LIB} ++ NAMES ${LIB} + PATHS ${BORINGSSL_LIB} + PATH_SUFFIXES Debug Release MinSizeRel RelWithDebInfo + NO_DEFAULT_PATH) + ELSE() +- FIND_LIBRARY(BORINGSSL_LIB_${LIB_NAME} +- NAMES lib${LIB_NAME}${LIB_SUFFIX} ++ FIND_LIBRARY(BORINGSSL_LIB_${LIB} ++ NAMES lib${LI}${LIB_SUFFIX} + PATHS ${BORINGSSL_LIB} +- PATH_SUFFIXES ${LIB_NAME} ++ PATH_SUFFIXES ${LIB} + NO_DEFAULT_PATH) + ENDIF() + ENDIF() +- IF(BORINGSSL_LIB_${LIB_NAME}) ++ IF(BORINGSSL_LIB_${LIB}) + MESSAGE(STATUS "Found ${LIB_NAME} library: ${BORINGSSL_LIB_${LIB_NAME}}") + ELSE() + MESSAGE(FATAL_ERROR "BORINGSSL_LIB_${LIB_NAME} library not found") diff --git a/vcpkg-overlay/liblsquic/lsquic_conn_ssl.patch b/vcpkg-overlay/liblsquic/lsquic_conn_ssl.patch new file mode 100644 index 0000000..ae7be54 --- /dev/null +++ b/vcpkg-overlay/liblsquic/lsquic_conn_ssl.patch @@ -0,0 +1,80 @@ +diff --git a/include/lsquic.h b/include/lsquic.h +index 389fbcc..c38d027 100644 +--- a/include/lsquic.h ++++ b/include/lsquic.h +@@ -1671,6 +1671,10 @@ int lsquic_stream_close(lsquic_stream_t *s); + int + lsquic_stream_has_unacked_data (lsquic_stream_t *s); + ++/* Return SSL object associated with this connection */ ++struct ssl_st * ++lsquic_conn_ssl(struct lsquic_conn *conn); ++ + /** + * Get certificate chain returned by the server. This can be used for + * server certificate verification. +diff --git a/src/liblsquic/lsquic_conn.c b/src/liblsquic/lsquic_conn.c +index f76550d..31e5285 100644 +--- a/src/liblsquic/lsquic_conn.c ++++ b/src/liblsquic/lsquic_conn.c +@@ -128,6 +128,12 @@ lsquic_conn_crypto_alg_keysize (const lsquic_conn_t *lconn) + } + + ++struct ssl_st * ++lsquic_conn_ssl(struct lsquic_conn *lconn) { ++ return lconn->cn_esf_c->esf_get_ssl(lconn->cn_enc_session); ++} ++ ++ + struct stack_st_X509 * + lsquic_conn_get_server_cert_chain (struct lsquic_conn *lconn) + { +diff --git a/src/liblsquic/lsquic_enc_sess.h b/src/liblsquic/lsquic_enc_sess.h +index f45c15f..3505fbd 100644 +--- a/src/liblsquic/lsquic_enc_sess.h ++++ b/src/liblsquic/lsquic_enc_sess.h +@@ -115,6 +115,9 @@ struct enc_session_funcs_common + (*esf_decrypt_packet)(enc_session_t *, struct lsquic_engine_public *, + const struct lsquic_conn *, struct lsquic_packet_in *); + ++ struct ssl_st * ++ (*esf_get_ssl)(enc_session_t *); ++ + struct stack_st_X509 * + (*esf_get_server_cert_chain) (enc_session_t *); + +diff --git a/src/liblsquic/lsquic_enc_sess_ietf.c b/src/liblsquic/lsquic_enc_sess_ietf.c +index 66329c1..076c4c5 100644 +--- a/src/liblsquic/lsquic_enc_sess_ietf.c ++++ b/src/liblsquic/lsquic_enc_sess_ietf.c +@@ -2519,6 +2519,13 @@ iquic_esf_global_cleanup (void) + } + + ++static struct ssl_st * ++iquic_esf_get_ssl(enc_session_t *enc_session_p) { ++ struct enc_sess_iquic *const enc_sess = enc_session_p; ++ return enc_sess->esi_ssl; ++} ++ ++ + static struct stack_st_X509 * + iquic_esf_get_server_cert_chain (enc_session_t *enc_session_p) + { +@@ -2744,6 +2751,7 @@ const struct enc_session_funcs_common lsquic_enc_session_common_ietf_v1 = + .esf_global_cleanup = iquic_esf_global_cleanup, + .esf_global_init = iquic_esf_global_init, + .esf_tag_len = IQUIC_TAG_LEN, ++ .esf_get_ssl = iquic_esf_get_ssl, + .esf_get_server_cert_chain + = iquic_esf_get_server_cert_chain, + .esf_get_sni = iquic_esf_get_sni, +@@ -2763,6 +2771,7 @@ const struct enc_session_funcs_common lsquic_enc_session_common_ietf_v1_no_flush + .esf_global_cleanup = iquic_esf_global_cleanup, + .esf_global_init = iquic_esf_global_init, + .esf_tag_len = IQUIC_TAG_LEN, ++ .esf_get_ssl = iquic_esf_get_ssl, + .esf_get_server_cert_chain + = iquic_esf_get_server_cert_chain, + .esf_get_sni = iquic_esf_get_sni, diff --git a/vcpkg-overlay/liblsquic/portfile.cmake b/vcpkg-overlay/liblsquic/portfile.cmake new file mode 100644 index 0000000..3602c59 --- /dev/null +++ b/vcpkg-overlay/liblsquic/portfile.cmake @@ -0,0 +1,78 @@ +if(VCPKG_TARGET_IS_WINDOWS) + # The lib uses CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS, at least until + # https://github.com/litespeedtech/lsquic/pull/371 or similar is merged + vcpkg_check_linkage(ONLY_STATIC_LIBRARY) +endif() + +vcpkg_from_github(OUT_SOURCE_PATH SOURCE_PATH + REPO litespeedtech/lsquic + REF v${VERSION} + SHA512 40d742779bfa2dc6fdaf0ee8e9349498d373dcffcc6dd27867c18d87309a288ea6811d693043b5d98364d816b818b49445214497475844201241193c0f37b349 + HEAD_REF master + PATCHES + disable-asan.patch + fix-found-boringssl.patch + lsquic_conn_ssl.patch +) + +# Submodules +vcpkg_from_github(OUT_SOURCE_PATH LSQPACK_SOURCE_PATH + REPO litespeedtech/ls-qpack + REF v2.5.3 + HEAD_REF master + SHA512 f90502c763abc84532f33d1b8f952aea7869e4e0c5f6bd344532ddd51c4a180958de4086d88b9ec96673a059c806eec9e70007651d4d4e1a73395919dee47ce0 +) +if(NOT EXISTS "${SOURCE_PATH}/src/ls-hpack/CMakeLists.txt") + file(REMOVE_RECURSE "${SOURCE_PATH}/src/liblsquic/ls-qpack") + file(RENAME "${LSQPACK_SOURCE_PATH}" "${SOURCE_PATH}/src/liblsquic/ls-qpack") +endif() + +vcpkg_from_github(OUT_SOURCE_PATH LSHPACK_SOURCE_PATH + REPO litespeedtech/ls-hpack + REF v2.3.2 + HEAD_REF master + SHA512 45d6c8296e8eee511e6a083f89460d5333fc9a49bc078dac55fdec6c46db199de9f150379f02e054571f954a5e3c79af3864dbc53dc57d10a8d2ed26a92d4278 +) +if(NOT EXISTS "${SOURCE_PATH}/src/lshpack/CMakeLists.txt") + file(REMOVE_RECURSE "${SOURCE_PATH}/src/lshpack") + file(RENAME "${LSHPACK_SOURCE_PATH}" "${SOURCE_PATH}/src/lshpack") +endif() + +# Configuration +vcpkg_find_acquire_program(PERL) + +string(COMPARE EQUAL "${VCPKG_LIBRARY_LINKAGE}" "dynamic" LSQUIC_SHARED_LIB) + +vcpkg_cmake_configure( + SOURCE_PATH "${SOURCE_PATH}" + OPTIONS + "-DPERL=${PERL}" + "-DPERL_EXECUTABLE=${PERL}" + "-DLSQUIC_SHARED_LIB=${LSQUIC_SHARED_LIB}" + "-DBORINGSSL_INCLUDE=${CURRENT_INSTALLED_DIR}/include" + -DLSQUIC_BIN=OFF + -DLSQUIC_TESTS=OFF + OPTIONS_RELEASE + "-DBORINGSSL_LIB=${CURRENT_INSTALLED_DIR}/lib" + OPTIONS_DEBUG + "-DBORINGSSL_LIB=${CURRENT_INSTALLED_DIR}/debug/lib" + -DLSQUIC_DEVEL=ON +) + +vcpkg_cmake_install() +if(VCPKG_TARGET_IS_WINDOWS) + # Upstream removed installation of this header after merging changes + file(INSTALL "${SOURCE_PATH}/wincompat/vc_compat.h" DESTINATION "${CURRENT_INSTALLED_DIR}/include/lsquic") +endif() + +vcpkg_cmake_config_fixup(PACKAGE_NAME lsquic) + +# Concatenate license files and install +vcpkg_install_copyright(FILE_LIST + "${SOURCE_PATH}/LICENSE" + "${SOURCE_PATH}/LICENSE.chrome" +) + +# Remove duplicated include directory +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") + diff --git a/vcpkg-overlay/liblsquic/vcpkg.json b/vcpkg-overlay/liblsquic/vcpkg.json new file mode 100644 index 0000000..ec90032 --- /dev/null +++ b/vcpkg-overlay/liblsquic/vcpkg.json @@ -0,0 +1,25 @@ +{ + "name": "liblsquic", + "version": "3.3.2", + "port-version": 1, + "description": "An implementation of the QUIC and HTTP/3 protocols.", + "homepage": "https://github.com/litespeedtech/lsquic", + "license": "MIT AND BSD-3-Clause", + "supports": "!x86", + "dependencies": [ + "boringssl", + { + "name": "getopt", + "platform": "windows" + }, + { + "name": "vcpkg-cmake", + "host": true + }, + { + "name": "vcpkg-cmake-config", + "host": true + }, + "zlib" + ] +} diff --git a/vcpkg.json b/vcpkg.json index bfdb1ba..d1029cf 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -2,9 +2,12 @@ "name": "cpp-jam", "version": "0.0.1", "dependencies": [ + "boost-asio", + "cppcodec", "fmt", "kagome-crates", "libb2", + "liblsquic", "scale" ], "features": { From f67cf4e2f66e8ddab1550ed526b7599a9495fa56 Mon Sep 17 00:00:00 2001 From: turuslan Date: Tue, 11 Feb 2025 13:14:44 +0500 Subject: [PATCH 02/40] ci Signed-off-by: turuslan --- .ci/.env | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.ci/.env b/.ci/.env index d17091b..46e9d13 100644 --- a/.ci/.env +++ b/.ci/.env @@ -6,6 +6,7 @@ LINUX_PACKAGES="make \ curl \ git \ libtool \ + nasm \ ninja-build \ pkg-config \ python3.12 \ @@ -23,6 +24,7 @@ MACOS_PACKAGES="make \ curl \ git \ libtool \ + nasm \ ninja \ pkg-config \ python@3.12 \ From ed8eb71be2b30f50cf2df35ff2f437c7fb3707b2 Mon Sep 17 00:00:00 2001 From: turuslan Date: Tue, 11 Feb 2025 13:42:36 +0500 Subject: [PATCH 03/40] ci Signed-off-by: turuslan --- .ci/.env | 1 + src/TODO_qtils/from_span.hpp | 1 + 2 files changed, 2 insertions(+) diff --git a/.ci/.env b/.ci/.env index 46e9d13..0159808 100644 --- a/.ci/.env +++ b/.ci/.env @@ -23,6 +23,7 @@ MACOS_PACKAGES="make \ rust \ curl \ git \ + go \ libtool \ nasm \ ninja \ diff --git a/src/TODO_qtils/from_span.hpp b/src/TODO_qtils/from_span.hpp index f19ef22..f81f84a 100644 --- a/src/TODO_qtils/from_span.hpp +++ b/src/TODO_qtils/from_span.hpp @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include From f7cf13218aaab67c268e595ffe5f25baf0753d99 Mon Sep 17 00:00:00 2001 From: turuslan Date: Wed, 12 Feb 2025 20:37:27 +0500 Subject: [PATCH 04/40] snp quic + rebase Signed-off-by: turuslan --- .ci/.env | 3 + BUILD.md | 4 + CMakeLists.txt | 19 +- src/CMakeLists.txt | 3 + src/TODO_qtils/asio_buffer.hpp | 31 + src/TODO_qtils/from_span.hpp | 32 + src/TODO_qtils/macro/forward.hpp | 11 + src/TODO_qtils/macro/make_shared.hpp | 16 + src/TODO_qtils/macro/move.hpp | 19 + src/TODO_qtils/macro/weak.hpp | 18 + src/TODO_qtils/map_entry.hpp | 84 +++ src/TODO_qtils/std_hash_of.hpp | 16 + src/coro/coro.hpp | 49 ++ src/coro/future.hpp | 68 ++ src/coro/handler.hpp | 35 ++ src/coro/init.hpp | 75 +++ src/coro/io_context_ptr.hpp | 17 + src/coro/set_thread.hpp | 20 + src/coro/spawn.hpp | 61 ++ src/coro/weak.hpp | 33 + src/coro/yield.hpp | 18 + src/crypto/ed25519.hpp | 25 +- src/injector/node_injector.cpp | 3 +- src/snp/CMakeLists.txt | 33 + src/snp/connections/address.hpp | 21 + src/snp/connections/alpn.cpp | 47 ++ src/snp/connections/alpn.hpp | 38 ++ src/snp/connections/config.hpp | 20 + src/snp/connections/connection.cpp | 38 ++ src/snp/connections/connection.hpp | 44 ++ src/snp/connections/connection_id.hpp | 17 + src/snp/connections/connection_id_counter.hpp | 25 + src/snp/connections/connection_info.hpp | 19 + src/snp/connections/connection_ptr.hpp | 21 + src/snp/connections/connections.cpp | 149 +++++ src/snp/connections/connections.hpp | 92 +++ src/snp/connections/controller.hpp | 26 + src/snp/connections/dns_name.cpp | 47 ++ src/snp/connections/dns_name.hpp | 37 ++ src/snp/connections/error.hpp | 131 ++++ src/snp/connections/key.hpp | 14 + src/snp/connections/lsquic/controller.hpp | 36 ++ src/snp/connections/lsquic/engine.cpp | 592 ++++++++++++++++++ src/snp/connections/lsquic/engine.hpp | 197 ++++++ src/snp/connections/lsquic/init.cpp | 24 + src/snp/connections/lsquic/init.hpp | 13 + src/snp/connections/lsquic/log.hpp | 22 + src/snp/connections/message_size.hpp | 15 + src/snp/connections/port.hpp | 13 + src/snp/connections/prefer_key.cpp | 13 + src/snp/connections/prefer_key.hpp | 17 + src/snp/connections/protocol_id.cpp | 19 + src/snp/connections/protocol_id.hpp | 54 ++ src/snp/connections/stream.cpp | 110 ++++ src/snp/connections/stream.hpp | 84 +++ src/snp/connections/stream_ptr.hpp | 20 + src/snp/connections/tls_certificate.cpp | 105 ++++ src/snp/connections/tls_certificate.hpp | 61 ++ src/snp/example_chat.cpp | 241 +++++++ src/types/genesis_hash.hpp | 13 + test-vectors/asn1.cmake | 1 - vcpkg-overlay/cppcodec.cmake | 4 + vcpkg-overlay/liblsquic/disable-asan.patch | 23 + .../liblsquic/fix-found-boringssl.patch | 53 ++ vcpkg-overlay/liblsquic/lsquic_conn_ssl.patch | 80 +++ vcpkg-overlay/liblsquic/portfile.cmake | 78 +++ vcpkg-overlay/liblsquic/vcpkg.json | 25 + vcpkg-overlay/scale/portfile.cmake | 4 +- vcpkg.json | 20 +- 69 files changed, 3392 insertions(+), 24 deletions(-) create mode 100644 BUILD.md create mode 100644 src/TODO_qtils/asio_buffer.hpp create mode 100644 src/TODO_qtils/from_span.hpp create mode 100644 src/TODO_qtils/macro/forward.hpp create mode 100644 src/TODO_qtils/macro/make_shared.hpp create mode 100644 src/TODO_qtils/macro/move.hpp create mode 100644 src/TODO_qtils/macro/weak.hpp create mode 100644 src/TODO_qtils/map_entry.hpp create mode 100644 src/TODO_qtils/std_hash_of.hpp create mode 100644 src/coro/coro.hpp create mode 100644 src/coro/future.hpp create mode 100644 src/coro/handler.hpp create mode 100644 src/coro/init.hpp create mode 100644 src/coro/io_context_ptr.hpp create mode 100644 src/coro/set_thread.hpp create mode 100644 src/coro/spawn.hpp create mode 100644 src/coro/weak.hpp create mode 100644 src/coro/yield.hpp create mode 100644 src/snp/CMakeLists.txt create mode 100644 src/snp/connections/address.hpp create mode 100644 src/snp/connections/alpn.cpp create mode 100644 src/snp/connections/alpn.hpp create mode 100644 src/snp/connections/config.hpp create mode 100644 src/snp/connections/connection.cpp create mode 100644 src/snp/connections/connection.hpp create mode 100644 src/snp/connections/connection_id.hpp create mode 100644 src/snp/connections/connection_id_counter.hpp create mode 100644 src/snp/connections/connection_info.hpp create mode 100644 src/snp/connections/connection_ptr.hpp create mode 100644 src/snp/connections/connections.cpp create mode 100644 src/snp/connections/connections.hpp create mode 100644 src/snp/connections/controller.hpp create mode 100644 src/snp/connections/dns_name.cpp create mode 100644 src/snp/connections/dns_name.hpp create mode 100644 src/snp/connections/error.hpp create mode 100644 src/snp/connections/key.hpp create mode 100644 src/snp/connections/lsquic/controller.hpp create mode 100644 src/snp/connections/lsquic/engine.cpp create mode 100644 src/snp/connections/lsquic/engine.hpp create mode 100644 src/snp/connections/lsquic/init.cpp create mode 100644 src/snp/connections/lsquic/init.hpp create mode 100644 src/snp/connections/lsquic/log.hpp create mode 100644 src/snp/connections/message_size.hpp create mode 100644 src/snp/connections/port.hpp create mode 100644 src/snp/connections/prefer_key.cpp create mode 100644 src/snp/connections/prefer_key.hpp create mode 100644 src/snp/connections/protocol_id.cpp create mode 100644 src/snp/connections/protocol_id.hpp create mode 100644 src/snp/connections/stream.cpp create mode 100644 src/snp/connections/stream.hpp create mode 100644 src/snp/connections/stream_ptr.hpp create mode 100644 src/snp/connections/tls_certificate.cpp create mode 100644 src/snp/connections/tls_certificate.hpp create mode 100644 src/snp/example_chat.cpp create mode 100644 src/types/genesis_hash.hpp create mode 100644 vcpkg-overlay/cppcodec.cmake create mode 100644 vcpkg-overlay/liblsquic/disable-asan.patch create mode 100644 vcpkg-overlay/liblsquic/fix-found-boringssl.patch create mode 100644 vcpkg-overlay/liblsquic/lsquic_conn_ssl.patch create mode 100644 vcpkg-overlay/liblsquic/portfile.cmake create mode 100644 vcpkg-overlay/liblsquic/vcpkg.json diff --git a/.ci/.env b/.ci/.env index d17091b..0159808 100644 --- a/.ci/.env +++ b/.ci/.env @@ -6,6 +6,7 @@ LINUX_PACKAGES="make \ curl \ git \ libtool \ + nasm \ ninja-build \ pkg-config \ python3.12 \ @@ -22,7 +23,9 @@ MACOS_PACKAGES="make \ rust \ curl \ git \ + go \ libtool \ + nasm \ ninja \ pkg-config \ python@3.12 \ diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000..15e6fa2 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,4 @@ + +```bash +brew install nasm # vcpkg liblsquic +``` diff --git a/CMakeLists.txt b/CMakeLists.txt index a84f22b..c194ebd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,21 +30,20 @@ find_package(PkgConfig REQUIRED) pkg_check_modules(libb2 REQUIRED IMPORTED_TARGET GLOBAL libb2) find_package(Boost CONFIG REQUIRED COMPONENTS algorithm outcome program_options) +find_package(Boost.DI CONFIG REQUIRED) find_package(fmt CONFIG REQUIRED) -find_package(yaml-cpp CONFIG REQUIRED) find_package(jam_crust CONFIG REQUIRED) +find_package(lsquic CONFIG REQUIRED) +find_package(OpenSSL REQUIRED) +find_package(prometheus-cpp CONFIG REQUIRED) +find_package(qtils CONFIG REQUIRED) find_package(scale CONFIG REQUIRED) -find_package(soralog CONFIG REQUIRED) find_package(schnorrkel_crust CONFIG REQUIRED) -find_package(Boost.DI CONFIG REQUIRED) -find_package(qtils CONFIG REQUIRED) -find_package(prometheus-cpp CONFIG REQUIRED) +find_package(soralog CONFIG REQUIRED) +find_package(yaml-cpp CONFIG REQUIRED) +find_package(ZLIB REQUIRED) -add_library(headers INTERFACE) -target_include_directories(headers INTERFACE - $ - $ -) +include(vcpkg-overlay/cppcodec.cmake) add_subdirectory(src) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 08f0356..4781ccc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -24,3 +24,6 @@ add_subdirectory(metrics) # Clocks and time subsystem add_subdirectory(clock) +# Simple Network Protocol +add_subdirectory(snp) + diff --git a/src/TODO_qtils/asio_buffer.hpp b/src/TODO_qtils/asio_buffer.hpp new file mode 100644 index 0000000..f613833 --- /dev/null +++ b/src/TODO_qtils/asio_buffer.hpp @@ -0,0 +1,31 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +namespace qtils { + inline boost::asio::const_buffer asioBuffer(BytesIn s) { + return {s.data(), s.size()}; + } + + boost::asio::mutable_buffer asioBuffer(auto &&t) + requires(requires { BytesOut{t}; }) + { + BytesOut s{t}; + return {s.data(), s.size()}; + } + + inline BytesIn asioBuffer(const boost::asio::const_buffer &s) { + return {static_cast(s.data()), s.size()}; + } + + inline BytesOut asioBuffer(const boost::asio::mutable_buffer &s) { + return {static_cast(s.data()), s.size()}; + } +} // namespace qtils diff --git a/src/TODO_qtils/from_span.hpp b/src/TODO_qtils/from_span.hpp new file mode 100644 index 0000000..0a5636a --- /dev/null +++ b/src/TODO_qtils/from_span.hpp @@ -0,0 +1,32 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +#include + +namespace qtils { + inline bool fromSpan(BytesOut out, BytesIn span) { + if (span.size() != out.size()) { + return false; + } + memcpy(out.data(), span.data(), out.size()); + return true; + } + + template + std::optional fromSpan(BytesIn span) { + T out; + if (not fromSpan(out, span)) { + return std::nullopt; + } + return out; + } +} // namespace qtils diff --git a/src/TODO_qtils/macro/forward.hpp b/src/TODO_qtils/macro/forward.hpp new file mode 100644 index 0000000..d03cec6 --- /dev/null +++ b/src/TODO_qtils/macro/forward.hpp @@ -0,0 +1,11 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#define FORWARD(x) std::forward(x) diff --git a/src/TODO_qtils/macro/make_shared.hpp b/src/TODO_qtils/macro/make_shared.hpp new file mode 100644 index 0000000..fb49925 --- /dev/null +++ b/src/TODO_qtils/macro/make_shared.hpp @@ -0,0 +1,16 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#define MAKE_SHARED_(x_, ...) \ + x_ { \ + std::make_shared(__VA_ARGS__) \ + } + +#define MAKE_SHARED_T(T, ...) std::make_shared(__VA_ARGS__) diff --git a/src/TODO_qtils/macro/move.hpp b/src/TODO_qtils/macro/move.hpp new file mode 100644 index 0000000..2fba319 --- /dev/null +++ b/src/TODO_qtils/macro/move.hpp @@ -0,0 +1,19 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#define MOVE(x) \ + x { \ + std::move(x) \ + } + +#define MOVE_(x) \ + x##_ { \ + std::move(x) \ + } diff --git a/src/TODO_qtils/macro/weak.hpp b/src/TODO_qtils/macro/weak.hpp new file mode 100644 index 0000000..34d7bd8 --- /dev/null +++ b/src/TODO_qtils/macro/weak.hpp @@ -0,0 +1,18 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#define WEAK_SELF \ + weak_self { \ + weak_from_this() \ + } + +#define WEAK_LOCK(name) \ + auto name = weak_##name.lock(); \ + if (not name) return diff --git a/src/TODO_qtils/map_entry.hpp b/src/TODO_qtils/map_entry.hpp new file mode 100644 index 0000000..b4c714b --- /dev/null +++ b/src/TODO_qtils/map_entry.hpp @@ -0,0 +1,84 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include + +namespace qtils { + template + struct MapEntry { + using I = typename M::iterator; + using K = typename M::key_type; + + MapEntry(M &map, const K &key) : map{map} { + if (auto it = map.find(key); it != map.end()) { + it_or_key = it; + } else { + it_or_key = key; + } + } + + bool has() const { + return std::holds_alternative(it_or_key); + } + operator bool() const { + return has(); + } + auto &operator*() { + if (not has()) { + throw std::logic_error{"MapEntry::operator*"}; + } + return std::get(it_or_key)->second; + } + auto *operator->() { + if (not has()) { + throw std::logic_error{"MapEntry::operator->"}; + } + return &std::get(it_or_key)->second; + } + void insert(M::mapped_type value) { + if (has()) { + throw std::logic_error{"MapEntry::insert"}; + } + it_or_key = + map.emplace(std::move(std::get(it_or_key)), std::move(value)) + .first; + } + void insert_or_assign(M::mapped_type value) { + if (not has()) { + insert(std::move(value)); + } else { + **this = std::move(value); + } + } + M::mapped_type remove() { + if (not has()) { + throw std::logic_error{"MapEntry::remove"}; + } + auto node = map.extract(std::get(it_or_key)); + it_or_key = std::move(node.key()); + return std::move(node.mapped()); + } + + // NOLINTNEXTLINE(cppcoreguidelines-avoid-const-or-ref-data-members) + M ↦ + std::variant it_or_key{}; + }; + + template + auto entry(std::map &map, const K &key) { + return MapEntry>{map, key}; + } + + template + auto entry(std::unordered_map &map, const K &key) { + return MapEntry>{map, key}; + } +} // namespace qtils diff --git a/src/TODO_qtils/std_hash_of.hpp b/src/TODO_qtils/std_hash_of.hpp new file mode 100644 index 0000000..f20aa01 --- /dev/null +++ b/src/TODO_qtils/std_hash_of.hpp @@ -0,0 +1,16 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace qtils { + template + size_t stdHashOf(const T &v) { + return std::hash()(v); + } +} // namespace qtils diff --git a/src/coro/coro.hpp b/src/coro/coro.hpp new file mode 100644 index 0000000..99c29a7 --- /dev/null +++ b/src/coro/coro.hpp @@ -0,0 +1,49 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +namespace jam { + /** + * Return type for coroutine. + * + * Does not resume when: + * - called directly outside executor, returns coroutine. + * - `coroSpawn` called when not running inside executor, + * resumes on next executor tick. + * int main() { + * boost::asio::io_context io; + * coroSpawn(io, []() -> Coro { co_return; }); // suspended + * io.run_one(); // resumes + * // may complete before next statement + * } + * Resumes when: + * - `coroSpawn` when running inside specified executor. + * post(executor, [] { + * coroSpawn(executor, []() -> Coro { co_return; }) // resumes + * // may complete before next statement + * }) + * co_await coroSpawn([]() -> Coro { co_return; }) // resumes + * // may complete before next statement + * - `co_await` + * co_await foo() // resumes + * // may complete before next statement + * After resuming may complete before specified statement ends. + * + * Use `CORO_YIELD` explicitly to suspend coroutine until next executor tick. + */ + template + using Coro = boost::asio::awaitable; + + /** + * Return type for coroutine returning outcome. + */ + template + using CoroOutcome = Coro>; +} // namespace jam diff --git a/src/coro/future.hpp b/src/coro/future.hpp new file mode 100644 index 0000000..11c2593 --- /dev/null +++ b/src/coro/future.hpp @@ -0,0 +1,68 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +#include + +#include "coro/handler.hpp" +#include "coro/set_thread.hpp" + +namespace jam { + template + class SharedFuture { + public: + using Self = std::shared_ptr>; + + SharedFuture(IoContextPtr io_context_ptr) : MOVE_(io_context_ptr) {} + + static Coro ready(Self self) { + SET_CORO_THREAD(self->io_context_ptr_); + co_return std::holds_alternative(self->state_); + } + + /** + * Resumes coroutine immediately or inside `set`. + */ + static Coro get(Self self) { + SET_CORO_THREAD(self->io_context_ptr_); + if (auto *value = std::get_if(&self->state_)) { + co_return *value; + } + auto &handlers = std::get(self->state_); + co_return co_await coroHandler([&](CoroHandler &&handler) { + handlers.emplace_back(std::move(handler)); + }); + } + + /** + * Set value and wake waiting coroutines. + * Coroutines may complete before `set` returns. + */ + static Coro set(Self self, T value) { + SET_CORO_THREAD(self->io_context_ptr_); + if (std::holds_alternative(self->state_)) { + throw std::logic_error{"SharedFuture::set must be called once"}; + } + auto handlers = std::move(std::get(self->state_)); + self->state_ = std::move(value); + auto &state_value = std::get(self->state_); + for (auto &handler : handlers) { + handler(state_value); + } + } + + private: + using Handlers = std::deque>; + + IoContextPtr io_context_ptr_; + std::variant state_; + }; +} // namespace jam diff --git a/src/coro/handler.hpp b/src/coro/handler.hpp new file mode 100644 index 0000000..696175e --- /dev/null +++ b/src/coro/handler.hpp @@ -0,0 +1,35 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include + +#include "coro/coro.hpp" + +namespace jam { + template + using CoroHandler = std::conditional_t< + std::is_void_v, + boost::asio::detail::awaitable_handler::executor_type>, + boost::asio::detail::awaitable_handler::executor_type, + T>>; + + /** + * Create handler for coroutine. + * Coroutine may complete earlier than handler returns. + */ + template + Coro coroHandler(std::invocable &&> auto &&f) { + co_await [&](auto *frame) { + f(CoroHandler{frame->detach_thread()}); + return nullptr; + }; + abort(); + } +} // namespace jam diff --git a/src/coro/init.hpp b/src/coro/init.hpp new file mode 100644 index 0000000..dd06bba --- /dev/null +++ b/src/coro/init.hpp @@ -0,0 +1,75 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "coro/future.hpp" +#include "coro/spawn.hpp" + +namespace jam { + /** + * Async init flag. + * struct Foo { + * CoroOutcome init() { + * auto init = init_.init(); // dtor will fail incomplete init + * ... + * init.ready(); // init completed + * } + * CoroOutcome foo() { + * if (not co_await init_.ready()) // init failed + * ... // ready + * } + * CoroInit init_; + * } + */ + class CoroInit { + class Init { + public: + Init(CoroInit &self) : self_{self} {} + ~Init() { + self_.set(false); + } + void ready() { + self_.set(true); + } + + private: + CoroInit &self_; + }; + + public: + CoroInit(IoContextPtr io_context_ptr) + : MOVE_(io_context_ptr), MAKE_SHARED_(future_, io_context_ptr_) {} + + auto init() { + if (init_called_) { + throw std::logic_error{"Coro::init init must be called once"}; + } + init_called_ = true; + return Init{*this}; + } + + Coro ready() { + return future_->get(future_); + } + + private: + void set(bool ready) { + coroSpawn(*io_context_ptr_, [future{future_}, ready]() -> Coro { + if (not ready and co_await future->ready(future)) { + co_return; + } + co_await future->set(future, ready); + }); + } + + IoContextPtr io_context_ptr_; + std::shared_ptr> future_; + bool init_called_ = false; + }; +} // namespace jam diff --git a/src/coro/io_context_ptr.hpp b/src/coro/io_context_ptr.hpp new file mode 100644 index 0000000..d88248e --- /dev/null +++ b/src/coro/io_context_ptr.hpp @@ -0,0 +1,17 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace boost::asio { + class io_context; +} // namespace boost::asio + +namespace jam { + using IoContextPtr = std::shared_ptr; +} // namespace jam diff --git a/src/coro/set_thread.hpp b/src/coro/set_thread.hpp new file mode 100644 index 0000000..eeb0909 --- /dev/null +++ b/src/coro/set_thread.hpp @@ -0,0 +1,20 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +#include "coro/io_context_ptr.hpp" + +#define SET_CORO_THREAD(io_context_ptr) \ + ({ \ + if (not io_context_ptr->get_executor().running_in_this_thread()) { \ + co_await boost::asio::post(*io_context_ptr, boost::asio::use_awaitable); \ + } \ + }) diff --git a/src/coro/spawn.hpp b/src/coro/spawn.hpp new file mode 100644 index 0000000..ade2513 --- /dev/null +++ b/src/coro/spawn.hpp @@ -0,0 +1,61 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include +#include +#include + +#include "coro/coro.hpp" + +namespace jam { + void coroSpawn(auto &&executor, Coro &&coro) { + boost::asio::co_spawn( + FORWARD(executor), std::move(coro), [](std::exception_ptr e) { + if (e != nullptr) { + std::rethrow_exception(e); + } + }); + } + + template + void coroSpawn(auto &&executor, Coro &&coro) { + coroSpawn(FORWARD(executor), [MOVE(coro)]() mutable -> Coro { + std::ignore = co_await std::move(coro); + }); + } + + /** + * Start coroutine on specified executor. + * Spawning on same executor would execute coroutine immediately, + * so coroutine may complete before `coroSpawn` returns. + * Prevents dangling lambda capture in `coroSpawn([capture] { ... })`. + * `co_spawn([capture] { ... })` doesn't work + * because lambda is destroyed after returning coroutine object. + * `co_spawn([](args){ ... }(capture))` + * works because arguments are stored in coroutine state. + */ + void coroSpawn(auto &&executor, auto f) { + coroSpawn(FORWARD(executor), [](decltype(f) f) -> Coro { + if constexpr (std::is_void_v) { + co_await f(); + } else { + std::ignore = co_await f(); + } + }(std::move(f))); + } + + /** + * `coroSpawn` with current coroutine executor. + */ + Coro coroSpawn(auto f) { + coroSpawn(co_await boost::asio::this_coro::executor, std::move(f)); + co_return; + } +} // namespace jam diff --git a/src/coro/weak.hpp b/src/coro/weak.hpp new file mode 100644 index 0000000..21e9a87 --- /dev/null +++ b/src/coro/weak.hpp @@ -0,0 +1,33 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "coro/coro.hpp" + +#define _CORO_WEAK_AWAIT(tmp_weak, tmp_coro, auto_r, r, shared, coro, ...) \ + ({ \ + auto tmp_weak = std::weak_ptr{shared}; \ + auto tmp_coro = (coro); \ + shared.reset(); \ + auto_r co_await std::move(tmp_coro); \ + shared = tmp_weak.lock(); \ + if (not shared) co_return __VA_ARGS__; \ + r \ + }) +#define CORO_WEAK_AWAIT(shared, coro, ...) \ + _CORO_WEAK_AWAIT( \ + QTILS_UNIQUE_NAME(tmp_weak), QTILS_UNIQUE_NAME(tmp_coro), auto r =, r; \ + , shared, coro, __VA_ARGS__) + +#define CORO_WEAK_AWAIT_V(shared, coro, ...) \ + _CORO_WEAK_AWAIT(QTILS_UNIQUE_NAME(tmp_weak), \ + QTILS_UNIQUE_NAME(tmp_coro), \ + , \ + , \ + shared, \ + coro, \ + __VA_ARGS__) diff --git a/src/coro/yield.hpp b/src/coro/yield.hpp new file mode 100644 index 0000000..e8b4cbc --- /dev/null +++ b/src/coro/yield.hpp @@ -0,0 +1,18 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +/** + * Thread switch operation always completes, so it can't leak `shared_ptr`. + */ +#define CORO_YIELD \ + co_await boost::asio::post(co_await boost::asio::this_coro::executor, \ + boost::asio::use_awaitable) diff --git a/src/crypto/ed25519.hpp b/src/crypto/ed25519.hpp index 224d46c..441bf9e 100644 --- a/src/crypto/ed25519.hpp +++ b/src/crypto/ed25519.hpp @@ -4,10 +4,15 @@ * SPDX-License-Identifier: Apache-2.0 */ +#pragma once + #include + +#include #include namespace jam::crypto::ed25519 { + using Seed = qtils::BytesN; using Secret = qtils::BytesN; using Public = qtils::BytesN; using KeyPair = qtils::BytesN; @@ -34,4 +39,22 @@ namespace jam::crypto::ed25519 { message.size_bytes()); return res == ED25519_RESULT_OK; } -} // namespace jam::ed25519 + + inline KeyPair from_seed(const Seed &seed) { + KeyPair keypair; + ed25519_keypair_from_seed(keypair.data(), seed.data()); + return keypair; + } + + inline Public get_public(const KeyPair &keypair) { + return qtils::fromSpan( + std::span{keypair}.subspan(ED25519_SECRET_KEY_LENGTH)) + .value(); + } + + inline Public get_secret(const KeyPair &keypair) { + return qtils::fromSpan( + std::span{keypair}.first(ED25519_SECRET_KEY_LENGTH)) + .value(); + } +} // namespace jam::crypto::ed25519 diff --git a/src/injector/node_injector.cpp b/src/injector/node_injector.cpp index 48ef7d5..26962a5 100644 --- a/src/injector/node_injector.cpp +++ b/src/injector/node_injector.cpp @@ -50,6 +50,7 @@ namespace { di::bind.to(logsys), di::bind.to(), di::bind.to(), + useConfig(metrics::Session::Configuration{}), di::bind.to([](const auto &injector) { return metrics::Exposer::Configuration{ {boost::asio::ip::address_v4::from_string("127.0.0.1"), 7777} @@ -92,7 +93,7 @@ namespace jam::injector { NodeInjector::NodeInjector(std::shared_ptr logsys, std::shared_ptr config) : pimpl_{std::make_unique( - makeNodeInjector(std::move(logsys), std::move(config)))} {} + makeNodeInjector(std::move(logsys), std::move(config)))} {} std::shared_ptr NodeInjector::injectApplication() { return pimpl_->injector_ diff --git a/src/snp/CMakeLists.txt b/src/snp/CMakeLists.txt new file mode 100644 index 0000000..82f6781 --- /dev/null +++ b/src/snp/CMakeLists.txt @@ -0,0 +1,33 @@ +# +# Copyright Quadrivium LLC +# All Rights Reserved +# SPDX-License-Identifier: Apache-2.0 +# + +add_library(snp + connections/alpn.cpp + connections/connection.cpp + connections/connections.cpp + connections/dns_name.cpp + connections/lsquic/engine.cpp + connections/lsquic/init.cpp + connections/prefer_key.cpp + connections/protocol_id.cpp + connections/stream.cpp + connections/tls_certificate.cpp +) +target_link_libraries(snp + fmt::fmt + lsquic::lsquic + OpenSSL::SSL + schnorrkel_crust::schnorrkel_crust + ZLIB::ZLIB +) + +add_executable(example_chat + example_chat.cpp +) +target_link_libraries(example_chat + snp +) + diff --git a/src/snp/connections/address.hpp b/src/snp/connections/address.hpp new file mode 100644 index 0000000..1e5e8cc --- /dev/null +++ b/src/snp/connections/address.hpp @@ -0,0 +1,21 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "snp/connections/key.hpp" +#include "snp/connections/port.hpp" + +namespace jam::snp { + struct Address { + using Ip = qtils::BytesN<16>; + static constexpr Ip kLocal{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; + + Ip ip; + Port port; + Key key; + }; +} // namespace jam::snp diff --git a/src/snp/connections/alpn.cpp b/src/snp/connections/alpn.cpp new file mode 100644 index 0000000..1fe4c56 --- /dev/null +++ b/src/snp/connections/alpn.cpp @@ -0,0 +1,47 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "snp/connections/alpn.hpp" + +#include +#include +#include +#include + +#include "snp/connections/error.hpp" + +namespace jam::snp { + Alpn::Alpn(const GenesisHash &genesis) { + const auto protocol = + fmt::format("jamnp-s/{}/{:x}", kVersion, std::span{genesis}.first(4)); + bytes_.reserve(1 + protocol.size()); + bytes_.emplace_back(protocol.size()); + qtils::append(bytes_, qtils::str2byte(protocol)); + } + + outcome::result Alpn::set(ssl_ctx_st *ssl_ctx) { + if (SSL_CTX_set_alpn_protos(ssl_ctx, bytes_.data(), bytes_.size()) != 0) { + return OpenSslError::SSL_CTX_set_alpn_protos; + } + SSL_CTX_set_alpn_select_cb(ssl_ctx, select, this); + return outcome::success(); + } + + int Alpn::select(ssl_st *ssl, + const unsigned char **out, + unsigned char *outlen, + const unsigned char *in, + unsigned int inlen, + void *void_self) { + auto *self = static_cast(void_self); + uint8_t *out2 = nullptr; + int r = SSL_select_next_proto( + &out2, outlen, in, inlen, self->bytes_.data(), self->bytes_.size()); + *out = out2; + return r == OPENSSL_NPN_NEGOTIATED ? SSL_TLSEXT_ERR_OK + : SSL_TLSEXT_ERR_ALERT_FATAL; + } +} // namespace jam::snp diff --git a/src/snp/connections/alpn.hpp b/src/snp/connections/alpn.hpp new file mode 100644 index 0000000..a3dec6f --- /dev/null +++ b/src/snp/connections/alpn.hpp @@ -0,0 +1,38 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include + +#include "types/genesis_hash.hpp" + +struct ssl_ctx_st; +struct ssl_st; + +namespace jam::snp { + // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L30-L41 + class Alpn { + static constexpr auto kVersion = 0; + + public: + Alpn(const GenesisHash &genesis); + + outcome::result set(ssl_ctx_st *ssl_ctx); + + private: + static int select(ssl_st *ssl, + const unsigned char **out, + unsigned char *outlen, + const unsigned char *in, + unsigned int inlen, + void *void_self); + + qtils::Bytes bytes_; + }; +} // namespace jam::snp diff --git a/src/snp/connections/config.hpp b/src/snp/connections/config.hpp new file mode 100644 index 0000000..ad421de --- /dev/null +++ b/src/snp/connections/config.hpp @@ -0,0 +1,20 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "crypto/ed25519.hpp" +#include "snp/connections/port.hpp" +#include "types/genesis_hash.hpp" + +namespace jam::snp { + struct ConnectionsConfig { + // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L30-L35 + GenesisHash genesis; + crypto::ed25519::KeyPair keypair; + std::optional listen_port; + }; +} // namespace jam::snp diff --git a/src/snp/connections/connection.cpp b/src/snp/connections/connection.cpp new file mode 100644 index 0000000..50a3f1c --- /dev/null +++ b/src/snp/connections/connection.cpp @@ -0,0 +1,38 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "snp/connections/connection.hpp" + +#include +#include + +#include "coro/set_thread.hpp" +#include "snp/connections/error.hpp" +#include "snp/connections/lsquic/engine.hpp" + +namespace jam::snp { + using lsquic::Engine; + + Connection::Connection(IoContextPtr io_context_ptr, + lsquic::ConnCtx *conn_ctx, + ConnectionInfo info) + : MOVE_(io_context_ptr), MOVE_(conn_ctx), MOVE_(info) {} + + Connection::~Connection() { + boost::asio::dispatch(*io_context_ptr_, [conn_ctx{conn_ctx_}] { + Engine::destroyConnection(conn_ctx); + }); + } + + const ConnectionInfo &Connection::info() const { + return info_; + } + + StreamPtrCoroOutcome Connection::open(Self self, ProtocolId protocol_id) { + SET_CORO_THREAD(self->io_context_ptr_); + co_return co_await Engine::openStream(self->conn_ctx_, protocol_id); + } +} // namespace jam::snp diff --git a/src/snp/connections/connection.hpp b/src/snp/connections/connection.hpp new file mode 100644 index 0000000..2e710ca --- /dev/null +++ b/src/snp/connections/connection.hpp @@ -0,0 +1,44 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "coro/coro.hpp" +#include "coro/io_context_ptr.hpp" +#include "snp/connections/connection_info.hpp" +#include "snp/connections/protocol_id.hpp" +#include "snp/connections/stream_ptr.hpp" + +namespace jam::snp::lsquic { + struct ConnCtx; + class Engine; +} // namespace jam::snp::lsquic + +namespace jam::snp { + class Connection { + friend lsquic::Engine; + + public: + using Self = std::shared_ptr; + + Connection(IoContextPtr io_context_ptr, + lsquic::ConnCtx *conn_ctx, + ConnectionInfo info); + ~Connection(); + + const ConnectionInfo &info() const; + + /** + * Open stream with specified `ProtocolId`. + */ + static StreamPtrCoroOutcome open(Self self, ProtocolId protocol_id); + + private: + IoContextPtr io_context_ptr_; + lsquic::ConnCtx *conn_ctx_; + ConnectionInfo info_; + }; +} // namespace jam::snp diff --git a/src/snp/connections/connection_id.hpp b/src/snp/connections/connection_id.hpp new file mode 100644 index 0000000..040e250 --- /dev/null +++ b/src/snp/connections/connection_id.hpp @@ -0,0 +1,17 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace jam::snp { + /** + * Is not QUIC connection id. + * Used to distinguish connections with same peer key. + */ + using ConnectionId = uint64_t; +} // namespace jam::snp diff --git a/src/snp/connections/connection_id_counter.hpp b/src/snp/connections/connection_id_counter.hpp new file mode 100644 index 0000000..efc416e --- /dev/null +++ b/src/snp/connections/connection_id_counter.hpp @@ -0,0 +1,25 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +#include "snp/connections/connection_id.hpp" + +namespace jam::snp { + class ConnectionIdCounter { + public: + ConnectionId make() { + return connection_id_->fetch_add(1); + } + + private: + using Atomic = std::atomic; + std::shared_ptr connection_id_ = std::make_shared(); + }; +} // namespace jam::snp diff --git a/src/snp/connections/connection_info.hpp b/src/snp/connections/connection_info.hpp new file mode 100644 index 0000000..ac3b02a --- /dev/null +++ b/src/snp/connections/connection_info.hpp @@ -0,0 +1,19 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "snp/connections/connection_id.hpp" +#include "snp/connections/key.hpp" + +namespace jam::snp { + struct ConnectionInfo { + ConnectionId id; + Key key; + + bool operator==(const ConnectionInfo &) const = default; + }; +} // namespace jam::snp diff --git a/src/snp/connections/connection_ptr.hpp b/src/snp/connections/connection_ptr.hpp new file mode 100644 index 0000000..12f8d3d --- /dev/null +++ b/src/snp/connections/connection_ptr.hpp @@ -0,0 +1,21 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "coro/coro.hpp" + +namespace jam::snp { + class Connection; +} // namespace jam::snp + +namespace jam::snp { + using ConnectionPtr = std::shared_ptr; + using ConnectionPtrOutcome = outcome::result; + using ConnectionPtrCoroOutcome = CoroOutcome; +} // namespace jam::snp diff --git a/src/snp/connections/connections.cpp b/src/snp/connections/connections.cpp new file mode 100644 index 0000000..877e844 --- /dev/null +++ b/src/snp/connections/connections.cpp @@ -0,0 +1,149 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "snp/connections/connections.hpp" + +#include +#include +#include +#include + +#include "coro/set_thread.hpp" +#include "coro/spawn.hpp" +#include "coro/weak.hpp" +#include "coro/yield.hpp" +#include "snp/connections/address.hpp" +#include "snp/connections/connection.hpp" +#include "snp/connections/controller.hpp" +#include "snp/connections/error.hpp" +#include "snp/connections/lsquic/engine.hpp" + +namespace jam::snp { + inline void todoPreferConnection() { + // TODO(turuslan): how to deduplicate connections between two peers? + throw std::logic_error{"TODO: prefer connection"}; + } + + Connections::Connections(IoContextPtr io_context_ptr, + ConnectionsConfig config) + : MOVE_(io_context_ptr), + init_{io_context_ptr_}, + MOVE_(config), + key_{crypto::ed25519::get_public(config_.keypair)} {} + + CoroOutcome Connections::init( + Self self, std::weak_ptr controller) { + SET_CORO_THREAD(self->io_context_ptr_); + auto init = self->init_.init(); + self->controller_ = std::move(controller); + BOOST_OUTCOME_CO_TRY(auto certificate, TlsCertificate::make(self->config_)); + BOOST_OUTCOME_CO_TRY(self->client_, + lsquic::Engine::make(self->io_context_ptr_, + self->connection_id_counter_, + certificate, + std::nullopt, + self)); + if (self->config_.listen_port) { + BOOST_OUTCOME_CO_TRY(self->server_, + lsquic::Engine::make(self->io_context_ptr_, + self->connection_id_counter_, + certificate, + self->config_.listen_port, + self)); + } + init.ready(); + co_return outcome::success(); + } + + const Key &Connections::key() const { + return key_; + } + + ConnectionPtrCoroOutcome Connections::connect(Self self, Address address) { + SET_CORO_THREAD(self->io_context_ptr_); + if (not co_await self->init_.ready()) { + co_return ConnectionsError::CONNECTIONS_INIT; + } + auto state = qtils::entry(self->connections_, address.key); + if (not state) { + state.insert(MAKE_SHARED_T(Connecting, self->io_context_ptr_)); + co_await coroSpawn([self, address, state]() mutable -> Coro { + CORO_YIELD; + auto connection_result = CORO_WEAK_AWAIT( + self, self->client_->connect(self->client_, address)); + auto state = qtils::entry(self->connections_, address.key); + if (not state or not std::holds_alternative(*state)) { + todoPreferConnection(); + } + auto connecting = std::move(std::get(*state)); + if (connection_result) { + auto &connection = connection_result.value(); + *state = Connected{connection}; + if (auto controller = self->controller_.lock()) { + controller->onOpen(address.key); + } + } else { + state.remove(); + } + CORO_WEAK_AWAIT_V( + self, connecting->set(connecting, std::move(connection_result))); + }); + } else if (auto *connected = std::get_if(&*state)) { + co_return *connected; + } + auto connecting = std::get(*state); + self.reset(); + co_return co_await connecting->get(connecting); + } + + Coro Connections::serve(Self self, + ProtocolId protocol_id, + ServeProtocol serve) { + SET_CORO_THREAD(self->io_context_ptr_); + qtils::entry(self->protocols_, protocol_id).insert(std::move(serve)); + } + + void Connections::onConnectionAccept(ConnectionPtr connection) { + auto state = entry(connections_, connection->info().key); + if (state) { + todoPreferConnection(); + } + state.insert(Connected{connection}); + if (auto controller = controller_.lock()) { + controller->onOpen(connection->info().key); + } + } + + void Connections::onConnectionClose(ConnectionInfo connection_info) { + auto state = entry(connections_, connection_info.key); + if (not state or not std::holds_alternative(*state) + or std::get(*state)->info() != connection_info) { + todoPreferConnection(); + } + state.remove(); + if (auto controller = controller_.lock()) { + controller->onClose(connection_info.key); + } + } + + void Connections::onStreamAccept(ConnectionPtr connection, + ProtocolId protocol_id, + StreamPtr stream) { + coroSpawn(*io_context_ptr_, + [self{shared_from_this()}, + protocol_id, + MOVE(stream), + connection_info{connection->info()}]() mutable -> Coro { + auto serve = qtils::entry(self->protocols_, protocol_id); + if (not serve) { + co_return; + } + auto copy = *serve; + std::ignore = CORO_WEAK_AWAIT( + self, copy(connection_info, std::move(stream))); + }); + } +} // namespace jam::snp diff --git a/src/snp/connections/connections.hpp b/src/snp/connections/connections.hpp new file mode 100644 index 0000000..0c241df --- /dev/null +++ b/src/snp/connections/connections.hpp @@ -0,0 +1,92 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +#include + +#include "coro/coro.hpp" +#include "coro/init.hpp" +#include "coro/io_context_ptr.hpp" +#include "snp/connections/config.hpp" +#include "snp/connections/connection_id_counter.hpp" +#include "snp/connections/connection_ptr.hpp" +#include "snp/connections/key.hpp" +#include "snp/connections/lsquic/controller.hpp" + +namespace jam::snp { + class Address; + class ConnectionsController; +} // namespace jam::snp + +namespace jam::snp::lsquic { + class Engine; +} // namespace jam::snp::lsquic + +namespace jam::snp { + /** + * Initiates and accepts connections with peers. + * Prevents duplicate connections with peers. + */ + class Connections : public std::enable_shared_from_this, + public lsquic::EngineController { + public: + using Self = std::shared_ptr; + + Connections(IoContextPtr io_context_ptr, ConnectionsConfig config); + + /** + * Set controller. + * Start quic server and client. + */ + static CoroOutcome init( + Self self, std::weak_ptr controller); + + const Key &key() const; + + /** + * Connect or return existing connection. + */ + static ConnectionPtrCoroOutcome connect(Self self, Address address); + + using ServeProtocol = + std::function(ConnectionInfo, StreamPtr)>; + /** + * Set callback to handle protocol on server side. + */ + static Coro serve(Self self, + ProtocolId protocol_id, + ServeProtocol serve); + + // EngineController + void onConnectionAccept(ConnectionPtr connection) override; + void onConnectionClose(ConnectionInfo connection_info) override; + void onStreamAccept(ConnectionPtr connection, + ProtocolId protocol_id, + StreamPtr stream) override; + + private: + using Connecting = std::shared_ptr>; + using Connected = ConnectionPtr; + + IoContextPtr io_context_ptr_; + CoroInit init_; + ConnectionsConfig config_; + Key key_; + std::weak_ptr controller_; + std::shared_ptr client_; + std::optional> server_; + std::unordered_map, + qtils::BytesStdHash> + connections_; + std::unordered_map protocols_; + ConnectionIdCounter connection_id_counter_; + }; +} // namespace jam::snp diff --git a/src/snp/connections/controller.hpp b/src/snp/connections/controller.hpp new file mode 100644 index 0000000..9cf8962 --- /dev/null +++ b/src/snp/connections/controller.hpp @@ -0,0 +1,26 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "snp/connections/key.hpp" + +namespace jam::snp { + class ConnectionsController { + public: + virtual ~ConnectionsController() = default; + + /** + * There is now some connection with peer. + */ + virtual void onOpen(Key key) {} + + /** + * There are no more connections with peer. + */ + virtual void onClose(Key key) {} + }; +} // namespace jam::snp diff --git a/src/snp/connections/dns_name.cpp b/src/snp/connections/dns_name.cpp new file mode 100644 index 0000000..5200ac3 --- /dev/null +++ b/src/snp/connections/dns_name.cpp @@ -0,0 +1,47 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "snp/connections/dns_name.hpp" + +#include +#include + +namespace jam::snp::base32 { + constexpr auto kAlphabet = "abcdefghijklmnopqrstuvwxyz234567"; + + struct Config { + template + using codec_impl = cppcodec::detail::stream_codec; + + static CPPCODEC_ALWAYS_INLINE constexpr size_t alphabet_size() { + return 32; + } + static CPPCODEC_ALWAYS_INLINE constexpr char symbol( + cppcodec::detail::alphabet_index_t idx) { + return kAlphabet[idx]; + } + static CPPCODEC_ALWAYS_INLINE constexpr bool generates_padding() { + return false; + } + }; + + inline void encode(std::span out, qtils::BytesIn bytes) { + using codec = cppcodec::detail::codec>; + codec::encode(out.data(), out.size(), bytes); + } +} // namespace jam::snp::base32 + +namespace jam::snp { + DnsName::DnsName(const Key &key) { + chars[0] = 'e'; + base32::encode(std::span{chars}.subspan(1), key); + } + + outcome::result DnsName::set(x509_st *x509) const { + // TODO(turuslan): cert.alt = DnsName(key) + return outcome::success(); + } +} // namespace jam::snp diff --git a/src/snp/connections/dns_name.hpp b/src/snp/connections/dns_name.hpp new file mode 100644 index 0000000..a9ee4bc --- /dev/null +++ b/src/snp/connections/dns_name.hpp @@ -0,0 +1,37 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include + +#include "snp/connections/key.hpp" + +struct x509_st; + +namespace jam::snp { + // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L15-L16 + struct DnsName { + explicit DnsName(const Key &key); + + constexpr operator std::string_view() const { + return std::string_view{chars.data(), chars.size()}; + } + + /** + * Set `DnsName` as subject alternative name for certificate. + */ + outcome::result set(x509_st *x509) const; + + static constexpr size_t kSize = 53; + std::array chars; + }; + constexpr auto format_as(const DnsName &v) { + return v.operator std::string_view(); + } +} // namespace jam::snp diff --git a/src/snp/connections/error.hpp b/src/snp/connections/error.hpp new file mode 100644 index 0000000..c225a4a --- /dev/null +++ b/src/snp/connections/error.hpp @@ -0,0 +1,131 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace jam::snp { + enum class OpenSslError { + EVP_PKEY_get_raw_public_key, + EVP_PKEY_new_raw_private_key, + SSL_CTX_set_alpn_protos, + SSL_CTX_use_certificate, + SSL_CTX_use_PrivateKey, + SSL_CTX_set_signing_algorithm_prefs, + SSL_CTX_set_verify_algorithm_prefs, + SSL_get_peer_certificate, + X509_gmtime_adj, + X509_set_pubkey, + X509_get_pubkey, + X509_sign, + }; + Q_ENUM_ERROR_CODE(OpenSslError) { + using E = decltype(e); + switch (e) { + case E::EVP_PKEY_get_raw_public_key: + return "EVP_PKEY_get_raw_public_key"; + case E::EVP_PKEY_new_raw_private_key: + return "EVP_PKEY_new_raw_private_key"; + case E::SSL_CTX_set_alpn_protos: + return "SSL_CTX_set_alpn_protos"; + case E::SSL_CTX_use_certificate: + return "SSL_CTX_use_certificate"; + case E::SSL_CTX_use_PrivateKey: + return "SSL_CTX_use_PrivateKey"; + case E::SSL_CTX_set_signing_algorithm_prefs: + return "SSL_CTX_set_signing_algorithm_prefs"; + case E::SSL_CTX_set_verify_algorithm_prefs: + return "SSL_CTX_set_verify_algorithm_prefs"; + case E::SSL_get_peer_certificate: + return "SSL_get_peer_certificate"; + case E::X509_gmtime_adj: + return "X509_gmtime_adj"; + case E::X509_set_pubkey: + return "X509_set_pubkey"; + case E::X509_get_pubkey: + return "X509_get_pubkey"; + case E::X509_sign: + return "X509_sign"; + } + } + + enum class LsQuicError { + lsquic_conn_make_stream, + lsquic_engine_connect, + lsquic_engine_new, + lsquic_global_init, + }; + Q_ENUM_ERROR_CODE(LsQuicError) { + using E = decltype(e); + switch (e) { + case E::lsquic_conn_make_stream: + return "lsquic_conn_make_stream"; + case E::lsquic_engine_connect: + return "lsquic_engine_connect"; + case E::lsquic_engine_new: + return "lsquic_engine_new"; + case E::lsquic_global_init: + return "lsquic_global_init"; + } + } + + enum class ConnectionsError { + CONNECTION_OPEN_CLOSED, + CONNECTION_OPEN_DUPLICATE, + CONNECTIONS_INIT, + ENGINE_CONNECT_ALREADY, + ENGINE_CONNECT_CLOSED, + ENGINE_CONNECT_KEY_MISMATCH, + ENGINE_OPEN_STREAM_ALREADY, + ENGINE_OPEN_STREAM_TOO_MANY, + HANDSHAKE_FAILED, + PROTOCOL_ID_MAKE_INVALID, + STREAM_READ_CLOSED, + STREAM_READ_DESTROYED, + STREAM_READ_PROTOCOL_ID_CLOSED, + STREAM_READ_TOO_BIG, + STREAM_WRITE_CLOSED, + STREAM_WRITE_DESTROYED, + }; + Q_ENUM_ERROR_CODE(ConnectionsError) { + using E = decltype(e); + switch (e) { + case E::CONNECTION_OPEN_CLOSED: + return "Connection::open closed"; + case E::CONNECTION_OPEN_DUPLICATE: + return "Connection::open duplicate"; + case E::CONNECTIONS_INIT: + return "Connections::init error"; + case E::ENGINE_CONNECT_ALREADY: + return "Engine::connect already"; + case E::ENGINE_CONNECT_CLOSED: + return "Engine::connect closed"; + case E::ENGINE_CONNECT_KEY_MISMATCH: + return "Engine::connect key mismatch"; + case E::ENGINE_OPEN_STREAM_ALREADY: + return "Engine::openStream already"; + case E::ENGINE_OPEN_STREAM_TOO_MANY: + return "Engine::openStream too many streams"; + case E::HANDSHAKE_FAILED: + return "handshake failed"; + case E::PROTOCOL_ID_MAKE_INVALID: + return "ProtocolId::make invalid"; + case E::STREAM_READ_CLOSED: + return "Stream::read closed"; + case E::STREAM_READ_DESTROYED: + return "Stream::read destroyed"; + case E::STREAM_READ_PROTOCOL_ID_CLOSED: + return "Stream::readProtocolId closed"; + case E::STREAM_READ_TOO_BIG: + return "Stream::read too big"; + case E::STREAM_WRITE_CLOSED: + return "Stream::write closed"; + case E::STREAM_WRITE_DESTROYED: + return "Stream::write destroyed"; + } + } +} // namespace jam::snp diff --git a/src/snp/connections/key.hpp b/src/snp/connections/key.hpp new file mode 100644 index 0000000..01217e0 --- /dev/null +++ b/src/snp/connections/key.hpp @@ -0,0 +1,14 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "crypto/ed25519.hpp" + +namespace jam::snp { + // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L13-L14 + using Key = crypto::ed25519::Public; +} // namespace jam::snp diff --git a/src/snp/connections/lsquic/controller.hpp b/src/snp/connections/lsquic/controller.hpp new file mode 100644 index 0000000..10af36d --- /dev/null +++ b/src/snp/connections/lsquic/controller.hpp @@ -0,0 +1,36 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "snp/connections/connection_info.hpp" +#include "snp/connections/connection_ptr.hpp" +#include "snp/connections/protocol_id.hpp" +#include "snp/connections/stream_ptr.hpp" + +namespace jam::snp::lsquic { + class EngineController { + public: + virtual ~EngineController() = default; + + /** + * Connection was accepted. + */ + virtual void onConnectionAccept(ConnectionPtr connection) {} + + /** + * Connection was closed. + */ + virtual void onConnectionClose(ConnectionInfo connection_info) {} + + /** + * Stream was accepted. + */ + virtual void onStreamAccept(ConnectionPtr connection, + ProtocolId protocol_id, + StreamPtr stream) {} + }; +} // namespace jam::snp::lsquic diff --git a/src/snp/connections/lsquic/engine.cpp b/src/snp/connections/lsquic/engine.cpp new file mode 100644 index 0000000..6a92056 --- /dev/null +++ b/src/snp/connections/lsquic/engine.cpp @@ -0,0 +1,592 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "snp/connections/lsquic/engine.hpp" + +#include +#include +#include +#include + +#include "coro/set_thread.hpp" +#include "coro/spawn.hpp" +#include "snp/connections/config.hpp" +#include "snp/connections/connection.hpp" +#include "snp/connections/error.hpp" +#include "snp/connections/lsquic/controller.hpp" +#include "snp/connections/lsquic/init.hpp" +#include "snp/connections/stream.hpp" + +#define SELF_FROM_VOID Engine *self = static_cast(void_self) + +// TODO(turuslan): unique streams +// TODO(turuslan): connection/stream close event lag + +namespace jam::snp::lsquic { + // TODO(turuslan): config + constexpr uint32_t kWindowSize = 64 << 10; + + template + T::Ls *to_ls(T *ptr) { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + return reinterpret_cast(ptr); + } + template + T *from_ls(typename T::Ls *ptr) { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + return reinterpret_cast(ptr); + } + + void tryDelete(auto *ptr) { + if (not ptr->canDelete()) { + return; + } + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + delete ptr; + } + + Socket::endpoint_type make_endpoint(const Address &address) { + auto ip = boost::asio::ip::make_address_v6(address.ip); + return Socket::endpoint_type{ip, address.port}; + } + + outcome::result> Engine::make( + IoContextPtr io_context_ptr, + ConnectionIdCounter connection_id_counter, + TlsCertificate certificate, + std::optional listen_port, + std::weak_ptr controller) { + OUTCOME_TRY(init()); + + uint32_t flags = 0; + if (listen_port) { + flags |= LSENG_SERVER; + } + + lsquic_engine_settings settings{}; + lsquic_engine_init_settings(&settings, flags); + settings.es_init_max_stream_data_bidi_remote = kWindowSize; + settings.es_init_max_stream_data_bidi_local = kWindowSize; + + static lsquic_stream_if stream_if{}; + stream_if.on_new_conn = on_new_conn; + stream_if.on_conn_closed = on_conn_closed; + stream_if.on_hsk_done = on_hsk_done; + stream_if.on_new_stream = on_new_stream; + stream_if.on_close = on_close; + stream_if.on_read = on_read; + stream_if.on_write = on_write; + + lsquic_engine_api api{}; + api.ea_settings = &settings; + + Socket socket{*io_context_ptr}; + boost::system::error_code ec; + socket.open(boost::asio::ip::udp::v6(), ec); + if (ec) { + return ec; + } + socket.non_blocking(true, ec); + if (ec) { + return ec; + } + if (listen_port) { + auto ip = boost::asio::ip::address_v6::any(); + socket.bind({ip, listen_port.value()}, ec); + if (ec) { + return ec; + } + } + auto socket_local_endpoint = socket.local_endpoint(ec); + if (ec) { + return ec; + } + auto self = std::make_shared(Private{}, + io_context_ptr, + std::move(connection_id_counter), + std::move(certificate), + std::move(socket), + socket_local_endpoint, + std::move(controller)); + + api.ea_stream_if = &stream_if; + api.ea_stream_if_ctx = self.get(); + api.ea_packets_out = ea_packets_out; + api.ea_packets_out_ctx = self.get(); + api.ea_get_ssl_ctx = ea_get_ssl_ctx; + + self->engine_ = lsquic_engine_new(flags, &api); + if (self->engine_ == nullptr) { + return LsQuicError::lsquic_engine_new; + } + + io_context_ptr->post([weak_self{std::weak_ptr{self}}] { + WEAK_LOCK(self); + self->readLoop(); + }); + + return self; + } + + Engine::Engine(Private, + IoContextPtr io_context_ptr, + ConnectionIdCounter connection_id_counter, + TlsCertificate &&certificate, + Socket &&socket, + Socket::endpoint_type socket_local_endpoint, + std::weak_ptr controller) + : MOVE_(io_context_ptr), + MOVE_(connection_id_counter), + MOVE_(certificate), + MOVE_(socket), + MOVE_(socket_local_endpoint), + MOVE_(controller), + timer_{*io_context_ptr_} {} + + Engine::~Engine() { + if (engine_ != nullptr) { + boost::asio::dispatch(*io_context_ptr_, [engine{engine_}] { + // will call `Engine::on_conn_closed`, `Engine::on_close`. + lsquic_engine_destroy(engine); + }); + } + } + + ConnectionPtrCoroOutcome Engine::connect(Self self, Address address) { + SET_CORO_THREAD(self->io_context_ptr_); + if (self->connecting_) { + co_return ConnectionsError::ENGINE_CONNECT_ALREADY; + } + co_return co_await coroHandler( + [&](CoroHandler &&handler) { + self->connecting_.emplace(Connecting{ + .address = address, + .handler = std::move(handler), + }); + // will call `Engine::ea_get_ssl_ctx`, `Engine::on_new_conn`. + lsquic_engine_connect(self->engine_, + N_LSQVER, + self->socket_local_endpoint_.data(), + make_endpoint(address).data(), + self.get(), + nullptr, + nullptr, + 0, + nullptr, + 0, + nullptr, + 0); + if (auto connecting = qtils::optionTake(self->connecting_)) { + connecting->handler(LsQuicError::lsquic_engine_connect); + } + self->wantProcess(); + }); + } + + void Engine::wantFlush(StreamCtx *stream_ctx) { + if (stream_ctx->want_flush) { + return; + } + stream_ctx->want_flush = true; + if (not stream_ctx->stream) { + return; + } + want_flush_.emplace_back(stream_ctx->stream.value()); + wantProcess(); + } + + void Engine::wantProcess() { + if (want_process_) { + return; + } + want_process_ = true; + boost::asio::post(*io_context_ptr_, [WEAK_SELF] { + WEAK_LOCK(self); + self->process(); + }); + } + + void Engine::process() { + want_process_ = false; + auto want_flush = std::exchange(want_flush_, {}); + for (auto &weak_stream : want_flush) { + auto stream = weak_stream.lock(); + if (not stream) { + continue; + } + if (not stream->stream_ctx_->ls_stream) { + continue; + } + stream->stream_ctx_->want_flush = false; + lsquic_stream_flush(stream->stream_ctx_->ls_stream.value()); + } + // will call `Engine::on_new_conn`, `Engine::on_conn_closed`, + // `Engine::on_new_stream`, `Engine::on_close`, `Engine::on_read`, + // `Engine::on_write`, `Engine::ea_packets_out`. + lsquic_engine_process_conns(engine_); + int us = 0; + if (not lsquic_engine_earliest_adv_tick(engine_, &us)) { + return; + } + timer_.expires_after(std::chrono::microseconds{us}); + auto cb = [WEAK_SELF](boost::system::error_code ec) { + WEAK_LOCK(self); + if (ec) { + return; + } + self->process(); + }; + timer_.async_wait(std::move(cb)); + } + + void Engine::readLoop() { + // https://github.com/cbodley/nexus/blob/d1d8486f713fd089917331239d755932c7c8ed8e/src/socket.cc#L293 + while (true) { + socklen_t len = socket_local_endpoint_.size(); + auto n = recvfrom(socket_.native_handle(), + reading_.buffer.data(), + reading_.buffer.size(), + 0, + reading_.remote_endpoint.data(), + &len); + if (n == -1) { + if (errno == EAGAIN or errno == EWOULDBLOCK) { + auto cb = [WEAK_SELF](boost::system::error_code ec) { + WEAK_LOCK(self); + if (ec) { + return; + } + self->readLoop(); + }; + socket_.async_wait(boost::asio::socket_base::wait_read, + std::move(cb)); + } + break; + } + // will call `Engine::on_hsk_done`, `Engine::ea_get_ssl_ctx`. + lsquic_engine_packet_in(engine_, + reading_.buffer.data(), + n, + socket_local_endpoint_.data(), + reading_.remote_endpoint.data(), + this, + 0); + } + process(); + } + + void Engine::destroyConnection(ConnCtx *conn_ctx) { + conn_ctx->connection.reset(); + if (conn_ctx->ls_conn) { + lsquic_conn_close(conn_ctx->ls_conn.value()); + } else { + tryDelete(conn_ctx); + } + } + + StreamPtrCoroOutcome Engine::openStream(ConnCtx *conn_ctx, + ProtocolId protocol_id) { + if (not conn_ctx->ls_conn) { + co_return ConnectionsError::CONNECTION_OPEN_CLOSED; + } + if (conn_ctx->open_stream) { + co_return ConnectionsError::ENGINE_OPEN_STREAM_ALREADY; + } + if (lsquic_conn_n_avail_streams(conn_ctx->ls_conn.value()) == 0) { + co_return ConnectionsError::ENGINE_OPEN_STREAM_TOO_MANY; + } + conn_ctx->open_stream = nullptr; + // will call `Engine::on_new_stream`. + lsquic_conn_make_stream(conn_ctx->ls_conn.value()); + auto stream = qtils::optionTake(conn_ctx->open_stream).value(); + if (stream == nullptr) { + co_return LsQuicError::lsquic_conn_make_stream; + } + // stream not weak, because no other owners yet + BOOST_OUTCOME_CO_TRY(co_await stream->writeProtocolId(protocol_id)); + co_return stream; + } + + void Engine::destroyStream(StreamCtx *stream_ctx) { + stream_ctx->stream.reset(); + if (stream_ctx->ls_stream) { + lsquic_stream_close(stream_ctx->ls_stream.value()); + } else { + tryDelete(stream_ctx); + } + } + + void Engine::streamAccept(StreamPtr &&stream) { + coroSpawn(*io_context_ptr_, + [weak_controller{controller_}, + MOVE(stream)]() mutable -> CoroOutcome { + // stream not weak, because no other owners yet + BOOST_OUTCOME_CO_TRY(auto protocol_id, + co_await stream->readProtocolId()); + if (auto controller = weak_controller.lock()) { + auto &connection = stream->connection_; + controller->onStreamAccept( + connection, protocol_id, std::move(stream)); + } + co_return outcome::success(); + }); + } + + void Engine::streamReadFin(StreamCtx *stream_ctx) { + if (not stream_ctx->ls_stream) { + return; + } + lsquic_stream_shutdown(stream_ctx->ls_stream.value(), SHUT_RD); + } + + void Engine::streamWriteFin(StreamCtx *stream_ctx) { + if (not stream_ctx->ls_stream) { + return; + } + lsquic_stream_shutdown(stream_ctx->ls_stream.value(), SHUT_WR); + } + + CoroOutcome Engine::streamReadRaw(StreamCtx *stream_ctx, + qtils::BytesOut message) { + if (stream_ctx->reading) { + throw std::logic_error{"Engine::streamReadRaw duplicate"}; + } + auto remaining = message; + while (not remaining.empty()) { + if (not stream_ctx->ls_stream) { + co_return ConnectionsError::STREAM_READ_CLOSED; + } + auto n = lsquic_stream_read( + stream_ctx->ls_stream.value(), remaining.data(), remaining.size()); + if (n == 0) { + if (remaining.size() == message.size()) { + co_return false; + } else { + co_return ConnectionsError::STREAM_READ_CLOSED; + } + } + if (n == -1) { + if (errno != EWOULDBLOCK) { + co_return ConnectionsError::STREAM_READ_CLOSED; + } + co_await coroHandler([&](CoroHandler &&handler) { + stream_ctx->reading.emplace(std::move(handler)); + lsquic_stream_wantread(stream_ctx->ls_stream.value(), 1); + }); + continue; + } + remaining = remaining.subspan(n); + } + co_return true; + } + + CoroOutcome Engine::streamWriteRaw(StreamCtx *stream_ctx, + qtils::BytesIn message) { + if (stream_ctx->writing) { + throw std::logic_error{"Engine::streamWriteRaw duplicate"}; + } + auto remaining = message; + while (not remaining.empty()) { + if (not stream_ctx->ls_stream) { + co_return ConnectionsError::STREAM_WRITE_CLOSED; + } + auto n = lsquic_stream_write( + stream_ctx->ls_stream.value(), remaining.data(), remaining.size()); + if (n < 0) { + co_return ConnectionsError::STREAM_WRITE_CLOSED; + } + if (n != 0) { + remaining = remaining.subspan(n); + auto self = stream_ctx->engine.lock(); + if (not self) { + co_return ConnectionsError::STREAM_WRITE_CLOSED; + } + self->wantFlush(stream_ctx); + } + if (remaining.empty()) { + break; + } + co_await coroHandler([&](CoroHandler &&handler) { + stream_ctx->writing.emplace(std::move(handler)); + lsquic_stream_wantwrite(stream_ctx->ls_stream.value(), 1); + }); + } + co_return outcome::success(); + } + + lsquic_conn_ctx_t *Engine::on_new_conn(void *void_self, + lsquic_conn_t *ls_conn) { + SELF_FROM_VOID; + auto connecting = qtils::optionTake(self->connecting_); + auto is_connecting = connecting.has_value(); + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + auto *conn_ctx = new ConnCtx{ + .engine = self->weak_from_this(), + .ls_conn = ls_conn, + .connecting = std::move(connecting), + }; + auto *ls_conn_ctx = to_ls(conn_ctx); + lsquic_conn_set_ctx(ls_conn, ls_conn_ctx); + if (not is_connecting) { + // lsquic doesn't call `on_hsk_done` for incoming connection + on_hsk_done(ls_conn, LSQ_HSK_OK); + } + return ls_conn_ctx; + } + + void Engine::on_conn_closed(lsquic_conn_t *ls_conn) { + auto *conn_ctx = from_ls(lsquic_conn_get_ctx(ls_conn)); + conn_ctx->ls_conn.reset(); + lsquic_conn_set_ctx(ls_conn, nullptr); + if (auto connecting = qtils::optionTake(conn_ctx->connecting)) { + connecting->handler(ConnectionsError::ENGINE_CONNECT_CLOSED); + } else if (auto self = conn_ctx->engine.lock()) { + if (auto controller = self->controller_.lock()) { + controller->onConnectionClose(conn_ctx->info.value()); + } + } + tryDelete(conn_ctx); + } + + void Engine::on_hsk_done(lsquic_conn_t *ls_conn, lsquic_hsk_status status) { + auto *conn_ctx = from_ls(lsquic_conn_get_ctx(ls_conn)); + auto self = conn_ctx->engine.lock(); + if (not self) { + return; + } + auto ok = status == LSQ_HSK_OK or status == LSQ_HSK_RESUMED_OK; + auto connecting = qtils::optionTake(conn_ctx->connecting); + auto connection_result = [&]() -> ConnectionPtrOutcome { + if (not ok) { + return ConnectionsError::HANDSHAKE_FAILED; + } + OUTCOME_TRY(key, TlsCertificate::get_key(lsquic_conn_ssl(ls_conn))); + if (connecting and key != connecting->address.key) { + return ConnectionsError::ENGINE_CONNECT_KEY_MISMATCH; + } + conn_ctx->info = ConnectionInfo{ + .id = self->connection_id_counter_.make(), + .key = key, + }; + auto connection = std::make_shared( + self->io_context_ptr_, conn_ctx, conn_ctx->info.value()); + conn_ctx->connection = connection; + return connection; + }(); + if (not connection_result) { + lsquic_conn_close(ls_conn); + } + if (connecting) { + connecting->handler(std::move(connection_result)); + } else if (connection_result) { + auto &connection = connection_result.value(); + if (auto controller = self->controller_.lock()) { + controller->onConnectionAccept(std::move(connection)); + } + } + } + + lsquic_stream_ctx_t *Engine::on_new_stream(void *void_self, + lsquic_stream_t *ls_stream) { + SELF_FROM_VOID; + auto *conn_ctx = + from_ls(lsquic_conn_get_ctx(lsquic_stream_conn(ls_stream))); + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + auto *stream_ctx = new StreamCtx{ + .engine = self->weak_from_this(), + .ls_stream = ls_stream, + }; + ConnectionPtr connection; + if (conn_ctx->connection) { + connection = conn_ctx->connection->lock(); + } + if (connection) { + auto stream = std::make_shared( + self->io_context_ptr_, connection, stream_ctx); + stream_ctx->stream = stream; + if (conn_ctx->open_stream) { + conn_ctx->open_stream.value() = stream; + } else { + self->streamAccept(std::move(stream)); + } + } else { + lsquic_stream_close(ls_stream); + } + return to_ls(stream_ctx); + } + + void Engine::on_close(lsquic_stream_t *ls_stream, + lsquic_stream_ctx_t *ls_stream_ctx) { + auto *stream_ctx = from_ls(ls_stream_ctx); + stream_ctx->ls_stream.reset(); + if (auto reading = qtils::optionTake(stream_ctx->reading)) { + reading.value()(); + } + if (auto writing = qtils::optionTake(stream_ctx->writing)) { + writing.value()(); + } + tryDelete(stream_ctx); + } + + void Engine::on_read(lsquic_stream_t *ls_stream, + lsquic_stream_ctx_t *ls_stream_ctx) { + lsquic_stream_wantread(ls_stream, 0); + auto *stream_ctx = from_ls(ls_stream_ctx); + if (auto reading = qtils::optionTake(stream_ctx->reading)) { + reading.value()(); + } + } + + void Engine::on_write(lsquic_stream_t *ls_stream, + lsquic_stream_ctx_t *ls_stream_ctx) { + lsquic_stream_wantwrite(ls_stream, 0); + auto *stream_ctx = from_ls(ls_stream_ctx); + if (auto writing = qtils::optionTake(stream_ctx->writing)) { + writing.value()(); + } + } + + ssl_ctx_st *Engine::ea_get_ssl_ctx(void *void_self, const sockaddr *) { + SELF_FROM_VOID; + return self->certificate_; + } + + int Engine::ea_packets_out(void *void_self, + const lsquic_out_spec *out_spec, + unsigned n_packets_out) { + SELF_FROM_VOID; + // https://github.com/cbodley/nexus/blob/d1d8486f713fd089917331239d755932c7c8ed8e/src/socket.cc#L218 + int r = 0; + for (auto &spec : std::span{out_spec, n_packets_out}) { + msghdr msg{}; + msg.msg_iov = spec.iov; + msg.msg_iovlen = spec.iovlen; + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) + msg.msg_name = const_cast(spec.dest_sa); + msg.msg_namelen = spec.dest_sa->sa_family == AF_INET + ? sizeof(sockaddr_in) + : sizeof(sockaddr_in6); + auto n = sendmsg(self->socket_.native_handle(), &msg, 0); + if (n == -1) { + if (errno == EAGAIN or errno == EWOULDBLOCK) { + auto cb = [weak_self{self->weak_from_this()}]( + boost::system::error_code ec) { + WEAK_LOCK(self); + if (ec) { + return; + } + // will call `Engine::ea_packets_out`. + lsquic_engine_send_unsent_packets(self->engine_); + }; + self->socket_.async_wait(Socket::wait_write, std::move(cb)); + } + break; + } + ++r; + } + return r; + } +} // namespace jam::snp::lsquic diff --git a/src/snp/connections/lsquic/engine.hpp b/src/snp/connections/lsquic/engine.hpp new file mode 100644 index 0000000..229949b --- /dev/null +++ b/src/snp/connections/lsquic/engine.hpp @@ -0,0 +1,197 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +#include +#include + +#include "coro/coro.hpp" +#include "coro/handler.hpp" +#include "coro/io_context_ptr.hpp" +#include "snp/connections/address.hpp" +#include "snp/connections/connection_id_counter.hpp" +#include "snp/connections/connection_info.hpp" +#include "snp/connections/connection_ptr.hpp" +#include "snp/connections/protocol_id.hpp" +#include "snp/connections/stream_ptr.hpp" +#include "snp/connections/tls_certificate.hpp" + +struct sockaddr; + +namespace jam::snp { + class ConnectionsConfig; +} // namespace jam::snp + +namespace jam::snp::lsquic { + class Engine; + class EngineController; +} // namespace jam::snp::lsquic + +namespace jam::snp::lsquic { + using Socket = boost::asio::ip::udp::socket; + Socket::endpoint_type make_endpoint(const Address &address); + + /** + * Captures `Engine::connect` arguments. + */ + struct Connecting { + Address address; + CoroHandler handler; + }; + + /** + * `lsquic_conn_ctx_t`. + */ + struct ConnCtx { + using Ls = lsquic_conn_ctx_t; + + std::weak_ptr engine; + std::optional ls_conn; + std::optional> connection; + std::optional connecting; + std::optional info; + std::optional open_stream; + + bool canDelete() const { + return not ls_conn and not connection; + } + }; + + /** + * `lsquic_stream_ctx_t`. + */ + struct StreamCtx { + using Ls = lsquic_stream_ctx_t; + + std::weak_ptr engine; + std::optional ls_stream; + std::optional> stream; + std::optional> reading; + std::optional> writing; + bool want_flush = false; + + bool canDelete() const { + return not ls_stream and not stream; + } + }; + + class Engine : public std::enable_shared_from_this { + friend Connection; + friend Stream; + + struct Private {}; + + public: + using Self = std::shared_ptr; + + static outcome::result> make( + IoContextPtr io_context_ptr, + ConnectionIdCounter connection_id_counter, + TlsCertificate certificate, + std::optional listen_port, + std::weak_ptr controller); + Engine(Private, + IoContextPtr io_context_ptr, + ConnectionIdCounter connection_id_counter, + TlsCertificate &&certificate, + Socket &&socket, + Socket::endpoint_type socket_local_endpoint, + std::weak_ptr controller); + ~Engine(); + + static ConnectionPtrCoroOutcome connect(Self self, Address address); + + private: + struct Reading { + static constexpr size_t kMaxUdpPacketSize = 64 << 10; + qtils::BytesN buffer; + boost::asio::ip::udp::endpoint remote_endpoint; + }; + + void wantFlush(StreamCtx *stream_ctx); + void wantProcess(); + void process(); + void readLoop(); + static void destroyConnection(ConnCtx *conn_ctx); + static StreamPtrCoroOutcome openStream(ConnCtx *conn_ctx, + ProtocolId protocol_id); + static void destroyStream(StreamCtx *stream_ctx); + void streamAccept(StreamPtr &&stream); + static void streamReadFin(StreamCtx *stream_ctx); + static void streamWriteFin(StreamCtx *stream_ctx); + static CoroOutcome streamReadRaw(StreamCtx *stream_ctx, + qtils::BytesOut message); + static CoroOutcome streamWriteRaw(StreamCtx *stream_ctx, + qtils::BytesIn message); + + /** + * Called from `lsquic_engine_connect` (client), + * `lsquic_engine_process_conns` (server). + */ + static lsquic_conn_ctx_t *on_new_conn(void *void_self, + lsquic_conn_t *ls_conn); + /** + * Called from `lsquic_engine_process_conns`, `lsquic_engine_destroy`. + */ + static void on_conn_closed(lsquic_conn_t *ls_conn); + /** + * Called from `lsquic_engine_packet_in` (client), + * `on_new_conn` (server). + */ + static void on_hsk_done(lsquic_conn_t *ls_conn, lsquic_hsk_status status); + /** + * Called from `lsquic_conn_make_stream` (client), + * `lsquic_engine_process_conns` (server). + */ + static lsquic_stream_ctx_t *on_new_stream(void *void_self, + lsquic_stream_t *ls_stream); + /** + * Called from `lsquic_engine_process_conns`, `lsquic_engine_destroy`. + */ + static void on_close(lsquic_stream_t *ls_stream, + lsquic_stream_ctx_t *ls_stream_ctx); + /** + * Called from `lsquic_engine_process_conns`. + * `lsquic_stream_flush` doesn't work inside `on_read`. + */ + static void on_read(lsquic_stream_t *ls_stream, + lsquic_stream_ctx_t *ls_stream_ctx); + /** + * Called from `lsquic_engine_process_conns`. + */ + static void on_write(lsquic_stream_t *ls_stream, + lsquic_stream_ctx_t *ls_stream_ctx); + /** + * Called from `lsquic_engine_connect` (client), + * `lsquic_engine_packet_in` (server). + */ + static ssl_ctx_st *ea_get_ssl_ctx(void *void_self, const sockaddr *); + /** + * Called from `lsquic_engine_process_conns`, + * `lsquic_engine_send_unsent_packets`. + */ + static int ea_packets_out(void *void_self, + const lsquic_out_spec *out_spec, + unsigned n_packets_out); + + IoContextPtr io_context_ptr_; + ConnectionIdCounter connection_id_counter_; + TlsCertificate certificate_; + Socket socket_; + Socket::endpoint_type socket_local_endpoint_; + std::weak_ptr controller_; + boost::asio::steady_timer timer_; + lsquic_engine_t *engine_ = nullptr; + Reading reading_; + std::optional connecting_; + std::deque> want_flush_; + bool want_process_ = false; + }; +} // namespace jam::snp::lsquic diff --git a/src/snp/connections/lsquic/init.cpp b/src/snp/connections/lsquic/init.cpp new file mode 100644 index 0000000..df6c069 --- /dev/null +++ b/src/snp/connections/lsquic/init.cpp @@ -0,0 +1,24 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "snp/connections/lsquic/init.hpp" + +#include + +#include "snp/connections/error.hpp" + +namespace jam::snp::lsquic { + outcome::result init() { + static auto ok = [] { + return lsquic_global_init(LSQUIC_GLOBAL_CLIENT | LSQUIC_GLOBAL_SERVER) + == 0; + }(); + if (not ok) { + return LsQuicError::lsquic_global_init; + } + return outcome::success(); + } +} // namespace jam::snp::lsquic diff --git a/src/snp/connections/lsquic/init.hpp b/src/snp/connections/lsquic/init.hpp new file mode 100644 index 0000000..b51dc58 --- /dev/null +++ b/src/snp/connections/lsquic/init.hpp @@ -0,0 +1,13 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace jam::snp::lsquic { + outcome::result init(); +} // namespace jam::snp::lsquic diff --git a/src/snp/connections/lsquic/log.hpp b/src/snp/connections/lsquic/log.hpp new file mode 100644 index 0000000..1e6c770 --- /dev/null +++ b/src/snp/connections/lsquic/log.hpp @@ -0,0 +1,22 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +namespace jam::snp::lsquic { + inline void log() { + static lsquic_logger_if log{ + +[](void *, const char *buf, size_t len) { + return (int)fwrite(buf, sizeof(char), len, stdout); + }, + }; + lsquic_logger_init(&log, nullptr, LLTS_HHMMSSMS); + lsquic_set_log_level("debug"); + } +} // namespace jam::snp::lsquic diff --git a/src/snp/connections/message_size.hpp b/src/snp/connections/message_size.hpp new file mode 100644 index 0000000..34837e2 --- /dev/null +++ b/src/snp/connections/message_size.hpp @@ -0,0 +1,15 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace jam::snp { + // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L109-L111 + using MessageSize = uint32_t; + constexpr MessageSize kMessageSizeMax = UINT32_MAX; +} // namespace jam::snp diff --git a/src/snp/connections/port.hpp b/src/snp/connections/port.hpp new file mode 100644 index 0000000..8f93034 --- /dev/null +++ b/src/snp/connections/port.hpp @@ -0,0 +1,13 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace jam::snp { + using Port = uint16_t; +} // namespace jam::snp diff --git a/src/snp/connections/prefer_key.cpp b/src/snp/connections/prefer_key.cpp new file mode 100644 index 0000000..0326594 --- /dev/null +++ b/src/snp/connections/prefer_key.cpp @@ -0,0 +1,13 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "snp/connections/prefer_key.hpp" + +namespace jam::snp { + bool prefer_key(const Key &a, const Key &b) { + return ((a[31] > 127) != (b[31] > 127)) != (a < b); + } +} // namespace jam::snp diff --git a/src/snp/connections/prefer_key.hpp b/src/snp/connections/prefer_key.hpp new file mode 100644 index 0000000..8dc6c0b --- /dev/null +++ b/src/snp/connections/prefer_key.hpp @@ -0,0 +1,17 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "snp/connections/key.hpp" + +namespace jam::snp { + // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L52-L62 + /** + * Is first key preferred over second. + */ + bool prefer_key(const Key &a, const Key &b); +} // namespace jam::snp diff --git a/src/snp/connections/protocol_id.cpp b/src/snp/connections/protocol_id.cpp new file mode 100644 index 0000000..a234ea6 --- /dev/null +++ b/src/snp/connections/protocol_id.cpp @@ -0,0 +1,19 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "snp/connections/protocol_id.hpp" + +#include "snp/connections/error.hpp" + +namespace jam::snp { + outcome::result ProtocolId::make(Id id, bool unique) { + ProtocolId protocol_id{id}; + if (unique != protocol_id.unique()) { + return ConnectionsError::PROTOCOL_ID_MAKE_INVALID; + } + return protocol_id; + } +} // namespace jam::snp diff --git a/src/snp/connections/protocol_id.hpp b/src/snp/connections/protocol_id.hpp new file mode 100644 index 0000000..be32dac --- /dev/null +++ b/src/snp/connections/protocol_id.hpp @@ -0,0 +1,54 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include +#include + +namespace jam::snp { + class Stream; +} // namespace jam::snp + +namespace jam::snp { + // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L87-L101 + class ProtocolId { + friend Stream; + + using Id = uint8_t; + + ProtocolId(Id id) : id_{id} {} + + public: + /** + * Construct protocol with specified `id`. + * Check expected `unique` consistency with `id` range. + */ + static outcome::result make(Id id, bool unique); + + auto &id() const { + return id_; + } + + bool unique() const { + return id() < 128; + } + + auto operator<=>(const ProtocolId &) const = default; + + private: + Id id_; + }; +} // namespace jam::snp + +template <> +struct std::hash { + size_t operator()(const jam::snp::ProtocolId &v) const { + return qtils::stdHashOf(v.id()); + } +}; diff --git a/src/snp/connections/stream.cpp b/src/snp/connections/stream.cpp new file mode 100644 index 0000000..0addfae --- /dev/null +++ b/src/snp/connections/stream.cpp @@ -0,0 +1,110 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "snp/connections/stream.hpp" + +#include +#include +#include +#include + +#include "coro/set_thread.hpp" +#include "coro/weak.hpp" +#include "snp/connections/error.hpp" +#include "snp/connections/lsquic/engine.hpp" + +namespace jam::snp { + using lsquic::Engine; + + using ProtocolIdBytes = qtils::BytesN<1>; + using MessageSizeBytes = qtils::BytesN<4>; + + Stream::Stream(IoContextPtr io_context_ptr, + ConnectionPtr connection, + lsquic::StreamCtx *stream_ctx) + : MOVE_(io_context_ptr), MOVE_(connection), MOVE_(stream_ctx) {} + + Stream::~Stream() { + boost::asio::dispatch(*io_context_ptr_, [stream_ctx{stream_ctx_}] { + Engine::destroyStream(stream_ctx); + }); + } + + CoroOutcome Stream::read(Self self, + qtils::Bytes &buffer, + MessageSize max) { + SET_CORO_THREAD(self->io_context_ptr_); + MessageSizeBytes size_bytes; + BOOST_OUTCOME_CO_TRY( + auto read_size, + CORO_WEAK_AWAIT(self, + Engine::streamReadRaw(self->stream_ctx_, size_bytes), + ConnectionsError::STREAM_READ_DESTROYED)); + if (not read_size) { + co_return false; + } + auto size = boost::endian::load_little_u32(size_bytes.data()); + if (size > max) { + co_return ConnectionsError::STREAM_READ_TOO_BIG; + } + buffer.resize(size); + BOOST_OUTCOME_CO_TRY( + auto read_message, + CORO_WEAK_AWAIT(self, + Engine::streamReadRaw(self->stream_ctx_, buffer), + ConnectionsError::STREAM_READ_DESTROYED)); + if (not read_message) { + co_return ConnectionsError::STREAM_READ_CLOSED; + } + co_return true; + } + + Coro Stream::readFin(Self self) { + SET_CORO_THREAD(self->io_context_ptr_); + Engine::streamReadFin(self->stream_ctx_); + co_return; + } + + CoroOutcome Stream::write(Self self, qtils::BytesIn message) { + SET_CORO_THREAD(self->io_context_ptr_); + MessageSizeBytes size_bytes; + auto size = message.size(); + if (size > kMessageSizeMax) { + throw std::logic_error{"Stream::write max"}; + } + boost::endian::store_little_u32(size_bytes.data(), size); + BOOST_OUTCOME_CO_TRY( + CORO_WEAK_AWAIT(self, + Engine::streamWriteRaw(self->stream_ctx_, size_bytes), + ConnectionsError::STREAM_WRITE_DESTROYED)); + BOOST_OUTCOME_CO_TRY( + CORO_WEAK_AWAIT(self, + Engine::streamWriteRaw(self->stream_ctx_, message), + ConnectionsError::STREAM_WRITE_DESTROYED)); + co_return outcome::success(); + } + + Coro Stream::writeFin(Self self) { + SET_CORO_THREAD(self->io_context_ptr_); + Engine::streamWriteFin(self->stream_ctx_); + co_return; + } + + CoroOutcome Stream::readProtocolId() { + ProtocolIdBytes bytes; + BOOST_OUTCOME_CO_TRY(auto read, + co_await Engine::streamReadRaw(stream_ctx_, bytes)); + if (not read) { + co_return ConnectionsError::STREAM_READ_PROTOCOL_ID_CLOSED; + } + co_return ProtocolId{bytes[0]}; + } + + CoroOutcome Stream::writeProtocolId(ProtocolId protocol_id) { + ProtocolIdBytes bytes{protocol_id.id()}; + co_return co_await Engine::streamWriteRaw(stream_ctx_, bytes); + } +} // namespace jam::snp diff --git a/src/snp/connections/stream.hpp b/src/snp/connections/stream.hpp new file mode 100644 index 0000000..25b9161 --- /dev/null +++ b/src/snp/connections/stream.hpp @@ -0,0 +1,84 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "coro/coro.hpp" +#include "coro/io_context_ptr.hpp" +#include "snp/connections/connection_ptr.hpp" +#include "snp/connections/message_size.hpp" +#include "snp/connections/protocol_id.hpp" + +namespace jam::snp::lsquic { + struct StreamCtx; + class Engine; +} // namespace jam::snp::lsquic + +namespace jam::snp { + class Stream { + friend lsquic::Engine; + + public: + using Self = std::shared_ptr; + + Stream(IoContextPtr io_context_ptr, + ConnectionPtr connection, + lsquic::StreamCtx *stream_ctx); + /** + * Will close stream and decrement `Connection` shared use count. + */ + ~Stream(); + + // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L109-L111 + /** + * Read whole size prefixed message, no more than `max` bytes. + * Returns `true` if message was read, or `false` if fin was received or + * stream was closed. + */ + static CoroOutcome read(Self self, + qtils::Bytes &buffer, + MessageSize max); + + /** + * Close reading side of stream. + */ + static Coro readFin(Self self); + + // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L109-L111 + /** + * Write while size prefixed message. + */ + static CoroOutcome write(Self self, qtils::BytesIn message); + + /** + * Write fin. + * Closes writing side of stream. + */ + static Coro writeFin(Self self); + + private: + /** + * Read protocol id (server). + */ + CoroOutcome readProtocolId(); + /** + * Write protocol id (client). + */ + CoroOutcome writeProtocolId(ProtocolId protocol_id); + + /** + * `Stream`, `Engine` operations executed on one `IoContextPtr` thread. + */ + IoContextPtr io_context_ptr_; + /** + * `Stream` keeps `Connection` shared use count alive. + */ + ConnectionPtr connection_; + lsquic::StreamCtx *stream_ctx_; + }; +} // namespace jam::snp diff --git a/src/snp/connections/stream_ptr.hpp b/src/snp/connections/stream_ptr.hpp new file mode 100644 index 0000000..737f465 --- /dev/null +++ b/src/snp/connections/stream_ptr.hpp @@ -0,0 +1,20 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "coro/coro.hpp" + +namespace jam::snp { + class Stream; +} // namespace jam::snp + +namespace jam::snp { + using StreamPtr = std::shared_ptr; + using StreamPtrCoroOutcome = CoroOutcome; +} // namespace jam::snp diff --git a/src/snp/connections/tls_certificate.cpp b/src/snp/connections/tls_certificate.cpp new file mode 100644 index 0000000..f28aa2a --- /dev/null +++ b/src/snp/connections/tls_certificate.cpp @@ -0,0 +1,105 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "snp/connections/tls_certificate.hpp" + +#include +#include +#include + +#include "snp/connections/alpn.hpp" +#include "snp/connections/config.hpp" +#include "snp/connections/dns_name.hpp" +#include "snp/connections/error.hpp" + +namespace jam::snp { + outcome::result set_relative_time(ASN1_TIME *o_time, auto delta) { + if (not X509_gmtime_adj( + o_time, + std::chrono::duration_cast(delta).count())) { + return OpenSslError::X509_gmtime_adj; + } + return outcome::success(); + } + + TlsCertificate::TlsCertificate(const ConnectionsConfig &config) + : MAKE_SHARED_(alpn_, config.genesis), + MAKE_SHARED_(context_, Context::tlsv13) {} + + outcome::result TlsCertificate::make( + const ConnectionsConfig &config) { + TlsCertificate self{config}; + OUTCOME_TRY(self.alpn_->set(self)); + self.context_->set_verify_mode(Context::verify_peer + | Context::verify_fail_if_no_peer_cert + | Context::verify_client_once); + self.context_->set_verify_callback(verify); + std::array prefs{SSL_SIGN_ED25519}; + if (not SSL_CTX_set_signing_algorithm_prefs( + self, prefs.data(), prefs.size())) { + return OpenSslError::SSL_CTX_set_signing_algorithm_prefs; + } + if (not SSL_CTX_set_verify_algorithm_prefs( + self, prefs.data(), prefs.size())) { + return OpenSslError::SSL_CTX_set_verify_algorithm_prefs; + } + auto secret = crypto::ed25519::get_secret(config.keypair); + // `EVP_PKEY_new_raw_private_key` requires seed, but in ed25519 secret=seed + bssl::UniquePtr pkey(EVP_PKEY_new_raw_private_key( + EVP_PKEY_ED25519, nullptr, secret.data(), secret.size())); + if (not pkey) { + return OpenSslError::EVP_PKEY_new_raw_private_key; + } + if (not SSL_CTX_use_PrivateKey(self, pkey.get())) { + return OpenSslError::SSL_CTX_use_PrivateKey; + } + + bssl::UniquePtr x509(X509_new()); + OUTCOME_TRY(set_relative_time(X509_getm_notBefore(x509.get()), + -std::chrono::days{1})); + OUTCOME_TRY(set_relative_time(X509_getm_notAfter(x509.get()), + std::chrono::years{1})); + if (not X509_set_pubkey(x509.get(), pkey.get())) { + return OpenSslError::X509_set_pubkey; + } + OUTCOME_TRY( + DnsName{crypto::ed25519::get_public(config.keypair)}.set(x509.get())); + if (not X509_sign(x509.get(), pkey.get(), nullptr)) { + return OpenSslError::X509_sign; + } + if (not SSL_CTX_use_certificate(self, x509.get())) { + return OpenSslError::SSL_CTX_use_certificate; + } + return self; + } + + TlsCertificate::operator ssl_ctx_st *() const { + return context_->native_handle(); + } + + outcome::result TlsCertificate::get_key(ssl_st *ssl) { + bssl::UniquePtr x509(SSL_get_peer_certificate(ssl)); + if (not x509) { + return OpenSslError::SSL_get_peer_certificate; + } + bssl::UniquePtr pkey(X509_get_pubkey(x509.get())); + if (not pkey) { + return OpenSslError::X509_get_pubkey; + } + Key key; + size_t key_size = key.size(); + if (not EVP_PKEY_get_raw_public_key(pkey.get(), key.data(), &key_size)) { + return OpenSslError::EVP_PKEY_get_raw_public_key; + } + return key; + } + + bool TlsCertificate::verify(bool, boost::asio::ssl::verify_context &ctx) { + X509_STORE_CTX *store_ctx = ctx.native_handle(); + // TODO(turuslan): DnsName(key) == cert.alt + return true; + } +} // namespace jam::snp diff --git a/src/snp/connections/tls_certificate.hpp b/src/snp/connections/tls_certificate.hpp new file mode 100644 index 0000000..bd5a2e5 --- /dev/null +++ b/src/snp/connections/tls_certificate.hpp @@ -0,0 +1,61 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include + +#include "snp/connections/key.hpp" + +struct ssl_st; + +namespace boost::asio::ssl { + class context; + class verify_context; +} // namespace boost::asio::ssl + +namespace jam::snp { + struct ConnectionsConfig; + class Alpn; +} // namespace jam::snp + +struct ssl_ctx_st; + +namespace jam::snp { + class TlsCertificate { + TlsCertificate(const ConnectionsConfig &config); + + public: + /** + * Generate self-signed tls certificate. + */ + static outcome::result make( + const ConnectionsConfig &config); + + /** + * Allows passing `*this` to openssl functions. + */ + operator ssl_ctx_st *() const; + + /** + * Get peer key from tls certificate. + */ + static outcome::result get_key(ssl_st *ssl); + + private: + using Context = boost::asio::ssl::context; + + static bool verify(bool, boost::asio::ssl::verify_context &ctx); + + /** + * Keeps `Alpn` alive for `SSL_CTX_set_alpn_select_cb`. + */ + std::shared_ptr alpn_; + std::shared_ptr context_; + }; +} // namespace jam::snp diff --git a/src/snp/example_chat.cpp b/src/snp/example_chat.cpp new file mode 100644 index 0000000..f8ae088 --- /dev/null +++ b/src/snp/example_chat.cpp @@ -0,0 +1,241 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "coro/spawn.hpp" +#include "snp/connections/address.hpp" +#include "snp/connections/connection.hpp" +#include "snp/connections/connections.hpp" +#include "snp/connections/controller.hpp" +#include "snp/connections/stream.hpp" + +using jam::Coro; +using jam::coroHandler; +using jam::CoroHandler; +using jam::CoroOutcome; +using jam::coroSpawn; +using jam::GenesisHash; +using jam::IoContextPtr; +using jam::crypto::ed25519::KeyPair; +using jam::snp::Address; +using jam::snp::ConnectionInfo; +using jam::snp::Connections; +using jam::snp::ConnectionsConfig; +using jam::snp::ConnectionsController; +using jam::snp::Key; +using jam::snp::Port; +using jam::snp::ProtocolId; +using jam::snp::StreamPtr; + +inline auto operator""_ed25519(const char *c, size_t s) { + auto seed = qtils::unhex({c, s}).value(); + return jam::crypto::ed25519::from_seed(seed); +} + +std::vector keys{ + "f8dfdb0f1103d9fb2905204ac32529d5f148761c4321b2865b0a40e15be75f57"_ed25519, + "96c891b8726cb18c781aefc082dbafcb827e16c8f18f22d461e83eabd618e780"_ed25519, + "619d5e68139f714ee8e7892ce5afd8fbe7a4172a675fea5c5a06fb94fe3d797d"_ed25519, + "8d0c5f498a763eaa8c04861cac06289784140b4bbfa814fef898f1f4095de4a3"_ed25519, +}; +Address server_address{ + Address::kLocal, + 10000, + jam::crypto::ed25519::get_public(keys[0]), +}; +ProtocolId protocol_id = ProtocolId::make(0, true).value(); + +size_t indexOfKey(const Key &key) { + auto it = std::ranges::find_if(keys, [&](const KeyPair &keypair) { + return jam::crypto::ed25519::get_public(keypair) == key; + }); + if (it == keys.end()) { + throw std::logic_error{"TODO: example"}; + } + return it - keys.begin(); +} + +struct ChatController : ConnectionsController { + static constexpr size_t kMaxMsg = 8; + + struct Writer { + StreamPtr stream; + std::deque queue; + bool writing = false; + }; + using WriterPtr = std::shared_ptr; + + std::map writers; + + static CoroOutcome write(WriterPtr writer, + size_t i_msg, + const std::string msg) { + qtils::Bytes buffer; + buffer.emplace_back(i_msg); + qtils::append(buffer, qtils::str2byte(msg)); + writer->queue.emplace_back(buffer); + if (writer->writing) { + co_return outcome::success(); + } + writer->writing = true; + while (not writer->queue.empty()) { + auto buffer = writer->queue.front(); + writer->queue.pop_front(); + BOOST_OUTCOME_CO_TRY( + co_await writer->stream->write(writer->stream, buffer)); + } + writer->writing = false; + co_return outcome::success(); + } + + void onOpen(Key key) override { + fmt::println("#{} (connected)", indexOfKey(key)); + } + + void onClose(Key key) override { + fmt::println("#{} (disconnected)", indexOfKey(key)); + } + + void print(size_t i_msg, std::string msg) { + fmt::println("#{} > {}", i_msg, msg); + } + + Coro broadcast(std::optional i_read, + size_t i_msg, + std::string msg) { + for (auto &[i_write, writer] : writers) { + if (i_write == i_read) { + continue; + } + co_await coroSpawn([this, i_write, writer, i_msg, msg]() -> Coro { + if (not co_await write(writer, i_msg, msg)) { + writers.erase(i_write); + } + }); + } + } + + Coro onRead(size_t i_read, size_t i_msg, std::string msg) { + print(i_msg, msg); + co_await broadcast(i_read, i_msg, msg); + } + + CoroOutcome add(ConnectionInfo info, StreamPtr stream) { + auto i_read = indexOfKey(info.key); + writers.emplace(i_read, std::make_shared(Writer{stream})); + qtils::Bytes buffer; + while (true) { + BOOST_OUTCOME_CO_TRY(auto read, + co_await stream->read(stream, buffer, 1 + kMaxMsg)); + if (not read) { + break; + } + if (buffer.size() < 1) { + break; + } + auto i_msg = buffer[0]; + co_await onRead( + i_read, i_msg, std::string{qtils::byte2str(buffer).substr(1)}); + } + co_await stream->readFin(stream); + co_return outcome::success(); + } +}; + +struct Input { + Input(IoContextPtr io_context_ptr) : fd_{*io_context_ptr, STDIN_FILENO} {} + + Coro> read() { + auto [ec, n] = co_await boost::asio::async_read_until( + fd_, buf_, "\n", boost::asio::as_tuple(boost::asio::use_awaitable)); + if (ec) { + co_return std::nullopt; + } + auto s = qtils::byte2str(qtils::asioBuffer(buf_.data())); + auto i = s.find("\n"); + if (i != s.npos) { + s = s.substr(0, i); + } + auto r = std::string{s}; + buf_.consume(buf_.size()); + co_return r; + } + + boost::asio::posix::stream_descriptor fd_; + boost::asio::streambuf buf_; +}; + +CoroOutcome co_main(IoContextPtr io_context_ptr, size_t arg_i) { + fmt::println("#{} (self)", arg_i); + + std::optional listen_port; + GenesisHash genesis; + ConnectionsConfig config{genesis, keys.at(arg_i)}; + auto is_server = arg_i == 0; + if (is_server) { + config.listen_port = server_address.port; + } + auto connections = std::make_shared(io_context_ptr, config); + auto chat = std::make_shared(); + BOOST_OUTCOME_CO_TRY(co_await connections->init(connections, chat)); + co_await coroSpawn([io_context_ptr, arg_i, chat]() -> Coro { + Input input{io_context_ptr}; + while (true) { + auto msg = co_await input.read(); + if (not msg) { + break; + } + msg->resize(std::min(msg->size(), ChatController::kMaxMsg)); + if (msg->empty()) { + continue; + } + co_await chat->broadcast(std::nullopt, arg_i, *msg); + } + io_context_ptr->stop(); + }); + if (not is_server) { + BOOST_OUTCOME_CO_TRY( + auto connection, + co_await connections->connect(connections, server_address)); + BOOST_OUTCOME_CO_TRY(auto stream, + co_await connection->open(connection, protocol_id)); + std::ignore = co_await chat->add(connection->info(), stream); + fmt::println("(disconnected)"); + io_context_ptr->stop(); + } else { + co_await connections->serve( + connections, + protocol_id, + [chat](ConnectionInfo info, StreamPtr stream) -> CoroOutcome { + co_return co_await chat->add(info, stream); + }); + std::optional> work_guard; + co_await coroHandler([&](CoroHandler &&handler) { + work_guard.emplace(std::move(handler)); + }); + } + co_return outcome::success(); +} + +int main(int argc, char **argv) { + setvbuf(stdout, nullptr, _IONBF, 0); + setvbuf(stderr, nullptr, _IONBF, 0); + + size_t arg_i = 0; + if (argc == 2) { + arg_i = std::atoi(argv[1]); + } + + auto io_context_ptr = std::make_shared(); + coroSpawn(*io_context_ptr, [io_context_ptr, arg_i]() -> Coro { + (co_await co_main(io_context_ptr, arg_i)).value(); + }); + io_context_ptr->run(); +} diff --git a/src/types/genesis_hash.hpp b/src/types/genesis_hash.hpp new file mode 100644 index 0000000..8c79800 --- /dev/null +++ b/src/types/genesis_hash.hpp @@ -0,0 +1,13 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace jam { + using GenesisHash = qtils::BytesN<32>; +} // namespace jam diff --git a/test-vectors/asn1.cmake b/test-vectors/asn1.cmake index 052640a..f03ed29 100644 --- a/test-vectors/asn1.cmake +++ b/test-vectors/asn1.cmake @@ -135,7 +135,6 @@ function(add_test_vector name) target_link_libraries(${TEST_VECTOR}__transition_test fmt::fmt ${GTEST_DEPS} - headers ${TEST_VECTOR}__types ) add_test(${TEST_VECTOR}__transition_test ${TEST_VECTOR}__transition_test) diff --git a/vcpkg-overlay/cppcodec.cmake b/vcpkg-overlay/cppcodec.cmake new file mode 100644 index 0000000..c21ba7c --- /dev/null +++ b/vcpkg-overlay/cppcodec.cmake @@ -0,0 +1,4 @@ + +find_path(CPPCODEC_INCLUDE_DIRS "cppcodec/base32_crockford.hpp") +add_library(cppcodec INTERFACE) +target_include_directories(cppcodec INTERFACE ${CPPCODEC_INCLUDE_DIRS}) diff --git a/vcpkg-overlay/liblsquic/disable-asan.patch b/vcpkg-overlay/liblsquic/disable-asan.patch new file mode 100644 index 0000000..2b05d0e --- /dev/null +++ b/vcpkg-overlay/liblsquic/disable-asan.patch @@ -0,0 +1,23 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 65c4776..5d4086a 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -60,12 +60,12 @@ ENDIF() + + IF(CMAKE_BUILD_TYPE STREQUAL "Debug") + SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -O0 -g3") +- IF(CMAKE_C_COMPILER MATCHES "clang" AND +- NOT "$ENV{TRAVIS}" MATCHES "^true$" AND +- NOT "$ENV{EXTRA_CFLAGS}" MATCHES "-fsanitize") +- SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -fsanitize=address") +- SET(LIBS ${LIBS} -fsanitize=address) +- ENDIF() ++ # IF(CMAKE_C_COMPILER MATCHES "clang" AND ++ # NOT "$ENV{TRAVIS}" MATCHES "^true$" AND ++ # NOT "$ENV{EXTRA_CFLAGS}" MATCHES "-fsanitize") ++ # SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -fsanitize=address") ++ # SET(LIBS ${LIBS} -fsanitize=address) ++ # ENDIF() + # Uncomment to enable cleartext protocol mode (no crypto): + #SET (MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -DLSQUIC_ENABLE_HANDSHAKE_DISABLE=1") + ELSE() diff --git a/vcpkg-overlay/liblsquic/fix-found-boringssl.patch b/vcpkg-overlay/liblsquic/fix-found-boringssl.patch new file mode 100644 index 0000000..a3a632c --- /dev/null +++ b/vcpkg-overlay/liblsquic/fix-found-boringssl.patch @@ -0,0 +1,53 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 5d4086a..e085a83 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -120,10 +120,12 @@ IF(CMAKE_BUILD_TYPE STREQUAL "Debug") + SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -Od") + #SET (MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -DFIU_ENABLE=1") + #SET(LIBS ${LIBS} fiu) ++ SET(LIB_NAME ssld cryptod) + ELSE() + SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -Ox") + # Comment out the following line to compile out debug messages: + #SET(MY_CMAKE_FLAGS "${MY_CMAKE_FLAGS} -DLSQUIC_LOWEST_LOG_LEVEL=LSQ_LOG_INFO") ++ SET(LIB_NAME ssl crypto) + ENDIF() + + ENDIF() #MSVC +@@ -191,7 +193,7 @@ IF (NOT DEFINED BORINGSSL_LIB AND DEFINED BORINGSSL_DIR) + ELSE() + + +- FOREACH(LIB_NAME ssl crypto) ++ FOREACH(LIB ${LIB_NAME}) + # If BORINGSSL_LIB is defined, try find each lib. Otherwise, user should define BORINGSSL_LIB_ssl, + # BORINGSSL_LIB_crypto and so on explicitly. For example, including boringssl and lsquic both via + # add_subdirectory: +@@ -201,20 +203,20 @@ ELSE() + # add_subdirectory(third_party/lsquic) + IF (DEFINED BORINGSSL_LIB) + IF (CMAKE_SYSTEM_NAME STREQUAL Windows) +- FIND_LIBRARY(BORINGSSL_LIB_${LIB_NAME} +- NAMES ${LIB_NAME} ++ FIND_LIBRARY(BORINGSSL_LIB_${LIB} ++ NAMES ${LIB} + PATHS ${BORINGSSL_LIB} + PATH_SUFFIXES Debug Release MinSizeRel RelWithDebInfo + NO_DEFAULT_PATH) + ELSE() +- FIND_LIBRARY(BORINGSSL_LIB_${LIB_NAME} +- NAMES lib${LIB_NAME}${LIB_SUFFIX} ++ FIND_LIBRARY(BORINGSSL_LIB_${LIB} ++ NAMES lib${LI}${LIB_SUFFIX} + PATHS ${BORINGSSL_LIB} +- PATH_SUFFIXES ${LIB_NAME} ++ PATH_SUFFIXES ${LIB} + NO_DEFAULT_PATH) + ENDIF() + ENDIF() +- IF(BORINGSSL_LIB_${LIB_NAME}) ++ IF(BORINGSSL_LIB_${LIB}) + MESSAGE(STATUS "Found ${LIB_NAME} library: ${BORINGSSL_LIB_${LIB_NAME}}") + ELSE() + MESSAGE(FATAL_ERROR "BORINGSSL_LIB_${LIB_NAME} library not found") diff --git a/vcpkg-overlay/liblsquic/lsquic_conn_ssl.patch b/vcpkg-overlay/liblsquic/lsquic_conn_ssl.patch new file mode 100644 index 0000000..ae7be54 --- /dev/null +++ b/vcpkg-overlay/liblsquic/lsquic_conn_ssl.patch @@ -0,0 +1,80 @@ +diff --git a/include/lsquic.h b/include/lsquic.h +index 389fbcc..c38d027 100644 +--- a/include/lsquic.h ++++ b/include/lsquic.h +@@ -1671,6 +1671,10 @@ int lsquic_stream_close(lsquic_stream_t *s); + int + lsquic_stream_has_unacked_data (lsquic_stream_t *s); + ++/* Return SSL object associated with this connection */ ++struct ssl_st * ++lsquic_conn_ssl(struct lsquic_conn *conn); ++ + /** + * Get certificate chain returned by the server. This can be used for + * server certificate verification. +diff --git a/src/liblsquic/lsquic_conn.c b/src/liblsquic/lsquic_conn.c +index f76550d..31e5285 100644 +--- a/src/liblsquic/lsquic_conn.c ++++ b/src/liblsquic/lsquic_conn.c +@@ -128,6 +128,12 @@ lsquic_conn_crypto_alg_keysize (const lsquic_conn_t *lconn) + } + + ++struct ssl_st * ++lsquic_conn_ssl(struct lsquic_conn *lconn) { ++ return lconn->cn_esf_c->esf_get_ssl(lconn->cn_enc_session); ++} ++ ++ + struct stack_st_X509 * + lsquic_conn_get_server_cert_chain (struct lsquic_conn *lconn) + { +diff --git a/src/liblsquic/lsquic_enc_sess.h b/src/liblsquic/lsquic_enc_sess.h +index f45c15f..3505fbd 100644 +--- a/src/liblsquic/lsquic_enc_sess.h ++++ b/src/liblsquic/lsquic_enc_sess.h +@@ -115,6 +115,9 @@ struct enc_session_funcs_common + (*esf_decrypt_packet)(enc_session_t *, struct lsquic_engine_public *, + const struct lsquic_conn *, struct lsquic_packet_in *); + ++ struct ssl_st * ++ (*esf_get_ssl)(enc_session_t *); ++ + struct stack_st_X509 * + (*esf_get_server_cert_chain) (enc_session_t *); + +diff --git a/src/liblsquic/lsquic_enc_sess_ietf.c b/src/liblsquic/lsquic_enc_sess_ietf.c +index 66329c1..076c4c5 100644 +--- a/src/liblsquic/lsquic_enc_sess_ietf.c ++++ b/src/liblsquic/lsquic_enc_sess_ietf.c +@@ -2519,6 +2519,13 @@ iquic_esf_global_cleanup (void) + } + + ++static struct ssl_st * ++iquic_esf_get_ssl(enc_session_t *enc_session_p) { ++ struct enc_sess_iquic *const enc_sess = enc_session_p; ++ return enc_sess->esi_ssl; ++} ++ ++ + static struct stack_st_X509 * + iquic_esf_get_server_cert_chain (enc_session_t *enc_session_p) + { +@@ -2744,6 +2751,7 @@ const struct enc_session_funcs_common lsquic_enc_session_common_ietf_v1 = + .esf_global_cleanup = iquic_esf_global_cleanup, + .esf_global_init = iquic_esf_global_init, + .esf_tag_len = IQUIC_TAG_LEN, ++ .esf_get_ssl = iquic_esf_get_ssl, + .esf_get_server_cert_chain + = iquic_esf_get_server_cert_chain, + .esf_get_sni = iquic_esf_get_sni, +@@ -2763,6 +2771,7 @@ const struct enc_session_funcs_common lsquic_enc_session_common_ietf_v1_no_flush + .esf_global_cleanup = iquic_esf_global_cleanup, + .esf_global_init = iquic_esf_global_init, + .esf_tag_len = IQUIC_TAG_LEN, ++ .esf_get_ssl = iquic_esf_get_ssl, + .esf_get_server_cert_chain + = iquic_esf_get_server_cert_chain, + .esf_get_sni = iquic_esf_get_sni, diff --git a/vcpkg-overlay/liblsquic/portfile.cmake b/vcpkg-overlay/liblsquic/portfile.cmake new file mode 100644 index 0000000..3602c59 --- /dev/null +++ b/vcpkg-overlay/liblsquic/portfile.cmake @@ -0,0 +1,78 @@ +if(VCPKG_TARGET_IS_WINDOWS) + # The lib uses CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS, at least until + # https://github.com/litespeedtech/lsquic/pull/371 or similar is merged + vcpkg_check_linkage(ONLY_STATIC_LIBRARY) +endif() + +vcpkg_from_github(OUT_SOURCE_PATH SOURCE_PATH + REPO litespeedtech/lsquic + REF v${VERSION} + SHA512 40d742779bfa2dc6fdaf0ee8e9349498d373dcffcc6dd27867c18d87309a288ea6811d693043b5d98364d816b818b49445214497475844201241193c0f37b349 + HEAD_REF master + PATCHES + disable-asan.patch + fix-found-boringssl.patch + lsquic_conn_ssl.patch +) + +# Submodules +vcpkg_from_github(OUT_SOURCE_PATH LSQPACK_SOURCE_PATH + REPO litespeedtech/ls-qpack + REF v2.5.3 + HEAD_REF master + SHA512 f90502c763abc84532f33d1b8f952aea7869e4e0c5f6bd344532ddd51c4a180958de4086d88b9ec96673a059c806eec9e70007651d4d4e1a73395919dee47ce0 +) +if(NOT EXISTS "${SOURCE_PATH}/src/ls-hpack/CMakeLists.txt") + file(REMOVE_RECURSE "${SOURCE_PATH}/src/liblsquic/ls-qpack") + file(RENAME "${LSQPACK_SOURCE_PATH}" "${SOURCE_PATH}/src/liblsquic/ls-qpack") +endif() + +vcpkg_from_github(OUT_SOURCE_PATH LSHPACK_SOURCE_PATH + REPO litespeedtech/ls-hpack + REF v2.3.2 + HEAD_REF master + SHA512 45d6c8296e8eee511e6a083f89460d5333fc9a49bc078dac55fdec6c46db199de9f150379f02e054571f954a5e3c79af3864dbc53dc57d10a8d2ed26a92d4278 +) +if(NOT EXISTS "${SOURCE_PATH}/src/lshpack/CMakeLists.txt") + file(REMOVE_RECURSE "${SOURCE_PATH}/src/lshpack") + file(RENAME "${LSHPACK_SOURCE_PATH}" "${SOURCE_PATH}/src/lshpack") +endif() + +# Configuration +vcpkg_find_acquire_program(PERL) + +string(COMPARE EQUAL "${VCPKG_LIBRARY_LINKAGE}" "dynamic" LSQUIC_SHARED_LIB) + +vcpkg_cmake_configure( + SOURCE_PATH "${SOURCE_PATH}" + OPTIONS + "-DPERL=${PERL}" + "-DPERL_EXECUTABLE=${PERL}" + "-DLSQUIC_SHARED_LIB=${LSQUIC_SHARED_LIB}" + "-DBORINGSSL_INCLUDE=${CURRENT_INSTALLED_DIR}/include" + -DLSQUIC_BIN=OFF + -DLSQUIC_TESTS=OFF + OPTIONS_RELEASE + "-DBORINGSSL_LIB=${CURRENT_INSTALLED_DIR}/lib" + OPTIONS_DEBUG + "-DBORINGSSL_LIB=${CURRENT_INSTALLED_DIR}/debug/lib" + -DLSQUIC_DEVEL=ON +) + +vcpkg_cmake_install() +if(VCPKG_TARGET_IS_WINDOWS) + # Upstream removed installation of this header after merging changes + file(INSTALL "${SOURCE_PATH}/wincompat/vc_compat.h" DESTINATION "${CURRENT_INSTALLED_DIR}/include/lsquic") +endif() + +vcpkg_cmake_config_fixup(PACKAGE_NAME lsquic) + +# Concatenate license files and install +vcpkg_install_copyright(FILE_LIST + "${SOURCE_PATH}/LICENSE" + "${SOURCE_PATH}/LICENSE.chrome" +) + +# Remove duplicated include directory +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") + diff --git a/vcpkg-overlay/liblsquic/vcpkg.json b/vcpkg-overlay/liblsquic/vcpkg.json new file mode 100644 index 0000000..ec90032 --- /dev/null +++ b/vcpkg-overlay/liblsquic/vcpkg.json @@ -0,0 +1,25 @@ +{ + "name": "liblsquic", + "version": "3.3.2", + "port-version": 1, + "description": "An implementation of the QUIC and HTTP/3 protocols.", + "homepage": "https://github.com/litespeedtech/lsquic", + "license": "MIT AND BSD-3-Clause", + "supports": "!x86", + "dependencies": [ + "boringssl", + { + "name": "getopt", + "platform": "windows" + }, + { + "name": "vcpkg-cmake", + "host": true + }, + { + "name": "vcpkg-cmake-config", + "host": true + }, + "zlib" + ] +} diff --git a/vcpkg-overlay/scale/portfile.cmake b/vcpkg-overlay/scale/portfile.cmake index 4addb69..253b1b2 100644 --- a/vcpkg-overlay/scale/portfile.cmake +++ b/vcpkg-overlay/scale/portfile.cmake @@ -2,8 +2,8 @@ vcpkg_check_linkage(ONLY_STATIC_LIBRARY) vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO qdrvm/scale-codec-cpp - REF 6ecc0c67eadacc9ab8acdeb73488bdc4c6e30d1c - SHA512 22267674113f04fb9c4624bd2cb69e6f4053edd129bfa5ffb83136e4ce2c04f6fe3dce9c729d5df08c84fd92a8a69938c249fa5068132ebc4a42d205142fb921 + REF 6d80d0f4aa52c86cf2875c77a94583a0a4069626 + SHA512 0472caa6804f5ee693c6de577e78fc07a77d12aabe061e4ac9dce4957b18e54a8814cf3555adbaa4bf54f9c0c9023c28be7fdac54ac511006a2b98bedac30f30 ) vcpkg_cmake_configure( SOURCE_PATH "${SOURCE_PATH}" diff --git a/vcpkg.json b/vcpkg.json index c3dbc24..ea9826f 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -2,19 +2,21 @@ "name": "cpp-jam", "version": "0.0.1", "dependencies": [ - "qtils", - "scale", + "boost-asio", + "boost-beast", + "boost-di", + "boost-program-options", + "cppcodec", "fmt", - "soralog", "kagome-crates", "libb2", - "boost-di", - "boost-program-options", - "boost-asio", - "boost-beast", - "prometheus-cpp" + "liblsquic", + "prometheus-cpp", + "qtils", + "scale", + "soralog" ], "features": { - "test": { "description": "Test", "dependencies": ["gtest"]} + "test": { "description": "Test", "dependencies": ["gtest"] } } } From bffc90bae3237879739ba569cffeb7a6f7890904 Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 17 Feb 2025 12:05:38 +0500 Subject: [PATCH 05/40] forward Signed-off-by: turuslan --- src/TODO_qtils/macro/forward.hpp | 11 ---------- src/coro/spawn.hpp | 36 +++++++++++++++++--------------- 2 files changed, 19 insertions(+), 28 deletions(-) delete mode 100644 src/TODO_qtils/macro/forward.hpp diff --git a/src/TODO_qtils/macro/forward.hpp b/src/TODO_qtils/macro/forward.hpp deleted file mode 100644 index d03cec6..0000000 --- a/src/TODO_qtils/macro/forward.hpp +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright Quadrivium LLC - * All Rights Reserved - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include - -#define FORWARD(x) std::forward(x) diff --git a/src/coro/spawn.hpp b/src/coro/spawn.hpp index ade2513..e7512fd 100644 --- a/src/coro/spawn.hpp +++ b/src/coro/spawn.hpp @@ -8,7 +8,6 @@ #include -#include #include #include @@ -16,19 +15,21 @@ namespace jam { void coroSpawn(auto &&executor, Coro &&coro) { - boost::asio::co_spawn( - FORWARD(executor), std::move(coro), [](std::exception_ptr e) { - if (e != nullptr) { - std::rethrow_exception(e); - } - }); + boost::asio::co_spawn(std::forward(executor), + std::move(coro), + [](std::exception_ptr e) { + if (e != nullptr) { + std::rethrow_exception(e); + } + }); } template void coroSpawn(auto &&executor, Coro &&coro) { - coroSpawn(FORWARD(executor), [MOVE(coro)]() mutable -> Coro { - std::ignore = co_await std::move(coro); - }); + coroSpawn(std::forward(executor), + [MOVE(coro)]() mutable -> Coro { + std::ignore = co_await std::move(coro); + }); } /** @@ -42,13 +43,14 @@ namespace jam { * works because arguments are stored in coroutine state. */ void coroSpawn(auto &&executor, auto f) { - coroSpawn(FORWARD(executor), [](decltype(f) f) -> Coro { - if constexpr (std::is_void_v) { - co_await f(); - } else { - std::ignore = co_await f(); - } - }(std::move(f))); + coroSpawn(std::forward(executor), + [](decltype(f) f) -> Coro { + if constexpr (std::is_void_v) { + co_await f(); + } else { + std::ignore = co_await f(); + } + }(std::move(f))); } /** From e499d39496002dc57ab5d5231557b9e30958c1df Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 17 Feb 2025 12:13:20 +0500 Subject: [PATCH 06/40] make_shared Signed-off-by: turuslan --- src/TODO_qtils/macro/make_shared.hpp | 16 ---------------- src/coro/init.hpp | 6 +++--- src/snp/connections/connections.cpp | 3 ++- src/snp/connections/tls_certificate.cpp | 6 +++--- 4 files changed, 8 insertions(+), 23 deletions(-) delete mode 100644 src/TODO_qtils/macro/make_shared.hpp diff --git a/src/TODO_qtils/macro/make_shared.hpp b/src/TODO_qtils/macro/make_shared.hpp deleted file mode 100644 index fb49925..0000000 --- a/src/TODO_qtils/macro/make_shared.hpp +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright Quadrivium LLC - * All Rights Reserved - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include - -#define MAKE_SHARED_(x_, ...) \ - x_ { \ - std::make_shared(__VA_ARGS__) \ - } - -#define MAKE_SHARED_T(T, ...) std::make_shared(__VA_ARGS__) diff --git a/src/coro/init.hpp b/src/coro/init.hpp index dd06bba..bbe179f 100644 --- a/src/coro/init.hpp +++ b/src/coro/init.hpp @@ -6,8 +6,6 @@ #pragma once -#include - #include "coro/future.hpp" #include "coro/spawn.hpp" @@ -44,7 +42,9 @@ namespace jam { public: CoroInit(IoContextPtr io_context_ptr) - : MOVE_(io_context_ptr), MAKE_SHARED_(future_, io_context_ptr_) {} + : MOVE_(io_context_ptr), + future_{std::make_shared( + io_context_ptr_)} {} auto init() { if (init_called_) { diff --git a/src/snp/connections/connections.cpp b/src/snp/connections/connections.cpp index 877e844..d7e13d8 100644 --- a/src/snp/connections/connections.cpp +++ b/src/snp/connections/connections.cpp @@ -69,7 +69,8 @@ namespace jam::snp { } auto state = qtils::entry(self->connections_, address.key); if (not state) { - state.insert(MAKE_SHARED_T(Connecting, self->io_context_ptr_)); + state.insert( + std::make_shared(self->io_context_ptr_)); co_await coroSpawn([self, address, state]() mutable -> Coro { CORO_YIELD; auto connection_result = CORO_WEAK_AWAIT( diff --git a/src/snp/connections/tls_certificate.cpp b/src/snp/connections/tls_certificate.cpp index f28aa2a..1d942f9 100644 --- a/src/snp/connections/tls_certificate.cpp +++ b/src/snp/connections/tls_certificate.cpp @@ -7,7 +7,6 @@ #include "snp/connections/tls_certificate.hpp" #include -#include #include #include "snp/connections/alpn.hpp" @@ -26,8 +25,9 @@ namespace jam::snp { } TlsCertificate::TlsCertificate(const ConnectionsConfig &config) - : MAKE_SHARED_(alpn_, config.genesis), - MAKE_SHARED_(context_, Context::tlsv13) {} + : alpn_{std::make_shared(config.genesis)}, + context_{std::make_shared( + Context::tlsv13)} {} outcome::result TlsCertificate::make( const ConnectionsConfig &config) { From 95699a2264d25a7f4a8824dc6c3142ae47bf695b Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 17 Feb 2025 12:19:16 +0500 Subject: [PATCH 07/40] move Signed-off-by: turuslan --- src/TODO_qtils/macro/move.hpp | 19 ------------------- src/coro/future.hpp | 5 ++--- src/coro/init.hpp | 2 +- src/coro/spawn.hpp | 3 +-- src/snp/connections/connection.cpp | 5 +++-- src/snp/connections/connections.cpp | 7 +++---- src/snp/connections/lsquic/engine.cpp | 15 +++++++-------- src/snp/connections/stream.cpp | 5 +++-- 8 files changed, 20 insertions(+), 41 deletions(-) delete mode 100644 src/TODO_qtils/macro/move.hpp diff --git a/src/TODO_qtils/macro/move.hpp b/src/TODO_qtils/macro/move.hpp deleted file mode 100644 index 2fba319..0000000 --- a/src/TODO_qtils/macro/move.hpp +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright Quadrivium LLC - * All Rights Reserved - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include - -#define MOVE(x) \ - x { \ - std::move(x) \ - } - -#define MOVE_(x) \ - x##_ { \ - std::move(x) \ - } diff --git a/src/coro/future.hpp b/src/coro/future.hpp index 11c2593..7909a83 100644 --- a/src/coro/future.hpp +++ b/src/coro/future.hpp @@ -10,8 +10,6 @@ #include #include -#include - #include "coro/handler.hpp" #include "coro/set_thread.hpp" @@ -21,7 +19,8 @@ namespace jam { public: using Self = std::shared_ptr>; - SharedFuture(IoContextPtr io_context_ptr) : MOVE_(io_context_ptr) {} + SharedFuture(IoContextPtr io_context_ptr) + : io_context_ptr_{std::move(io_context_ptr)} {} static Coro ready(Self self) { SET_CORO_THREAD(self->io_context_ptr_); diff --git a/src/coro/init.hpp b/src/coro/init.hpp index bbe179f..834cf61 100644 --- a/src/coro/init.hpp +++ b/src/coro/init.hpp @@ -42,7 +42,7 @@ namespace jam { public: CoroInit(IoContextPtr io_context_ptr) - : MOVE_(io_context_ptr), + : io_context_ptr_{std::move(io_context_ptr)}, future_{std::make_shared( io_context_ptr_)} {} diff --git a/src/coro/spawn.hpp b/src/coro/spawn.hpp index e7512fd..506a0eb 100644 --- a/src/coro/spawn.hpp +++ b/src/coro/spawn.hpp @@ -8,7 +8,6 @@ #include -#include #include #include "coro/coro.hpp" @@ -27,7 +26,7 @@ namespace jam { template void coroSpawn(auto &&executor, Coro &&coro) { coroSpawn(std::forward(executor), - [MOVE(coro)]() mutable -> Coro { + [coro{std::move(coro)}]() mutable -> Coro { std::ignore = co_await std::move(coro); }); } diff --git a/src/snp/connections/connection.cpp b/src/snp/connections/connection.cpp index 50a3f1c..e4a581f 100644 --- a/src/snp/connections/connection.cpp +++ b/src/snp/connections/connection.cpp @@ -6,7 +6,6 @@ #include "snp/connections/connection.hpp" -#include #include #include "coro/set_thread.hpp" @@ -19,7 +18,9 @@ namespace jam::snp { Connection::Connection(IoContextPtr io_context_ptr, lsquic::ConnCtx *conn_ctx, ConnectionInfo info) - : MOVE_(io_context_ptr), MOVE_(conn_ctx), MOVE_(info) {} + : io_context_ptr_{std::move(io_context_ptr)}, + conn_ctx_{std::move(conn_ctx)}, + info_{std::move(info)} {} Connection::~Connection() { boost::asio::dispatch(*io_context_ptr_, [conn_ctx{conn_ctx_}] { diff --git a/src/snp/connections/connections.cpp b/src/snp/connections/connections.cpp index d7e13d8..e3bfd29 100644 --- a/src/snp/connections/connections.cpp +++ b/src/snp/connections/connections.cpp @@ -6,7 +6,6 @@ #include "snp/connections/connections.hpp" -#include #include #include #include @@ -29,9 +28,9 @@ namespace jam::snp { Connections::Connections(IoContextPtr io_context_ptr, ConnectionsConfig config) - : MOVE_(io_context_ptr), + : io_context_ptr_{std::move(io_context_ptr)}, init_{io_context_ptr_}, - MOVE_(config), + config_{std::move(config)}, key_{crypto::ed25519::get_public(config_.keypair)} {} CoroOutcome Connections::init( @@ -136,7 +135,7 @@ namespace jam::snp { coroSpawn(*io_context_ptr_, [self{shared_from_this()}, protocol_id, - MOVE(stream), + stream{std::move(stream)}, connection_info{connection->info()}]() mutable -> Coro { auto serve = qtils::entry(self->protocols_, protocol_id); if (not serve) { diff --git a/src/snp/connections/lsquic/engine.cpp b/src/snp/connections/lsquic/engine.cpp index 6a92056..d392702 100644 --- a/src/snp/connections/lsquic/engine.cpp +++ b/src/snp/connections/lsquic/engine.cpp @@ -6,7 +6,6 @@ #include "snp/connections/lsquic/engine.hpp" -#include #include #include #include @@ -138,12 +137,12 @@ namespace jam::snp::lsquic { Socket &&socket, Socket::endpoint_type socket_local_endpoint, std::weak_ptr controller) - : MOVE_(io_context_ptr), - MOVE_(connection_id_counter), - MOVE_(certificate), - MOVE_(socket), - MOVE_(socket_local_endpoint), - MOVE_(controller), + : io_context_ptr_{std::move(io_context_ptr)}, + connection_id_counter_{std::move(connection_id_counter)}, + certificate_{std::move(certificate)}, + socket_{std::move(socket)}, + socket_local_endpoint_{std::move(socket_local_endpoint)}, + controller_{std::move(controller)}, timer_{*io_context_ptr_} {} Engine::~Engine() { @@ -322,7 +321,7 @@ namespace jam::snp::lsquic { void Engine::streamAccept(StreamPtr &&stream) { coroSpawn(*io_context_ptr_, [weak_controller{controller_}, - MOVE(stream)]() mutable -> CoroOutcome { + stream{std::move(stream)}]() mutable -> CoroOutcome { // stream not weak, because no other owners yet BOOST_OUTCOME_CO_TRY(auto protocol_id, co_await stream->readProtocolId()); diff --git a/src/snp/connections/stream.cpp b/src/snp/connections/stream.cpp index 0addfae..2619f73 100644 --- a/src/snp/connections/stream.cpp +++ b/src/snp/connections/stream.cpp @@ -6,7 +6,6 @@ #include "snp/connections/stream.hpp" -#include #include #include #include @@ -25,7 +24,9 @@ namespace jam::snp { Stream::Stream(IoContextPtr io_context_ptr, ConnectionPtr connection, lsquic::StreamCtx *stream_ctx) - : MOVE_(io_context_ptr), MOVE_(connection), MOVE_(stream_ctx) {} + : io_context_ptr_{std::move(io_context_ptr)}, + connection_{std::move(connection)}, + stream_ctx_{std::move(stream_ctx)} {} Stream::~Stream() { boost::asio::dispatch(*io_context_ptr_, [stream_ctx{stream_ctx_}] { From 89f987b83e746befffc0d061634a401baeee8892 Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 17 Feb 2025 12:23:07 +0500 Subject: [PATCH 08/40] weak Signed-off-by: turuslan --- src/TODO_qtils/macro/weak.hpp | 18 ----------- src/snp/connections/connections.cpp | 1 - src/snp/connections/lsquic/engine.cpp | 43 ++++++++++++++++++--------- 3 files changed, 29 insertions(+), 33 deletions(-) delete mode 100644 src/TODO_qtils/macro/weak.hpp diff --git a/src/TODO_qtils/macro/weak.hpp b/src/TODO_qtils/macro/weak.hpp deleted file mode 100644 index 34d7bd8..0000000 --- a/src/TODO_qtils/macro/weak.hpp +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright Quadrivium LLC - * All Rights Reserved - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include - -#define WEAK_SELF \ - weak_self { \ - weak_from_this() \ - } - -#define WEAK_LOCK(name) \ - auto name = weak_##name.lock(); \ - if (not name) return diff --git a/src/snp/connections/connections.cpp b/src/snp/connections/connections.cpp index e3bfd29..b8e6ed3 100644 --- a/src/snp/connections/connections.cpp +++ b/src/snp/connections/connections.cpp @@ -6,7 +6,6 @@ #include "snp/connections/connections.hpp" -#include #include #include diff --git a/src/snp/connections/lsquic/engine.cpp b/src/snp/connections/lsquic/engine.cpp index d392702..ca12ea6 100644 --- a/src/snp/connections/lsquic/engine.cpp +++ b/src/snp/connections/lsquic/engine.cpp @@ -6,7 +6,6 @@ #include "snp/connections/lsquic/engine.hpp" -#include #include #include @@ -123,7 +122,10 @@ namespace jam::snp::lsquic { } io_context_ptr->post([weak_self{std::weak_ptr{self}}] { - WEAK_LOCK(self); + auto self = weak_self.lock(); + if (not self) { + return; + } self->readLoop(); }); @@ -202,8 +204,11 @@ namespace jam::snp::lsquic { return; } want_process_ = true; - boost::asio::post(*io_context_ptr_, [WEAK_SELF] { - WEAK_LOCK(self); + boost::asio::post(*io_context_ptr_, [weak_self{weak_from_this()}] { + auto self = weak_self.lock(); + if (not self) { + return; + } self->process(); }); } @@ -231,8 +236,11 @@ namespace jam::snp::lsquic { return; } timer_.expires_after(std::chrono::microseconds{us}); - auto cb = [WEAK_SELF](boost::system::error_code ec) { - WEAK_LOCK(self); + auto cb = [weak_self{weak_from_this()}](boost::system::error_code ec) { + auto self = weak_self.lock(); + if (not self) { + return; + } if (ec) { return; } @@ -253,13 +261,17 @@ namespace jam::snp::lsquic { &len); if (n == -1) { if (errno == EAGAIN or errno == EWOULDBLOCK) { - auto cb = [WEAK_SELF](boost::system::error_code ec) { - WEAK_LOCK(self); - if (ec) { - return; - } - self->readLoop(); - }; + auto cb = + [weak_self{weak_from_this()}](boost::system::error_code ec) { + auto self = weak_self.lock(); + if (not self) { + return; + } + if (ec) { + return; + } + self->readLoop(); + }; socket_.async_wait(boost::asio::socket_base::wait_read, std::move(cb)); } @@ -573,7 +585,10 @@ namespace jam::snp::lsquic { if (errno == EAGAIN or errno == EWOULDBLOCK) { auto cb = [weak_self{self->weak_from_this()}]( boost::system::error_code ec) { - WEAK_LOCK(self); + auto self = weak_self.lock(); + if (not self) { + return; + } if (ec) { return; } From 0c9f159b63de3670b4c85a278dfeb3c635b589d6 Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 17 Feb 2025 12:27:06 +0500 Subject: [PATCH 09/40] set coro thread Signed-off-by: turuslan --- src/coro/future.hpp | 6 +++--- src/coro/set_thread.hpp | 14 ++++++++------ src/snp/connections/connection.cpp | 2 +- src/snp/connections/connections.cpp | 6 +++--- src/snp/connections/lsquic/engine.cpp | 2 +- src/snp/connections/stream.cpp | 8 ++++---- 6 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/coro/future.hpp b/src/coro/future.hpp index 7909a83..3f7f9ae 100644 --- a/src/coro/future.hpp +++ b/src/coro/future.hpp @@ -23,7 +23,7 @@ namespace jam { : io_context_ptr_{std::move(io_context_ptr)} {} static Coro ready(Self self) { - SET_CORO_THREAD(self->io_context_ptr_); + co_await setCoroThread(self->io_context_ptr_); co_return std::holds_alternative(self->state_); } @@ -31,7 +31,7 @@ namespace jam { * Resumes coroutine immediately or inside `set`. */ static Coro get(Self self) { - SET_CORO_THREAD(self->io_context_ptr_); + co_await setCoroThread(self->io_context_ptr_); if (auto *value = std::get_if(&self->state_)) { co_return *value; } @@ -46,7 +46,7 @@ namespace jam { * Coroutines may complete before `set` returns. */ static Coro set(Self self, T value) { - SET_CORO_THREAD(self->io_context_ptr_); + co_await setCoroThread(self->io_context_ptr_); if (std::holds_alternative(self->state_)) { throw std::logic_error{"SharedFuture::set must be called once"}; } diff --git a/src/coro/set_thread.hpp b/src/coro/set_thread.hpp index eeb0909..f84e401 100644 --- a/src/coro/set_thread.hpp +++ b/src/coro/set_thread.hpp @@ -10,11 +10,13 @@ #include #include +#include "coro/coro.hpp" #include "coro/io_context_ptr.hpp" -#define SET_CORO_THREAD(io_context_ptr) \ - ({ \ - if (not io_context_ptr->get_executor().running_in_this_thread()) { \ - co_await boost::asio::post(*io_context_ptr, boost::asio::use_awaitable); \ - } \ - }) +namespace jam { + inline Coro setCoroThread(IoContextPtr io_context_ptr) { + if (not io_context_ptr->get_executor().running_in_this_thread()) { + co_await boost::asio::post(*io_context_ptr, boost::asio::use_awaitable); + } + } +} // namespace jam diff --git a/src/snp/connections/connection.cpp b/src/snp/connections/connection.cpp index e4a581f..d4c928d 100644 --- a/src/snp/connections/connection.cpp +++ b/src/snp/connections/connection.cpp @@ -33,7 +33,7 @@ namespace jam::snp { } StreamPtrCoroOutcome Connection::open(Self self, ProtocolId protocol_id) { - SET_CORO_THREAD(self->io_context_ptr_); + co_await setCoroThread(self->io_context_ptr_); co_return co_await Engine::openStream(self->conn_ctx_, protocol_id); } } // namespace jam::snp diff --git a/src/snp/connections/connections.cpp b/src/snp/connections/connections.cpp index b8e6ed3..09df645 100644 --- a/src/snp/connections/connections.cpp +++ b/src/snp/connections/connections.cpp @@ -34,7 +34,7 @@ namespace jam::snp { CoroOutcome Connections::init( Self self, std::weak_ptr controller) { - SET_CORO_THREAD(self->io_context_ptr_); + co_await setCoroThread(self->io_context_ptr_); auto init = self->init_.init(); self->controller_ = std::move(controller); BOOST_OUTCOME_CO_TRY(auto certificate, TlsCertificate::make(self->config_)); @@ -61,7 +61,7 @@ namespace jam::snp { } ConnectionPtrCoroOutcome Connections::connect(Self self, Address address) { - SET_CORO_THREAD(self->io_context_ptr_); + co_await setCoroThread(self->io_context_ptr_); if (not co_await self->init_.ready()) { co_return ConnectionsError::CONNECTIONS_INIT; } @@ -101,7 +101,7 @@ namespace jam::snp { Coro Connections::serve(Self self, ProtocolId protocol_id, ServeProtocol serve) { - SET_CORO_THREAD(self->io_context_ptr_); + co_await setCoroThread(self->io_context_ptr_); qtils::entry(self->protocols_, protocol_id).insert(std::move(serve)); } diff --git a/src/snp/connections/lsquic/engine.cpp b/src/snp/connections/lsquic/engine.cpp index ca12ea6..8da9453 100644 --- a/src/snp/connections/lsquic/engine.cpp +++ b/src/snp/connections/lsquic/engine.cpp @@ -157,7 +157,7 @@ namespace jam::snp::lsquic { } ConnectionPtrCoroOutcome Engine::connect(Self self, Address address) { - SET_CORO_THREAD(self->io_context_ptr_); + co_await setCoroThread(self->io_context_ptr_); if (self->connecting_) { co_return ConnectionsError::ENGINE_CONNECT_ALREADY; } diff --git a/src/snp/connections/stream.cpp b/src/snp/connections/stream.cpp index 2619f73..8519345 100644 --- a/src/snp/connections/stream.cpp +++ b/src/snp/connections/stream.cpp @@ -37,7 +37,7 @@ namespace jam::snp { CoroOutcome Stream::read(Self self, qtils::Bytes &buffer, MessageSize max) { - SET_CORO_THREAD(self->io_context_ptr_); + co_await setCoroThread(self->io_context_ptr_); MessageSizeBytes size_bytes; BOOST_OUTCOME_CO_TRY( auto read_size, @@ -64,13 +64,13 @@ namespace jam::snp { } Coro Stream::readFin(Self self) { - SET_CORO_THREAD(self->io_context_ptr_); + co_await setCoroThread(self->io_context_ptr_); Engine::streamReadFin(self->stream_ctx_); co_return; } CoroOutcome Stream::write(Self self, qtils::BytesIn message) { - SET_CORO_THREAD(self->io_context_ptr_); + co_await setCoroThread(self->io_context_ptr_); MessageSizeBytes size_bytes; auto size = message.size(); if (size > kMessageSizeMax) { @@ -89,7 +89,7 @@ namespace jam::snp { } Coro Stream::writeFin(Self self) { - SET_CORO_THREAD(self->io_context_ptr_); + co_await setCoroThread(self->io_context_ptr_); Engine::streamWriteFin(self->stream_ctx_); co_return; } From f742c39a2bd4365e452b09da45aa9a7d15c5f1be Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 17 Feb 2025 12:29:22 +0500 Subject: [PATCH 10/40] coro yield Signed-off-by: turuslan --- src/coro/yield.hpp | 17 +++++++++++------ src/snp/connections/connections.cpp | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/coro/yield.hpp b/src/coro/yield.hpp index e8b4cbc..3e6b2a3 100644 --- a/src/coro/yield.hpp +++ b/src/coro/yield.hpp @@ -10,9 +10,14 @@ #include #include -/** - * Thread switch operation always completes, so it can't leak `shared_ptr`. - */ -#define CORO_YIELD \ - co_await boost::asio::post(co_await boost::asio::this_coro::executor, \ - boost::asio::use_awaitable) +#include "coro/coro.hpp" + +namespace jam { + /** + * Thread switch operation always completes, so it can't leak `shared_ptr`. + */ + Coro coroYield() { + co_await boost::asio::post(co_await boost::asio::this_coro::executor, + boost::asio::use_awaitable); + } +} // namespace jam diff --git a/src/snp/connections/connections.cpp b/src/snp/connections/connections.cpp index 09df645..b632594 100644 --- a/src/snp/connections/connections.cpp +++ b/src/snp/connections/connections.cpp @@ -70,7 +70,7 @@ namespace jam::snp { state.insert( std::make_shared(self->io_context_ptr_)); co_await coroSpawn([self, address, state]() mutable -> Coro { - CORO_YIELD; + co_await coroYield(); auto connection_result = CORO_WEAK_AWAIT( self, self->client_->connect(self->client_, address)); auto state = qtils::entry(self->connections_, address.key); From e8140632841a9be4a3ea7f6c1f2990f499f1d112 Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 17 Feb 2025 12:39:37 +0500 Subject: [PATCH 11/40] enum type Signed-off-by: turuslan --- src/snp/connections/error.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/snp/connections/error.hpp b/src/snp/connections/error.hpp index c225a4a..99f4761 100644 --- a/src/snp/connections/error.hpp +++ b/src/snp/connections/error.hpp @@ -9,7 +9,7 @@ #include namespace jam::snp { - enum class OpenSslError { + enum class OpenSslError : uint8_t { EVP_PKEY_get_raw_public_key, EVP_PKEY_new_raw_private_key, SSL_CTX_set_alpn_protos, @@ -53,7 +53,7 @@ namespace jam::snp { } } - enum class LsQuicError { + enum class LsQuicError : uint8_t { lsquic_conn_make_stream, lsquic_engine_connect, lsquic_engine_new, @@ -73,7 +73,7 @@ namespace jam::snp { } } - enum class ConnectionsError { + enum class ConnectionsError : uint8_t { CONNECTION_OPEN_CLOSED, CONNECTION_OPEN_DUPLICATE, CONNECTIONS_INIT, From bbddad4112960a48955200ab1ec106b9058aff2f Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 17 Feb 2025 12:41:49 +0500 Subject: [PATCH 12/40] cppcodec macro Signed-off-by: turuslan --- src/snp/connections/dns_name.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/snp/connections/dns_name.cpp b/src/snp/connections/dns_name.cpp index 5200ac3..5f82fe8 100644 --- a/src/snp/connections/dns_name.cpp +++ b/src/snp/connections/dns_name.cpp @@ -16,14 +16,13 @@ namespace jam::snp::base32 { template using codec_impl = cppcodec::detail::stream_codec; - static CPPCODEC_ALWAYS_INLINE constexpr size_t alphabet_size() { + static constexpr size_t alphabet_size() { return 32; } - static CPPCODEC_ALWAYS_INLINE constexpr char symbol( - cppcodec::detail::alphabet_index_t idx) { + static constexpr char symbol(cppcodec::detail::alphabet_index_t idx) { return kAlphabet[idx]; } - static CPPCODEC_ALWAYS_INLINE constexpr bool generates_padding() { + static constexpr bool generates_padding() { return false; } }; From b12bdcd18f54c143cd8976ff13594cf21be1d6ef Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 17 Feb 2025 12:43:53 +0500 Subject: [PATCH 13/40] numeric limits Signed-off-by: turuslan --- src/snp/connections/message_size.hpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/snp/connections/message_size.hpp b/src/snp/connections/message_size.hpp index 34837e2..910244e 100644 --- a/src/snp/connections/message_size.hpp +++ b/src/snp/connections/message_size.hpp @@ -7,9 +7,11 @@ #pragma once #include +#include namespace jam::snp { // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L109-L111 using MessageSize = uint32_t; - constexpr MessageSize kMessageSizeMax = UINT32_MAX; + constexpr MessageSize kMessageSizeMax = + std::numeric_limits::max(); } // namespace jam::snp From 856d7f48582535c251eb1677e908df31d95d3a2c Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 17 Feb 2025 13:03:31 +0500 Subject: [PATCH 14/40] if return Signed-off-by: turuslan --- src/snp/connections/lsquic/engine.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/snp/connections/lsquic/engine.cpp b/src/snp/connections/lsquic/engine.cpp index 8da9453..7dcdd41 100644 --- a/src/snp/connections/lsquic/engine.cpp +++ b/src/snp/connections/lsquic/engine.cpp @@ -347,17 +347,15 @@ namespace jam::snp::lsquic { } void Engine::streamReadFin(StreamCtx *stream_ctx) { - if (not stream_ctx->ls_stream) { - return; + if (stream_ctx->ls_stream) { + lsquic_stream_shutdown(stream_ctx->ls_stream.value(), SHUT_RD); } - lsquic_stream_shutdown(stream_ctx->ls_stream.value(), SHUT_RD); } void Engine::streamWriteFin(StreamCtx *stream_ctx) { - if (not stream_ctx->ls_stream) { - return; + if (stream_ctx->ls_stream) { + lsquic_stream_shutdown(stream_ctx->ls_stream.value(), SHUT_WR); } - lsquic_stream_shutdown(stream_ctx->ls_stream.value(), SHUT_WR); } CoroOutcome Engine::streamReadRaw(StreamCtx *stream_ctx, From 5bb57a4e729f3342241ed30c6b2754faa52b6863 Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 17 Feb 2025 13:03:36 +0500 Subject: [PATCH 15/40] likely Signed-off-by: turuslan --- src/snp/connections/lsquic/engine.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/snp/connections/lsquic/engine.cpp b/src/snp/connections/lsquic/engine.cpp index 7dcdd41..33916d3 100644 --- a/src/snp/connections/lsquic/engine.cpp +++ b/src/snp/connections/lsquic/engine.cpp @@ -365,7 +365,7 @@ namespace jam::snp::lsquic { } auto remaining = message; while (not remaining.empty()) { - if (not stream_ctx->ls_stream) { + [[unlikely]] if (not stream_ctx->ls_stream) { co_return ConnectionsError::STREAM_READ_CLOSED; } auto n = lsquic_stream_read( @@ -399,7 +399,7 @@ namespace jam::snp::lsquic { } auto remaining = message; while (not remaining.empty()) { - if (not stream_ctx->ls_stream) { + [[unlikely]] if (not stream_ctx->ls_stream) { co_return ConnectionsError::STREAM_WRITE_CLOSED; } auto n = lsquic_stream_write( From b853b9ad18f9510be61bce2fcf9020ebe0034b4a Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 17 Feb 2025 13:04:05 +0500 Subject: [PATCH 16/40] self from void Signed-off-by: turuslan --- src/snp/connections/lsquic/engine.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/snp/connections/lsquic/engine.cpp b/src/snp/connections/lsquic/engine.cpp index 33916d3..97a4703 100644 --- a/src/snp/connections/lsquic/engine.cpp +++ b/src/snp/connections/lsquic/engine.cpp @@ -18,8 +18,6 @@ #include "snp/connections/lsquic/init.hpp" #include "snp/connections/stream.hpp" -#define SELF_FROM_VOID Engine *self = static_cast(void_self) - // TODO(turuslan): unique streams // TODO(turuslan): connection/stream close event lag @@ -428,7 +426,7 @@ namespace jam::snp::lsquic { lsquic_conn_ctx_t *Engine::on_new_conn(void *void_self, lsquic_conn_t *ls_conn) { - SELF_FROM_VOID; + Engine *self = static_cast(void_self); auto connecting = qtils::optionTake(self->connecting_); auto is_connecting = connecting.has_value(); // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) @@ -500,7 +498,7 @@ namespace jam::snp::lsquic { lsquic_stream_ctx_t *Engine::on_new_stream(void *void_self, lsquic_stream_t *ls_stream) { - SELF_FROM_VOID; + Engine *self = static_cast(void_self); auto *conn_ctx = from_ls(lsquic_conn_get_ctx(lsquic_stream_conn(ls_stream))); // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) @@ -559,14 +557,14 @@ namespace jam::snp::lsquic { } ssl_ctx_st *Engine::ea_get_ssl_ctx(void *void_self, const sockaddr *) { - SELF_FROM_VOID; + Engine *self = static_cast(void_self); return self->certificate_; } int Engine::ea_packets_out(void *void_self, const lsquic_out_spec *out_spec, unsigned n_packets_out) { - SELF_FROM_VOID; + Engine *self = static_cast(void_self); // https://github.com/cbodley/nexus/blob/d1d8486f713fd089917331239d755932c7c8ed8e/src/socket.cc#L218 int r = 0; for (auto &spec : std::span{out_spec, n_packets_out}) { From 22699ad992944181b3a460ba73a0427664e22715 Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 17 Feb 2025 13:17:58 +0500 Subject: [PATCH 17/40] example Signed-off-by: turuslan --- CMakeLists.txt | 8 ++++++++ example/CMakeLists.txt | 7 +++++++ example/snp_chat/CMakeLists.txt | 13 +++++++++++++ .../example_chat.cpp => example/snp_chat/main.cpp | 0 src/CMakeLists.txt | 2 -- src/executable/CMakeLists.txt | 2 -- src/snp/CMakeLists.txt | 8 -------- 7 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 example/CMakeLists.txt create mode 100644 example/snp_chat/CMakeLists.txt rename src/snp/example_chat.cpp => example/snp_chat/main.cpp (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index c194ebd..d571c12 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,8 @@ if (TESTING) list(APPEND VCPKG_MANIFEST_FEATURES test) endif () +option(BUILD_EXAMPLES "Build examples" ON) + set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) @@ -45,6 +47,8 @@ find_package(ZLIB REQUIRED) include(vcpkg-overlay/cppcodec.cmake) +include_directories(${CMAKE_SOURCE_DIR}/src) + add_subdirectory(src) if (TESTING) @@ -56,3 +60,7 @@ if (TESTING) add_subdirectory(test-vectors) add_subdirectory(tests) endif () + +if (BUILD_EXAMPLES) + add_subdirectory(example) +endif () diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt new file mode 100644 index 0000000..efd41af --- /dev/null +++ b/example/CMakeLists.txt @@ -0,0 +1,7 @@ +# +# Copyright Quadrivium LLC +# All Rights Reserved +# SPDX-License-Identifier: Apache-2.0 +# + +add_subdirectory(snp_chat) diff --git a/example/snp_chat/CMakeLists.txt b/example/snp_chat/CMakeLists.txt new file mode 100644 index 0000000..d082c3f --- /dev/null +++ b/example/snp_chat/CMakeLists.txt @@ -0,0 +1,13 @@ +# +# Copyright Quadrivium LLC +# All Rights Reserved +# SPDX-License-Identifier: Apache-2.0 +# + +add_executable(example_snp_chat + main.cpp +) +target_link_libraries(example_snp_chat + snp +) + diff --git a/src/snp/example_chat.cpp b/example/snp_chat/main.cpp similarity index 100% rename from src/snp/example_chat.cpp rename to example/snp_chat/main.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4781ccc..36888c0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,8 +4,6 @@ # SPDX-License-Identifier: Apache-2.0 # -include_directories(${CMAKE_CURRENT_SOURCE_DIR}) - # Executables (should contain `main()` function) add_subdirectory(executable) diff --git a/src/executable/CMakeLists.txt b/src/executable/CMakeLists.txt index 392671f..742a8e8 100644 --- a/src/executable/CMakeLists.txt +++ b/src/executable/CMakeLists.txt @@ -11,8 +11,6 @@ set(LIBRARIES ) include_directories( - ${PROJECT_SOURCE_DIR} - ${CMAKE_SOURCE_DIR}/src ${CMAKE_BINARY_DIR}/generated ) diff --git a/src/snp/CMakeLists.txt b/src/snp/CMakeLists.txt index 82f6781..81b815a 100644 --- a/src/snp/CMakeLists.txt +++ b/src/snp/CMakeLists.txt @@ -23,11 +23,3 @@ target_link_libraries(snp schnorrkel_crust::schnorrkel_crust ZLIB::ZLIB ) - -add_executable(example_chat - example_chat.cpp -) -target_link_libraries(example_chat - snp -) - From ef17c47081b637d706e995d7be8d760b2c9b2a8a Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 17 Feb 2025 13:20:52 +0500 Subject: [PATCH 18/40] sizeof Signed-off-by: turuslan --- src/snp/connections/stream.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/snp/connections/stream.cpp b/src/snp/connections/stream.cpp index 8519345..2eafc90 100644 --- a/src/snp/connections/stream.cpp +++ b/src/snp/connections/stream.cpp @@ -19,7 +19,7 @@ namespace jam::snp { using lsquic::Engine; using ProtocolIdBytes = qtils::BytesN<1>; - using MessageSizeBytes = qtils::BytesN<4>; + using MessageSizeBytes = qtils::BytesN; Stream::Stream(IoContextPtr io_context_ptr, ConnectionPtr connection, From 9814d46e95b959e737fe8c0919c272267a5509f4 Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 17 Feb 2025 13:37:56 +0500 Subject: [PATCH 19/40] rename Signed-off-by: turuslan --- src/snp/connections/connections.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/snp/connections/connections.cpp b/src/snp/connections/connections.cpp index b632594..8e6df00 100644 --- a/src/snp/connections/connections.cpp +++ b/src/snp/connections/connections.cpp @@ -136,13 +136,13 @@ namespace jam::snp { protocol_id, stream{std::move(stream)}, connection_info{connection->info()}]() mutable -> Coro { - auto serve = qtils::entry(self->protocols_, protocol_id); - if (not serve) { + auto serve_it = qtils::entry(self->protocols_, protocol_id); + if (not serve_it) { co_return; } - auto copy = *serve; + auto serve = *serve_it; std::ignore = CORO_WEAK_AWAIT( - self, copy(connection_info, std::move(stream))); + self, serve(connection_info, std::move(stream))); }); } } // namespace jam::snp From 387193e04d2e755d65c70c9f3581871a69f2c9f3 Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 17 Feb 2025 13:45:11 +0500 Subject: [PATCH 20/40] rename self Signed-off-by: turuslan --- src/coro/future.hpp | 8 ++++---- src/snp/connections/connection.cpp | 2 +- src/snp/connections/connection.hpp | 4 ++-- src/snp/connections/connections.cpp | 7 ++++--- src/snp/connections/connections.hpp | 8 ++++---- src/snp/connections/lsquic/engine.cpp | 2 +- src/snp/connections/lsquic/engine.hpp | 4 ++-- src/snp/connections/stream.cpp | 8 ++++---- src/snp/connections/stream.hpp | 10 +++++----- 9 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/coro/future.hpp b/src/coro/future.hpp index 3f7f9ae..fe24197 100644 --- a/src/coro/future.hpp +++ b/src/coro/future.hpp @@ -17,12 +17,12 @@ namespace jam { template class SharedFuture { public: - using Self = std::shared_ptr>; + using SelfSPtr = std::shared_ptr>; SharedFuture(IoContextPtr io_context_ptr) : io_context_ptr_{std::move(io_context_ptr)} {} - static Coro ready(Self self) { + static Coro ready(SelfSPtr self) { co_await setCoroThread(self->io_context_ptr_); co_return std::holds_alternative(self->state_); } @@ -30,7 +30,7 @@ namespace jam { /** * Resumes coroutine immediately or inside `set`. */ - static Coro get(Self self) { + static Coro get(SelfSPtr self) { co_await setCoroThread(self->io_context_ptr_); if (auto *value = std::get_if(&self->state_)) { co_return *value; @@ -45,7 +45,7 @@ namespace jam { * Set value and wake waiting coroutines. * Coroutines may complete before `set` returns. */ - static Coro set(Self self, T value) { + static Coro set(SelfSPtr self, T value) { co_await setCoroThread(self->io_context_ptr_); if (std::holds_alternative(self->state_)) { throw std::logic_error{"SharedFuture::set must be called once"}; diff --git a/src/snp/connections/connection.cpp b/src/snp/connections/connection.cpp index d4c928d..c7ad566 100644 --- a/src/snp/connections/connection.cpp +++ b/src/snp/connections/connection.cpp @@ -32,7 +32,7 @@ namespace jam::snp { return info_; } - StreamPtrCoroOutcome Connection::open(Self self, ProtocolId protocol_id) { + StreamPtrCoroOutcome Connection::open(SelfSPtr self, ProtocolId protocol_id) { co_await setCoroThread(self->io_context_ptr_); co_return co_await Engine::openStream(self->conn_ctx_, protocol_id); } diff --git a/src/snp/connections/connection.hpp b/src/snp/connections/connection.hpp index 2e710ca..f39f2f6 100644 --- a/src/snp/connections/connection.hpp +++ b/src/snp/connections/connection.hpp @@ -22,7 +22,7 @@ namespace jam::snp { friend lsquic::Engine; public: - using Self = std::shared_ptr; + using SelfSPtr = std::shared_ptr; Connection(IoContextPtr io_context_ptr, lsquic::ConnCtx *conn_ctx, @@ -34,7 +34,7 @@ namespace jam::snp { /** * Open stream with specified `ProtocolId`. */ - static StreamPtrCoroOutcome open(Self self, ProtocolId protocol_id); + static StreamPtrCoroOutcome open(SelfSPtr self, ProtocolId protocol_id); private: IoContextPtr io_context_ptr_; diff --git a/src/snp/connections/connections.cpp b/src/snp/connections/connections.cpp index 8e6df00..d857206 100644 --- a/src/snp/connections/connections.cpp +++ b/src/snp/connections/connections.cpp @@ -33,7 +33,7 @@ namespace jam::snp { key_{crypto::ed25519::get_public(config_.keypair)} {} CoroOutcome Connections::init( - Self self, std::weak_ptr controller) { + SelfSPtr self, std::weak_ptr controller) { co_await setCoroThread(self->io_context_ptr_); auto init = self->init_.init(); self->controller_ = std::move(controller); @@ -60,7 +60,8 @@ namespace jam::snp { return key_; } - ConnectionPtrCoroOutcome Connections::connect(Self self, Address address) { + ConnectionPtrCoroOutcome Connections::connect(SelfSPtr self, + Address address) { co_await setCoroThread(self->io_context_ptr_); if (not co_await self->init_.ready()) { co_return ConnectionsError::CONNECTIONS_INIT; @@ -98,7 +99,7 @@ namespace jam::snp { co_return co_await connecting->get(connecting); } - Coro Connections::serve(Self self, + Coro Connections::serve(SelfSPtr self, ProtocolId protocol_id, ServeProtocol serve) { co_await setCoroThread(self->io_context_ptr_); diff --git a/src/snp/connections/connections.hpp b/src/snp/connections/connections.hpp index 0c241df..34a0af4 100644 --- a/src/snp/connections/connections.hpp +++ b/src/snp/connections/connections.hpp @@ -37,7 +37,7 @@ namespace jam::snp { class Connections : public std::enable_shared_from_this, public lsquic::EngineController { public: - using Self = std::shared_ptr; + using SelfSPtr = std::shared_ptr; Connections(IoContextPtr io_context_ptr, ConnectionsConfig config); @@ -46,21 +46,21 @@ namespace jam::snp { * Start quic server and client. */ static CoroOutcome init( - Self self, std::weak_ptr controller); + SelfSPtr self, std::weak_ptr controller); const Key &key() const; /** * Connect or return existing connection. */ - static ConnectionPtrCoroOutcome connect(Self self, Address address); + static ConnectionPtrCoroOutcome connect(SelfSPtr self, Address address); using ServeProtocol = std::function(ConnectionInfo, StreamPtr)>; /** * Set callback to handle protocol on server side. */ - static Coro serve(Self self, + static Coro serve(SelfSPtr self, ProtocolId protocol_id, ServeProtocol serve); diff --git a/src/snp/connections/lsquic/engine.cpp b/src/snp/connections/lsquic/engine.cpp index 97a4703..f72663f 100644 --- a/src/snp/connections/lsquic/engine.cpp +++ b/src/snp/connections/lsquic/engine.cpp @@ -154,7 +154,7 @@ namespace jam::snp::lsquic { } } - ConnectionPtrCoroOutcome Engine::connect(Self self, Address address) { + ConnectionPtrCoroOutcome Engine::connect(SelfSPtr self, Address address) { co_await setCoroThread(self->io_context_ptr_); if (self->connecting_) { co_return ConnectionsError::ENGINE_CONNECT_ALREADY; diff --git a/src/snp/connections/lsquic/engine.hpp b/src/snp/connections/lsquic/engine.hpp index 229949b..acd8e48 100644 --- a/src/snp/connections/lsquic/engine.hpp +++ b/src/snp/connections/lsquic/engine.hpp @@ -89,7 +89,7 @@ namespace jam::snp::lsquic { struct Private {}; public: - using Self = std::shared_ptr; + using SelfSPtr = std::shared_ptr; static outcome::result> make( IoContextPtr io_context_ptr, @@ -106,7 +106,7 @@ namespace jam::snp::lsquic { std::weak_ptr controller); ~Engine(); - static ConnectionPtrCoroOutcome connect(Self self, Address address); + static ConnectionPtrCoroOutcome connect(SelfSPtr self, Address address); private: struct Reading { diff --git a/src/snp/connections/stream.cpp b/src/snp/connections/stream.cpp index 2eafc90..e0f0c9f 100644 --- a/src/snp/connections/stream.cpp +++ b/src/snp/connections/stream.cpp @@ -34,7 +34,7 @@ namespace jam::snp { }); } - CoroOutcome Stream::read(Self self, + CoroOutcome Stream::read(SelfSPtr self, qtils::Bytes &buffer, MessageSize max) { co_await setCoroThread(self->io_context_ptr_); @@ -63,13 +63,13 @@ namespace jam::snp { co_return true; } - Coro Stream::readFin(Self self) { + Coro Stream::readFin(SelfSPtr self) { co_await setCoroThread(self->io_context_ptr_); Engine::streamReadFin(self->stream_ctx_); co_return; } - CoroOutcome Stream::write(Self self, qtils::BytesIn message) { + CoroOutcome Stream::write(SelfSPtr self, qtils::BytesIn message) { co_await setCoroThread(self->io_context_ptr_); MessageSizeBytes size_bytes; auto size = message.size(); @@ -88,7 +88,7 @@ namespace jam::snp { co_return outcome::success(); } - Coro Stream::writeFin(Self self) { + Coro Stream::writeFin(SelfSPtr self) { co_await setCoroThread(self->io_context_ptr_); Engine::streamWriteFin(self->stream_ctx_); co_return; diff --git a/src/snp/connections/stream.hpp b/src/snp/connections/stream.hpp index 25b9161..ae9b29a 100644 --- a/src/snp/connections/stream.hpp +++ b/src/snp/connections/stream.hpp @@ -24,7 +24,7 @@ namespace jam::snp { friend lsquic::Engine; public: - using Self = std::shared_ptr; + using SelfSPtr = std::shared_ptr; Stream(IoContextPtr io_context_ptr, ConnectionPtr connection, @@ -40,26 +40,26 @@ namespace jam::snp { * Returns `true` if message was read, or `false` if fin was received or * stream was closed. */ - static CoroOutcome read(Self self, + static CoroOutcome read(SelfSPtr self, qtils::Bytes &buffer, MessageSize max); /** * Close reading side of stream. */ - static Coro readFin(Self self); + static Coro readFin(SelfSPtr self); // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L109-L111 /** * Write while size prefixed message. */ - static CoroOutcome write(Self self, qtils::BytesIn message); + static CoroOutcome write(SelfSPtr self, qtils::BytesIn message); /** * Write fin. * Closes writing side of stream. */ - static Coro writeFin(Self self); + static Coro writeFin(SelfSPtr self); private: /** From 0bfb4fdb72f6cdb1595ba06844a4e521a2a450e8 Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 17 Feb 2025 16:27:28 +0500 Subject: [PATCH 21/40] error text Signed-off-by: turuslan --- src/TODO_qtils/map_entry.hpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/TODO_qtils/map_entry.hpp b/src/TODO_qtils/map_entry.hpp index b4c714b..ea24aee 100644 --- a/src/TODO_qtils/map_entry.hpp +++ b/src/TODO_qtils/map_entry.hpp @@ -33,13 +33,16 @@ namespace qtils { } auto &operator*() { if (not has()) { - throw std::logic_error{"MapEntry::operator*"}; + throw std::logic_error{ + "Call dereference operator of MapEntry without valid iterator"}; } return std::get(it_or_key)->second; } auto *operator->() { if (not has()) { - throw std::logic_error{"MapEntry::operator->"}; + throw std::logic_error{ + "Call member access through pointer operator of MapEntry without " + "valid iterator"}; } return &std::get(it_or_key)->second; } From b9d83ccc6194151909ee4f9877c16f68f6501374 Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 17 Feb 2025 16:56:58 +0500 Subject: [PATCH 22/40] comment Signed-off-by: turuslan --- src/snp/connections/lsquic/log.hpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/snp/connections/lsquic/log.hpp b/src/snp/connections/lsquic/log.hpp index 1e6c770..e076b60 100644 --- a/src/snp/connections/lsquic/log.hpp +++ b/src/snp/connections/lsquic/log.hpp @@ -10,13 +10,22 @@ #include namespace jam::snp::lsquic { - inline void log() { + /** + * Enable lsquic log. + * + * Possible levels: "emerg", "alert", "crit", "error", "warn", "notice", + * "info", "debug". + */ + inline void log(const char *level = "debug") { static lsquic_logger_if log{ +[](void *, const char *buf, size_t len) { return (int)fwrite(buf, sizeof(char), len, stdout); }, }; - lsquic_logger_init(&log, nullptr, LLTS_HHMMSSMS); - lsquic_set_log_level("debug"); + static auto init = [] { + lsquic_logger_init(&log, nullptr, LLTS_HHMMSSMS); + return 0; + }(); + lsquic_set_log_level(level); } } // namespace jam::snp::lsquic From 90b06abec7a0d62406e353342be90683e19a9434 Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 17 Feb 2025 17:06:06 +0500 Subject: [PATCH 23/40] comment Signed-off-by: turuslan --- src/snp/connections/protocol_id.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/snp/connections/protocol_id.hpp b/src/snp/connections/protocol_id.hpp index be32dac..3530a02 100644 --- a/src/snp/connections/protocol_id.hpp +++ b/src/snp/connections/protocol_id.hpp @@ -35,6 +35,11 @@ namespace jam::snp { return id_; } + // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L87-L101 + /** + * Unique protocols reuse one stream per peer. + * Ephemeral protocols may create multiple streams. + */ bool unique() const { return id() < 128; } From a34d339d934f9f1edccbfcb90b40b2a1fdab8a70 Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 17 Feb 2025 17:14:37 +0500 Subject: [PATCH 24/40] comment Signed-off-by: turuslan --- src/snp/connections/alpn.hpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/snp/connections/alpn.hpp b/src/snp/connections/alpn.hpp index a3dec6f..0df2988 100644 --- a/src/snp/connections/alpn.hpp +++ b/src/snp/connections/alpn.hpp @@ -17,15 +17,27 @@ struct ssl_st; namespace jam::snp { // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L30-L41 + /** + * TLS ALPN (Application-Layer Protocol Negotiation) used by jam peers. + */ class Alpn { static constexpr auto kVersion = 0; public: + /** + * Make ALPN from jam genesis hash. + */ Alpn(const GenesisHash &genesis); + /** + * Set ALPN for `ssl_ctx`. + */ outcome::result set(ssl_ctx_st *ssl_ctx); private: + /** + * Validate peer ALPN. + */ static int select(ssl_st *ssl, const unsigned char **out, unsigned char *outlen, From 25c3e6d0487f093cbb7d163181a7eba404117628 Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 17 Feb 2025 17:28:06 +0500 Subject: [PATCH 25/40] variant get Signed-off-by: turuslan --- src/TODO_qtils/variant_get.hpp | 41 +++++++++++++++++++++++++++++ src/coro/future.hpp | 4 ++- src/snp/connections/connections.cpp | 3 ++- 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 src/TODO_qtils/variant_get.hpp diff --git a/src/TODO_qtils/variant_get.hpp b/src/TODO_qtils/variant_get.hpp new file mode 100644 index 0000000..84cecf1 --- /dev/null +++ b/src/TODO_qtils/variant_get.hpp @@ -0,0 +1,41 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace qtils { + /** + * Some people don't like pointer in + * if (auto *t = std::get_if(&v)) + */ + template + struct VariantGet { + VariantGet(V &variant) : variant_{variant} {} + operator bool() const { + return std::holds_alternative(variant_); + } + auto &operator*() { + return std::get(variant_); + } + auto *operator->() { + return &std::get(variant_); + } + // NOLINTNEXTLINE(cppcoreguidelines-avoid-const-or-ref-data-members) + V &variant_; + }; + + template + auto variantGet(const std::variant &variant) { + return VariantGet{variant}; + } + + template + auto variantGet(std::variant &variant) { + return VariantGet{variant}; + } +} // namespace qtils diff --git a/src/coro/future.hpp b/src/coro/future.hpp index fe24197..9dad641 100644 --- a/src/coro/future.hpp +++ b/src/coro/future.hpp @@ -10,6 +10,8 @@ #include #include +#include + #include "coro/handler.hpp" #include "coro/set_thread.hpp" @@ -32,7 +34,7 @@ namespace jam { */ static Coro get(SelfSPtr self) { co_await setCoroThread(self->io_context_ptr_); - if (auto *value = std::get_if(&self->state_)) { + if (auto value = qtils::variantGet(self->state_)) { co_return *value; } auto &handlers = std::get(self->state_); diff --git a/src/snp/connections/connections.cpp b/src/snp/connections/connections.cpp index d857206..6ead48e 100644 --- a/src/snp/connections/connections.cpp +++ b/src/snp/connections/connections.cpp @@ -7,6 +7,7 @@ #include "snp/connections/connections.hpp" #include +#include #include #include "coro/set_thread.hpp" @@ -91,7 +92,7 @@ namespace jam::snp { CORO_WEAK_AWAIT_V( self, connecting->set(connecting, std::move(connection_result))); }); - } else if (auto *connected = std::get_if(&*state)) { + } else if (auto connected = qtils::variantGet(*state)) { co_return *connected; } auto connecting = std::get(*state); From 6aa4b490f6d8f0625283fd2b05642d14f72c0cde Mon Sep 17 00:00:00 2001 From: turuslan Date: Tue, 18 Feb 2025 07:37:36 +0500 Subject: [PATCH 26/40] hash constraint Signed-off-by: turuslan --- src/TODO_qtils/std_hash_of.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/TODO_qtils/std_hash_of.hpp b/src/TODO_qtils/std_hash_of.hpp index f20aa01..b1fe861 100644 --- a/src/TODO_qtils/std_hash_of.hpp +++ b/src/TODO_qtils/std_hash_of.hpp @@ -10,6 +10,7 @@ namespace qtils { template + requires(requires(const T &v) { std::hash()(v); }) size_t stdHashOf(const T &v) { return std::hash()(v); } From ad703edc267e7f6a63c717578b8ad4f3575d3b5b Mon Sep 17 00:00:00 2001 From: turuslan Date: Tue, 18 Feb 2025 08:19:14 +0500 Subject: [PATCH 27/40] forward Signed-off-by: turuslan --- src/coro/spawn.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/coro/spawn.hpp b/src/coro/spawn.hpp index 506a0eb..c972b5d 100644 --- a/src/coro/spawn.hpp +++ b/src/coro/spawn.hpp @@ -41,15 +41,15 @@ namespace jam { * `co_spawn([](args){ ... }(capture))` * works because arguments are stored in coroutine state. */ - void coroSpawn(auto &&executor, auto f) { + void coroSpawn(auto &&executor, auto &&f) { coroSpawn(std::forward(executor), - [](decltype(f) f) -> Coro { + [](std::remove_cvref_t f) -> Coro { if constexpr (std::is_void_v) { co_await f(); } else { std::ignore = co_await f(); } - }(std::move(f))); + }(std::forward(f))); } /** From 749ee004a3546ffb96f0f5584a013239c55d64f1 Mon Sep 17 00:00:00 2001 From: turuslan Date: Tue, 18 Feb 2025 08:19:57 +0500 Subject: [PATCH 28/40] comment Signed-off-by: turuslan --- src/coro/weak.hpp | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/coro/weak.hpp b/src/coro/weak.hpp index 21e9a87..fc8e95d 100644 --- a/src/coro/weak.hpp +++ b/src/coro/weak.hpp @@ -8,14 +8,31 @@ #include "coro/coro.hpp" +/** + * Converts shared pointer to weak pointer for `co_await` duration. + * Shared pointer owner can cancel operation by destroying shared pointer. + * Checks that state wasn't destroyed during `co_await` before continuing. + * auto cb2 = [cb, tmp_weak = std::weak_ptr{shared}, ...](auto r) { + * auto shared = tmp_weak.lock(); + * if (not shared) { + * return cb(...); + * } + * ... + * cb(); + * }; + */ #define _CORO_WEAK_AWAIT(tmp_weak, tmp_coro, auto_r, r, shared, coro, ...) \ ({ \ auto tmp_weak = std::weak_ptr{shared}; \ + /* coroutine constructor may need `shared` alive */ \ auto tmp_coro = (coro); \ + /* reset `shared` after coroutine is constructed */ \ shared.reset(); \ auto_r co_await std::move(tmp_coro); \ shared = tmp_weak.lock(); \ - if (not shared) co_return __VA_ARGS__; \ + if (not shared) { \ + co_return __VA_ARGS__; \ + } \ r \ }) #define CORO_WEAK_AWAIT(shared, coro, ...) \ From 34c487ae4e5670632c41153400376467ae3cfa48 Mon Sep 17 00:00:00 2001 From: turuslan Date: Tue, 18 Feb 2025 12:43:22 +0500 Subject: [PATCH 29/40] executor constraint Signed-off-by: turuslan --- src/coro/spawn.hpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/coro/spawn.hpp b/src/coro/spawn.hpp index c972b5d..8829acd 100644 --- a/src/coro/spawn.hpp +++ b/src/coro/spawn.hpp @@ -13,7 +13,13 @@ #include "coro/coro.hpp" namespace jam { - void coroSpawn(auto &&executor, Coro &&coro) { + template + concept CoroSpawnExecutor = + boost::asio::is_executor::value + || boost::asio::execution::is_executor::value + || std::is_convertible_v; + + void coroSpawn(CoroSpawnExecutor auto &&executor, Coro &&coro) { boost::asio::co_spawn(std::forward(executor), std::move(coro), [](std::exception_ptr e) { @@ -24,7 +30,7 @@ namespace jam { } template - void coroSpawn(auto &&executor, Coro &&coro) { + void coroSpawn(CoroSpawnExecutor auto &&executor, Coro &&coro) { coroSpawn(std::forward(executor), [coro{std::move(coro)}]() mutable -> Coro { std::ignore = co_await std::move(coro); @@ -41,7 +47,7 @@ namespace jam { * `co_spawn([](args){ ... }(capture))` * works because arguments are stored in coroutine state. */ - void coroSpawn(auto &&executor, auto &&f) { + void coroSpawn(CoroSpawnExecutor auto &&executor, auto &&f) { coroSpawn(std::forward(executor), [](std::remove_cvref_t f) -> Coro { if constexpr (std::is_void_v) { From 8e9d06e66a13712a0148c7fd8035c6ecfb7488d5 Mon Sep 17 00:00:00 2001 From: turuslan Date: Tue, 18 Feb 2025 14:51:43 +0500 Subject: [PATCH 30/40] optional Signed-off-by: turuslan --- example/snp_chat/main.cpp | 15 ++-- src/TODO_qtils/from_span.hpp | 3 +- src/TODO_qtils/optional.hpp | 79 ++++++++++++++++++ src/crypto/ed25519.hpp | 10 +-- src/snp/connections/config.hpp | 2 +- src/snp/connections/connections.cpp | 2 +- src/snp/connections/connections.hpp | 2 +- src/snp/connections/lsquic/engine.cpp | 115 +++++++++++++------------- src/snp/connections/lsquic/engine.hpp | 27 +++--- 9 files changed, 167 insertions(+), 88 deletions(-) create mode 100644 src/TODO_qtils/optional.hpp diff --git a/example/snp_chat/main.cpp b/example/snp_chat/main.cpp index f8ae088..0a76155 100644 --- a/example/snp_chat/main.cpp +++ b/example/snp_chat/main.cpp @@ -107,7 +107,7 @@ struct ChatController : ConnectionsController { fmt::println("#{} > {}", i_msg, msg); } - Coro broadcast(std::optional i_read, + Coro broadcast(qtils::Optional i_read, size_t i_msg, std::string msg) { for (auto &[i_write, writer] : writers) { @@ -152,7 +152,7 @@ struct ChatController : ConnectionsController { struct Input { Input(IoContextPtr io_context_ptr) : fd_{*io_context_ptr, STDIN_FILENO} {} - Coro> read() { + Coro> read() { auto [ec, n] = co_await boost::asio::async_read_until( fd_, buf_, "\n", boost::asio::as_tuple(boost::asio::use_awaitable)); if (ec) { @@ -175,7 +175,7 @@ struct Input { CoroOutcome co_main(IoContextPtr io_context_ptr, size_t arg_i) { fmt::println("#{} (self)", arg_i); - std::optional listen_port; + qtils::Optional listen_port; GenesisHash genesis; ConnectionsConfig config{genesis, keys.at(arg_i)}; auto is_server = arg_i == 0; @@ -189,7 +189,7 @@ CoroOutcome co_main(IoContextPtr io_context_ptr, size_t arg_i) { Input input{io_context_ptr}; while (true) { auto msg = co_await input.read(); - if (not msg) { + if (not msg.has_value()) { break; } msg->resize(std::min(msg->size(), ChatController::kMaxMsg)); @@ -216,10 +216,9 @@ CoroOutcome co_main(IoContextPtr io_context_ptr, size_t arg_i) { [chat](ConnectionInfo info, StreamPtr stream) -> CoroOutcome { co_return co_await chat->add(info, stream); }); - std::optional> work_guard; - co_await coroHandler([&](CoroHandler &&handler) { - work_guard.emplace(std::move(handler)); - }); + qtils::Optional> work_guard; + co_await coroHandler( + [&](CoroHandler &&handler) { work_guard = std::move(handler); }); } co_return outcome::success(); } diff --git a/src/TODO_qtils/from_span.hpp b/src/TODO_qtils/from_span.hpp index 0a5636a..f169208 100644 --- a/src/TODO_qtils/from_span.hpp +++ b/src/TODO_qtils/from_span.hpp @@ -10,6 +10,7 @@ #include #include +#include #include namespace qtils { @@ -22,7 +23,7 @@ namespace qtils { } template - std::optional fromSpan(BytesIn span) { + Optional fromSpan(BytesIn span) { T out; if (not fromSpan(out, span)) { return std::nullopt; diff --git a/src/TODO_qtils/optional.hpp b/src/TODO_qtils/optional.hpp new file mode 100644 index 0000000..f549cd3 --- /dev/null +++ b/src/TODO_qtils/optional.hpp @@ -0,0 +1,79 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace qtils { + /** + * `std::optional` wrapper. + * Becomes `std::nullopt` when moved from. + * `std::nullopt` throws on access to value. + * Requires to write `.has_value()`. + * Can be move assigned for move only types. + */ + template + class Optional { + public: + Optional() : value_{std::nullopt} {} + + Optional(const std::nullopt_t &) : value_{std::nullopt} {} + + Optional(const T &value) : value_{value} {} + void operator=(const T &value) { + value_.emplace(value); + } + + Optional(T &&value) : value_{std::move(value)} {} + void operator=(T &&value) { + value_.emplace(std::move(value)); + } + + Optional(const Optional &other) : value_{other.value_} {} + void operator=(const Optional &other) { + value_ = other.value_; + } + + Optional(Optional &&other) + : value_{std::exchange(other.value_, std::nullopt)} {} + void operator=(Optional &&other) { + if (other.value_.has_value()) { + value_.emplace(std::exchange(other.value_, std::nullopt).value()); + } else { + value_.reset(); + } + } + + bool has_value() const { + return value_.has_value(); + } + + T &operator*() { + return value_.value(); + } + const T &operator*() const { + return value_.value(); + } + T *operator->() { + return &value_.value(); + } + const T *operator->() const { + return &value_.value(); + } + + Optional take() { + return std::exchange(*this, Optional{}); + } + + bool operator==(const T &value) const { + return value_ == value; + } + + private: + std::optional value_; + }; +} // namespace qtils diff --git a/src/crypto/ed25519.hpp b/src/crypto/ed25519.hpp index 441bf9e..6624ad4 100644 --- a/src/crypto/ed25519.hpp +++ b/src/crypto/ed25519.hpp @@ -47,14 +47,12 @@ namespace jam::crypto::ed25519 { } inline Public get_public(const KeyPair &keypair) { - return qtils::fromSpan( - std::span{keypair}.subspan(ED25519_SECRET_KEY_LENGTH)) - .value(); + return *qtils::fromSpan( + std::span{keypair}.subspan(ED25519_SECRET_KEY_LENGTH)); } inline Public get_secret(const KeyPair &keypair) { - return qtils::fromSpan( - std::span{keypair}.first(ED25519_SECRET_KEY_LENGTH)) - .value(); + return *qtils::fromSpan( + std::span{keypair}.first(ED25519_SECRET_KEY_LENGTH)); } } // namespace jam::crypto::ed25519 diff --git a/src/snp/connections/config.hpp b/src/snp/connections/config.hpp index ad421de..f25682f 100644 --- a/src/snp/connections/config.hpp +++ b/src/snp/connections/config.hpp @@ -15,6 +15,6 @@ namespace jam::snp { // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L30-L35 GenesisHash genesis; crypto::ed25519::KeyPair keypair; - std::optional listen_port; + qtils::Optional listen_port; }; } // namespace jam::snp diff --git a/src/snp/connections/connections.cpp b/src/snp/connections/connections.cpp index 6ead48e..b213d1b 100644 --- a/src/snp/connections/connections.cpp +++ b/src/snp/connections/connections.cpp @@ -45,7 +45,7 @@ namespace jam::snp { certificate, std::nullopt, self)); - if (self->config_.listen_port) { + if (self->config_.listen_port.has_value()) { BOOST_OUTCOME_CO_TRY(self->server_, lsquic::Engine::make(self->io_context_ptr_, self->connection_id_counter_, diff --git a/src/snp/connections/connections.hpp b/src/snp/connections/connections.hpp index 34a0af4..e03b094 100644 --- a/src/snp/connections/connections.hpp +++ b/src/snp/connections/connections.hpp @@ -81,7 +81,7 @@ namespace jam::snp { Key key_; std::weak_ptr controller_; std::shared_ptr client_; - std::optional> server_; + qtils::Optional> server_; std::unordered_map, qtils::BytesStdHash> diff --git a/src/snp/connections/lsquic/engine.cpp b/src/snp/connections/lsquic/engine.cpp index f72663f..dca77d1 100644 --- a/src/snp/connections/lsquic/engine.cpp +++ b/src/snp/connections/lsquic/engine.cpp @@ -53,12 +53,12 @@ namespace jam::snp::lsquic { IoContextPtr io_context_ptr, ConnectionIdCounter connection_id_counter, TlsCertificate certificate, - std::optional listen_port, + qtils::Optional listen_port, std::weak_ptr controller) { OUTCOME_TRY(init()); uint32_t flags = 0; - if (listen_port) { + if (listen_port.has_value()) { flags |= LSENG_SERVER; } @@ -89,9 +89,9 @@ namespace jam::snp::lsquic { if (ec) { return ec; } - if (listen_port) { + if (listen_port.has_value()) { auto ip = boost::asio::ip::address_v6::any(); - socket.bind({ip, listen_port.value()}, ec); + socket.bind({ip, *listen_port}, ec); if (ec) { return ec; } @@ -156,15 +156,15 @@ namespace jam::snp::lsquic { ConnectionPtrCoroOutcome Engine::connect(SelfSPtr self, Address address) { co_await setCoroThread(self->io_context_ptr_); - if (self->connecting_) { + if (self->connecting_.has_value()) { co_return ConnectionsError::ENGINE_CONNECT_ALREADY; } co_return co_await coroHandler( [&](CoroHandler &&handler) { - self->connecting_.emplace(Connecting{ + self->connecting_ = Connecting{ .address = address, .handler = std::move(handler), - }); + }; // will call `Engine::ea_get_ssl_ctx`, `Engine::on_new_conn`. lsquic_engine_connect(self->engine_, N_LSQVER, @@ -178,7 +178,8 @@ namespace jam::snp::lsquic { 0, nullptr, 0); - if (auto connecting = qtils::optionTake(self->connecting_)) { + if (auto connecting = self->connecting_.take(); + connecting.has_value()) { connecting->handler(LsQuicError::lsquic_engine_connect); } self->wantProcess(); @@ -190,10 +191,10 @@ namespace jam::snp::lsquic { return; } stream_ctx->want_flush = true; - if (not stream_ctx->stream) { + if (not stream_ctx->stream.has_value()) { return; } - want_flush_.emplace_back(stream_ctx->stream.value()); + want_flush_.emplace_back(*stream_ctx->stream); wantProcess(); } @@ -219,11 +220,11 @@ namespace jam::snp::lsquic { if (not stream) { continue; } - if (not stream->stream_ctx_->ls_stream) { + if (not stream->stream_ctx_->ls_stream.has_value()) { continue; } stream->stream_ctx_->want_flush = false; - lsquic_stream_flush(stream->stream_ctx_->ls_stream.value()); + lsquic_stream_flush(*stream->stream_ctx_->ls_stream); } // will call `Engine::on_new_conn`, `Engine::on_conn_closed`, // `Engine::on_new_stream`, `Engine::on_close`, `Engine::on_read`, @@ -288,9 +289,9 @@ namespace jam::snp::lsquic { } void Engine::destroyConnection(ConnCtx *conn_ctx) { - conn_ctx->connection.reset(); - if (conn_ctx->ls_conn) { - lsquic_conn_close(conn_ctx->ls_conn.value()); + conn_ctx->connection.take(); + if (conn_ctx->ls_conn.has_value()) { + lsquic_conn_close(*conn_ctx->ls_conn); } else { tryDelete(conn_ctx); } @@ -298,19 +299,19 @@ namespace jam::snp::lsquic { StreamPtrCoroOutcome Engine::openStream(ConnCtx *conn_ctx, ProtocolId protocol_id) { - if (not conn_ctx->ls_conn) { + if (not conn_ctx->ls_conn.has_value()) { co_return ConnectionsError::CONNECTION_OPEN_CLOSED; } - if (conn_ctx->open_stream) { + if (conn_ctx->open_stream.has_value()) { co_return ConnectionsError::ENGINE_OPEN_STREAM_ALREADY; } - if (lsquic_conn_n_avail_streams(conn_ctx->ls_conn.value()) == 0) { + if (lsquic_conn_n_avail_streams(*conn_ctx->ls_conn) == 0) { co_return ConnectionsError::ENGINE_OPEN_STREAM_TOO_MANY; } conn_ctx->open_stream = nullptr; // will call `Engine::on_new_stream`. - lsquic_conn_make_stream(conn_ctx->ls_conn.value()); - auto stream = qtils::optionTake(conn_ctx->open_stream).value(); + lsquic_conn_make_stream(*conn_ctx->ls_conn); + auto stream = *conn_ctx->open_stream.take(); if (stream == nullptr) { co_return LsQuicError::lsquic_conn_make_stream; } @@ -320,9 +321,9 @@ namespace jam::snp::lsquic { } void Engine::destroyStream(StreamCtx *stream_ctx) { - stream_ctx->stream.reset(); - if (stream_ctx->ls_stream) { - lsquic_stream_close(stream_ctx->ls_stream.value()); + stream_ctx->stream.take(); + if (stream_ctx->ls_stream.has_value()) { + lsquic_stream_close(*stream_ctx->ls_stream); } else { tryDelete(stream_ctx); } @@ -345,29 +346,29 @@ namespace jam::snp::lsquic { } void Engine::streamReadFin(StreamCtx *stream_ctx) { - if (stream_ctx->ls_stream) { - lsquic_stream_shutdown(stream_ctx->ls_stream.value(), SHUT_RD); + if (stream_ctx->ls_stream.has_value()) { + lsquic_stream_shutdown(*stream_ctx->ls_stream, SHUT_RD); } } void Engine::streamWriteFin(StreamCtx *stream_ctx) { - if (stream_ctx->ls_stream) { - lsquic_stream_shutdown(stream_ctx->ls_stream.value(), SHUT_WR); + if (stream_ctx->ls_stream.has_value()) { + lsquic_stream_shutdown(*stream_ctx->ls_stream, SHUT_WR); } } CoroOutcome Engine::streamReadRaw(StreamCtx *stream_ctx, qtils::BytesOut message) { - if (stream_ctx->reading) { + if (stream_ctx->reading.has_value()) { throw std::logic_error{"Engine::streamReadRaw duplicate"}; } auto remaining = message; while (not remaining.empty()) { - [[unlikely]] if (not stream_ctx->ls_stream) { + [[unlikely]] if (not stream_ctx->ls_stream.has_value()) { co_return ConnectionsError::STREAM_READ_CLOSED; } auto n = lsquic_stream_read( - stream_ctx->ls_stream.value(), remaining.data(), remaining.size()); + *stream_ctx->ls_stream, remaining.data(), remaining.size()); if (n == 0) { if (remaining.size() == message.size()) { co_return false; @@ -380,8 +381,8 @@ namespace jam::snp::lsquic { co_return ConnectionsError::STREAM_READ_CLOSED; } co_await coroHandler([&](CoroHandler &&handler) { - stream_ctx->reading.emplace(std::move(handler)); - lsquic_stream_wantread(stream_ctx->ls_stream.value(), 1); + stream_ctx->reading = std::move(handler); + lsquic_stream_wantread(*stream_ctx->ls_stream, 1); }); continue; } @@ -392,16 +393,16 @@ namespace jam::snp::lsquic { CoroOutcome Engine::streamWriteRaw(StreamCtx *stream_ctx, qtils::BytesIn message) { - if (stream_ctx->writing) { + if (stream_ctx->writing.has_value()) { throw std::logic_error{"Engine::streamWriteRaw duplicate"}; } auto remaining = message; while (not remaining.empty()) { - [[unlikely]] if (not stream_ctx->ls_stream) { + [[unlikely]] if (not stream_ctx->ls_stream.has_value()) { co_return ConnectionsError::STREAM_WRITE_CLOSED; } auto n = lsquic_stream_write( - stream_ctx->ls_stream.value(), remaining.data(), remaining.size()); + *stream_ctx->ls_stream, remaining.data(), remaining.size()); if (n < 0) { co_return ConnectionsError::STREAM_WRITE_CLOSED; } @@ -417,8 +418,8 @@ namespace jam::snp::lsquic { break; } co_await coroHandler([&](CoroHandler &&handler) { - stream_ctx->writing.emplace(std::move(handler)); - lsquic_stream_wantwrite(stream_ctx->ls_stream.value(), 1); + stream_ctx->writing = std::move(handler); + lsquic_stream_wantwrite(*stream_ctx->ls_stream, 1); }); } co_return outcome::success(); @@ -427,7 +428,7 @@ namespace jam::snp::lsquic { lsquic_conn_ctx_t *Engine::on_new_conn(void *void_self, lsquic_conn_t *ls_conn) { Engine *self = static_cast(void_self); - auto connecting = qtils::optionTake(self->connecting_); + auto connecting = self->connecting_.take(); auto is_connecting = connecting.has_value(); // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) auto *conn_ctx = new ConnCtx{ @@ -446,13 +447,13 @@ namespace jam::snp::lsquic { void Engine::on_conn_closed(lsquic_conn_t *ls_conn) { auto *conn_ctx = from_ls(lsquic_conn_get_ctx(ls_conn)); - conn_ctx->ls_conn.reset(); + conn_ctx->ls_conn.take(); lsquic_conn_set_ctx(ls_conn, nullptr); - if (auto connecting = qtils::optionTake(conn_ctx->connecting)) { + if (auto connecting = conn_ctx->connecting.take(); connecting.has_value()) { connecting->handler(ConnectionsError::ENGINE_CONNECT_CLOSED); } else if (auto self = conn_ctx->engine.lock()) { if (auto controller = self->controller_.lock()) { - controller->onConnectionClose(conn_ctx->info.value()); + controller->onConnectionClose(*conn_ctx->info); } } tryDelete(conn_ctx); @@ -465,13 +466,13 @@ namespace jam::snp::lsquic { return; } auto ok = status == LSQ_HSK_OK or status == LSQ_HSK_RESUMED_OK; - auto connecting = qtils::optionTake(conn_ctx->connecting); + auto connecting = conn_ctx->connecting.take(); auto connection_result = [&]() -> ConnectionPtrOutcome { if (not ok) { return ConnectionsError::HANDSHAKE_FAILED; } OUTCOME_TRY(key, TlsCertificate::get_key(lsquic_conn_ssl(ls_conn))); - if (connecting and key != connecting->address.key) { + if (connecting.has_value() and key != connecting->address.key) { return ConnectionsError::ENGINE_CONNECT_KEY_MISMATCH; } conn_ctx->info = ConnectionInfo{ @@ -479,14 +480,14 @@ namespace jam::snp::lsquic { .key = key, }; auto connection = std::make_shared( - self->io_context_ptr_, conn_ctx, conn_ctx->info.value()); + self->io_context_ptr_, conn_ctx, *conn_ctx->info); conn_ctx->connection = connection; return connection; }(); if (not connection_result) { lsquic_conn_close(ls_conn); } - if (connecting) { + if (connecting.has_value()) { connecting->handler(std::move(connection_result)); } else if (connection_result) { auto &connection = connection_result.value(); @@ -507,15 +508,15 @@ namespace jam::snp::lsquic { .ls_stream = ls_stream, }; ConnectionPtr connection; - if (conn_ctx->connection) { + if (conn_ctx->connection.has_value()) { connection = conn_ctx->connection->lock(); } if (connection) { auto stream = std::make_shared( self->io_context_ptr_, connection, stream_ctx); stream_ctx->stream = stream; - if (conn_ctx->open_stream) { - conn_ctx->open_stream.value() = stream; + if (conn_ctx->open_stream.has_value()) { + conn_ctx->open_stream = stream; } else { self->streamAccept(std::move(stream)); } @@ -528,12 +529,12 @@ namespace jam::snp::lsquic { void Engine::on_close(lsquic_stream_t *ls_stream, lsquic_stream_ctx_t *ls_stream_ctx) { auto *stream_ctx = from_ls(ls_stream_ctx); - stream_ctx->ls_stream.reset(); - if (auto reading = qtils::optionTake(stream_ctx->reading)) { - reading.value()(); + stream_ctx->ls_stream.take(); + if (auto reading = stream_ctx->reading.take(); reading.has_value()) { + (*reading)(); } - if (auto writing = qtils::optionTake(stream_ctx->writing)) { - writing.value()(); + if (auto writing = stream_ctx->writing.take(); writing.has_value()) { + (*writing)(); } tryDelete(stream_ctx); } @@ -542,8 +543,8 @@ namespace jam::snp::lsquic { lsquic_stream_ctx_t *ls_stream_ctx) { lsquic_stream_wantread(ls_stream, 0); auto *stream_ctx = from_ls(ls_stream_ctx); - if (auto reading = qtils::optionTake(stream_ctx->reading)) { - reading.value()(); + if (auto reading = stream_ctx->reading.take(); reading.has_value()) { + (*reading)(); } } @@ -551,8 +552,8 @@ namespace jam::snp::lsquic { lsquic_stream_ctx_t *ls_stream_ctx) { lsquic_stream_wantwrite(ls_stream, 0); auto *stream_ctx = from_ls(ls_stream_ctx); - if (auto writing = qtils::optionTake(stream_ctx->writing)) { - writing.value()(); + if (auto writing = stream_ctx->writing.take(); writing.has_value()) { + (*writing)(); } } diff --git a/src/snp/connections/lsquic/engine.hpp b/src/snp/connections/lsquic/engine.hpp index acd8e48..d5fce92 100644 --- a/src/snp/connections/lsquic/engine.hpp +++ b/src/snp/connections/lsquic/engine.hpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -53,14 +54,14 @@ namespace jam::snp::lsquic { using Ls = lsquic_conn_ctx_t; std::weak_ptr engine; - std::optional ls_conn; - std::optional> connection; - std::optional connecting; - std::optional info; - std::optional open_stream; + qtils::Optional ls_conn; + qtils::Optional> connection; + qtils::Optional connecting; + qtils::Optional info; + qtils::Optional open_stream; bool canDelete() const { - return not ls_conn and not connection; + return not ls_conn.has_value() and not connection.has_value(); } }; @@ -71,14 +72,14 @@ namespace jam::snp::lsquic { using Ls = lsquic_stream_ctx_t; std::weak_ptr engine; - std::optional ls_stream; - std::optional> stream; - std::optional> reading; - std::optional> writing; + qtils::Optional ls_stream; + qtils::Optional> stream; + qtils::Optional> reading; + qtils::Optional> writing; bool want_flush = false; bool canDelete() const { - return not ls_stream and not stream; + return not ls_stream.has_value() and not stream.has_value(); } }; @@ -95,7 +96,7 @@ namespace jam::snp::lsquic { IoContextPtr io_context_ptr, ConnectionIdCounter connection_id_counter, TlsCertificate certificate, - std::optional listen_port, + qtils::Optional listen_port, std::weak_ptr controller); Engine(Private, IoContextPtr io_context_ptr, @@ -190,7 +191,7 @@ namespace jam::snp::lsquic { boost::asio::steady_timer timer_; lsquic_engine_t *engine_ = nullptr; Reading reading_; - std::optional connecting_; + qtils::Optional connecting_; std::deque> want_flush_; bool want_process_ = false; }; From 4c9e48b1df64480bd525c28bbd9fe8f6efa59430 Mon Sep 17 00:00:00 2001 From: turuslan Date: Tue, 25 Feb 2025 13:57:57 +0500 Subject: [PATCH 31/40] revert optional Signed-off-by: turuslan --- example/snp_chat/main.cpp | 13 +++-- src/TODO_qtils/from_span.hpp | 3 +- src/TODO_qtils/optional.hpp | 79 -------------------------- src/crypto/ed25519.hpp | 10 ++-- src/snp/connections/config.hpp | 2 +- src/snp/connections/connections.hpp | 2 +- src/snp/connections/lsquic/engine.cpp | 81 +++++++++++++-------------- src/snp/connections/lsquic/engine.hpp | 23 ++++---- 8 files changed, 67 insertions(+), 146 deletions(-) delete mode 100644 src/TODO_qtils/optional.hpp diff --git a/example/snp_chat/main.cpp b/example/snp_chat/main.cpp index 0a76155..e94b86d 100644 --- a/example/snp_chat/main.cpp +++ b/example/snp_chat/main.cpp @@ -107,7 +107,7 @@ struct ChatController : ConnectionsController { fmt::println("#{} > {}", i_msg, msg); } - Coro broadcast(qtils::Optional i_read, + Coro broadcast(std::optional i_read, size_t i_msg, std::string msg) { for (auto &[i_write, writer] : writers) { @@ -152,7 +152,7 @@ struct ChatController : ConnectionsController { struct Input { Input(IoContextPtr io_context_ptr) : fd_{*io_context_ptr, STDIN_FILENO} {} - Coro> read() { + Coro> read() { auto [ec, n] = co_await boost::asio::async_read_until( fd_, buf_, "\n", boost::asio::as_tuple(boost::asio::use_awaitable)); if (ec) { @@ -175,7 +175,7 @@ struct Input { CoroOutcome co_main(IoContextPtr io_context_ptr, size_t arg_i) { fmt::println("#{} (self)", arg_i); - qtils::Optional listen_port; + std::optional listen_port; GenesisHash genesis; ConnectionsConfig config{genesis, keys.at(arg_i)}; auto is_server = arg_i == 0; @@ -216,9 +216,10 @@ CoroOutcome co_main(IoContextPtr io_context_ptr, size_t arg_i) { [chat](ConnectionInfo info, StreamPtr stream) -> CoroOutcome { co_return co_await chat->add(info, stream); }); - qtils::Optional> work_guard; - co_await coroHandler( - [&](CoroHandler &&handler) { work_guard = std::move(handler); }); + std::optional> work_guard; + co_await coroHandler([&](CoroHandler &&handler) { + work_guard.emplace(std::move(handler)); + }); } co_return outcome::success(); } diff --git a/src/TODO_qtils/from_span.hpp b/src/TODO_qtils/from_span.hpp index f169208..0a5636a 100644 --- a/src/TODO_qtils/from_span.hpp +++ b/src/TODO_qtils/from_span.hpp @@ -10,7 +10,6 @@ #include #include -#include #include namespace qtils { @@ -23,7 +22,7 @@ namespace qtils { } template - Optional fromSpan(BytesIn span) { + std::optional fromSpan(BytesIn span) { T out; if (not fromSpan(out, span)) { return std::nullopt; diff --git a/src/TODO_qtils/optional.hpp b/src/TODO_qtils/optional.hpp deleted file mode 100644 index f549cd3..0000000 --- a/src/TODO_qtils/optional.hpp +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright Quadrivium LLC - * All Rights Reserved - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include - -namespace qtils { - /** - * `std::optional` wrapper. - * Becomes `std::nullopt` when moved from. - * `std::nullopt` throws on access to value. - * Requires to write `.has_value()`. - * Can be move assigned for move only types. - */ - template - class Optional { - public: - Optional() : value_{std::nullopt} {} - - Optional(const std::nullopt_t &) : value_{std::nullopt} {} - - Optional(const T &value) : value_{value} {} - void operator=(const T &value) { - value_.emplace(value); - } - - Optional(T &&value) : value_{std::move(value)} {} - void operator=(T &&value) { - value_.emplace(std::move(value)); - } - - Optional(const Optional &other) : value_{other.value_} {} - void operator=(const Optional &other) { - value_ = other.value_; - } - - Optional(Optional &&other) - : value_{std::exchange(other.value_, std::nullopt)} {} - void operator=(Optional &&other) { - if (other.value_.has_value()) { - value_.emplace(std::exchange(other.value_, std::nullopt).value()); - } else { - value_.reset(); - } - } - - bool has_value() const { - return value_.has_value(); - } - - T &operator*() { - return value_.value(); - } - const T &operator*() const { - return value_.value(); - } - T *operator->() { - return &value_.value(); - } - const T *operator->() const { - return &value_.value(); - } - - Optional take() { - return std::exchange(*this, Optional{}); - } - - bool operator==(const T &value) const { - return value_ == value; - } - - private: - std::optional value_; - }; -} // namespace qtils diff --git a/src/crypto/ed25519.hpp b/src/crypto/ed25519.hpp index 6624ad4..441bf9e 100644 --- a/src/crypto/ed25519.hpp +++ b/src/crypto/ed25519.hpp @@ -47,12 +47,14 @@ namespace jam::crypto::ed25519 { } inline Public get_public(const KeyPair &keypair) { - return *qtils::fromSpan( - std::span{keypair}.subspan(ED25519_SECRET_KEY_LENGTH)); + return qtils::fromSpan( + std::span{keypair}.subspan(ED25519_SECRET_KEY_LENGTH)) + .value(); } inline Public get_secret(const KeyPair &keypair) { - return *qtils::fromSpan( - std::span{keypair}.first(ED25519_SECRET_KEY_LENGTH)); + return qtils::fromSpan( + std::span{keypair}.first(ED25519_SECRET_KEY_LENGTH)) + .value(); } } // namespace jam::crypto::ed25519 diff --git a/src/snp/connections/config.hpp b/src/snp/connections/config.hpp index f25682f..ad421de 100644 --- a/src/snp/connections/config.hpp +++ b/src/snp/connections/config.hpp @@ -15,6 +15,6 @@ namespace jam::snp { // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L30-L35 GenesisHash genesis; crypto::ed25519::KeyPair keypair; - qtils::Optional listen_port; + std::optional listen_port; }; } // namespace jam::snp diff --git a/src/snp/connections/connections.hpp b/src/snp/connections/connections.hpp index e03b094..34a0af4 100644 --- a/src/snp/connections/connections.hpp +++ b/src/snp/connections/connections.hpp @@ -81,7 +81,7 @@ namespace jam::snp { Key key_; std::weak_ptr controller_; std::shared_ptr client_; - qtils::Optional> server_; + std::optional> server_; std::unordered_map, qtils::BytesStdHash> diff --git a/src/snp/connections/lsquic/engine.cpp b/src/snp/connections/lsquic/engine.cpp index dca77d1..1ce4a3a 100644 --- a/src/snp/connections/lsquic/engine.cpp +++ b/src/snp/connections/lsquic/engine.cpp @@ -53,7 +53,7 @@ namespace jam::snp::lsquic { IoContextPtr io_context_ptr, ConnectionIdCounter connection_id_counter, TlsCertificate certificate, - qtils::Optional listen_port, + std::optional listen_port, std::weak_ptr controller) { OUTCOME_TRY(init()); @@ -91,7 +91,7 @@ namespace jam::snp::lsquic { } if (listen_port.has_value()) { auto ip = boost::asio::ip::address_v6::any(); - socket.bind({ip, *listen_port}, ec); + socket.bind({ip, listen_port.value()}, ec); if (ec) { return ec; } @@ -161,10 +161,10 @@ namespace jam::snp::lsquic { } co_return co_await coroHandler( [&](CoroHandler &&handler) { - self->connecting_ = Connecting{ + self->connecting_.emplace(Connecting{ .address = address, .handler = std::move(handler), - }; + }); // will call `Engine::ea_get_ssl_ctx`, `Engine::on_new_conn`. lsquic_engine_connect(self->engine_, N_LSQVER, @@ -178,8 +178,7 @@ namespace jam::snp::lsquic { 0, nullptr, 0); - if (auto connecting = self->connecting_.take(); - connecting.has_value()) { + if (auto connecting = qtils::optionTake(self->connecting_)) { connecting->handler(LsQuicError::lsquic_engine_connect); } self->wantProcess(); @@ -194,7 +193,7 @@ namespace jam::snp::lsquic { if (not stream_ctx->stream.has_value()) { return; } - want_flush_.emplace_back(*stream_ctx->stream); + want_flush_.emplace_back(stream_ctx->stream.value()); wantProcess(); } @@ -224,7 +223,7 @@ namespace jam::snp::lsquic { continue; } stream->stream_ctx_->want_flush = false; - lsquic_stream_flush(*stream->stream_ctx_->ls_stream); + lsquic_stream_flush(stream->stream_ctx_->ls_stream.value()); } // will call `Engine::on_new_conn`, `Engine::on_conn_closed`, // `Engine::on_new_stream`, `Engine::on_close`, `Engine::on_read`, @@ -289,9 +288,9 @@ namespace jam::snp::lsquic { } void Engine::destroyConnection(ConnCtx *conn_ctx) { - conn_ctx->connection.take(); + conn_ctx->connection.reset(); if (conn_ctx->ls_conn.has_value()) { - lsquic_conn_close(*conn_ctx->ls_conn); + lsquic_conn_close(conn_ctx->ls_conn.value()); } else { tryDelete(conn_ctx); } @@ -299,19 +298,19 @@ namespace jam::snp::lsquic { StreamPtrCoroOutcome Engine::openStream(ConnCtx *conn_ctx, ProtocolId protocol_id) { - if (not conn_ctx->ls_conn.has_value()) { + if (not conn_ctx->ls_conn) { co_return ConnectionsError::CONNECTION_OPEN_CLOSED; } - if (conn_ctx->open_stream.has_value()) { + if (conn_ctx->open_stream) { co_return ConnectionsError::ENGINE_OPEN_STREAM_ALREADY; } - if (lsquic_conn_n_avail_streams(*conn_ctx->ls_conn) == 0) { + if (lsquic_conn_n_avail_streams(conn_ctx->ls_conn.value()) == 0) { co_return ConnectionsError::ENGINE_OPEN_STREAM_TOO_MANY; } conn_ctx->open_stream = nullptr; // will call `Engine::on_new_stream`. - lsquic_conn_make_stream(*conn_ctx->ls_conn); - auto stream = *conn_ctx->open_stream.take(); + lsquic_conn_make_stream(conn_ctx->ls_conn.value()); + auto stream = qtils::optionTake(conn_ctx->open_stream).value(); if (stream == nullptr) { co_return LsQuicError::lsquic_conn_make_stream; } @@ -321,9 +320,9 @@ namespace jam::snp::lsquic { } void Engine::destroyStream(StreamCtx *stream_ctx) { - stream_ctx->stream.take(); + stream_ctx->stream.reset(); if (stream_ctx->ls_stream.has_value()) { - lsquic_stream_close(*stream_ctx->ls_stream); + lsquic_stream_close(stream_ctx->ls_stream.value()); } else { tryDelete(stream_ctx); } @@ -347,13 +346,13 @@ namespace jam::snp::lsquic { void Engine::streamReadFin(StreamCtx *stream_ctx) { if (stream_ctx->ls_stream.has_value()) { - lsquic_stream_shutdown(*stream_ctx->ls_stream, SHUT_RD); + lsquic_stream_shutdown(stream_ctx->ls_stream.value(), SHUT_RD); } } void Engine::streamWriteFin(StreamCtx *stream_ctx) { if (stream_ctx->ls_stream.has_value()) { - lsquic_stream_shutdown(*stream_ctx->ls_stream, SHUT_WR); + lsquic_stream_shutdown(stream_ctx->ls_stream.value(), SHUT_WR); } } @@ -368,7 +367,7 @@ namespace jam::snp::lsquic { co_return ConnectionsError::STREAM_READ_CLOSED; } auto n = lsquic_stream_read( - *stream_ctx->ls_stream, remaining.data(), remaining.size()); + stream_ctx->ls_stream.value(), remaining.data(), remaining.size()); if (n == 0) { if (remaining.size() == message.size()) { co_return false; @@ -381,8 +380,8 @@ namespace jam::snp::lsquic { co_return ConnectionsError::STREAM_READ_CLOSED; } co_await coroHandler([&](CoroHandler &&handler) { - stream_ctx->reading = std::move(handler); - lsquic_stream_wantread(*stream_ctx->ls_stream, 1); + stream_ctx->reading.emplace(std::move(handler)); + lsquic_stream_wantread(stream_ctx->ls_stream.value(), 1); }); continue; } @@ -402,7 +401,7 @@ namespace jam::snp::lsquic { co_return ConnectionsError::STREAM_WRITE_CLOSED; } auto n = lsquic_stream_write( - *stream_ctx->ls_stream, remaining.data(), remaining.size()); + stream_ctx->ls_stream.value(), remaining.data(), remaining.size()); if (n < 0) { co_return ConnectionsError::STREAM_WRITE_CLOSED; } @@ -418,8 +417,8 @@ namespace jam::snp::lsquic { break; } co_await coroHandler([&](CoroHandler &&handler) { - stream_ctx->writing = std::move(handler); - lsquic_stream_wantwrite(*stream_ctx->ls_stream, 1); + stream_ctx->writing.emplace(std::move(handler)); + lsquic_stream_wantwrite(stream_ctx->ls_stream.value(), 1); }); } co_return outcome::success(); @@ -428,7 +427,7 @@ namespace jam::snp::lsquic { lsquic_conn_ctx_t *Engine::on_new_conn(void *void_self, lsquic_conn_t *ls_conn) { Engine *self = static_cast(void_self); - auto connecting = self->connecting_.take(); + auto connecting = qtils::optionTake(self->connecting_); auto is_connecting = connecting.has_value(); // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) auto *conn_ctx = new ConnCtx{ @@ -447,13 +446,13 @@ namespace jam::snp::lsquic { void Engine::on_conn_closed(lsquic_conn_t *ls_conn) { auto *conn_ctx = from_ls(lsquic_conn_get_ctx(ls_conn)); - conn_ctx->ls_conn.take(); + conn_ctx->ls_conn.reset(); lsquic_conn_set_ctx(ls_conn, nullptr); - if (auto connecting = conn_ctx->connecting.take(); connecting.has_value()) { + if (auto connecting = qtils::optionTake(conn_ctx->connecting)) { connecting->handler(ConnectionsError::ENGINE_CONNECT_CLOSED); } else if (auto self = conn_ctx->engine.lock()) { if (auto controller = self->controller_.lock()) { - controller->onConnectionClose(*conn_ctx->info); + controller->onConnectionClose(conn_ctx->info.value()); } } tryDelete(conn_ctx); @@ -466,7 +465,7 @@ namespace jam::snp::lsquic { return; } auto ok = status == LSQ_HSK_OK or status == LSQ_HSK_RESUMED_OK; - auto connecting = conn_ctx->connecting.take(); + auto connecting = qtils::optionTake(conn_ctx->connecting); auto connection_result = [&]() -> ConnectionPtrOutcome { if (not ok) { return ConnectionsError::HANDSHAKE_FAILED; @@ -480,7 +479,7 @@ namespace jam::snp::lsquic { .key = key, }; auto connection = std::make_shared( - self->io_context_ptr_, conn_ctx, *conn_ctx->info); + self->io_context_ptr_, conn_ctx, conn_ctx->info.value()); conn_ctx->connection = connection; return connection; }(); @@ -516,7 +515,7 @@ namespace jam::snp::lsquic { self->io_context_ptr_, connection, stream_ctx); stream_ctx->stream = stream; if (conn_ctx->open_stream.has_value()) { - conn_ctx->open_stream = stream; + conn_ctx->open_stream.value() = stream; } else { self->streamAccept(std::move(stream)); } @@ -529,12 +528,12 @@ namespace jam::snp::lsquic { void Engine::on_close(lsquic_stream_t *ls_stream, lsquic_stream_ctx_t *ls_stream_ctx) { auto *stream_ctx = from_ls(ls_stream_ctx); - stream_ctx->ls_stream.take(); - if (auto reading = stream_ctx->reading.take(); reading.has_value()) { - (*reading)(); + stream_ctx->ls_stream.reset(); + if (auto reading = qtils::optionTake(stream_ctx->reading)) { + reading.value()(); } - if (auto writing = stream_ctx->writing.take(); writing.has_value()) { - (*writing)(); + if (auto writing = qtils::optionTake(stream_ctx->writing)) { + writing.value()(); } tryDelete(stream_ctx); } @@ -543,8 +542,8 @@ namespace jam::snp::lsquic { lsquic_stream_ctx_t *ls_stream_ctx) { lsquic_stream_wantread(ls_stream, 0); auto *stream_ctx = from_ls(ls_stream_ctx); - if (auto reading = stream_ctx->reading.take(); reading.has_value()) { - (*reading)(); + if (auto reading = qtils::optionTake(stream_ctx->reading)) { + reading.value()(); } } @@ -552,8 +551,8 @@ namespace jam::snp::lsquic { lsquic_stream_ctx_t *ls_stream_ctx) { lsquic_stream_wantwrite(ls_stream, 0); auto *stream_ctx = from_ls(ls_stream_ctx); - if (auto writing = stream_ctx->writing.take(); writing.has_value()) { - (*writing)(); + if (auto writing = qtils::optionTake(stream_ctx->writing)) { + writing.value()(); } } diff --git a/src/snp/connections/lsquic/engine.hpp b/src/snp/connections/lsquic/engine.hpp index d5fce92..5fc551e 100644 --- a/src/snp/connections/lsquic/engine.hpp +++ b/src/snp/connections/lsquic/engine.hpp @@ -9,7 +9,6 @@ #include #include -#include #include #include @@ -54,11 +53,11 @@ namespace jam::snp::lsquic { using Ls = lsquic_conn_ctx_t; std::weak_ptr engine; - qtils::Optional ls_conn; - qtils::Optional> connection; - qtils::Optional connecting; - qtils::Optional info; - qtils::Optional open_stream; + std::optional ls_conn; + std::optional> connection; + std::optional connecting; + std::optional info; + std::optional open_stream; bool canDelete() const { return not ls_conn.has_value() and not connection.has_value(); @@ -72,10 +71,10 @@ namespace jam::snp::lsquic { using Ls = lsquic_stream_ctx_t; std::weak_ptr engine; - qtils::Optional ls_stream; - qtils::Optional> stream; - qtils::Optional> reading; - qtils::Optional> writing; + std::optional ls_stream; + std::optional> stream; + std::optional> reading; + std::optional> writing; bool want_flush = false; bool canDelete() const { @@ -96,7 +95,7 @@ namespace jam::snp::lsquic { IoContextPtr io_context_ptr, ConnectionIdCounter connection_id_counter, TlsCertificate certificate, - qtils::Optional listen_port, + std::optional listen_port, std::weak_ptr controller); Engine(Private, IoContextPtr io_context_ptr, @@ -191,7 +190,7 @@ namespace jam::snp::lsquic { boost::asio::steady_timer timer_; lsquic_engine_t *engine_ = nullptr; Reading reading_; - qtils::Optional connecting_; + std::optional connecting_; std::deque> want_flush_; bool want_process_ = false; }; From f5b246643c6c799f8a20f612c5e711ab15f3fd96 Mon Sep 17 00:00:00 2001 From: turuslan Date: Wed, 26 Feb 2025 10:15:07 +0500 Subject: [PATCH 32/40] update qtils Signed-off-by: turuslan --- vcpkg-overlay/qtils/portfile.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vcpkg-overlay/qtils/portfile.cmake b/vcpkg-overlay/qtils/portfile.cmake index 661caa8..bbbf345 100644 --- a/vcpkg-overlay/qtils/portfile.cmake +++ b/vcpkg-overlay/qtils/portfile.cmake @@ -2,8 +2,8 @@ vcpkg_check_linkage(ONLY_STATIC_LIBRARY) vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO qdrvm/qtils - REF f5013e52027f6b3014c2ae902a94903583898639 - SHA512 6492edc93437b8edc254014b5face0521f0fd46fce6e6cab4e577fc00b6964b5930c75fd228fd8f3bb10bf57a6dd8722dcf2912a488b3f1174a99ed991ae4c54 + REF ad6c66c18c9e6d50491c42ac74e879f5a5176f51 + SHA512 407619b1aedce2290de0ae90c2363b15fc407ca975ef5df032f449a0d8f9d3c2ac12aef4d2c472237c974164cda4a4ddff874dbe6ef6871114d727d499c399ec ) vcpkg_cmake_configure(SOURCE_PATH "${SOURCE_PATH}") vcpkg_cmake_install() From 927632cc0b9c3efbff8c5df6d8f4ffa99512a698 Mon Sep 17 00:00:00 2001 From: turuslan Date: Wed, 26 Feb 2025 11:06:01 +0500 Subject: [PATCH 33/40] soralog Signed-off-by: turuslan --- src/snp/connections/lsquic/log.hpp | 38 +++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/snp/connections/lsquic/log.hpp b/src/snp/connections/lsquic/log.hpp index e076b60..a473e22 100644 --- a/src/snp/connections/lsquic/log.hpp +++ b/src/snp/connections/lsquic/log.hpp @@ -6,26 +6,42 @@ #pragma once -#include #include +#include "log/logger.hpp" + +namespace jam::snp::lsquic::log_level { + constexpr auto *emerg = "emerg"; + constexpr auto *alert = "alert"; + constexpr auto *crit = "crit"; + constexpr auto *error = "error"; + constexpr auto *warn = "warn"; + constexpr auto *notice = "notice"; + constexpr auto *info = "info"; + constexpr auto *debug = "debug"; +} // namespace jam::snp::lsquic::log_level + namespace jam::snp::lsquic { /** * Enable lsquic log. - * - * Possible levels: "emerg", "alert", "crit", "error", "warn", "notice", - * "info", "debug". */ - inline void log(const char *level = "debug") { - static lsquic_logger_if log{ + inline void log(std::shared_ptr log, + const char *level = log_level::debug) { + static std::shared_ptr static_log; + static lsquic_logger_if ls_log{ +[](void *, const char *buf, size_t len) { - return (int)fwrite(buf, sizeof(char), len, stdout); + if (static_log != nullptr) { + std::string_view message{buf, len}; + while (message.ends_with("\n")) { + message.remove_suffix(1); + } + static_log->info("{}", message); + } + return 0; }, }; - static auto init = [] { - lsquic_logger_init(&log, nullptr, LLTS_HHMMSSMS); - return 0; - }(); + static_log = std::move(log); + lsquic_logger_init(&ls_log, nullptr, LLTS_NONE); lsquic_set_log_level(level); } } // namespace jam::snp::lsquic From e97ecc88e67722399a21b7f6ef8ea3e3685bc046 Mon Sep 17 00:00:00 2001 From: turuslan Date: Wed, 26 Feb 2025 11:08:26 +0500 Subject: [PATCH 34/40] if Signed-off-by: turuslan --- src/snp/connections/lsquic/engine.cpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/snp/connections/lsquic/engine.cpp b/src/snp/connections/lsquic/engine.cpp index 1ce4a3a..44e6580 100644 --- a/src/snp/connections/lsquic/engine.cpp +++ b/src/snp/connections/lsquic/engine.cpp @@ -120,11 +120,9 @@ namespace jam::snp::lsquic { } io_context_ptr->post([weak_self{std::weak_ptr{self}}] { - auto self = weak_self.lock(); - if (not self) { - return; + if (auto self = weak_self.lock()) { + self->readLoop(); } - self->readLoop(); }); return self; @@ -203,11 +201,9 @@ namespace jam::snp::lsquic { } want_process_ = true; boost::asio::post(*io_context_ptr_, [weak_self{weak_from_this()}] { - auto self = weak_self.lock(); - if (not self) { - return; + if (auto self = weak_self.lock()) { + self->process(); } - self->process(); }); } From 457252998bb7a6bb43b53fdde8b801260b34790e Mon Sep 17 00:00:00 2001 From: turuslan Date: Thu, 27 Feb 2025 10:32:09 +0500 Subject: [PATCH 35/40] make shared Signed-off-by: turuslan --- src/snp/connections/lsquic/engine.cpp | 16 +++++++--------- src/snp/connections/lsquic/engine.hpp | 6 +++--- vcpkg-overlay/qtils/portfile.cmake | 4 ++-- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/snp/connections/lsquic/engine.cpp b/src/snp/connections/lsquic/engine.cpp index 44e6580..7a89388 100644 --- a/src/snp/connections/lsquic/engine.cpp +++ b/src/snp/connections/lsquic/engine.cpp @@ -100,13 +100,12 @@ namespace jam::snp::lsquic { if (ec) { return ec; } - auto self = std::make_shared(Private{}, - io_context_ptr, - std::move(connection_id_counter), - std::move(certificate), - std::move(socket), - socket_local_endpoint, - std::move(controller)); + auto self = create_shared(io_context_ptr, + std::move(connection_id_counter), + std::move(certificate), + std::move(socket), + socket_local_endpoint, + std::move(controller)); api.ea_stream_if = &stream_if; api.ea_stream_if_ctx = self.get(); @@ -128,8 +127,7 @@ namespace jam::snp::lsquic { return self; } - Engine::Engine(Private, - IoContextPtr io_context_ptr, + Engine::Engine(IoContextPtr io_context_ptr, ConnectionIdCounter connection_id_counter, TlsCertificate &&certificate, Socket &&socket, diff --git a/src/snp/connections/lsquic/engine.hpp b/src/snp/connections/lsquic/engine.hpp index 5fc551e..38a4f65 100644 --- a/src/snp/connections/lsquic/engine.hpp +++ b/src/snp/connections/lsquic/engine.hpp @@ -11,6 +11,7 @@ #include #include +#include #include "coro/coro.hpp" #include "coro/handler.hpp" @@ -86,7 +87,7 @@ namespace jam::snp::lsquic { friend Connection; friend Stream; - struct Private {}; + CREATE_SHARED_METHOD(Engine); public: using SelfSPtr = std::shared_ptr; @@ -97,8 +98,7 @@ namespace jam::snp::lsquic { TlsCertificate certificate, std::optional listen_port, std::weak_ptr controller); - Engine(Private, - IoContextPtr io_context_ptr, + Engine(IoContextPtr io_context_ptr, ConnectionIdCounter connection_id_counter, TlsCertificate &&certificate, Socket &&socket, diff --git a/vcpkg-overlay/qtils/portfile.cmake b/vcpkg-overlay/qtils/portfile.cmake index bbbf345..1bc76ce 100644 --- a/vcpkg-overlay/qtils/portfile.cmake +++ b/vcpkg-overlay/qtils/portfile.cmake @@ -2,8 +2,8 @@ vcpkg_check_linkage(ONLY_STATIC_LIBRARY) vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO qdrvm/qtils - REF ad6c66c18c9e6d50491c42ac74e879f5a5176f51 - SHA512 407619b1aedce2290de0ae90c2363b15fc407ca975ef5df032f449a0d8f9d3c2ac12aef4d2c472237c974164cda4a4ddff874dbe6ef6871114d727d499c399ec + REF 6d02006f7c0fe44958acbfe53016fa81f1ac0bf5 + SHA512 3e46c767bb30a5e6d1df38e7a4c6fb5ec61c320bbbf6bbe622043402119216a8f7ddc1e6841a26c8848c20f38e1dfbd37f2b505698802e3a95ace464bb8c2cb9 ) vcpkg_cmake_configure(SOURCE_PATH "${SOURCE_PATH}") vcpkg_cmake_install() From bb881b5b6c4016aab16b5e45d85b82d47a23ce7a Mon Sep 17 00:00:00 2001 From: turuslan Date: Thu, 27 Feb 2025 10:42:41 +0500 Subject: [PATCH 36/40] revert "make shared" Signed-off-by: turuslan --- src/snp/connections/lsquic/engine.cpp | 16 +++++++++------- src/snp/connections/lsquic/engine.hpp | 6 +++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/snp/connections/lsquic/engine.cpp b/src/snp/connections/lsquic/engine.cpp index 7a89388..44e6580 100644 --- a/src/snp/connections/lsquic/engine.cpp +++ b/src/snp/connections/lsquic/engine.cpp @@ -100,12 +100,13 @@ namespace jam::snp::lsquic { if (ec) { return ec; } - auto self = create_shared(io_context_ptr, - std::move(connection_id_counter), - std::move(certificate), - std::move(socket), - socket_local_endpoint, - std::move(controller)); + auto self = std::make_shared(Private{}, + io_context_ptr, + std::move(connection_id_counter), + std::move(certificate), + std::move(socket), + socket_local_endpoint, + std::move(controller)); api.ea_stream_if = &stream_if; api.ea_stream_if_ctx = self.get(); @@ -127,7 +128,8 @@ namespace jam::snp::lsquic { return self; } - Engine::Engine(IoContextPtr io_context_ptr, + Engine::Engine(Private, + IoContextPtr io_context_ptr, ConnectionIdCounter connection_id_counter, TlsCertificate &&certificate, Socket &&socket, diff --git a/src/snp/connections/lsquic/engine.hpp b/src/snp/connections/lsquic/engine.hpp index 38a4f65..5fc551e 100644 --- a/src/snp/connections/lsquic/engine.hpp +++ b/src/snp/connections/lsquic/engine.hpp @@ -11,7 +11,6 @@ #include #include -#include #include "coro/coro.hpp" #include "coro/handler.hpp" @@ -87,7 +86,7 @@ namespace jam::snp::lsquic { friend Connection; friend Stream; - CREATE_SHARED_METHOD(Engine); + struct Private {}; public: using SelfSPtr = std::shared_ptr; @@ -98,7 +97,8 @@ namespace jam::snp::lsquic { TlsCertificate certificate, std::optional listen_port, std::weak_ptr controller); - Engine(IoContextPtr io_context_ptr, + Engine(Private, + IoContextPtr io_context_ptr, ConnectionIdCounter connection_id_counter, TlsCertificate &&certificate, Socket &&socket, From 812e33f9f9098c598dc953ea644d59f85514ea5b Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 3 Mar 2025 17:21:51 +0500 Subject: [PATCH 37/40] port type Signed-off-by: turuslan --- example/snp_chat/main.cpp | 3 +-- src/snp/connections/address.hpp | 3 +-- src/snp/connections/config.hpp | 3 +-- src/snp/connections/lsquic/engine.cpp | 2 +- src/snp/connections/lsquic/engine.hpp | 2 +- src/snp/connections/port.hpp | 13 ------------- 6 files changed, 5 insertions(+), 21 deletions(-) delete mode 100644 src/snp/connections/port.hpp diff --git a/example/snp_chat/main.cpp b/example/snp_chat/main.cpp index e94b86d..3552ebd 100644 --- a/example/snp_chat/main.cpp +++ b/example/snp_chat/main.cpp @@ -30,7 +30,6 @@ using jam::snp::Connections; using jam::snp::ConnectionsConfig; using jam::snp::ConnectionsController; using jam::snp::Key; -using jam::snp::Port; using jam::snp::ProtocolId; using jam::snp::StreamPtr; @@ -175,7 +174,7 @@ struct Input { CoroOutcome co_main(IoContextPtr io_context_ptr, size_t arg_i) { fmt::println("#{} (self)", arg_i); - std::optional listen_port; + std::optional listen_port; GenesisHash genesis; ConnectionsConfig config{genesis, keys.at(arg_i)}; auto is_server = arg_i == 0; diff --git a/src/snp/connections/address.hpp b/src/snp/connections/address.hpp index 1e5e8cc..baecd87 100644 --- a/src/snp/connections/address.hpp +++ b/src/snp/connections/address.hpp @@ -7,7 +7,6 @@ #pragma once #include "snp/connections/key.hpp" -#include "snp/connections/port.hpp" namespace jam::snp { struct Address { @@ -15,7 +14,7 @@ namespace jam::snp { static constexpr Ip kLocal{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; Ip ip; - Port port; + uint16_t port; Key key; }; } // namespace jam::snp diff --git a/src/snp/connections/config.hpp b/src/snp/connections/config.hpp index ad421de..a8a813e 100644 --- a/src/snp/connections/config.hpp +++ b/src/snp/connections/config.hpp @@ -7,7 +7,6 @@ #pragma once #include "crypto/ed25519.hpp" -#include "snp/connections/port.hpp" #include "types/genesis_hash.hpp" namespace jam::snp { @@ -15,6 +14,6 @@ namespace jam::snp { // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L30-L35 GenesisHash genesis; crypto::ed25519::KeyPair keypair; - std::optional listen_port; + std::optional listen_port; }; } // namespace jam::snp diff --git a/src/snp/connections/lsquic/engine.cpp b/src/snp/connections/lsquic/engine.cpp index 44e6580..196c366 100644 --- a/src/snp/connections/lsquic/engine.cpp +++ b/src/snp/connections/lsquic/engine.cpp @@ -53,7 +53,7 @@ namespace jam::snp::lsquic { IoContextPtr io_context_ptr, ConnectionIdCounter connection_id_counter, TlsCertificate certificate, - std::optional listen_port, + std::optional listen_port, std::weak_ptr controller) { OUTCOME_TRY(init()); diff --git a/src/snp/connections/lsquic/engine.hpp b/src/snp/connections/lsquic/engine.hpp index 5fc551e..c6ee51e 100644 --- a/src/snp/connections/lsquic/engine.hpp +++ b/src/snp/connections/lsquic/engine.hpp @@ -95,7 +95,7 @@ namespace jam::snp::lsquic { IoContextPtr io_context_ptr, ConnectionIdCounter connection_id_counter, TlsCertificate certificate, - std::optional listen_port, + std::optional listen_port, std::weak_ptr controller); Engine(Private, IoContextPtr io_context_ptr, diff --git a/src/snp/connections/port.hpp b/src/snp/connections/port.hpp deleted file mode 100644 index 8f93034..0000000 --- a/src/snp/connections/port.hpp +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright Quadrivium LLC - * All Rights Reserved - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include - -namespace jam::snp { - using Port = uint16_t; -} // namespace jam::snp From 2eaf302488f72b350277c3d645025a9e6d54c2d0 Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 3 Mar 2025 17:23:57 +0500 Subject: [PATCH 38/40] shutdown Signed-off-by: turuslan --- example/snp_chat/main.cpp | 2 +- src/snp/connections/lsquic/engine.cpp | 4 ++-- src/snp/connections/lsquic/engine.hpp | 4 ++-- src/snp/connections/stream.cpp | 8 ++++---- src/snp/connections/stream.hpp | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/example/snp_chat/main.cpp b/example/snp_chat/main.cpp index 3552ebd..df40b28 100644 --- a/example/snp_chat/main.cpp +++ b/example/snp_chat/main.cpp @@ -143,7 +143,7 @@ struct ChatController : ConnectionsController { co_await onRead( i_read, i_msg, std::string{qtils::byte2str(buffer).substr(1)}); } - co_await stream->readFin(stream); + co_await stream->shutdownRead(stream); co_return outcome::success(); } }; diff --git a/src/snp/connections/lsquic/engine.cpp b/src/snp/connections/lsquic/engine.cpp index 196c366..327c4c6 100644 --- a/src/snp/connections/lsquic/engine.cpp +++ b/src/snp/connections/lsquic/engine.cpp @@ -340,13 +340,13 @@ namespace jam::snp::lsquic { }); } - void Engine::streamReadFin(StreamCtx *stream_ctx) { + void Engine::streamShutdownRead(StreamCtx *stream_ctx) { if (stream_ctx->ls_stream.has_value()) { lsquic_stream_shutdown(stream_ctx->ls_stream.value(), SHUT_RD); } } - void Engine::streamWriteFin(StreamCtx *stream_ctx) { + void Engine::streamShutdownWrite(StreamCtx *stream_ctx) { if (stream_ctx->ls_stream.has_value()) { lsquic_stream_shutdown(stream_ctx->ls_stream.value(), SHUT_WR); } diff --git a/src/snp/connections/lsquic/engine.hpp b/src/snp/connections/lsquic/engine.hpp index c6ee51e..479b2f3 100644 --- a/src/snp/connections/lsquic/engine.hpp +++ b/src/snp/connections/lsquic/engine.hpp @@ -124,8 +124,8 @@ namespace jam::snp::lsquic { ProtocolId protocol_id); static void destroyStream(StreamCtx *stream_ctx); void streamAccept(StreamPtr &&stream); - static void streamReadFin(StreamCtx *stream_ctx); - static void streamWriteFin(StreamCtx *stream_ctx); + static void streamShutdownRead(StreamCtx *stream_ctx); + static void streamShutdownWrite(StreamCtx *stream_ctx); static CoroOutcome streamReadRaw(StreamCtx *stream_ctx, qtils::BytesOut message); static CoroOutcome streamWriteRaw(StreamCtx *stream_ctx, diff --git a/src/snp/connections/stream.cpp b/src/snp/connections/stream.cpp index e0f0c9f..18b9955 100644 --- a/src/snp/connections/stream.cpp +++ b/src/snp/connections/stream.cpp @@ -63,9 +63,9 @@ namespace jam::snp { co_return true; } - Coro Stream::readFin(SelfSPtr self) { + Coro Stream::shutdownRead(SelfSPtr self) { co_await setCoroThread(self->io_context_ptr_); - Engine::streamReadFin(self->stream_ctx_); + Engine::streamShutdownRead(self->stream_ctx_); co_return; } @@ -88,9 +88,9 @@ namespace jam::snp { co_return outcome::success(); } - Coro Stream::writeFin(SelfSPtr self) { + Coro Stream::shutdownWrite(SelfSPtr self) { co_await setCoroThread(self->io_context_ptr_); - Engine::streamWriteFin(self->stream_ctx_); + Engine::streamShutdownWrite(self->stream_ctx_); co_return; } diff --git a/src/snp/connections/stream.hpp b/src/snp/connections/stream.hpp index ae9b29a..9306ce0 100644 --- a/src/snp/connections/stream.hpp +++ b/src/snp/connections/stream.hpp @@ -47,7 +47,7 @@ namespace jam::snp { /** * Close reading side of stream. */ - static Coro readFin(SelfSPtr self); + static Coro shutdownRead(SelfSPtr self); // https://github.com/zdave-parity/jam-np/blob/5d374b53578cdd93646e3ee19e2b19ea132317b8/simple.md?plain=1#L109-L111 /** @@ -59,7 +59,7 @@ namespace jam::snp { * Write fin. * Closes writing side of stream. */ - static Coro writeFin(SelfSPtr self); + static Coro shutdownWrite(SelfSPtr self); private: /** From 56326970577e94329352f49b8fc33fbebde89975 Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 3 Mar 2025 17:31:16 +0500 Subject: [PATCH 39/40] remove Signed-off-by: turuslan --- src/TODO_qtils/map_entry.hpp | 5 +++-- src/snp/connections/connections.cpp | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/TODO_qtils/map_entry.hpp b/src/TODO_qtils/map_entry.hpp index ea24aee..96c624a 100644 --- a/src/TODO_qtils/map_entry.hpp +++ b/src/TODO_qtils/map_entry.hpp @@ -61,9 +61,10 @@ namespace qtils { **this = std::move(value); } } - M::mapped_type remove() { + /// Remove from map and return value. + M::mapped_type extract() { if (not has()) { - throw std::logic_error{"MapEntry::remove"}; + throw std::logic_error{"MapEntry::extract"}; } auto node = map.extract(std::get(it_or_key)); it_or_key = std::move(node.key()); diff --git a/src/snp/connections/connections.cpp b/src/snp/connections/connections.cpp index b213d1b..25ead47 100644 --- a/src/snp/connections/connections.cpp +++ b/src/snp/connections/connections.cpp @@ -87,7 +87,7 @@ namespace jam::snp { controller->onOpen(address.key); } } else { - state.remove(); + state.extract(); } CORO_WEAK_AWAIT_V( self, connecting->set(connecting, std::move(connection_result))); @@ -124,7 +124,7 @@ namespace jam::snp { or std::get(*state)->info() != connection_info) { todoPreferConnection(); } - state.remove(); + state.extract(); if (auto controller = controller_.lock()) { controller->onClose(connection_info.key); } From 790ce8774e7bbc9480ed57628dcbb72bc63ba726 Mon Sep 17 00:00:00 2001 From: turuslan Date: Mon, 3 Mar 2025 17:51:51 +0500 Subject: [PATCH 40/40] log socket read/write error Signed-off-by: turuslan --- example/config.yaml | 7 +++--- example/snp_chat/CMakeLists.txt | 1 + example/snp_chat/main.cpp | 6 ++++- src/log/simple.hpp | 35 +++++++++++++++++++++++++++ src/snp/connections/connections.cpp | 4 +++ src/snp/connections/connections.hpp | 9 ++++++- src/snp/connections/lsquic/engine.cpp | 7 ++++++ src/snp/connections/lsquic/engine.hpp | 11 +++++++++ 8 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 src/log/simple.hpp diff --git a/example/config.yaml b/example/config.yaml index 1992f97..091782d 100644 --- a/example/config.yaml +++ b/example/config.yaml @@ -22,8 +22,9 @@ logging: children: - name: jam children: - - name: injector - name: application - - name: rpc + - name: injector - name: metrics - - name: threads \ No newline at end of file + - name: rpc + - name: snp + - name: threads diff --git a/example/snp_chat/CMakeLists.txt b/example/snp_chat/CMakeLists.txt index d082c3f..3c8c9e2 100644 --- a/example/snp_chat/CMakeLists.txt +++ b/example/snp_chat/CMakeLists.txt @@ -8,6 +8,7 @@ add_executable(example_snp_chat main.cpp ) target_link_libraries(example_snp_chat + logger snp ) diff --git a/example/snp_chat/main.cpp b/example/snp_chat/main.cpp index df40b28..8187c00 100644 --- a/example/snp_chat/main.cpp +++ b/example/snp_chat/main.cpp @@ -10,6 +10,7 @@ #include #include "coro/spawn.hpp" +#include "log/simple.hpp" #include "snp/connections/address.hpp" #include "snp/connections/connection.hpp" #include "snp/connections/connections.hpp" @@ -33,6 +34,8 @@ using jam::snp::Key; using jam::snp::ProtocolId; using jam::snp::StreamPtr; +auto logsys = jam::log::simpleLoggingSystem(); + inline auto operator""_ed25519(const char *c, size_t s) { auto seed = qtils::unhex({c, s}).value(); return jam::crypto::ed25519::from_seed(seed); @@ -181,7 +184,8 @@ CoroOutcome co_main(IoContextPtr io_context_ptr, size_t arg_i) { if (is_server) { config.listen_port = server_address.port; } - auto connections = std::make_shared(io_context_ptr, config); + auto connections = + std::make_shared(io_context_ptr, logsys, config); auto chat = std::make_shared(); BOOST_OUTCOME_CO_TRY(co_await connections->init(connections, chat)); co_await coroSpawn([io_context_ptr, arg_i, chat]() -> Coro { diff --git a/src/log/simple.hpp b/src/log/simple.hpp new file mode 100644 index 0000000..732c486 --- /dev/null +++ b/src/log/simple.hpp @@ -0,0 +1,35 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "log/logger.hpp" + +namespace jam::log { + std::shared_ptr simpleLoggingSystem() { + std::string yaml = R"( + sinks: + - name: console + type: console + capacity: 4 + latency: 0 + groups: + - name: main + sink: console + level: info + is_fallback: true + )"; + auto logsys = std::make_shared( + std::make_shared( + std::shared_ptr(nullptr), yaml)); + if (auto r = logsys->configure().message; not r.empty()) { + fmt::println(stderr, "soralog error: {}", r); + } + return std::make_shared(logsys); + } +} // namespace jam::log diff --git a/src/snp/connections/connections.cpp b/src/snp/connections/connections.cpp index 25ead47..abb2e06 100644 --- a/src/snp/connections/connections.cpp +++ b/src/snp/connections/connections.cpp @@ -27,8 +27,10 @@ namespace jam::snp { } Connections::Connections(IoContextPtr io_context_ptr, + std::shared_ptr logsys, ConnectionsConfig config) : io_context_ptr_{std::move(io_context_ptr)}, + logsys_{std::move(logsys)}, init_{io_context_ptr_}, config_{std::move(config)}, key_{crypto::ed25519::get_public(config_.keypair)} {} @@ -41,6 +43,7 @@ namespace jam::snp { BOOST_OUTCOME_CO_TRY(auto certificate, TlsCertificate::make(self->config_)); BOOST_OUTCOME_CO_TRY(self->client_, lsquic::Engine::make(self->io_context_ptr_, + self->logsys_, self->connection_id_counter_, certificate, std::nullopt, @@ -48,6 +51,7 @@ namespace jam::snp { if (self->config_.listen_port.has_value()) { BOOST_OUTCOME_CO_TRY(self->server_, lsquic::Engine::make(self->io_context_ptr_, + self->logsys_, self->connection_id_counter_, certificate, self->config_.listen_port, diff --git a/src/snp/connections/connections.hpp b/src/snp/connections/connections.hpp index 34a0af4..6d50c32 100644 --- a/src/snp/connections/connections.hpp +++ b/src/snp/connections/connections.hpp @@ -20,6 +20,10 @@ #include "snp/connections/key.hpp" #include "snp/connections/lsquic/controller.hpp" +namespace jam::log { + class LoggingSystem; +} // namespace jam::log + namespace jam::snp { class Address; class ConnectionsController; @@ -39,7 +43,9 @@ namespace jam::snp { public: using SelfSPtr = std::shared_ptr; - Connections(IoContextPtr io_context_ptr, ConnectionsConfig config); + Connections(IoContextPtr io_context_ptr, + std::shared_ptr logsys, + ConnectionsConfig config); /** * Set controller. @@ -76,6 +82,7 @@ namespace jam::snp { using Connected = ConnectionPtr; IoContextPtr io_context_ptr_; + std::shared_ptr logsys_; CoroInit init_; ConnectionsConfig config_; Key key_; diff --git a/src/snp/connections/lsquic/engine.cpp b/src/snp/connections/lsquic/engine.cpp index 327c4c6..04bdeaf 100644 --- a/src/snp/connections/lsquic/engine.cpp +++ b/src/snp/connections/lsquic/engine.cpp @@ -11,6 +11,7 @@ #include "coro/set_thread.hpp" #include "coro/spawn.hpp" +#include "log/logger.hpp" #include "snp/connections/config.hpp" #include "snp/connections/connection.hpp" #include "snp/connections/error.hpp" @@ -51,6 +52,7 @@ namespace jam::snp::lsquic { outcome::result> Engine::make( IoContextPtr io_context_ptr, + std::shared_ptr logsys, ConnectionIdCounter connection_id_counter, TlsCertificate certificate, std::optional listen_port, @@ -102,6 +104,7 @@ namespace jam::snp::lsquic { } auto self = std::make_shared(Private{}, io_context_ptr, + std::move(logsys), std::move(connection_id_counter), std::move(certificate), std::move(socket), @@ -130,6 +133,7 @@ namespace jam::snp::lsquic { Engine::Engine(Private, IoContextPtr io_context_ptr, + std::shared_ptr logsys, ConnectionIdCounter connection_id_counter, TlsCertificate &&certificate, Socket &&socket, @@ -138,6 +142,7 @@ namespace jam::snp::lsquic { : io_context_ptr_{std::move(io_context_ptr)}, connection_id_counter_{std::move(connection_id_counter)}, certificate_{std::move(certificate)}, + log_{logsys->getLogger("Engine", "snp")}, socket_{std::move(socket)}, socket_local_endpoint_{std::move(socket_local_endpoint)}, controller_{std::move(controller)}, @@ -262,6 +267,7 @@ namespace jam::snp::lsquic { return; } if (ec) { + SL_ERROR(self->log_, "udp socket read failed"); return; } self->readLoop(); @@ -582,6 +588,7 @@ namespace jam::snp::lsquic { return; } if (ec) { + SL_ERROR(self->log_, "udp socket write failed"); return; } // will call `Engine::ea_packets_out`. diff --git a/src/snp/connections/lsquic/engine.hpp b/src/snp/connections/lsquic/engine.hpp index 479b2f3..40e5550 100644 --- a/src/snp/connections/lsquic/engine.hpp +++ b/src/snp/connections/lsquic/engine.hpp @@ -25,6 +25,14 @@ struct sockaddr; +namespace soralog { + class Logger; +} // namespace soralog + +namespace jam::log { + class LoggingSystem; +} // namespace jam::log + namespace jam::snp { class ConnectionsConfig; } // namespace jam::snp @@ -93,12 +101,14 @@ namespace jam::snp::lsquic { static outcome::result> make( IoContextPtr io_context_ptr, + std::shared_ptr logsys, ConnectionIdCounter connection_id_counter, TlsCertificate certificate, std::optional listen_port, std::weak_ptr controller); Engine(Private, IoContextPtr io_context_ptr, + std::shared_ptr logsys, ConnectionIdCounter connection_id_counter, TlsCertificate &&certificate, Socket &&socket, @@ -182,6 +192,7 @@ namespace jam::snp::lsquic { unsigned n_packets_out); IoContextPtr io_context_ptr_; + std::shared_ptr log_; ConnectionIdCounter connection_id_counter_; TlsCertificate certificate_; Socket socket_;