Initial commit of rewrite based on base/ library
Should make the code actually possible to finish :v
This commit is contained in:
commit
4851371f56
|
@ -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
|
|
@ -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*
|
|
@ -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
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
|
@ -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.
|
|
@ -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()
|
|
@ -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;
|
|
@ -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
|
||||||
|
|
|
@ -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,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)
|
|
@ -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
|
|
@ -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(); \
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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[] = {
|
||||||
|
"",
|
||||||
|
""",
|
||||||
|
"&",
|
||||||
|
"'",
|
||||||
|
"/",
|
||||||
|
"<",
|
||||||
|
">"
|
||||||
|
};
|
||||||
|
// 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit ade1527bef91b85a09439f98014d048735605e60
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
||||||
|
#include <boost/beast/src.hpp>
|
|
@ -0,0 +1,2 @@
|
||||||
|
#define TOML_IMPLEMENTATION
|
||||||
|
#include <toml++/toml.hpp>
|
|
@ -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
|
||||||
|
)
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
||||||
|
add_subdirectory(boost)
|
||||||
|
add_subdirectory(tomlplusplus)
|
|
@ -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()
|
|
@ -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`
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit faac048d59948b1990c0a8772a050d8e47279343
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 5ad7df63cd792fbdb801d600b93cad1a432f0151
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit ecc47cb42c98261d6abf39fb5575c38eac6db748
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 2e49b21732e5d1bb3bb98f209164c30816b0bc79
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit a2817b89f48a8fdbe4130762d0d47a355ca5b562
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit b91d55150f5bb6a26a8560a58ede9164b8812de6
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit dc8798c917a2d527e4d64497b2b85e2694fc037c
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit dded373cc705781b8d0778343892a290aa87b09f
|
|
@ -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
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit ee0d6d543a37d9b7243682549e9ae359eb89daa9
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 05a83223e4494edd86d099b672eba4367b45677b
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 37c9bddf0bdefaaae0ca5852c1a153d9fc43f278
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 601598f8325350acb0c905d8f3293c17ae61cae3
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 6e697d796897b32b471b4f0740dcaa03d8ee57cc
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 7288df8beea1c3c8222cd48af1c07c589f7d3f8a
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 08679361bd0feb2ef3a6e7e9562a181f7e9c957d
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 9f285ef0c43c101e49b37bf5e6085e8d635887dc
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 0a35bb6a20bd13e85ff492a5be01b3894807a0d1
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 1e1347c0b1910b9310ec1719edad8b0bf2fd03c8
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 39714907b7d32ed8f005b5a01d1c2b885b5717b3
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit fad199e782ca027957cbba6be7bbec1dee48afba
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 845567f026b6e7606b237c92aa8337a1457b672b
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit c9b436e5dfce85e8ae365e5aabbb872dd35c29eb
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit b9170a02f102250b308c9f94ed6593c5f30eab39
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit a10762e8b130b1a363cbc0dfe42647dbbc4b11da
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 6876969bfca3b83c90480f7e276b23f6f5a9310c
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 895335874d67987ada0d8bf6ca1725e70642ed49
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 6a573e4b8333ee63ee62ce95558c3667348db233
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 7d4c03fa032299f2d46149b7b3136c9fd43e4f81
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit e7ed9918c16f11a9b885b043f7aa7994b3e21582
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit e997641e7d385748d706209c2fff28871c31c667
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 342e4c6d10d586058818daa84201a2d301357a53
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit dfe11e71443edd8e798da50984a8459134dfbc16
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit db92f8c22360990f450fe27b86ea1a5830b5cf05
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit ed8f9cd32f4fde695d497502f696f6f861b68559
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit fc5ffb67f8fcdce0f843460f9374eecddc5ac5a5
|
|
@ -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
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 145778490c2d332c1411df6a5274a4b53ec3e091
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 7c01072629d83a7b54c99de70ef535d699ebd200
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 863d8b8d2b20f2acd0b5870f23e553df9ce90e6c
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue