Compare commits
1
Commits
main
..
networking2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28fb81ad45 |
No files matched your search
+12
-2
@@ -41,7 +41,7 @@ configured player slot connects or disconnects.
|
|||||||
|
|
||||||
`WebsocketGameClient` implements `IGameServer`.
|
`WebsocketGameClient` implements `IGameServer`.
|
||||||
|
|
||||||
- It connects asynchronously to `ws://127.0.0.1:8080/`.
|
- It connects asynchronously to a `ws://` or `wss://` endpoint (`WebsocketEndpoint::parse`).
|
||||||
- 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,6 +145,13 @@ 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.
|
||||||
|
|
||||||
@@ -196,7 +203,10 @@ 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.
|
- The endpoint is hardcoded in the graphical client (`kMultiplayerServerUrl` in
|
||||||
|
`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,3 +56,30 @@ 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()
|
||||||
+103
-1
@@ -725,4 +725,106 @@ set_target_properties(SDL2_mixer_external_lib PROPERTIES
|
|||||||
"$<IF:$<CONFIG:Debug>,${SDL2MIXER_BASE_DIR}-Debug/include,${SDL2MIXER_BASE_DIR}-Release/include>"
|
"$<IF:$<CONFIG:Debug>,${SDL2MIXER_BASE_DIR}-Debug/include,${SDL2MIXER_BASE_DIR}-Release/include>"
|
||||||
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,6 +143,7 @@ target_compile_definitions(ShadowOverBishkekDemo PRIVATE
|
|||||||
_WIN32_WINNT=0x0A00
|
_WIN32_WINNT=0x0A00
|
||||||
PNG_ENABLED
|
PNG_ENABLED
|
||||||
NETWORK
|
NETWORK
|
||||||
|
NETWORK_TLS
|
||||||
)
|
)
|
||||||
|
|
||||||
# Линкуем с SDL2main, если он вообще установлен
|
# Линкуем с SDL2main, если он вообще установлен
|
||||||
@@ -159,7 +160,8 @@ target_link_libraries(ShadowOverBishkekDemo PRIVATE
|
|||||||
eigen_external_lib
|
eigen_external_lib
|
||||||
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)
|
||||||
|
|||||||
+9
-1
@@ -557,7 +557,15 @@ 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;
|
||||||
websocketGameClient = std::make_unique<WebsocketGameClient>();
|
// Use "ws://127.0.0.1:8080/" for a local development server.
|
||||||
|
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,6 +5,21 @@
|
|||||||
#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>
|
||||||
@@ -23,19 +38,104 @@ 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)
|
Impl(std::string host, std::string port, std::string path, bool useTls)
|
||||||
: 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_ = "ws://" + host_ + ':' + port_ +
|
url_ = std::string(useTls ? "wss://" : "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;
|
||||||
|
|
||||||
@@ -143,14 +243,38 @@ 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)
|
Impl(std::string host, std::string port, std::string path, bool useTls)
|
||||||
: 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)
|
||||||
{
|
{
|
||||||
std::cout << "[network] Resolving ws://" << host_ << ':' << port_
|
#ifdef NETWORK_TLS
|
||||||
|
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(); });
|
||||||
@@ -184,6 +308,20 @@ 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(
|
||||||
@@ -192,60 +330,97 @@ 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;
|
||||||
asio::async_connect(
|
withStream([&](auto& stream) {
|
||||||
socket_.next_layer(), results,
|
asio::async_connect(
|
||||||
[this](beast::error_code connectError,
|
beast::get_lowest_layer(stream), results,
|
||||||
const tcp::endpoint&) {
|
[this](beast::error_code connectError,
|
||||||
if (connectError) return fail("connect", connectError);
|
const tcp::endpoint&) {
|
||||||
std::cout << "[network] TCP connected; starting WebSocket handshake"
|
if (connectError) return fail("connect", connectError);
|
||||||
<< std::endl;
|
std::cout << "[network] TCP connected" << std::endl;
|
||||||
socket_.set_option(
|
startTlsOrWebsocketHandshake();
|
||||||
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()
|
||||||
{
|
{
|
||||||
socket_.async_read(
|
withStream([this](auto& stream) {
|
||||||
buffer_,
|
stream.async_read(
|
||||||
[this](beast::error_code error, std::size_t) {
|
buffer_,
|
||||||
if (error) {
|
[this](beast::error_code error, std::size_t) {
|
||||||
connected_.store(false);
|
if (error) {
|
||||||
if (error != websocket::error::closed) fail("read", error);
|
connected_.store(false);
|
||||||
return;
|
if (error != websocket::error::closed) fail("read", error);
|
||||||
}
|
return;
|
||||||
std::string message = beast::buffers_to_string(buffer_.data());
|
}
|
||||||
buffer_.consume(buffer_.size());
|
std::string message = beast::buffers_to_string(buffer_.data());
|
||||||
{
|
buffer_.consume(buffer_.size());
|
||||||
std::lock_guard<std::mutex> lock(messagesMutex_);
|
{
|
||||||
messages_.push_back(std::move(message));
|
std::lock_guard<std::mutex> lock(messagesMutex_);
|
||||||
}
|
messages_.push_back(std::move(message));
|
||||||
read();
|
}
|
||||||
});
|
read();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void write()
|
void write()
|
||||||
{
|
{
|
||||||
socket_.async_write(
|
withStream([this](auto& stream) {
|
||||||
asio::buffer(outbound_.front()),
|
stream.async_write(
|
||||||
[this](beast::error_code error, std::size_t) {
|
asio::buffer(outbound_.front()),
|
||||||
if (error) return fail("write", error);
|
[this](beast::error_code error, std::size_t) {
|
||||||
outbound_.pop_front();
|
if (error) return fail("write", error);
|
||||||
if (!outbound_.empty()) write();
|
outbound_.pop_front();
|
||||||
});
|
if (!outbound_.empty()) write();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void fail(const char* operation, beast::error_code error)
|
void fail(const char* operation, beast::error_code error)
|
||||||
@@ -257,13 +432,20 @@ 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_;
|
||||||
@@ -273,9 +455,15 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
WebsocketGameClient::WebsocketGameClient(
|
WebsocketGameClient::WebsocketGameClient(
|
||||||
std::string host, std::string port, std::string path)
|
std::string host, std::string port, std::string path, bool useTls)
|
||||||
: impl_(std::make_unique<Impl>(
|
: impl_(std::make_unique<Impl>(
|
||||||
std::move(host), std::move(port), std::move(path)))
|
std::move(host), std::move(port), std::move(path), useTls))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
WebsocketGameClient::WebsocketGameClient(const WebsocketEndpoint& endpoint)
|
||||||
|
: WebsocketGameClient(
|
||||||
|
endpoint.host, endpoint.port, endpoint.path, endpoint.useTls)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,26 @@ 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