Initial commit of rewrite based on base/ library

Should make the code actually possible to finish :v
This commit is contained in:
Lily Tsuru 2024-02-05 06:24:48 -05:00
commit 4851371f56
127 changed files with 3349 additions and 0 deletions

44
.clang-format Normal file
View File

@ -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

8
.gitignore vendored Normal file
View File

@ -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*

198
.gitmodules vendored Normal file
View File

@ -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

30
CMakeLists.txt Normal file
View File

@ -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)

76
cmake/CompilerFlags.cmake Normal file
View File

@ -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")

22
cmake/Policies.cmake Normal file
View File

@ -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.

24
cmake/ProjectFuncs.cmake Normal file
View File

@ -0,0 +1,24 @@
function(lobbyserver_target target)
target_compile_definitions(${target} PRIVATE "$<$<CONFIG:DEBUG>: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()

7
doc/init.sql Normal file
View File

@ -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;

41
doc/lobbyserver.service Normal file
View File

@ -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

16
doc/lobbyserver.toml Normal file
View File

@ -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"

0
doc/notes.md Normal file
View File

18
lib/base/CMakeLists.txt Normal file
View File

@ -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)

14
lib/base/assert.cpp Normal file
View File

@ -0,0 +1,14 @@
#include <cstdio>
#include <cstdlib>
#include <base/logger.hpp>
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

39
lib/base/assert.hpp Normal file
View File

@ -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 <format>
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(); \
}

View File

@ -0,0 +1,31 @@
#pragma once
#include <base/types.hpp>
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 <class Predicate>
Awaitable<void> Wait(Predicate pred) {
while(!pred()) {
try {
co_await timer.async_wait(asio::deferred);
} catch(...) {
// swallow errors
}
}
co_return;
}
private:
SteadyTimer timer;
};
} // namespace base

13
lib/base/backoff.cpp Normal file
View File

@ -0,0 +1,13 @@
#include <chrono>
#include <cmath>
#include <base/backoff.hpp>
namespace base {
Awaitable<void> 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<u64>(t)) };
co_await timer.async_wait(asio::deferred);
}
} // namespace base

20
lib/base/backoff.hpp Normal file
View File

@ -0,0 +1,20 @@
#pragma once
#include <base/types.hpp>
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<void> Delay();
inline void Reset() { count = 0; }
private:
double base {};
u32 count {};
};
} // namespace base

120
lib/base/channel.hpp Normal file
View File

@ -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 <base/assert.hpp>
#include <base/types.hpp>
#include <boost/asio/experimental/concurrent_channel.hpp>
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 <class Sig>
using ChannelImplType = asio::experimental::concurrent_channel<Sig>;
template <class Send>
struct Channel {
Channel(asio::any_io_executor exec, usize sendQueueLen = 0) : exec(exec), channel(exec, sendQueueLen) {}
Awaitable<void> Write(const Send& value) { co_await channel.async_send(bsys::error_code {}, value, asio::deferred); }
Awaitable<Send> 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<void> 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<void(bsys::error_code, Send)> channel;
};
template <>
struct Channel<void> {
Channel(asio::any_io_executor exec) : exec(exec), channel(exec) {}
Awaitable<void> Write() { co_await channel.async_send(bsys::error_code {}, asio::deferred); }
Awaitable<void> 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<void> Close() {
channel.close();
co_return;
}
auto& Raw() { return channel; }
private:
asio::any_io_executor exec;
ChannelImplType<void(bsys::error_code)> channel;
};
// Channel adapters to make producer/consumer logic *way* more typesafe.
template <class T>
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<T>& chan) : chan(chan) {}
bool IsOpen() const { return chan.IsOpen(); }
Awaitable<T> Read() { return chan.Read(); }
private:
Channel<T>& chan;
};
template <class T>
struct WriteChannel {
WriteChannel(Channel<T>& chan) : chan(chan) {}
bool IsOpen() const { return chan.IsOpen(); }
Awaitable<void> Write(const T& val) { co_await chan.Write(val); }
private:
Channel<T>& chan;
};
// A little bit depressing that this can't be sfinae'd away
// (or `requires`'d away), but oh well
template <>
struct WriteChannel<void> {
WriteChannel(Channel<void>& chan) : chan(chan) {}
bool IsOpen() const { return chan.IsOpen(); }
Awaitable<void> Write() { co_await chan.Write(); }
private:
Channel<void>& chan;
};
} // namespace base

27
lib/base/fixed_string.hpp Normal file
View File

@ -0,0 +1,27 @@
#pragma once
#include <base/types.hpp>
namespace base {
/// A compile time fixed string, fit for usage as a C++20 cNTTP.
template <usize N>
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 <usize N>
FixedString(char const (&)[N]) -> FixedString<N - 1>;
} // namespace base

32
lib/base/fourcc.hpp Normal file
View File

@ -0,0 +1,32 @@
#pragma once
#include <base/fixed_string.hpp>
#include <bit>
namespace base {
/// Type system magic
enum class FourCC32_t : u32 {};
template <FixedString fccString, std::endian Endian = std::endian::little>
consteval FourCC32_t FourCC32() {
static_assert(fccString.Length() == 4, "Provided string is not a FourCC");
switch(Endian) {
case std::endian::little:
return static_cast<FourCC32_t>((fccString[0]) | (fccString[1] << 8) | (fccString[2] << 16) | (fccString[3] << 24));
case std::endian::big:
return static_cast<FourCC32_t>((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

61
lib/base/html_escape.cpp Normal file
View File

@ -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 <base/html_escape.hpp>
// 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[] = {
"",
"&quot;",
"&amp;",
"&#39;",
"&#47;",
"&lt;",
"&gt;"
};
// clang-format on
namespace base {
std::string HtmlEscape(const std::span<u8> 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<u8> { std::bit_cast<u8*>(input.data()), input.length() });
}
std::string HtmlEscape(const std::string& input) {
return HtmlEscape(std::span<u8> { std::bit_cast<u8*>(input.data()), input.length() });
}
} // namespace base

13
lib/base/html_escape.hpp Normal file
View File

@ -0,0 +1,13 @@
#include <base/types.hpp>
namespace base {
std::string HtmlEscape(const std::span<u8> buffer);
std::string HtmlEscape(const std::string_view);
/// HTML-escape a string.
std::string HtmlEscape(const std::string& input);
} // namespace base

172
lib/base/logger.cpp Normal file
View File

@ -0,0 +1,172 @@
#include <base/assert.hpp>
#include <base/logger.hpp>
#include <base/types.hpp>
#include <condition_variable>
#include <cstddef>
#include <deque>
#include <iostream>
#include <mutex>
#include <thread>
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<std::size_t>(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<u16>(src));
}
}
struct LoggerThreadData {
// Logger thread stuff
std::thread loggerThread;
std::mutex logQueueMutex;
std::condition_variable logQueueCv;
std::deque<MessageData> 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<std::mutex> lk(logQueueMutex);
logQueue.emplace_back(std::move(md));
}
logQueueCv.notify_one();
}
};
Unique<LoggerThreadData> threadData;
void LoggerGlobalState::LoggerThread() {
auto& self = The();
// Fancy thread names.
pthread_setname_np(pthread_self(), "LoggerThread");
while(true) {
std::unique_lock<std::mutex> 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<LoggerThreadData>();
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<std::chrono::milliseconds>(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

212
lib/base/logger.hpp Normal file
View File

@ -0,0 +1,212 @@
//! Logging utilities for the Support Library
//! Using Standard C++ <format>
#pragma once
#include <chrono>
#include <cstdint>
#include <format>
#include <base/types.hpp>
#include <vector>
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<u16>(source);
auto cmpbits = std::bit_cast<u16>(component);
return static_cast<ChannelId>((static_cast<u32>(srcbits) << 16 | cmpbits));
}
/// Splits a channel ID into its individual components.
constexpr static void ChannelIdToComponents(ChannelId id, MessageSource& src, MessageComponentSource& component) {
src = static_cast<MessageSource>((static_cast<u32>(id) & 0xffff0000) >> 16);
component = static_cast<MessageComponentSource>(static_cast<u32>(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<Sink*> 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 <class... Args>
inline void Debug(std::string_view fmt, Args... args) {
VOut(MessageSeverity::Debug, fmt, std::make_format_args(std::forward<Args>(args)...));
}
template <class... Args>
inline void Info(std::string_view fmt, Args... args) {
VOut(MessageSeverity::Info, fmt, std::make_format_args(std::forward<Args>(args)...));
}
template <class... Args>
inline void Warning(std::string_view fmt, Args... args) {
VOut(MessageSeverity::Warning, fmt, std::make_format_args(std::forward<Args>(args)...));
}
template <class... Args>
inline void Error(std::string_view fmt, Args... args) {
VOut(MessageSeverity::Error, fmt, std::make_format_args(std::forward<Args>(args)...));
}
template <class... Args>
inline void Fatal(std::string_view fmt, Args... args) {
VOut(MessageSeverity::Fatal, fmt, std::make_format_args(std::forward<Args>(args)...));
}
private:
void VOut(MessageSeverity severity, std::string_view format, std::format_args args);
ChannelId channelId;
};
template <class... Args>
constexpr void LogDebug(std::string_view format, Args... args) {
Logger::Global().Debug(format, std::forward<Args>(args)...);
}
template <class... Args>
constexpr void LogInfo(std::string_view format, Args... args) {
Logger::Global().Info(format, std::forward<Args>(args)...);
}
template <class... Args>
constexpr void LogWarning(std::string_view format, Args... args) {
Logger::Global().Warning(format, std::forward<Args>(args)...);
}
template <class... Args>
constexpr void LogError(std::string_view format, Args... args) {
Logger::Global().Error(format, std::forward<Args>(args)...);
}
template <class... Args>
constexpr void LogFatal(std::string_view format, Args... args) {
Logger::Global().Fatal(format, std::forward<Args>(args)...);
}
} // namespace base

View File

@ -0,0 +1,47 @@
#pragma once
#include <bit>
#include <base/types.hpp>
namespace base {
// required until C++23 support :(
namespace detail {
template <class T, class U>
constexpr T PunCast(U v) {
return *std::bit_cast<const T*>(&v);
}
template <class T>
constexpr T ByteSwap(T v) {
if constexpr(sizeof(T) == 2)
return static_cast<T>(__builtin_bswap16(PunCast<u16>(v)));
else if constexpr(sizeof(T) == 4)
return static_cast<T>(__builtin_bswap32(PunCast<u32>(v)));
else if constexpr(sizeof(T) == 8)
return static_cast<T>(__builtin_bswap64(PunCast<u64>(v)));
return v;
}
} // namespace detail
/// A network-order field, swapped automatically
template <class T>
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

98
lib/base/rate_limit.hpp Normal file
View File

@ -0,0 +1,98 @@
#pragma once
#include <chrono>
#include <base/types.hpp>
namespace base {
template <class Clock = std::chrono::steady_clock, class Dur = std::chrono::microseconds>
struct BasicRateLimiter final {
using TimePointType = std::chrono::time_point<Clock, Dur>;
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<TimePointType> lastEvent {};
/// Current event count
std::atomic<EventGrainType> eventCount {};
};
template <class Dur2, class Dur3>
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<Dur>(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 <class Dur2>
constexpr void SetMaxRate(Dur2 dur) noexcept {
maxRate = dur;
}
template <class Dur2>
constexpr void SetCooldownTime(Dur2 dur) noexcept {
cooldownTime = dur;
}
private:
Dur cooldownTime;
Dur maxRate;
EventGrainType maxEventCount;
};
using RateLimiter = BasicRateLimiter<>;
} // namespace base

18
lib/base/stdout_sink.cpp Normal file
View File

@ -0,0 +1,18 @@
#include <base/stdout_sink.hpp>
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

17
lib/base/stdout_sink.hpp Normal file
View File

@ -0,0 +1,17 @@
#pragma once
#include <base/logger.hpp>
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

145
lib/base/types.hpp Normal file
View File

@ -0,0 +1,145 @@
//! Core types and includes
#pragma once
#include <cstdint>
#include <memory>
// 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 <impl/asio_config.hpp>
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 <class T>
struct Point {
T x;
T y;
};
template <class T>
struct Size {
T width;
T height;
constexpr usize Linear() const { return width * height; }
};
template <class T>
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<T> size) : x(0), y(0), width(size.width), height(size.height) {}
/**
* Get the origin coordinate as a point.
* \return a Point<T> with the origin.
*/
constexpr auto GetOrigin() const { return Point<T> { .x = x, .y = y }; }
/**
* Get the size of this rect.
* \return a Point<T> which contains the calculated size of the rect
*/
constexpr auto GetSize() const { return Size<T> { .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<u8>(((pixel & 0xF800) >> 11) << 3),
.g = static_cast<u8>(((pixel & 0x7E0) >> 5) << 2),
.b = static_cast<u8>((pixel & 0x1F) << 3),
.a = 255 };
}
};
using detail::Point;
using detail::Rect;
using detail::Size;
template <class T>
using Ref = std::shared_ptr<T>;
template <class T, class Deleter = std::default_delete<T>>
using Unique = std::unique_ptr<T, Deleter>;
template <typename... Ts>
struct OverloadVisitor : Ts... {
using Ts::operator()...;
};
template <class... Ts>
OverloadVisitor(Ts...) -> OverloadVisitor<Ts...>;
template <class T, auto* Free>
struct UniqueCDeleter {
constexpr void operator()(T* ptr) {
if(ptr)
Free(reinterpret_cast<void*>(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 <class T, auto Free = std::free>
using CUnique = base::Unique<T, UniqueCDeleter<T, Free>>;
} // namespace base

78
lib/base/xoshiro.hpp Normal file
View File

@ -0,0 +1,78 @@
#pragma once
#include <climits> // CHAR_BIT
#include <cstddef>
#include <cstdint>
#include <random>
namespace base {
namespace detail {
template <class T>
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<std::uint64_t>() - 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

25
lib/http/CMakeLists.txt Normal file
View File

@ -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)

21
lib/http/config.hpp Normal file
View File

@ -0,0 +1,21 @@
#pragma once
#include <impl/asio_config.hpp>
// 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<GenericProtocol>;
} // namespace base::http

View File

@ -0,0 +1,44 @@
#include <http/proxy_address.hpp>
#include <base/assert.hpp>
#include <base/types.hpp>
namespace base::http {
asio::ip::address GetProxyAddress(asio::ip::address fallback, beast::http::request<beast::http::string_body>& 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

View File

@ -0,0 +1,22 @@
#pragma once
// clang-format off
#include <http/config.hpp>
#include <impl/asio_config.hpp>
// clang-format on
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
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<beast::http::string_body>& req);
} // namespace base::http

10
lib/http/r3_helpers.hpp Normal file
View File

@ -0,0 +1,10 @@
#pragma once
#include <r3.h>
#include <base/types.hpp>
namespace base::http {
inline std::string R3IovecString(r3_iovec_t& iovec) {
return std::string(iovec.base, iovec.len);
}
} // namespace base::http

68
lib/http/request.hpp Normal file
View File

@ -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 <cstdint>
//clang-format on
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/url/url_view.hpp>
#include <http/r3_helpers.hpp>
#include <r3.hpp>
#include <base/assert.hpp>
#include <base/types.hpp>
#include <unordered_map>
namespace base::http {
struct Request {
using BeastRequest = beast::http::request<beast::http::string_body>;
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<std::string, std::string> matches;
// Query string parameters
std::unordered_map<std::string, std::string> query;
private:
friend struct Server;
BeastRequest req;
burl::url_view url;
};
} // namespace base::http

25
lib/http/response.hpp Normal file
View File

@ -0,0 +1,25 @@
#pragma once
// See lib/http/request.hpp
//clang-format off
#include <cstdint>
//clang-format on
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/message_generator.hpp>
#include <base/types.hpp>
//
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

406
lib/http/server.cpp Normal file
View File

@ -0,0 +1,406 @@
#include <sys/socket.h>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/http/read.hpp>
#include <boost/beast/http/write.hpp>
#include <boost/beast/websocket/rfc6455.hpp>
#include <boost/url.hpp>
#include <boost/url/parse.hpp>
#include <http/proxy_address.hpp>
#include <http/r3_helpers.hpp>
#include <http/server.hpp>
#include <http/utils.hpp>
#include <r3.hpp>
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<void> 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<sockaddr_in*>(sa);
return asio::ip::address_v4 { htonl(sain->sin_addr.s_addr) };
} break;
case AF_INET6: {
auto* sain6 = std::bit_cast<sockaddr_in6*>(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<void> 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<void>(addr);
static_cast<void>(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<Server::RouteHandler, Server::WebSocketRouteHandler>;
template <class ProtocolEndpoint>
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<void*>(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<void*>(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<std::optional<Response>> Route(Request::BeastRequest& req, asio::ip::address srcAddress, Session& session) {
auto const noSuchRouteError = [&]() -> beast::http::message_generator {
beast::http::response<beast::http::string_body> 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<beast::http::string_body> 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<beast::http::string_body> 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<i32>(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<usize>(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<Server::RouteHandler>(&handler); httpHandler) {
co_return co_await (*httpHandler)(routed_request);
} else if(auto* wsHandler = std::get_if<Server::WebSocketRouteHandler>(&handler); wsHandler) {
if(!beast::websocket::is_upgrade(routed_request.Native())) {
auto res = wsRequired();
co_return Response { std::move(res) };
}
auto ws = std::make_shared<WebSocketClient>(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<void> 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<SOL_SOCKET, SO_REUSEPORT>;
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<void> RunSession(StreamType stream) {
// std::shared_ptr<Session> session = std::make_shared<Session>(*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<RouteVariant> handlerStorage;
asio::any_io_executor exec;
EndpointType ep;
bool listening { false };
asio::basic_socket_acceptor<GenericProtocol> 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<Impl>(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

41
lib/http/server.hpp Normal file
View File

@ -0,0 +1,41 @@
#pragma once
#include <http/config.hpp>
#include <http/request.hpp>
#include <http/response.hpp>
#include <http/websocket_client.hpp>
#include <base/logger.hpp>
#include <base/types.hpp>
namespace base::http {
/// A HTTP server.
struct Server {
using SocketType = GenericProtocol::socket;
using StreamType = BeastStream<GenericProtocol>;
using EndpointType = GenericProtocol::endpoint;
using RouteHandler = std::function<Awaitable<Response>(Request& request)>;
using WebSocketRouteHandler = std::function<Awaitable<void>(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> impl;
};
} // namespace base::http

1
lib/http/third_party/r3 vendored Submodule

@ -0,0 +1 @@
Subproject commit ade1527bef91b85a09439f98014d048735605e60

21
lib/http/utils.hpp Normal file
View File

@ -0,0 +1,21 @@
#pragma once
#include <boost/beast/http/message.hpp>
#include <chrono>
#include <base/types.hpp>
namespace base::http {
template <typename Clock>
inline std::string ImfDate(std::chrono::time_point<Clock> point) {
return std::format("{0:%a}, {0:%d %b %Y} {0:%T} GMT",
std::chrono::clock_cast<std::chrono::utc_clock>(std::chrono::time_point_cast<std::chrono::seconds>(point)));
}
// use this on a response please :)
template <class Body, class Fields>
constexpr void SetCommonResponseFields(beast::http::response<Body, Fields>& res) {
res.set(beast::http::field::date, ImfDate(std::chrono::utc_clock::now()));
res.set(beast::http::field::server, "Holgol");
}
} // namespace base::http

View File

@ -0,0 +1,193 @@
#include <cstdint>
#include <http/utils.hpp>
#include <http/websocket_client.hpp>
#include <http/proxy_address.hpp>
#include <base/assert.hpp>
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<bool> 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<void> WebSocketClient::Close() {
return CloseWithReason("Generic close.");
}
Awaitable<void> 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<void> 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<void> 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<WebSocketMessage>(
std::string { reinterpret_cast<char*>(messageBuffer.data().data()), messageBuffer.size() }));
} else if(self->stream.got_binary()) {
co_await self->listener->OnMessage(std::make_shared<WebSocketMessage>(
std::span<u8> { reinterpret_cast<u8*>(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

View File

@ -0,0 +1,81 @@
#pragma once
#include <boost/beast/websocket/stream.hpp>
#include <http/config.hpp>
#include <http/request.hpp>
#include <http/websocket_message.hpp>
#include <base/async_condition_variable.hpp>
#include <base/logger.hpp>
#include <base/types.hpp>
namespace base::http {
// TODO: maybe pimpl?
struct WebSocketClient : public std::enable_shared_from_this<WebSocketClient> {
using RawStream = GenericStream;
using StreamType = beast::websocket::stream<RawStream>;
using EndpointType = typename GenericProtocol::endpoint;
using Ptr = std::shared_ptr<WebSocketClient>;
using MessagePtr = std::shared_ptr<WebSocketMessage>;
using ConstMessagePtr = std::shared_ptr<const WebSocketMessage>;
using MessageHandler = std::function<Awaitable<void>(Ptr, ConstMessagePtr)>;
using CloseHandler = std::function<Awaitable<void>(Ptr)>; // TODO: close reason?
struct Listener {
virtual ~Listener() = default;
virtual Awaitable<void> OnClose() = 0; // TODO: close reason
virtual Awaitable<void> 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<bool> Handshake();
Awaitable<void> Close();
Awaitable<void> CloseWithReason(const std::string& reason);
asio::ip::address Address();
Request& GetUpgradeRequest() { return upgrade; }
private:
StreamType stream;
std::vector<ConstMessagePtr> 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<void> WriteEnd();
Awaitable<void> ReadEnd();
};
} // namespace base::http

View File

@ -0,0 +1,38 @@
#include <http/websocket_message.hpp>
namespace base::http {
WebSocketMessage::WebSocketMessage(const std::string& str) {
payload.emplace<Text>(str);
}
WebSocketMessage::WebSocketMessage(const std::vector<u8>& data) {
payload.emplace<Binary>(data);
}
WebSocketMessage::WebSocketMessage(const std::span<u8>& data) {
auto vec = std::vector<u8> {};
vec.resize(data.size());
std::memcpy(vec.data(), data.data(), data.size());
payload.emplace<Binary>(vec);
}
WebSocketMessage::Type WebSocketMessage::GetType() const {
if(std::holds_alternative<Text>(payload))
return Type::Text;
else if(std::holds_alternative<Binary>(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<Text>(payload).data;
}
const std::vector<u8>& WebSocketMessage::AsBinary() const {
BASE_ASSERT(GetType() == Type::Binary, "WebSocketMessage isn't holding a Binary message");
return std::get<Binary>(payload).data;
}
} // namespace base::http

View File

@ -0,0 +1,41 @@
#pragma once
#include <base/assert.hpp>
#include <base/logger.hpp>
#include <base/types.hpp>
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<u8>& data);
explicit WebSocketMessage(const std::span<u8>& data);
Type GetType() const;
const std::string& AsText() const;
const std::vector<u8>& AsBinary() const;
private:
struct Text {
std::string data;
};
struct Binary {
std::vector<u8> data;
};
// no payload (yet?)
// struct Ping {};
std::variant<Text, Binary> payload;
};
// Helper to more easily build a websocket message object
template <class In>
inline std::shared_ptr<const WebSocketMessage> BuildMessage(const In& in) {
return std::make_shared<WebSocketMessage>(in);
}
} // namespace base::http

39
lib/impl/CMakeLists.txt Normal file
View File

@ -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)

43
lib/impl/asio_config.hpp Normal file
View File

@ -0,0 +1,43 @@
#pragma once
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/as_tuple.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/basic_waitable_timer.hpp>
#include <boost/asio/generic/stream_protocol.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/local/stream_protocol.hpp>
#include <boost/asio/strand.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/asio/deferred.hpp>
#include <boost/beast/core/basic_stream.hpp>
#include <boost/beast/core/error.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/detached.hpp>
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 <class T>
using Awaitable = asio::awaitable<T, ExecutorType>;
template <typename Protocol>
using Acceptor = asio::basic_socket_acceptor<Protocol, ExecutorType>;
template <typename Protocol>
using Socket = asio::basic_stream_socket<Protocol, ExecutorType>;
using SteadyTimer = asio::basic_waitable_timer<std::chrono::steady_clock, asio::wait_traits<std::chrono::steady_clock>, ExecutorType>;
template <typename Protocol>
using BeastStream = beast::basic_stream<Protocol, ExecutorType>;
} // namespace base

6
lib/impl/asio_src.cpp Normal file
View File

@ -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 <boost/asio/impl/src.hpp>
//#include <boost/asio/ssl/impl/src.hpp>

1
lib/impl/beast_src.cpp Normal file
View File

@ -0,0 +1 @@
#include <boost/beast/src.hpp>

2
lib/impl/tomlpp_src.cpp Normal file
View File

@ -0,0 +1,2 @@
#define TOML_IMPLEMENTATION
#include <toml++/toml.hpp>

15
src/CMakeLists.txt Normal file
View File

@ -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
)

104
src/main.cpp Normal file
View File

@ -0,0 +1,104 @@
#include <base/assert.hpp>
#include <base/stdout_sink.hpp>
#include <base/types.hpp>
#include <thread>
#include <boost/asio/thread_pool.hpp>
#include <boost/asio/signal_set.hpp>
#include <toml++/toml.hpp>
std::optional<asio::thread_pool> ioc;
// ls server global here
constexpr static std::string_view CONFIG_FILE = "lobbyserver.toml";
base::Awaitable<void> 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<void> 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<u16>(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;
}

175
src/messages/IMessage.cpp Normal file
View File

@ -0,0 +1,175 @@
#include "IMessage.hpp"
#include <base/logger.hpp>
#include <impl/asio_config.hpp>
#include "WireMessage.hpp"
namespace ls {
bool IMessage::ParseFromInputBuffer(std::span<const u8> 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<char>(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<char>(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<char>(inputBuffer[inputIndex]) == '\"')
break;
val += static_cast<char>(inputBuffer[inputIndex]);
break;
}
break;
}
inputIndex++;
}
// Parse succeeded
return true;
}
void IMessage::SerializeTo(std::vector<u8>& 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<u32>(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<std::string_view> 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<void> Process(base::Ref<ls::Client> 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<IMessage> MessageFactory::CreateMessage(base::FourCC32_t fourCC) {
const auto& factories = GetFactoryMap();
if(const auto it = factories.find(fourCC); it == factories.end())
return std::make_shared<DebugMessage>(fourCC);
else
return (it->second)();
}
} // namespace ls

80
src/messages/IMessage.hpp Normal file
View File

@ -0,0 +1,80 @@
#include <base/fourcc.hpp>
#include <base/types.hpp>
#include <impl/asio_config.hpp>
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<const u8> data);
/// Serializes to a output data buffer.
void SerializeTo(std::vector<u8>& dataBuffer) const;
virtual base::FourCC32_t TypeCode() const = 0;
/// Process a single message.
virtual base::Awaitable<void> Process(base::Ref<Client> client) = 0;
const std::optional<std::string_view> MaybeGetKey(const std::string& key) const;
void SetKey(const std::string& key, const std::string& value);
protected:
/// all properties.
std::unordered_map<std::string, std::string> properties {};
/// The client this message is for.
base::Ref<Client> client {};
};
struct MessageFactory {
static base::Ref<IMessage> CreateMessage(base::FourCC32_t fourCC);
private:
template <base::FixedString fourcc, class Impl>
friend struct MessageMixin;
using FactoryMap = std::unordered_map<base::FourCC32_t, base::Ref<IMessage> (*)()>;
static FactoryMap& GetFactoryMap();
};
template <base::FixedString fourcc, class Impl>
struct MessageMixin : IMessage {
constexpr static auto TYPE_CODE = base::FourCC32<fourcc>();
explicit MessageMixin()
: IMessage() {
static_cast<void>(registered);
}
base::FourCC32_t TypeCode() const override {
return TYPE_CODE;
}
private:
static bool Register() {
MessageFactory::GetFactoryMap().insert({ TYPE_CODE, []() -> base::Ref<IMessage> {
return std::make_shared<Impl>();
} });
return true;
}
static inline bool registered = Register();
};
// :( Makes the boilerplate shorter and sweeter though.
#define LS_MESSAGE(T, fourCC) struct T : public ls::MessageMixin<fourCC, T>
#define LS_MESSAGE_CTOR(T, fourCC) \
using Super = ls::MessageMixin<fourCC, T>; \
explicit T() \
: Super() { \
}
} // namespace ls

View File

@ -0,0 +1,13 @@
#include <impl/asio_config.hpp>
#include "base/logger.hpp"
#include "IMessage.hpp"
LS_MESSAGE(PingMessage, "~png") {
LS_MESSAGE_CTOR(PingMessage, "~png")
base::Awaitable<void> Process(base::Ref<ls::Client> client) override {
base::LogInfo("Got ping message!");
co_return;
}
};

View File

@ -0,0 +1,20 @@
#include <base/network_order.hpp>
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<u32> payloadSize {};
};
// Sanity checking.
static_assert(sizeof(WireMessageHeader) == 12, "Wire message header size is invalid");
} // namespace ls::proto

2
third_party/CMakeLists.txt vendored Normal file
View File

@ -0,0 +1,2 @@
add_subdirectory(boost)
add_subdirectory(tomlplusplus)

11
third_party/boost/CMakeLists.txt vendored Normal file
View File

@ -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()

11
third_party/boost/README.md vendored Normal file
View File

@ -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`

10
third_party/boost/add.sh vendored Executable file
View File

@ -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

1
third_party/boost/algorithm vendored Submodule

@ -0,0 +1 @@
Subproject commit faac048d59948b1990c0a8772a050d8e47279343

1
third_party/boost/align vendored Submodule

@ -0,0 +1 @@
Subproject commit 5ad7df63cd792fbdb801d600b93cad1a432f0151

1
third_party/boost/array vendored Submodule

@ -0,0 +1 @@
Subproject commit ecc47cb42c98261d6abf39fb5575c38eac6db748

1
third_party/boost/asio vendored Submodule

@ -0,0 +1 @@
Subproject commit 2e49b21732e5d1bb3bb98f209164c30816b0bc79

1
third_party/boost/assert vendored Submodule

@ -0,0 +1 @@
Subproject commit a2817b89f48a8fdbe4130762d0d47a355ca5b562

1
third_party/boost/atomic vendored Submodule

@ -0,0 +1 @@
Subproject commit b91d55150f5bb6a26a8560a58ede9164b8812de6

1
third_party/boost/beast vendored Submodule

@ -0,0 +1 @@
Subproject commit dc8798c917a2d527e4d64497b2b85e2694fc037c

1
third_party/boost/bind vendored Submodule

@ -0,0 +1 @@
Subproject commit dded373cc705781b8d0778343892a290aa87b09f

10
third_party/boost/bump_version.sh vendored Executable file
View File

@ -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

1
third_party/boost/chrono vendored Submodule

@ -0,0 +1 @@
Subproject commit ee0d6d543a37d9b7243682549e9ae359eb89daa9

1
third_party/boost/circular_buffer vendored Submodule

@ -0,0 +1 @@
Subproject commit 05a83223e4494edd86d099b672eba4367b45677b

1
third_party/boost/concept_check vendored Submodule

@ -0,0 +1 @@
Subproject commit 37c9bddf0bdefaaae0ca5852c1a153d9fc43f278

1
third_party/boost/config vendored Submodule

@ -0,0 +1 @@
Subproject commit 601598f8325350acb0c905d8f3293c17ae61cae3

1
third_party/boost/container vendored Submodule

@ -0,0 +1 @@
Subproject commit 6e697d796897b32b471b4f0740dcaa03d8ee57cc

1
third_party/boost/container_hash vendored Submodule

@ -0,0 +1 @@
Subproject commit 7288df8beea1c3c8222cd48af1c07c589f7d3f8a

1
third_party/boost/context vendored Submodule

@ -0,0 +1 @@
Subproject commit 08679361bd0feb2ef3a6e7e9562a181f7e9c957d

1
third_party/boost/conversion vendored Submodule

@ -0,0 +1 @@
Subproject commit 9f285ef0c43c101e49b37bf5e6085e8d635887dc

1
third_party/boost/core vendored Submodule

@ -0,0 +1 @@
Subproject commit 0a35bb6a20bd13e85ff492a5be01b3894807a0d1

1
third_party/boost/coroutine vendored Submodule

@ -0,0 +1 @@
Subproject commit 1e1347c0b1910b9310ec1719edad8b0bf2fd03c8

1
third_party/boost/date_time vendored Submodule

@ -0,0 +1 @@
Subproject commit 39714907b7d32ed8f005b5a01d1c2b885b5717b3

1
third_party/boost/describe vendored Submodule

@ -0,0 +1 @@
Subproject commit fad199e782ca027957cbba6be7bbec1dee48afba

1
third_party/boost/detail vendored Submodule

@ -0,0 +1 @@
Subproject commit 845567f026b6e7606b237c92aa8337a1457b672b

1
third_party/boost/endian vendored Submodule

@ -0,0 +1 @@
Subproject commit c9b436e5dfce85e8ae365e5aabbb872dd35c29eb

1
third_party/boost/exception vendored Submodule

@ -0,0 +1 @@
Subproject commit b9170a02f102250b308c9f94ed6593c5f30eab39

1
third_party/boost/filesystem vendored Submodule

@ -0,0 +1 @@
Subproject commit a10762e8b130b1a363cbc0dfe42647dbbc4b11da

1
third_party/boost/function vendored Submodule

@ -0,0 +1 @@
Subproject commit 6876969bfca3b83c90480f7e276b23f6f5a9310c

1
third_party/boost/function_types vendored Submodule

@ -0,0 +1 @@
Subproject commit 895335874d67987ada0d8bf6ca1725e70642ed49

1
third_party/boost/functional vendored Submodule

@ -0,0 +1 @@
Subproject commit 6a573e4b8333ee63ee62ce95558c3667348db233

1
third_party/boost/fusion vendored Submodule

@ -0,0 +1 @@
Subproject commit 7d4c03fa032299f2d46149b7b3136c9fd43e4f81

1
third_party/boost/integer vendored Submodule

@ -0,0 +1 @@
Subproject commit e7ed9918c16f11a9b885b043f7aa7994b3e21582

1
third_party/boost/intrusive vendored Submodule

@ -0,0 +1 @@
Subproject commit e997641e7d385748d706209c2fff28871c31c667

1
third_party/boost/io vendored Submodule

@ -0,0 +1 @@
Subproject commit 342e4c6d10d586058818daa84201a2d301357a53

1
third_party/boost/iterator vendored Submodule

@ -0,0 +1 @@
Subproject commit dfe11e71443edd8e798da50984a8459134dfbc16

1
third_party/boost/json vendored Submodule

@ -0,0 +1 @@
Subproject commit db92f8c22360990f450fe27b86ea1a5830b5cf05

1
third_party/boost/leaf vendored Submodule

@ -0,0 +1 @@
Subproject commit ed8f9cd32f4fde695d497502f696f6f861b68559

1
third_party/boost/lexical_cast vendored Submodule

@ -0,0 +1 @@
Subproject commit fc5ffb67f8fcdce0f843460f9374eecddc5ac5a5

64
third_party/boost/list vendored Normal file
View File

@ -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

1
third_party/boost/logic vendored Submodule

@ -0,0 +1 @@
Subproject commit 145778490c2d332c1411df6a5274a4b53ec3e091

1
third_party/boost/move vendored Submodule

@ -0,0 +1 @@
Subproject commit 7c01072629d83a7b54c99de70ef535d699ebd200

1
third_party/boost/mp11 vendored Submodule

@ -0,0 +1 @@
Subproject commit 863d8b8d2b20f2acd0b5870f23e553df9ce90e6c

Some files were not shown because too many files have changed in this diff Show More