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