Compare commits
1
Commits
server
...
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`.
|
||||
|
||||
- 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
|
||||
thread from `update()`.
|
||||
- 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.
|
||||
`WebsocketGameClient.cpp` selects the browser WebSocket API on Emscripten; it
|
||||
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
|
||||
network transport.
|
||||
|
||||
@@ -196,7 +203,10 @@ submission, and contiguous tick consumption.
|
||||
not implemented.
|
||||
- There is no authentication or reconnect/replay history.
|
||||
- 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.
|
||||
- Determinism is supported for identical executable/content builds. The
|
||||
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
|
||||
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>"
|
||||
INTERFACE_LINK_LIBRARIES
|
||||
"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
|
||||
PNG_ENABLED
|
||||
NETWORK
|
||||
NETWORK_TLS
|
||||
)
|
||||
|
||||
# Линкуем с SDL2main, если он вообще установлен
|
||||
@@ -159,7 +160,8 @@ target_link_libraries(ShadowOverBishkekDemo PRIVATE
|
||||
eigen_external_lib
|
||||
boost_external_lib
|
||||
sol2_external_lib
|
||||
SDL2_mixer_external_lib
|
||||
SDL2_mixer_external_lib
|
||||
openssl_ssl_external_lib
|
||||
)
|
||||
|
||||
# Линкуем OpenGL (Windows)
|
||||
|
||||
+9
-1
@@ -557,7 +557,15 @@ namespace FRG
|
||||
if (remoteCampaignActive) {
|
||||
logger() << "[network] Campaign '" << campaignId
|
||||
<< "' 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 =
|
||||
std::make_unique<RemoteGameServerHost>(*websocketGameClient);
|
||||
gameServer.use(*websocketGameClient);
|
||||
|
||||
@@ -5,6 +5,21 @@
|
||||
#else
|
||||
#include <boost/asio.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
|
||||
#include <atomic>
|
||||
#include <deque>
|
||||
@@ -23,19 +38,104 @@ namespace asio = boost::asio;
|
||||
namespace beast = boost::beast;
|
||||
namespace websocket = beast::websocket;
|
||||
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
|
||||
|
||||
} // 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 {
|
||||
public:
|
||||
#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))
|
||||
, port_(std::move(port))
|
||||
, path_(std::move(path))
|
||||
{
|
||||
url_ = "ws://" + host_ + ':' + port_ +
|
||||
url_ = std::string(useTls ? "wss://" : "ws://") + host_ + ':' + port_ +
|
||||
(path_.empty() || path_.front() == '/' ? path_ : "/" + path_);
|
||||
std::cout << "[network] Opening " << url_ << std::endl;
|
||||
|
||||
@@ -143,14 +243,38 @@ private:
|
||||
bool closing_ = false;
|
||||
std::vector<std::string> messages_;
|
||||
#else
|
||||
Impl(std::string host, std::string port, std::string path)
|
||||
: resolver_(io_)
|
||||
Impl(std::string host, std::string port, std::string path, bool useTls)
|
||||
:
|
||||
#ifdef NETWORK_TLS
|
||||
sslContext_(asio::ssl::context::tls_client)
|
||||
, resolver_(io_)
|
||||
, secureSocket_(io_, sslContext_)
|
||||
#else
|
||||
resolver_(io_)
|
||||
#endif
|
||||
, socket_(io_)
|
||||
, host_(std::move(host))
|
||||
, port_(std::move(port))
|
||||
, 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;
|
||||
resolve();
|
||||
thread_ = std::thread([this]() { io_.run(); });
|
||||
@@ -184,6 +308,20 @@ 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()
|
||||
{
|
||||
resolver_.async_resolve(
|
||||
@@ -192,60 +330,97 @@ private:
|
||||
if (error) return fail("resolve", error);
|
||||
std::cout << "[network] WebSocket host resolved; connecting"
|
||||
<< std::endl;
|
||||
asio::async_connect(
|
||||
socket_.next_layer(), results,
|
||||
[this](beast::error_code connectError,
|
||||
const tcp::endpoint&) {
|
||||
if (connectError) return fail("connect", connectError);
|
||||
std::cout << "[network] TCP connected; starting WebSocket handshake"
|
||||
<< std::endl;
|
||||
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();
|
||||
});
|
||||
});
|
||||
withStream([&](auto& stream) {
|
||||
asio::async_connect(
|
||||
beast::get_lowest_layer(stream), results,
|
||||
[this](beast::error_code connectError,
|
||||
const tcp::endpoint&) {
|
||||
if (connectError) return fail("connect", connectError);
|
||||
std::cout << "[network] TCP connected" << std::endl;
|
||||
startTlsOrWebsocketHandshake();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
socket_.async_read(
|
||||
buffer_,
|
||||
[this](beast::error_code error, std::size_t) {
|
||||
if (error) {
|
||||
connected_.store(false);
|
||||
if (error != websocket::error::closed) fail("read", error);
|
||||
return;
|
||||
}
|
||||
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));
|
||||
}
|
||||
read();
|
||||
});
|
||||
withStream([this](auto& stream) {
|
||||
stream.async_read(
|
||||
buffer_,
|
||||
[this](beast::error_code error, std::size_t) {
|
||||
if (error) {
|
||||
connected_.store(false);
|
||||
if (error != websocket::error::closed) fail("read", error);
|
||||
return;
|
||||
}
|
||||
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));
|
||||
}
|
||||
read();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void write()
|
||||
{
|
||||
socket_.async_write(
|
||||
asio::buffer(outbound_.front()),
|
||||
[this](beast::error_code error, std::size_t) {
|
||||
if (error) return fail("write", error);
|
||||
outbound_.pop_front();
|
||||
if (!outbound_.empty()) write();
|
||||
});
|
||||
withStream([this](auto& stream) {
|
||||
stream.async_write(
|
||||
asio::buffer(outbound_.front()),
|
||||
[this](beast::error_code error, std::size_t) {
|
||||
if (error) return fail("write", error);
|
||||
outbound_.pop_front();
|
||||
if (!outbound_.empty()) write();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void fail(const char* operation, beast::error_code error)
|
||||
@@ -257,13 +432,20 @@ private:
|
||||
}
|
||||
|
||||
asio::io_context io_;
|
||||
#ifdef NETWORK_TLS
|
||||
asio::ssl::context sslContext_;
|
||||
#endif
|
||||
tcp::resolver resolver_;
|
||||
#ifdef NETWORK_TLS
|
||||
websocket::stream<beast::ssl_stream<tcp::socket>> secureSocket_;
|
||||
#endif
|
||||
websocket::stream<tcp::socket> socket_;
|
||||
beast::flat_buffer buffer_;
|
||||
std::thread thread_;
|
||||
std::string host_;
|
||||
std::string port_;
|
||||
std::string path_;
|
||||
bool useTls_ = false;
|
||||
std::atomic_bool connected_{false};
|
||||
std::atomic_bool connectionFailed_{false};
|
||||
std::mutex messagesMutex_;
|
||||
@@ -273,9 +455,15 @@ private:
|
||||
};
|
||||
|
||||
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>(
|
||||
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;
|
||||
|
||||
// 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 {
|
||||
public:
|
||||
WebsocketGameClient(
|
||||
std::string host = "127.0.0.1",
|
||||
std::string port = "8080",
|
||||
std::string path = "/");
|
||||
std::string path = "/",
|
||||
bool useTls = false);
|
||||
explicit WebsocketGameClient(const WebsocketEndpoint& endpoint);
|
||||
~WebsocketGameClient();
|
||||
|
||||
WebsocketGameClient(const WebsocketGameClient&) = delete;
|
||||
|
||||
Reference in new issue
Block a user