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..4d52618 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,7 @@ + +```bash +brew install nasm # vcpkg liblsquic +``` + +`pip` can't install packages on MacOS. +Specify venv directory with `-D Python3_ROOT_DIR=$PWD/.venv` cmake flag. diff --git a/CMakeLists.txt b/CMakeLists.txt index a84f22b..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) @@ -30,21 +32,22 @@ 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) + +include_directories(${CMAKE_SOURCE_DIR}/src) add_subdirectory(src) @@ -57,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/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 new file mode 100644 index 0000000..3c8c9e2 --- /dev/null +++ b/example/snp_chat/CMakeLists.txt @@ -0,0 +1,14 @@ +# +# 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 + logger + snp +) + diff --git a/example/snp_chat/main.cpp b/example/snp_chat/main.cpp new file mode 100644 index 0000000..8187c00 --- /dev/null +++ b/example/snp_chat/main.cpp @@ -0,0 +1,244 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "coro/spawn.hpp" +#include "log/simple.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::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); +} + +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->shutdownRead(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, 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 { + Input input{io_context_ptr}; + while (true) { + auto msg = co_await input.read(); + if (not msg.has_value()) { + 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/CMakeLists.txt b/src/CMakeLists.txt index 08f0356..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) @@ -24,3 +22,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/make_shared_private.hpp b/src/TODO_qtils/make_shared_private.hpp new file mode 100644 index 0000000..6405c38 --- /dev/null +++ b/src/TODO_qtils/make_shared_private.hpp @@ -0,0 +1,36 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace qtils { + /** + * `enable_shared_from_this` requires `make_shared`, + * which requires public constructor. + * `injector` may accidentially call wrong constructor. + * Using `MakeSharedPrivate` argument in public constructor prevents injector + * from using it. + * class Foo : public std::enable_shared_from_this { + * public: + * Foo(MakeSharedPrivate, ...); + * static auto factory(...) { + * return MakeSharedPrivate::make(...); + * } + * }; + */ + class MakeSharedPrivate { + MakeSharedPrivate() = default; + + public: + template + static std::shared_ptr make(A &&...args) { + return std::make_shared(MakeSharedPrivate{}, + std::forward(args)...); + } + }; +} // namespace qtils diff --git a/src/TODO_qtils/map_entry.hpp b/src/TODO_qtils/map_entry.hpp new file mode 100644 index 0000000..e48c62a --- /dev/null +++ b/src/TODO_qtils/map_entry.hpp @@ -0,0 +1,96 @@ +/** + * 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{ + "Call dereference operator of MapEntry without valid iterator"}; + } + return std::get(it_or_key)->second; + } + auto *operator->() { + if (not has()) { + throw std::logic_error{ + "Call member access through pointer operator of MapEntry without " + "valid iterator"}; + } + 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); + } + } + /// Remove from map and return value. + [[nodiscard]] M::mapped_type extract() { + if (not has()) { + throw std::logic_error{"MapEntry::extract"}; + } + auto node = map.extract(std::get(it_or_key)); + it_or_key = std::move(node.key()); + return std::move(node.mapped()); + } + + void eraseIfExists() { + if (has()) { + auto it = std::get(it_or_key); + it_or_key = it->first; + map.erase(it); + } + } + + // 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..b1fe861 --- /dev/null +++ b/src/TODO_qtils/std_hash_of.hpp @@ -0,0 +1,17 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace qtils { + template + requires(requires(const T &v) { std::hash()(v); }) + size_t stdHashOf(const T &v) { + return std::hash()(v); + } +} // namespace qtils 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/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..9dad641 --- /dev/null +++ b/src/coro/future.hpp @@ -0,0 +1,69 @@ +/** + * 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 SelfSPtr = std::shared_ptr>; + + SharedFuture(IoContextPtr io_context_ptr) + : io_context_ptr_{std::move(io_context_ptr)} {} + + static Coro ready(SelfSPtr self) { + co_await setCoroThread(self->io_context_ptr_); + co_return std::holds_alternative(self->state_); + } + + /** + * Resumes coroutine immediately or inside `set`. + */ + static Coro get(SelfSPtr self) { + co_await setCoroThread(self->io_context_ptr_); + if (auto value = qtils::variantGet(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(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"}; + } + 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..834cf61 --- /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 "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) + : io_context_ptr_{std::move(io_context_ptr)}, + future_{std::make_shared( + 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..f84e401 --- /dev/null +++ b/src/coro/set_thread.hpp @@ -0,0 +1,22 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +#include "coro/coro.hpp" +#include "coro/io_context_ptr.hpp" + +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/coro/spawn.hpp b/src/coro/spawn.hpp new file mode 100644 index 0000000..8829acd --- /dev/null +++ b/src/coro/spawn.hpp @@ -0,0 +1,68 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include + +#include "coro/coro.hpp" + +namespace jam { + 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) { + if (e != nullptr) { + std::rethrow_exception(e); + } + }); + } + + template + void coroSpawn(CoroSpawnExecutor auto &&executor, Coro &&coro) { + coroSpawn(std::forward(executor), + [coro{std::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(CoroSpawnExecutor auto &&executor, auto &&f) { + coroSpawn(std::forward(executor), + [](std::remove_cvref_t f) -> Coro { + if constexpr (std::is_void_v) { + co_await f(); + } else { + std::ignore = co_await f(); + } + }(std::forward(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..fc8e95d --- /dev/null +++ b/src/coro/weak.hpp @@ -0,0 +1,50 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#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__; \ + } \ + 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..3e6b2a3 --- /dev/null +++ b/src/coro/yield.hpp @@ -0,0 +1,23 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include + +#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/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/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/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/log/simple.hpp b/src/log/simple.hpp new file mode 100644 index 0000000..0694f6a --- /dev/null +++ b/src/log/simple.hpp @@ -0,0 +1,34 @@ +/** + * 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(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/CMakeLists.txt b/src/snp/CMakeLists.txt new file mode 100644 index 0000000..81b815a --- /dev/null +++ b/src/snp/CMakeLists.txt @@ -0,0 +1,25 @@ +# +# 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 +) diff --git a/src/snp/connections/address.hpp b/src/snp/connections/address.hpp new file mode 100644 index 0000000..baecd87 --- /dev/null +++ b/src/snp/connections/address.hpp @@ -0,0 +1,20 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "snp/connections/key.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; + uint16_t 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..0df2988 --- /dev/null +++ b/src/snp/connections/alpn.hpp @@ -0,0 +1,50 @@ +/** + * 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 + /** + * 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, + 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..a8a813e --- /dev/null +++ b/src/snp/connections/config.hpp @@ -0,0 +1,19 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "crypto/ed25519.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..c7ad566 --- /dev/null +++ b/src/snp/connections/connection.cpp @@ -0,0 +1,39 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "snp/connections/connection.hpp" + +#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) + : 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_}] { + Engine::destroyConnection(conn_ctx); + }); + } + + const ConnectionInfo &Connection::info() const { + return info_; + } + + 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); + } +} // namespace jam::snp diff --git a/src/snp/connections/connection.hpp b/src/snp/connections/connection.hpp new file mode 100644 index 0000000..f39f2f6 --- /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 SelfSPtr = 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(SelfSPtr 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..bd307f2 --- /dev/null +++ b/src/snp/connections/connections.cpp @@ -0,0 +1,154 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "snp/connections/connections.hpp" + +#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, + 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)} {} + + CoroOutcome Connections::init( + SelfSPtr self, std::weak_ptr controller) { + 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_)); + BOOST_OUTCOME_CO_TRY(self->client_, + lsquic::Engine::make(self->io_context_ptr_, + self->logsys_, + self->connection_id_counter_, + certificate, + std::nullopt, + self)); + 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, + self)); + } + init.ready(); + co_return outcome::success(); + } + + const Key &Connections::key() const { + return key_; + } + + 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; + } + auto state = qtils::entry(self->connections_, address.key); + if (not state) { + state.insert( + std::make_shared(self->io_context_ptr_)); + co_await coroSpawn([self, address, state]() mutable -> Coro { + co_await coroYield(); + 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.eraseIfExists(); + } + CORO_WEAK_AWAIT_V( + self, connecting->set(connecting, std::move(connection_result))); + }); + } else if (auto connected = qtils::variantGet(*state)) { + co_return *connected; + } + auto connecting = std::get(*state); + self.reset(); + co_return co_await connecting->get(connecting); + } + + Coro Connections::serve(SelfSPtr self, + ProtocolId protocol_id, + ServeProtocol serve) { + co_await setCoroThread(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.eraseIfExists(); + 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, + stream{std::move(stream)}, + connection_info{connection->info()}]() mutable -> Coro { + auto serve_it = qtils::entry(self->protocols_, protocol_id); + if (not serve_it) { + co_return; + } + auto serve = *serve_it; + std::ignore = CORO_WEAK_AWAIT( + self, serve(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..6d50c32 --- /dev/null +++ b/src/snp/connections/connections.hpp @@ -0,0 +1,99 @@ +/** + * 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::log { + class LoggingSystem; +} // namespace jam::log + +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 SelfSPtr = std::shared_ptr; + + Connections(IoContextPtr io_context_ptr, + std::shared_ptr logsys, + ConnectionsConfig config); + + /** + * Set controller. + * Start quic server and client. + */ + static CoroOutcome init( + SelfSPtr self, std::weak_ptr controller); + + const Key &key() const; + + /** + * Connect or return existing connection. + */ + static ConnectionPtrCoroOutcome connect(SelfSPtr self, Address address); + + using ServeProtocol = + std::function(ConnectionInfo, StreamPtr)>; + /** + * Set callback to handle protocol on server side. + */ + static Coro serve(SelfSPtr 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_; + std::shared_ptr logsys_; + 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..5f82fe8 --- /dev/null +++ b/src/snp/connections/dns_name.cpp @@ -0,0 +1,46 @@ +/** + * 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 constexpr size_t alphabet_size() { + return 32; + } + static constexpr char symbol(cppcodec::detail::alphabet_index_t idx) { + return kAlphabet[idx]; + } + static 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..99f4761 --- /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 : uint8_t { + 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 : uint8_t { + 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 : uint8_t { + 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..1e3541c --- /dev/null +++ b/src/snp/connections/lsquic/engine.cpp @@ -0,0 +1,605 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "snp/connections/lsquic/engine.hpp" + +#include +#include + +#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" +#include "snp/connections/lsquic/controller.hpp" +#include "snp/connections/lsquic/init.hpp" +#include "snp/connections/stream.hpp" + +// 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, + std::shared_ptr logsys, + ConnectionIdCounter connection_id_counter, + TlsCertificate certificate, + std::optional listen_port, + std::weak_ptr controller) { + OUTCOME_TRY(init()); + + uint32_t flags = 0; + if (listen_port.has_value()) { + 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.has_value()) { + 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 = + qtils::MakeSharedPrivate::make(io_context_ptr, + std::move(logsys), + 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}}] { + if (auto self = weak_self.lock()) { + self->readLoop(); + } + }); + + return self; + } + + Engine::Engine(qtils::MakeSharedPrivate, + IoContextPtr io_context_ptr, + std::shared_ptr logsys, + ConnectionIdCounter connection_id_counter, + TlsCertificate &&certificate, + Socket &&socket, + Socket::endpoint_type socket_local_endpoint, + std::weak_ptr controller) + : 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)}, + 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(SelfSPtr self, Address address) { + co_await setCoroThread(self->io_context_ptr_); + if (self->connecting_.has_value()) { + 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.has_value()) { + 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_from_this()}] { + if (auto self = weak_self.lock()) { + 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.has_value()) { + 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{weak_from_this()}](boost::system::error_code ec) { + auto self = weak_self.lock(); + if (not self) { + return; + } + 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{weak_from_this()}](boost::system::error_code ec) { + auto self = weak_self.lock(); + if (not self) { + return; + } + if (ec) { + SL_ERROR(self->log_, "udp socket read failed"); + 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.has_value()) { + 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.has_value()) { + lsquic_stream_close(stream_ctx->ls_stream.value()); + } else { + tryDelete(stream_ctx); + } + } + + void Engine::streamAccept(StreamPtr &&stream) { + coroSpawn(*io_context_ptr_, + [weak_controller{controller_}, + 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()); + if (auto controller = weak_controller.lock()) { + auto &connection = stream->connection_; + controller->onStreamAccept( + connection, protocol_id, std::move(stream)); + } + co_return outcome::success(); + }); + } + + 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::streamShutdownWrite(StreamCtx *stream_ctx) { + if (stream_ctx->ls_stream.has_value()) { + lsquic_stream_shutdown(stream_ctx->ls_stream.value(), SHUT_WR); + } + } + + CoroOutcome Engine::streamReadRaw(StreamCtx *stream_ctx, + qtils::BytesOut message) { + 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.has_value()) { + 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.has_value()) { + throw std::logic_error{"Engine::streamWriteRaw duplicate"}; + } + auto remaining = message; + while (not remaining.empty()) { + [[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()); + 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) { + Engine *self = static_cast(void_self); + 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.has_value() 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.has_value()) { + 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) { + Engine *self = static_cast(void_self); + 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.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.has_value()) { + 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 *) { + 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) { + 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}) { + 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) { + auto self = weak_self.lock(); + if (not self) { + return; + } + if (ec) { + SL_ERROR(self->log_, "udp socket write failed"); + 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..7b75c9e --- /dev/null +++ b/src/snp/connections/lsquic/engine.hpp @@ -0,0 +1,207 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#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 soralog { + class Logger; +} // namespace soralog + +namespace jam::log { + class LoggingSystem; +} // namespace jam::log + +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.has_value() and not connection.has_value(); + } + }; + + /** + * `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.has_value() and not stream.has_value(); + } + }; + + class Engine : public std::enable_shared_from_this { + friend Connection; + friend Stream; + + public: + using SelfSPtr = std::shared_ptr; + + 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(qtils::MakeSharedPrivate, + IoContextPtr io_context_ptr, + std::shared_ptr logsys, + ConnectionIdCounter connection_id_counter, + TlsCertificate &&certificate, + Socket &&socket, + Socket::endpoint_type socket_local_endpoint, + std::weak_ptr controller); + ~Engine(); + + static ConnectionPtrCoroOutcome connect(SelfSPtr 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 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, + 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_; + std::shared_ptr log_; + 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..a473e22 --- /dev/null +++ b/src/snp/connections/lsquic/log.hpp @@ -0,0 +1,47 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#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. + */ + 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) { + 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_log = std::move(log); + lsquic_logger_init(&ls_log, nullptr, LLTS_NONE); + lsquic_set_log_level(level); + } +} // 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..910244e --- /dev/null +++ b/src/snp/connections/message_size.hpp @@ -0,0 +1,17 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#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 = + std::numeric_limits::max(); +} // 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..3530a02 --- /dev/null +++ b/src/snp/connections/protocol_id.hpp @@ -0,0 +1,59 @@ +/** + * 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_; + } + + // 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; + } + + 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..18b9955 --- /dev/null +++ b/src/snp/connections/stream.cpp @@ -0,0 +1,111 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "snp/connections/stream.hpp" + +#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; + + Stream::Stream(IoContextPtr io_context_ptr, + ConnectionPtr connection, + lsquic::StreamCtx *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_}] { + Engine::destroyStream(stream_ctx); + }); + } + + CoroOutcome Stream::read(SelfSPtr self, + qtils::Bytes &buffer, + MessageSize max) { + co_await setCoroThread(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::shutdownRead(SelfSPtr self) { + co_await setCoroThread(self->io_context_ptr_); + Engine::streamShutdownRead(self->stream_ctx_); + co_return; + } + + CoroOutcome Stream::write(SelfSPtr self, qtils::BytesIn message) { + co_await setCoroThread(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::shutdownWrite(SelfSPtr self) { + co_await setCoroThread(self->io_context_ptr_); + Engine::streamShutdownWrite(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..9306ce0 --- /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 SelfSPtr = 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(SelfSPtr self, + qtils::Bytes &buffer, + MessageSize max); + + /** + * Close reading side of stream. + */ + static Coro shutdownRead(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(SelfSPtr self, qtils::BytesIn message); + + /** + * Write fin. + * Closes writing side of stream. + */ + static Coro shutdownWrite(SelfSPtr 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..1d942f9 --- /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 "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) + : alpn_{std::make_shared(config.genesis)}, + context_{std::make_shared( + 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/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.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"] } } }