Initial public khodo snapshot
publish / publish (push) Successful in 31m41s

This commit is contained in:
khodowany
2026-09-04 19:30:11 -05:00
commit 67429eacd1
34 changed files with 8279 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
name: publish
on:
push:
branches:
- master
pull_request:
workflow_dispatch:
permissions:
contents: read
packages: write
actions: write
env:
CONAN_HOME: build/build_common/conan_cache
MODULUS_DIRECT: "1"
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install build dependencies
run: |
if [ -r /etc/os-release ]; then
. /etc/os-release
fi
if [ "${ID:-}" = "ubuntu" ] && [ -n "${VERSION_CODENAME:-}" ]; then
if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then
sed -i '/^Types: deb$/a Architectures: amd64' /etc/apt/sources.list.d/ubuntu.sources
printf '%s\n' \
'Types: deb' \
'URIs: http://ports.ubuntu.com/ubuntu-ports/' \
"Suites: ${VERSION_CODENAME} ${VERSION_CODENAME}-updates ${VERSION_CODENAME}-backports" \
'Components: main universe restricted multiverse' \
'Architectures: arm64' \
'Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg' \
'' \
'Types: deb' \
'URIs: http://ports.ubuntu.com/ubuntu-ports/' \
"Suites: ${VERSION_CODENAME}-security" \
'Components: main universe restricted multiverse' \
'Architectures: arm64' \
'Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg' \
> /etc/apt/sources.list.d/ubuntu-arm64.sources
elif [ -f /etc/apt/sources.list ]; then
sed -i 's|^deb http://archive.ubuntu.com/ubuntu|deb [arch=amd64] http://archive.ubuntu.com/ubuntu|' /etc/apt/sources.list
sed -i 's|^deb http://security.ubuntu.com/ubuntu|deb [arch=amd64] http://security.ubuntu.com/ubuntu|' /etc/apt/sources.list
printf '%s\n' \
"deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports ${VERSION_CODENAME} main universe restricted multiverse" \
"deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports ${VERSION_CODENAME}-updates main universe restricted multiverse" \
"deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports ${VERSION_CODENAME}-backports main universe restricted multiverse" \
"deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports ${VERSION_CODENAME}-security main universe restricted multiverse" \
> /etc/apt/sources.list.d/ubuntu-arm64.list
fi
fi
dpkg --add-architecture arm64
apt-get update
apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
cmake \
g++-12 \
g++-aarch64-linux-gnu \
gcc-12 \
gcc-aarch64-linux-gnu \
git \
libdrm-dev \
libdrm-dev:arm64 \
libegl-dev \
libegl-dev:arm64 \
libgbm-dev \
libgbm-dev:arm64 \
libgles-dev \
libgles-dev:arm64 \
libglvnd-dev \
libglvnd-dev:arm64 \
ninja-build \
pkg-config \
python3 \
python3-venv
python3 -m venv build/build_common/ci_python
build/build_common/ci_python/bin/python -m pip install --no-cache-dir conan==2.26.2
echo "$PWD/build/build_common/ci_python/bin" >> "$GITHUB_PATH"
- name: Restore Conan cache
uses: actions/cache@v4
with:
path: build/build_common/conan_cache
key: conan-${{ runner.os }}-${{ hashFiles('conanfile.py', 'modulus.json') }}
restore-keys: conan-${{ runner.os }}-
- name: Configure native release
run: python3 modulus.py configure --target x86_64_gcc12_release
- name: Build native release
run: python3 modulus.py build --target x86_64_gcc12_release
- name: Test native release
run: python3 modulus.py test --target x86_64_gcc12_release
- name: Create and upload packages
if: ${{ gitea.event_name == 'workflow_dispatch' || (gitea.event_name == 'push' && gitea.ref == 'refs/heads/master') }}
env:
CONAN_USER: ${{ secrets.CONAN_USER }}
CONAN_TOKEN: ${{ secrets.CONAN_TOKEN }}
LIGO_CONAN_UPLOAD: "1"
MODULUS_ALLOW_PACKAGE_REPLACE: "1"
run: python3 modulus.py package --all --replace
Executable
+20
View File
@@ -0,0 +1,20 @@
build/
.clangd
.clang-format
pyrightconfig.json
**/__pycache__/
*.pyc
*.pyo
*.pyd
*.log
package/CMakeCache.txt
package/CMakeFiles/
package/metadata/
package/.ninja_*
package/*.json
package/*.cmake
package/*.ninja
package/*.sh
package/*-smoke
+73
View File
@@ -0,0 +1,73 @@
cmake_minimum_required(VERSION 3.25)
project(ligo VERSION 0.1.0 LANGUAGES CXX)
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
include(CTest)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
find_package(modulus CONFIG REQUIRED)
add_library(ligo
src/config.cpp
src/detail/errors.cpp
src/detail/lifecycle.cpp
src/detail/transport.cpp
src/http.cpp
src/net.cpp
src/security.cpp
src/service.cpp
src/tcp.cpp
src/udp.cpp
)
target_include_directories(ligo PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
target_compile_features(ligo PUBLIC cxx_std_23)
target_link_libraries(ligo PUBLIC
modulus::modulus
modulus::boost_json
modulus::openssl
)
add_library(ligo::ligo ALIAS ligo)
if(BUILD_TESTING)
add_subdirectory(tests)
endif()
install(TARGETS ligo
EXPORT ligo-targets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(EXPORT ligo-targets
NAMESPACE ligo::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ligo
)
configure_package_config_file(
${CMAKE_CURRENT_SOURCE_DIR}/cmake/ligo_config.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/ligo-config.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ligo
)
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/ligo-config-version.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/ligo-config.cmake
${CMAKE_CURRENT_BINARY_DIR}/ligo-config-version.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ligo
)
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Khodo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+43
View File
@@ -0,0 +1,43 @@
# Ligo
Ligo is a C++23 networking and transport library. It provides HTTP JSON calls, framed TCP JSON calls, reliable UDP datagrams, TCP/TLS handoff helpers, timeout handling, and `std::expected`-based error reporting.
Ligo uses Modulus for shared build tooling, Boost.JSON, and OpenSSL integration. Product-specific routes and workflows belong in consuming applications.
## Build
Configure, build, and test the default target:
```sh
python3 modulus.py configure
python3 modulus.py build
python3 modulus.py test
```
Release verification:
```sh
python3 modulus.py configure --target x86_64_gcc12_release
python3 modulus.py build --target x86_64_gcc12_release
python3 modulus.py test --target x86_64_gcc12_release
```
Create packages:
```sh
python3 modulus.py package --all
```
Configure/build/test can read public packages anonymously from the khodo Conan registry. Publishing requires credentials only on the upload step:
```sh
CONAN_USER=<user> \
CONAN_TOKEN=<token> \
LIGO_CONAN_UPLOAD=1 \
MODULUS_ALLOW_PACKAGE_REPLACE=1 \
python3 modulus.py package --all --replace
```
## License
Ligo is released under the MIT License.
+6
View File
@@ -0,0 +1,6 @@
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
find_dependency(modulus CONFIG REQUIRED)
include("${CMAKE_CURRENT_LIST_DIR}/ligo-targets.cmake")
+107
View File
@@ -0,0 +1,107 @@
import conan
import conan.tools.build
import conan.tools.cmake
class ligo_conan(conan.ConanFile):
name = "ligo"
version = "latest"
description = "C++23 networking and transport library"
license = "MIT"
package_type = "library"
settings = "os", "arch", "compiler", "build_type"
options = {
"shared": [
True,
False,
],
"fPIC": [
True,
False,
],
"with_tests": [
True,
False,
],
}
default_options = {
"shared": False,
"fPIC": True,
"with_tests": False,
}
exports = "modulus.json"
exports_sources = (
"CMakeLists.txt",
"cmake/*.in",
"include/*.hpp",
"include/ligo/*.hpp",
"include/ligo/detail/*.hpp",
"src/*.cpp",
"src/detail/*.cpp",
"tests/CMakeLists.txt",
"tests/*.cpp",
"tests/*.hpp",
)
def config_options(self):
if self.settings.os == "Windows":
del self.options.fPIC
def configure(self):
if self.options.shared:
del self.options.fPIC
def requirements(self):
self.requires(
"modulus/latest",
transitive_headers=True,
transitive_libs=True,
)
def generate(self):
deps = conan.tools.cmake.CMakeDeps(self)
deps.generate()
toolchain = conan.tools.cmake.CMakeToolchain(self)
if hasattr(toolchain, "user_presets_path"):
toolchain.user_presets_path = False
toolchain.variables["BUILD_SHARED_LIBS"] = bool(self.options.shared)
toolchain.variables["BUILD_TESTING"] = bool(self.options.with_tests)
toolchain.generate()
def build(self):
cmake = conan.tools.cmake.CMake(self)
cmake.configure()
cmake.build()
if self.options.with_tests and conan.tools.build.can_run(self):
cmake.test(target="check")
def package_id(self):
self.info.options.rm_safe("with_tests")
for requirement in (
"sqlite3",
"raylib",
"glfw",
"opengl",
"miniaudio",
):
if requirement in self.info.requires.pkg_names:
self.info.requires.remove(requirement)
def package(self):
cmake = conan.tools.cmake.CMake(self)
cmake.install()
def package_info(self):
self.cpp_info.set_property(
"cmake_file_name",
"ligo",
)
self.cpp_info.set_property(
"cmake_target_name",
"ligo::ligo",
)
self.cpp_info.libs = ["ligo"]
self.cpp_info.requires = [
"modulus::modulus",
"modulus::boost_json",
"modulus::openssl",
]
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <ligo/config.hpp>
#include <ligo/http.hpp>
#include <ligo/net.hpp>
#include <ligo/security.hpp>
#include <ligo/service.hpp>
#include <ligo/detail/errors.hpp>
#include <ligo/tcp.hpp>
#include <ligo/udp.hpp>
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <expected>
#include <chrono>
#include <cstdint>
#include <string>
#include <vector>
#include <boost/json/value.hpp>
#include <modulus.hpp>
#include <ligo/security.hpp>
namespace ligo {
enum class protocol {
HTTP,
TCP,
UDP,
};
struct udp_reliability_config {
std::chrono::milliseconds resend_interval{50};
std::uint32_t max_in_flight{64};
std::chrono::milliseconds session_timeout{5000};
};
struct http_config {
std::uint64_t max_body_size{16U * 1024U * 1024U};
};
struct endpoint_config {
std::string name{};
ligo::protocol protocol{ligo::protocol::TCP};
std::string bind{};
std::string connect{};
std::chrono::milliseconds timeout{5000};
ligo::security_config security{};
ligo::http_config http{};
ligo::udp_reliability_config udp{};
};
auto validate(ligo::endpoint_config const& config) -> std::expected<void, modulus::error>;
auto parse_endpoint_config(boost::json::value const& value) -> std::expected<ligo::endpoint_config, modulus::error>;
auto parse_endpoint_configs(boost::json::value const& value) -> std::expected<std::vector<ligo::endpoint_config>, modulus::error>;
auto load_endpoint_configs(std::string const& path) -> std::expected<std::vector<ligo::endpoint_config>, modulus::error>;
auto to_json(ligo::udp_reliability_config const& config) -> boost::json::value;
auto to_json(ligo::http_config const& config) -> boost::json::value;
auto to_json(ligo::security_config const& config) -> boost::json::value;
auto to_json(ligo::endpoint_config const& config) -> boost::json::value;
auto to_json(std::vector<ligo::endpoint_config> const& configs) -> boost::json::value;
}
+53
View File
@@ -0,0 +1,53 @@
#pragma once
#include <cassert>
#include <cstdint>
#include <expected>
#include <string>
#include <utility>
#include <modulus.hpp>
namespace ligo::detail {
constexpr std::int32_t error_ok{0};
constexpr std::int32_t error_invalid_argument{1};
constexpr std::int32_t error_not_found{2};
constexpr std::int32_t error_unavailable{3};
constexpr std::int32_t error_timeout{4};
constexpr std::int32_t error_internal{5};
auto make_error(std::int32_t code, std::string message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> modulus::error;
auto fail_void(modulus::error error) -> std::expected<void, modulus::error>;
template <typename VALUE>
auto fail(modulus::error error) -> std::expected<VALUE, modulus::error>;
template <typename VALUE>
auto fail(std::expected<void, modulus::error> const& result) -> std::expected<VALUE, modulus::error>;
template <typename VALUE, typename MESSAGE>
auto fail(std::int32_t code, MESSAGE&& message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> std::expected<VALUE, modulus::error>;
template <typename MESSAGE>
auto fail_void(std::int32_t code, MESSAGE&& message, modulus::source_location location = modulus::source_location(__builtin_FILE(), __builtin_LINE(), __builtin_FUNCTION())) -> std::expected<void, modulus::error>;
}
template <typename VALUE>
auto ligo::detail::fail(modulus::error error) -> std::expected<VALUE, modulus::error> {
return modulus::fail<VALUE>(std::move(error));
}
template <typename VALUE>
auto ligo::detail::fail(std::expected<void, modulus::error> const& result) -> std::expected<VALUE, modulus::error> {
assert(!result);
return ligo::detail::fail<VALUE>(result.error());
}
template <typename VALUE, typename MESSAGE>
auto ligo::detail::fail(std::int32_t code, MESSAGE&& message, modulus::source_location location) -> std::expected<VALUE, modulus::error> {
return modulus::fail<VALUE>(ligo::detail::make_error(code, std::string{std::forward<MESSAGE>(message)}, location));
}
template <typename MESSAGE>
auto ligo::detail::fail_void(std::int32_t code, MESSAGE&& message, modulus::source_location location) -> std::expected<void, modulus::error> {
return modulus::fail<void>(ligo::detail::make_error(code, std::string{std::forward<MESSAGE>(message)}, location));
}
+242
View File
@@ -0,0 +1,242 @@
#pragma once
#include <expected>
#include <atomic>
#include <chrono>
#include <memory>
#include <mutex>
#include <set>
#include <vector>
#include <boost/asio/as_tuple.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/this_coro.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/beast/core.hpp>
#include <boost/system/error_code.hpp>
#include <modulus.hpp>
#include <ligo/detail/errors.hpp>
#include <ligo/detail/transport.hpp>
namespace ligo::detail {
auto cancel_session_stream(boost::beast::tcp_stream& stream) -> void;
template <typename NEXT_LAYER>
auto cancel_session_stream(boost::asio::ssl::stream<NEXT_LAYER>& stream) -> void;
template <typename S>
struct session_registry {
static auto cancel_waiters(std::vector<std::shared_ptr<boost::asio::steady_timer>> const& live) -> void;
auto add(std::shared_ptr<S> const& stream) -> void;
auto remove(std::shared_ptr<S> const& stream) -> void;
auto cancel_all() -> void;
auto empty() const -> std::bool_t;
auto wait_empty(std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>>;
auto notify_waiters() -> void;
auto live_waiters_locked() -> std::vector<std::shared_ptr<boost::asio::steady_timer>>;
mutable std::mutex mutex{};
std::set<std::shared_ptr<S>> sessions{};
std::vector<std::weak_ptr<boost::asio::steady_timer>> waiters{};
};
template <typename S>
struct session_guard {
~session_guard();
auto release() -> void;
std::shared_ptr<ligo::detail::session_registry<S>> registry{};
std::shared_ptr<S> stream{};
};
template <typename S>
auto stop_accepting_server(std::shared_ptr<S> state) -> void;
template <typename S>
auto stop_accepting_server_now(std::shared_ptr<S> const& state) -> void;
template <typename S>
auto async_stop_accepting_server(std::shared_ptr<S> state, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>>;
}
template <typename NEXT_LAYER>
auto ligo::detail::cancel_session_stream(boost::asio::ssl::stream<NEXT_LAYER>& stream) -> void {
auto ignored = boost::system::error_code{};
stream.lowest_layer().cancel(ignored);
stream.lowest_layer().close(ignored);
}
template <typename S>
auto ligo::detail::session_registry<S>::cancel_waiters(std::vector<std::shared_ptr<boost::asio::steady_timer>> const& live) -> void {
for (auto& waiter : live) {
waiter->cancel();
}
}
template <typename S>
auto ligo::detail::session_registry<S>::add(std::shared_ptr<S> const& stream) -> void {
auto lock = std::scoped_lock{mutex};
sessions.insert(stream);
}
template <typename S>
auto ligo::detail::session_registry<S>::remove(std::shared_ptr<S> const& stream) -> void {
auto live = std::vector<std::shared_ptr<boost::asio::steady_timer>>{};
{
auto lock = std::scoped_lock{mutex};
sessions.erase(stream);
live = live_waiters_locked();
}
cancel_waiters(live);
}
template <typename S>
auto ligo::detail::session_registry<S>::cancel_all() -> void {
auto copy = std::vector<std::shared_ptr<S>>{};
{
auto lock = std::scoped_lock{mutex};
copy.assign(sessions.begin(), sessions.end());
}
for (auto& stream : copy) {
boost::asio::post(stream->get_executor(), [stream] {
ligo::detail::cancel_session_stream(*stream);
});
}
}
template <typename S>
auto ligo::detail::session_registry<S>::empty() const -> std::bool_t {
auto lock = std::scoped_lock{mutex};
return sessions.empty();
}
template <typename S>
auto ligo::detail::session_registry<S>::wait_empty(std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto valid_timeout = ligo::detail::validate_positive_timeout(timeout, "shutdown timeout");
if (!valid_timeout) {
co_return valid_timeout;
}
auto executor = co_await boost::asio::this_coro::executor;
if (empty()) {
co_return std::expected<void, modulus::error>{};
}
auto notify_timer = std::make_shared<boost::asio::steady_timer>(executor);
auto deadline_timer = std::make_shared<boost::asio::steady_timer>(executor);
auto timed_out = std::make_shared<std::atomic<std::bool_t>>(false);
{
auto lock = std::scoped_lock{mutex};
waiters.push_back(notify_timer);
}
deadline_timer->expires_after(timeout);
deadline_timer->async_wait([notify_timer, timed_out](boost::system::error_code const& error) {
if (!error) {
timed_out->store(true, std::memory_order_relaxed);
notify_timer->cancel();
}
});
for (;;) {
if (empty()) {
deadline_timer->cancel();
co_return std::expected<void, modulus::error>{};
}
if (timed_out->load(std::memory_order_relaxed)) {
co_return ligo::detail::fail_void(ligo::detail::error_timeout, "timed out waiting for sessions to drain");
}
notify_timer->expires_at(std::chrono::steady_clock::time_point::max());
auto [error] = co_await notify_timer->async_wait(boost::asio::as_tuple(boost::asio::use_awaitable));
if (error && error != boost::asio::error::operation_aborted) {
co_return ligo::detail::fail_void(ligo::detail::network_failure(error));
}
}
}
template <typename S>
auto ligo::detail::session_registry<S>::notify_waiters() -> void {
auto live = std::vector<std::shared_ptr<boost::asio::steady_timer>>{};
{
auto lock = std::scoped_lock{mutex};
live = live_waiters_locked();
}
cancel_waiters(live);
}
template <typename S>
auto ligo::detail::session_registry<S>::live_waiters_locked() -> std::vector<std::shared_ptr<boost::asio::steady_timer>> {
auto live = std::vector<std::shared_ptr<boost::asio::steady_timer>>{};
auto retained = std::vector<std::weak_ptr<boost::asio::steady_timer>>{};
for (auto& waiter : waiters) {
if (auto locked = waiter.lock()) {
live.push_back(locked);
retained.push_back(locked);
}
}
waiters = std::move(retained);
return live;
}
template <typename S>
ligo::detail::session_guard<S>::~session_guard() {
if (registry && stream) {
registry->remove(stream);
}
}
template <typename S>
auto ligo::detail::session_guard<S>::release() -> void {
if (registry && stream) {
registry->remove(stream);
stream.reset();
registry.reset();
}
}
template <typename S>
auto ligo::detail::stop_accepting_server(std::shared_ptr<S> state) -> void {
auto lock = std::scoped_lock{state->lifecycle_mutex};
if (state->stopped.exchange(true)) {
return;
}
state->sessions->cancel_all();
auto server_executor = state->strand;
boost::asio::post(server_executor, [state = std::move(state)] {
auto ignored = boost::system::error_code{};
state->acceptor->close(ignored);
});
}
template <typename S>
auto ligo::detail::stop_accepting_server_now(std::shared_ptr<S> const& state) -> void {
auto lock = std::scoped_lock{state->lifecycle_mutex};
if (state->stopped.exchange(true)) {
return;
}
state->sessions->cancel_all();
auto ignored = boost::system::error_code{};
state->acceptor->close(ignored);
}
template <typename S>
auto ligo::detail::async_stop_accepting_server(std::shared_ptr<S> state, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto valid_timeout = ligo::detail::validate_positive_timeout(timeout, "shutdown timeout");
if (!valid_timeout) {
co_return valid_timeout;
}
auto should_stop = false;
{
auto lock = std::scoped_lock{state->lifecycle_mutex};
should_stop = !state->stopped.exchange(true);
if (should_stop) {
state->sessions->cancel_all();
}
}
if (should_stop) {
co_await boost::asio::post(state->strand, boost::asio::use_awaitable);
auto ignored = boost::system::error_code{};
state->acceptor->close(ignored);
}
co_return co_await state->sessions->wait_empty(timeout);
}
+70
View File
@@ -0,0 +1,70 @@
#pragma once
#include <atomic>
#include <functional>
#include <memory>
#include <utility>
#include <modulus.hpp>
namespace ligo::detail {
class state_handle {
public:
state_handle() = default;
state_handle(ligo::detail::state_handle const&) = delete;
state_handle(ligo::detail::state_handle&& other) noexcept;
~state_handle() = default;
auto operator=(ligo::detail::state_handle const&) -> ligo::detail::state_handle& = delete;
auto operator=(ligo::detail::state_handle&& other) noexcept -> ligo::detail::state_handle&;
explicit operator std::bool_t() const noexcept;
template <typename S>
explicit state_handle(std::shared_ptr<S> state);
template <typename S>
auto as() const -> std::shared_ptr<S>;
private:
std::atomic<std::shared_ptr<void>> state_{};
};
class unique_state {
public:
unique_state() noexcept = default;
unique_state(ligo::detail::unique_state const&) = delete;
unique_state(ligo::detail::unique_state&&) noexcept = default;
~unique_state() = default;
auto operator=(ligo::detail::unique_state const&) -> ligo::detail::unique_state& = delete;
auto operator=(ligo::detail::unique_state&&) noexcept -> ligo::detail::unique_state& = default;
explicit operator std::bool_t() const noexcept;
template <typename STATE>
explicit unique_state(std::unique_ptr<STATE> state);
template <typename STATE>
auto as() const noexcept -> STATE*;
private:
static auto ignore_state(void* state) -> void;
template <typename STATE>
static auto delete_state(void* state) -> void;
std::unique_ptr<void, std::function<void(void*)>> state_{nullptr, ignore_state};
};
}
template <typename STATE>
ligo::detail::state_handle::state_handle(std::shared_ptr<STATE> state) : state_{std::shared_ptr<void>{std::move(state)}} {}
template <typename STATE>
auto ligo::detail::state_handle::as() const -> std::shared_ptr<STATE> {
return std::static_pointer_cast<STATE>(state_.load());
}
template <typename STATE>
ligo::detail::unique_state::unique_state(std::unique_ptr<STATE> state) : state_{state.release(), delete_state<STATE>} {}
template <typename STATE>
auto ligo::detail::unique_state::as() const noexcept -> STATE* {
return static_cast<STATE*>(state_.get());
}
template <typename STATE>
auto ligo::detail::unique_state::delete_state(void* state) -> void {
delete static_cast<STATE*>(state);
}
+117
View File
@@ -0,0 +1,117 @@
#pragma once
#include <expected>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/as_tuple.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/bind_cancellation_slot.hpp>
#include <boost/asio/cancellation_signal.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/beast/core.hpp>
#include <boost/json/value.hpp>
#include <boost/system/error_code.hpp>
#include <modulus.hpp>
#include <ligo/detail/errors.hpp>
#include <ligo/service.hpp>
namespace ligo::detail {
struct endpoint_parts {
std::string host{"127.0.0.1"};
std::uint16_t port{};
};
auto status_name(std::int32_t code) -> std::string;
auto parse_status_code(std::string_view value) -> std::int32_t;
auto network_failure(boost::system::error_code const& error) -> modulus::error;
auto operation_failure(boost::system::error_code const& error, std::string timeout_message) -> modulus::error;
auto invalid_argument(std::string message) -> modulus::error;
auto unavailable(std::string message) -> modulus::error;
auto validate_positive_timeout(std::chrono::milliseconds timeout, std::string_view name) -> std::expected<void, modulus::error>;
auto report_error(std::function<void(modulus::error)> const& on_error, modulus::error error) -> void;
auto report_error(std::function<void(modulus::error)> const& on_error, std::expected<void, modulus::error> status) -> void;
auto response_envelope(std::expected<boost::json::value, modulus::error> result) -> boost::json::value;
auto serialize_response_envelope(std::expected<boost::json::value, modulus::error> result) -> std::string;
auto unwrap_response_envelope(boost::json::value value, std::string_view protocol_name) -> std::expected<boost::json::value, modulus::error>;
auto parse_port(std::string_view text) -> std::expected<std::uint16_t, modulus::error>;
auto parse_endpoint_text(std::string_view text, std::uint16_t fallback_port) -> std::expected<ligo::detail::endpoint_parts, modulus::error>;
auto parse_client_endpoint_text(std::string_view text) -> std::expected<ligo::detail::endpoint_parts, modulus::error>;
auto parse_server_endpoint_text(std::string_view text) -> std::expected<ligo::detail::endpoint_parts, modulus::error>;
auto start_tcp_acceptor(boost::asio::ip::tcp::acceptor& acceptor, std::string const& host, std::uint16_t port) -> std::expected<std::uint16_t, modulus::error>;
auto resolve_tcp_endpoints(boost::asio::any_io_executor executor, std::string const& host, std::uint16_t port, std::chrono::milliseconds timeout, std::string timeout_message) -> boost::asio::awaitable<std::expected<boost::asio::ip::tcp::resolver::results_type, modulus::error>>;
auto call_handler(ligo::service_router const& router, ligo::request_context context, boost::json::value payload) -> boost::asio::awaitable<std::expected<boost::json::value, modulus::error>>;
auto call_response_handler(ligo::service_router const& router, ligo::request_context context, boost::json::value payload) -> boost::asio::awaitable<std::expected<ligo::service_response, modulus::error>>;
struct stream_deadline {
std::shared_ptr<boost::asio::steady_timer> timer{};
std::shared_ptr<std::atomic<std::bool_t>> timed_out{};
std::shared_ptr<std::atomic<std::bool_t>> active{};
std::shared_ptr<boost::asio::cancellation_signal> cancellation{};
};
auto start_stream_deadline(boost::beast::tcp_stream& stream, std::chrono::milliseconds timeout) -> ligo::detail::stream_deadline;
auto start_stream_deadline(boost::asio::ip::tcp::socket& socket, std::chrono::milliseconds timeout) -> ligo::detail::stream_deadline;
template <typename NEXT_LAYER>
auto start_stream_deadline(boost::asio::ssl::stream<NEXT_LAYER>& stream, std::chrono::milliseconds timeout) -> ligo::detail::stream_deadline;
auto cancel_stream_deadline(ligo::detail::stream_deadline& deadline) -> void;
auto deadline_token(ligo::detail::stream_deadline& deadline) -> decltype(boost::asio::bind_cancellation_slot(std::declval<boost::asio::cancellation_slot>(), boost::asio::as_tuple(boost::asio::use_awaitable)));
auto operation_failure(boost::system::error_code const& error, ligo::detail::stream_deadline const& deadline, std::string timeout_message) -> modulus::error;
template <typename NEXT_LAYER>
auto async_tls_handshake(boost::asio::ssl::stream<NEXT_LAYER>& stream, boost::asio::ssl::stream_base::handshake_type type, std::chrono::milliseconds timeout, std::string timeout_message) -> boost::asio::awaitable<std::expected<void, modulus::error>>;
template <typename NEXT_LAYER>
auto async_tls_shutdown(boost::asio::ssl::stream<NEXT_LAYER>& stream, std::chrono::milliseconds timeout, std::string timeout_message) -> boost::asio::awaitable<std::expected<void, modulus::error>>;
}
template <typename NEXT_LAYER>
auto ligo::detail::start_stream_deadline(boost::asio::ssl::stream<NEXT_LAYER>& stream, std::chrono::milliseconds timeout) -> ligo::detail::stream_deadline {
auto deadline = ligo::detail::stream_deadline{.timer = std::make_shared<boost::asio::steady_timer>(stream.get_executor()), .timed_out = std::make_shared<std::atomic<std::bool_t>>(false), .active = std::make_shared<std::atomic<std::bool_t>>(true), .cancellation = std::make_shared<boost::asio::cancellation_signal>()};
deadline.timer->expires_after(timeout);
deadline.timer->async_wait([timer = deadline.timer, timed_out = deadline.timed_out, active = deadline.active, cancellation = deadline.cancellation](boost::system::error_code const& error) {
if (!error && active->exchange(false, std::memory_order_acq_rel)) {
timed_out->store(true, std::memory_order_release);
cancellation->emit(boost::asio::cancellation_type::terminal);
}
});
return deadline;
}
template <typename NEXT_LAYER>
auto ligo::detail::async_tls_handshake(boost::asio::ssl::stream<NEXT_LAYER>& stream, boost::asio::ssl::stream_base::handshake_type type, std::chrono::milliseconds timeout, std::string timeout_message) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto deadline = ligo::detail::start_stream_deadline(stream, timeout);
auto [error] = co_await stream.async_handshake(type, ligo::detail::deadline_token(deadline));
ligo::detail::cancel_stream_deadline(deadline);
if (error) {
co_return ligo::detail::fail_void(ligo::detail::operation_failure(error, deadline, std::move(timeout_message)));
}
co_return std::expected<void, modulus::error>{};
}
template <typename NEXT_LAYER>
auto ligo::detail::async_tls_shutdown(boost::asio::ssl::stream<NEXT_LAYER>& stream, std::chrono::milliseconds timeout, std::string timeout_message) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto deadline = ligo::detail::start_stream_deadline(stream, timeout);
auto [error] = co_await stream.async_shutdown(ligo::detail::deadline_token(deadline));
ligo::detail::cancel_stream_deadline(deadline);
if (error) {
co_return ligo::detail::fail_void(ligo::detail::operation_failure(error, deadline, std::move(timeout_message)));
}
co_return std::expected<void, modulus::error>{};
}
+86
View File
@@ -0,0 +1,86 @@
#pragma once
#include <expected>
#include <chrono>
#include <cstdint>
#include <functional>
#include <string>
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/json/value.hpp>
#include <modulus.hpp>
#include <ligo/detail/state.hpp>
#include <ligo/config.hpp>
#include <ligo/security.hpp>
#include <ligo/service.hpp>
namespace ligo::http {
enum class method {
GET,
POST,
PUT,
PATCH,
DELETE,
};
struct endpoint {
std::string host{"127.0.0.1"};
std::uint16_t port{0};
};
struct request {
ligo::http::method method{ligo::http::method::POST};
std::string target{"/"};
boost::json::value payload{};
};
struct client_options {
std::chrono::milliseconds timeout{5000};
std::uint64_t max_body_size{16U * 1024U * 1024U};
ligo::security_config security{};
};
struct server_options {
std::chrono::milliseconds timeout{5000};
std::uint64_t max_body_size{16U * 1024U * 1024U};
std::function<void(modulus::error)> on_error{};
ligo::security_config security{};
};
class client {
public:
explicit client(ligo::http::endpoint endpoint, ligo::http::client_options options = {});
auto call(ligo::http::request const& request) const -> boost::asio::awaitable<std::expected<boost::json::value, modulus::error>>;
private:
ligo::http::endpoint endpoint_{};
ligo::http::client_options options_{};
};
class server {
public:
server(boost::asio::io_context& io, ligo::http::endpoint endpoint, ligo::service_router router, ligo::http::server_options options = {});
server(boost::asio::any_io_executor executor, ligo::http::endpoint endpoint, ligo::service_router router, ligo::http::server_options options = {});
server(ligo::http::server const&) = delete;
server(ligo::http::server&&) noexcept = default;
~server();
auto operator=(ligo::http::server const&) -> ligo::http::server& = delete;
auto operator=(ligo::http::server&&) noexcept -> ligo::http::server&;
auto start() -> std::expected<void, modulus::error>;
auto stop() -> void;
auto async_stop(std::chrono::milliseconds timeout = std::chrono::milliseconds{5000}) -> boost::asio::awaitable<std::expected<void, modulus::error>>;
auto selected_port() const -> std::uint16_t;
private:
ligo::detail::state_handle state_{};
};
auto client_endpoint_from_config(ligo::endpoint_config const& config) -> std::expected<ligo::http::endpoint, modulus::error>;
auto server_endpoint_from_config(ligo::endpoint_config const& config) -> std::expected<ligo::http::endpoint, modulus::error>;
auto client_options_from_config(ligo::endpoint_config const& config) -> ligo::http::client_options;
auto server_options_from_config(ligo::endpoint_config const& config) -> ligo::http::server_options;
}
+71
View File
@@ -0,0 +1,71 @@
#pragma once
#include <expected>
#include <chrono>
#include <cstddef>
#include <memory>
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/buffer.hpp>
#include <boost/system/error_code.hpp>
#include <modulus.hpp>
#include <ligo/detail/state.hpp>
#include <ligo/config.hpp>
#include <ligo/security.hpp>
#include <ligo/tcp.hpp>
#include <cstdint>
namespace ligo::net {
class byte_stream_state;
class byte_stream {
public:
byte_stream() = default;
byte_stream(ligo::net::byte_stream const&) = delete;
byte_stream(ligo::net::byte_stream&&) noexcept = default;
~byte_stream();
auto operator=(ligo::net::byte_stream const&) -> ligo::net::byte_stream& = delete;
auto operator=(ligo::net::byte_stream&&) noexcept -> ligo::net::byte_stream&;
static auto from_socket(ligo::tcp::socket_handle socket) -> std::expected<ligo::net::byte_stream, modulus::error>;
static auto from_tls_socket(ligo::tcp::tls_socket socket) -> std::expected<ligo::net::byte_stream, modulus::error>;
auto async_read_some(boost::asio::mutable_buffer buffer, std::chrono::milliseconds timeout = std::chrono::milliseconds{5000}) -> boost::asio::awaitable<std::expected<std::size_t, modulus::error>>;
auto async_write_some(boost::asio::const_buffer buffer, std::chrono::milliseconds timeout = std::chrono::milliseconds{5000}) -> boost::asio::awaitable<std::expected<std::size_t, modulus::error>>;
auto async_write_all(boost::asio::const_buffer buffer, std::chrono::milliseconds timeout = std::chrono::milliseconds{5000}) -> boost::asio::awaitable<std::expected<void, modulus::error>>;
auto async_shutdown(std::chrono::milliseconds timeout = std::chrono::milliseconds{5000}) -> boost::asio::awaitable<std::expected<void, modulus::error>>;
auto close(boost::system::error_code& error) -> void;
auto is_open() const -> std::bool_t;
private:
explicit byte_stream(std::shared_ptr<ligo::net::byte_stream_state> state);
std::shared_ptr<ligo::net::byte_stream_state> state_{};
};
class listener {
public:
listener() = default;
listener(ligo::net::listener const&) = delete;
listener(ligo::net::listener&&) noexcept = default;
~listener();
auto operator=(ligo::net::listener const&) -> ligo::net::listener& = delete;
auto operator=(ligo::net::listener&&) noexcept -> ligo::net::listener&;
static auto bind(boost::asio::any_io_executor executor, ligo::tcp::endpoint endpoint) -> std::expected<ligo::net::listener, modulus::error>;
auto selected_port() const -> std::uint16_t;
auto async_accept() -> boost::asio::awaitable<std::expected<ligo::tcp::socket_handle, modulus::error>>;
auto async_accept_tls_configured(ligo::security_config security, std::chrono::milliseconds timeout = std::chrono::milliseconds{5000}) -> boost::asio::awaitable<std::expected<ligo::tcp::tls_socket, modulus::error>>;
auto async_accept_tls_handshaken(ligo::security_config security, std::chrono::milliseconds timeout = std::chrono::milliseconds{5000}) -> boost::asio::awaitable<std::expected<ligo::tcp::tls_socket, modulus::error>>;
auto close() -> void;
private:
explicit listener(ligo::detail::state_handle state);
ligo::detail::state_handle state_{};
};
auto connect_tcp(boost::asio::any_io_executor executor, ligo::tcp::endpoint endpoint, ligo::tcp::client_options options = {}) -> boost::asio::awaitable<std::expected<ligo::tcp::socket_handle, modulus::error>>;
auto connect(boost::asio::any_io_executor executor, ligo::tcp::endpoint endpoint, ligo::tcp::client_options options = {}) -> boost::asio::awaitable<std::expected<ligo::tcp::socket_handle, modulus::error>>;
auto connect(boost::asio::any_io_executor executor, ligo::endpoint_config const& config) -> boost::asio::awaitable<std::expected<ligo::tcp::socket_handle, modulus::error>>;
auto connect_tls_configured(boost::asio::any_io_executor executor, ligo::tcp::endpoint endpoint, ligo::tcp::client_options options = {}, ligo::security_config security = {.mode = ligo::security_mode::TLS}) -> boost::asio::awaitable<std::expected<ligo::tcp::tls_socket, modulus::error>>;
auto connect_tls_handshaken(boost::asio::any_io_executor executor, ligo::tcp::endpoint endpoint, ligo::tcp::client_options options = {}, ligo::security_config security = {.mode = ligo::security_mode::TLS}) -> boost::asio::awaitable<std::expected<ligo::tcp::tls_socket, modulus::error>>;
}
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include <expected>
#include <memory>
#include <string>
#include <string_view>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <modulus.hpp>
namespace ligo {
enum class security_mode {
NONE,
TLS,
DTLS,
};
enum class verify_mode {
DEFAULTED,
NONE,
PEER,
};
struct security_config {
ligo::security_mode mode{ligo::security_mode::NONE};
ligo::verify_mode verify{ligo::verify_mode::DEFAULTED};
std::string peer_name{};
std::string ca_file{};
std::string certificate_chain_file{};
std::string private_key_file{};
std::shared_ptr<boost::asio::ssl::context> tls_context{};
};
auto make_tls_client_context(ligo::security_config const& config) -> std::expected<std::shared_ptr<boost::asio::ssl::context>, modulus::error>;
auto make_tls_server_context(ligo::security_config const& config) -> std::expected<std::shared_ptr<boost::asio::ssl::context>, modulus::error>;
auto apply_tls_client_verification(boost::asio::ssl::stream<boost::asio::ip::tcp::socket>& stream, std::string const& host, ligo::security_config const& config) -> std::expected<void, modulus::error>;
auto configure_tls_client_stream(boost::asio::ssl::stream<boost::asio::ip::tcp::socket>& stream, std::string const& host, ligo::security_config const& config) -> std::expected<void, modulus::error>;
auto validate_tls_server_config(std::string_view protocol, ligo::security_config const& config) -> std::expected<void, modulus::error>;
auto validate_tls_server_context(std::shared_ptr<boost::asio::ssl::context> const& context) -> std::expected<void, modulus::error>;
auto unsupported_security(std::string_view protocol, ligo::security_mode mode) -> modulus::error;
}
+49
View File
@@ -0,0 +1,49 @@
#pragma once
#include <expected>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <utility>
#include <boost/asio/awaitable.hpp>
#include <boost/json/value.hpp>
#include <modulus.hpp>
namespace ligo {
struct request_context {
std::string peer{};
std::string method{};
std::string route{};
};
struct service_response {
std::string content_type{"application/json"};
std::string body{};
};
class service_router {
public:
service_router() = default;
service_router(ligo::service_router const& other);
service_router(ligo::service_router&& other);
~service_router() = default;
auto operator=(ligo::service_router const& other) -> ligo::service_router&;
auto operator=(ligo::service_router&& other) -> ligo::service_router&;
auto add_route(std::string method, std::string route, std::function<boost::asio::awaitable<std::expected<boost::json::value, modulus::error>>(ligo::request_context, boost::json::value)> handler) -> void;
auto add_response_route(std::string method, std::string route, std::function<boost::asio::awaitable<std::expected<ligo::service_response, modulus::error>>(ligo::request_context, boost::json::value)> handler) -> void;
auto call(ligo::request_context context, boost::json::value payload) const -> boost::asio::awaitable<std::expected<boost::json::value, modulus::error>>;
auto call_response(ligo::request_context context, boost::json::value payload) const -> boost::asio::awaitable<std::expected<ligo::service_response, modulus::error>>;
private:
mutable std::mutex mutex_{};
std::map<std::pair<std::string, std::string>, std::shared_ptr<std::function<boost::asio::awaitable<std::expected<ligo::service_response, modulus::error>>(ligo::request_context, boost::json::value)> const>> handlers_{};
};
auto json_response(boost::json::value payload) -> ligo::service_response;
auto text_response(std::string content_type, std::string body) -> ligo::service_response;
}
+128
View File
@@ -0,0 +1,128 @@
#pragma once
#include <expected>
#include <chrono>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <utility>
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/json/value.hpp>
#include <boost/system/error_code.hpp>
#include <modulus.hpp>
#include <ligo/detail/state.hpp>
#include <ligo/config.hpp>
#include <ligo/security.hpp>
#include <ligo/service.hpp>
namespace ligo::tcp {
struct endpoint {
std::string host{"127.0.0.1"};
std::uint16_t port{0};
};
struct socket_handle {
socket_handle() = default;
socket_handle(ligo::tcp::socket_handle const&) = delete;
socket_handle(ligo::tcp::socket_handle&&) noexcept = default;
explicit socket_handle(boost::asio::ip::tcp::socket socket);
explicit socket_handle(std::shared_ptr<boost::asio::ip::tcp::socket> socket);
auto operator=(ligo::tcp::socket_handle const&) -> ligo::tcp::socket_handle& = delete;
auto operator=(ligo::tcp::socket_handle&&) noexcept -> ligo::tcp::socket_handle& = default;
auto socket() -> std::expected<std::reference_wrapper<boost::asio::ip::tcp::socket>, modulus::error>;
auto socket() const -> std::expected<std::reference_wrapper<boost::asio::ip::tcp::socket const>, modulus::error>;
auto is_open() const -> std::bool_t;
auto close(boost::system::error_code& error) -> void;
auto release_socket() -> std::expected<boost::asio::ip::tcp::socket, modulus::error>;
private:
std::shared_ptr<boost::asio::ip::tcp::socket> socket_{};
};
struct tls_socket {
struct released_stream {
released_stream(std::shared_ptr<boost::asio::ssl::context> context, boost::asio::ssl::stream<boost::asio::ip::tcp::socket> stream);
std::shared_ptr<boost::asio::ssl::context> context{};
boost::asio::ssl::stream<boost::asio::ip::tcp::socket> stream;
};
tls_socket() = default;
tls_socket(ligo::tcp::tls_socket const&) = delete;
tls_socket(ligo::tcp::tls_socket&&) noexcept = default;
tls_socket(std::shared_ptr<boost::asio::ssl::context> context, boost::asio::ip::tcp::socket socket);
auto operator=(ligo::tcp::tls_socket const&) -> ligo::tcp::tls_socket& = delete;
auto operator=(ligo::tcp::tls_socket&&) noexcept -> ligo::tcp::tls_socket& = default;
auto stream() -> std::expected<std::reference_wrapper<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>, modulus::error>;
auto stream() const -> std::expected<std::reference_wrapper<boost::asio::ssl::stream<boost::asio::ip::tcp::socket> const>, modulus::error>;
auto context() -> std::expected<std::reference_wrapper<boost::asio::ssl::context>, modulus::error>;
auto context() const -> std::expected<std::reference_wrapper<boost::asio::ssl::context const>, modulus::error>;
auto is_open() const -> std::bool_t;
auto close(boost::system::error_code& error) -> void;
auto release_stream() -> std::expected<ligo::tcp::tls_socket::released_stream, modulus::error>;
private:
std::shared_ptr<boost::asio::ssl::context> context_{};
std::unique_ptr<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>> stream_{};
};
struct request {
std::string route{};
boost::json::value payload{};
};
struct client_options {
std::chrono::milliseconds timeout{5000};
ligo::security_config security{};
};
struct server_options {
std::chrono::milliseconds timeout{5000};
std::function<void(modulus::error)> on_error{};
ligo::security_config security{};
};
class client {
public:
explicit client(ligo::tcp::endpoint endpoint, ligo::tcp::client_options options = {});
auto call(ligo::tcp::request const& request) const -> boost::asio::awaitable<std::expected<boost::json::value, modulus::error>>;
private:
ligo::tcp::endpoint endpoint_{};
ligo::tcp::client_options options_{};
};
class server {
public:
server(boost::asio::io_context& io, ligo::tcp::endpoint endpoint, ligo::service_router router, ligo::tcp::server_options options = {});
server(boost::asio::any_io_executor executor, ligo::tcp::endpoint endpoint, ligo::service_router router, ligo::tcp::server_options options = {});
server(ligo::tcp::server const&) = delete;
server(ligo::tcp::server&&) noexcept = default;
~server();
auto operator=(ligo::tcp::server const&) -> ligo::tcp::server& = delete;
auto operator=(ligo::tcp::server&&) noexcept -> ligo::tcp::server&;
auto start() -> std::expected<void, modulus::error>;
auto stop() -> void;
auto async_stop(std::chrono::milliseconds timeout = std::chrono::milliseconds{5000}) -> boost::asio::awaitable<std::expected<void, modulus::error>>;
auto selected_port() const -> std::uint16_t;
private:
ligo::detail::state_handle state_{};
};
auto client_endpoint_from_config(ligo::endpoint_config const& config) -> std::expected<ligo::tcp::endpoint, modulus::error>;
auto server_endpoint_from_config(ligo::endpoint_config const& config) -> std::expected<ligo::tcp::endpoint, modulus::error>;
auto client_options_from_config(ligo::endpoint_config const& config) -> ligo::tcp::client_options;
auto server_options_from_config(ligo::endpoint_config const& config) -> ligo::tcp::server_options;
auto connect(boost::asio::any_io_executor executor, ligo::tcp::endpoint endpoint, ligo::tcp::client_options options = {}) -> boost::asio::awaitable<std::expected<ligo::tcp::socket_handle, modulus::error>>;
auto connect(boost::asio::any_io_executor executor, ligo::endpoint_config const& config) -> boost::asio::awaitable<std::expected<ligo::tcp::socket_handle, modulus::error>>;
auto connect_tls_configured(boost::asio::any_io_executor executor, ligo::tcp::endpoint endpoint, ligo::tcp::client_options options = {}, ligo::security_config security = {.mode = ligo::security_mode::TLS}) -> boost::asio::awaitable<std::expected<ligo::tcp::tls_socket, modulus::error>>;
auto connect_tls_handshaken(boost::asio::any_io_executor executor, ligo::tcp::endpoint endpoint, ligo::tcp::client_options options = {}, ligo::security_config security = {.mode = ligo::security_mode::TLS}) -> boost::asio::awaitable<std::expected<ligo::tcp::tls_socket, modulus::error>>;
inline tls_socket::released_stream::released_stream(std::shared_ptr<boost::asio::ssl::context> context, boost::asio::ssl::stream<boost::asio::ip::tcp::socket> stream) : context{std::move(context)}, stream{std::move(stream)} {}
}
+92
View File
@@ -0,0 +1,92 @@
#pragma once
#include <expected>
#include <chrono>
#include <cstdint>
#include <memory>
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/udp.hpp>
#include <boost/json/value.hpp>
#include <modulus.hpp>
#include <ligo/detail/state.hpp>
#include <ligo/config.hpp>
#include <ligo/security.hpp>
namespace ligo::udp {
struct endpoint {
boost::asio::ip::udp::endpoint local{boost::asio::ip::udp::v4(), 0};
boost::asio::ip::udp::endpoint peer{boost::asio::ip::address_v4::loopback(), 0};
};
struct message {
boost::json::value payload{};
};
struct client_options {
ligo::udp_reliability_config reliability{};
ligo::security_config security{};
std::uint64_t session_id{1};
};
struct server_options {
ligo::udp_reliability_config reliability{};
ligo::security_config security{};
std::uint64_t session_id{1};
};
class client {
public:
client(ligo::udp::client const&) = delete;
client(ligo::udp::client&&) noexcept = default;
~client();
auto operator=(ligo::udp::client const&) -> ligo::udp::client& = delete;
auto operator=(ligo::udp::client&&) noexcept -> ligo::udp::client&;
static auto open(boost::asio::any_io_executor executor, boost::asio::ip::udp::endpoint peer, ligo::udp::client_options options = {}) -> std::expected<ligo::udp::client, modulus::error>;
static auto open(boost::asio::io_context& io, boost::asio::ip::udp::endpoint peer, ligo::udp::client_options options = {}) -> std::expected<ligo::udp::client, modulus::error>;
static auto bind(boost::asio::any_io_executor executor, ligo::udp::endpoint endpoint, ligo::udp::client_options options = {}) -> std::expected<ligo::udp::client, modulus::error>;
static auto bind(boost::asio::io_context& io, ligo::udp::endpoint endpoint, ligo::udp::client_options options = {}) -> std::expected<ligo::udp::client, modulus::error>;
static auto adopt(std::unique_ptr<boost::asio::ip::udp::socket> socket, boost::asio::ip::udp::endpoint peer, ligo::udp::client_options options = {}) -> std::expected<ligo::udp::client, modulus::error>;
auto send(boost::json::value payload) -> boost::asio::awaitable<std::expected<void, modulus::error>>;
auto send(ligo::udp::message message) -> boost::asio::awaitable<std::expected<void, modulus::error>>;
auto receive() -> boost::asio::awaitable<std::expected<ligo::udp::message, modulus::error>>;
auto local_endpoint() const -> std::expected<boost::asio::ip::udp::endpoint, modulus::error>;
auto close() -> void;
auto async_close(std::chrono::milliseconds timeout = std::chrono::milliseconds{5000}) -> boost::asio::awaitable<std::expected<void, modulus::error>>;
private:
explicit client(ligo::detail::state_handle state);
ligo::detail::state_handle state_{};
};
class server {
public:
server(ligo::udp::server const&) = delete;
server(ligo::udp::server&&) noexcept = default;
~server();
auto operator=(ligo::udp::server const&) -> ligo::udp::server& = delete;
auto operator=(ligo::udp::server&&) noexcept -> ligo::udp::server&;
static auto bind(boost::asio::any_io_executor executor, ligo::udp::endpoint endpoint, ligo::udp::server_options options = {}) -> std::expected<ligo::udp::server, modulus::error>;
static auto bind(boost::asio::io_context& io, ligo::udp::endpoint endpoint, ligo::udp::server_options options = {}) -> std::expected<ligo::udp::server, modulus::error>;
static auto adopt(std::unique_ptr<boost::asio::ip::udp::socket> socket, boost::asio::ip::udp::endpoint peer, ligo::udp::server_options options = {}) -> std::expected<ligo::udp::server, modulus::error>;
auto send(boost::json::value payload) -> boost::asio::awaitable<std::expected<void, modulus::error>>;
auto send(ligo::udp::message message) -> boost::asio::awaitable<std::expected<void, modulus::error>>;
auto receive() -> boost::asio::awaitable<std::expected<ligo::udp::message, modulus::error>>;
auto local_endpoint() const -> std::expected<boost::asio::ip::udp::endpoint, modulus::error>;
auto close() -> void;
auto async_close(std::chrono::milliseconds timeout = std::chrono::milliseconds{5000}) -> boost::asio::awaitable<std::expected<void, modulus::error>>;
private:
explicit server(ligo::detail::state_handle state);
ligo::detail::state_handle state_{};
};
auto client_endpoint_from_config(ligo::endpoint_config const& config) -> std::expected<ligo::udp::endpoint, modulus::error>;
auto server_endpoint_from_config(ligo::endpoint_config const& config) -> std::expected<ligo::udp::endpoint, modulus::error>;
auto client_options_from_config(ligo::endpoint_config const& config) -> ligo::udp::client_options;
auto server_options_from_config(ligo::endpoint_config const& config) -> ligo::udp::server_options;
}
+11
View File
@@ -0,0 +1,11 @@
{
"package": {
"name": "ligo"
},
"tooling": {
"remote": {
"name": "khodo",
"url": "https://git.thyme-labs.com/api/packages/khodo/conan"
}
}
}
Executable
+367
View File
@@ -0,0 +1,367 @@
#!/usr/bin/env python3
import json
import os
import pathlib
import shlex
import subprocess
import sys
PACKAGE_REF = "modulus/latest"
CONAN_VERSION = "2.26.2"
FETCH_IMAGE = "modulus-fetch:dev"
COMMANDS = (
"configure",
"build",
"test",
"package",
"lsp",
)
COMMON_DIR = pathlib.Path("build") / "build_common"
TOOLS_DIR = pathlib.Path("build") / "build_tools"
RUNTIME_DIR = COMMON_DIR / "modulus"
CONAN_HOME_DIR = COMMON_DIR / "conan_cache"
DEFAULT_REMOTE_NAME = "khodo"
DEFAULT_REMOTE_URL = "https://git.thyme-labs.com/api/packages/khodo/conan"
DEFAULT_DOCKER_MARKER = "/opt/modulus-build-image"
def repo_root():
root = pathlib.Path(__file__).resolve().parent
if not (root / "modulus.json").is_file():
raise SystemExit("modulus.py must be run from a Modulus-managed repository root")
return root
def read_json(path):
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
raise SystemExit(f"failed to read {path}: {error}") from error
if not isinstance(data, dict):
raise SystemExit(f"{path} must contain a JSON object")
return data
def remote_config(root):
config = read_json(root / "modulus.json")
remote = config.get(
"tooling",
{},
).get("remote", {})
if not isinstance(remote, dict):
remote = {}
return (
str(remote.get("name") or DEFAULT_REMOTE_NAME),
str(remote.get("url") or DEFAULT_REMOTE_URL),
)
def runtime_ready(root):
return (
(root / TOOLS_DIR / "configure.py").is_file()
and (root / TOOLS_DIR / "lsp.py").is_file()
and (root / RUNTIME_DIR / "modulus" / "cli.py").is_file()
and (root / RUNTIME_DIR / "modulus" / "export.py").is_file()
)
def optional_credentials():
user = os.environ.get("CONAN_USER")
token = os.environ.get("CONAN_TOKEN")
if bool(user) != bool(token):
raise SystemExit("CONAN_USER and CONAN_TOKEN must either both be set or both be unset")
return user, token
def env_flag(name):
return os.environ.get(name, "").lower() in {"1", "true", "yes", "on"}
def docker_marker(root):
config = read_json(root / "modulus.json")
marker = config.get(
"tooling",
{},
).get("docker", {}).get("marker", DEFAULT_DOCKER_MARKER)
return pathlib.Path(str(marker))
def direct_mode(root):
return env_flag("MODULUS_DIRECT") or docker_marker(root).exists()
def run(command, input_text=None, capture=False, display_command=None, env=None, check=True):
visible = display_command if display_command is not None else command
print("+", " ".join(shlex.quote(str(part)) for part in visible))
completed = subprocess.run(
[str(part) for part in command],
input=input_text,
stdout=subprocess.PIPE if capture else None,
stderr=subprocess.PIPE if capture else None,
text=True,
check=False,
env=env,
)
if check and completed.returncode != 0:
if capture and completed.stdout:
print(completed.stdout, end="")
if capture and completed.stderr:
print(completed.stderr, end="", file=sys.stderr)
raise SystemExit(completed.returncode)
return completed
def fetch_dockerfile():
return f"""FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \\
&& apt-get install -y --no-install-recommends \\
ca-certificates \\
python3 \\
python3-venv \\
&& rm -rf /var/lib/apt/lists/*
RUN python3 -m venv /opt/modulus-python
ENV PATH=/opt/modulus-python/bin:$PATH
RUN python -m pip install --no-cache-dir conan=={CONAN_VERSION}
WORKDIR /workspace
"""
def ensure_fetch_image():
inspected = subprocess.run(
[
"docker",
"image",
"inspect",
FETCH_IMAGE,
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
)
if inspected.returncode == 0:
return
run(
[
"docker",
"build",
"--tag",
FETCH_IMAGE,
"-",
],
input_text=fetch_dockerfile(),
)
def in_container():
return pathlib.Path("/.dockerenv").exists()
def docker_workspace_args(root):
if in_container():
container = os.environ.get("HOSTNAME", "").strip()
if not container:
raise SystemExit("HOSTNAME is required to share the CI workspace with the Modulus fetch container")
return [
"--volumes-from",
container,
]
return [
"--volume",
f"{root}:{root}",
]
def fetch_env(root):
env = os.environ.copy()
env["CONAN_HOME"] = str(root / CONAN_HOME_DIR)
return env
def run_fetch(root, command, capture=False, display_command=None, check=True):
if direct_mode(root):
return run(command, capture=capture, display_command=display_command, env=fetch_env(root), check=check)
ensure_fetch_image()
docker_command = [
"docker",
"run",
"--rm",
"--interactive",
"--user",
f"{os.getuid()}:{os.getgid()}",
"--env",
"HOME=/tmp",
"--env",
"CONAN_HOME",
"--env",
"CONAN_USER",
"--env",
"CONAN_TOKEN",
*docker_workspace_args(root),
"--workdir",
str(root),
FETCH_IMAGE,
*command,
]
return run(docker_command, capture=capture, display_command=display_command, env=fetch_env(root), check=check)
def configure_remote(root, remote_name, remote_url):
user, token = optional_credentials()
run_fetch(
root,
[
"conan",
"remote",
"add",
"-f",
remote_name,
remote_url,
],
)
if not user and not token:
return
run_fetch(
root,
[
"conan",
"remote",
"login",
remote_name,
user,
"-p",
token,
],
display_command=[
"conan",
"remote",
"login",
remote_name,
user,
"-p",
"<hidden>",
],
)
def package_refs(data):
refs = []
for location_data in data.values():
if not isinstance(location_data, dict):
continue
for ref, ref_data in location_data.items():
if not ref.startswith(PACKAGE_REF) or not isinstance(ref_data, dict):
continue
revisions = ref_data.get(
"revisions",
{},
)
if not isinstance(revisions, dict):
continue
for recipe_revision, revision_data in revisions.items():
if not isinstance(revision_data, dict):
continue
packages = revision_data.get(
"packages",
{},
)
if not isinstance(packages, dict):
continue
for package_id, package_data in packages.items():
package_revisions = package_data.get(
"revisions",
{},
)if isinstance(package_data, dict) else {}
if isinstance(package_revisions, dict) and package_revisions:
for package_revision in package_revisions:
refs.append(f"{PACKAGE_REF}#{recipe_revision}:{package_id}#{package_revision}")
else:
refs.append(f"{PACKAGE_REF}#{recipe_revision}:{package_id}")
return sorted(refs)
def remove_cached_modulus(root):
run_fetch(
root,
[
"conan",
"remove",
PACKAGE_REF + ":*",
"-c",
],
check=False,
)
def download_modulus(root, remote_name):
remove_cached_modulus(root)
run_fetch(
root,
[
"conan",
"download",
PACKAGE_REF + ":*",
"-r",
remote_name,
],
)
completed = run_fetch(
root,
[
"conan",
"list",
PACKAGE_REF + ":*",
"-c",
"-f",
"json",
],
capture=True,
)
refs = package_refs(json.loads(completed.stdout))
if not refs:
raise SystemExit(f"no downloaded {PACKAGE_REF} package found in the Conan cache")
package_ref = refs[-1]
completed = run_fetch(
root,
[
"conan",
"cache",
"path",
package_ref,
],
capture=True,
)
package_root = pathlib.Path(completed.stdout.strip())
if not (package_root / "modulus" / "export.py").is_file() or not (package_root / "tools").is_dir():
raise SystemExit(f"downloaded {PACKAGE_REF} package does not contain Modulus export files: {package_root}")
return package_root
def materialize_runtime(root):
remote_name, remote_url = remote_config(root)
configure_remote(root, remote_name, remote_url)
package_root = download_modulus(root, remote_name)
sys.path.insert(0, str(package_root))
import modulus.export
modulus.export.sync_project(root, package_root)
def ensure_runtime(root):
if runtime_ready(root):
return
materialize_runtime(root)
def delegate(command, argv):
root = repo_root()
ensure_runtime(root)
sys.path.insert(0, str(root / RUNTIME_DIR))
import modulus.cli
previous_argv = sys.argv
previous_cwd = pathlib.Path.cwd()
sys.argv = [
str(root / "modulus.py"),
*argv,
]
os.chdir(root)
try:
return modulus.cli.main(command)
finally:
os.chdir(previous_cwd)
sys.argv = previous_argv
def main(argv):
if not argv or argv[0] in ("-h", "--help"):
print("usage: python3 modulus.py " + "|".join(COMMANDS) + " ...")
return 0 if argv else 2
command = argv[0]
if command not in COMMANDS:
raise SystemExit(f"unknown Modulus command: {command}")
return delegate(command, argv[1:])
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
Executable
+414
View File
@@ -0,0 +1,414 @@
#include <ligo/config.hpp>
#include <cstdint>
#include <expected>
#include <limits>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <boost/json/array.hpp>
#include <boost/json/object.hpp>
#include <boost/json/value.hpp>
#include <modulus.hpp>
#include <ligo/detail/errors.hpp>
#include <ligo/security.hpp>
namespace {
constexpr auto json_options = modulus::json_config::options{
.category = "ligo",
.invalid_argument_code = ligo::detail::error_invalid_argument,
.not_found_code = ligo::detail::error_not_found
};
auto protocol_name(ligo::protocol value) -> std::string_view {
switch (value) {
case ligo::protocol::HTTP:
return "http";
case ligo::protocol::TCP:
return "tcp";
case ligo::protocol::UDP:
return "udp";
}
return "tcp";
}
auto security_mode_name(ligo::security_mode value) -> std::string_view {
switch (value) {
case ligo::security_mode::NONE:
return "none";
case ligo::security_mode::TLS:
return "tls";
case ligo::security_mode::DTLS:
return "dtls";
}
return "none";
}
auto verify_mode_name(ligo::verify_mode value) -> std::string_view {
switch (value) {
case ligo::verify_mode::DEFAULTED:
return "defaulted";
case ligo::verify_mode::NONE:
return "none";
case ligo::verify_mode::PEER:
return "peer";
}
return "defaulted";
}
auto parse_security_config(boost::json::value const& value) -> std::expected<ligo::security_config, modulus::error> {
auto object = modulus::json_config::require_object(value, "endpoint security", json_options);
if (!object) {
return ligo::detail::fail<ligo::security_config>(object.error());
}
auto unknown = modulus::json_config::reject_unknown_fields(**object, {
"mode",
"verify",
"peer_name",
"ca_file",
"certificate_chain_file",
"private_key_file",
}, "endpoint security", json_options);
if (!unknown) {
return ligo::detail::fail<ligo::security_config>(unknown.error());
}
auto config = ligo::security_config{};
auto mode = modulus::json_config::optional_enum_field(**object, "mode", config.mode, {
{
.name = "none",
.value = ligo::security_mode::NONE,
},
{
.name = "tls",
.value = ligo::security_mode::TLS,
},
{
.name = "dtls",
.value = ligo::security_mode::DTLS,
},
}, "endpoint security mode must be none, tls, or dtls", json_options);
if (!mode) {
return ligo::detail::fail<ligo::security_config>(mode.error());
}
auto verify = modulus::json_config::optional_enum_field(**object, "verify", config.verify, {
{
.name = "defaulted",
.value = ligo::verify_mode::DEFAULTED,
},
{
.name = "none",
.value = ligo::verify_mode::NONE,
},
{
.name = "peer",
.value = ligo::verify_mode::PEER,
},
}, "endpoint security verify must be defaulted, none, or peer", json_options);
if (!verify) {
return ligo::detail::fail<ligo::security_config>(verify.error());
}
auto peer_name = modulus::json_config::optional_string_field(**object, "peer_name", config.peer_name, json_options);
auto ca_file = modulus::json_config::optional_string_field(**object, "ca_file", config.ca_file, json_options);
auto certificate_chain_file = modulus::json_config::optional_string_field(**object, "certificate_chain_file", config.certificate_chain_file, json_options);
auto private_key_file = modulus::json_config::optional_string_field(**object, "private_key_file", config.private_key_file, json_options);
if (!peer_name) {
return ligo::detail::fail<ligo::security_config>(peer_name.error());
}
if (!ca_file) {
return ligo::detail::fail<ligo::security_config>(ca_file.error());
}
if (!certificate_chain_file) {
return ligo::detail::fail<ligo::security_config>(certificate_chain_file.error());
}
if (!private_key_file) {
return ligo::detail::fail<ligo::security_config>(private_key_file.error());
}
config.mode = *mode;
config.verify = *verify;
config.peer_name = std::move(*peer_name);
config.ca_file = std::move(*ca_file);
config.certificate_chain_file = std::move(*certificate_chain_file);
config.private_key_file = std::move(*private_key_file);
return modulus::success(std::move(config));
}
}
auto ligo::validate(ligo::endpoint_config const& config) -> std::expected<void, modulus::error> {
if (config.name.empty()) {
return ligo::detail::fail_void(ligo::detail::error_invalid_argument, "endpoint name is required");
}
if (config.timeout.count() <= 0) {
return ligo::detail::fail_void(ligo::detail::error_invalid_argument, "timeout_ms must be positive");
}
if (config.http.max_body_size == 0) {
return ligo::detail::fail_void(ligo::detail::error_invalid_argument, "max_body_size must be positive");
}
if (config.udp.resend_interval.count() <= 0) {
return ligo::detail::fail_void(ligo::detail::error_invalid_argument, "resend_interval_ms must be positive");
}
if (config.udp.max_in_flight == 0) {
return ligo::detail::fail_void(ligo::detail::error_invalid_argument, "max_in_flight must be positive");
}
if (config.udp.session_timeout.count() <= 0) {
return ligo::detail::fail_void(ligo::detail::error_invalid_argument, "session_timeout_ms must be positive");
}
return {};
}
auto ligo::parse_endpoint_config(boost::json::value const& value) -> std::expected<ligo::endpoint_config, modulus::error> {
auto object = modulus::json_config::require_object(value, "endpoint", json_options);
if (!object) {
return ligo::detail::fail<ligo::endpoint_config>(object.error());
}
auto unknown = modulus::json_config::reject_unknown_fields(**object, {
"name",
"protocol",
"bind",
"connect",
"timeout_ms",
"security",
"http",
"udp",
}, "endpoint", json_options);
if (!unknown) {
return ligo::detail::fail<ligo::endpoint_config>(unknown.error());
}
auto config = ligo::endpoint_config{};
auto name = modulus::json_config::required_string_field(**object, "name", "endpoint", json_options);
if (!name) {
return ligo::detail::fail<ligo::endpoint_config>(name.error());
}
auto protocol = modulus::json_config::optional_enum_field(**object, "protocol", config.protocol, {
{
.name = "http",
.value = ligo::protocol::HTTP,
},
{
.name = "tcp",
.value = ligo::protocol::TCP,
},
{
.name = "udp",
.value = ligo::protocol::UDP,
},
}, "endpoint protocol must be http, tcp, or udp", json_options);
if (!protocol) {
return ligo::detail::fail<ligo::endpoint_config>(protocol.error());
}
auto bind = modulus::json_config::optional_string_field(**object, "bind", config.bind, json_options);
auto connect = modulus::json_config::optional_string_field(**object, "connect", config.connect, json_options);
auto timeout = modulus::json_config::positive_milliseconds_field(**object, "timeout_ms", config.timeout, static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max()), json_options);
if (!bind) {
return ligo::detail::fail<ligo::endpoint_config>(bind.error());
}
if (!connect) {
return ligo::detail::fail<ligo::endpoint_config>(connect.error());
}
if (!timeout) {
return ligo::detail::fail<ligo::endpoint_config>(timeout.error());
}
config.name = std::move(*name);
config.protocol = *protocol;
config.bind = std::move(*bind);
config.connect = std::move(*connect);
config.timeout = *timeout;
if (auto found = (*object)->find("security"); found != (*object)->end()) {
auto parsed_security = parse_security_config(found->value());
if (!parsed_security) {
return ligo::detail::fail<ligo::endpoint_config>(parsed_security.error());
}
config.security = std::move(*parsed_security);
}
if (auto found = (*object)->find("http"); found != (*object)->end()) {
auto http = modulus::json_config::require_object(found->value(), "endpoint http", json_options);
if (!http) {
return ligo::detail::fail<ligo::endpoint_config>(http.error());
}
auto http_unknown = modulus::json_config::reject_unknown_fields(**http, {"max_body_size"}, "endpoint http", json_options);
if (!http_unknown) {
return ligo::detail::fail<ligo::endpoint_config>(http_unknown.error());
}
auto max_body_size = modulus::json_config::positive_uint64_field(**http, "max_body_size", config.http.max_body_size, std::numeric_limits<std::uint64_t>::max(), json_options);
if (!max_body_size) {
return ligo::detail::fail<ligo::endpoint_config>(max_body_size.error());
}
config.http.max_body_size = *max_body_size;
}
if (auto found = (*object)->find("udp"); found != (*object)->end()) {
auto udp = modulus::json_config::require_object(found->value(), "endpoint udp", json_options);
if (!udp) {
return ligo::detail::fail<ligo::endpoint_config>(udp.error());
}
auto udp_unknown = modulus::json_config::reject_unknown_fields(**udp, {
"resend_interval_ms",
"max_in_flight",
"session_timeout_ms",
}, "endpoint udp", json_options);
if (!udp_unknown) {
return ligo::detail::fail<ligo::endpoint_config>(udp_unknown.error());
}
auto resend_interval = modulus::json_config::positive_milliseconds_field(**udp, "resend_interval_ms", config.udp.resend_interval, static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max()), json_options);
if (!resend_interval) {
return ligo::detail::fail<ligo::endpoint_config>(resend_interval.error());
}
auto max_in_flight = modulus::json_config::positive_uint64_field(**udp, "max_in_flight", config.udp.max_in_flight, std::numeric_limits<std::uint32_t>::max(), json_options);
if (!max_in_flight) {
return ligo::detail::fail<ligo::endpoint_config>(max_in_flight.error());
}
auto session_timeout = modulus::json_config::positive_milliseconds_field(**udp, "session_timeout_ms", config.udp.session_timeout, static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max()), json_options);
if (!session_timeout) {
return ligo::detail::fail<ligo::endpoint_config>(session_timeout.error());
}
config.udp.resend_interval = *resend_interval;
config.udp.max_in_flight = static_cast<std::uint32_t>(*max_in_flight);
config.udp.session_timeout = *session_timeout;
}
auto valid = ligo::validate(config);
if (!valid) {
return ligo::detail::fail<ligo::endpoint_config>(valid.error());
}
return modulus::success(std::move(config));
}
auto ligo::parse_endpoint_configs(boost::json::value const& value) -> std::expected<std::vector<ligo::endpoint_config>, modulus::error> {
auto object = modulus::json_config::require_object(value, "config root", json_options);
if (!object) {
return ligo::detail::fail<std::vector<ligo::endpoint_config>>(object.error());
}
auto unknown = modulus::json_config::reject_unknown_fields(**object, {"endpoints"}, "config root", json_options);
if (!unknown) {
return ligo::detail::fail<std::vector<ligo::endpoint_config>>(unknown.error());
}
auto found = (*object)->find("endpoints");
if (found == (*object)->end() || !found->value().is_array()) {
return ligo::detail::fail<std::vector<ligo::endpoint_config>>(ligo::detail::error_invalid_argument, "config endpoints array is required");
}
auto configs = std::vector<ligo::endpoint_config>{};
for (auto const& endpoint_value : found->value().as_array()) {
auto parsed = ligo::parse_endpoint_config(endpoint_value);
if (!parsed) {
return ligo::detail::fail<std::vector<ligo::endpoint_config>>(parsed.error());
}
configs.push_back(std::move(*parsed));
}
return modulus::success(std::move(configs));
}
auto ligo::load_endpoint_configs(std::string const& path) -> std::expected<std::vector<ligo::endpoint_config>, modulus::error> {
auto value = modulus::json_config::load_value(path, json_options);
if (!value) {
return ligo::detail::fail<std::vector<ligo::endpoint_config>>(value.error());
}
return ligo::parse_endpoint_configs(*value);
}
auto ligo::to_json(ligo::udp_reliability_config const& config) -> boost::json::value {
return boost::json::object{
{
"resend_interval_ms",
config.resend_interval.count(),
},
{
"max_in_flight",
config.max_in_flight,
},
{
"session_timeout_ms",
config.session_timeout.count(),
},
};
}
auto ligo::to_json(ligo::http_config const& config) -> boost::json::value {
return boost::json::object{
{
"max_body_size",
config.max_body_size,
},
};
}
auto ligo::to_json(ligo::security_config const& config) -> boost::json::value {
return boost::json::object{
{
"mode",
std::string{security_mode_name(config.mode)},
},
{
"verify",
std::string{verify_mode_name(config.verify)},
},
{
"peer_name",
config.peer_name,
},
{
"ca_file",
config.ca_file,
},
{
"certificate_chain_file",
config.certificate_chain_file,
},
{
"private_key_file",
config.private_key_file,
},
};
}
auto ligo::to_json(ligo::endpoint_config const& config) -> boost::json::value {
return boost::json::object{
{
"name",
config.name,
},
{
"protocol",
std::string{protocol_name(config.protocol)},
},
{
"bind",
config.bind,
},
{
"connect",
config.connect,
},
{
"timeout_ms",
config.timeout.count(),
},
{
"security",
ligo::to_json(config.security),
},
{
"http",
ligo::to_json(config.http),
},
{
"udp",
ligo::to_json(config.udp),
},
};
}
auto ligo::to_json(std::vector<ligo::endpoint_config> const& configs) -> boost::json::value {
auto endpoints = boost::json::array{};
for (auto const& config : configs) {
endpoints.push_back(ligo::to_json(config));
}
return boost::json::object{
{
"endpoints",
std::move(endpoints),
},
};
}
+14
View File
@@ -0,0 +1,14 @@
#include <ligo/detail/errors.hpp>
#include <utility>
#include <modulus.hpp>
#include <cstdint>
auto ligo::detail::make_error(std::int32_t code, std::string message, modulus::source_location location) -> modulus::error {
return modulus::make_error("ligo", code, std::move(message), location);
}
auto ligo::detail::fail_void(modulus::error error) -> std::expected<void, modulus::error> {
return modulus::fail<void>(std::move(error));
}
+10
View File
@@ -0,0 +1,10 @@
#include <ligo/detail/lifecycle.hpp>
#include <boost/beast/core.hpp>
#include <boost/system/error_code.hpp>
auto ligo::detail::cancel_session_stream(boost::beast::tcp_stream& stream) -> void {
auto ignored = boost::system::error_code{};
stream.socket().cancel(ignored);
stream.socket().close(ignored);
}
+403
View File
@@ -0,0 +1,403 @@
#include <ligo/detail/transport.hpp>
#include <atomic>
#include <charconv>
#include <cstdint>
#include <expected>
#include <limits>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/as_tuple.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/ip/address.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/beast/core/error.hpp>
#include <boost/json/object.hpp>
#include <boost/json/serialize.hpp>
#include <boost/json/value.hpp>
#include <boost/system/error_code.hpp>
#include <modulus.hpp>
#include <ligo/detail/errors.hpp>
#include <ligo/detail/state.hpp>
#include <ligo/service.hpp>
namespace {
auto error_status_code(boost::system::error_code const& error) -> std::int32_t {
if (!error) {
return ligo::detail::error_ok;
}
if (error == boost::beast::error::timeout || error == boost::asio::error::timed_out) {
return ligo::detail::error_timeout;
}
if (error == boost::asio::error::invalid_argument || error == boost::asio::error::address_family_not_supported || error == boost::asio::error::no_protocol_option || error == boost::asio::error::not_socket) {
return ligo::detail::error_invalid_argument;
}
return ligo::detail::error_unavailable;
}
}
ligo::detail::state_handle::state_handle(ligo::detail::state_handle&& other) noexcept : state_{other.state_.exchange(nullptr)} {}
auto ligo::detail::state_handle::operator=(ligo::detail::state_handle&& other) noexcept -> ligo::detail::state_handle& {
if (this != &other) {
state_.store(other.state_.exchange(nullptr));
}
return *this;
}
ligo::detail::state_handle::operator std::bool_t() const noexcept {
return static_cast<std::bool_t>(state_.load());
}
ligo::detail::unique_state::operator std::bool_t() const noexcept {
return static_cast<std::bool_t>(state_);
}
auto ligo::detail::unique_state::ignore_state(void* state) -> void {
static_cast<void>(state);
}
auto ligo::detail::status_name(std::int32_t code) -> std::string {
switch (code) {
case ligo::detail::error_ok:
return "ok";
case ligo::detail::error_invalid_argument:
return "invalid_argument";
case ligo::detail::error_not_found:
return "not_found";
case ligo::detail::error_unavailable:
return "unavailable";
case ligo::detail::error_timeout:
return "timeout";
case ligo::detail::error_internal:
return "internal";
}
return "internal";
}
auto ligo::detail::parse_status_code(std::string_view value) -> std::int32_t {
if (value == "invalid_argument") {
return ligo::detail::error_invalid_argument;
}
if (value == "not_found") {
return ligo::detail::error_not_found;
}
if (value == "unavailable") {
return ligo::detail::error_unavailable;
}
if (value == "timeout") {
return ligo::detail::error_timeout;
}
if (value == "ok") {
return ligo::detail::error_ok;
}
return ligo::detail::error_internal;
}
auto ligo::detail::network_failure(boost::system::error_code const& error) -> modulus::error {
return ligo::detail::make_error(error_status_code(error), error.message());
}
auto ligo::detail::operation_failure(boost::system::error_code const& error, std::string timeout_message) -> modulus::error {
if (error_status_code(error) == ligo::detail::error_timeout) {
return ligo::detail::make_error(ligo::detail::error_timeout, std::move(timeout_message));
}
return ligo::detail::network_failure(error);
}
auto ligo::detail::start_stream_deadline(boost::beast::tcp_stream& stream, std::chrono::milliseconds timeout) -> ligo::detail::stream_deadline {
stream.expires_after(timeout);
return {
.cancellation = std::make_shared<boost::asio::cancellation_signal>(),
};
}
auto ligo::detail::start_stream_deadline(boost::asio::ip::tcp::socket& socket, std::chrono::milliseconds timeout) -> ligo::detail::stream_deadline {
auto deadline = ligo::detail::stream_deadline{
.timer = std::make_shared<boost::asio::steady_timer>(socket.get_executor()),
.timed_out = std::make_shared<std::atomic<std::bool_t>>(false),
.active = std::make_shared<std::atomic<std::bool_t>>(true),
.cancellation = std::make_shared<boost::asio::cancellation_signal>(),
};
deadline.timer->expires_after(timeout);
deadline.timer->async_wait([timer = deadline.timer, timed_out = deadline.timed_out, active = deadline.active, cancellation = deadline.cancellation](boost::system::error_code const& error) {
if (!error && active->exchange(false, std::memory_order_acq_rel)) {
timed_out->store(true, std::memory_order_release);
cancellation->emit(boost::asio::cancellation_type::terminal);
}
});
return deadline;
}
auto ligo::detail::cancel_stream_deadline(ligo::detail::stream_deadline& deadline) -> void {
if (deadline.active) {
deadline.active->store(false, std::memory_order_release);
}
if (deadline.timer) {
deadline.timer->cancel();
}
}
auto ligo::detail::deadline_token(ligo::detail::stream_deadline& deadline) -> decltype(boost::asio::bind_cancellation_slot(std::declval<boost::asio::cancellation_slot>(), boost::asio::as_tuple(boost::asio::use_awaitable))) {
return boost::asio::bind_cancellation_slot(deadline.cancellation->slot(), boost::asio::as_tuple(boost::asio::use_awaitable));
}
auto ligo::detail::operation_failure(boost::system::error_code const& error, ligo::detail::stream_deadline const& deadline, std::string timeout_message) -> modulus::error {
if (deadline.timed_out && deadline.timed_out->load(std::memory_order_acquire)) {
return ligo::detail::make_error(ligo::detail::error_timeout, std::move(timeout_message));
}
return ligo::detail::operation_failure(error, std::move(timeout_message));
}
auto ligo::detail::invalid_argument(std::string message) -> modulus::error {
return ligo::detail::make_error(ligo::detail::error_invalid_argument, std::move(message));
}
auto ligo::detail::unavailable(std::string message) -> modulus::error {
return ligo::detail::make_error(ligo::detail::error_unavailable, std::move(message));
}
auto ligo::detail::validate_positive_timeout(std::chrono::milliseconds timeout, std::string_view name) -> std::expected<void, modulus::error> {
if (timeout <= std::chrono::milliseconds{0}) {
return ligo::detail::fail_void(ligo::detail::invalid_argument(std::string{name} + " must be positive"));
}
return {};
}
auto ligo::detail::report_error(std::function<void(modulus::error)> const& on_error, modulus::error error) -> void {
if (!on_error) {
return;
}
try {
on_error(std::move(error));
} catch (...) {
}
}
auto ligo::detail::report_error(std::function<void(modulus::error)> const& on_error, std::expected<void, modulus::error> status) -> void {
if (!status) {
ligo::detail::report_error(on_error, status.error());
}
}
auto ligo::detail::response_envelope(std::expected<boost::json::value, modulus::error> result) -> boost::json::value {
auto envelope = boost::json::object{};
envelope["ok"] = result.has_value();
if (result) {
envelope["payload"] = std::move(*result);
} else {
envelope["code"] = ligo::detail::status_name(result.error().code());
envelope["message"] = result.error().message();
}
return envelope;
}
auto ligo::detail::serialize_response_envelope(std::expected<boost::json::value, modulus::error> result) -> std::string {
return boost::json::serialize(ligo::detail::response_envelope(std::move(result)));
}
auto ligo::detail::unwrap_response_envelope(boost::json::value value, std::string_view protocol_name) -> std::expected<boost::json::value, modulus::error> {
auto const prefix = std::string{protocol_name};
if (!value.is_object()) {
return ligo::detail::fail<boost::json::value>(ligo::detail::error_invalid_argument, prefix + " response must be an object");
}
auto const& object = value.as_object();
auto ok = object.find("ok");
if (ok == object.end() || !ok->value().is_bool()) {
return ligo::detail::fail<boost::json::value>(ligo::detail::error_invalid_argument, prefix + " response missing ok flag");
}
if (ok->value().as_bool()) {
auto payload = object.find("payload");
if (payload == object.end()) {
return ligo::detail::fail<boost::json::value>(ligo::detail::error_invalid_argument, prefix + " response missing payload");
}
return modulus::success(boost::json::value{payload->value()});
}
auto code = object.find("code");
auto message_member = object.find("message");
return ligo::detail::fail<boost::json::value>(code != object.end() && code->value().is_string() ? ligo::detail::parse_status_code(std::string_view{code->value().as_string()}) : ligo::detail::error_internal, message_member != object.end() && message_member->value().is_string() ? std::string{message_member->value().as_string()} : prefix + " request failed");
}
auto ligo::detail::parse_port(std::string_view text) -> std::expected<std::uint16_t, modulus::error> {
if (text.empty()) {
return ligo::detail::fail<std::uint16_t>(ligo::detail::error_invalid_argument, "endpoint port is required");
}
auto parsed = 0U;
auto const* begin = text.data();
auto const* end = text.data() + text.size();
auto [ptr, error] = std::from_chars(begin, end, parsed);
if (error != std::errc{} || ptr != end || parsed > std::numeric_limits<std::uint16_t>::max()) {
return ligo::detail::fail<std::uint16_t>(ligo::detail::error_invalid_argument, "endpoint port is invalid");
}
return modulus::success(static_cast<std::uint16_t>(parsed));
}
auto ligo::detail::parse_endpoint_text(std::string_view text, std::uint16_t fallback_port) -> std::expected<ligo::detail::endpoint_parts, modulus::error> {
if (text.empty()) {
return modulus::success(ligo::detail::endpoint_parts{
.host = "127.0.0.1",
.port = fallback_port,
});
}
auto host = std::string{};
auto port = fallback_port;
if (text.front() == '[') {
auto const close = text.find(']');
if (close == std::string_view::npos || close == 1) {
return ligo::detail::fail<ligo::detail::endpoint_parts>(ligo::detail::error_invalid_argument, "endpoint IPv6 host is invalid");
}
host = std::string{text.substr(1, close - 1)};
auto const rest = text.substr(close + 1);
if (!rest.empty()) {
if (rest.front() != ':') {
return ligo::detail::fail<ligo::detail::endpoint_parts>(ligo::detail::error_invalid_argument, "endpoint has trailing characters");
}
auto parsed_port = ligo::detail::parse_port(rest.substr(1));
if (!parsed_port) {
return ligo::detail::fail<ligo::detail::endpoint_parts>(parsed_port.error());
}
port = *parsed_port;
}
} else {
auto colon_count = std::size_t{};
for (auto character : text) {
colon_count += character == ':' ? 1U : 0U;
}
if (colon_count == 1U) {
auto const separator = text.find(':');
host = std::string{text.substr(0, separator)};
auto parsed_port = ligo::detail::parse_port(text.substr(separator + 1));
if (!parsed_port) {
return ligo::detail::fail<ligo::detail::endpoint_parts>(parsed_port.error());
}
port = *parsed_port;
} else {
host = std::string{text};
}
}
if (host.empty()) {
return ligo::detail::fail<ligo::detail::endpoint_parts>(ligo::detail::error_invalid_argument, "endpoint host is required");
}
return modulus::success(ligo::detail::endpoint_parts{
.host = std::move(host),
.port = port,
});
}
auto ligo::detail::parse_client_endpoint_text(std::string_view text) -> std::expected<ligo::detail::endpoint_parts, modulus::error> {
if (text.empty()) {
return ligo::detail::fail<ligo::detail::endpoint_parts>(ligo::detail::error_invalid_argument, "endpoint connect is required");
}
auto parsed = ligo::detail::parse_endpoint_text(text, 0);
if (!parsed) {
return parsed;
}
if (parsed->port == 0) {
return ligo::detail::fail<ligo::detail::endpoint_parts>(ligo::detail::error_invalid_argument, "endpoint connect port is required");
}
return parsed;
}
auto ligo::detail::parse_server_endpoint_text(std::string_view text) -> std::expected<ligo::detail::endpoint_parts, modulus::error> {
return ligo::detail::parse_endpoint_text(text, 0);
}
auto ligo::detail::start_tcp_acceptor(boost::asio::ip::tcp::acceptor& acceptor, std::string const& host, std::uint16_t port) -> std::expected<std::uint16_t, modulus::error> {
auto close_on_failure = [&acceptor] {
auto ignored = boost::system::error_code{};
acceptor.close(ignored);
};
auto bind_endpoint = [&](boost::asio::ip::tcp::endpoint endpoint) -> std::expected<std::uint16_t, modulus::error> {
auto error = boost::system::error_code{};
close_on_failure();
acceptor.open(endpoint.protocol(), error);
if (error) {
return ligo::detail::fail<std::uint16_t>(ligo::detail::error_unavailable, error.message());
}
acceptor.set_option(boost::asio::socket_base::reuse_address(true), error);
if (error) {
close_on_failure();
return ligo::detail::fail<std::uint16_t>(ligo::detail::error_unavailable, error.message());
}
acceptor.bind(endpoint, error);
if (error) {
close_on_failure();
return ligo::detail::fail<std::uint16_t>(ligo::detail::error_unavailable, error.message());
}
acceptor.listen(boost::asio::socket_base::max_listen_connections, error);
if (error) {
close_on_failure();
return ligo::detail::fail<std::uint16_t>(ligo::detail::error_unavailable, error.message());
}
auto bound_endpoint = acceptor.local_endpoint(error);
if (error) {
close_on_failure();
return ligo::detail::fail<std::uint16_t>(ligo::detail::error_unavailable, error.message());
}
return modulus::success(bound_endpoint.port());
};
auto error = boost::system::error_code{};
auto address = boost::asio::ip::make_address(host, error);
if (!error) {
return bind_endpoint({
address,
port,
});
}
auto resolver = boost::asio::ip::tcp::resolver{acceptor.get_executor()};
auto endpoints = resolver.resolve(host, std::to_string(port), boost::asio::ip::tcp::resolver::passive, error);
if (error) {
return ligo::detail::fail<std::uint16_t>(ligo::detail::error_unavailable, error.message());
}
auto last_error = ligo::detail::unavailable("no TCP bind endpoint resolved");
for (auto const& endpoint : endpoints) {
auto selected = bind_endpoint(endpoint.endpoint());
if (selected) {
return selected;
}
last_error = selected.error();
}
return ligo::detail::fail<std::uint16_t>(last_error);
}
auto ligo::detail::resolve_tcp_endpoints(boost::asio::any_io_executor executor, std::string const& host, std::uint16_t port, std::chrono::milliseconds timeout, std::string timeout_message) -> boost::asio::awaitable<std::expected<boost::asio::ip::tcp::resolver::results_type, modulus::error>> {
auto resolver = std::make_shared<boost::asio::ip::tcp::resolver>(executor);
auto timer = std::make_shared<boost::asio::steady_timer>(executor);
auto timed_out = std::make_shared<std::atomic<std::bool_t>>(false);
timer->expires_after(timeout);
timer->async_wait([resolver, timer, timed_out](boost::system::error_code const& error) {
if (!error) {
timed_out->store(true, std::memory_order_release);
resolver->cancel();
}
});
auto [resolve_error, endpoints] = co_await resolver->async_resolve(host, std::to_string(port), boost::asio::as_tuple(boost::asio::use_awaitable));
timer->cancel();
if (resolve_error) {
if (resolve_error == boost::asio::error::operation_aborted && timed_out->load(std::memory_order_acquire)) {
co_return ligo::detail::fail<boost::asio::ip::tcp::resolver::results_type>(ligo::detail::error_timeout, std::move(timeout_message));
}
auto failure_status = ligo::detail::network_failure(resolve_error);
co_return ligo::detail::fail<boost::asio::ip::tcp::resolver::results_type>(failure_status);
}
co_return modulus::success(std::move(endpoints));
}
auto ligo::detail::call_handler(ligo::service_router const& router, ligo::request_context context, boost::json::value payload) -> boost::asio::awaitable<std::expected<boost::json::value, modulus::error>> {
co_return co_await router.call(std::move(context), std::move(payload));
}
auto ligo::detail::call_response_handler(ligo::service_router const& router, ligo::request_context context, boost::json::value payload) -> boost::asio::awaitable<std::expected<ligo::service_response, modulus::error>> {
co_return co_await router.call_response(std::move(context), std::move(payload));
}
Executable
+632
View File
@@ -0,0 +1,632 @@
#include <ligo/http.hpp>
#include <atomic>
#include <cassert>
#include <chrono>
#include <cstdint>
#include <expected>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <utility>
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/as_tuple.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/asio/this_coro.hpp>
#include <boost/asio/strand.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/serialize.hpp>
#include <boost/json/value.hpp>
#include <boost/system/error_code.hpp>
#include <modulus.hpp>
#include <ligo/detail/errors.hpp>
#include <ligo/detail/lifecycle.hpp>
#include <ligo/detail/state.hpp>
#include <ligo/detail/transport.hpp>
#include <ligo/config.hpp>
#include <ligo/security.hpp>
#include <ligo/service.hpp>
namespace {
struct http_server_state {
http_server_state(boost::asio::any_io_executor executor, ligo::http::endpoint endpoint, ligo::service_router router, ligo::http::server_options options);
boost::asio::any_io_executor executor;
boost::asio::strand<boost::asio::any_io_executor> strand;
std::shared_ptr<boost::asio::ip::tcp::acceptor> acceptor{};
ligo::http::endpoint endpoint{};
ligo::http::server_options options{};
std::shared_ptr<ligo::service_router const> router{};
std::shared_ptr<ligo::detail::session_registry<boost::beast::tcp_stream>> sessions{std::make_shared<ligo::detail::session_registry<boost::beast::tcp_stream>>()};
std::shared_ptr<ligo::detail::session_registry<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>> tls_sessions{std::make_shared<ligo::detail::session_registry<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>>()};
std::shared_ptr<boost::asio::ssl::context> tls_context{};
std::atomic_uint16_t selected_port{};
std::atomic<std::bool_t> start_called{false};
std::atomic<std::bool_t> stopped{false};
mutable std::mutex lifecycle_mutex{};
};
auto http_status(std::int32_t code) -> boost::beast::http::status {
switch (code) {
case ligo::detail::error_ok:
return boost::beast::http::status::ok;
case ligo::detail::error_invalid_argument:
return boost::beast::http::status::bad_request;
case ligo::detail::error_not_found:
return boost::beast::http::status::not_found;
case ligo::detail::error_unavailable:
return boost::beast::http::status::service_unavailable;
case ligo::detail::error_timeout:
return boost::beast::http::status::gateway_timeout;
case ligo::detail::error_internal:
return boost::beast::http::status::internal_server_error;
}
return boost::beast::http::status::internal_server_error;
}
auto http_body_limit_exceeded(boost::system::error_code const& error) -> std::bool_t {
return error == boost::beast::http::error::body_limit;
}
auto http_body_limit_status() -> modulus::error {
return ligo::detail::make_error(ligo::detail::error_invalid_argument, "HTTP body limit exceeded");
}
auto http_verb(ligo::http::method method) -> std::expected<boost::beast::http::verb, modulus::error> {
switch (method) {
case ligo::http::method::GET:
return modulus::success(boost::beast::http::verb::get);
case ligo::http::method::POST:
return modulus::success(boost::beast::http::verb::post);
case ligo::http::method::PUT:
return modulus::success(boost::beast::http::verb::put);
case ligo::http::method::PATCH:
return modulus::success(boost::beast::http::verb::patch);
case ligo::http::method::DELETE:
return modulus::success(boost::beast::http::verb::delete_);
}
return ligo::detail::fail<boost::beast::http::verb>(ligo::detail::error_invalid_argument, "HTTP method is invalid");
}
auto should_send_json_body(ligo::http::request const& request) -> std::bool_t {
if (request.method != ligo::http::method::GET) {
return true;
}
if (request.payload.is_null()) {
return false;
}
return !request.payload.is_object() || !request.payload.as_object().empty();
}
auto host_header(std::string const& host, std::uint16_t port) -> std::string {
auto value = std::string{};
if (!host.empty() && host.front() == '[' && host.back() == ']') {
value = host;
} else if (host.find(':') != std::string::npos) {
value = "[" + host + "]";
} else {
value = host;
}
value += ":";
value += std::to_string(port);
return value;
}
auto remote_peer(boost::beast::tcp_stream& stream) -> std::string {
auto endpoint_error = boost::system::error_code{};
auto remote_endpoint = stream.socket().remote_endpoint(endpoint_error);
return endpoint_error ? std::string{} : remote_endpoint.address().to_string();
}
auto remote_peer(boost::asio::ssl::stream<boost::asio::ip::tcp::socket>& stream) -> std::string {
auto endpoint_error = boost::system::error_code{};
auto remote_endpoint = stream.next_layer().remote_endpoint(endpoint_error);
return endpoint_error ? std::string{} : remote_endpoint.address().to_string();
}
auto close_http_session(boost::beast::tcp_stream& stream, std::chrono::milliseconds) -> boost::asio::awaitable<void> {
auto ignored = boost::beast::error_code{};
stream.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_send, ignored);
co_return;
}
auto close_http_session(boost::asio::ssl::stream<boost::asio::ip::tcp::socket>& stream, std::chrono::milliseconds timeout) -> boost::asio::awaitable<void> {
static_cast<void>(co_await ligo::detail::async_tls_shutdown(stream, timeout, "HTTP TLS shutdown timed out"));
}
template <typename STREAM>
auto write_http_failure_response(STREAM& stream, modulus::error status, std::chrono::milliseconds timeout, std::function<void(modulus::error)> const& on_error) -> boost::asio::awaitable<void> {
auto response = boost::beast::http::response<boost::beast::http::string_body>{};
response.version(11);
response.set(boost::beast::http::field::content_type, "application/json");
response.keep_alive(false);
response.result(http_status(status.code()));
response.body() = ligo::detail::serialize_response_envelope(ligo::detail::fail<boost::json::value>(status));
response.prepare_payload();
auto write_deadline = ligo::detail::start_stream_deadline(stream, timeout);
auto [write_error, written] = co_await boost::beast::http::async_write(stream, response, ligo::detail::deadline_token(write_deadline));
ligo::detail::cancel_stream_deadline(write_deadline);
static_cast<void>(written);
if (write_error) {
ligo::detail::report_error(on_error, ligo::detail::operation_failure(write_error, write_deadline, "HTTP response timed out"));
}
co_await close_http_session(stream, timeout);
}
template <typename STREAM>
auto handle_session(STREAM& stream, std::shared_ptr<ligo::service_router const> router, ligo::http::server_options options) -> boost::asio::awaitable<void> {
assert(router != nullptr);
auto buffer = boost::beast::flat_buffer{};
auto parser = boost::beast::http::request_parser<boost::beast::http::string_body>{};
parser.body_limit(options.max_body_size);
auto read_deadline = ligo::detail::start_stream_deadline(stream, options.timeout);
auto [read_error, _] = co_await boost::beast::http::async_read(stream, buffer, parser, ligo::detail::deadline_token(read_deadline));
ligo::detail::cancel_stream_deadline(read_deadline);
if (read_error) {
if (http_body_limit_exceeded(read_error)) {
co_await write_http_failure_response(stream, http_body_limit_status(), options.timeout, options.on_error);
}
co_return;
}
auto http_request = parser.release();
auto response = boost::beast::http::response<boost::beast::http::string_body>{};
response.version(http_request.version());
response.set(boost::beast::http::field::content_type, "application/json");
response.keep_alive(false);
auto payload = boost::json::value{
boost::json::object{},
};
if (!http_request.body().empty()) {
auto parse_error = boost::system::error_code{};
payload = boost::json::parse(http_request.body(), parse_error);
if (parse_error) {
auto parse_status = ligo::detail::make_error(ligo::detail::error_invalid_argument, parse_error.message());
co_await write_http_failure_response(stream, parse_status, options.timeout, options.on_error);
co_return;
}
}
auto context = ligo::request_context{
.peer = remote_peer(stream),
.method = std::string{http_request.method_string()},
.route = std::string{http_request.target()},
};
auto result = co_await ligo::detail::call_response_handler(*router, std::move(context), std::move(payload));
if (result) {
response.result(boost::beast::http::status::ok);
response.set(boost::beast::http::field::content_type, result->content_type);
response.body() = std::move(result->body);
} else {
response.result(http_status(result.error().code()));
response.body() = ligo::detail::serialize_response_envelope(ligo::detail::fail<boost::json::value>(result.error()));
}
response.prepare_payload();
auto write_deadline = ligo::detail::start_stream_deadline(stream, options.timeout);
auto [write_error, written] = co_await boost::beast::http::async_write(stream, response, ligo::detail::deadline_token(write_deadline));
ligo::detail::cancel_stream_deadline(write_deadline);
static_cast<void>(written);
if (write_error) {
ligo::detail::report_error(options.on_error, ligo::detail::operation_failure(write_error, write_deadline, "HTTP response timed out"));
}
co_await close_http_session(stream, options.timeout);
}
auto serve_session(std::shared_ptr<boost::beast::tcp_stream> stream, std::shared_ptr<ligo::service_router const> router, ligo::http::server_options options, std::shared_ptr<ligo::detail::session_registry<boost::beast::tcp_stream>> registry) -> boost::asio::awaitable<void> {
auto guard = ligo::detail::session_guard<boost::beast::tcp_stream>{
.registry = std::move(registry),
.stream = stream,
};
co_await handle_session(*stream, std::move(router), options);
guard.release();
}
auto serve_tls_session(std::shared_ptr<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>> stream, std::shared_ptr<ligo::service_router const> router, ligo::http::server_options options, std::shared_ptr<ligo::detail::session_registry<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>> registry) -> boost::asio::awaitable<void> {
auto guard = ligo::detail::session_guard<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>{
.registry = std::move(registry),
.stream = stream,
};
auto handshake = co_await ligo::detail::async_tls_handshake(*stream, boost::asio::ssl::stream_base::server, options.timeout, "HTTP TLS handshake timed out");
if (!handshake) {
ligo::detail::report_error(options.on_error, handshake);
guard.release();
co_return;
}
co_await handle_session(*stream, std::move(router), options);
guard.release();
}
auto accept_loop(std::shared_ptr<http_server_state> state) -> boost::asio::awaitable<void> {
for (;;) {
auto session_strand = boost::asio::make_strand(state->executor);
auto stream = std::make_shared<boost::beast::tcp_stream>(session_strand);
auto [error] = co_await state->acceptor->async_accept(stream->socket(), boost::asio::as_tuple(boost::asio::use_awaitable));
if (error == boost::asio::error::operation_aborted || error == boost::asio::error::bad_descriptor || (error && state->stopped.load())) {
co_return;
}
if (!error) {
state->sessions->add(stream);
auto session_executor = stream->get_executor();
boost::asio::co_spawn(session_executor, serve_session(std::move(stream), state->router, state->options, state->sessions), boost::asio::detached);
} else {
ligo::detail::report_error(state->options.on_error, ligo::detail::network_failure(error));
}
}
}
auto accept_tls_loop(std::shared_ptr<http_server_state> state) -> boost::asio::awaitable<void> {
for (;;) {
auto session_strand = boost::asio::make_strand(state->executor);
auto socket = std::make_shared<boost::asio::ip::tcp::socket>(session_strand);
auto [error] = co_await state->acceptor->async_accept(*socket, boost::asio::as_tuple(boost::asio::use_awaitable));
if (error == boost::asio::error::operation_aborted || error == boost::asio::error::bad_descriptor || (error && state->stopped.load())) {
co_return;
}
if (!error) {
auto stream = std::make_shared<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>(std::move(*socket), *state->tls_context);
state->tls_sessions->add(stream);
auto session_executor = stream->get_executor();
boost::asio::co_spawn(session_executor, serve_tls_session(std::move(stream), state->router, state->options, state->tls_sessions), boost::asio::detached);
} else {
ligo::detail::report_error(state->options.on_error, ligo::detail::network_failure(error));
}
}
}
auto http_state(ligo::detail::state_handle const& state) -> std::shared_ptr<http_server_state> {
return state.as<http_server_state>();
}
auto uninitialized_http_server() -> std::expected<void, modulus::error> {
return ligo::detail::fail_void(ligo::detail::unavailable("HTTP server is not initialized"));
}
auto start_http_server(std::shared_ptr<http_server_state> const& state) -> std::expected<void, modulus::error> {
assert(!state || state->acceptor != nullptr);
assert(!state || state->router != nullptr);
if (!state) {
return uninitialized_http_server();
}
auto lock = std::scoped_lock{state->lifecycle_mutex};
auto valid_timeout = ligo::detail::validate_positive_timeout(state->options.timeout, "HTTP server timeout");
if (!valid_timeout) {
return valid_timeout;
}
if (state->options.security.mode == ligo::security_mode::DTLS) {
return ligo::detail::fail_void(ligo::unsupported_security("HTTP server", state->options.security.mode));
}
if (state->start_called.exchange(true)) {
return ligo::detail::fail_void(ligo::detail::unavailable("HTTP server has already been started"));
}
if (state->options.security.mode == ligo::security_mode::TLS) {
auto valid_config = ligo::validate_tls_server_config("HTTP", state->options.security);
if (!valid_config) {
state->start_called.store(false);
return valid_config;
}
auto context = ligo::make_tls_server_context(state->options.security);
if (!context) {
state->start_called.store(false);
return ligo::detail::fail_void(context.error());
}
auto valid_context = ligo::validate_tls_server_context(*context);
if (!valid_context) {
state->start_called.store(false);
return valid_context;
}
state->tls_context = std::move(*context);
}
auto selected = ligo::detail::start_tcp_acceptor(*state->acceptor, state->endpoint.host, state->endpoint.port);
if (!selected) {
state->start_called.store(false);
return ligo::detail::fail_void(selected.error());
}
state->selected_port.store(*selected);
assert(state->selected_port.load() == *selected);
state->stopped.store(false);
modulus::log_debug("ligo HTTP server started on port " + std::to_string(*selected));
if (state->options.security.mode == ligo::security_mode::TLS) {
boost::asio::co_spawn(state->strand, accept_tls_loop(state), boost::asio::detached);
} else {
boost::asio::co_spawn(state->strand, accept_loop(state), boost::asio::detached);
}
return {};
}
auto stop_http_server(std::shared_ptr<http_server_state> state) -> void {
assert(state != nullptr);
auto lock = std::scoped_lock{state->lifecycle_mutex};
if (state->stopped.exchange(true)) {
return;
}
modulus::log_debug("ligo HTTP server stopping");
state->sessions->cancel_all();
state->tls_sessions->cancel_all();
auto server_executor = state->strand;
boost::asio::post(server_executor, [state = std::move(state)] {
auto ignored = boost::system::error_code{};
state->acceptor->close(ignored);
});
}
auto stop_http_server_now(std::shared_ptr<http_server_state> const& state) -> void {
auto lock = std::scoped_lock{state->lifecycle_mutex};
if (state->stopped.exchange(true)) {
return;
}
state->sessions->cancel_all();
state->tls_sessions->cancel_all();
auto ignored = boost::system::error_code{};
state->acceptor->close(ignored);
}
auto async_stop_http_server(std::shared_ptr<http_server_state> state, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
if (!state) {
co_return uninitialized_http_server();
}
auto valid_timeout = ligo::detail::validate_positive_timeout(timeout, "shutdown timeout");
if (!valid_timeout) {
co_return valid_timeout;
}
auto should_stop = false;
{
auto lock = std::scoped_lock{state->lifecycle_mutex};
should_stop = !state->stopped.exchange(true);
if (should_stop) {
state->sessions->cancel_all();
state->tls_sessions->cancel_all();
}
}
if (should_stop) {
co_await boost::asio::post(state->strand, boost::asio::use_awaitable);
auto ignored = boost::system::error_code{};
state->acceptor->close(ignored);
}
auto drained = co_await state->sessions->wait_empty(timeout);
if (!drained) {
co_return drained;
}
co_return co_await state->tls_sessions->wait_empty(timeout);
}
auto selected_http_port(std::shared_ptr<http_server_state> const& state) -> std::uint16_t {
if (!state) {
return 0;
}
return state->selected_port.load();
}
auto make_http_request_message(ligo::http::endpoint const& endpoint, ligo::http::request const& request, boost::beast::http::verb verb) -> boost::beast::http::request<boost::beast::http::string_body> {
auto http_request = boost::beast::http::request<boost::beast::http::string_body>{
verb,
request.target,
11,
};
http_request.set(boost::beast::http::field::host, host_header(endpoint.host, endpoint.port));
if (should_send_json_body(request)) {
http_request.set(boost::beast::http::field::content_type, "application/json");
http_request.body() = boost::json::serialize(request.payload);
}
http_request.prepare_payload();
return http_request;
}
template <typename STREAM>
auto read_http_response(STREAM& stream, std::chrono::milliseconds timeout, std::uint64_t max_body_size) -> boost::asio::awaitable<std::expected<boost::json::value, modulus::error>> {
auto buffer = boost::beast::flat_buffer{};
auto parser = boost::beast::http::response_parser<boost::beast::http::string_body>{};
parser.body_limit(max_body_size);
auto read_deadline = ligo::detail::start_stream_deadline(stream, timeout);
auto [read_error, read] = co_await boost::beast::http::async_read(stream, buffer, parser, ligo::detail::deadline_token(read_deadline));
ligo::detail::cancel_stream_deadline(read_deadline);
static_cast<void>(read);
if (read_error) {
if (http_body_limit_exceeded(read_error)) {
auto failure_status = http_body_limit_status();
co_return ligo::detail::fail<boost::json::value>(failure_status);
}
auto failure_status = ligo::detail::operation_failure(read_error, read_deadline, "HTTP request timed out");
co_return ligo::detail::fail<boost::json::value>(failure_status);
}
auto http_response = parser.release();
auto parse_error = boost::system::error_code{};
auto body = boost::json::parse(http_response.body(), parse_error);
if (parse_error) {
co_return ligo::detail::fail<boost::json::value>(ligo::detail::error_invalid_argument, parse_error.message());
}
co_return ligo::detail::unwrap_response_envelope(std::move(body), "HTTP");
}
auto call_https_endpoint(ligo::http::endpoint endpoint, boost::beast::http::request<boost::beast::http::string_body> http_request, ligo::http::client_options options) -> boost::asio::awaitable<std::expected<boost::json::value, modulus::error>> {
auto context = ligo::make_tls_client_context(options.security);
if (!context) {
co_return ligo::detail::fail<boost::json::value>(context.error());
}
auto executor = co_await boost::asio::this_coro::executor;
auto endpoints = co_await ligo::detail::resolve_tcp_endpoints(executor, endpoint.host, endpoint.port, options.timeout, "HTTP request timed out");
if (!endpoints) {
co_return ligo::detail::fail<boost::json::value>(endpoints.error());
}
auto tcp_stream = boost::beast::tcp_stream{executor};
tcp_stream.expires_after(options.timeout);
auto [connect_error, _] = co_await tcp_stream.async_connect(*endpoints, boost::asio::as_tuple(boost::asio::use_awaitable));
if (connect_error) {
auto failure_status = ligo::detail::operation_failure(connect_error, "HTTP request timed out");
co_return ligo::detail::fail<boost::json::value>(failure_status);
}
auto stream = boost::asio::ssl::stream<boost::asio::ip::tcp::socket>{
tcp_stream.release_socket(),
**context,
};
auto verification = ligo::configure_tls_client_stream(stream, endpoint.host, options.security);
if (!verification) {
co_return ligo::detail::fail<boost::json::value>(verification.error());
}
auto handshake = co_await ligo::detail::async_tls_handshake(stream, boost::asio::ssl::stream_base::client, options.timeout, "HTTP TLS handshake timed out");
if (!handshake) {
co_return ligo::detail::fail<boost::json::value>(handshake.error());
}
auto write_deadline = ligo::detail::start_stream_deadline(stream, options.timeout);
auto [write_error, written] = co_await boost::beast::http::async_write(stream, http_request, ligo::detail::deadline_token(write_deadline));
ligo::detail::cancel_stream_deadline(write_deadline);
static_cast<void>(written);
if (write_error) {
auto failure_status = ligo::detail::operation_failure(write_error, write_deadline, "HTTP request timed out");
co_return ligo::detail::fail<boost::json::value>(failure_status);
}
auto response = co_await read_http_response(stream, options.timeout, options.max_body_size);
static_cast<void>(co_await ligo::detail::async_tls_shutdown(stream, options.timeout, "HTTP TLS shutdown timed out"));
co_return response;
}
auto call_http_endpoint(ligo::http::endpoint endpoint, std::shared_ptr<ligo::http::request const> request, ligo::http::client_options options) -> boost::asio::awaitable<std::expected<boost::json::value, modulus::error>> {
assert(request != nullptr);
modulus::log_debug("ligo HTTP client call to " + endpoint.host + ":" + std::to_string(endpoint.port));
auto valid_timeout = ligo::detail::validate_positive_timeout(options.timeout, "HTTP timeout");
if (!valid_timeout) {
co_return ligo::detail::fail<boost::json::value>(valid_timeout.error());
}
if (endpoint.port == 0) {
co_return ligo::detail::fail<boost::json::value>(ligo::detail::error_invalid_argument, "HTTP port must be non-zero");
}
if (request->target.empty()) {
co_return ligo::detail::fail<boost::json::value>(ligo::detail::error_invalid_argument, "HTTP target must not be empty");
}
if (request->target.front() != '/') {
co_return ligo::detail::fail<boost::json::value>(ligo::detail::error_invalid_argument, "HTTP target must start with /");
}
auto verb = http_verb(request->method);
if (!verb) {
co_return ligo::detail::fail<boost::json::value>(verb.error());
}
if (options.security.mode == ligo::security_mode::DTLS) {
auto status = ligo::unsupported_security("HTTP client", options.security.mode);
co_return ligo::detail::fail<boost::json::value>(status);
}
auto http_request = make_http_request_message(endpoint, *request, *verb);
if (options.security.mode == ligo::security_mode::TLS) {
co_return co_await call_https_endpoint(std::move(endpoint), std::move(http_request), options);
}
auto executor = co_await boost::asio::this_coro::executor;
auto stream = boost::beast::tcp_stream{executor};
auto endpoints = co_await ligo::detail::resolve_tcp_endpoints(executor, endpoint.host, endpoint.port, options.timeout, "HTTP request timed out");
if (!endpoints) {
co_return ligo::detail::fail<boost::json::value>(endpoints.error());
}
stream.expires_after(options.timeout);
auto [connect_error, _] = co_await stream.async_connect(*endpoints, boost::asio::as_tuple(boost::asio::use_awaitable));
if (connect_error) {
auto failure_status = ligo::detail::operation_failure(connect_error, "HTTP request timed out");
co_return ligo::detail::fail<boost::json::value>(failure_status);
}
stream.expires_after(options.timeout);
auto [write_error, written] = co_await boost::beast::http::async_write(stream, http_request, boost::asio::as_tuple(boost::asio::use_awaitable));
static_cast<void>(written);
if (write_error) {
auto failure_status = ligo::detail::operation_failure(write_error, "HTTP request timed out");
co_return ligo::detail::fail<boost::json::value>(failure_status);
}
stream.expires_after(options.timeout);
co_return co_await read_http_response(stream, options.timeout, options.max_body_size);
}
http_server_state::http_server_state(boost::asio::any_io_executor executor, ligo::http::endpoint endpoint, ligo::service_router router, ligo::http::server_options options) : executor{std::move(executor)}, strand{boost::asio::make_strand(this->executor)}, acceptor{std::make_shared<boost::asio::ip::tcp::acceptor>(strand)}, endpoint{std::move(endpoint)}, options{options}, router{std::make_shared<ligo::service_router>(std::move(router))} {}
}
ligo::http::client::client(ligo::http::endpoint endpoint, ligo::http::client_options options) : endpoint_{std::move(endpoint)}, options_{options} {}
auto ligo::http::client::call(ligo::http::request const& request) const -> boost::asio::awaitable<std::expected<boost::json::value, modulus::error>> {
return call_http_endpoint(endpoint_, std::make_shared<ligo::http::request const>(request), options_);
}
ligo::http::server::server(boost::asio::io_context& io, ligo::http::endpoint endpoint, ligo::service_router router, ligo::http::server_options options) : ligo::http::server{io.get_executor(), std::move(endpoint), std::move(router), options} {}
ligo::http::server::server(boost::asio::any_io_executor executor, ligo::http::endpoint endpoint, ligo::service_router router, ligo::http::server_options options) : state_{ligo::detail::state_handle{std::make_shared<http_server_state>(std::move(executor), std::move(endpoint), std::move(router), options)}} {}
ligo::http::server::~server() {
if (auto state = http_state(state_)) {
stop_http_server_now(state);
}
}
auto ligo::http::server::operator=(ligo::http::server&& other) noexcept -> ligo::http::server& {
if (this != &other) {
if (auto state = http_state(state_)) {
stop_http_server_now(state);
}
state_ = std::move(other.state_);
}
return *this;
}
auto ligo::http::server::start() -> std::expected<void, modulus::error> {
return start_http_server(http_state(state_));
}
auto ligo::http::server::stop() -> void {
if (auto state = http_state(state_)) {
stop_http_server(std::move(state));
}
}
auto ligo::http::server::async_stop(std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
return async_stop_http_server(http_state(state_), timeout);
}
auto ligo::http::server::selected_port() const -> std::uint16_t {
return selected_http_port(http_state(state_));
}
auto ligo::http::client_endpoint_from_config(ligo::endpoint_config const& config) -> std::expected<ligo::http::endpoint, modulus::error> {
auto parsed = ligo::detail::parse_client_endpoint_text(config.connect);
if (!parsed) {
return ligo::detail::fail<ligo::http::endpoint>(parsed.error());
}
return modulus::success(ligo::http::endpoint{
.host = std::move(parsed->host),
.port = parsed->port
});
}
auto ligo::http::server_endpoint_from_config(ligo::endpoint_config const& config) -> std::expected<ligo::http::endpoint, modulus::error> {
auto parsed = ligo::detail::parse_server_endpoint_text(config.bind);
if (!parsed) {
return ligo::detail::fail<ligo::http::endpoint>(parsed.error());
}
return modulus::success(ligo::http::endpoint{
.host = std::move(parsed->host),
.port = parsed->port,
});
}
auto ligo::http::client_options_from_config(ligo::endpoint_config const& config) -> ligo::http::client_options {
return {
.timeout = config.timeout,
.max_body_size = config.http.max_body_size,
.security = config.security,
};
}
auto ligo::http::server_options_from_config(ligo::endpoint_config const& config) -> ligo::http::server_options {
return {
.timeout = config.timeout,
.max_body_size = config.http.max_body_size,
.on_error = {},
.security = config.security,
};
}
+510
View File
@@ -0,0 +1,510 @@
#include <ligo/net.hpp>
#include <atomic>
#include <cassert>
#include <chrono>
#include <cstddef>
#include <expected>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/as_tuple.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/bind_executor.hpp>
#include <boost/asio/buffer.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/strand.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/asio/write.hpp>
#include <boost/system/error_code.hpp>
#include <modulus.hpp>
#include <ligo/config.hpp>
#include <ligo/detail/errors.hpp>
#include <ligo/detail/state.hpp>
#include <ligo/detail/transport.hpp>
#include <ligo/security.hpp>
#include <ligo/tcp.hpp>
#include <cstdint>
class ligo::net::byte_stream_state {
public:
byte_stream_state() = default;
byte_stream_state(ligo::net::byte_stream_state const&) = delete;
byte_stream_state(ligo::net::byte_stream_state&&) noexcept = delete;
virtual ~byte_stream_state() = default;
auto operator=(ligo::net::byte_stream_state const&) -> ligo::net::byte_stream_state& = delete;
auto operator=(ligo::net::byte_stream_state&&) noexcept -> ligo::net::byte_stream_state& = delete;
virtual auto async_read_some(boost::asio::mutable_buffer buffer, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<std::size_t, modulus::error>> = 0;
virtual auto async_write_some(boost::asio::const_buffer buffer, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<std::size_t, modulus::error>> = 0;
virtual auto async_write_all(boost::asio::const_buffer buffer, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>> = 0;
virtual auto async_shutdown(std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>> = 0;
virtual auto close(boost::system::error_code& error) -> void = 0;
virtual auto is_open() const -> std::bool_t = 0;
};
namespace {
struct net_listener_state {
explicit net_listener_state(boost::asio::any_io_executor executor);
boost::asio::strand<boost::asio::any_io_executor> strand;
boost::asio::ip::tcp::acceptor acceptor;
std::atomic_uint16_t selected_port{};
std::atomic<std::bool_t> closed{true};
};
class tcp_byte_stream_state final : public ligo::net::byte_stream_state {
public:
explicit tcp_byte_stream_state(boost::asio::ip::tcp::socket socket);
auto async_read_some(boost::asio::mutable_buffer buffer, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<std::size_t, modulus::error>> override;
auto async_write_some(boost::asio::const_buffer buffer, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<std::size_t, modulus::error>> override;
auto async_write_all(boost::asio::const_buffer buffer, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>> override;
auto async_shutdown(std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>> override;
auto close(boost::system::error_code& error) -> void override;
auto is_open() const -> std::bool_t override;
private:
std::shared_ptr<boost::asio::ip::tcp::socket> socket_{};
};
class tls_byte_stream_state final : public ligo::net::byte_stream_state {
public:
explicit tls_byte_stream_state(ligo::tcp::tls_socket::released_stream released);
auto async_read_some(boost::asio::mutable_buffer buffer, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<std::size_t, modulus::error>> override;
auto async_write_some(boost::asio::const_buffer buffer, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<std::size_t, modulus::error>> override;
auto async_write_all(boost::asio::const_buffer buffer, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>> override;
auto async_shutdown(std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>> override;
auto close(boost::system::error_code& error) -> void override;
auto is_open() const -> std::bool_t override;
private:
std::shared_ptr<boost::asio::ssl::context> context_{};
std::unique_ptr<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>> stream_{};
};
auto listener_state(ligo::detail::state_handle const& state) -> std::shared_ptr<net_listener_state> {
return state.as<net_listener_state>();
}
auto close_listener(std::shared_ptr<net_listener_state> const& state) -> void {
if (!state || state->closed.exchange(true)) {
return;
}
boost::asio::post(state->strand, [state] {
auto ignored = boost::system::error_code{};
state->acceptor.cancel(ignored);
state->acceptor.close(ignored);
});
}
auto validate_tls_accept_security(ligo::security_config const& security) -> std::expected<void, modulus::error> {
if (security.mode != ligo::security_mode::TLS) {
return ligo::detail::fail_void(ligo::unsupported_security("TCP TLS accept", security.mode));
}
return {};
}
auto validate_stream_timeout(std::chrono::milliseconds timeout) -> std::expected<void, modulus::error> {
return ligo::detail::validate_positive_timeout(timeout, "byte stream timeout");
}
template <typename STREAM>
auto read_some_from(STREAM& stream, boost::asio::mutable_buffer buffer, std::chrono::milliseconds timeout, std::string timeout_message) -> boost::asio::awaitable<std::expected<std::size_t, modulus::error>> {
auto valid = validate_stream_timeout(timeout);
if (!valid) {
co_return ligo::detail::fail<std::size_t>(valid.error());
}
auto deadline = ligo::detail::start_stream_deadline(stream, timeout);
auto [error, bytes] = co_await stream.async_read_some(buffer, ligo::detail::deadline_token(deadline));
ligo::detail::cancel_stream_deadline(deadline);
if (error) {
auto failure_status = ligo::detail::operation_failure(error, deadline, std::move(timeout_message));
co_return ligo::detail::fail<std::size_t>(failure_status);
}
co_return modulus::success(bytes);
}
template <typename STREAM>
auto write_some_to(STREAM& stream, boost::asio::const_buffer buffer, std::chrono::milliseconds timeout, std::string timeout_message) -> boost::asio::awaitable<std::expected<std::size_t, modulus::error>> {
auto valid = validate_stream_timeout(timeout);
if (!valid) {
co_return ligo::detail::fail<std::size_t>(valid.error());
}
auto deadline = ligo::detail::start_stream_deadline(stream, timeout);
auto [error, bytes] = co_await stream.async_write_some(buffer, ligo::detail::deadline_token(deadline));
ligo::detail::cancel_stream_deadline(deadline);
if (error) {
auto failure_status = ligo::detail::operation_failure(error, deadline, std::move(timeout_message));
co_return ligo::detail::fail<std::size_t>(failure_status);
}
co_return modulus::success(bytes);
}
template <typename STREAM>
auto write_all_to(STREAM& stream, boost::asio::const_buffer buffer, std::chrono::milliseconds timeout, std::string timeout_message) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto valid = validate_stream_timeout(timeout);
if (!valid) {
co_return valid;
}
auto deadline = ligo::detail::start_stream_deadline(stream, timeout);
auto [error, bytes] = co_await boost::asio::async_write(stream, buffer, ligo::detail::deadline_token(deadline));
ligo::detail::cancel_stream_deadline(deadline);
static_cast<void>(bytes);
if (error) {
co_return ligo::detail::fail_void(ligo::detail::operation_failure(error, deadline, std::move(timeout_message)));
}
co_return std::expected<void, modulus::error>{};
}
auto accept_socket(std::shared_ptr<net_listener_state> state, std::optional<std::chrono::milliseconds> timeout = std::nullopt) -> boost::asio::awaitable<std::expected<ligo::tcp::socket_handle, modulus::error>> {
if (!state) {
co_return ligo::detail::fail<ligo::tcp::socket_handle>(ligo::detail::error_unavailable, "net listener is not initialized");
}
if (timeout.has_value()) {
auto valid_timeout = ligo::detail::validate_positive_timeout(*timeout, "TCP accept timeout");
if (!valid_timeout) {
co_return ligo::detail::fail<ligo::tcp::socket_handle>(valid_timeout.error());
}
}
co_await boost::asio::post(state->strand, boost::asio::use_awaitable);
if (state->closed.load(std::memory_order_acquire) || !state->acceptor.is_open()) {
co_return ligo::detail::fail<ligo::tcp::socket_handle>(ligo::detail::error_unavailable, "net listener is closed");
}
auto socket = boost::asio::ip::tcp::socket{state->strand};
auto timed_out = std::make_shared<std::atomic<std::bool_t>>(false);
auto timer = std::shared_ptr<boost::asio::steady_timer>{};
if (timeout.has_value()) {
timer = std::make_shared<boost::asio::steady_timer>(state->strand);
timer->expires_after(*timeout);
timer->async_wait([state, timer, timed_out](boost::system::error_code const& error) {
if (!error) {
timed_out->store(true, std::memory_order_release);
auto ignored = boost::system::error_code{};
state->acceptor.cancel(ignored);
}
});
}
auto [error] = co_await state->acceptor.async_accept(socket, boost::asio::bind_executor(state->strand, boost::asio::as_tuple(boost::asio::use_awaitable)));
if (timer) {
timer->cancel();
}
if (error) {
if (error == boost::asio::error::operation_aborted && timed_out->load(std::memory_order_acquire)) {
co_return ligo::detail::fail<ligo::tcp::socket_handle>(ligo::detail::error_timeout, "TCP accept timed out");
}
auto failure_status = ligo::detail::network_failure(error);
co_return ligo::detail::fail<ligo::tcp::socket_handle>(failure_status);
}
co_return modulus::success(ligo::tcp::socket_handle{std::move(socket)});
}
auto make_server_tls_context(ligo::security_config const& security) -> std::expected<std::shared_ptr<boost::asio::ssl::context>, modulus::error> {
auto valid_security = validate_tls_accept_security(security);
if (!valid_security) {
return ligo::detail::fail<std::shared_ptr<boost::asio::ssl::context>>(valid_security.error());
}
auto valid_config = ligo::validate_tls_server_config("TCP", security);
if (!valid_config) {
return ligo::detail::fail<std::shared_ptr<boost::asio::ssl::context>>(valid_config.error());
}
auto context = ligo::make_tls_server_context(security);
if (!context) {
return ligo::detail::fail<std::shared_ptr<boost::asio::ssl::context>>(context.error());
}
auto valid_context = ligo::validate_tls_server_context(*context);
if (!valid_context) {
return ligo::detail::fail<std::shared_ptr<boost::asio::ssl::context>>(valid_context.error());
}
return context;
}
net_listener_state::net_listener_state(boost::asio::any_io_executor executor) : strand{boost::asio::make_strand(std::move(executor))}, acceptor{strand} {}
tcp_byte_stream_state::tcp_byte_stream_state(boost::asio::ip::tcp::socket socket) : socket_{std::make_shared<boost::asio::ip::tcp::socket>(std::move(socket))} {}
auto tcp_byte_stream_state::async_read_some(boost::asio::mutable_buffer buffer, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<std::size_t, modulus::error>> {
if (!is_open()) {
co_return ligo::detail::fail<std::size_t>(ligo::detail::error_unavailable, "byte stream is closed");
}
co_return co_await read_some_from(*socket_, buffer, timeout, "byte stream read timed out");
}
auto tcp_byte_stream_state::async_write_some(boost::asio::const_buffer buffer, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<std::size_t, modulus::error>> {
if (!is_open()) {
co_return ligo::detail::fail<std::size_t>(ligo::detail::error_unavailable, "byte stream is closed");
}
co_return co_await write_some_to(*socket_, buffer, timeout, "byte stream write timed out");
}
auto tcp_byte_stream_state::async_write_all(boost::asio::const_buffer buffer, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
if (!is_open()) {
co_return ligo::detail::fail_void(ligo::detail::unavailable("byte stream is closed"));
}
co_return co_await write_all_to(*socket_, buffer, timeout, "byte stream write timed out");
}
auto tcp_byte_stream_state::async_shutdown(std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
auto valid = validate_stream_timeout(timeout);
if (!valid) {
co_return valid;
}
if (!socket_) {
co_return ligo::detail::fail_void(ligo::detail::unavailable("byte stream is closed"));
}
auto error = boost::system::error_code{};
socket_->shutdown(boost::asio::ip::tcp::socket::shutdown_both, error);
if (error && error != boost::asio::error::not_connected) {
co_return ligo::detail::fail_void(ligo::detail::network_failure(error));
}
socket_->close(error);
if (error) {
co_return ligo::detail::fail_void(ligo::detail::network_failure(error));
}
co_return std::expected<void, modulus::error>{};
}
auto tcp_byte_stream_state::close(boost::system::error_code& error) -> void {
if (socket_) {
socket_->close(error);
}
}
auto tcp_byte_stream_state::is_open() const -> std::bool_t {
return socket_ && socket_->is_open();
}
tls_byte_stream_state::tls_byte_stream_state(ligo::tcp::tls_socket::released_stream released) : context_{std::move(released.context)}, stream_{std::make_unique<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>(std::move(released.stream))} {}
auto tls_byte_stream_state::async_read_some(boost::asio::mutable_buffer buffer, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<std::size_t, modulus::error>> {
if (!is_open()) {
co_return ligo::detail::fail<std::size_t>(ligo::detail::error_unavailable, "byte stream is closed");
}
co_return co_await read_some_from(*stream_, buffer, timeout, "TLS byte stream read timed out");
}
auto tls_byte_stream_state::async_write_some(boost::asio::const_buffer buffer, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<std::size_t, modulus::error>> {
if (!is_open()) {
co_return ligo::detail::fail<std::size_t>(ligo::detail::error_unavailable, "byte stream is closed");
}
co_return co_await write_some_to(*stream_, buffer, timeout, "TLS byte stream write timed out");
}
auto tls_byte_stream_state::async_write_all(boost::asio::const_buffer buffer, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
if (!is_open()) {
co_return ligo::detail::fail_void(ligo::detail::unavailable("byte stream is closed"));
}
co_return co_await write_all_to(*stream_, buffer, timeout, "TLS byte stream write timed out");
}
auto tls_byte_stream_state::async_shutdown(std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
if (!stream_) {
co_return ligo::detail::fail_void(ligo::detail::unavailable("byte stream is closed"));
}
auto status = co_await ligo::detail::async_tls_shutdown(*stream_, timeout, "TLS byte stream shutdown timed out");
auto ignored = boost::system::error_code{};
stream_->lowest_layer().close(ignored);
co_return status;
}
auto tls_byte_stream_state::close(boost::system::error_code& error) -> void {
if (stream_) {
stream_->lowest_layer().close(error);
}
}
auto tls_byte_stream_state::is_open() const -> std::bool_t {
return stream_ && stream_->lowest_layer().is_open();
}
}
ligo::net::byte_stream::~byte_stream() {
auto error = boost::system::error_code{};
close(error);
}
auto ligo::net::byte_stream::operator=(ligo::net::byte_stream&& other) noexcept -> ligo::net::byte_stream& {
if (this != &other) {
auto error = boost::system::error_code{};
close(error);
state_ = std::move(other.state_);
}
return *this;
}
auto ligo::net::byte_stream::from_socket(ligo::tcp::socket_handle socket) -> std::expected<ligo::net::byte_stream, modulus::error> {
modulus::log_debug("ligo byte stream adopting TCP socket");
auto released = socket.release_socket();
if (!released) {
return ligo::detail::fail<ligo::net::byte_stream>(released.error());
}
auto state = std::make_shared<tcp_byte_stream_state>(std::move(*released));
return modulus::success(ligo::net::byte_stream{std::move(state)});
}
auto ligo::net::byte_stream::from_tls_socket(ligo::tcp::tls_socket socket) -> std::expected<ligo::net::byte_stream, modulus::error> {
modulus::log_debug("ligo byte stream adopting TLS socket");
auto released = socket.release_stream();
if (!released) {
return ligo::detail::fail<ligo::net::byte_stream>(released.error());
}
auto state = std::make_shared<tls_byte_stream_state>(std::move(*released));
return modulus::success(ligo::net::byte_stream{std::move(state)});
}
auto ligo::net::byte_stream::async_read_some(boost::asio::mutable_buffer buffer, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<std::size_t, modulus::error>> {
if (!state_) {
co_return ligo::detail::fail<std::size_t>(ligo::detail::error_unavailable, "byte stream is not initialized");
}
co_return co_await state_->async_read_some(buffer, timeout);
}
auto ligo::net::byte_stream::async_write_some(boost::asio::const_buffer buffer, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<std::size_t, modulus::error>> {
if (!state_) {
co_return ligo::detail::fail<std::size_t>(ligo::detail::error_unavailable, "byte stream is not initialized");
}
co_return co_await state_->async_write_some(buffer, timeout);
}
auto ligo::net::byte_stream::async_write_all(boost::asio::const_buffer buffer, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
if (!state_) {
co_return ligo::detail::fail_void(ligo::detail::unavailable("byte stream is not initialized"));
}
co_return co_await state_->async_write_all(buffer, timeout);
}
auto ligo::net::byte_stream::async_shutdown(std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
if (!state_) {
co_return ligo::detail::fail_void(ligo::detail::unavailable("byte stream is not initialized"));
}
co_return co_await state_->async_shutdown(timeout);
}
auto ligo::net::byte_stream::close(boost::system::error_code& error) -> void {
if (state_) {
state_->close(error);
}
}
auto ligo::net::byte_stream::is_open() const -> std::bool_t {
if (state_) {
return state_->is_open();
}
return false;
}
ligo::net::byte_stream::byte_stream(std::shared_ptr<ligo::net::byte_stream_state> state) : state_{std::move(state)} {}
ligo::net::listener::~listener() {
close();
}
auto ligo::net::listener::operator=(ligo::net::listener&& other) noexcept -> ligo::net::listener& {
if (this != &other) {
close();
state_ = std::move(other.state_);
}
return *this;
}
auto ligo::net::listener::bind(boost::asio::any_io_executor executor, ligo::tcp::endpoint endpoint) -> std::expected<ligo::net::listener, modulus::error> {
auto state = std::make_shared<net_listener_state>(std::move(executor));
auto selected = ligo::detail::start_tcp_acceptor(state->acceptor, endpoint.host, endpoint.port);
if (!selected) {
return ligo::detail::fail<ligo::net::listener>(selected.error());
}
state->selected_port.store(*selected);
assert(state->selected_port.load() == *selected);
state->closed.store(false);
modulus::log_debug("ligo net listener bound on port " + std::to_string(*selected));
return modulus::success(ligo::net::listener{
ligo::detail::state_handle{std::move(state)},
});
}
auto ligo::net::listener::selected_port() const -> std::uint16_t {
if (auto state = listener_state(state_)) {
return state->selected_port.load();
}
return 0;
}
auto ligo::net::listener::async_accept() -> boost::asio::awaitable<std::expected<ligo::tcp::socket_handle, modulus::error>> {
co_return co_await accept_socket(listener_state(state_));
}
auto ligo::net::listener::async_accept_tls_configured(ligo::security_config security, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<ligo::tcp::tls_socket, modulus::error>> {
auto context = make_server_tls_context(security);
if (!context) {
co_return ligo::detail::fail<ligo::tcp::tls_socket>(context.error());
}
auto accepted = co_await accept_socket(listener_state(state_), timeout);
if (!accepted) {
co_return ligo::detail::fail<ligo::tcp::tls_socket>(accepted.error());
}
auto socket = accepted->release_socket();
if (!socket) {
co_return ligo::detail::fail<ligo::tcp::tls_socket>(socket.error());
}
co_return std::expected<ligo::tcp::tls_socket, modulus::error>{
std::in_place,
ligo::tcp::tls_socket{
std::move(*context),
std::move(*socket),
},
};
}
auto ligo::net::listener::async_accept_tls_handshaken(ligo::security_config security, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<ligo::tcp::tls_socket, modulus::error>> {
auto accepted = co_await async_accept_tls_configured(std::move(security), timeout);
if (!accepted) {
co_return ligo::detail::fail<ligo::tcp::tls_socket>(accepted.error());
}
auto stream = accepted->stream();
if (!stream) {
co_return ligo::detail::fail<ligo::tcp::tls_socket>(stream.error());
}
auto handshake = co_await ligo::detail::async_tls_handshake(stream->get(), boost::asio::ssl::stream_base::server, timeout, "TCP TLS handshake timed out");
if (!handshake) {
co_return ligo::detail::fail<ligo::tcp::tls_socket>(handshake.error());
}
co_return std::expected<ligo::tcp::tls_socket, modulus::error>{
std::in_place,
std::move(*accepted),
};
}
auto ligo::net::listener::close() -> void {
close_listener(listener_state(state_));
}
ligo::net::listener::listener(ligo::detail::state_handle state) : state_{std::move(state)} {}
auto ligo::net::connect_tcp(boost::asio::any_io_executor executor, ligo::tcp::endpoint endpoint, ligo::tcp::client_options options) -> boost::asio::awaitable<std::expected<ligo::tcp::socket_handle, modulus::error>> {
co_return co_await ligo::tcp::connect(std::move(executor), std::move(endpoint), options);
}
auto ligo::net::connect(boost::asio::any_io_executor executor, ligo::tcp::endpoint endpoint, ligo::tcp::client_options options) -> boost::asio::awaitable<std::expected<ligo::tcp::socket_handle, modulus::error>> {
co_return co_await ligo::net::connect_tcp(std::move(executor), std::move(endpoint), options);
}
auto ligo::net::connect(boost::asio::any_io_executor executor, ligo::endpoint_config const& config) -> boost::asio::awaitable<std::expected<ligo::tcp::socket_handle, modulus::error>> {
co_return co_await ligo::tcp::connect(std::move(executor), config);
}
auto ligo::net::connect_tls_configured(boost::asio::any_io_executor executor, ligo::tcp::endpoint endpoint, ligo::tcp::client_options options, ligo::security_config security) -> boost::asio::awaitable<std::expected<ligo::tcp::tls_socket, modulus::error>> {
co_return co_await ligo::tcp::connect_tls_configured(std::move(executor), std::move(endpoint), options, std::move(security));
}
auto ligo::net::connect_tls_handshaken(boost::asio::any_io_executor executor, ligo::tcp::endpoint endpoint, ligo::tcp::client_options options, ligo::security_config security) -> boost::asio::awaitable<std::expected<ligo::tcp::tls_socket, modulus::error>> {
co_return co_await ligo::tcp::connect_tls_handshaken(std::move(executor), std::move(endpoint), options, std::move(security));
}
+238
View File
@@ -0,0 +1,238 @@
#include <ligo/security.hpp>
#include <expected>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/host_name_verification.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/system/error_code.hpp>
#include <openssl/err.h>
#include <openssl/ssl.h>
#include <modulus.hpp>
#include <ligo/detail/errors.hpp>
#include <cstdint>
namespace {
auto ssl_failure(std::int32_t code, std::string message) -> modulus::error {
auto const error = ERR_get_error();
if (error != 0) {
message += ": ";
message += ERR_error_string(error, nullptr);
}
return ligo::detail::make_error(code, std::move(message));
}
auto ssl_failure(std::string message) -> modulus::error {
return ssl_failure(ligo::detail::error_unavailable, std::move(message));
}
auto apply_verify_mode(boost::asio::ssl::context& context, ligo::verify_mode verify) -> std::expected<void, modulus::error> {
auto const mode = verify == ligo::verify_mode::PEER || verify == ligo::verify_mode::DEFAULTED ? boost::asio::ssl::verify_peer : boost::asio::ssl::verify_none;
auto error = boost::system::error_code{};
context.set_verify_mode(mode, error);
if (error) {
return ligo::detail::fail_void(ligo::detail::error_unavailable, error.message());
}
return {};
}
auto apply_server_verify_mode(boost::asio::ssl::context& context, ligo::verify_mode verify) -> std::expected<void, modulus::error> {
auto const mode = verify == ligo::verify_mode::PEER ? boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert : boost::asio::ssl::verify_none;
auto error = boost::system::error_code{};
context.set_verify_mode(mode, error);
if (error) {
return ligo::detail::fail_void(ligo::detail::error_unavailable, error.message());
}
return {};
}
auto load_verify_paths(boost::asio::ssl::context& context, ligo::security_config const& config) -> std::expected<void, modulus::error> {
if (config.verify == ligo::verify_mode::NONE) {
return {};
}
auto error = boost::system::error_code{};
if (!config.ca_file.empty()) {
context.load_verify_file(config.ca_file, error);
} else {
context.set_default_verify_paths(error);
}
if (error) {
return ligo::detail::fail_void(ligo::detail::error_unavailable, error.message());
}
return {};
}
auto load_server_identity(boost::asio::ssl::context& context, ligo::security_config const& config) -> std::expected<void, modulus::error> {
if (config.certificate_chain_file.empty() && config.private_key_file.empty()) {
return {};
}
if (config.certificate_chain_file.empty() || config.private_key_file.empty()) {
return ligo::detail::fail_void(ligo::detail::error_invalid_argument, "TLS server certificate chain and private key must both be configured");
}
auto error = boost::system::error_code{};
context.use_certificate_chain_file(config.certificate_chain_file, error);
if (error) {
return ligo::detail::fail_void(ligo::detail::error_unavailable, error.message());
}
context.use_private_key_file(config.private_key_file, boost::asio::ssl::context::pem, error);
if (error) {
return ligo::detail::fail_void(ligo::detail::error_unavailable, error.message());
}
if (SSL_CTX_check_private_key(context.native_handle()) != 1) {
return ligo::detail::fail_void(ssl_failure(ligo::detail::error_invalid_argument, "TLS server private key does not match certificate"));
}
return {};
}
auto make_context(boost::asio::ssl::context::method method, ligo::security_config const& config, std::bool_t server) -> std::expected<std::shared_ptr<boost::asio::ssl::context>, modulus::error> {
if (config.mode == ligo::security_mode::DTLS) {
return ligo::detail::fail<std::shared_ptr<boost::asio::ssl::context>>(ligo::detail::error_invalid_argument, "DTLS config cannot create a TLS context");
}
if (config.tls_context) {
if (server) {
if (config.verify != ligo::verify_mode::DEFAULTED) {
auto verify = apply_server_verify_mode(*config.tls_context, config.verify);
if (!verify) {
return ligo::detail::fail<std::shared_ptr<boost::asio::ssl::context>>(verify.error());
}
}
auto identity = load_server_identity(*config.tls_context, config);
if (!identity) {
return ligo::detail::fail<std::shared_ptr<boost::asio::ssl::context>>(identity.error());
}
} else {
if (config.verify != ligo::verify_mode::DEFAULTED) {
auto verify = apply_verify_mode(*config.tls_context, config.verify);
if (!verify) {
return ligo::detail::fail<std::shared_ptr<boost::asio::ssl::context>>(verify.error());
}
}
if (!config.ca_file.empty()) {
auto verify_paths = load_verify_paths(*config.tls_context, config);
if (!verify_paths) {
return ligo::detail::fail<std::shared_ptr<boost::asio::ssl::context>>(verify_paths.error());
}
}
}
return modulus::success(config.tls_context);
}
auto context = std::make_shared<boost::asio::ssl::context>(method);
auto error = boost::system::error_code{};
context->set_options(boost::asio::ssl::context::default_workarounds | boost::asio::ssl::context::no_compression | boost::asio::ssl::context::no_sslv2 | boost::asio::ssl::context::no_sslv3 | boost::asio::ssl::context::no_tlsv1 | boost::asio::ssl::context::no_tlsv1_1, error);
if (error) {
return ligo::detail::fail<std::shared_ptr<boost::asio::ssl::context>>(ligo::detail::error_unavailable, error.message());
}
if (server) {
auto verify = apply_server_verify_mode(*context, config.verify);
if (!verify) {
return ligo::detail::fail<std::shared_ptr<boost::asio::ssl::context>>(verify.error());
}
auto identity = load_server_identity(*context, config);
if (!identity) {
return ligo::detail::fail<std::shared_ptr<boost::asio::ssl::context>>(identity.error());
}
} else {
auto verify = apply_verify_mode(*context, config.verify);
if (!verify) {
return ligo::detail::fail<std::shared_ptr<boost::asio::ssl::context>>(verify.error());
}
auto verify_paths = load_verify_paths(*context, config);
if (!verify_paths) {
return ligo::detail::fail<std::shared_ptr<boost::asio::ssl::context>>(verify_paths.error());
}
}
return modulus::success(std::move(context));
}
}
auto ligo::make_tls_client_context(ligo::security_config const& config) -> std::expected<std::shared_ptr<boost::asio::ssl::context>, modulus::error> {
return make_context(boost::asio::ssl::context::tls_client, config, false);
}
auto ligo::make_tls_server_context(ligo::security_config const& config) -> std::expected<std::shared_ptr<boost::asio::ssl::context>, modulus::error> {
return make_context(boost::asio::ssl::context::tls_server, config, true);
}
auto ligo::apply_tls_client_verification(boost::asio::ssl::stream<boost::asio::ip::tcp::socket>& stream, std::string const& host, ligo::security_config const& config) -> std::expected<void, modulus::error> {
return ligo::configure_tls_client_stream(stream, host, config);
}
auto ligo::configure_tls_client_stream(boost::asio::ssl::stream<boost::asio::ip::tcp::socket>& stream, std::string const& host, ligo::security_config const& config) -> std::expected<void, modulus::error> {
auto const identity = config.peer_name.empty() ? host : config.peer_name;
if (!identity.empty()) {
if (SSL_set_tlsext_host_name(stream.native_handle(), identity.c_str()) != 1) {
return ligo::detail::fail_void(ssl_failure("failed to configure TLS SNI"));
}
}
if (config.verify == ligo::verify_mode::NONE) {
auto error = boost::system::error_code{};
stream.set_verify_mode(boost::asio::ssl::verify_none, error);
if (error) {
return ligo::detail::fail_void(ligo::detail::error_unavailable, error.message());
}
return {};
}
auto error = boost::system::error_code{};
stream.set_verify_mode(boost::asio::ssl::verify_peer, error);
if (error) {
return ligo::detail::fail_void(ligo::detail::error_unavailable, error.message());
}
if (!identity.empty()) {
stream.set_verify_callback(boost::asio::ssl::host_name_verification(identity), error);
if (error) {
return ligo::detail::fail_void(ligo::detail::error_unavailable, error.message());
}
}
return {};
}
auto ligo::validate_tls_server_config(std::string_view protocol, ligo::security_config const& config) -> std::expected<void, modulus::error> {
if (config.mode != ligo::security_mode::TLS) {
return {};
}
if (config.tls_context) {
return {};
}
if (!config.certificate_chain_file.empty() && !config.private_key_file.empty()) {
return {};
}
if (!config.certificate_chain_file.empty() || !config.private_key_file.empty()) {
return ligo::detail::fail_void(ligo::detail::error_invalid_argument, std::string{protocol} + " TLS server certificate chain and private key must both be configured");
}
return ligo::detail::fail_void(ligo::detail::error_invalid_argument, std::string{protocol} + " TLS server requires certificate chain/private key files or an SSL context");
}
auto ligo::validate_tls_server_context(std::shared_ptr<boost::asio::ssl::context> const& context) -> std::expected<void, modulus::error> {
if (!context) {
return ligo::detail::fail_void(ligo::detail::error_invalid_argument, "TLS server requires an SSL context");
}
if (SSL_CTX_check_private_key(context->native_handle()) != 1) {
return ligo::detail::fail_void(ssl_failure(ligo::detail::error_invalid_argument, "TLS server context requires matching certificate/private key material"));
}
return {};
}
auto ligo::unsupported_security(std::string_view protocol, ligo::security_mode mode) -> modulus::error {
auto suffix = std::string{};
switch (mode) {
case ligo::security_mode::NONE:
suffix = "none";
break;
case ligo::security_mode::TLS:
suffix = "TLS";
break;
case ligo::security_mode::DTLS:
suffix = "DTLS";
break;
}
return ligo::detail::make_error(ligo::detail::error_invalid_argument, std::string{suffix} + " is not supported by " + std::string{protocol});
}
+131
View File
@@ -0,0 +1,131 @@
#include <ligo/service.hpp>
#include <expected>
#include <exception>
#include <memory>
#include <mutex>
#include <string>
#include <utility>
#include <boost/asio/awaitable.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/serialize.hpp>
#include <boost/json/value.hpp>
#include <boost/system/error_code.hpp>
#include <modulus.hpp>
#include <ligo/detail/errors.hpp>
#include <ligo/detail/transport.hpp>
namespace {
auto json_content_type(std::string const& value) -> std::bool_t {
return value == "application/json" || value.starts_with("application/json;");
}
}
ligo::service_router::service_router(ligo::service_router const& other) {
auto lock = std::scoped_lock{other.mutex_};
handlers_ = other.handlers_;
}
ligo::service_router::service_router(ligo::service_router&& other) {
auto lock = std::scoped_lock{other.mutex_};
handlers_ = std::move(other.handlers_);
}
auto ligo::service_router::operator=(ligo::service_router const& other) -> ligo::service_router& {
if (this != &other) {
auto lock = std::scoped_lock{
mutex_,
other.mutex_,
};
handlers_ = other.handlers_;
}
return *this;
}
auto ligo::service_router::operator=(ligo::service_router&& other) -> ligo::service_router& {
if (this != &other) {
auto lock = std::scoped_lock{
mutex_,
other.mutex_,
};
handlers_ = std::move(other.handlers_);
}
return *this;
}
auto ligo::service_router::add_route(std::string method, std::string route, std::function<boost::asio::awaitable<std::expected<boost::json::value, modulus::error>>(ligo::request_context, boost::json::value)> handler) -> void {
auto wrapped = [handler = std::move(handler)](ligo::request_context context, boost::json::value payload) -> boost::asio::awaitable<std::expected<ligo::service_response, modulus::error>> {
auto result = co_await handler(std::move(context), std::move(payload));
if (!result) {
co_return std::unexpected(result.error());
}
co_return ligo::json_response(std::move(*result));
};
add_response_route(std::move(method), std::move(route), std::move(wrapped));
}
auto ligo::service_router::add_response_route(std::string method, std::string route, std::function<boost::asio::awaitable<std::expected<ligo::service_response, modulus::error>>(ligo::request_context, boost::json::value)> handler) -> void {
auto stored = std::make_shared<std::function<boost::asio::awaitable<std::expected<ligo::service_response, modulus::error>>(ligo::request_context, boost::json::value)> const>(std::move(handler));
auto lock = std::scoped_lock{mutex_};
handlers_[{
std::move(method),
std::move(route),
}] = std::move(stored);
}
auto ligo::service_router::call(ligo::request_context context, boost::json::value payload) const -> boost::asio::awaitable<std::expected<boost::json::value, modulus::error>> {
auto response = co_await call_response(std::move(context), std::move(payload));
if (!response) {
co_return ligo::detail::fail<boost::json::value>(response.error());
}
if (!json_content_type(response->content_type)) {
co_return ligo::detail::fail<boost::json::value>(ligo::detail::error_invalid_argument, "service response is not JSON");
}
auto parse_error = boost::system::error_code{};
auto value = boost::json::parse(response->body, parse_error);
if (parse_error) {
co_return ligo::detail::fail<boost::json::value>(ligo::detail::error_invalid_argument, parse_error.message());
}
co_return ligo::detail::unwrap_response_envelope(std::move(value), "service");
}
auto ligo::service_router::call_response(ligo::request_context context, boost::json::value payload) const -> boost::asio::awaitable<std::expected<ligo::service_response, modulus::error>> {
auto handler = std::shared_ptr<std::function<boost::asio::awaitable<std::expected<ligo::service_response, modulus::error>>(ligo::request_context, boost::json::value)> const>{};
{
auto lock = std::scoped_lock{mutex_};
auto found = handlers_.find({
context.method,
context.route,
});
if (found == handlers_.end()) {
co_return ligo::detail::fail<ligo::service_response>(ligo::detail::error_not_found, "route not found");
}
handler = found->second;
}
try {
co_return co_await (*handler)(std::move(context), std::move(payload));
} catch (std::exception const& error) {
co_return ligo::detail::fail<ligo::service_response>(ligo::detail::error_internal, std::string{"handler failed: "} + error.what());
} catch (...) {
co_return ligo::detail::fail<ligo::service_response>(ligo::detail::error_internal, "handler failed");
}
}
auto ligo::json_response(boost::json::value payload) -> ligo::service_response {
return {
.content_type = "application/json",
.body = ligo::detail::serialize_response_envelope(modulus::success(std::move(payload))),
};
}
auto ligo::text_response(std::string content_type, std::string body) -> ligo::service_response {
return {
.content_type = std::move(content_type),
.body = std::move(body),
};
}
Executable
+791
View File
@@ -0,0 +1,791 @@
#include <ligo/tcp.hpp>
#include <array>
#include <atomic>
#include <cassert>
#include <chrono>
#include <cstdint>
#include <expected>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <utility>
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/as_tuple.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/buffer.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/read.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/asio/strand.hpp>
#include <boost/asio/this_coro.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/asio/write.hpp>
#include <boost/beast/core.hpp>
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/serialize.hpp>
#include <boost/json/value.hpp>
#include <boost/system/error_code.hpp>
#include <modulus.hpp>
#include <ligo/detail/errors.hpp>
#include <ligo/detail/lifecycle.hpp>
#include <ligo/detail/state.hpp>
#include <ligo/detail/transport.hpp>
#include <ligo/config.hpp>
#include <ligo/security.hpp>
#include <ligo/service.hpp>
namespace {
struct tcp_server_state {
tcp_server_state(boost::asio::any_io_executor executor, ligo::tcp::endpoint endpoint, ligo::service_router router, ligo::tcp::server_options options);
boost::asio::any_io_executor executor;
boost::asio::strand<boost::asio::any_io_executor> strand;
std::shared_ptr<boost::asio::ip::tcp::acceptor> acceptor{};
ligo::tcp::endpoint endpoint{};
ligo::tcp::server_options options{};
std::shared_ptr<ligo::service_router const> router{};
std::shared_ptr<ligo::detail::session_registry<boost::beast::tcp_stream>> sessions{std::make_shared<ligo::detail::session_registry<boost::beast::tcp_stream>>()};
std::shared_ptr<ligo::detail::session_registry<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>> tls_sessions{std::make_shared<ligo::detail::session_registry<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>>()};
std::shared_ptr<boost::asio::ssl::context> tls_context{};
std::atomic_uint16_t selected_port{};
std::atomic<std::bool_t> start_called{false};
std::atomic<std::bool_t> stopped{false};
mutable std::mutex lifecycle_mutex{};
};
constexpr auto max_frame_size = std::uint32_t{16U * 1024U * 1024U};
struct parsed_tcp_request {
std::string route{};
boost::json::value payload{};
};
auto encode_size(std::uint32_t size) -> std::array<std::uchar_t, 4> {
return {
static_cast<std::uchar_t>((size >> 24U) & 0xffU),
static_cast<std::uchar_t>((size >> 16U) & 0xffU),
static_cast<std::uchar_t>((size >> 8U) & 0xffU),
static_cast<std::uchar_t>(size & 0xffU),
};
}
auto decode_size(std::array<std::uchar_t, 4> const& bytes) -> std::uint32_t {
return (static_cast<std::uint32_t>(bytes[0]) << 24U) | (static_cast<std::uint32_t>(bytes[1]) << 16U) | (static_cast<std::uint32_t>(bytes[2]) << 8U) | static_cast<std::uint32_t>(bytes[3]);
}
template <typename S>
auto read_frame_from(S& stream, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<boost::json::value, modulus::error>> {
auto header = std::array<std::uchar_t, 4>{};
auto header_deadline = ligo::detail::start_stream_deadline(stream, timeout);
auto [header_error, header_size] = co_await boost::asio::async_read(stream, boost::asio::buffer(header), ligo::detail::deadline_token(header_deadline));
ligo::detail::cancel_stream_deadline(header_deadline);
if (header_error) {
auto failure_status = ligo::detail::operation_failure(header_error, header_deadline, "TCP request timed out");
co_return ligo::detail::fail<boost::json::value>(failure_status);
}
if (header_size != header.size()) {
co_return ligo::detail::fail<boost::json::value>(ligo::detail::error_unavailable, "incomplete frame header");
}
auto const size = decode_size(header);
if (size == 0 || size > max_frame_size) {
co_return ligo::detail::fail<boost::json::value>(ligo::detail::error_invalid_argument, "invalid frame size");
}
auto body = std::string(size, static_cast<std::char_t>('\0'));
auto body_deadline = ligo::detail::start_stream_deadline(stream, timeout);
auto [body_error, body_size] = co_await boost::asio::async_read(stream, boost::asio::buffer(body), ligo::detail::deadline_token(body_deadline));
ligo::detail::cancel_stream_deadline(body_deadline);
if (body_error) {
auto failure_status = ligo::detail::operation_failure(body_error, body_deadline, "TCP request timed out");
co_return ligo::detail::fail<boost::json::value>(failure_status);
}
if (body_size != body.size()) {
co_return ligo::detail::fail<boost::json::value>(ligo::detail::error_unavailable, "incomplete frame body");
}
auto parse_error = boost::system::error_code{};
auto value = boost::json::parse(body, parse_error);
if (parse_error) {
co_return ligo::detail::fail<boost::json::value>(ligo::detail::error_invalid_argument, parse_error.message());
}
co_return modulus::success(std::move(value));
}
template <typename S>
auto write_frame_to(S& stream, std::string body, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
if (body.size() > max_frame_size) {
co_return ligo::detail::fail_void(ligo::detail::error_invalid_argument, "frame too large");
}
auto header = encode_size(static_cast<std::uint32_t>(body.size()));
auto header_deadline = ligo::detail::start_stream_deadline(stream, timeout);
auto [header_error, header_size] = co_await boost::asio::async_write(stream, boost::asio::buffer(header), ligo::detail::deadline_token(header_deadline));
ligo::detail::cancel_stream_deadline(header_deadline);
if (header_error) {
co_return ligo::detail::fail_void(ligo::detail::operation_failure(header_error, header_deadline, "TCP request timed out"));
}
if (header_size != header.size()) {
co_return ligo::detail::fail_void(ligo::detail::error_unavailable, "incomplete frame header write");
}
auto body_deadline = ligo::detail::start_stream_deadline(stream, timeout);
auto [error, body_size] = co_await boost::asio::async_write(stream, boost::asio::buffer(body), ligo::detail::deadline_token(body_deadline));
ligo::detail::cancel_stream_deadline(body_deadline);
if (error) {
co_return ligo::detail::fail_void(ligo::detail::operation_failure(error, body_deadline, "TCP request timed out"));
}
if (body_size != body.size()) {
co_return ligo::detail::fail_void(ligo::detail::error_unavailable, "incomplete frame body write");
}
co_return std::expected<void, modulus::error>{};
}
auto parse_tcp_request(boost::json::value const& value) -> std::expected<parsed_tcp_request, modulus::error> {
if (!value.is_object()) {
return ligo::detail::fail<parsed_tcp_request>(ligo::detail::error_invalid_argument, "TCP request must be an object");
}
auto const& object = value.as_object();
auto route = object.find("route");
auto payload = object.find("payload");
if (route == object.end() || !route->value().is_string() || payload == object.end()) {
return ligo::detail::fail<parsed_tcp_request>(ligo::detail::error_invalid_argument, "TCP request missing route or payload");
}
return modulus::success(parsed_tcp_request{
.route = std::string{route->value().as_string()},
.payload = boost::json::value{payload->value()},
});
}
auto serialize_tcp_request(ligo::tcp::request const& request) -> std::expected<std::string, modulus::error> {
auto envelope = boost::json::object{};
envelope["route"] = request.route;
envelope["payload"] = request.payload;
auto body = boost::json::serialize(envelope);
if (body.size() > max_frame_size) {
return ligo::detail::fail<std::string>(ligo::detail::error_invalid_argument, "frame too large");
}
return modulus::success(std::move(body));
}
auto remote_peer(boost::beast::tcp_stream& stream) -> std::string {
auto endpoint_error = boost::system::error_code{};
auto remote_endpoint = stream.socket().remote_endpoint(endpoint_error);
return endpoint_error ? std::string{} : remote_endpoint.address().to_string();
}
auto remote_peer(boost::asio::ssl::stream<boost::asio::ip::tcp::socket>& stream) -> std::string {
auto endpoint_error = boost::system::error_code{};
auto remote_endpoint = stream.next_layer().remote_endpoint(endpoint_error);
return endpoint_error ? std::string{} : remote_endpoint.address().to_string();
}
template <typename STREAM>
auto handle_session(STREAM& stream, std::shared_ptr<ligo::service_router const> router, ligo::tcp::server_options options) -> boost::asio::awaitable<void> {
assert(router != nullptr);
auto request_value = co_await read_frame_from(stream, options.timeout);
if (!request_value) {
auto written = co_await write_frame_to(stream, ligo::detail::serialize_response_envelope(ligo::detail::fail<boost::json::value>(request_value.error())), options.timeout);
ligo::detail::report_error(options.on_error, written);
co_return;
}
auto parsed_request = parse_tcp_request(*request_value);
if (!parsed_request) {
auto written = co_await write_frame_to(stream, ligo::detail::serialize_response_envelope(ligo::detail::fail<boost::json::value>(parsed_request.error())), options.timeout);
ligo::detail::report_error(options.on_error, written);
co_return;
}
auto context = ligo::request_context{
.peer = remote_peer(stream),
.method = "TCP",
.route = std::move(parsed_request->route),
};
auto result = co_await ligo::detail::call_handler(*router, std::move(context), std::move(parsed_request->payload));
auto written = co_await write_frame_to(stream, ligo::detail::serialize_response_envelope(std::move(result)), options.timeout);
ligo::detail::report_error(options.on_error, written);
}
auto serve_session(std::shared_ptr<boost::beast::tcp_stream> stream, std::shared_ptr<ligo::service_router const> router, ligo::tcp::server_options options, std::shared_ptr<ligo::detail::session_registry<boost::beast::tcp_stream>> registry) -> boost::asio::awaitable<void> {
auto guard = ligo::detail::session_guard<boost::beast::tcp_stream>{
.registry = std::move(registry),
.stream = stream,
};
co_await handle_session(*stream, std::move(router), options);
guard.release();
}
auto serve_tls_session(std::shared_ptr<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>> stream, std::shared_ptr<ligo::service_router const> router, ligo::tcp::server_options options, std::shared_ptr<ligo::detail::session_registry<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>> registry) -> boost::asio::awaitable<void> {
auto guard = ligo::detail::session_guard<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>{
.registry = std::move(registry),
.stream = stream,
};
auto handshake = co_await ligo::detail::async_tls_handshake(*stream, boost::asio::ssl::stream_base::server, options.timeout, "TCP TLS handshake timed out");
if (!handshake) {
ligo::detail::report_error(options.on_error, handshake);
guard.release();
co_return;
}
co_await handle_session(*stream, std::move(router), options);
guard.release();
}
auto accept_loop(std::shared_ptr<tcp_server_state> state) -> boost::asio::awaitable<void> {
for (;;) {
auto session_strand = boost::asio::make_strand(state->executor);
auto stream = std::make_shared<boost::beast::tcp_stream>(session_strand);
auto [error] = co_await state->acceptor->async_accept(stream->socket(), boost::asio::as_tuple(boost::asio::use_awaitable));
if (error == boost::asio::error::operation_aborted || error == boost::asio::error::bad_descriptor || (error && state->stopped.load())) {
co_return;
}
if (!error) {
state->sessions->add(stream);
auto session_executor = stream->get_executor();
boost::asio::co_spawn(session_executor, serve_session(std::move(stream), state->router, state->options, state->sessions), boost::asio::detached);
} else {
ligo::detail::report_error(state->options.on_error, ligo::detail::network_failure(error));
}
}
}
auto accept_tls_loop(std::shared_ptr<tcp_server_state> state) -> boost::asio::awaitable<void> {
for (;;) {
auto session_strand = boost::asio::make_strand(state->executor);
auto socket = std::make_shared<boost::asio::ip::tcp::socket>(session_strand);
auto [error] = co_await state->acceptor->async_accept(*socket, boost::asio::as_tuple(boost::asio::use_awaitable));
if (error == boost::asio::error::operation_aborted || error == boost::asio::error::bad_descriptor || (error && state->stopped.load())) {
co_return;
}
if (!error) {
auto stream = std::make_shared<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>(std::move(*socket), *state->tls_context);
state->tls_sessions->add(stream);
auto session_executor = stream->get_executor();
boost::asio::co_spawn(session_executor, serve_tls_session(std::move(stream), state->router, state->options, state->tls_sessions), boost::asio::detached);
} else {
ligo::detail::report_error(state->options.on_error, ligo::detail::network_failure(error));
}
}
}
auto tcp_state(ligo::detail::state_handle const& state) -> std::shared_ptr<tcp_server_state> {
return state.as<tcp_server_state>();
}
auto uninitialized_tcp_server() -> std::expected<void, modulus::error> {
return ligo::detail::fail_void(ligo::detail::unavailable("TCP server is not initialized"));
}
auto start_tcp_server(std::shared_ptr<tcp_server_state> const& state) -> std::expected<void, modulus::error> {
assert(!state || state->acceptor != nullptr);
assert(!state || state->router != nullptr);
if (!state) {
return uninitialized_tcp_server();
}
auto lock = std::scoped_lock{state->lifecycle_mutex};
auto valid_timeout = ligo::detail::validate_positive_timeout(state->options.timeout, "TCP server timeout");
if (!valid_timeout) {
return valid_timeout;
}
if (state->options.security.mode == ligo::security_mode::DTLS) {
return ligo::detail::fail_void(ligo::unsupported_security("TCP server", state->options.security.mode));
}
if (state->start_called.exchange(true)) {
return ligo::detail::fail_void(ligo::detail::unavailable("TCP server has already been started"));
}
if (state->options.security.mode == ligo::security_mode::TLS) {
auto valid_config = ligo::validate_tls_server_config("TCP", state->options.security);
if (!valid_config) {
state->start_called.store(false);
return valid_config;
}
auto context = ligo::make_tls_server_context(state->options.security);
if (!context) {
state->start_called.store(false);
return ligo::detail::fail_void(context.error());
}
auto valid_context = ligo::validate_tls_server_context(*context);
if (!valid_context) {
state->start_called.store(false);
return valid_context;
}
state->tls_context = std::move(*context);
}
auto selected = ligo::detail::start_tcp_acceptor(*state->acceptor, state->endpoint.host, state->endpoint.port);
if (!selected) {
state->start_called.store(false);
return ligo::detail::fail_void(selected.error());
}
state->selected_port.store(*selected);
assert(state->selected_port.load() == *selected);
state->stopped.store(false);
modulus::log_debug("ligo TCP server started on port " + std::to_string(*selected));
if (state->options.security.mode == ligo::security_mode::TLS) {
boost::asio::co_spawn(state->strand, accept_tls_loop(state), boost::asio::detached);
} else {
boost::asio::co_spawn(state->strand, accept_loop(state), boost::asio::detached);
}
return {};
}
auto stop_tcp_server(std::shared_ptr<tcp_server_state> state) -> void {
assert(state != nullptr);
auto lock = std::scoped_lock{state->lifecycle_mutex};
if (state->stopped.exchange(true)) {
return;
}
modulus::log_debug("ligo TCP server stopping");
state->sessions->cancel_all();
state->tls_sessions->cancel_all();
auto server_executor = state->strand;
boost::asio::post(server_executor, [state = std::move(state)] {
auto ignored = boost::system::error_code{};
state->acceptor->close(ignored);
});
}
auto stop_tcp_server_now(std::shared_ptr<tcp_server_state> const& state) -> void {
auto lock = std::scoped_lock{state->lifecycle_mutex};
if (state->stopped.exchange(true)) {
return;
}
state->sessions->cancel_all();
state->tls_sessions->cancel_all();
auto ignored = boost::system::error_code{};
state->acceptor->close(ignored);
}
auto async_stop_tcp_server(std::shared_ptr<tcp_server_state> state, std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
if (!state) {
co_return uninitialized_tcp_server();
}
auto valid_timeout = ligo::detail::validate_positive_timeout(timeout, "shutdown timeout");
if (!valid_timeout) {
co_return valid_timeout;
}
auto should_stop = false;
{
auto lock = std::scoped_lock{state->lifecycle_mutex};
should_stop = !state->stopped.exchange(true);
if (should_stop) {
state->sessions->cancel_all();
state->tls_sessions->cancel_all();
}
}
if (should_stop) {
co_await boost::asio::post(state->strand, boost::asio::use_awaitable);
auto ignored = boost::system::error_code{};
state->acceptor->close(ignored);
}
auto drained = co_await state->sessions->wait_empty(timeout);
if (!drained) {
co_return drained;
}
co_return co_await state->tls_sessions->wait_empty(timeout);
}
auto selected_tcp_port(std::shared_ptr<tcp_server_state> const& state) -> std::uint16_t {
if (!state) {
return 0;
}
return state->selected_port.load();
}
auto failed_tcp_call(modulus::error status) -> boost::asio::awaitable<std::expected<boost::json::value, modulus::error>> {
co_return ligo::detail::fail<boost::json::value>(std::move(status));
}
auto call_tls_tcp_endpoint(ligo::tcp::endpoint endpoint, std::shared_ptr<std::string const> request_body, ligo::tcp::client_options options) -> boost::asio::awaitable<std::expected<boost::json::value, modulus::error>> {
auto executor = co_await boost::asio::this_coro::executor;
auto socket = co_await ligo::tcp::connect_tls_handshaken(executor, std::move(endpoint), options, options.security);
if (!socket) {
co_return ligo::detail::fail<boost::json::value>(socket.error());
}
auto stream = socket->stream();
if (!stream) {
co_return ligo::detail::fail<boost::json::value>(stream.error());
}
auto sent = co_await write_frame_to(stream->get(), *request_body, options.timeout);
if (!sent) {
co_return ligo::detail::fail<boost::json::value>(sent.error());
}
auto response = co_await read_frame_from(stream->get(), options.timeout);
if (!response) {
co_return response;
}
co_return ligo::detail::unwrap_response_envelope(std::move(*response), "TCP");
}
auto call_tcp_endpoint(ligo::tcp::endpoint endpoint, std::shared_ptr<std::string const> request_body, ligo::tcp::client_options options) -> boost::asio::awaitable<std::expected<boost::json::value, modulus::error>> {
assert(request_body != nullptr);
modulus::log_debug("ligo TCP client call to " + endpoint.host + ":" + std::to_string(endpoint.port));
auto valid_timeout = ligo::detail::validate_positive_timeout(options.timeout, "TCP timeout");
if (!valid_timeout) {
co_return ligo::detail::fail<boost::json::value>(valid_timeout.error());
}
if (endpoint.port == 0) {
co_return ligo::detail::fail<boost::json::value>(ligo::detail::error_invalid_argument, "TCP port must be non-zero");
}
if (options.security.mode == ligo::security_mode::DTLS) {
auto status = ligo::unsupported_security("TCP client", options.security.mode);
co_return ligo::detail::fail<boost::json::value>(status);
}
if (options.security.mode == ligo::security_mode::TLS) {
co_return co_await call_tls_tcp_endpoint(std::move(endpoint), std::move(request_body), options);
}
auto executor = co_await boost::asio::this_coro::executor;
auto stream = boost::beast::tcp_stream{executor};
auto endpoints = co_await ligo::detail::resolve_tcp_endpoints(executor, endpoint.host, endpoint.port, options.timeout, "TCP request timed out");
if (!endpoints) {
co_return ligo::detail::fail<boost::json::value>(endpoints.error());
}
stream.expires_after(options.timeout);
auto [connect_error, _] = co_await stream.async_connect(*endpoints, boost::asio::as_tuple(boost::asio::use_awaitable));
if (connect_error) {
auto failure_status = ligo::detail::operation_failure(connect_error, "TCP request timed out");
co_return ligo::detail::fail<boost::json::value>(failure_status);
}
stream.expires_after(options.timeout);
auto sent = co_await write_frame_to(stream, *request_body, options.timeout);
if (!sent) {
co_return ligo::detail::fail<boost::json::value>(sent.error());
}
stream.expires_after(options.timeout);
auto response = co_await read_frame_from(stream, options.timeout);
if (!response) {
co_return response;
}
co_return ligo::detail::unwrap_response_envelope(std::move(*response), "TCP");
}
auto connect_tcp_socket(boost::asio::any_io_executor executor, ligo::tcp::endpoint endpoint, ligo::tcp::client_options options) -> boost::asio::awaitable<std::expected<ligo::tcp::socket_handle, modulus::error>> {
auto valid_timeout = ligo::detail::validate_positive_timeout(options.timeout, "TCP timeout");
if (!valid_timeout) {
co_return ligo::detail::fail<ligo::tcp::socket_handle>(valid_timeout.error());
}
if (options.security.mode != ligo::security_mode::NONE) {
auto status = ligo::unsupported_security("raw TCP connect", options.security.mode);
co_return ligo::detail::fail<ligo::tcp::socket_handle>(status);
}
if (endpoint.port == 0) {
co_return ligo::detail::fail<ligo::tcp::socket_handle>(ligo::detail::error_invalid_argument, "TCP port must be non-zero");
}
auto endpoints = co_await ligo::detail::resolve_tcp_endpoints(executor, endpoint.host, endpoint.port, options.timeout, "TCP connect timed out");
if (!endpoints) {
co_return ligo::detail::fail<ligo::tcp::socket_handle>(endpoints.error());
}
auto stream = boost::beast::tcp_stream{executor};
stream.expires_after(options.timeout);
auto [connect_error, _] = co_await stream.async_connect(*endpoints, boost::asio::as_tuple(boost::asio::use_awaitable));
if (connect_error) {
auto failure_status = ligo::detail::operation_failure(connect_error, "TCP connect timed out");
co_return ligo::detail::fail<ligo::tcp::socket_handle>(failure_status);
}
co_return modulus::success(ligo::tcp::socket_handle{stream.release_socket()});
}
auto validate_tls_connect_security(ligo::security_config const& security) -> std::expected<void, modulus::error> {
if (security.mode == ligo::security_mode::TLS) {
return {};
}
return ligo::detail::fail_void(ligo::detail::error_invalid_argument, "TLS connect requires TLS security mode");
}
tcp_server_state::tcp_server_state(boost::asio::any_io_executor executor, ligo::tcp::endpoint endpoint, ligo::service_router router, ligo::tcp::server_options options) : executor{std::move(executor)}, strand{boost::asio::make_strand(this->executor)}, acceptor{std::make_shared<boost::asio::ip::tcp::acceptor>(strand)}, endpoint{std::move(endpoint)}, options{options}, router{std::make_shared<ligo::service_router>(std::move(router))} {}
}
ligo::tcp::socket_handle::socket_handle(boost::asio::ip::tcp::socket socket) : socket_{std::make_shared<boost::asio::ip::tcp::socket>(std::move(socket))} {}
ligo::tcp::socket_handle::socket_handle(std::shared_ptr<boost::asio::ip::tcp::socket> socket) : socket_{std::move(socket)} {}
auto ligo::tcp::socket_handle::socket() -> std::expected<std::reference_wrapper<boost::asio::ip::tcp::socket>, modulus::error> {
if (!socket_) {
return ligo::detail::fail<std::reference_wrapper<boost::asio::ip::tcp::socket>>(ligo::detail::error_unavailable, "TCP socket has been released");
}
return modulus::success(std::ref(*socket_));
}
auto ligo::tcp::socket_handle::socket() const -> std::expected<std::reference_wrapper<boost::asio::ip::tcp::socket const>, modulus::error> {
if (!socket_) {
return ligo::detail::fail<std::reference_wrapper<boost::asio::ip::tcp::socket const>>(ligo::detail::error_unavailable, "TCP socket has been released");
}
return modulus::success(std::cref(*socket_));
}
auto ligo::tcp::socket_handle::is_open() const -> std::bool_t {
return socket_ && socket_->is_open();
}
auto ligo::tcp::socket_handle::close(boost::system::error_code& error) -> void {
if (socket_) {
socket_->close(error);
}
}
auto ligo::tcp::socket_handle::release_socket() -> std::expected<boost::asio::ip::tcp::socket, modulus::error> {
if (!socket_) {
return ligo::detail::fail<boost::asio::ip::tcp::socket>(ligo::detail::error_unavailable, "TCP socket has been released");
}
auto released = std::move(*socket_);
socket_.reset();
return std::expected<boost::asio::ip::tcp::socket, modulus::error>{
std::in_place,
std::move(released),
};
}
ligo::tcp::tls_socket::tls_socket(std::shared_ptr<boost::asio::ssl::context> context, boost::asio::ip::tcp::socket socket) : context_{std::move(context)} {
if (context_) {
stream_ = std::make_unique<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>(std::move(socket), *context_);
}
}
auto ligo::tcp::tls_socket::stream() -> std::expected<std::reference_wrapper<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>, modulus::error> {
if (!stream_) {
return ligo::detail::fail<std::reference_wrapper<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>>(ligo::detail::error_unavailable, "TCP TLS stream has been released");
}
return modulus::success(std::ref(*stream_));
}
auto ligo::tcp::tls_socket::stream() const -> std::expected<std::reference_wrapper<boost::asio::ssl::stream<boost::asio::ip::tcp::socket> const>, modulus::error> {
if (!stream_) {
return ligo::detail::fail<std::reference_wrapper<boost::asio::ssl::stream<boost::asio::ip::tcp::socket> const>>(ligo::detail::error_unavailable, "TCP TLS stream has been released");
}
return modulus::success(std::cref(*stream_));
}
auto ligo::tcp::tls_socket::context() -> std::expected<std::reference_wrapper<boost::asio::ssl::context>, modulus::error> {
if (!context_) {
return ligo::detail::fail<std::reference_wrapper<boost::asio::ssl::context>>(ligo::detail::error_unavailable, "TCP TLS context is not initialized");
}
return modulus::success(std::ref(*context_));
}
auto ligo::tcp::tls_socket::context() const -> std::expected<std::reference_wrapper<boost::asio::ssl::context const>, modulus::error> {
if (!context_) {
return ligo::detail::fail<std::reference_wrapper<boost::asio::ssl::context const>>(ligo::detail::error_unavailable, "TCP TLS context is not initialized");
}
return modulus::success(std::cref(*context_));
}
auto ligo::tcp::tls_socket::is_open() const -> std::bool_t {
return stream_ && stream_->lowest_layer().is_open();
}
auto ligo::tcp::tls_socket::close(boost::system::error_code& error) -> void {
if (stream_) {
stream_->lowest_layer().close(error);
}
}
auto ligo::tcp::tls_socket::release_stream() -> std::expected<ligo::tcp::tls_socket::released_stream, modulus::error> {
if (!stream_) {
return ligo::detail::fail<ligo::tcp::tls_socket::released_stream>(ligo::detail::error_unavailable, "TCP TLS stream has been released");
}
auto released = std::move(*stream_);
stream_.reset();
return std::expected<ligo::tcp::tls_socket::released_stream, modulus::error>{
std::in_place,
context_,
std::move(released),
};
}
ligo::tcp::client::client(ligo::tcp::endpoint endpoint, ligo::tcp::client_options options) : endpoint_{std::move(endpoint)}, options_{options} {}
auto ligo::tcp::client::call(ligo::tcp::request const& request) const -> boost::asio::awaitable<std::expected<boost::json::value, modulus::error>> {
auto body = serialize_tcp_request(request);
if (!body) {
return failed_tcp_call(body.error());
}
return call_tcp_endpoint(endpoint_, std::make_shared<std::string const>(std::move(*body)), options_);
}
ligo::tcp::server::server(boost::asio::io_context& io, ligo::tcp::endpoint endpoint, ligo::service_router router, ligo::tcp::server_options options) : ligo::tcp::server{io.get_executor(), std::move(endpoint), std::move(router), options} {}
ligo::tcp::server::server(boost::asio::any_io_executor executor, ligo::tcp::endpoint endpoint, ligo::service_router router, ligo::tcp::server_options options) : state_{ligo::detail::state_handle{std::make_shared<tcp_server_state>(std::move(executor), std::move(endpoint), std::move(router), options)}} {}
ligo::tcp::server::~server() {
if (auto state = tcp_state(state_)) {
stop_tcp_server_now(state);
}
}
auto ligo::tcp::server::operator=(ligo::tcp::server&& other) noexcept -> ligo::tcp::server& {
if (this != &other) {
if (auto state = tcp_state(state_)) {
stop_tcp_server_now(state);
}
state_ = std::move(other.state_);
}
return *this;
}
auto ligo::tcp::server::start() -> std::expected<void, modulus::error> {
return start_tcp_server(tcp_state(state_));
}
auto ligo::tcp::server::stop() -> void {
if (auto state = tcp_state(state_)) {
stop_tcp_server(std::move(state));
}
}
auto ligo::tcp::server::async_stop(std::chrono::milliseconds timeout) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
return async_stop_tcp_server(tcp_state(state_), timeout);
}
auto ligo::tcp::server::selected_port() const -> std::uint16_t {
return selected_tcp_port(tcp_state(state_));
}
auto ligo::tcp::client_endpoint_from_config(ligo::endpoint_config const& config) -> std::expected<ligo::tcp::endpoint, modulus::error> {
auto parsed = ligo::detail::parse_client_endpoint_text(config.connect);
if (!parsed) {
return ligo::detail::fail<ligo::tcp::endpoint>(parsed.error());
}
return modulus::success(ligo::tcp::endpoint{
.host = std::move(parsed->host),
.port = parsed->port,
});
}
auto ligo::tcp::server_endpoint_from_config(ligo::endpoint_config const& config) -> std::expected<ligo::tcp::endpoint, modulus::error> {
auto parsed = ligo::detail::parse_server_endpoint_text(config.bind);
if (!parsed) {
return ligo::detail::fail<ligo::tcp::endpoint>(parsed.error());
}
return modulus::success(ligo::tcp::endpoint{
.host = std::move(parsed->host),
.port = parsed->port,
});
}
auto ligo::tcp::client_options_from_config(ligo::endpoint_config const& config) -> ligo::tcp::client_options {
return {
.timeout = config.timeout,
.security = config.security,
};
}
auto ligo::tcp::server_options_from_config(ligo::endpoint_config const& config) -> ligo::tcp::server_options {
return {
.timeout = config.timeout,
.on_error = {},
.security = config.security,
};
}
auto ligo::tcp::connect(boost::asio::any_io_executor executor, ligo::tcp::endpoint endpoint, ligo::tcp::client_options options) -> boost::asio::awaitable<std::expected<ligo::tcp::socket_handle, modulus::error>> {
co_return co_await connect_tcp_socket(std::move(executor), std::move(endpoint), options);
}
auto ligo::tcp::connect(boost::asio::any_io_executor executor, ligo::endpoint_config const& config) -> boost::asio::awaitable<std::expected<ligo::tcp::socket_handle, modulus::error>> {
auto endpoint = ligo::tcp::client_endpoint_from_config(config);
if (!endpoint) {
co_return ligo::detail::fail<ligo::tcp::socket_handle>(endpoint.error());
}
co_return co_await ligo::tcp::connect(std::move(executor), std::move(*endpoint), ligo::tcp::client_options_from_config(config));
}
auto ligo::tcp::connect_tls_configured(boost::asio::any_io_executor executor, ligo::tcp::endpoint endpoint, ligo::tcp::client_options options, ligo::security_config security) -> boost::asio::awaitable<std::expected<ligo::tcp::tls_socket, modulus::error>> {
auto host = endpoint.host;
auto valid_timeout = ligo::detail::validate_positive_timeout(options.timeout, "TCP timeout");
if (!valid_timeout) {
co_return ligo::detail::fail<ligo::tcp::tls_socket>(valid_timeout.error());
}
auto valid_security = validate_tls_connect_security(security);
if (!valid_security) {
co_return ligo::detail::fail<ligo::tcp::tls_socket>(valid_security.error());
}
if (endpoint.port == 0) {
co_return ligo::detail::fail<ligo::tcp::tls_socket>(ligo::detail::error_invalid_argument, "TCP port must be non-zero");
}
auto context = ligo::make_tls_client_context(security);
if (!context) {
co_return ligo::detail::fail<ligo::tcp::tls_socket>(context.error());
}
auto endpoints = co_await ligo::detail::resolve_tcp_endpoints(executor, endpoint.host, endpoint.port, options.timeout, "TCP connect timed out");
if (!endpoints) {
co_return ligo::detail::fail<ligo::tcp::tls_socket>(endpoints.error());
}
auto stream = boost::beast::tcp_stream{executor};
stream.expires_after(options.timeout);
auto [connect_error, _] = co_await stream.async_connect(*endpoints, boost::asio::as_tuple(boost::asio::use_awaitable));
if (connect_error) {
auto failure_status = ligo::detail::operation_failure(connect_error, "TCP connect timed out");
co_return ligo::detail::fail<ligo::tcp::tls_socket>(failure_status);
}
auto socket = ligo::tcp::tls_socket{
std::move(*context),
stream.release_socket(),
};
auto socket_stream = socket.stream();
if (!socket_stream) {
co_return ligo::detail::fail<ligo::tcp::tls_socket>(socket_stream.error());
}
auto verification = ligo::configure_tls_client_stream(socket_stream->get(), host, security);
if (!verification) {
co_return ligo::detail::fail<ligo::tcp::tls_socket>(verification.error());
}
co_return std::expected<ligo::tcp::tls_socket, modulus::error>{
std::in_place,
std::move(socket),
};
}
auto ligo::tcp::connect_tls_handshaken(boost::asio::any_io_executor executor, ligo::tcp::endpoint endpoint, ligo::tcp::client_options options, ligo::security_config security) -> boost::asio::awaitable<std::expected<ligo::tcp::tls_socket, modulus::error>> {
auto host = endpoint.host;
auto valid_timeout = ligo::detail::validate_positive_timeout(options.timeout, "TCP timeout");
if (!valid_timeout) {
co_return ligo::detail::fail<ligo::tcp::tls_socket>(valid_timeout.error());
}
auto valid_security = validate_tls_connect_security(security);
if (!valid_security) {
co_return ligo::detail::fail<ligo::tcp::tls_socket>(valid_security.error());
}
if (endpoint.port == 0) {
co_return ligo::detail::fail<ligo::tcp::tls_socket>(ligo::detail::error_invalid_argument, "TCP port must be non-zero");
}
auto context = ligo::make_tls_client_context(security);
if (!context) {
co_return ligo::detail::fail<ligo::tcp::tls_socket>(context.error());
}
auto endpoints = co_await ligo::detail::resolve_tcp_endpoints(executor, endpoint.host, endpoint.port, options.timeout, "TCP connect timed out");
if (!endpoints) {
co_return ligo::detail::fail<ligo::tcp::tls_socket>(endpoints.error());
}
auto stream = boost::beast::tcp_stream{executor};
stream.expires_after(options.timeout);
auto [connect_error, _] = co_await stream.async_connect(*endpoints, boost::asio::as_tuple(boost::asio::use_awaitable));
if (connect_error) {
auto failure_status = ligo::detail::operation_failure(connect_error, "TCP connect timed out");
co_return ligo::detail::fail<ligo::tcp::tls_socket>(failure_status);
}
auto socket = ligo::tcp::tls_socket{
std::move(*context),
stream.release_socket(),
};
auto socket_stream = socket.stream();
if (!socket_stream) {
co_return ligo::detail::fail<ligo::tcp::tls_socket>(socket_stream.error());
}
auto verification = ligo::apply_tls_client_verification(socket_stream->get(), host, security);
if (!verification) {
co_return ligo::detail::fail<ligo::tcp::tls_socket>(verification.error());
}
auto handshake = co_await ligo::detail::async_tls_handshake(socket_stream->get(), boost::asio::ssl::stream_base::client, options.timeout, "TCP TLS handshake timed out");
if (!handshake) {
co_return ligo::detail::fail<ligo::tcp::tls_socket>(handshake.error());
}
co_return std::expected<ligo::tcp::tls_socket, modulus::error>{
std::in_place,
std::move(socket),
};
}
Executable
+1079
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
add_executable(ligo-test
test.cpp
)
find_package(Threads REQUIRED)
target_include_directories(ligo-test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(ligo-test PRIVATE
ligo::ligo
Threads::Threads
)
target_compile_features(ligo-test PRIVATE cxx_std_23)
target_compile_definitions(ligo-test PRIVATE
$<$<CONFIG:Debug>:LIGO_EXPECT_DEBUG_ASSERTS>
$<$<NOT:$<CONFIG:Debug>>:LIGO_EXPECT_NDEBUG>
)
add_test(NAME ligo COMMAND ligo-test)
set_tests_properties(ligo PROPERTIES TIMEOUT 60)
add_custom_target(check
COMMAND ${CMAKE_CTEST_COMMAND}
--test-dir ${CMAKE_BINARY_DIR}
-R ^ligo$
--output-on-failure
--timeout 60
USES_TERMINAL
)
+2256
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
#pragma once
#define BOOST_TEST_MODULE ligo
#include <boost/test/included/unit_test.hpp>