commit 4851371f569141c762d3252bde011d418b7f1276 Author: modeco80 Date: Mon Feb 5 06:24:48 2024 -0500 Initial commit of rewrite based on base/ library Should make the code actually possible to finish :v diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..6142558 --- /dev/null +++ b/.clang-format @@ -0,0 +1,44 @@ +BasedOnStyle: Google + +# force T* or T& +DerivePointerAlignment: false +PointerAlignment: Left + +TabWidth: 4 +IndentWidth: 4 +UseTab: Always +IndentPPDirectives: BeforeHash + +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortFunctionsOnASingleLine: InlineOnly +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: true + +BinPackArguments: true +BinPackParameters: true +BreakConstructorInitializers: BeforeColon +BreakStringLiterals: false + +ColumnLimit: 0 +CompactNamespaces: false + +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ContinuationIndentWidth: 0 + +# turning this on causes major issues with initializer lists +Cpp11BracedListStyle: false +SpaceBeforeCpp11BracedList: true + +FixNamespaceComments: true + +NamespaceIndentation: All +ReflowComments: true + +SortIncludes: CaseInsensitive +SortUsingDeclarations: true + +SpacesInSquareBrackets: false +SpaceBeforeParens: Never +SpacesBeforeTrailingComments: 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..54ea067 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# ignores all the autogenerated directories we need to +/.vs +/out +/build +.cache/ + +# live test hidden, a standalone html will be made later +/client/livetest* \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..bdef7cc --- /dev/null +++ b/.gitmodules @@ -0,0 +1,198 @@ +[submodule "third_party/boost/algorithm"] + path = third_party/boost/algorithm + url = https://github.com/boostorg/algorithm.git +[submodule "third_party/boost/align"] + path = third_party/boost/align + url = https://github.com/boostorg/align.git +[submodule "third_party/boost/array"] + path = third_party/boost/array + url = https://github.com/boostorg/array.git +[submodule "third_party/boost/assert"] + path = third_party/boost/assert + url = https://github.com/boostorg/assert.git +[submodule "third_party/boost/atomic"] + path = third_party/boost/atomic + url = https://github.com/boostorg/atomic.git +[submodule "third_party/boost/bind"] + path = third_party/boost/bind + url = https://github.com/boostorg/bind.git +[submodule "third_party/boost/chrono"] + path = third_party/boost/chrono + url = https://github.com/boostorg/chrono.git +[submodule "third_party/boost/circular_buffer"] + path = third_party/boost/circular_buffer + url = https://github.com/boostorg/circular_buffer.git +[submodule "third_party/boost/concept_check"] + path = third_party/boost/concept_check + url = https://github.com/boostorg/concept_check.git +[submodule "third_party/boost/config"] + path = third_party/boost/config + url = https://github.com/boostorg/config.git +[submodule "third_party/boost/container"] + path = third_party/boost/container + url = https://github.com/boostorg/container.git +[submodule "third_party/boost/container_hash"] + path = third_party/boost/container_hash + url = https://github.com/boostorg/container_hash.git +[submodule "third_party/boost/context"] + path = third_party/boost/context + url = https://github.com/boostorg/context.git +[submodule "third_party/boost/conversion"] + path = third_party/boost/conversion + url = https://github.com/boostorg/conversion.git +[submodule "third_party/boost/core"] + path = third_party/boost/core + url = https://github.com/boostorg/core.git +[submodule "third_party/boost/coroutine"] + path = third_party/boost/coroutine + url = https://github.com/boostorg/coroutine.git +[submodule "third_party/boost/date_time"] + path = third_party/boost/date_time + url = https://github.com/boostorg/date_time.git +[submodule "third_party/boost/describe"] + path = third_party/boost/describe + url = https://github.com/boostorg/describe.git +[submodule "third_party/boost/detail"] + path = third_party/boost/detail + url = https://github.com/boostorg/detail.git +[submodule "third_party/boost/endian"] + path = third_party/boost/endian + url = https://github.com/boostorg/endian.git +[submodule "third_party/boost/exception"] + path = third_party/boost/exception + url = https://github.com/boostorg/exception.git +[submodule "third_party/boost/filesystem"] + path = third_party/boost/filesystem + url = https://github.com/boostorg/filesystem.git +[submodule "third_party/boost/function"] + path = third_party/boost/function + url = https://github.com/boostorg/function.git +[submodule "third_party/boost/functional"] + path = third_party/boost/functional + url = https://github.com/boostorg/functional.git +[submodule "third_party/boost/function_types"] + path = third_party/boost/function_types + url = https://github.com/boostorg/function_types.git +[submodule "third_party/boost/fusion"] + path = third_party/boost/fusion + url = https://github.com/boostorg/fusion.git +[submodule "third_party/boost/integer"] + path = third_party/boost/integer + url = https://github.com/boostorg/integer.git +[submodule "third_party/boost/intrusive"] + path = third_party/boost/intrusive + url = https://github.com/boostorg/intrusive.git +[submodule "third_party/boost/io"] + path = third_party/boost/io + url = https://github.com/boostorg/io.git +[submodule "third_party/boost/iterator"] + path = third_party/boost/iterator + url = https://github.com/boostorg/iterator.git +[submodule "third_party/boost/json"] + path = third_party/boost/json + url = https://github.com/boostorg/json.git +[submodule "third_party/boost/leaf"] + path = third_party/boost/leaf + url = https://github.com/boostorg/leaf.git +[submodule "third_party/boost/lexical_cast"] + path = third_party/boost/lexical_cast + url = https://github.com/boostorg/lexical_cast.git +[submodule "third_party/boost/logic"] + path = third_party/boost/logic + url = https://github.com/boostorg/logic.git +[submodule "third_party/boost/move"] + path = third_party/boost/move + url = https://github.com/boostorg/move.git +[submodule "third_party/boost/mp11"] + path = third_party/boost/mp11 + url = https://github.com/boostorg/mp11.git +[submodule "third_party/boost/mpl"] + path = third_party/boost/mpl + url = https://github.com/boostorg/mpl.git +[submodule "third_party/boost/numeric_conversion"] + path = third_party/boost/numeric_conversion + url = https://github.com/boostorg/numeric_conversion.git +[submodule "third_party/boost/optional"] + path = third_party/boost/optional + url = https://github.com/boostorg/optional.git +[submodule "third_party/boost/pool"] + path = third_party/boost/pool + url = https://github.com/boostorg/pool.git +[submodule "third_party/boost/predef"] + path = third_party/boost/predef + url = https://github.com/boostorg/predef.git +[submodule "third_party/boost/preprocessor"] + path = third_party/boost/preprocessor + url = https://github.com/boostorg/preprocessor.git +[submodule "third_party/boost/range"] + path = third_party/boost/range + url = https://github.com/boostorg/range.git +[submodule "third_party/boost/ratio"] + path = third_party/boost/ratio + url = https://github.com/boostorg/ratio.git +[submodule "third_party/boost/rational"] + path = third_party/boost/rational + url = https://github.com/boostorg/rational.git +[submodule "third_party/boost/regex"] + path = third_party/boost/regex + url = https://github.com/boostorg/regex.git +[submodule "third_party/boost/smart_ptr"] + path = third_party/boost/smart_ptr + url = https://github.com/boostorg/smart_ptr.git +[submodule "third_party/boost/static_assert"] + path = third_party/boost/static_assert + url = https://github.com/boostorg/static_assert.git +[submodule "third_party/boost/static_string"] + path = third_party/boost/static_string + url = https://github.com/boostorg/static_string.git +[submodule "third_party/boost/system"] + path = third_party/boost/system + url = https://github.com/boostorg/system.git +[submodule "third_party/boost/throw_exception"] + path = third_party/boost/throw_exception + url = https://github.com/boostorg/throw_exception.git +[submodule "third_party/boost/tokenizer"] + path = third_party/boost/tokenizer + url = https://github.com/boostorg/tokenizer.git +[submodule "third_party/boost/tuple"] + path = third_party/boost/tuple + url = https://github.com/boostorg/tuple.git +[submodule "third_party/boost/type_index"] + path = third_party/boost/type_index + url = https://github.com/boostorg/type_index.git +[submodule "third_party/boost/typeof"] + path = third_party/boost/typeof + url = https://github.com/boostorg/typeof.git +[submodule "third_party/boost/type_traits"] + path = third_party/boost/type_traits + url = https://github.com/boostorg/type_traits.git +[submodule "third_party/boost/unordered"] + path = third_party/boost/unordered + url = https://github.com/boostorg/unordered.git +[submodule "third_party/boost/url"] + path = third_party/boost/url + url = https://github.com/boostorg/url.git +[submodule "third_party/boost/utility"] + path = third_party/boost/utility + url = https://github.com/boostorg/utility.git +[submodule "third_party/boost/variant2"] + path = third_party/boost/variant2 + url = https://github.com/boostorg/variant2.git +[submodule "third_party/boost/winapi"] + path = third_party/boost/winapi + url = https://github.com/boostorg/winapi.git +[submodule "third_party/boost/asio"] + path = third_party/boost/asio + url = https://github.com/boostorg/asio.git +[submodule "third_party/boost/beast"] + path = third_party/boost/beast + url = https://github.com/boostorg/beast.git +[submodule "third_party/r3"] + path = lib/http/third_party/r3 + url = https://git.computernewb.com/collabvm/r3 +[submodule "third_party/tomlplusplus"] + path = third_party/tomlplusplus + url = https://github.com/marzer/tomlplusplus +[submodule "third_party/boost/mysql"] + path = third_party/boost/mysql + url = https://github.com/boostorg/mysql.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7b58774 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.15) + +if(WIN32 OR APPLE OR BSD) + message(FATAL_ERROR "not happening, sorry") +endif() + + +project(SSX3LobbyServer + LANGUAGES CXX +) + +list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") + +include(Policies) +include(ProjectFuncs) +include(CompilerFlags) + +lobbyserver_set_alternate_linker() + +# third party vendor dependencies +add_subdirectory(third_party) + +# libraries +add_subdirectory(lib/base) +add_subdirectory(lib/impl) +add_subdirectory(lib/http) + +# projects +add_subdirectory(src) + diff --git a/cmake/CompilerFlags.cmake b/cmake/CompilerFlags.cmake new file mode 100644 index 0000000..417756b --- /dev/null +++ b/cmake/CompilerFlags.cmake @@ -0,0 +1,76 @@ + +# Core compile arguments used for the whole server +# TODO: This currently assumes libstdc++, later on we should *probably* set this with some detection +# to check the current c++ standard library (but we only will compile and run with libstdc++ anyways since libc++ is a bit lacking with even c++17, so..) +# also TODO: Use a list so that this isn't one giant line (list JOIN should help.) + +set(LOBBYSERVER_CORE_COMPILE_ARGS "-Wall -Wformat=2 -Wimplicit-fallthrough -fstrict-flex-arrays=3 -fstack-clash-protection -fstack-protector-strong ") +set(LOBBYSERVER_CORE_LINKER_ARGS "-fuse-ld=${LOBBYSERVER_LINKER}") + +if("${CMAKE_BUILD_TYPE}" STREQUAL "Release") # OR "${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo" + +set(LOBBYSERVER_CORE_COMPILE_ARGS "${LOBBYSERVER_CORE_COMPILE_ARGS} -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3") + +# If on Release use link-time optimizations. +# On clang we use ThinLTO for even better build performance. +if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + set(LOBBYSERVER_CORE_COMPILE_ARGS "${LOBBYSERVER_CORE_COMPILE_ARGS} -flto=thin") + set(LOBBYSERVER_CORE_LINKER_ARGS "${LOBBYSERVER_CORE_LINKER_ARGS} -flto=thin") +else() + set(LOBBYSERVER_CORE_COMPILE_ARGS "${LOBBYSERVER_CORE_COMPILE_ARGS} -flto") + set(LOBBYSERVER_CORE_LINKER_ARGS "${LOBBYSERVER_CORE_LINKER_ARGS} -flto") +endif() + +endif() + +set(_LOBBYSERVER_CORE_WANTED_SANITIZERS "") + +if("asan" IN_LIST LOBBYSERVER_BUILD_FEATURES) + # Error if someone's trying to mix asan and tsan together, + # they aren't compatible. + if("tsan" IN_LIST LOBBYSERVER_BUILD_FEATURES) + message(FATAL_ERROR "ASAN and TSAN cannot be used together.") + endif() + + message(STATUS "Enabling ASAN because it was in LOBBYSERVER_BUILD_FEATURES") + list(APPEND _LOBBYSERVER_CORE_WANTED_SANITIZERS "address") +endif() + +if("tsan" IN_LIST LOBBYSERVER_BUILD_FEATURES) + if("asan" IN_LIST LOBBYSERVER_BUILD_FEATURES) + message(FATAL_ERROR "ASAN and TSAN cannot be used together.") + endif() + + message(STATUS "Enabling TSAN because it was in LOBBYSERVER_BUILD_FEATURES") + list(APPEND _LOBBYSERVER_CORE_WANTED_SANITIZERS "thread") +endif() + +if("ubsan" IN_LIST LOBBYSERVER_BUILD_FEATURES) + message(STATUS "Enabling UBSAN because it was in LOBBYSERVER_BUILD_FEATURES") + list(APPEND _LOBBYSERVER_CORE_WANTED_SANITIZERS "undefined") +endif() + +list(LENGTH _LOBBYSERVER_CORE_WANTED_SANITIZERS _LOBBYSERVER_CORE_WANTED_SANITIZERS_LENGTH) +if(NOT _LOBBYSERVER_CORE_WANTED_SANITIZERS_LENGTH EQUAL 0) + list(JOIN _LOBBYSERVER_CORE_WANTED_SANITIZERS "," _LOBBYSERVER_CORE_WANTED_SANITIZERS_ARG) + message(STATUS "Enabled sanitizers: ${_LOBBYSERVER_CORE_WANTED_SANITIZERS_ARG}") + set(LOBBYSERVER_CORE_COMPILE_ARGS "${LOBBYSERVER_CORE_COMPILE_ARGS} -fsanitize=${_COLLABVM_CORE_WANTED_SANITIZERS_ARG}") + set(LOBBYSERVER_CORE_LINKER_ARGS "${LOBBYSERVER_CORE_LINKER_ARGS} -fsanitize=${_COLLABVM_CORE_WANTED_SANITIZERS_ARG}") +endif() + +# Set core CMake toolchain variables so that they get applied to all projects. +# A bit nasty, but /shrug, this way our third party libraries can be mostly sanitized/etc as well. +# -g3 is temporary for release build deployment testing + +set(CMAKE_C_FLAGS "${LOBBYSERVER_CORE_COMPILE_ARGS}") +set(CMAKE_CXX_FLAGS "${LOBBYSERVER_CORE_COMPILE_ARGS}") + +set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS} -O0 -g3") +set(CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS} -O3 -g3") +set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS} -O3 -g3") + +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS} -O0 -g3") +set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS} -O3 -g3") +set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS} -O3 -g3") + +set(CMAKE_EXE_LINKER_FLAGS "${LOBBYSERVER_CORE_LINKER_ARGS} -Wl,-z,noexecstack,-z,relro,-z,now") diff --git a/cmake/Policies.cmake b/cmake/Policies.cmake new file mode 100644 index 0000000..d7a084b --- /dev/null +++ b/cmake/Policies.cmake @@ -0,0 +1,22 @@ +# CMake policy configuration + +# Macro to enable new CMake policy. +# Makes this file a *LOT* shorter. +macro (_new_cmake_policy policy) + if(POLICY ${policy}) + #message(STATUS "Enabling new policy ${policy}") + cmake_policy(SET ${policy} NEW) + endif() +endmacro() + +_new_cmake_policy(CMP0026) # CMake 3.0: Disallow use of the LOCATION property for build targets. +_new_cmake_policy(CMP0042) # CMake 3.0+ (2.8.12): MacOS "@rpath" in target's install name +_new_cmake_policy(CMP0046) # warn about non-existent dependencies +_new_cmake_policy(CMP0048) # CMake 3.0+: project() command now maintains VERSION +_new_cmake_policy(CMP0054) # CMake 3.1: Only interpret if() arguments as variables or keywords when unquoted. +_new_cmake_policy(CMP0056) # try_compile() linker flags +_new_cmake_policy(CMP0066) # CMake 3.7: try_compile(): use per-config flags, like CMAKE_CXX_FLAGS_RELEASE +_new_cmake_policy(CMP0067) # CMake 3.8: try_compile(): honor language standard variables (like C++11) +_new_cmake_policy(CMP0068) # CMake 3.9+: `RPATH` settings on macOS do not affect `install_name`. +_new_cmake_policy(CMP0075) # CMake 3.12+: Include file check macros honor `CMAKE_REQUIRED_LIBRARIES` +_new_cmake_policy(CMP0077) # CMake 3.13+: option() honors normal variables. diff --git a/cmake/ProjectFuncs.cmake b/cmake/ProjectFuncs.cmake new file mode 100644 index 0000000..e69af03 --- /dev/null +++ b/cmake/ProjectFuncs.cmake @@ -0,0 +1,24 @@ +function(lobbyserver_target target) + target_compile_definitions(${target} PRIVATE "$<$:LOBBYSERVER_DEBUG>") + #target_include_directories(${target} PRIVATE ${PROJECT_SOURCE_DIR}) + target_compile_features(${target} PRIVATE cxx_std_20) + target_include_directories(${target} PRIVATE ${PROJECT_SOURCE_DIR}/lib ${CMAKE_CURRENT_BINARY_DIR}) +endfunction() + +function(lobbyserver_set_alternate_linker) + find_program(LINKER_EXECUTABLE ld.${LOBBYSERVER_LINKER} ${HOLGOL_LINKER}) + if(LINKER_EXECUTABLE) + message(STATUS "Using ${LOBBYSERVER_LINKER} as linker") + else() + message(FATAL_ERROR "Linker ${LOBBYSERVER_LINKER} does not exist on your system. Please specify one which does or omit this option from your configure command.") + endif() +endfunction() + + +# Set a default linker if the user never provided one. +# This defaults based on the detected compiler to the "best" linker possible +if(NOT LOBBYSERVER_LINKER AND "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + set(LOBBYSERVER_LINKER "lld") +elseif(NOT LOBBYSERVER_LINKER) + set(LOBBYSERVER_LINKER "bfd") +endif() diff --git a/doc/init.sql b/doc/init.sql new file mode 100644 index 0000000..24b1454 --- /dev/null +++ b/doc/init.sql @@ -0,0 +1,7 @@ +-- This SQL file creates the SSX3LobbyServer database on a MariaDB database server (preferably local). +-- Please change the password. DO NOT run this without doing so. +SET old_passwords=0; +CREATE USER 'lobbyserver' IDENTIFIED BY 'pa55w0rd1234'; +CREATE DATABASE lobbyserver CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci'; +GRANT ALL PRIVILEGES ON lobbyserver.* TO 'lobbyserver'; +FLUSH PRIVILEGES; diff --git a/doc/lobbyserver.service b/doc/lobbyserver.service new file mode 100644 index 0000000..6332ea9 --- /dev/null +++ b/doc/lobbyserver.service @@ -0,0 +1,41 @@ +[Unit] +Description=SSX 3 Lobby Server +Wants=network-online.target +After=network-online.target + +[Service] +# TODO: Import code so we can do Type=notify? +Type=simple + +Restart=always +RestartSec=10 + +User=lobbyserver +Group=lobbyserver + +WorkingDirectory=/srv/lobbyserver +ExecStart=/srv/lobbyserver/lobbyserver + +# limits +OOMScoreAdjust=200 +CPUQuota=100% +IOWeight=50 +# ram limits, may be amended at some point +# but i don't think the server will be *that* much of a hog +MemoryHigh=820M +MemoryMax=1G + +# Hardening +PrivateTmp=true +NoNewPrivileges=true +RestrictNamespaces=true +ProtectSystem=strict +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +PrivateDevices=yes +RestrictSUIDSGID=true + +[Install] +WantedBy=multi-user.target + diff --git a/doc/lobbyserver.toml b/doc/lobbyserver.toml new file mode 100644 index 0000000..0d84dee --- /dev/null +++ b/doc/lobbyserver.toml @@ -0,0 +1,16 @@ +# This is the SSX3LobbyServer configuration file. + +[lobbyserver] +listen_address = "127.0.0.1" + +# Listen ports. +buddy_listen_port = 10998 +lobby_listen_port = 11000 + +# Database configuration. This is required to run the service. +[lobbyserver.database] +host = "/run/mysqld/mysqld.sock" +user = "lobbyserver" +# Change this! +password = "pa55w0rd1234" +database = "lobbyserver" diff --git a/doc/notes.md b/doc/notes.md new file mode 100644 index 0000000..e69de29 diff --git a/lib/base/CMakeLists.txt b/lib/base/CMakeLists.txt new file mode 100644 index 0000000..5b8f87a --- /dev/null +++ b/lib/base/CMakeLists.txt @@ -0,0 +1,18 @@ +add_library(base_base + assert.cpp + backoff.cpp + html_escape.cpp + + # logging library + logger.cpp + stdout_sink.cpp +) + +lobbyserver_target(base_base) + +target_link_libraries(base_base PUBLIC + # techinically not needed anymore but /shrug + base::impl +) + +add_library(base::base ALIAS base_base) diff --git a/lib/base/assert.cpp b/lib/base/assert.cpp new file mode 100644 index 0000000..dfeab8b --- /dev/null +++ b/lib/base/assert.cpp @@ -0,0 +1,14 @@ +#include +#include +#include + +namespace base { + + static Logger gAssertLogger{MakeChannelId(MessageSource::Base, MessageComponentSource::Base_Assertions)}; + + [[noreturn]] void ExitMsg(const char* message) { + gAssertLogger.Fatal("{}", message); + std::exit(0xAF); + } + +} // namespace base diff --git a/lib/base/assert.hpp b/lib/base/assert.hpp new file mode 100644 index 0000000..8b77814 --- /dev/null +++ b/lib/base/assert.hpp @@ -0,0 +1,39 @@ +//! Support Library Assert Wrappers +//! +//! The Support Library uses its own assertion system which is more flexible than the +//! standard C library's assertion macros. +//! +//! They are not intended to be directly compatible with some of the quirks +//! the Standard C library allows (like using assert() as an expression). +//! +//! They are: +//! - BASE_ASSERT() +//! - active in debug builds and removed on release +//! - BASE_CHECK() +//! - always active, even in release builds + +#pragma once + +#include + +namespace base { + [[noreturn]] void ExitMsg(const char* message); +} // namespace base + +#ifdef BASE_DEBUG + #define BASE_ASSERT(expr, fmt, ...) \ + if(!(expr)) [[unlikely]] { \ + auto msg = std::format("Assertion \"{}\" @ {}:{} failed with message: {}", #expr, __FILE__, __LINE__, std::format(fmt, ##__VA_ARGS__)); \ + ::base::ExitMsg(msg.c_str()); \ + __builtin_unreachable(); \ + } +#else + #define BASE_ASSERT(expr, format, ...) +#endif + +#define BASE_CHECK(expr, fmt, ...) \ + if(!(expr)) [[unlikely]] { \ + auto msg = std::format("Check \"{}\" @ {}:{} failed with message: {}", #expr, __FILE__, __LINE__, std::format(fmt, ##__VA_ARGS__)); \ + ::base::ExitMsg(msg.c_str()); \ + __builtin_unreachable(); \ + } diff --git a/lib/base/async_condition_variable.hpp b/lib/base/async_condition_variable.hpp new file mode 100644 index 0000000..b65c5a9 --- /dev/null +++ b/lib/base/async_condition_variable.hpp @@ -0,0 +1,31 @@ +#pragma once +#include + +namespace base { + + /// An asynchronous version of std::condition_variable. + /// Useful for synchronizing or pausing coroutines. + struct AsyncConditionVariable { + AsyncConditionVariable(asio::any_io_executor exec) : timer(exec) { timer.expires_at(std::chrono::steady_clock::time_point::max()); } + + void NotifyOne() { timer.cancel_one(); } + + void NotifyAll() { timer.cancel(); } + + template + Awaitable Wait(Predicate pred) { + while(!pred()) { + try { + co_await timer.async_wait(asio::deferred); + } catch(...) { + // swallow errors + } + } + co_return; + } + + private: + SteadyTimer timer; + }; + +} // namespace base diff --git a/lib/base/backoff.cpp b/lib/base/backoff.cpp new file mode 100644 index 0000000..e980fd2 --- /dev/null +++ b/lib/base/backoff.cpp @@ -0,0 +1,13 @@ +#include +#include +#include + +namespace base { + + Awaitable Backoff::Delay() { + const auto t = std::pow(base, count++); + auto timer = asio::steady_timer { co_await asio::this_coro::executor, std::chrono::seconds(static_cast(t)) }; + co_await timer.async_wait(asio::deferred); + } + +} // namespace base diff --git a/lib/base/backoff.hpp b/lib/base/backoff.hpp new file mode 100644 index 0000000..a8c79d8 --- /dev/null +++ b/lib/base/backoff.hpp @@ -0,0 +1,20 @@ +#pragma once +#include + +namespace base { + + /// Exponential backoff implementation. + /// Backoff time is calculated using the classicial t = bᶜ formula. + struct Backoff { + explicit constexpr Backoff(double base = 2.) : base(base) {}; + + Awaitable Delay(); + + inline void Reset() { count = 0; } + + private: + double base {}; + u32 count {}; + }; + +} // namespace base diff --git a/lib/base/channel.hpp b/lib/base/channel.hpp new file mode 100644 index 0000000..5e1296c --- /dev/null +++ b/lib/base/channel.hpp @@ -0,0 +1,120 @@ + +//! a wrapper over Boost.Asio-provided channel facilities to give it a slightly saner API +//! that we would probably prefer instead of having to manually dick with completion handlers +//! and all that. this is a alternative to asiochan because it seems to be iffy and possibly +//! broken when dealing with multiple threads, causing empty coroutine frames to be generated +//! which cause crashes very quickly. (that actually wasn't because of asiochan, but, +//! using the Asio provided facilities has seemed better anyhow) + +#pragma once +#include +#include +#include + +namespace base { + + // n.b: we only support one signature here. the asio one supports Several but that seems + // unneeded for our use case (and we can use variants to encode any states in a far less.. jagged, + // shall we say, fashion) + // + // also, concurrent_channel is used here because we can be used in multithreaded contexts, often + // for synchronization/message passing between threads in a safe fashion (without having to post back/forth executors) + template + using ChannelImplType = asio::experimental::concurrent_channel; + + template + struct Channel { + Channel(asio::any_io_executor exec, usize sendQueueLen = 0) : exec(exec), channel(exec, sendQueueLen) {} + + Awaitable Write(const Send& value) { co_await channel.async_send(bsys::error_code {}, value, asio::deferred); } + + Awaitable Read() { + // BASE_ASSERT(channel.is_open() == true, "cant really do that with a closed channel now can you"); + co_return co_await channel.async_receive(asio::deferred); + } + + bool IsOpen() const { return channel.is_open(); } + + Awaitable Close() { + channel.close(); + co_return; + } + + /// get the raw ASIO channel type. Used in the worker thread pool. + auto& Raw() { return channel; } + + private: + asio::any_io_executor exec; + ChannelImplType channel; + }; + + template <> + struct Channel { + Channel(asio::any_io_executor exec) : exec(exec), channel(exec) {} + + Awaitable Write() { co_await channel.async_send(bsys::error_code {}, asio::deferred); } + + Awaitable Read() { + // BASE_ASSERT(channel.is_open() == true, "cant really do that with a closed channel now can you"); + co_await channel.async_receive(asio::deferred); + co_return; + } + + bool IsOpen() const { return channel.is_open(); } + + Awaitable Close() { + channel.close(); + co_return; + } + + auto& Raw() { return channel; } + + private: + asio::any_io_executor exec; + ChannelImplType channel; + }; + + // Channel adapters to make producer/consumer logic *way* more typesafe. + + template + struct ReadChannel { + // N.B: This is not `explicit` by design, to allow implicitly "downgrading" + // a full duplex channel into a read-only channel (or write-only in the case of + // WriteChannel) + ReadChannel(Channel& chan) : chan(chan) {} + + bool IsOpen() const { return chan.IsOpen(); } + + Awaitable Read() { return chan.Read(); } + + private: + Channel& chan; + }; + + template + struct WriteChannel { + WriteChannel(Channel& chan) : chan(chan) {} + + bool IsOpen() const { return chan.IsOpen(); } + + Awaitable Write(const T& val) { co_await chan.Write(val); } + + private: + Channel& chan; + }; + + // A little bit depressing that this can't be sfinae'd away + // (or `requires`'d away), but oh well + template <> + struct WriteChannel { + WriteChannel(Channel& chan) : chan(chan) {} + + bool IsOpen() const { return chan.IsOpen(); } + + Awaitable Write() { co_await chan.Write(); } + + private: + Channel& chan; + }; + +} // namespace base diff --git a/lib/base/fixed_string.hpp b/lib/base/fixed_string.hpp new file mode 100644 index 0000000..e7577fa --- /dev/null +++ b/lib/base/fixed_string.hpp @@ -0,0 +1,27 @@ +#pragma once +#include + +namespace base { + + /// A compile time fixed string, fit for usage as a C++20 cNTTP. + template + struct FixedString { + char buf[N + 1] {}; + + constexpr FixedString(const char* s) { // NOLINT + for(unsigned i = 0; i != N; ++i) + buf[i] = s[i]; + } + + constexpr operator const char*() const { // NOLINT + return buf; + } + + [[nodiscard]] constexpr usize Length() const { + return N; + } + }; + + template + FixedString(char const (&)[N]) -> FixedString; +} // namespace base \ No newline at end of file diff --git a/lib/base/fourcc.hpp b/lib/base/fourcc.hpp new file mode 100644 index 0000000..48cb320 --- /dev/null +++ b/lib/base/fourcc.hpp @@ -0,0 +1,32 @@ +#pragma once +#include +#include + +namespace base { + + /// Type system magic + enum class FourCC32_t : u32 {}; + + template + consteval FourCC32_t FourCC32() { + static_assert(fccString.Length() == 4, "Provided string is not a FourCC"); + + switch(Endian) { + case std::endian::little: + return static_cast((fccString[0]) | (fccString[1] << 8) | (fccString[2] << 16) | (fccString[3] << 24)); + + case std::endian::big: + return static_cast((fccString[0] << 24) | (fccString[1] << 16) | (fccString[2] << 8) | fccString[3]); + + // endian::native is practically implemented in most standard libraries + // by aliasing the native endian enumerator, so that it will match + // one of the two cases here. therefore this code is literally useless + // and i have no idea why i even wrote it 4 years ago :') + //default: + // throw "Invalid endian provided? How'd you do that?"; // NOLINT + } + } + + // TODO: 64-bit version which returns a u64 (if required?) + +} // namespace base \ No newline at end of file diff --git a/lib/base/html_escape.cpp b/lib/base/html_escape.cpp new file mode 100644 index 0000000..b6361f8 --- /dev/null +++ b/lib/base/html_escape.cpp @@ -0,0 +1,61 @@ +//! See [Houdini](https://github.com/vmg/houdini/blob/master/houdini_html_e.c) for where this code +//! originally came from. +#include + +// clang-format off +constexpr static u8 HTML_ESCAPE_TABLE[] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 2, 3, 0, 0, 0, 0, 0, 0, 0, 4, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 6, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +}; + +constexpr static std::string_view HTML_ESCAPES[] = { + "", + """, + "&", + "'", + "/", + "<", + ">" +}; +// clang-format on + +namespace base { + std::string HtmlEscape(const std::span input) { + std::string out; + + // TODO: rewrite this underlying implementation to be + // UTF-8 safe, so we can allow UTF-8 input. + + for(auto& c : input) { + const u8 escape = HTML_ESCAPE_TABLE[c]; + if(escape == 0) { + out.push_back(c); + } else { + out += HTML_ESCAPES[escape]; + } + } + return out; + } + + std::string HtmlEscape(const std::string_view input) { + return HtmlEscape(std::span { std::bit_cast(input.data()), input.length() }); + } + + std::string HtmlEscape(const std::string& input) { + return HtmlEscape(std::span { std::bit_cast(input.data()), input.length() }); + } +} // namespace base diff --git a/lib/base/html_escape.hpp b/lib/base/html_escape.hpp new file mode 100644 index 0000000..0a0b665 --- /dev/null +++ b/lib/base/html_escape.hpp @@ -0,0 +1,13 @@ +#include + +namespace base { + + std::string HtmlEscape(const std::span buffer); + + + std::string HtmlEscape(const std::string_view); + + /// HTML-escape a string. + std::string HtmlEscape(const std::string& input); + +} // namespace base diff --git a/lib/base/logger.cpp b/lib/base/logger.cpp new file mode 100644 index 0000000..2c21089 --- /dev/null +++ b/lib/base/logger.cpp @@ -0,0 +1,172 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace base { + + namespace logger_detail { + + static constexpr std::string_view SeverityToString(MessageSeverity sev) { + // This must match order of logger_detail::MessageSeverity. + const char* MessageSeverityStringTable[] = { "Debug", "Info", "Warn", "Error", "Fatal" }; + return MessageSeverityStringTable[static_cast(sev)]; + } + + constexpr static auto ChannelToString(ChannelId channelId) -> std::string_view { + MessageSource src; + MessageComponentSource comp; + + ChannelIdToComponents(channelId, src, comp); + + switch(src) { + case MessageSource::Main: { + switch(comp) { + case MessageComponentSource::Main_Global: return "Main::Global"; + default: BASE_CHECK(false, "Invalid component source"); + } + }; + + case MessageSource::Base: { + switch(comp) { + case MessageComponentSource::Base_Assertions: return "Base::Assert"; + default: BASE_CHECK(false, "Invalid component source"); + } + }; + + case MessageSource::Http: { + switch(comp) { + case MessageComponentSource::Http_Server: return "HTTP::Server"; + case MessageComponentSource::Http_WebSocketClient: return "HTTP::WebSocketClient"; + default: BASE_CHECK(false, "Invalid component source"); + } + }; + + case MessageSource::Server: { + switch(comp) { + case MessageComponentSource::Server_Server: return "LobbyServer2::Server"; + default: BASE_CHECK(false, "Invalid component source"); + } + }; + + default: BASE_CHECK(false, "Invalid channel source {:04x}", static_cast(src)); + } + } + + struct LoggerThreadData { + // Logger thread stuff + std::thread loggerThread; + std::mutex logQueueMutex; + std::condition_variable logQueueCv; + std::deque logQueue; + + bool logThreadShutdown = false; + + // N.B: This is stored/cached here just because it really does + // *NOT* need to be in a hot path. + const std::chrono::time_zone* timeZone = std::chrono::current_zone(); + + bool ShouldUnblock() { + // Always unblock if the logger thread needs to be shut down. + if(logThreadShutdown) + return true; + + return !logQueue.empty(); + } + + void PushMessage(MessageData&& md) { + { + std::unique_lock lk(logQueueMutex); + logQueue.emplace_back(std::move(md)); + } + logQueueCv.notify_one(); + } + }; + + Unique threadData; + + void LoggerGlobalState::LoggerThread() { + auto& self = The(); + + // Fancy thread names. + pthread_setname_np(pthread_self(), "LoggerThread"); + + while(true) { + std::unique_lock lk(threadData->logQueueMutex); + if(threadData->logQueue.empty()) { + // Await for messages. + threadData->logQueueCv.wait(lk, []() { return threadData->ShouldUnblock(); }); + } + + // Flush the logger queue until there are no more messages. + while(!threadData->logQueue.empty()) { + self.OutputMessage(threadData->logQueue.back()); + threadData->logQueue.pop_back(); + } + + // Shutdown if requested. + if(threadData->logThreadShutdown) + break; + } + } + + LoggerGlobalState& LoggerGlobalState::The() { + static LoggerGlobalState storage; + return storage; + } + + LoggerGlobalState::LoggerGlobalState() { + // Spawn the logger thread + threadData = std::make_unique(); + threadData->loggerThread = std::thread(&LoggerGlobalState::LoggerThread); + } + + LoggerGlobalState::~LoggerGlobalState() { + // Shut down the logger thread + threadData->logThreadShutdown = true; + threadData->logQueueCv.notify_all(); + threadData->loggerThread.join(); + } + + void LoggerGlobalState::AttachSink(Sink& sink) { + sinks.push_back(&sink); + } + + void LoggerGlobalState::OutputMessage(const MessageData& data) { + // give up early if no sinks are attached + if(sinks.empty()) + return; + + if(data.severity < logLevel) + return; + + auto formattedLoggerMessage = + std::format("[{:%F %H:%M:%S}|{}|{}] {}", std::chrono::floor(threadData->timeZone->to_local(data.time)), + SeverityToString(data.severity), ChannelToString(data.channelId), data.message); + + for(auto sink : sinks) + sink->OutputMessage(formattedLoggerMessage); + } + + } // namespace logger_detail + + Logger& Logger::Global() { + static Logger globalLogger; + return globalLogger; + } + + void Logger::VOut(MessageSeverity severity, std::string_view format, std::format_args args) { + logger_detail::MessageData data { + .time = std::chrono::system_clock::now(), .severity = severity, .channelId = channelId, .message = std::vformat(format, args) + }; + + // Push data into logger thread. + logger_detail::threadData->PushMessage(std::move(data)); + } + +} // namespace base diff --git a/lib/base/logger.hpp b/lib/base/logger.hpp new file mode 100644 index 0000000..6f678f9 --- /dev/null +++ b/lib/base/logger.hpp @@ -0,0 +1,212 @@ +//! Logging utilities for the Support Library +//! Using Standard C++ +#pragma once + +#include +#include +#include +#include +#include + +namespace base { + + namespace logger_detail { + + enum class MessageSeverity { Debug, Info, Warning, Error, Fatal }; + + /// A message source. In our tree, this is essentially defined as + /// each library the server's made up of. + enum class MessageSource : u16 { Main = 0x1000, Base, Http, Server }; + + /// A component source. This basically defines "what" part of a source + /// is creating the message. For instance, the HTTP library has multiple + /// components which can each log messages. + enum class MessageComponentSource : u16 { + // The "default" global logger channel + Main_Global = 0x1000, + + Base_Assertions = 0x10, + + Http_Server = 0x50, + Http_Router, + Http_WebSocketClient, + + Server_Server = 0x60, + }; + + /// A channel ID. `enum class`es are used to avoid confusion with a normal u32, + /// and to also add addional interface type safety. + /// + /// A channel ID looks something like: + /// ssssssss ssssssss cccccccc cccccccc + /// + /// Where: + /// s: message source + /// c: message component source + enum class ChannelId : u32 {}; + + /// Create a channel ID from a source and component source. + constexpr static ChannelId MakeChannelId(MessageSource source, MessageComponentSource component) { + auto srcbits = std::bit_cast(source); + auto cmpbits = std::bit_cast(component); + + return static_cast((static_cast(srcbits) << 16 | cmpbits)); + } + + /// Splits a channel ID into its individual components. + constexpr static void ChannelIdToComponents(ChannelId id, MessageSource& src, MessageComponentSource& component) { + src = static_cast((static_cast(id) & 0xffff0000) >> 16); + component = static_cast(static_cast(id) & 0x0000ffff); + } + + /// The default global channel ID. + constexpr static auto ChannelGlobal = MakeChannelId(MessageSource::Main, MessageComponentSource::Main_Global); + + /// Message data. This is only used by logger sinks. + struct MessageData { + std::chrono::system_clock::time_point time; + MessageSeverity severity; + + ChannelId channelId; // the channel ID. + + std::string message; // DO NOT SET THIS, IT WILL BE OVERWRITTEN AND I WILL BE VERY SAD -lily + }; + + /// A logger sink. Outputs messages to some device (a TTY), a file, + /// what have you. Basically a interface for the logger to spit stuff out. + /// + /// # Notes + /// Sinks do not run on the main application thread. Instead, they run on a + /// single Support Library internal thread which the only purpose of is to + /// stream logger messages and output them, from a internally managed queue. + /// + /// This is techinically a implementation detail, but for implementers of logger + /// sinks it's probably a good idea for them to know just in case. + /// + /// Do note that you probably don't have to actually do any thread-safety magic + /// in your sink implementation, since it's only a single thread/etc... + struct Sink { + virtual void OutputMessage(std::string_view message) = 0; + }; + + /// Shared global state all loggers use. + struct LoggerGlobalState { + static LoggerGlobalState& The(); + + void AttachSink(Sink& sink); + + void OutputMessage(const MessageData& data); + + /// Get the current log level. + MessageSeverity GetLogLevel() const { return logLevel; } + + /// Set the current log level. + void SetLogLevel(MessageSeverity newLogLevel) { logLevel = newLogLevel; } + + private: + LoggerGlobalState(); + ~LoggerGlobalState(); + + std::vector sinks; + MessageSeverity logLevel { MessageSeverity::Info }; + + static void LoggerThread(); + }; + + inline auto& GlobalState() { + return LoggerGlobalState::The(); + } + + } // namespace logger_detail + + using logger_detail::ChannelId; + using logger_detail::MessageComponentSource; + using logger_detail::MessageSource; + + using logger_detail::MakeChannelId; + + /// Attach a sink to all Support loggers; allowing it to output logger messages. + inline void AttachSink(logger_detail::Sink& sink) { + logger_detail::GlobalState().AttachSink(sink); + } + + inline logger_detail::MessageSeverity GetLogLevel() { + return logger_detail::GlobalState().GetLogLevel(); + } + + inline void SetLogLevel(logger_detail::MessageSeverity newLevel) { + return logger_detail::GlobalState().SetLogLevel(newLevel); + } + + struct Logger { + using MessageSeverity = logger_detail::MessageSeverity; + using MessageData = logger_detail::MessageData; + using Sink = logger_detail::Sink; + + /// Get the global instance of the logger. + static Logger& Global(); + + Logger() : Logger(logger_detail::ChannelGlobal) {} + + constexpr explicit Logger(ChannelId channel) { this->channelId = channel; } + + Logger(const Logger&) = delete; + Logger(Logger&&) = delete; + + template + inline void Debug(std::string_view fmt, Args... args) { + VOut(MessageSeverity::Debug, fmt, std::make_format_args(std::forward(args)...)); + } + + template + inline void Info(std::string_view fmt, Args... args) { + VOut(MessageSeverity::Info, fmt, std::make_format_args(std::forward(args)...)); + } + + template + inline void Warning(std::string_view fmt, Args... args) { + VOut(MessageSeverity::Warning, fmt, std::make_format_args(std::forward(args)...)); + } + + template + inline void Error(std::string_view fmt, Args... args) { + VOut(MessageSeverity::Error, fmt, std::make_format_args(std::forward(args)...)); + } + + template + inline void Fatal(std::string_view fmt, Args... args) { + VOut(MessageSeverity::Fatal, fmt, std::make_format_args(std::forward(args)...)); + } + + private: + void VOut(MessageSeverity severity, std::string_view format, std::format_args args); + + ChannelId channelId; + }; + + template + constexpr void LogDebug(std::string_view format, Args... args) { + Logger::Global().Debug(format, std::forward(args)...); + } + + template + constexpr void LogInfo(std::string_view format, Args... args) { + Logger::Global().Info(format, std::forward(args)...); + } + + template + constexpr void LogWarning(std::string_view format, Args... args) { + Logger::Global().Warning(format, std::forward(args)...); + } + + template + constexpr void LogError(std::string_view format, Args... args) { + Logger::Global().Error(format, std::forward(args)...); + } + + template + constexpr void LogFatal(std::string_view format, Args... args) { + Logger::Global().Fatal(format, std::forward(args)...); + } + +} // namespace base diff --git a/lib/base/network_order.hpp b/lib/base/network_order.hpp new file mode 100644 index 0000000..e126dc6 --- /dev/null +++ b/lib/base/network_order.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +namespace base { + + // required until C++23 support :( + namespace detail { + + template + constexpr T PunCast(U v) { + return *std::bit_cast(&v); + } + + template + constexpr T ByteSwap(T v) { + if constexpr(sizeof(T) == 2) + return static_cast(__builtin_bswap16(PunCast(v))); + else if constexpr(sizeof(T) == 4) + return static_cast(__builtin_bswap32(PunCast(v))); + else if constexpr(sizeof(T) == 8) + return static_cast(__builtin_bswap64(PunCast(v))); + + return v; + } + + } // namespace detail + + /// A network-order field, swapped automatically + template + struct [[gnu::packed]] NetworkOrder { + constexpr NetworkOrder() = default; + constexpr NetworkOrder(T v) : field(SwapIfRequired(v)) {} + + operator T() const { return SwapIfRequired(field); } + + private: + constexpr static T SwapIfRequired(T value) { + if constexpr(std::endian::native == std::endian::little) + return detail::ByteSwap(value); + return value; + } + + T field; + }; +} // namespace base diff --git a/lib/base/rate_limit.hpp b/lib/base/rate_limit.hpp new file mode 100644 index 0000000..d5b5ee2 --- /dev/null +++ b/lib/base/rate_limit.hpp @@ -0,0 +1,98 @@ +#pragma once + +#include +#include + +namespace base { + + template + struct BasicRateLimiter final { + using TimePointType = std::chrono::time_point; + using EventGrainType = std::uint64_t; + + /// (mostly) Opaque per-object state. + struct State { + [[nodiscard]] bool CoolingDown() const noexcept { return coolingDown.load(); } + + private: + friend struct BasicRateLimiter; + + std::atomic_bool coolingDown {}; + + /// Time point of when last event was taken. + std::atomic lastEvent {}; + + /// Current event count + std::atomic eventCount {}; + }; + + template + constexpr BasicRateLimiter(EventGrainType maxEvents, Dur2 maxRate, Dur3 cooldownTime) noexcept + : cooldownTime(cooldownTime), maxRate(maxRate), maxEventCount(maxEvents) {} + + // Disallow copying, but allow movement, if so desired. + constexpr BasicRateLimiter(const BasicRateLimiter&) = delete; + constexpr BasicRateLimiter(BasicRateLimiter&&) noexcept = default; + + /// Try and take a single event, possibly activating the rate limit. + [[nodiscard]] constexpr bool TryTakeEvent(State& state) const noexcept { return TryTakeEvents(state, 1); } + + /// Try and take events, possibly activating the rate limit. + [[nodiscard]] bool TryTakeEvents(State& state, EventGrainType nrEvents) const noexcept { + // Pre-calculate the current time & the delta time that has + // elapsed since we last entered this function. + // + // This doesn't speed things up per se, but it probably aides + // the compiler a bit to optimize things a bit better. + + const auto now = std::chrono::time_point_cast(Clock::now()); + const auto elapsedSinceLastEvent = (now - state.lastEvent.load()); + + if(state.coolingDown.load()) { + // Check if we have passed the cool-down time (if we're cooling down). + // If we have, we can let the cooldown go, and let the state take the + // events. + if(elapsedSinceLastEvent >= cooldownTime && state.coolingDown.load()) + state.coolingDown.store(false); + else + return false; + } + + if(elapsedSinceLastEvent < maxRate) { + // Check if the event count has went past [maxEventCount]/[maxRate]. + // If it has, we start the cool-down process. + if((state.eventCount += nrEvents) >= maxEventCount) { + state.coolingDown.store(true); + state.lastEvent.store(now); + return false; + } + } else { + // The event happened far after max rate, so it's probably fine. + state.eventCount = 0; + state.lastEvent.store(now); + } + + return true; + } + + constexpr void SetMaxEventCount(EventGrainType count) noexcept { maxEventCount = count; } + + template + constexpr void SetMaxRate(Dur2 dur) noexcept { + maxRate = dur; + } + + template + constexpr void SetCooldownTime(Dur2 dur) noexcept { + cooldownTime = dur; + } + + private: + Dur cooldownTime; + Dur maxRate; + EventGrainType maxEventCount; + }; + + using RateLimiter = BasicRateLimiter<>; + +} // namespace base diff --git a/lib/base/stdout_sink.cpp b/lib/base/stdout_sink.cpp new file mode 100644 index 0000000..66e6e7c --- /dev/null +++ b/lib/base/stdout_sink.cpp @@ -0,0 +1,18 @@ +#include + +namespace base { + StdoutLoggerSink& StdoutLoggerSink::The() { + static StdoutLoggerSink sink; + return sink; + } + + void StdoutLoggerSink::OutputMessage(std::string_view message) { + fputs(message.data(), stdout); + fputc('\n', stdout); + fflush(stdout); + } + + void LoggerAttachStdout() { + AttachSink(StdoutLoggerSink::The()); + } +} // namespace base diff --git a/lib/base/stdout_sink.hpp b/lib/base/stdout_sink.hpp new file mode 100644 index 0000000..437cf42 --- /dev/null +++ b/lib/base/stdout_sink.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace base { + + /// A logger sink implementation that prints to standard output. + struct StdoutLoggerSink : public Logger::Sink { + static StdoutLoggerSink& The(); + + void OutputMessage(std::string_view message) override; + }; + + /// Attach the stdout logger sink to the global Lucore logger. + void LoggerAttachStdout(); + +} // namespace base diff --git a/lib/base/types.hpp b/lib/base/types.hpp new file mode 100644 index 0000000..934eae4 --- /dev/null +++ b/lib/base/types.hpp @@ -0,0 +1,145 @@ +//! Core types and includes +#pragma once + +#include +#include + +// these are in the global namespace since most libraries +// won't try defining anything like this in the global namespace +// (and I'd like these types to be used globally a lot more anyways) +using u8 = std::uint8_t; +using i8 = std::int8_t; +using u16 = std::uint16_t; +using i16 = std::int16_t; +using u32 = std::uint32_t; +using i32 = std::int32_t; +using u64 = std::uint64_t; +using i64 = std::int64_t; +using usize = std::size_t; +using isize = std::intptr_t; + +// Include the impl library's ASIO config, since we pull it in +#include + +namespace boost::json {} +namespace boost::urls {} + +namespace burl = boost::urls; + +// Namespace aliases, these are used throughout the project +namespace bsys = boost::system; +namespace json = boost::json; + +namespace std::filesystem {} +namespace fs = std::filesystem; + +namespace base { + + namespace detail { + + template + struct Point { + T x; + T y; + }; + + template + struct Size { + T width; + T height; + + constexpr usize Linear() const { return width * height; } + }; + + template + struct Rect { + T x; + T y; + T width; + T height; + + // constexpr Rect(T x, T y, T w, T h) : x(x), y(y), width(w), height(h) {} + + // constexpr Rect() = default; + + // constexpr explicit Rect(Size size) : x(0), y(0), width(size.width), height(size.height) {} + + /** + * Get the origin coordinate as a point. + * \return a Point with the origin. + */ + constexpr auto GetOrigin() const { return Point { .x = x, .y = y }; } + + /** + * Get the size of this rect. + * \return a Point which contains the calculated size of the rect + */ + constexpr auto GetSize() const { return Size { .width = width, .height = height }; } + + constexpr bool InBounds(const Rect& other) { + if(x < other.x || x + other.width > other.x + other.width) + return false; + + if(y < other.y || x + other.height > other.y + other.height) + return false; + + return true; + } + + // more methods. + }; + + } // namespace detail + + union Pixel { + u32 raw; + + /// color accessors + struct { + u8 r; + u8 g; + u8 b; + u8 a; + }; + + constexpr static Pixel FromRgb565(u16 pixel) { + return Pixel { .r = static_cast(((pixel & 0xF800) >> 11) << 3), + .g = static_cast(((pixel & 0x7E0) >> 5) << 2), + .b = static_cast((pixel & 0x1F) << 3), + .a = 255 }; + } + }; + + using detail::Point; + using detail::Rect; + using detail::Size; + + template + using Ref = std::shared_ptr; + + template > + using Unique = std::unique_ptr; + + template + struct OverloadVisitor : Ts... { + using Ts::operator()...; + }; + + template + OverloadVisitor(Ts...) -> OverloadVisitor; + + template + struct UniqueCDeleter { + constexpr void operator()(T* ptr) { + if(ptr) + Free(reinterpret_cast(ptr)); + } + }; + + /// Use this for wrapping a C-allocated memory block. The defaults here assume + /// you're wrapping data allocated by malloc(), however, any deallocator pattern + /// is basically supported. + template + using CUnique = base::Unique>; + +} // namespace base diff --git a/lib/base/xoshiro.hpp b/lib/base/xoshiro.hpp new file mode 100644 index 0000000..e59e704 --- /dev/null +++ b/lib/base/xoshiro.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include // CHAR_BIT +#include +#include +#include + +namespace base { + namespace detail { + template + constexpr size_t BitSizeOf() { + return sizeof(T) * CHAR_BIT; + } + + constexpr std::uint64_t splitmix64(std::uint64_t x) { + std::uint64_t z = (x += 0x9e3779b97f4a7c15uLL); + z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9uLL; + z = (z ^ (z >> 27)) * 0x94d049bb133111ebuLL; + return z ^ (z >> 31); + } + + constexpr std::uint64_t rotl(std::uint64_t x, int k) { + return (x << k) | (x >> (BitSizeOf() - k)); + } + + struct Xoshiro256ss { + using result_type = std::uint64_t; + + std::uint64_t s[4] {}; + + constexpr explicit Xoshiro256ss() : Xoshiro256ss(0) {} + + constexpr explicit Xoshiro256ss(std::uint64_t seed) { + s[0] = splitmix64(seed); + s[1] = splitmix64(seed); + s[2] = splitmix64(seed); + s[3] = splitmix64(seed); + } + + constexpr explicit Xoshiro256ss(std::random_device& rd) { + // Get 64 bits out of the random device. + // + // This lambda is quite literal, as it fetches + // 2 iterations of the random engine, and + // shifts + OR's them into a 64bit value. + auto get_u64 = [&rd] { + std::uint64_t the_thing = rd(); + return (the_thing << 32) | rd(); + }; + + // seed with 256 bits of entropy from the random device + splitmix64 + // to ensure we seed it well, as per recommendation. + s[0] = splitmix64(get_u64()); + s[1] = splitmix64(get_u64()); + s[2] = splitmix64(get_u64()); + s[3] = splitmix64(get_u64()); + } + + static constexpr result_type min() { return 0; } + + static constexpr result_type max() { return std::uint64_t(-1); } + + constexpr result_type operator()() { + result_type result = rotl(s[1] * 5, 7) * 9; + result_type t = s[1] << 17; + s[2] ^= s[0]; + s[3] ^= s[1]; + s[1] ^= s[2]; + s[0] ^= s[3]; + s[2] ^= t; + s[3] = rotl(s[3], 45); + return result; + } + }; + } // namespace detail + + using detail::Xoshiro256ss; +} // namespace base diff --git a/lib/http/CMakeLists.txt b/lib/http/CMakeLists.txt new file mode 100644 index 0000000..2c52351 --- /dev/null +++ b/lib/http/CMakeLists.txt @@ -0,0 +1,25 @@ +# third party requirements +add_subdirectory(third_party/r3) + +add_library(base_http + server.cpp + + proxy_address.cpp + + # WebSocket stuff + websocket_client.cpp + websocket_message.cpp +) + +lobbyserver_target(base_http) + +target_link_libraries(base_http PUBLIC + base::impl + Boost::url + + # We use the R3 router library + # TODO: Actually stop exposing this as it's only a implementation detail.. + r3 +) + +add_library(base::http ALIAS base_http) diff --git a/lib/http/config.hpp b/lib/http/config.hpp new file mode 100644 index 0000000..4d7c9cd --- /dev/null +++ b/lib/http/config.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +// Enables support for reverse proxying. +// This should only be enabled if you're going to host +// behind a proxy server (including Cloudflare.). +#define BASE_HTTP_REVERSE_PROXY_SUPPORT + +// Tweaks the behavious of a few things to better adhere to Cloudflare stuff +// #define BASE_HTTP_CLOUDFLARE + +// Enables logging of HTTP requests/responses. Note that this only applies to true HTTP sessions, +// WebSocket connections will not be logged. +// +//#define BASE_HTTP_REQUEST_LOGGING + +namespace base::http { + using GenericProtocol = asio::generic::stream_protocol; + using GenericStream = BeastStream; +} // namespace base::http diff --git a/lib/http/proxy_address.cpp b/lib/http/proxy_address.cpp new file mode 100644 index 0000000..be2b6d2 --- /dev/null +++ b/lib/http/proxy_address.cpp @@ -0,0 +1,44 @@ +#include +#include +#include + +namespace base::http { + + asio::ip::address GetProxyAddress(asio::ip::address fallback, beast::http::request& req) { +#ifdef BASE_HTTP_REVERSE_PROXY_SUPPORT + auto forwarded_for = req[ + #ifndef BASE_HTTP_CLOUDFLARE + "X-Forwarded-For" + #else + "CF-Connecting-IP" + #endif + ]; + + if(forwarded_for == "") { + // if no header was provided, just return that hop + return fallback; + } else { + bsys::error_code ec; + asio::ip::address ip; + + // X-Forwarded-For is a tokenized list, where the first element is always the client hop. + // We only need to worry about the client hop. + + if(forwarded_for.find(',') != beast::string_view::npos) { + ip = asio::ip::make_address(std::string_view(forwarded_for.data(), forwarded_for.find(',')), ec); + } else { + ip = asio::ip::make_address(std::string_view(forwarded_for.data(), forwarded_for.length()), ec); + } + + // The X-Forwarded-For header is not controlled by user input (and should *not* be, with a properly written proxy server), + // so if this CHECK fires, you're probably in a bad enough situation not worth continuing anyways. + BASE_CHECK(!ec, "Invalid IP address in proxy IP header. Header: \"{}\"", + std::string_view(forwarded_for.data(), forwarded_for.length())); + return ip; + } +#else + return fallback; +#endif + } + +} // namespace base::http diff --git a/lib/http/proxy_address.hpp b/lib/http/proxy_address.hpp new file mode 100644 index 0000000..e77999d --- /dev/null +++ b/lib/http/proxy_address.hpp @@ -0,0 +1,22 @@ +#pragma once + +// clang-format off +#include +#include +// clang-format on + +#include +#include + +namespace base::http { + + /// A helper which returns the real IP address of a user behind a reverse proxy such as + /// NGINX, Apache, Caddy, or CloudFlare (or other reverse proxies - this list does not pretend to be exhaustive). + /// + /// # Remarks + /// If BASE_HTTP_REVERSE_PROXY_SUPPORT is not defined, this function + /// simply returns the fallback address, so it is safe to use in all contexts where a proxied + /// address might be used (in fact, it might be a good idea to just leave calls in regardless of configuration.) + asio::ip::address GetProxyAddress(asio::ip::address fallback, beast::http::request& req); + +} // namespace base::http diff --git a/lib/http/r3_helpers.hpp b/lib/http/r3_helpers.hpp new file mode 100644 index 0000000..fffdbed --- /dev/null +++ b/lib/http/r3_helpers.hpp @@ -0,0 +1,10 @@ +#pragma once +#include + +#include + +namespace base::http { + inline std::string R3IovecString(r3_iovec_t& iovec) { + return std::string(iovec.base, iovec.len); + } +} // namespace base::http diff --git a/lib/http/request.hpp b/lib/http/request.hpp new file mode 100644 index 0000000..a9dfed9 --- /dev/null +++ b/lib/http/request.hpp @@ -0,0 +1,68 @@ +#pragma once + +// This header needs to be included before beast headers +// because of a bit of a bug. I hope 1.84 solves this +//clang-format off +#include +//clang-format on + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace base::http { + + struct Request { + using BeastRequest = beast::http::request; + + Request(BeastRequest& request, burl::url_view url, r3::MatchEntry& me) : req(request), url(url) { + BASE_ASSERT(me.get() != nullptr, "Request constructor requires a valid match entry.."); + + // convert data from r3 into something with brain cells + auto& data = me.get()->vars; + for(u32 i = 0; i < data.tokens.size; ++i) { + auto& siov = data.slugs.entries[i]; + auto& eiov = data.tokens.entries[i]; + + matches[R3IovecString(siov)] = R3IovecString(eiov); + } + + // convert boost.URL params + for(auto kv : url.params()) { + if(kv.key.empty()) + continue; + + if(kv.has_value) { + query[kv.key] = kv.value; + } else { + query[kv.key] = ""; + } + } + } + + BeastRequest& Native() { return req; } + + const auto& Matches() const { return matches; } + + const std::string& Get(const std::string& key) const { return matches.at(key); } + + asio::ip::address src; + + std::unordered_map matches; + + // Query string parameters + std::unordered_map query; + + private: + friend struct Server; + + BeastRequest req; + burl::url_view url; + }; + +} // namespace base::http diff --git a/lib/http/response.hpp b/lib/http/response.hpp new file mode 100644 index 0000000..4b354b5 --- /dev/null +++ b/lib/http/response.hpp @@ -0,0 +1,25 @@ +#pragma once + +// See lib/http/request.hpp +//clang-format off +#include +//clang-format on + +#include +#include +#include +// + +namespace base::http { + + struct Response { + Response(beast::http::message_generator&& mg) : mg(std::move(mg)) {} + + auto&& Release() { return std::move(mg); } + + private: + friend struct Server; + beast::http::message_generator mg; + }; + +} // namespace base::http diff --git a/lib/http/server.cpp b/lib/http/server.cpp new file mode 100644 index 0000000..3d2a67d --- /dev/null +++ b/lib/http/server.cpp @@ -0,0 +1,406 @@ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace base::http { + + namespace detail { + +// method list for the below functions +#define ML() \ + C(get, METHOD_GET) \ + C(post, METHOD_POST) \ + C(put, METHOD_PUT) \ + C(delete_, METHOD_DELETE) \ + C(patch, METHOD_PATCH) \ + C(head, METHOD_HEAD) \ + C(options, METHOD_OPTIONS) + + constexpr auto BeastToR3(beast::http::verb verb) { + using enum beast::http::verb; + switch(verb) { +#define C(v, r) \ + case v: return r; + ML() +#undef C + default: return 0; + } + } + + constexpr beast::http::verb R3ToBeast(int method) { + using enum beast::http::verb; + switch(method) { +#define C(v, r) \ + case r: return v; + ML() +#undef C + default: BASE_CHECK(false, "should not reach this :)"); + } + } + +#undef ML + + } // namespace detail + + // make this a global helper? + constexpr auto stlify(boost::core::string_view sv) { + return std::string_view { sv.data(), sv.length() }; + } + + inline bool IsTcp(GenericProtocol p) { + const auto family = p.family(); + const auto protocol = p.protocol(); + + if(family != AF_INET && family != AF_INET6) + return false; + + // IPPROTO_IP is also used for remote TCP connections. Thanks BSD + return protocol == IPPROTO_TCP || protocol == IPPROTO_IP; + } + + struct Server::Impl { + struct Session { + Session(Impl& impl, Server::StreamType&& stream) : impl(impl), stream(std::move(stream)) {} + + void Close() { + if(stream.socket().is_open()) { + bsys::error_code ec; + stream.socket().shutdown(asio::socket_base::shutdown_send, ec); + } + } + + Awaitable DoSession() { + beast::flat_buffer buffer {}; + try { + while(true) { + curReq = {}; + + // Set the timeout. + stream.expires_after(std::chrono::seconds(30)); + + co_await beast::http::async_read(stream, buffer, curReq, asio::deferred); + + auto addr = GetProxyAddress(GetAddress(), curReq); + + auto res = co_await impl.Route(curReq, addr, *this); + + // This usually means that some higher level stuff (e.g: WebSockets) + // did the thing + if(!res.has_value()) + co_return; + + co_await WriteResponse(std::move(*res)); + + if(!stream.socket().is_open()) + break; + } + } catch(bsys::system_error& ec) { + // if(ec.code() != beast::http::error::end_of_stream) + // logger.Error("Error in http::BasicServer::DoSession(): {}", ec.what()); + } + + // todo: standalone close() function + Close(); + co_return; + } + + asio::ip::address GetAddress() const { + const auto protocol = stream.socket().remote_endpoint().protocol(); + + if(IsTcp(protocol)) { + // ugghhhhhhhhnhhnhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh + auto* sa = stream.socket().remote_endpoint().data(); + + switch(sa->sa_family) { + case AF_INET: { + auto* sain = std::bit_cast(sa); + return asio::ip::address_v4 { htonl(sain->sin_addr.s_addr) }; + } break; + + case AF_INET6: { + auto* sain6 = std::bit_cast(sa); + + // This is really annoying + // N.B: The array here isn't zerofilled because we copy into its whole capacity + // anyways so it's not really like it even matters + asio::ip::address_v6::bytes_type addr; + memcpy(&addr[0], sain6->sin6_addr.s6_addr, 16); + + return asio::ip::address_v6 { addr, sain6->sin6_scope_id }; + } break; + } + } + + // This is one of the reasons we should probably switch to a variant for this + // so we can expose unix support while not being wonky and broken, but we + // currently don't use unix support in the codebase or production so it's not really like it matters + // all that much + return asio::ip::make_address("1.2.3.4"); + } + + Awaitable WriteResponse(Response res) { + if(!stream.socket().is_open()) + co_return; + + auto addr = GetProxyAddress(GetAddress(), curReq); + + auto responseBytesWritten = co_await beast::async_write(stream, res.Release(), asio::deferred); + +#ifdef BASE_HTTP_REQUEST_LOGGING + impl.logger.Info("{} - {} {} HTTP/{}.{} ({})", addr.to_string(), stlify(curReq.method_string()), stlify(curReq.target()), + curReq.version() / 10, curReq.version() % 10, responseBytesWritten); +#else + static_cast(addr); + static_cast(responseBytesWritten); +#endif + + // Handle `Connection: close` semantics + if(!curReq.keep_alive()) + Close(); + co_return; + } + + // private: + Impl& impl; + Server::StreamType stream; + Request::BeastRequest curReq {}; + }; + + using RouteVariant = std::variant; + + template + Impl(asio::any_io_executor ioc, ProtocolEndpoint ep) : exec(ioc), ep(ep), acceptor(ioc) {} + + void On(beast::http::verb verb, std::string_view path, Server::RouteHandler handler) { + handlerStorage.emplace_back(handler); + char* errstr { nullptr }; + + // We use the "data" user pointer to instead index into our handler storage. + // It's messy and awful, but it does work! + tree.insert_routel(detail::BeastToR3(verb), path.data(), path.length(), std::bit_cast(handlerStorage.size() - 1), &errstr); + + if(errstr) { + // error inserting into tree + logger.Error("Error inserting route \"{}\" into tree: {}", path, errstr); + free(errstr); + return; + } + } + + void OnWebSocket(std::string_view path, Server::WebSocketRouteHandler handler) { + handlerStorage.emplace_back(handler); + char* errstr { nullptr }; + tree.insert_routel(detail::BeastToR3(beast::http::verb::get), path.data(), path.length(), std::bit_cast(handlerStorage.size() - 1), + &errstr); + if(errstr) { + // error inserting into tree + logger.Error("Error inserting route \"{}\" into tree: {}", path, errstr); + free(errstr); + return; + } + } + + // Call this after creating all required routes with On(). + bool CompileTree() { + char* errstr { nullptr }; + tree.compile(&errstr); + if(errstr) { + logger.Error("Error compiling route tree: {}", errstr); + free(errstr); + return false; + } + return true; + } + + Awaitable> Route(Request::BeastRequest& req, asio::ip::address srcAddress, Session& session) { + auto const noSuchRouteError = [&]() -> beast::http::message_generator { + beast::http::response resp { beast::http::status::bad_request, req.version() }; + SetCommonResponseFields(resp); + resp.set(beast::http::field::content_type, "text/plain"); + resp.keep_alive(false); + resp.body() = std::format("No such route \"{} {}\"", std::string_view { req.method_string().data(), req.method_string().length() }, + std::string_view { req.target().data(), req.target().length() }); + resp.prepare_payload(); + return resp; + }; + + auto const internalServerError = [&]() -> beast::http::message_generator { + beast::http::response resp { beast::http::status::internal_server_error, req.version() }; + SetCommonResponseFields(resp); + resp.set(beast::http::field::content_type, "text/plain"); + resp.keep_alive(false); + resp.body() = "HTTP/1.1 500 Internal Server Error"; + resp.prepare_payload(); + return resp; + }; + + auto const wsRequired = [&]() -> beast::http::message_generator { + beast::http::response resp { beast::http::status::bad_request, req.version() }; + SetCommonResponseFields(resp); + resp.set(beast::http::field::content_type, "text/plain"); + resp.keep_alive(false); + resp.body() = "Invalid HTTP request to WebSockets endpoint"; + resp.prepare_payload(); + return resp; + }; + + auto url = burl::parse_origin_form(req.target()); + if(url.has_error()) { + // If we were unable to parse the URL (somehow), then gracefully handle that + co_return Response { internalServerError() }; + } + + auto path = url->path(); + + r3::MatchEntry me { path.data(), static_cast(path.length()) }; + me.set_request_method(detail::BeastToR3(req.method())); + + auto route = tree.match_route(me); + if(auto rp = route.get()) { + // this is a mess but it's a mess which works :) + auto handlerIndex = std::bit_cast(rp->data); + auto& handler = handlerStorage[handlerIndex]; + + auto routed_request = Request { req, url.value(), me }; + routed_request.src = srcAddress; + + // Dispatch to the given handler variant + if(auto* httpHandler = std::get_if(&handler); httpHandler) { + co_return co_await (*httpHandler)(routed_request); + } else if(auto* wsHandler = std::get_if(&handler); wsHandler) { + if(!beast::websocket::is_upgrade(routed_request.Native())) { + auto res = wsRequired(); + co_return Response { std::move(res) }; + } + + auto ws = std::make_shared(std::move(session.stream), routed_request); + + if(!co_await ws->Handshake()) { + co_return std::nullopt; + } + + co_await (*wsHandler)(routed_request, std::move(ws)); + co_return std::nullopt; + } + } else { + // TODO: maybe a catch-all handler at some point. for now this is fine enough + + // Send a bad request detailing that a route is not available for the given + // URL path. + co_return Response { noSuchRouteError() }; + } + co_return std::nullopt; + } + + Awaitable Listener() { + try { + // Setup the acceptor + acceptor.open(ep.protocol()); + acceptor.set_option(asio::socket_base::reuse_address(true)); + + if(IsTcp(ep.protocol())) { + // set SO_REUSEPORT using a custom type. This is flaky but we pin boost + // so this will be ok I suppose + using reuse_port = asio::detail::socket_option::boolean; + acceptor.set_option(reuse_port(true)); + + logger.Info("Able to enable SO_NODELAY; doing so"); + acceptor.set_option(asio::ip::tcp::no_delay { true }); + } + + acceptor.bind(ep); + acceptor.listen(asio::socket_base::max_listen_connections); + + listening = true; + + // Now do the thing.. + while(true) { + auto socket = co_await acceptor.async_accept(asio::deferred); + asio::co_spawn(acceptor.get_executor(), RunSession(StreamType { std::move(socket) }), asio::detached); + } + } catch(bsys::system_error& ec) { + // Filter out "operation cancelled", because that's not really an error we need to know about. + if(ec.code() != asio::error::operation_aborted) + logger.Error("Error in http::BasicServer::Listener(): {}", ec.what()); + } + + listening = false; + } + + // TODO: Maybe we should actually store all sessions or something, and use shared_ptrs to refer to them + // That way on server shutdown we can cleanly shut them all down + + /// Runs a single HTTP client session. + Awaitable RunSession(StreamType stream) { + // std::shared_ptr session = std::make_shared(*this, std::move(stream)); + Session session { *this, std::move(stream) }; + co_await session.DoSession(); + co_return; + } + + void Start() { + if(!CompileTree()) { + BASE_CHECK(false, "Route tree compilation failure, cannot start HTTP server"); + } + + asio::co_spawn(exec, Listener(), asio::detached); + } + + void Stop() { + bsys::error_code ec; + acceptor.close(ec); + } + + // Router tree. + r3::Tree tree { 16 }; + + std::vector handlerStorage; + + asio::any_io_executor exec; + EndpointType ep; + bool listening { false }; + + asio::basic_socket_acceptor acceptor; + + Logger logger { MakeChannelId(MessageSource::Http, MessageComponentSource::Http_Server) }; + }; + + Server::Server(asio::any_io_executor ioc, asio::ip::tcp::endpoint ep) { + impl = std::make_unique(ioc, ep); + } + + Server::~Server() = default; + + bool Server::Listening() const { + return impl->listening; + } + + void Server::On(beast::http::verb verb, std::string_view path, RouteHandler handler) { + impl->On(verb, path, handler); + } + + void Server::GetWebSockets(std::string_view path, WebSocketRouteHandler wsRoute) { + impl->OnWebSocket(path, wsRoute); + } + + void Server::Start() { + impl->Start(); + } + + void Server::Stop() { + if(impl->listening) + impl->Stop(); + } + +} // namespace base::http diff --git a/lib/http/server.hpp b/lib/http/server.hpp new file mode 100644 index 0000000..ce2fa2d --- /dev/null +++ b/lib/http/server.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace base::http { + + /// A HTTP server. + struct Server { + using SocketType = GenericProtocol::socket; + using StreamType = BeastStream; + using EndpointType = GenericProtocol::endpoint; + + using RouteHandler = std::function(Request& request)>; + using WebSocketRouteHandler = std::function(Request& request, WebSocketClient::Ptr&& ws)>; + + Server(asio::any_io_executor ioc, asio::ip::tcp::endpoint ep); + ~Server(); + + void Start(); + void Stop(); + + bool Listening() const; + + /// Declare a route + void On(beast::http::verb verb, std::string_view path, RouteHandler handler); + + void Get(std::string_view path, RouteHandler handler) { On(beast::http::verb::get, path, handler); } + void GetWebSockets(std::string_view path, WebSocketRouteHandler wsRoute); + void Post(std::string_view path, RouteHandler handler) { On(beast::http::verb::post, path, handler); } + + private: + struct Impl; + Unique impl; + }; + +} // namespace base::http diff --git a/lib/http/third_party/r3 b/lib/http/third_party/r3 new file mode 160000 index 0000000..ade1527 --- /dev/null +++ b/lib/http/third_party/r3 @@ -0,0 +1 @@ +Subproject commit ade1527bef91b85a09439f98014d048735605e60 diff --git a/lib/http/utils.hpp b/lib/http/utils.hpp new file mode 100644 index 0000000..8f5cbea --- /dev/null +++ b/lib/http/utils.hpp @@ -0,0 +1,21 @@ +#pragma once +#include +#include +#include + +namespace base::http { + + template + inline std::string ImfDate(std::chrono::time_point point) { + return std::format("{0:%a}, {0:%d %b %Y} {0:%T} GMT", + std::chrono::clock_cast(std::chrono::time_point_cast(point))); + } + + // use this on a response please :) + template + constexpr void SetCommonResponseFields(beast::http::response& res) { + res.set(beast::http::field::date, ImfDate(std::chrono::utc_clock::now())); + res.set(beast::http::field::server, "Holgol"); + } + +} // namespace base::http diff --git a/lib/http/websocket_client.cpp b/lib/http/websocket_client.cpp new file mode 100644 index 0000000..9817e73 --- /dev/null +++ b/lib/http/websocket_client.cpp @@ -0,0 +1,193 @@ +#include +#include +#include +#include +#include + +namespace base::http { + + void WebSocketClient::Send(ConstMessagePtr message) { + BASE_ASSERT(message.get() != nullptr, "This function needs a valid message pointer."); + if(closed) + return; + + // If the backpressure is too large for this client + // then just disconnect it. It's not worth trying to catch up, + // especially if this client is actually holding references to messages + // and causing what effectively amounts to a memory leak! + if(sendQueue.size() > MAX_MESSAGES_IN_QUEUE) { + if(!closed && !forceClosed) { + forceClosed = true; + sendQueue.clear(); + asio::co_spawn(stream.get_executor(), Close(), asio::detached); + } + return; + } + + sendQueue.push_back(message); + sendCondition.NotifyOne(); + } + + Awaitable WebSocketClient::Handshake() { + BASE_ASSERT(upgrade.IsWebsocketUpgrade(), "Arrived here without a valid WebSocket upgrade?"); + + // Disable the expiry timeout that the HTTP server placed on the original stream, since + // websocket::stream has its own functionality to time itself out. + stream.next_layer().expires_never(); + + stream.set_option(beast::websocket::stream_base::timeout::suggested(beast::role_type::server)); + + // decorate the handshake with our common server field(s) + stream.set_option(beast::websocket::stream_base::decorator([](beast::websocket::response_type& res) { SetCommonResponseFields(res); })); + + // try accepting + try { + co_await stream.async_accept(upgrade.Native(), asio::deferred); + + // Spawn coroutines for read/write end now that we actually performed the websokcet handshake + auto exec = stream.next_layer().get_executor(); + asio::co_spawn(exec, WriteEnd(), asio::detached); + asio::co_spawn(exec, ReadEnd(), asio::detached); + co_return true; + } catch(bsys::system_error& ec) { + // just close the socket then. + bsys::error_code ignore; + stream.next_layer().socket().close(ignore); + co_return false; + } + co_return false; + } + + Awaitable WebSocketClient::Close() { + return CloseWithReason("Generic close."); + } + + Awaitable WebSocketClient::CloseWithReason(const std::string& reason) { + auto self = shared_from_this(); + + // don't try to close more than once + // (this avoids calling the close handler + // more than once, or trying to close the WebSocket stream + // multiple times, avoiding crashes.) + if(self->closed) + co_return; + + self->closed = true; + + if(self->listener) + co_await self->listener->OnClose(); + + try { + if(self->stream.next_layer().socket().is_open()) { + co_await self->stream.async_close(beast::websocket::close_reason { beast::websocket::close_code::try_again_later, reason }, + asio::deferred); + } + } catch(bsys::error_code& ec) { + logger.Error("Error when closing (not fatal): {}", ec.what()); + } + + // notify the send task that it needs to stop + self->sendQueue.clear(); + self->sendCondition.NotifyAll(); + co_return; + } + + asio::ip::address WebSocketClient::Address() { + // N.B. The previous "get it from the socket" method would "work" + // but we don't support any sort of multipath, so the IP we get + // the upgrade request on (all things considered) will be the IP we read + +#ifdef BASE_HTTP_REVERSE_PROXY_SUPPORT + return http::GetProxyAddress(upgrade.src, upgrade.Native()); +#else + return upgrade.src; +#endif + } + + Awaitable WebSocketClient::WriteEnd() { + auto self = shared_from_this(); + try { + while(true) { + // If the send queue was empty, then there isn't really anything we can do + if(self->sendQueue.empty()) { + self->sending = false; + self->sendCondition.NotifyOne(); + co_await self->sendCondition.Wait([self]() { + if(self->closed) + return true; + return !self->sendQueue.empty(); + }); + } + + if(closed) + break; + + self->sending = true; + auto& message = self->sendQueue.front(); + + if(message->GetType() == WebSocketMessage::Type::Text) { + self->stream.text(true); + co_await self->stream.async_write(asio::buffer(message->AsText()), asio::deferred); + } else if(message->GetType() == WebSocketMessage::Type::Binary) { + self->stream.binary(true); + co_await self->stream.async_write(asio::buffer(message->AsBinary()), asio::deferred); + } + + // pop the element off the queue + self->sendQueue.erase(sendQueue.begin()); + self->sendQueue.shrink_to_fit(); + } + } catch(bsys::system_error& ec) { + // logger.Error("failure in write end: {}", ec.what()); + } + + co_await self->Close(); + co_return; + } + + Awaitable WebSocketClient::ReadEnd() { + auto self = shared_from_this(); + beast::flat_buffer messageBuffer; + + try { + while(true) { + // wait for the send end to be done sending messages, if it decides to wake up + co_await self->sendCondition.Wait([self]() { + if(self->closed) + return true; + return !self->sending; + }); + + // If the connection was closed break out + if(self->closed) + break; + + // let's try reading a message + auto b = co_await self->stream.async_read(messageBuffer, asio::deferred); + + if(self->listener) { + if(self->stream.got_text()) { + co_await self->listener->OnMessage(std::make_shared( + std::string { reinterpret_cast(messageBuffer.data().data()), messageBuffer.size() })); + } else if(self->stream.got_binary()) { + co_await self->listener->OnMessage(std::make_shared( + std::span { reinterpret_cast(messageBuffer.data().data()), messageBuffer.size() })); + } + } + + messageBuffer.consume(b); + + // notify the send end we're done reading, so it can (if the queue isn't empty) write + // this might end up blocking us from reading for a while if it decides it can. + self->sendCondition.NotifyOne(); + } + } catch(bsys::system_error& ec) { + // TODO: this should be re-enabled but doesn't seem to limit properly + // if(ec.code() != beast::websocket::error::closed || ec.code() != asio::error::eof) + // logger.Error("fail in read end {}", ec.what()); + } + + co_await self->Close(); + co_return; + } +} // namespace base::http diff --git a/lib/http/websocket_client.hpp b/lib/http/websocket_client.hpp new file mode 100644 index 0000000..818ae60 --- /dev/null +++ b/lib/http/websocket_client.hpp @@ -0,0 +1,81 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +namespace base::http { + + // TODO: maybe pimpl? + + struct WebSocketClient : public std::enable_shared_from_this { + using RawStream = GenericStream; + using StreamType = beast::websocket::stream; + using EndpointType = typename GenericProtocol::endpoint; + + using Ptr = std::shared_ptr; + + using MessagePtr = std::shared_ptr; + using ConstMessagePtr = std::shared_ptr; + + using MessageHandler = std::function(Ptr, ConstMessagePtr)>; + using CloseHandler = std::function(Ptr)>; // TODO: close reason? + + struct Listener { + virtual ~Listener() = default; + + virtual Awaitable OnClose() = 0; // TODO: close reason + virtual Awaitable OnMessage(ConstMessagePtr message) = 0; + // TODO: control frames + }; + + WebSocketClient(RawStream&& stream, Request& req) + : stream(std::move(stream)), sendCondition(stream.get_executor()), upgrade(req) {} + + //~WebSocketClient() { + // logger.Info("~WebSocketClient"); + //} + + void SetListener(Listener* listener) { + this->listener = listener; + } + + void Send(ConstMessagePtr message); + + Awaitable Handshake(); + + Awaitable Close(); + + + Awaitable CloseWithReason(const std::string& reason); + + asio::ip::address Address(); + + Request& GetUpgradeRequest() { return upgrade; } + + private: + StreamType stream; + + std::vector sendQueue; + + Listener* listener{nullptr}; + + AsyncConditionVariable sendCondition; + bool sending { false }; + bool closed = false; + bool forceClosed { false }; + + Request upgrade; + + Logger logger { MakeChannelId(MessageSource::Http, MessageComponentSource::Http_WebSocketClient) }; + + constexpr static u32 MAX_MESSAGES_IN_QUEUE = 256; + + Awaitable WriteEnd(); + Awaitable ReadEnd(); + }; + +} // namespace base::http diff --git a/lib/http/websocket_message.cpp b/lib/http/websocket_message.cpp new file mode 100644 index 0000000..322fa82 --- /dev/null +++ b/lib/http/websocket_message.cpp @@ -0,0 +1,38 @@ +#include + +namespace base::http { + WebSocketMessage::WebSocketMessage(const std::string& str) { + payload.emplace(str); + } + + WebSocketMessage::WebSocketMessage(const std::vector& data) { + payload.emplace(data); + } + + WebSocketMessage::WebSocketMessage(const std::span& data) { + auto vec = std::vector {}; + vec.resize(data.size()); + std::memcpy(vec.data(), data.data(), data.size()); + payload.emplace(vec); + } + + WebSocketMessage::Type WebSocketMessage::GetType() const { + if(std::holds_alternative(payload)) + return Type::Text; + else if(std::holds_alternative(payload)) + return Type::Binary; + + BASE_CHECK(false, "Shouldn't get here."); + } + + const std::string& WebSocketMessage::AsText() const { + BASE_ASSERT(GetType() == Type::Text, "WebSocketMessage isn't holding a Text message"); + + return std::get(payload).data; + } + + const std::vector& WebSocketMessage::AsBinary() const { + BASE_ASSERT(GetType() == Type::Binary, "WebSocketMessage isn't holding a Binary message"); + return std::get(payload).data; + } +} // namespace base::http diff --git a/lib/http/websocket_message.hpp b/lib/http/websocket_message.hpp new file mode 100644 index 0000000..3e332b1 --- /dev/null +++ b/lib/http/websocket_message.hpp @@ -0,0 +1,41 @@ +#pragma once +#include +#include +#include + +namespace base::http { + struct WebSocketMessage { + enum class Type { Text, Binary }; + + WebSocketMessage() = default; // for d-construciton + + explicit WebSocketMessage(const std::string& str); + explicit WebSocketMessage(const std::vector& data); + explicit WebSocketMessage(const std::span& data); + + Type GetType() const; + const std::string& AsText() const; + const std::vector& AsBinary() const; + + private: + struct Text { + std::string data; + }; + + struct Binary { + std::vector data; + }; + + // no payload (yet?) + // struct Ping {}; + + std::variant payload; + }; + + // Helper to more easily build a websocket message object + template + inline std::shared_ptr BuildMessage(const In& in) { + return std::make_shared(in); + } + +} // namespace base::http diff --git a/lib/impl/CMakeLists.txt b/lib/impl/CMakeLists.txt new file mode 100644 index 0000000..c94199d --- /dev/null +++ b/lib/impl/CMakeLists.txt @@ -0,0 +1,39 @@ +add_library(base_impl + asio_src.cpp + beast_src.cpp + #mysql_src.cpp + tomlpp_src.cpp +) + +lobbyserver_target(base_impl) + +target_compile_definitions(base_impl PUBLIC + # We choose to only support Linux 5.15+ onwards, + # so explicitly use io_uring (when it works). + -DBOOST_ASIO_HAS_IO_URING=1 + -DBOOST_ASIO_HAS_IO_URING_AS_DEFAULT=1 + + # We compile all of these header-only libraries in separate .cpp source files + # to decrease build churn + -DBOOST_ASIO_SEPARATE_COMPILATION=1 + -DBOOST_BEAST_SEPARATE_COMPILATION=1 + # TODO: re-enable this (once we have boost mysql) + #-DBOOST_MYSQL_SEPARATE_COMPILATION=1 + -DTOML_HEADER_ONLY=0 + + # Disable deprecated functionality and some things which add additional dependencies or are + # simply baggage we aren't ever going to use + -DBOOST_ASIO_NO_DEPRECATED=1 + -DBOOST_ASIO_DISABLE_BOOST_ARRAY=1 + -DBOOST_ASIO_DISABLE_BOOST_BIND=1 + +) + +target_link_libraries(base_impl PUBLIC + Boost::asio + Boost::beast + uring + tomlplusplus::tomlplusplus +) + +add_library(base::impl ALIAS base_impl) diff --git a/lib/impl/asio_config.hpp b/lib/impl/asio_config.hpp new file mode 100644 index 0000000..f7d12fe --- /dev/null +++ b/lib/impl/asio_config.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace asio = boost::asio; +namespace beast = boost::beast; + +namespace base { + + // if we need strands this is here just in case :) + using BaseExecutorType = asio::any_io_executor; + using ExecutorType = BaseExecutorType; + + /// Awaitable type (configured for the current executor) + template + using Awaitable = asio::awaitable; + + template + using Acceptor = asio::basic_socket_acceptor; + + template + using Socket = asio::basic_stream_socket; + + using SteadyTimer = asio::basic_waitable_timer, ExecutorType>; + + template + using BeastStream = beast::basic_stream; + +} // namespace base diff --git a/lib/impl/asio_src.cpp b/lib/impl/asio_src.cpp new file mode 100644 index 0000000..9756d9a --- /dev/null +++ b/lib/impl/asio_src.cpp @@ -0,0 +1,6 @@ +// Since we're using (BOOST_)ASIO_SEPARATE_COMPILATION, we need +// to include the <(boost/)asio/impl/src.hpp> header in some TU. +// We use this one to explicitly do so. + +#include +//#include diff --git a/lib/impl/beast_src.cpp b/lib/impl/beast_src.cpp new file mode 100644 index 0000000..01b7c8f --- /dev/null +++ b/lib/impl/beast_src.cpp @@ -0,0 +1 @@ +#include diff --git a/lib/impl/tomlpp_src.cpp b/lib/impl/tomlpp_src.cpp new file mode 100644 index 0000000..5662bcc --- /dev/null +++ b/lib/impl/tomlpp_src.cpp @@ -0,0 +1,2 @@ +#define TOML_IMPLEMENTATION +#include diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..e383ef5 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,15 @@ +add_executable(lobbyserver + main.cpp + + # message implementations + messages/IMessage.cpp + messages/PingMessage.cpp +) + +lobbyserver_target(lobbyserver) + +target_link_libraries(lobbyserver PRIVATE + base::base + base::http + Boost::json +) diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..392c12b --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,104 @@ +#include +#include +#include +#include +#include +#include +#include + +std::optional ioc; +// ls server global here + +constexpr static std::string_view CONFIG_FILE = "lobbyserver.toml"; + +base::Awaitable CoWaitForSignal() { + boost::asio::signal_set interruptSignal(co_await asio::this_coro::executor, SIGINT, SIGTERM); + try { + co_await interruptSignal.async_wait(asio::deferred); + } catch(bsys::system_error& ec) { + base::LogError("Error waiting for signal? {}", ec.what()); + } + + base::LogInfo("SIGINT/SIGTERM recieved, stopping server..."); + + //co_await server->Stop(); + + base::LogInfo("Server stopped successfully"); + + // Deallocate the server +// server.reset(); + + // At this point, we can now stop the io_context, which will cause + // the main to return and ultimately exit the protgram + ioc->stop(); + + co_return; +} + +base::Awaitable CoMain() { + //server = std::make_unique<...>(co_await asio::this_coro::executor, config); + //co_await server->Launch(); + co_return; +} + +int main() { + base::LoggerAttachStdout(); + + + try { + auto table = toml::parse_file(CONFIG_FILE); + + if(table["lobbyserver"].is_table()) { + auto addr_ptr = table["lobbyserver"]["listen_address"].as_string(); + auto port_ptr = table["lobbyserver"]["listen_port"].as_integer(); + + if(!addr_ptr || !port_ptr) { + base::LogError("Invalid configuration file \"{}\".", CONFIG_FILE); + return 1; + } + + if(port_ptr->get() > 65535) { + base::LogError("Invalid listen port \"{}\", should be 65535 or less", port_ptr->get()); + return 1; + } + + //config.listenEndpoint = { asio::ip::make_address(addr_ptr->get()), static_cast(port_ptr->get()) }; + } else { + base::LogError("Invalid configuration file \"{}\"", CONFIG_FILE); + return 1; + } + + } catch(toml::parse_error& err) { + base::LogError("Error parsing configuration file \"{}\": {}", CONFIG_FILE, err.what()); + return 1; + } + + ioc.emplace((std::thread::hardware_concurrency() / 2) - 1); + + asio::co_spawn(*ioc, CoWaitForSignal(), [&](auto ep) { + if(ep) { + try { + std::rethrow_exception(ep); + } catch(std::exception& e) { + BASE_CHECK(false, "Unhandled exception in signal listener: {}", e.what()); + } + } + }); + + asio::co_spawn(*ioc, CoMain(), [&](auto ep) { + if(ep) { + try { + std::rethrow_exception(ep); + } catch(std::exception& e) { + BASE_CHECK(false, "Unhandled exception in server main loop: {}", e.what()); + } + } else { + base::LogInfo("Main coroutine returned, stopping server\n"); + // done + ioc->stop(); + } + }); + + ioc->attach(); + return 0; +} diff --git a/src/messages/IMessage.cpp b/src/messages/IMessage.cpp new file mode 100644 index 0000000..608c7b8 --- /dev/null +++ b/src/messages/IMessage.cpp @@ -0,0 +1,175 @@ +#include "IMessage.hpp" + +#include +#include + +#include "WireMessage.hpp" + +namespace ls { + + bool IMessage::ParseFromInputBuffer(std::span inputBuffer) { + // Nothing to parse, + // which isn't exclusively a failure condition. + if(inputBuffer.empty()) + return true; + + std::string key; + std::string val; + + usize inputIndex = 0; + + // TODO: Investigate rewriting this using ragel? + + enum class ReaderState : u32 { + InKey, ///< The state machine is currently parsing a key. + InValue ///< The state machine is currently parsing a value. + } state { ReaderState::InKey }; + + // Parse all properties, using a relatively simple state machine. + // + // State transition mappings: + // = - from key to value state (if in key state) + // \n - from value to key state (if in value state, otherwise error) + + while(inputIndex != inputBuffer.size()) { + switch(inputBuffer[inputIndex]) { + case '=': + if(state == ReaderState::InKey) { + state = ReaderState::InValue; + break; + } else { + // If we're in the value state, we're allowed to nest = signs, I think. + // if not, then this is PROBABLY an error state. + val += static_cast(inputBuffer[inputIndex]); + } + break; + + case '\n': + if(state == ReaderState::InValue) { + properties[key] = val; + + // Reset the state machine, to read another property. + key.clear(); + val.clear(); + state = ReaderState::InKey; + break; + } else { + // If we get here in the key state, this is DEFINITELY an error. + return false; + } + + // Any other characters are not important to the state machine, + // and are instead written to the given staging string for the current + // state machine state. + default: + switch(state) { + case ReaderState::InKey: + key += static_cast(inputBuffer[inputIndex]); + break; + case ReaderState::InValue: + // Skip past quotation marks. + // I dunno if it's really needed. + // (For reference: SSX3 Dirtysock does the same thing, even including '). + if(static_cast(inputBuffer[inputIndex]) == '\"') + break; + + val += static_cast(inputBuffer[inputIndex]); + break; + } + break; + } + + inputIndex++; + } + + // Parse succeeded + return true; + } + + void IMessage::SerializeTo(std::vector& dataBuffer) const { + std::string serializedProperties; + + // Reserve a sane amount, to avoid allocations when serializing properties + // (in most cases; larger messages MIGHT still cause some allocation pressure.) + serializedProperties.reserve(512); + + // Serialize properties + { + auto i = properties.size(); + for(auto [key, value] : properties) + if(--i != 0) + serializedProperties += std::format("{}={}\n", key, value); + else + serializedProperties += std::format("{}={}", key, value); + } + + // Null terminate the property data. + serializedProperties.push_back('\0'); + + // Create an appropriate header for the data. + proto::WireMessageHeader header { + .typeCode = static_cast(TypeCode()), + .typeCodeHi = 0, + .payloadSize = serializedProperties.length() - 1 + }; + + auto fullLength = sizeof(proto::WireMessageHeader) + serializedProperties.length(); + + // Resize the output buffer to the right size + dataBuffer.resize(fullLength); + + // Write to the output buffer now. + memcpy(&dataBuffer[0], &header, sizeof(proto::WireMessageHeader)); + memcpy(&dataBuffer[sizeof(proto::WireMessageHeader)], serializedProperties.data(), serializedProperties.length()); + } + + const std::optional IMessage::MaybeGetKey(const std::string& key) const { + if(properties.find(key) == properties.end()) + return std::nullopt; + else + return properties.at(key); + } + + void IMessage::SetKey(const std::string& key, const std::string& value) { + properties[key] = value; + } + + // message factory + + /// Debug message, used to.. well, debug, obviously. + struct DebugMessage : IMessage { + explicit DebugMessage(base::FourCC32_t myTypeCode) + : myTypeCode(myTypeCode) { + } + + base::FourCC32_t TypeCode() const override { return myTypeCode; } + + base::Awaitable Process(base::Ref client) override { + auto* fccbytes = ((uint8_t*)&myTypeCode); + + base::LogInfo("Debug Message FourCC lo: \"{:c}{:c}{:c}{:c}\"", fccbytes[0], fccbytes[1], fccbytes[2], fccbytes[3]); + base::LogInfo("Debug Message Properties:"); + + for(auto [key, value] : properties) + base::LogInfo("{}: {}", key, value); + co_return; + } + + private: + base::FourCC32_t myTypeCode {}; + }; + + MessageFactory::FactoryMap& MessageFactory::GetFactoryMap() { + static MessageFactory::FactoryMap factoryMap; + return factoryMap; + } + + base::Ref MessageFactory::CreateMessage(base::FourCC32_t fourCC) { + const auto& factories = GetFactoryMap(); + if(const auto it = factories.find(fourCC); it == factories.end()) + return std::make_shared(fourCC); + else + return (it->second)(); + } + +} // namespace ls \ No newline at end of file diff --git a/src/messages/IMessage.hpp b/src/messages/IMessage.hpp new file mode 100644 index 0000000..a3a90fd --- /dev/null +++ b/src/messages/IMessage.hpp @@ -0,0 +1,80 @@ +#include +#include +#include + +namespace ls { + struct Server; + struct Client; + + struct IMessage { + virtual ~IMessage() = default; + + /// Parses from input buffer. The data must live until + /// this function returns. + /// This function may return false (or later, a more well defined + /// error code enumeration..) if the parsing fails. + bool ParseFromInputBuffer(std::span data); + + /// Serializes to a output data buffer. + void SerializeTo(std::vector& dataBuffer) const; + + virtual base::FourCC32_t TypeCode() const = 0; + + /// Process a single message. + virtual base::Awaitable Process(base::Ref client) = 0; + + const std::optional MaybeGetKey(const std::string& key) const; + + void SetKey(const std::string& key, const std::string& value); + + protected: + /// all properties. + std::unordered_map properties {}; + + /// The client this message is for. + base::Ref client {}; + }; + + struct MessageFactory { + static base::Ref CreateMessage(base::FourCC32_t fourCC); + + private: + template + friend struct MessageMixin; + + using FactoryMap = std::unordered_map (*)()>; + static FactoryMap& GetFactoryMap(); + }; + + template + struct MessageMixin : IMessage { + constexpr static auto TYPE_CODE = base::FourCC32(); + + explicit MessageMixin() + : IMessage() { + static_cast(registered); + } + + base::FourCC32_t TypeCode() const override { + return TYPE_CODE; + } + + private: + static bool Register() { + MessageFactory::GetFactoryMap().insert({ TYPE_CODE, []() -> base::Ref { + return std::make_shared(); + } }); + return true; + } + static inline bool registered = Register(); + }; + +// :( Makes the boilerplate shorter and sweeter though. +#define LS_MESSAGE(T, fourCC) struct T : public ls::MessageMixin +#define LS_MESSAGE_CTOR(T, fourCC) \ + using Super = ls::MessageMixin; \ + explicit T() \ + : Super() { \ + } + +} // namespace ls \ No newline at end of file diff --git a/src/messages/PingMessage.cpp b/src/messages/PingMessage.cpp new file mode 100644 index 0000000..80b67c7 --- /dev/null +++ b/src/messages/PingMessage.cpp @@ -0,0 +1,13 @@ +#include + +#include "base/logger.hpp" +#include "IMessage.hpp" + +LS_MESSAGE(PingMessage, "~png") { + LS_MESSAGE_CTOR(PingMessage, "~png") + + base::Awaitable Process(base::Ref client) override { + base::LogInfo("Got ping message!"); + co_return; + } +}; diff --git a/src/messages/WireMessage.hpp b/src/messages/WireMessage.hpp new file mode 100644 index 0000000..5c215f8 --- /dev/null +++ b/src/messages/WireMessage.hpp @@ -0,0 +1,20 @@ +#include + +namespace ls::proto { + + /// The on-wire message header. + struct [[gnu::packed]] WireMessageHeader { + /// Message FourCC. + u32 typeCode {}; + + /// Apparently a extra 4 bytes of FourCC? + u32 typeCodeHi {}; + + /// The size of the data payload. + base::NetworkOrder payloadSize {}; + }; + + // Sanity checking. + static_assert(sizeof(WireMessageHeader) == 12, "Wire message header size is invalid"); + +} // namespace ls::proto \ No newline at end of file diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt new file mode 100644 index 0000000..1d45678 --- /dev/null +++ b/third_party/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(boost) +add_subdirectory(tomlplusplus) diff --git a/third_party/boost/CMakeLists.txt b/third_party/boost/CMakeLists.txt new file mode 100644 index 0000000..5d11eb5 --- /dev/null +++ b/third_party/boost/CMakeLists.txt @@ -0,0 +1,11 @@ +# Hack, but it works :) +set(BOOST_SUPERPROJECT_VERSION 1.84.0) + +# Populate library list +file(STRINGS ${CMAKE_CURRENT_SOURCE_DIR}/list _COLLABVM3_BOOST_LIBRARY_LIST) + +# Pull in each boost module/library +foreach(lib ${_COLLABVM3_BOOST_LIBRARY_LIST}) + message(STATUS "Adding boost module ${lib}") + add_subdirectory(${lib}) +endforeach() diff --git a/third_party/boost/README.md b/third_party/boost/README.md new file mode 100644 index 0000000..2227910 --- /dev/null +++ b/third_party/boost/README.md @@ -0,0 +1,11 @@ +# Welcome to hell + +This is where all the boost libraries live as submodules.. + +see ./list for them + +# Reinitalizing + +The following bash one liner was used to initalize this repo + +`for f in $(cat list); do git submodule add https://github.com/boostorg/$f.git $f; cd $f; git checkout boost-1.84.0; cd ..; done` diff --git a/third_party/boost/add.sh b/third_party/boost/add.sh new file mode 100755 index 0000000..f09c37d --- /dev/null +++ b/third_party/boost/add.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# note that this doesn't add the library to ./list +# you will need to do that yourself +# (it could be added but bleh) + +lib=$1 +git submodule add https://github.com/boostorg/$lib.git $lib +# remember to bump the version afterwards + diff --git a/third_party/boost/algorithm b/third_party/boost/algorithm new file mode 160000 index 0000000..faac048 --- /dev/null +++ b/third_party/boost/algorithm @@ -0,0 +1 @@ +Subproject commit faac048d59948b1990c0a8772a050d8e47279343 diff --git a/third_party/boost/align b/third_party/boost/align new file mode 160000 index 0000000..5ad7df6 --- /dev/null +++ b/third_party/boost/align @@ -0,0 +1 @@ +Subproject commit 5ad7df63cd792fbdb801d600b93cad1a432f0151 diff --git a/third_party/boost/array b/third_party/boost/array new file mode 160000 index 0000000..ecc47cb --- /dev/null +++ b/third_party/boost/array @@ -0,0 +1 @@ +Subproject commit ecc47cb42c98261d6abf39fb5575c38eac6db748 diff --git a/third_party/boost/asio b/third_party/boost/asio new file mode 160000 index 0000000..2e49b21 --- /dev/null +++ b/third_party/boost/asio @@ -0,0 +1 @@ +Subproject commit 2e49b21732e5d1bb3bb98f209164c30816b0bc79 diff --git a/third_party/boost/assert b/third_party/boost/assert new file mode 160000 index 0000000..a2817b8 --- /dev/null +++ b/third_party/boost/assert @@ -0,0 +1 @@ +Subproject commit a2817b89f48a8fdbe4130762d0d47a355ca5b562 diff --git a/third_party/boost/atomic b/third_party/boost/atomic new file mode 160000 index 0000000..b91d551 --- /dev/null +++ b/third_party/boost/atomic @@ -0,0 +1 @@ +Subproject commit b91d55150f5bb6a26a8560a58ede9164b8812de6 diff --git a/third_party/boost/beast b/third_party/boost/beast new file mode 160000 index 0000000..dc8798c --- /dev/null +++ b/third_party/boost/beast @@ -0,0 +1 @@ +Subproject commit dc8798c917a2d527e4d64497b2b85e2694fc037c diff --git a/third_party/boost/bind b/third_party/boost/bind new file mode 160000 index 0000000..dded373 --- /dev/null +++ b/third_party/boost/bind @@ -0,0 +1 @@ +Subproject commit dded373cc705781b8d0778343892a290aa87b09f diff --git a/third_party/boost/bump_version.sh b/third_party/boost/bump_version.sh new file mode 100755 index 0000000..0f4a1cb --- /dev/null +++ b/third_party/boost/bump_version.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# bump all the repositories +for library in $(cat $PWD/list); do + pushd $library >/dev/null 2>&1 +# git pull + git fetch + git checkout $1 + popd >/dev/null 2>&1 +done diff --git a/third_party/boost/chrono b/third_party/boost/chrono new file mode 160000 index 0000000..ee0d6d5 --- /dev/null +++ b/third_party/boost/chrono @@ -0,0 +1 @@ +Subproject commit ee0d6d543a37d9b7243682549e9ae359eb89daa9 diff --git a/third_party/boost/circular_buffer b/third_party/boost/circular_buffer new file mode 160000 index 0000000..05a8322 --- /dev/null +++ b/third_party/boost/circular_buffer @@ -0,0 +1 @@ +Subproject commit 05a83223e4494edd86d099b672eba4367b45677b diff --git a/third_party/boost/concept_check b/third_party/boost/concept_check new file mode 160000 index 0000000..37c9bdd --- /dev/null +++ b/third_party/boost/concept_check @@ -0,0 +1 @@ +Subproject commit 37c9bddf0bdefaaae0ca5852c1a153d9fc43f278 diff --git a/third_party/boost/config b/third_party/boost/config new file mode 160000 index 0000000..601598f --- /dev/null +++ b/third_party/boost/config @@ -0,0 +1 @@ +Subproject commit 601598f8325350acb0c905d8f3293c17ae61cae3 diff --git a/third_party/boost/container b/third_party/boost/container new file mode 160000 index 0000000..6e697d7 --- /dev/null +++ b/third_party/boost/container @@ -0,0 +1 @@ +Subproject commit 6e697d796897b32b471b4f0740dcaa03d8ee57cc diff --git a/third_party/boost/container_hash b/third_party/boost/container_hash new file mode 160000 index 0000000..7288df8 --- /dev/null +++ b/third_party/boost/container_hash @@ -0,0 +1 @@ +Subproject commit 7288df8beea1c3c8222cd48af1c07c589f7d3f8a diff --git a/third_party/boost/context b/third_party/boost/context new file mode 160000 index 0000000..0867936 --- /dev/null +++ b/third_party/boost/context @@ -0,0 +1 @@ +Subproject commit 08679361bd0feb2ef3a6e7e9562a181f7e9c957d diff --git a/third_party/boost/conversion b/third_party/boost/conversion new file mode 160000 index 0000000..9f285ef --- /dev/null +++ b/third_party/boost/conversion @@ -0,0 +1 @@ +Subproject commit 9f285ef0c43c101e49b37bf5e6085e8d635887dc diff --git a/third_party/boost/core b/third_party/boost/core new file mode 160000 index 0000000..0a35bb6 --- /dev/null +++ b/third_party/boost/core @@ -0,0 +1 @@ +Subproject commit 0a35bb6a20bd13e85ff492a5be01b3894807a0d1 diff --git a/third_party/boost/coroutine b/third_party/boost/coroutine new file mode 160000 index 0000000..1e1347c --- /dev/null +++ b/third_party/boost/coroutine @@ -0,0 +1 @@ +Subproject commit 1e1347c0b1910b9310ec1719edad8b0bf2fd03c8 diff --git a/third_party/boost/date_time b/third_party/boost/date_time new file mode 160000 index 0000000..3971490 --- /dev/null +++ b/third_party/boost/date_time @@ -0,0 +1 @@ +Subproject commit 39714907b7d32ed8f005b5a01d1c2b885b5717b3 diff --git a/third_party/boost/describe b/third_party/boost/describe new file mode 160000 index 0000000..fad199e --- /dev/null +++ b/third_party/boost/describe @@ -0,0 +1 @@ +Subproject commit fad199e782ca027957cbba6be7bbec1dee48afba diff --git a/third_party/boost/detail b/third_party/boost/detail new file mode 160000 index 0000000..845567f --- /dev/null +++ b/third_party/boost/detail @@ -0,0 +1 @@ +Subproject commit 845567f026b6e7606b237c92aa8337a1457b672b diff --git a/third_party/boost/endian b/third_party/boost/endian new file mode 160000 index 0000000..c9b436e --- /dev/null +++ b/third_party/boost/endian @@ -0,0 +1 @@ +Subproject commit c9b436e5dfce85e8ae365e5aabbb872dd35c29eb diff --git a/third_party/boost/exception b/third_party/boost/exception new file mode 160000 index 0000000..b9170a0 --- /dev/null +++ b/third_party/boost/exception @@ -0,0 +1 @@ +Subproject commit b9170a02f102250b308c9f94ed6593c5f30eab39 diff --git a/third_party/boost/filesystem b/third_party/boost/filesystem new file mode 160000 index 0000000..a10762e --- /dev/null +++ b/third_party/boost/filesystem @@ -0,0 +1 @@ +Subproject commit a10762e8b130b1a363cbc0dfe42647dbbc4b11da diff --git a/third_party/boost/function b/third_party/boost/function new file mode 160000 index 0000000..6876969 --- /dev/null +++ b/third_party/boost/function @@ -0,0 +1 @@ +Subproject commit 6876969bfca3b83c90480f7e276b23f6f5a9310c diff --git a/third_party/boost/function_types b/third_party/boost/function_types new file mode 160000 index 0000000..8953358 --- /dev/null +++ b/third_party/boost/function_types @@ -0,0 +1 @@ +Subproject commit 895335874d67987ada0d8bf6ca1725e70642ed49 diff --git a/third_party/boost/functional b/third_party/boost/functional new file mode 160000 index 0000000..6a573e4 --- /dev/null +++ b/third_party/boost/functional @@ -0,0 +1 @@ +Subproject commit 6a573e4b8333ee63ee62ce95558c3667348db233 diff --git a/third_party/boost/fusion b/third_party/boost/fusion new file mode 160000 index 0000000..7d4c03f --- /dev/null +++ b/third_party/boost/fusion @@ -0,0 +1 @@ +Subproject commit 7d4c03fa032299f2d46149b7b3136c9fd43e4f81 diff --git a/third_party/boost/integer b/third_party/boost/integer new file mode 160000 index 0000000..e7ed991 --- /dev/null +++ b/third_party/boost/integer @@ -0,0 +1 @@ +Subproject commit e7ed9918c16f11a9b885b043f7aa7994b3e21582 diff --git a/third_party/boost/intrusive b/third_party/boost/intrusive new file mode 160000 index 0000000..e997641 --- /dev/null +++ b/third_party/boost/intrusive @@ -0,0 +1 @@ +Subproject commit e997641e7d385748d706209c2fff28871c31c667 diff --git a/third_party/boost/io b/third_party/boost/io new file mode 160000 index 0000000..342e4c6 --- /dev/null +++ b/third_party/boost/io @@ -0,0 +1 @@ +Subproject commit 342e4c6d10d586058818daa84201a2d301357a53 diff --git a/third_party/boost/iterator b/third_party/boost/iterator new file mode 160000 index 0000000..dfe11e7 --- /dev/null +++ b/third_party/boost/iterator @@ -0,0 +1 @@ +Subproject commit dfe11e71443edd8e798da50984a8459134dfbc16 diff --git a/third_party/boost/json b/third_party/boost/json new file mode 160000 index 0000000..db92f8c --- /dev/null +++ b/third_party/boost/json @@ -0,0 +1 @@ +Subproject commit db92f8c22360990f450fe27b86ea1a5830b5cf05 diff --git a/third_party/boost/leaf b/third_party/boost/leaf new file mode 160000 index 0000000..ed8f9cd --- /dev/null +++ b/third_party/boost/leaf @@ -0,0 +1 @@ +Subproject commit ed8f9cd32f4fde695d497502f696f6f861b68559 diff --git a/third_party/boost/lexical_cast b/third_party/boost/lexical_cast new file mode 160000 index 0000000..fc5ffb6 --- /dev/null +++ b/third_party/boost/lexical_cast @@ -0,0 +1 @@ +Subproject commit fc5ffb67f8fcdce0f843460f9374eecddc5ac5a5 diff --git a/third_party/boost/list b/third_party/boost/list new file mode 100644 index 0000000..11d3040 --- /dev/null +++ b/third_party/boost/list @@ -0,0 +1,64 @@ +algorithm +align +array +assert +atomic +bind +chrono +circular_buffer +concept_check +config +container +container_hash +context +conversion +core +coroutine +date_time +describe +detail +endian +exception +filesystem +function +functional +function_types +fusion +integer +intrusive +io +iterator +json +leaf +lexical_cast +logic +move +mp11 +mpl +numeric_conversion +optional +pool +predef +preprocessor +range +ratio +rational +regex +smart_ptr +static_assert +static_string +system +throw_exception +tokenizer +tuple +type_index +typeof +type_traits +unordered +url +utility +variant2 +winapi +asio +beast +mysql diff --git a/third_party/boost/logic b/third_party/boost/logic new file mode 160000 index 0000000..1457784 --- /dev/null +++ b/third_party/boost/logic @@ -0,0 +1 @@ +Subproject commit 145778490c2d332c1411df6a5274a4b53ec3e091 diff --git a/third_party/boost/move b/third_party/boost/move new file mode 160000 index 0000000..7c01072 --- /dev/null +++ b/third_party/boost/move @@ -0,0 +1 @@ +Subproject commit 7c01072629d83a7b54c99de70ef535d699ebd200 diff --git a/third_party/boost/mp11 b/third_party/boost/mp11 new file mode 160000 index 0000000..863d8b8 --- /dev/null +++ b/third_party/boost/mp11 @@ -0,0 +1 @@ +Subproject commit 863d8b8d2b20f2acd0b5870f23e553df9ce90e6c diff --git a/third_party/boost/mpl b/third_party/boost/mpl new file mode 160000 index 0000000..b440c45 --- /dev/null +++ b/third_party/boost/mpl @@ -0,0 +1 @@ +Subproject commit b440c45c2810acbddc917db057f2e5194da1a199 diff --git a/third_party/boost/mysql b/third_party/boost/mysql new file mode 160000 index 0000000..bf2eb59 --- /dev/null +++ b/third_party/boost/mysql @@ -0,0 +1 @@ +Subproject commit bf2eb5969242c35d0c82bcb01491994a40f93517 diff --git a/third_party/boost/numeric_conversion b/third_party/boost/numeric_conversion new file mode 160000 index 0000000..50a1eae --- /dev/null +++ b/third_party/boost/numeric_conversion @@ -0,0 +1 @@ +Subproject commit 50a1eae942effb0a9b90724323ef8f2a67e7984a diff --git a/third_party/boost/optional b/third_party/boost/optional new file mode 160000 index 0000000..b7a1d66 --- /dev/null +++ b/third_party/boost/optional @@ -0,0 +1 @@ +Subproject commit b7a1d666f1fd658ff2d758dffe6a4cb9d11e07c1 diff --git a/third_party/boost/pool b/third_party/boost/pool new file mode 160000 index 0000000..8ec1be1 --- /dev/null +++ b/third_party/boost/pool @@ -0,0 +1 @@ +Subproject commit 8ec1be1e82ba559744ecfa3c6ec13f71f9c175cc diff --git a/third_party/boost/predef b/third_party/boost/predef new file mode 160000 index 0000000..614546d --- /dev/null +++ b/third_party/boost/predef @@ -0,0 +1 @@ +Subproject commit 614546d6fac1e68cd3511d3289736f31d5aed1eb diff --git a/third_party/boost/preprocessor b/third_party/boost/preprocessor new file mode 160000 index 0000000..667e87b --- /dev/null +++ b/third_party/boost/preprocessor @@ -0,0 +1 @@ +Subproject commit 667e87b3392db338a919cbe0213979713aca52e3 diff --git a/third_party/boost/range b/third_party/boost/range new file mode 160000 index 0000000..f0e1093 --- /dev/null +++ b/third_party/boost/range @@ -0,0 +1 @@ +Subproject commit f0e109312cab295a660ae94b138cb5f8fc7d182f diff --git a/third_party/boost/ratio b/third_party/boost/ratio new file mode 160000 index 0000000..d5b33ca --- /dev/null +++ b/third_party/boost/ratio @@ -0,0 +1 @@ +Subproject commit d5b33caa7d564be9be6d962b18659b7741d764ac diff --git a/third_party/boost/rational b/third_party/boost/rational new file mode 160000 index 0000000..5646231 --- /dev/null +++ b/third_party/boost/rational @@ -0,0 +1 @@ +Subproject commit 564623136417068916495e2b24737054d607347c diff --git a/third_party/boost/regex b/third_party/boost/regex new file mode 160000 index 0000000..237e69c --- /dev/null +++ b/third_party/boost/regex @@ -0,0 +1 @@ +Subproject commit 237e69caf65906d0313c9b852541b07fa84a99c1 diff --git a/third_party/boost/smart_ptr b/third_party/boost/smart_ptr new file mode 160000 index 0000000..ef0e40b --- /dev/null +++ b/third_party/boost/smart_ptr @@ -0,0 +1 @@ +Subproject commit ef0e40bcda63af57b7ee8592e160ac1b7573488d diff --git a/third_party/boost/static_assert b/third_party/boost/static_assert new file mode 160000 index 0000000..45eec41 --- /dev/null +++ b/third_party/boost/static_assert @@ -0,0 +1 @@ +Subproject commit 45eec41c293bc5cd36ec3ed83671f70bc1aadc9f diff --git a/third_party/boost/static_string b/third_party/boost/static_string new file mode 160000 index 0000000..42bb99e --- /dev/null +++ b/third_party/boost/static_string @@ -0,0 +1 @@ +Subproject commit 42bb99ed255846befdc5d85615c8feee1d4a5ca6 diff --git a/third_party/boost/system b/third_party/boost/system new file mode 160000 index 0000000..2fc720a --- /dev/null +++ b/third_party/boost/system @@ -0,0 +1 @@ +Subproject commit 2fc720a1cbe51d588fecc4e0af9417bd769381d8 diff --git a/third_party/boost/throw_exception b/third_party/boost/throw_exception new file mode 160000 index 0000000..7c8ec21 --- /dev/null +++ b/third_party/boost/throw_exception @@ -0,0 +1 @@ +Subproject commit 7c8ec2114bc1f9ab2a8afbd629b96fbdd5901294 diff --git a/third_party/boost/tokenizer b/third_party/boost/tokenizer new file mode 160000 index 0000000..90106f1 --- /dev/null +++ b/third_party/boost/tokenizer @@ -0,0 +1 @@ +Subproject commit 90106f155bd72b62aaca0d9ad826f4132030dba0 diff --git a/third_party/boost/tuple b/third_party/boost/tuple new file mode 160000 index 0000000..b67941d --- /dev/null +++ b/third_party/boost/tuple @@ -0,0 +1 @@ +Subproject commit b67941dd7d03536a854b96f001954792311ab515 diff --git a/third_party/boost/type_index b/third_party/boost/type_index new file mode 160000 index 0000000..e37bc99 --- /dev/null +++ b/third_party/boost/type_index @@ -0,0 +1 @@ +Subproject commit e37bc99e85e85bcac420ac1d4c1a8a5bca280d47 diff --git a/third_party/boost/type_traits b/third_party/boost/type_traits new file mode 160000 index 0000000..821c53c --- /dev/null +++ b/third_party/boost/type_traits @@ -0,0 +1 @@ +Subproject commit 821c53c0b45529dca508fadc7d018fb1bb6ece21 diff --git a/third_party/boost/typeof b/third_party/boost/typeof new file mode 160000 index 0000000..4bc9de3 --- /dev/null +++ b/third_party/boost/typeof @@ -0,0 +1 @@ +Subproject commit 4bc9de322cd44373435540d4e6c8f207892fc9a0 diff --git a/third_party/boost/unordered b/third_party/boost/unordered new file mode 160000 index 0000000..67c5cdb --- /dev/null +++ b/third_party/boost/unordered @@ -0,0 +1 @@ +Subproject commit 67c5cdb3a69f0b92d2779880ce9aa1d46e54cf7b diff --git a/third_party/boost/url b/third_party/boost/url new file mode 160000 index 0000000..8a82041 --- /dev/null +++ b/third_party/boost/url @@ -0,0 +1 @@ +Subproject commit 8a8204103f21b5fa0c05d33556f3395913f0e9c2 diff --git a/third_party/boost/utility b/third_party/boost/utility new file mode 160000 index 0000000..217f734 --- /dev/null +++ b/third_party/boost/utility @@ -0,0 +1 @@ +Subproject commit 217f7346f63d189d2ba1093c42bf3db810a0550c diff --git a/third_party/boost/variant2 b/third_party/boost/variant2 new file mode 160000 index 0000000..3298078 --- /dev/null +++ b/third_party/boost/variant2 @@ -0,0 +1 @@ +Subproject commit 3298078c8b3cdbe2691b1b2138e317973801fbf2 diff --git a/third_party/boost/winapi b/third_party/boost/winapi new file mode 160000 index 0000000..39396bd --- /dev/null +++ b/third_party/boost/winapi @@ -0,0 +1 @@ +Subproject commit 39396bd78254053f3137510478e8f956bd2b83d4 diff --git a/third_party/tomlplusplus b/third_party/tomlplusplus new file mode 160000 index 0000000..89406c7 --- /dev/null +++ b/third_party/tomlplusplus @@ -0,0 +1 @@ +Subproject commit 89406c77e6480c212b17e0ce7939f9ee833e909d