Compare commits
1
Commits
networking
..
server
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c4eeb9793 |
No files matched your search
+2
-12
@@ -41,7 +41,7 @@ configured player slot connects or disconnects.
|
|||||||
|
|
||||||
`WebsocketGameClient` implements `IGameServer`.
|
`WebsocketGameClient` implements `IGameServer`.
|
||||||
|
|
||||||
- It connects asynchronously to a `ws://` or `wss://` endpoint (`WebsocketEndpoint::parse`).
|
- It connects asynchronously to `ws://127.0.0.1:8080/`.
|
||||||
- Received messages are queued by the network thread and applied on the game
|
- Received messages are queued by the network thread and applied on the game
|
||||||
thread from `update()`.
|
thread from `update()`.
|
||||||
- It maintains an internal `InternalGameServer` replica so existing snapshot
|
- It maintains an internal `InternalGameServer` replica so existing snapshot
|
||||||
@@ -145,13 +145,6 @@ Networking is enabled for Windows and the Emscripten web client:
|
|||||||
replica, and remote-host code, and links Emscripten's `websocket.js` library.
|
replica, and remote-host code, and links Emscripten's `websocket.js` library.
|
||||||
`WebsocketGameClient.cpp` selects the browser WebSocket API on Emscripten; it
|
`WebsocketGameClient.cpp` selects the browser WebSocket API on Emscripten; it
|
||||||
does not use Boost.Asio or Boost.Beast there.
|
does not use Boost.Asio or Boost.Beast there.
|
||||||
- `wss://` (TLS) is supported by the Windows client only. `proj-windows` also defines
|
|
||||||
`NETWORK_TLS` and links static OpenSSL 3.4.1, which CMake downloads and builds
|
|
||||||
automatically at configure time (`cmake/ThirdParty.cmake`, section 12) using a
|
|
||||||
downloaded portable Strawberry Perl and the nmake from the installed Visual Studio
|
|
||||||
(located through `vswhere`). Builds go to `thirdparty/openssl-3.4.1/install-Debug`
|
|
||||||
and `install-Release`; later configures skip them. `NETWORK_TLS` unset (server tests)
|
|
||||||
means `wss://` endpoints fail with a logged error.
|
|
||||||
- Linux and Android builds do not define `NETWORK` and do not compile the
|
- Linux and Android builds do not define `NETWORK` and do not compile the
|
||||||
network transport.
|
network transport.
|
||||||
|
|
||||||
@@ -203,10 +196,7 @@ submission, and contiguous tick consumption.
|
|||||||
not implemented.
|
not implemented.
|
||||||
- There is no authentication or reconnect/replay history.
|
- There is no authentication or reconnect/replay history.
|
||||||
- A hash mismatch is logged but does not yet trigger checkpoint resynchronization.
|
- A hash mismatch is logged but does not yet trigger checkpoint resynchronization.
|
||||||
- The endpoint is hardcoded in the graphical client (`kMultiplayerServerUrl` in
|
- The endpoint is hardcoded in the graphical client.
|
||||||
`Game.cpp`, currently `wss://api.shadowoverbishkek.com/server001/`).
|
|
||||||
- TLS certificate or hostname problems are logged but deliberately do not abort the
|
|
||||||
connection. `GameServer` itself has no TLS; it must sit behind a TLS-terminating proxy.
|
|
||||||
- Multiplayer Lua and dialogue behavior remains unsupported.
|
- Multiplayer Lua and dialogue behavior remains unsupported.
|
||||||
- Determinism is supported for identical executable/content builds. The
|
- Determinism is supported for identical executable/content builds. The
|
||||||
floating-point simulation is not guaranteed bit-identical across platforms
|
floating-point simulation is not guaranteed bit-identical across platforms
|
||||||
|
|||||||
@@ -56,30 +56,3 @@ check_and_download("https://github.com/ThePhD/sol2/archive/refs/tags/v3.3.0.zip"
|
|||||||
|
|
||||||
# 11) SDL2_mixer
|
# 11) SDL2_mixer
|
||||||
check_and_download("https://github.com/libsdl-org/SDL_mixer/archive/refs/tags/release-2.8.0.zip" "SDL_mixer-release-2.8.0.zip" "SDL_mixer-release-2.8.0" "CMakeLists.txt")
|
check_and_download("https://github.com/libsdl-org/SDL_mixer/archive/refs/tags/release-2.8.0.zip" "SDL_mixer-release-2.8.0.zip" "SDL_mixer-release-2.8.0" "CMakeLists.txt")
|
||||||
|
|
||||||
# 12) OpenSSL (TLS for wss://) and a native Windows Perl needed to build it
|
|
||||||
if(WIN32)
|
|
||||||
check_and_download("https://github.com/openssl/openssl/releases/download/openssl-3.4.1/openssl-3.4.1.tar.gz" "openssl-3.4.1.tar.gz" "openssl-3.4.1" "Configure")
|
|
||||||
|
|
||||||
# Strawberry Perl portable. The zip has no top-level folder, so it is
|
|
||||||
# extracted into its own directory. (Git's MSYS perl cannot build OpenSSL.)
|
|
||||||
set(STRAWBERRY_PERL_DIR "${THIRDPARTY_DIR}/strawberry-perl")
|
|
||||||
set(STRAWBERRY_PERL_ARCHIVE "${THIRDPARTY_DIR}/strawberry-perl-5.42.3.1-64bit-portable.zip")
|
|
||||||
if(NOT EXISTS "${STRAWBERRY_PERL_DIR}/perl/bin/perl.exe")
|
|
||||||
if(NOT EXISTS "${STRAWBERRY_PERL_ARCHIVE}")
|
|
||||||
message(STATUS "Downloading Strawberry Perl...")
|
|
||||||
file(DOWNLOAD
|
|
||||||
"https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_54231_64bit/strawberry-perl-5.42.3.1-64bit-portable.zip"
|
|
||||||
"${STRAWBERRY_PERL_ARCHIVE}" SHOW_PROGRESS)
|
|
||||||
endif()
|
|
||||||
message(STATUS "Extracting Strawberry Perl...")
|
|
||||||
file(MAKE_DIRECTORY "${STRAWBERRY_PERL_DIR}")
|
|
||||||
execute_process(
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E tar xf "${STRAWBERRY_PERL_ARCHIVE}"
|
|
||||||
WORKING_DIRECTORY "${STRAWBERRY_PERL_DIR}"
|
|
||||||
)
|
|
||||||
if(NOT EXISTS "${STRAWBERRY_PERL_DIR}/perl/bin/perl.exe")
|
|
||||||
message(FATAL_ERROR "Strawberry Perl extraction failed")
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# cmake/FetchDependenciesServer.cmake
|
||||||
|
#
|
||||||
|
# Downloads only what the headless GameServer needs (Eigen + Boost headers).
|
||||||
|
# Skips anything already present in thirdparty/. Works on Windows and Linux.
|
||||||
|
|
||||||
|
set(THIRDPARTY_DIR "${CMAKE_CURRENT_LIST_DIR}/../thirdparty")
|
||||||
|
file(MAKE_DIRECTORY "${THIRDPARTY_DIR}")
|
||||||
|
|
||||||
|
macro(server_check_and_download URL ARCHIVE_NAME EXTRACTED_DIR_NAME CHECK_FILE)
|
||||||
|
set(_archive_path "${THIRDPARTY_DIR}/${ARCHIVE_NAME}")
|
||||||
|
set(_src_path "${THIRDPARTY_DIR}/${EXTRACTED_DIR_NAME}")
|
||||||
|
|
||||||
|
if(NOT EXISTS "${_src_path}/${CHECK_FILE}")
|
||||||
|
if(NOT EXISTS "${_archive_path}")
|
||||||
|
message(STATUS "Downloading ${ARCHIVE_NAME}...")
|
||||||
|
file(DOWNLOAD "${URL}" "${_archive_path}"
|
||||||
|
SHOW_PROGRESS STATUS _dl_status)
|
||||||
|
list(GET _dl_status 0 _dl_code)
|
||||||
|
if(NOT _dl_code EQUAL 0)
|
||||||
|
file(REMOVE "${_archive_path}")
|
||||||
|
message(FATAL_ERROR "Failed to download ${URL}: ${_dl_status}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
message(STATUS "Extracting ${ARCHIVE_NAME}...")
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E tar xf "${_archive_path}"
|
||||||
|
WORKING_DIRECTORY "${THIRDPARTY_DIR}"
|
||||||
|
RESULT_VARIABLE _tar_res)
|
||||||
|
if(NOT _tar_res EQUAL 0)
|
||||||
|
message(FATAL_ERROR "Failed to extract ${_archive_path}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endmacro()
|
||||||
|
|
||||||
|
# Eigen: only needed if no system install is available.
|
||||||
|
find_package(Eigen3 3.3 CONFIG QUIET)
|
||||||
|
if(NOT TARGET Eigen3::Eigen)
|
||||||
|
server_check_and_download(
|
||||||
|
"https://gitlab.com/libeigen/eigen/-/archive/5.0.0/eigen-5.0.0.zip"
|
||||||
|
"eigen-5.0.0.zip" "eigen-5.0.0" "signature_of_eigen3_matrix_library")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Boost (header-only usage: asio + beast). Use bundled copy always so the
|
||||||
|
# version matches the Windows build.
|
||||||
|
server_check_and_download(
|
||||||
|
"https://archives.boost.io/release/1.90.0/source/boost_1_90_0.zip"
|
||||||
|
"boost_1_90_0.zip" "boost_1_90_0" "boost/asio.hpp")
|
||||||
@@ -726,105 +726,3 @@ set_target_properties(SDL2_mixer_external_lib PROPERTIES
|
|||||||
INTERFACE_LINK_LIBRARIES
|
INTERFACE_LINK_LIBRARIES
|
||||||
"SDL2_external_lib"
|
"SDL2_external_lib"
|
||||||
)
|
)
|
||||||
# ===========================================
|
|
||||||
# 12) OpenSSL (3.4.1) - TLS for wss:// (Windows only, static libs)
|
|
||||||
# ===========================================
|
|
||||||
if(WIN32)
|
|
||||||
set(OPENSSL_SRC_DIR "${THIRDPARTY_DIR}/openssl-3.4.1")
|
|
||||||
set(OPENSSL_BASE_DIR "${OPENSSL_SRC_DIR}/install")
|
|
||||||
|
|
||||||
set(_have_openssl TRUE)
|
|
||||||
foreach(cfg IN LISTS BUILD_CONFIGS)
|
|
||||||
if(NOT EXISTS "${OPENSSL_BASE_DIR}-${cfg}/lib/libssl.lib" OR
|
|
||||||
NOT EXISTS "${OPENSSL_BASE_DIR}-${cfg}/lib/libcrypto.lib")
|
|
||||||
set(_have_openssl FALSE)
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
if(NOT _have_openssl)
|
|
||||||
# Locate Visual Studio's vcvarsall.bat (provides cl, link, nmake).
|
|
||||||
set(_vswhere "$ENV{ProgramFiles\(x86\)}/Microsoft Visual Studio/Installer/vswhere.exe")
|
|
||||||
set(_vcvarsall "")
|
|
||||||
if(EXISTS "${_vswhere}")
|
|
||||||
execute_process(
|
|
||||||
COMMAND "${_vswhere}" -latest -products * -property installationPath
|
|
||||||
OUTPUT_VARIABLE _vs_path
|
|
||||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
|
||||||
)
|
|
||||||
if(_vs_path AND EXISTS "${_vs_path}/VC/Auxiliary/Build/vcvarsall.bat")
|
|
||||||
set(_vcvarsall "${_vs_path}/VC/Auxiliary/Build/vcvarsall.bat")
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
if(_vcvarsall STREQUAL "")
|
|
||||||
message(FATAL_ERROR "Visual Studio (vcvarsall.bat) not found; it is required to build OpenSSL")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
|
||||||
set(_ossl_vc_arch x64)
|
|
||||||
set(_ossl_target VC-WIN64A)
|
|
||||||
else()
|
|
||||||
set(_ossl_vc_arch x86)
|
|
||||||
set(_ossl_target VC-WIN32)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
file(TO_NATIVE_PATH "${_vcvarsall}" _vcvarsall_native)
|
|
||||||
file(TO_NATIVE_PATH "${STRAWBERRY_PERL_DIR}/perl/bin" _perl_bin_native)
|
|
||||||
file(TO_NATIVE_PATH "${STRAWBERRY_PERL_DIR}/perl/site/bin" _perl_site_native)
|
|
||||||
|
|
||||||
foreach(cfg IN LISTS BUILD_CONFIGS)
|
|
||||||
set(_ossl_build "${OPENSSL_SRC_DIR}/build-${cfg}")
|
|
||||||
file(REMOVE_RECURSE "${_ossl_build}")
|
|
||||||
file(MAKE_DIRECTORY "${_ossl_build}")
|
|
||||||
file(TO_NATIVE_PATH "${_ossl_build}" _ossl_build_native)
|
|
||||||
file(TO_NATIVE_PATH "${OPENSSL_SRC_DIR}/Configure" _ossl_configure_native)
|
|
||||||
file(TO_NATIVE_PATH "${OPENSSL_BASE_DIR}-${cfg}" _ossl_prefix_native)
|
|
||||||
|
|
||||||
set(_ossl_cfg_flags "")
|
|
||||||
if(cfg STREQUAL "Debug")
|
|
||||||
# /MDd: link the dynamic debug CRT like the game (static libs default to /MTd)
|
|
||||||
set(_ossl_cfg_flags "--debug /MDd")
|
|
||||||
else()
|
|
||||||
set(_ossl_cfg_flags "--release /MD")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# A batch file avoids cmd.exe quoting problems.
|
|
||||||
set(_ossl_bat "${_ossl_build}/build_openssl.bat")
|
|
||||||
file(WRITE "${_ossl_bat}"
|
|
||||||
"@echo off
|
|
||||||
call \"${_vcvarsall_native}\" ${_ossl_vc_arch} || exit /b 1
|
|
||||||
set \"PATH=${_perl_bin_native};${_perl_site_native};%PATH%\"
|
|
||||||
set CL=/MP
|
|
||||||
cd /d \"${_ossl_build_native}\" || exit /b 1
|
|
||||||
perl \"${_ossl_configure_native}\" ${_ossl_target} no-shared no-asm no-tests no-docs ${_ossl_cfg_flags} --prefix=\"${_ossl_prefix_native}\" --openssldir=\"${_ossl_prefix_native}/ssl\" || exit /b 1
|
|
||||||
nmake || exit /b 1
|
|
||||||
nmake install_sw || exit /b 1
|
|
||||||
")
|
|
||||||
log("Building OpenSSL (${cfg}) - this takes several minutes ...")
|
|
||||||
execute_process(
|
|
||||||
COMMAND cmd /c "${_ossl_bat}"
|
|
||||||
RESULT_VARIABLE _ossl_res
|
|
||||||
)
|
|
||||||
if(NOT _ossl_res EQUAL 0)
|
|
||||||
message(FATAL_ERROR "OpenSSL build failed for ${cfg} (see output above)")
|
|
||||||
endif()
|
|
||||||
endforeach()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_library(openssl_crypto_external_lib UNKNOWN IMPORTED GLOBAL)
|
|
||||||
set_target_properties(openssl_crypto_external_lib PROPERTIES
|
|
||||||
IMPORTED_LOCATION_DEBUG "${OPENSSL_BASE_DIR}-Debug/lib/libcrypto.lib"
|
|
||||||
IMPORTED_LOCATION_RELEASE "${OPENSSL_BASE_DIR}-Release/lib/libcrypto.lib"
|
|
||||||
INTERFACE_INCLUDE_DIRECTORIES
|
|
||||||
"$<IF:$<CONFIG:Debug>,${OPENSSL_BASE_DIR}-Debug/include,${OPENSSL_BASE_DIR}-Release/include>"
|
|
||||||
INTERFACE_LINK_LIBRARIES "ws2_32;crypt32;advapi32;user32;gdi32"
|
|
||||||
)
|
|
||||||
|
|
||||||
add_library(openssl_ssl_external_lib UNKNOWN IMPORTED GLOBAL)
|
|
||||||
set_target_properties(openssl_ssl_external_lib PROPERTIES
|
|
||||||
IMPORTED_LOCATION_DEBUG "${OPENSSL_BASE_DIR}-Debug/lib/libssl.lib"
|
|
||||||
IMPORTED_LOCATION_RELEASE "${OPENSSL_BASE_DIR}-Release/lib/libssl.lib"
|
|
||||||
INTERFACE_INCLUDE_DIRECTORIES
|
|
||||||
"$<IF:$<CONFIG:Debug>,${OPENSSL_BASE_DIR}-Debug/include,${OPENSSL_BASE_DIR}-Release/include>"
|
|
||||||
INTERFACE_LINK_LIBRARIES "openssl_crypto_external_lib"
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
@@ -143,7 +143,6 @@ target_compile_definitions(ShadowOverBishkekDemo PRIVATE
|
|||||||
_WIN32_WINNT=0x0A00
|
_WIN32_WINNT=0x0A00
|
||||||
PNG_ENABLED
|
PNG_ENABLED
|
||||||
NETWORK
|
NETWORK
|
||||||
NETWORK_TLS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Линкуем с SDL2main, если он вообще установлен
|
# Линкуем с SDL2main, если он вообще установлен
|
||||||
@@ -161,7 +160,6 @@ target_link_libraries(ShadowOverBishkekDemo PRIVATE
|
|||||||
boost_external_lib
|
boost_external_lib
|
||||||
sol2_external_lib
|
sol2_external_lib
|
||||||
SDL2_mixer_external_lib
|
SDL2_mixer_external_lib
|
||||||
openssl_ssl_external_lib
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Линкуем OpenGL (Windows)
|
# Линкуем OpenGL (Windows)
|
||||||
|
|||||||
+33
-18
@@ -9,6 +9,7 @@ set(CMAKE_CXX_EXTENSIONS OFF)
|
|||||||
set(GAME_ROOT "${CMAKE_CURRENT_LIST_DIR}/..")
|
set(GAME_ROOT "${CMAKE_CURRENT_LIST_DIR}/..")
|
||||||
|
|
||||||
find_package(Threads REQUIRED)
|
find_package(Threads REQUIRED)
|
||||||
|
include("${GAME_ROOT}/cmake/FetchDependenciesServer.cmake")
|
||||||
find_package(Eigen3 3.3 CONFIG QUIET)
|
find_package(Eigen3 3.3 CONFIG QUIET)
|
||||||
if(NOT TARGET Eigen3::Eigen)
|
if(NOT TARGET Eigen3::Eigen)
|
||||||
set(EIGEN_BUNDLED_DIR "${GAME_ROOT}/thirdparty/eigen-5.0.0")
|
set(EIGEN_BUNDLED_DIR "${GAME_ROOT}/thirdparty/eigen-5.0.0")
|
||||||
@@ -105,24 +106,32 @@ endif()
|
|||||||
add_executable(GameServer src/main.cpp README.md)
|
add_executable(GameServer src/main.cpp README.md)
|
||||||
source_group("Server" FILES src/main.cpp README.md)
|
source_group("Server" FILES src/main.cpp README.md)
|
||||||
target_link_libraries(GameServer PRIVATE game_server_core)
|
target_link_libraries(GameServer PRIVATE game_server_core)
|
||||||
|
set(BOOST_BUNDLED_DIR "${GAME_ROOT}/thirdparty/boost_1_90_0")
|
||||||
|
if(NOT EXISTS "${BOOST_BUNDLED_DIR}/boost/asio.hpp")
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"Boost was not found at thirdparty/boost_1_90_0 and could not be "
|
||||||
|
"downloaded.")
|
||||||
|
endif()
|
||||||
|
target_sources(GameServer PRIVATE
|
||||||
|
"${GAME_ROOT}/src/game_server/NetworkProtocol.cpp"
|
||||||
|
"${GAME_ROOT}/src/game_server/NetworkProtocol.h"
|
||||||
|
"${GAME_ROOT}/src/game_server/WebsocketGameServer.cpp"
|
||||||
|
"${GAME_ROOT}/src/game_server/WebsocketGameServer.h")
|
||||||
|
target_include_directories(GameServer PRIVATE "${BOOST_BUNDLED_DIR}")
|
||||||
|
target_compile_definitions(GameServer PRIVATE NETWORK)
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
set(BOOST_BUNDLED_DIR "${GAME_ROOT}/thirdparty/boost_1_90_0")
|
|
||||||
if(NOT EXISTS "${BOOST_BUNDLED_DIR}/boost/asio.hpp")
|
|
||||||
message(FATAL_ERROR
|
|
||||||
"Boost was not found at thirdparty/boost_1_90_0. Configure the "
|
|
||||||
"graphical project once to populate it.")
|
|
||||||
endif()
|
|
||||||
target_sources(GameServer PRIVATE
|
|
||||||
"${GAME_ROOT}/src/game_server/NetworkProtocol.cpp"
|
|
||||||
"${GAME_ROOT}/src/game_server/NetworkProtocol.h"
|
|
||||||
"${GAME_ROOT}/src/game_server/WebsocketGameServer.cpp"
|
|
||||||
"${GAME_ROOT}/src/game_server/WebsocketGameServer.h")
|
|
||||||
target_include_directories(GameServer PRIVATE "${BOOST_BUNDLED_DIR}")
|
|
||||||
target_compile_definitions(
|
target_compile_definitions(
|
||||||
GameServer PRIVATE NETWORK WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0A00)
|
GameServer PRIVATE WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0A00)
|
||||||
target_link_libraries(GameServer PRIVATE ws2_32 mswsock)
|
target_link_libraries(GameServer PRIVATE ws2_32 mswsock)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# The server reads configs from ./resources relative to its working directory,
|
||||||
|
# so keep a copy next to the binary after every build.
|
||||||
|
add_custom_command(TARGET GameServer POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
|
"${GAME_ROOT}/resources" "$<TARGET_FILE_DIR:GameServer>/resources"
|
||||||
|
COMMENT "Copying resources next to GameServer")
|
||||||
|
|
||||||
include(CTest)
|
include(CTest)
|
||||||
if(BUILD_TESTING)
|
if(BUILD_TESTING)
|
||||||
add_executable(
|
add_executable(
|
||||||
@@ -141,7 +150,8 @@ if(BUILD_TESTING)
|
|||||||
add_test(
|
add_test(
|
||||||
NAME GameServer.LocationState
|
NAME GameServer.LocationState
|
||||||
COMMAND GameServerStateTests)
|
COMMAND GameServerStateTests)
|
||||||
if(WIN32)
|
# network tests (all platforms)
|
||||||
|
if(TRUE)
|
||||||
add_executable(
|
add_executable(
|
||||||
GameServerNetworkProtocolTests
|
GameServerNetworkProtocolTests
|
||||||
tests/NetworkProtocolTests.cpp
|
tests/NetworkProtocolTests.cpp
|
||||||
@@ -161,11 +171,16 @@ if(BUILD_TESTING)
|
|||||||
target_include_directories(
|
target_include_directories(
|
||||||
GameServerWebsocketLockstepTests PRIVATE "${BOOST_BUNDLED_DIR}")
|
GameServerWebsocketLockstepTests PRIVATE "${BOOST_BUNDLED_DIR}")
|
||||||
target_compile_definitions(
|
target_compile_definitions(
|
||||||
GameServerWebsocketLockstepTests PRIVATE
|
GameServerWebsocketLockstepTests PRIVATE NETWORK)
|
||||||
NETWORK WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0A00)
|
|
||||||
target_link_libraries(
|
target_link_libraries(
|
||||||
GameServerWebsocketLockstepTests PRIVATE
|
GameServerWebsocketLockstepTests PRIVATE game_server_core)
|
||||||
game_server_core ws2_32 mswsock)
|
if(WIN32)
|
||||||
|
target_compile_definitions(
|
||||||
|
GameServerWebsocketLockstepTests PRIVATE
|
||||||
|
WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0A00)
|
||||||
|
target_link_libraries(
|
||||||
|
GameServerWebsocketLockstepTests PRIVATE ws2_32 mswsock)
|
||||||
|
endif()
|
||||||
add_test(
|
add_test(
|
||||||
NAME GameServer.WebsocketLockstep
|
NAME GameServer.WebsocketLockstep
|
||||||
COMMAND GameServerWebsocketLockstepTests)
|
COMMAND GameServerWebsocketLockstepTests)
|
||||||
|
|||||||
+123
-62
@@ -1,11 +1,37 @@
|
|||||||
# Headless GameServer
|
# Headless GameServer
|
||||||
|
|
||||||
This target runs the authoritative `InternalGameServer` without SDL, OpenGL,
|
This target runs the authoritative `InternalGameServer` without SDL, OpenGL,
|
||||||
audio, Lua, or rendering dependencies. On Windows it exposes a deterministic
|
audio, Lua, or rendering dependencies. It exposes a deterministic lockstep
|
||||||
lockstep session through a Boost.Beast WebSocket listener.
|
session through a Boost.Beast WebSocket listener (Windows and Linux).
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- CMake >= 3.16, a C++17 compiler (MSVC, or GCC/Clang on Linux)
|
||||||
|
- Ubuntu: `sudo apt install build-essential cmake git unzip curl`
|
||||||
|
- Internet access on the first configure (see below)
|
||||||
|
|
||||||
|
Eigen and Boost are **downloaded automatically** by
|
||||||
|
`cmake/FetchDependenciesServer.cmake` into `thirdparty/` the first time CMake
|
||||||
|
configures the project (Boost zip is ~200 MB). A system Eigen3 is used if
|
||||||
|
installed; Boost is always taken from `thirdparty/boost_1_90_0` so versions match
|
||||||
|
the Windows build. Nothing else (SDL, zlib, libzip, ...) is needed.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
|
Linux (recommended helper, 1 compile job to stay within low RAM):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
server/scripts/build.sh # JOBS=2 server/scripts/build.sh for more
|
||||||
|
```
|
||||||
|
|
||||||
|
Or manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake -S server -B server/build -DCMAKE_BUILD_TYPE=Release
|
||||||
|
cmake --build server/build --target GameServer --parallel 1
|
||||||
|
ctest --test-dir server/build --output-on-failure
|
||||||
|
```
|
||||||
|
|
||||||
Windows:
|
Windows:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
@@ -14,91 +40,126 @@ cmake --build server/build --config Release --target GameServer
|
|||||||
ctest --test-dir server/build -C Release --output-on-failure
|
ctest --test-dir server/build -C Release --output-on-failure
|
||||||
```
|
```
|
||||||
|
|
||||||
Linux:
|
> **Memory note:** compiling Boost.Beast code can use 1-1.5 GB per translation
|
||||||
|
> unit. On a small VDS keep `--parallel 1`; if the compiler gets OOM-killed, add
|
||||||
|
> temporary swap (`sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile &&
|
||||||
|
> sudo mkswap /swapfile && sudo swapon /swapfile`). The running server itself
|
||||||
|
> needs only a few tens of MB.
|
||||||
|
|
||||||
|
After every build the repository's `resources/` directory is copied next to the
|
||||||
|
binary (`server/build/resources`, or `server/build/Release/resources` on
|
||||||
|
Windows). Content paths are resolved from the process working directory, so the
|
||||||
|
server must be started from the directory that contains `resources/`.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cmake -S server -B server/build -DCMAKE_BUILD_TYPE=Release
|
server/scripts/run-server.sh
|
||||||
cmake --build server/build --target GameServer --parallel
|
|
||||||
ctest --test-dir server/build --output-on-failure
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Eigen3 must either be installed or be available at
|
This does `cd server/build` and runs
|
||||||
`thirdparty/eigen-5.0.0`. The headless target reads ordinary filesystem resources
|
|
||||||
only; ZIP-backed campaign loading belongs to the later content-loading
|
|
||||||
milestone.
|
|
||||||
|
|
||||||
Run until interrupted:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./server/build/GameServer
|
./GameServer --campaign battle_zone_multiplayer --address 127.0.0.1 --port 8081
|
||||||
```
|
```
|
||||||
|
|
||||||
For a multi-configuration Windows build, the executable is normally
|
Environment overrides: `CAMPAIGN`, `LOCATION`, `ADDRESS`, `PORT`, `BUILD_DIR`.
|
||||||
`server/build/Release/GameServer.exe`.
|
|
||||||
|
|
||||||
For smoke tests, `--ticks N` limits execution and `--no-sleep` skips the
|
Windows equivalent:
|
||||||
real-time delay.
|
|
||||||
|
|
||||||
Start the multiplayer lockstep server:
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\server\build\Release\GameServer.exe --campaign battle_zone_multiplayer --location uni_exterior
|
.\server\build\Release\GameServer.exe --campaign battle_zone_multiplayer
|
||||||
```
|
```
|
||||||
|
|
||||||
The listener defaults to `ws://0.0.0.0:8080`. Launch the Windows graphical game
|
Command line options: `--campaign ID`, `--location ID`, `--campaigns PATH`
|
||||||
afterward and select `battle_zone_multiplayer`; that campaign connects to
|
(default `resources/config/campaigns.json`), `--address IP` (default `0.0.0.0`),
|
||||||
`ws://127.0.0.1:8080`. Other campaigns continue to use the in-process server.
|
`--port N` (default `8080`), `--ticks N` and `--no-sleep` (smoke tests).
|
||||||
|
|
||||||
The server sends one state snapshot when a player connects, then broadcasts one
|
The server sends one state snapshot when a player connects, then broadcasts one
|
||||||
numbered command bundle every 100 ms. Bundles may be empty. Destination input is
|
numbered command bundle every 100 ms. Bundles may be empty. Destination input is
|
||||||
scheduled two ticks (200 ms) in the future. Tick IDs, rather than server or
|
scheduled two ticks (200 ms) in the future. Tick IDs, rather than server or
|
||||||
client wall-clock values, define simulation time.
|
client wall-clock values, define simulation time.
|
||||||
|
|
||||||
If `--location` is omitted, the campaign's configured start location is used.
|
## Production deployment (Ubuntu, systemd + nginx)
|
||||||
`--campaigns PATH` selects a different campaign registry. Content paths are
|
|
||||||
resolved from the process working directory, so run from the repository root
|
|
||||||
when using the bundled `resources/` tree.
|
|
||||||
|
|
||||||
Available startup options:
|
The server listens on loopback only (`127.0.0.1:8081`); nginx terminates TLS and
|
||||||
|
proxies WebSocket traffic.
|
||||||
|
|
||||||
```text
|
### 1. systemd service
|
||||||
--campaign ID
|
|
||||||
--location ID
|
```bash
|
||||||
--campaigns PATH
|
sudo cp server/deploy/shadow-gameserver.service /etc/systemd/system/
|
||||||
--address IP
|
sudo systemctl daemon-reload
|
||||||
--port N
|
sudo systemctl enable --now shadow-gameserver
|
||||||
--tick-ms 100
|
systemctl status shadow-gameserver
|
||||||
--ticks N
|
journalctl -u shadow-gameserver -f # logs
|
||||||
--no-sleep
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Network mode requires a 100 ms tick; another `--tick-ms` value is rejected.
|
The unit caps memory at 512 MB and restarts on failure. After rebuilding run
|
||||||
The selected campaign's `networkPlayerSlots` lists pre-existing character IDs
|
`sudo systemctl restart shadow-gameserver`. Edit `User=`/paths in the unit if the
|
||||||
assigned to connections in order. `networkCompatibility` is a release token
|
repository lives elsewhere. To run a second server, copy the unit with another
|
||||||
that must be changed whenever deterministic content changes; clients reject a
|
`PORT` (and campaign) and add a matching `/server002/` location.
|
||||||
different token. The bundled multiplayer Battle Zone has two slots (`player`
|
|
||||||
and `player2`) and rejects additional clients. Disconnecting
|
|
||||||
releases the slot. The bot's random autonomous controller is disabled in
|
|
||||||
multiplayer.
|
|
||||||
|
|
||||||
The graphical client executes presentation behavior after building its runtime
|
### 2. nginx
|
||||||
`Location`; the headless content pass loads authoritative navigation, NPC,
|
|
||||||
interactive-object, trigger, teleport, taxi, and animation metadata. Lua and
|
|
||||||
dialogue-driven shared multiplayer behavior is not yet supported.
|
|
||||||
|
|
||||||
## Tests
|
```bash
|
||||||
|
sudo cp server/deploy/nginx-api.shadowoverbishkek.com.conf \
|
||||||
|
/etc/nginx/sites-available/api.shadowoverbishkek.com
|
||||||
|
sudo ln -s /etc/nginx/sites-available/api.shadowoverbishkek.com \
|
||||||
|
/etc/nginx/sites-enabled/
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
CTest covers:
|
`location /server001/` proxies to `http://127.0.0.1:8081/` with WebSocket
|
||||||
|
upgrade headers and a 1 hour read/send timeout (the trailing slashes strip the
|
||||||
|
`/server001` prefix).
|
||||||
|
|
||||||
- repeated `LocationGameState::setup()` preserving mutable runtime state;
|
### 3. HTTPS certificate
|
||||||
- ticking a prepared location with zero players;
|
|
||||||
- deterministic fixed-100 ms replay behavior;
|
|
||||||
- protocol-v2 parsing, validation, target normalization, state hashing, and
|
|
||||||
two-tick command ordering;
|
|
||||||
- a WebSocket initial-state/slot/destination/tick integration test;
|
|
||||||
- idle server startup;
|
|
||||||
- headless loading of `battle_zone_multiplayer/uni_exterior`.
|
|
||||||
|
|
||||||
The lockstep implementation currently targets identical executable and content
|
DNS for `api.shadowoverbishkek.com` must point at the VDS first.
|
||||||
builds. Diagnostic state hashes detect divergence, but there is no automatic
|
|
||||||
checkpoint resynchronization or reconnect input history yet.
|
```bash
|
||||||
|
sudo certbot --nginx -d api.shadowoverbishkek.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Certbot issues a Let's Encrypt certificate, adds the `443 ssl` block and an HTTP
|
||||||
|
to HTTPS redirect, and installs auto-renewal (`systemctl list-timers | grep certbot`).
|
||||||
|
|
||||||
|
## Connecting
|
||||||
|
|
||||||
|
Public endpoint: **`wss://api.shadowoverbishkek.com/server001/`**
|
||||||
|
|
||||||
|
Handshake check from any machine (expects `HTTP/1.1 101 Switching Protocols`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i -N --max-time 3 \
|
||||||
|
-H "Connection: Upgrade" -H "Upgrade: websocket" \
|
||||||
|
-H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==" \
|
||||||
|
https://api.shadowoverbishkek.com/server001/
|
||||||
|
```
|
||||||
|
|
||||||
|
Or interactively: `npx wscat -c wss://api.shadowoverbishkek.com/server001/`.
|
||||||
|
|
||||||
|
Locally on the VDS, bypassing nginx: `ws://127.0.0.1:8081`.
|
||||||
|
|
||||||
|
> **Client limitation:** `src/game_server/WebsocketGameClient.cpp` currently
|
||||||
|
> speaks plain `ws://host:port` to `/` (no TLS, no path). Until TLS and path
|
||||||
|
> support are added to the client, the graphical game can only connect directly
|
||||||
|
> to a non-proxied server (e.g. `ws://127.0.0.1:8080`), not to the `wss://` URL.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- `Animation metadata failed ... Invalid animation file`: the `.anim` files are
|
||||||
|
Git LFS pointer stubs (`version https://git-lfs...`). Fetch the real files with
|
||||||
|
`git lfs pull` (or copy `resources/` from a full checkout) and rebuild.
|
||||||
|
- `GameServer.LocationState` test currently fails on every platform: it expects
|
||||||
|
`battle_zone` and `battle_zone_multiplayer` to share a start-state file, but
|
||||||
|
`campaigns.json` points them to different files.
|
||||||
|
|
||||||
|
- `Server content setup failed`: the server was not started from the directory
|
||||||
|
containing `resources/` (use `run-server.sh`).
|
||||||
|
- `WebSocket listener setup failed`: port already in use (`ss -ltnp | grep 8081`).
|
||||||
|
- `502 Bad Gateway` from nginx: service not running (`systemctl status
|
||||||
|
shadow-gameserver`).
|
||||||
|
- Connection drops after ~60 s: check `proxy_read_timeout` in the nginx location.
|
||||||
|
- Compiler "Killed" / `internal compiler error`: out of memory, see the memory note.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Install to /etc/nginx/sites-available/api.shadowoverbishkek.com and symlink
|
||||||
|
# into sites-enabled. Then: sudo certbot --nginx -d api.shadowoverbishkek.com
|
||||||
|
server {
|
||||||
|
server_name api.shadowoverbishkek.com;
|
||||||
|
|
||||||
|
access_log /var/log/nginx/api.shadowoverbishkek.com-access.log;
|
||||||
|
error_log /var/log/nginx/api.shadowoverbishkek.com-error.log;
|
||||||
|
|
||||||
|
# wss://api.shadowoverbishkek.com/server001/ -> 127.0.0.1:8081
|
||||||
|
location /server001/ {
|
||||||
|
proxy_pass http://127.0.0.1:8081/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
proxy_send_timeout 3600s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
listen 443 ssl; # managed by Certbot
|
||||||
|
ssl_certificate /etc/letsencrypt/live/api.shadowoverbishkek.com/fullchain.pem; # managed by Certbot
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/api.shadowoverbishkek.com/privkey.pem; # managed by Certbot
|
||||||
|
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||||
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
if ($host = api.shadowoverbishkek.com) {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
} # managed by Certbot
|
||||||
|
|
||||||
|
|
||||||
|
server_name api.shadowoverbishkek.com;
|
||||||
|
|
||||||
|
listen 80;
|
||||||
|
return 404; # managed by Certbot
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Shadow Over Bishkek GameServer (server001)
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=ubuntu
|
||||||
|
ExecStart=/home/ubuntu/shadowoverbishkek-work/shadow-over-bishkek001/server/scripts/run-server.sh
|
||||||
|
Environment=PORT=8081
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=3
|
||||||
|
MemoryMax=512M
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Configure and build the headless GameServer (Release).
|
||||||
|
# Usage: server/scripts/build.sh (uses 1 job: safe for 2 GB RAM hosts)
|
||||||
|
# JOBS=4 server/scripts/build.sh
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cmake -S "$ROOT/server" -B "$ROOT/server/build" -DCMAKE_BUILD_TYPE=Release
|
||||||
|
cmake --build "$ROOT/server/build" --target GameServer --parallel "${JOBS:-1}"
|
||||||
|
echo "Built: $ROOT/server/build/GameServer"
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Start the multiplayer GameServer. Listens on loopback only; nginx proxies
|
||||||
|
# wss://api.shadowoverbishkek.com/server001/ to it.
|
||||||
|
# Overrides: CAMPAIGN, LOCATION, ADDRESS, PORT, BUILD_DIR
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
BUILD_DIR="${BUILD_DIR:-$ROOT/server/build}"
|
||||||
|
CAMPAIGN="${CAMPAIGN:-battle_zone_multiplayer}"
|
||||||
|
ADDRESS="${ADDRESS:-127.0.0.1}"
|
||||||
|
PORT="${PORT:-8081}"
|
||||||
|
|
||||||
|
# Content paths are resolved from the working directory.
|
||||||
|
cd "$BUILD_DIR"
|
||||||
|
ARGS=(--campaign "$CAMPAIGN" --address "$ADDRESS" --port "$PORT")
|
||||||
|
[ -n "${LOCATION:-}" ] && ARGS+=(--location "$LOCATION")
|
||||||
|
exec ./GameServer "${ARGS[@]}"
|
||||||
+1
-9
@@ -557,15 +557,7 @@ namespace FRG
|
|||||||
if (remoteCampaignActive) {
|
if (remoteCampaignActive) {
|
||||||
logger() << "[network] Campaign '" << campaignId
|
logger() << "[network] Campaign '" << campaignId
|
||||||
<< "' selected; creating WebSocket client" << std::endl;
|
<< "' selected; creating WebSocket client" << std::endl;
|
||||||
// Use "ws://127.0.0.1:8080/" for a local development server.
|
websocketGameClient = std::make_unique<WebsocketGameClient>();
|
||||||
static const char* const kMultiplayerServerUrl =
|
|
||||||
"wss://api.shadowoverbishkek.com/server001/";
|
|
||||||
WebsocketEndpoint endpoint;
|
|
||||||
if (!endpoint.parse(kMultiplayerServerUrl)) {
|
|
||||||
logger() << "[network] Invalid multiplayer server URL: "
|
|
||||||
<< kMultiplayerServerUrl << std::endl;
|
|
||||||
}
|
|
||||||
websocketGameClient = std::make_unique<WebsocketGameClient>(endpoint);
|
|
||||||
remoteGameServerHost =
|
remoteGameServerHost =
|
||||||
std::make_unique<RemoteGameServerHost>(*websocketGameClient);
|
std::make_unique<RemoteGameServerHost>(*websocketGameClient);
|
||||||
gameServer.use(*websocketGameClient);
|
gameServer.use(*websocketGameClient);
|
||||||
|
|||||||
@@ -5,21 +5,6 @@
|
|||||||
#else
|
#else
|
||||||
#include <boost/asio.hpp>
|
#include <boost/asio.hpp>
|
||||||
#include <boost/beast.hpp>
|
#include <boost/beast.hpp>
|
||||||
#ifdef NETWORK_TLS
|
|
||||||
#include <wincrypt.h>
|
|
||||||
// wincrypt.h defines macros that collide with OpenSSL identifiers.
|
|
||||||
#undef X509_NAME
|
|
||||||
#undef X509_EXTENSIONS
|
|
||||||
#undef X509_CERT_PAIR
|
|
||||||
#undef PKCS7_ISSUER_AND_SERIAL
|
|
||||||
#undef OCSP_REQUEST
|
|
||||||
#undef OCSP_RESPONSE
|
|
||||||
#include <boost/asio/ssl.hpp>
|
|
||||||
#include <boost/beast/ssl.hpp>
|
|
||||||
#include <openssl/err.h>
|
|
||||||
#include <openssl/x509.h>
|
|
||||||
#include <openssl/x509_vfy.h>
|
|
||||||
#endif
|
|
||||||
#endif
|
#endif
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
@@ -38,104 +23,19 @@ namespace asio = boost::asio;
|
|||||||
namespace beast = boost::beast;
|
namespace beast = boost::beast;
|
||||||
namespace websocket = beast::websocket;
|
namespace websocket = beast::websocket;
|
||||||
using tcp = asio::ip::tcp;
|
using tcp = asio::ip::tcp;
|
||||||
|
|
||||||
#ifdef NETWORK_TLS
|
|
||||||
// Loads the Windows "ROOT" certificate store into the OpenSSL context.
|
|
||||||
void loadWindowsRootCertificates(asio::ssl::context& context)
|
|
||||||
{
|
|
||||||
HCERTSTORE store = CertOpenSystemStoreA(0, "ROOT");
|
|
||||||
if (!store) {
|
|
||||||
std::cerr << "[network] Could not open the Windows root certificate store\n";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
X509_STORE* x509Store = SSL_CTX_get_cert_store(context.native_handle());
|
|
||||||
PCCERT_CONTEXT cert = nullptr;
|
|
||||||
while ((cert = CertEnumCertificatesInStore(store, cert)) != nullptr) {
|
|
||||||
const unsigned char* data = cert->pbCertEncoded;
|
|
||||||
X509* x509 = d2i_X509(nullptr, &data, static_cast<long>(cert->cbCertEncoded));
|
|
||||||
if (x509) {
|
|
||||||
X509_STORE_add_cert(x509Store, x509);
|
|
||||||
X509_free(x509);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CertCloseStore(store, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Certificate problems are logged but deliberately do not abort the connection.
|
|
||||||
bool logAndAcceptCertificate(bool preverified, asio::ssl::verify_context& context)
|
|
||||||
{
|
|
||||||
if (!preverified) {
|
|
||||||
X509_STORE_CTX* storeContext = context.native_handle();
|
|
||||||
const int error = X509_STORE_CTX_get_error(storeContext);
|
|
||||||
const int depth = X509_STORE_CTX_get_error_depth(storeContext);
|
|
||||||
char subject[256] = "?";
|
|
||||||
if (X509* cert = X509_STORE_CTX_get_current_cert(storeContext))
|
|
||||||
X509_NAME_oneline(X509_get_subject_name(cert), subject, sizeof(subject));
|
|
||||||
std::cerr << "[network] TLS certificate warning (depth " << depth
|
|
||||||
<< ", subject " << subject << "): "
|
|
||||||
<< X509_verify_cert_error_string(error)
|
|
||||||
<< " - continuing anyway\n";
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool WebsocketEndpoint::parse(const std::string& url)
|
|
||||||
{
|
|
||||||
bool tls = false;
|
|
||||||
std::string rest;
|
|
||||||
if (url.rfind("wss://", 0) == 0) {
|
|
||||||
tls = true;
|
|
||||||
rest = url.substr(6);
|
|
||||||
} else if (url.rfind("ws://", 0) == 0) {
|
|
||||||
rest = url.substr(5);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
std::string authority = rest;
|
|
||||||
std::string newPath = "/";
|
|
||||||
const std::size_t slash = rest.find('/');
|
|
||||||
if (slash != std::string::npos) {
|
|
||||||
authority = rest.substr(0, slash);
|
|
||||||
newPath = rest.substr(slash);
|
|
||||||
}
|
|
||||||
std::string newHost = authority;
|
|
||||||
std::string newPort = tls ? "443" : "80";
|
|
||||||
if (!authority.empty() && authority.front() == '[') { // [ipv6]:port
|
|
||||||
const std::size_t close = authority.find(']');
|
|
||||||
if (close == std::string::npos) return false;
|
|
||||||
newHost = authority.substr(1, close - 1);
|
|
||||||
if (close + 1 < authority.size()) {
|
|
||||||
if (authority[close + 1] != ':') return false;
|
|
||||||
newPort = authority.substr(close + 2);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const std::size_t colon = authority.rfind(':');
|
|
||||||
if (colon != std::string::npos) {
|
|
||||||
newHost = authority.substr(0, colon);
|
|
||||||
newPort = authority.substr(colon + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (newHost.empty() || newPort.empty()) return false;
|
|
||||||
host = std::move(newHost);
|
|
||||||
port = std::move(newPort);
|
|
||||||
path = std::move(newPath);
|
|
||||||
useTls = tls;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebsocketGameClient::Impl {
|
class WebsocketGameClient::Impl {
|
||||||
public:
|
public:
|
||||||
#ifdef __EMSCRIPTEN__
|
#ifdef __EMSCRIPTEN__
|
||||||
Impl(std::string host, std::string port, std::string path, bool useTls)
|
Impl(std::string host, std::string port, std::string path)
|
||||||
: host_(std::move(host))
|
: host_(std::move(host))
|
||||||
, port_(std::move(port))
|
, port_(std::move(port))
|
||||||
, path_(std::move(path))
|
, path_(std::move(path))
|
||||||
{
|
{
|
||||||
url_ = std::string(useTls ? "wss://" : "ws://") + host_ + ':' + port_ +
|
url_ = "ws://" + host_ + ':' + port_ +
|
||||||
(path_.empty() || path_.front() == '/' ? path_ : "/" + path_);
|
(path_.empty() || path_.front() == '/' ? path_ : "/" + path_);
|
||||||
std::cout << "[network] Opening " << url_ << std::endl;
|
std::cout << "[network] Opening " << url_ << std::endl;
|
||||||
|
|
||||||
@@ -243,38 +143,14 @@ private:
|
|||||||
bool closing_ = false;
|
bool closing_ = false;
|
||||||
std::vector<std::string> messages_;
|
std::vector<std::string> messages_;
|
||||||
#else
|
#else
|
||||||
Impl(std::string host, std::string port, std::string path, bool useTls)
|
Impl(std::string host, std::string port, std::string path)
|
||||||
:
|
: resolver_(io_)
|
||||||
#ifdef NETWORK_TLS
|
|
||||||
sslContext_(asio::ssl::context::tls_client)
|
|
||||||
, resolver_(io_)
|
|
||||||
, secureSocket_(io_, sslContext_)
|
|
||||||
#else
|
|
||||||
resolver_(io_)
|
|
||||||
#endif
|
|
||||||
, socket_(io_)
|
, socket_(io_)
|
||||||
, host_(std::move(host))
|
, host_(std::move(host))
|
||||||
, port_(std::move(port))
|
, port_(std::move(port))
|
||||||
, path_(std::move(path))
|
, path_(std::move(path))
|
||||||
, useTls_(useTls)
|
|
||||||
{
|
{
|
||||||
#ifdef NETWORK_TLS
|
std::cout << "[network] Resolving ws://" << host_ << ':' << port_
|
||||||
sslContext_.set_options(
|
|
||||||
asio::ssl::context::default_workarounds |
|
|
||||||
asio::ssl::context::no_sslv2 | asio::ssl::context::no_sslv3 |
|
|
||||||
asio::ssl::context::no_tlsv1 | asio::ssl::context::no_tlsv1_1);
|
|
||||||
loadWindowsRootCertificates(sslContext_);
|
|
||||||
sslContext_.set_verify_mode(asio::ssl::verify_peer);
|
|
||||||
sslContext_.set_verify_callback(&logAndAcceptCertificate);
|
|
||||||
#else
|
|
||||||
if (useTls_) {
|
|
||||||
std::cerr << "[network] wss:// requested but TLS support was not "
|
|
||||||
"compiled in (NETWORK_TLS)" << std::endl;
|
|
||||||
connectionFailed_.store(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
std::cout << "[network] Resolving " << scheme() << host_ << ':' << port_
|
|
||||||
<< path_ << std::endl;
|
<< path_ << std::endl;
|
||||||
resolve();
|
resolve();
|
||||||
thread_ = std::thread([this]() { io_.run(); });
|
thread_ = std::thread([this]() { io_.run(); });
|
||||||
@@ -308,20 +184,6 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const char* scheme() const { return useTls_ ? "wss://" : "ws://"; }
|
|
||||||
|
|
||||||
template <class F>
|
|
||||||
void withStream(F&& f)
|
|
||||||
{
|
|
||||||
#ifdef NETWORK_TLS
|
|
||||||
if (useTls_) {
|
|
||||||
f(secureSocket_);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
f(socket_);
|
|
||||||
}
|
|
||||||
|
|
||||||
void resolve()
|
void resolve()
|
||||||
{
|
{
|
||||||
resolver_.async_resolve(
|
resolver_.async_resolve(
|
||||||
@@ -330,97 +192,60 @@ private:
|
|||||||
if (error) return fail("resolve", error);
|
if (error) return fail("resolve", error);
|
||||||
std::cout << "[network] WebSocket host resolved; connecting"
|
std::cout << "[network] WebSocket host resolved; connecting"
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
withStream([&](auto& stream) {
|
asio::async_connect(
|
||||||
asio::async_connect(
|
socket_.next_layer(), results,
|
||||||
beast::get_lowest_layer(stream), results,
|
[this](beast::error_code connectError,
|
||||||
[this](beast::error_code connectError,
|
const tcp::endpoint&) {
|
||||||
const tcp::endpoint&) {
|
if (connectError) return fail("connect", connectError);
|
||||||
if (connectError) return fail("connect", connectError);
|
std::cout << "[network] TCP connected; starting WebSocket handshake"
|
||||||
std::cout << "[network] TCP connected" << std::endl;
|
<< std::endl;
|
||||||
startTlsOrWebsocketHandshake();
|
socket_.set_option(
|
||||||
});
|
websocket::stream_base::timeout::suggested(
|
||||||
});
|
beast::role_type::client));
|
||||||
|
socket_.async_handshake(
|
||||||
|
host_, path_,
|
||||||
|
[this](beast::error_code handshakeError) {
|
||||||
|
if (handshakeError)
|
||||||
|
return fail("handshake", handshakeError);
|
||||||
|
connected_.store(true);
|
||||||
|
std::cout
|
||||||
|
<< "[network] Connected to ws://"
|
||||||
|
<< host_ << ':' << port_ << path_ << std::endl;
|
||||||
|
read();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void startTlsOrWebsocketHandshake()
|
|
||||||
{
|
|
||||||
#ifdef NETWORK_TLS
|
|
||||||
if (useTls_) {
|
|
||||||
SSL* ssl = secureSocket_.next_layer().native_handle();
|
|
||||||
// SNI, and hostname verification (failures are only logged).
|
|
||||||
SSL_set_tlsext_host_name(ssl, const_cast<char*>(host_.c_str()));
|
|
||||||
SSL_set1_host(ssl, host_.c_str());
|
|
||||||
std::cout << "[network] Starting TLS handshake" << std::endl;
|
|
||||||
secureSocket_.next_layer().async_handshake(
|
|
||||||
asio::ssl::stream_base::client,
|
|
||||||
[this](beast::error_code tlsError) {
|
|
||||||
if (tlsError) return fail("TLS handshake", tlsError);
|
|
||||||
std::cout << "[network] TLS established" << std::endl;
|
|
||||||
startWebsocketHandshake();
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
startWebsocketHandshake();
|
|
||||||
}
|
|
||||||
|
|
||||||
void startWebsocketHandshake()
|
|
||||||
{
|
|
||||||
const bool defaultPort = port_ == (useTls_ ? "443" : "80");
|
|
||||||
const std::string hostHeader = defaultPort ? host_ : host_ + ':' + port_;
|
|
||||||
std::cout << "[network] Starting WebSocket handshake" << std::endl;
|
|
||||||
withStream([&](auto& stream) {
|
|
||||||
stream.set_option(
|
|
||||||
websocket::stream_base::timeout::suggested(
|
|
||||||
beast::role_type::client));
|
|
||||||
stream.async_handshake(
|
|
||||||
hostHeader, path_,
|
|
||||||
[this](beast::error_code handshakeError) {
|
|
||||||
if (handshakeError)
|
|
||||||
return fail("handshake", handshakeError);
|
|
||||||
connected_.store(true);
|
|
||||||
std::cout
|
|
||||||
<< "[network] Connected to " << scheme()
|
|
||||||
<< host_ << ':' << port_ << path_ << std::endl;
|
|
||||||
read();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void read()
|
void read()
|
||||||
{
|
{
|
||||||
withStream([this](auto& stream) {
|
socket_.async_read(
|
||||||
stream.async_read(
|
buffer_,
|
||||||
buffer_,
|
[this](beast::error_code error, std::size_t) {
|
||||||
[this](beast::error_code error, std::size_t) {
|
if (error) {
|
||||||
if (error) {
|
connected_.store(false);
|
||||||
connected_.store(false);
|
if (error != websocket::error::closed) fail("read", error);
|
||||||
if (error != websocket::error::closed) fail("read", error);
|
return;
|
||||||
return;
|
}
|
||||||
}
|
std::string message = beast::buffers_to_string(buffer_.data());
|
||||||
std::string message = beast::buffers_to_string(buffer_.data());
|
buffer_.consume(buffer_.size());
|
||||||
buffer_.consume(buffer_.size());
|
{
|
||||||
{
|
std::lock_guard<std::mutex> lock(messagesMutex_);
|
||||||
std::lock_guard<std::mutex> lock(messagesMutex_);
|
messages_.push_back(std::move(message));
|
||||||
messages_.push_back(std::move(message));
|
}
|
||||||
}
|
read();
|
||||||
read();
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void write()
|
void write()
|
||||||
{
|
{
|
||||||
withStream([this](auto& stream) {
|
socket_.async_write(
|
||||||
stream.async_write(
|
asio::buffer(outbound_.front()),
|
||||||
asio::buffer(outbound_.front()),
|
[this](beast::error_code error, std::size_t) {
|
||||||
[this](beast::error_code error, std::size_t) {
|
if (error) return fail("write", error);
|
||||||
if (error) return fail("write", error);
|
outbound_.pop_front();
|
||||||
outbound_.pop_front();
|
if (!outbound_.empty()) write();
|
||||||
if (!outbound_.empty()) write();
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void fail(const char* operation, beast::error_code error)
|
void fail(const char* operation, beast::error_code error)
|
||||||
@@ -432,20 +257,13 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
asio::io_context io_;
|
asio::io_context io_;
|
||||||
#ifdef NETWORK_TLS
|
|
||||||
asio::ssl::context sslContext_;
|
|
||||||
#endif
|
|
||||||
tcp::resolver resolver_;
|
tcp::resolver resolver_;
|
||||||
#ifdef NETWORK_TLS
|
|
||||||
websocket::stream<beast::ssl_stream<tcp::socket>> secureSocket_;
|
|
||||||
#endif
|
|
||||||
websocket::stream<tcp::socket> socket_;
|
websocket::stream<tcp::socket> socket_;
|
||||||
beast::flat_buffer buffer_;
|
beast::flat_buffer buffer_;
|
||||||
std::thread thread_;
|
std::thread thread_;
|
||||||
std::string host_;
|
std::string host_;
|
||||||
std::string port_;
|
std::string port_;
|
||||||
std::string path_;
|
std::string path_;
|
||||||
bool useTls_ = false;
|
|
||||||
std::atomic_bool connected_{false};
|
std::atomic_bool connected_{false};
|
||||||
std::atomic_bool connectionFailed_{false};
|
std::atomic_bool connectionFailed_{false};
|
||||||
std::mutex messagesMutex_;
|
std::mutex messagesMutex_;
|
||||||
@@ -455,15 +273,9 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
WebsocketGameClient::WebsocketGameClient(
|
WebsocketGameClient::WebsocketGameClient(
|
||||||
std::string host, std::string port, std::string path, bool useTls)
|
std::string host, std::string port, std::string path)
|
||||||
: impl_(std::make_unique<Impl>(
|
: impl_(std::make_unique<Impl>(
|
||||||
std::move(host), std::move(port), std::move(path), useTls))
|
std::move(host), std::move(port), std::move(path)))
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
WebsocketGameClient::WebsocketGameClient(const WebsocketEndpoint& endpoint)
|
|
||||||
: WebsocketGameClient(
|
|
||||||
endpoint.host, endpoint.port, endpoint.path, endpoint.useTls)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,26 +11,12 @@ namespace FRG {
|
|||||||
|
|
||||||
class RemoteGameServerHost;
|
class RemoteGameServerHost;
|
||||||
|
|
||||||
// Parsed "ws://host[:port][/path]" or "wss://host[:port][/path]".
|
|
||||||
struct WebsocketEndpoint {
|
|
||||||
std::string host = "127.0.0.1";
|
|
||||||
std::string port = "8080";
|
|
||||||
std::string path = "/";
|
|
||||||
bool useTls = false;
|
|
||||||
|
|
||||||
// Returns false (and leaves *this untouched) for an unsupported/malformed URL.
|
|
||||||
// Default ports: 80 for ws://, 443 for wss://.
|
|
||||||
bool parse(const std::string& url);
|
|
||||||
};
|
|
||||||
|
|
||||||
class WebsocketGameClient final : public IGameServer {
|
class WebsocketGameClient final : public IGameServer {
|
||||||
public:
|
public:
|
||||||
WebsocketGameClient(
|
WebsocketGameClient(
|
||||||
std::string host = "127.0.0.1",
|
std::string host = "127.0.0.1",
|
||||||
std::string port = "8080",
|
std::string port = "8080",
|
||||||
std::string path = "/",
|
std::string path = "/");
|
||||||
bool useTls = false);
|
|
||||||
explicit WebsocketGameClient(const WebsocketEndpoint& endpoint);
|
|
||||||
~WebsocketGameClient();
|
~WebsocketGameClient();
|
||||||
|
|
||||||
WebsocketGameClient(const WebsocketGameClient&) = delete;
|
WebsocketGameClient(const WebsocketGameClient&) = delete;
|
||||||
|
|||||||
Reference in new issue
Block a user