This commit is contained in:
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
@PACKAGE_INIT@
|
||||||
|
|
||||||
|
include(CMakeFindDependencyMacro)
|
||||||
|
find_dependency(modulus CONFIG REQUIRED)
|
||||||
|
|
||||||
|
include("${CMAKE_CURRENT_LIST_DIR}/ligo-targets.cmake")
|
||||||
+107
@@ -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",
|
||||||
|
]
|
||||||
Executable
+10
@@ -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>
|
||||||
Executable
+54
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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>{};
|
||||||
|
}
|
||||||
Executable
+86
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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>>;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
Executable
+49
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
Executable
+128
@@ -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)} {}
|
||||||
|
|
||||||
|
}
|
||||||
Executable
+92
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"package": {
|
||||||
|
"name": "ligo"
|
||||||
|
},
|
||||||
|
"tooling": {
|
||||||
|
"remote": {
|
||||||
|
"name": "khodo",
|
||||||
|
"url": "https://git.thyme-labs.com/api/packages/khodo/conan"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+367
@@ -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
@@ -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),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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));
|
||||||
|
}
|
||||||
@@ -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});
|
||||||
|
}
|
||||||
Executable
+131
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define BOOST_TEST_MODULE ligo
|
||||||
|
#include <boost/test/included/unit_test.hpp>
|
||||||
Reference in New Issue
Block a user