diff --git a/WEBSOCKET_NETWORKING.md b/WEBSOCKET_NETWORKING.md index a1c9b6a..6de59ec 100644 --- a/WEBSOCKET_NETWORKING.md +++ b/WEBSOCKET_NETWORKING.md @@ -144,14 +144,15 @@ Networking is enabled for Windows and the Emscripten web client: - `proj-web/CMakeLists.txt` defines `NETWORK`, compiles the shared protocol, 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 + does not use Boost.Asio, Boost.Beast, or OpenSSL there. The browser performs + the TLS handshake and certificate/hostname validation for `wss://`. +- `proj-windows` 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. + means native `wss://` endpoints fail with a logged error. - Linux and Android builds do not define `NETWORK` and do not compile the network transport. @@ -170,15 +171,24 @@ Run it from the repository root: .\server\build\Debug\GameServer.exe --campaign battle_zone_multiplayer --location uni_exterior ``` -The listener defaults to `0.0.0.0:8080`. It can be changed with: +The listener defaults to `0.0.0.0:8080` and serves plain WebSocket traffic. It can be changed with: ```text --address IP --port N ``` -Launch the Windows game and select `battle_zone_multiplayer`. The client connects to -`127.0.0.1:8080`. Selecting another campaign continues to use the local server. +Launch the Windows game and select `battle_zone_multiplayer`. The production client +default is `wss://api.shadowoverbishkek.com/server001/`; the endpoint is currently +configured in `Game.cpp`. Selecting another campaign continues to use the local +server. + +For the Web build, serve the page over HTTPS and ensure the TLS-terminating reverse +proxy presents a valid certificate for `api.shadowoverbishkek.com`, accepts the +WebSocket upgrade at `/server001/`, and forwards the upgraded connection to the +plain server listener. A page served over HTTPS cannot connect to a `ws://` endpoint +because browsers block that mixed-content request. Certificate and hostname errors +are enforced by the browser and cannot be bypassed by the C++ client. ## Verification diff --git a/server/tests/WebsocketLockstepTests.cpp b/server/tests/WebsocketLockstepTests.cpp index ea271de..f4812bc 100644 --- a/server/tests/WebsocketLockstepTests.cpp +++ b/server/tests/WebsocketLockstepTests.cpp @@ -32,6 +32,31 @@ bool waitFor(Predicate predicate) int main() { + FRG::WebsocketEndpoint secureEndpoint; + require( + secureEndpoint.parse( + "wss://api.shadowoverbishkek.com/server001/"), + "secure WebSocket endpoint was rejected"); + require( + secureEndpoint.host == "api.shadowoverbishkek.com" && + secureEndpoint.port == "443" && + secureEndpoint.path == "/server001/" && + secureEndpoint.useTls, + "secure WebSocket endpoint was parsed incorrectly"); + FRG::WebsocketEndpoint insecureEndpoint; + require( + insecureEndpoint.parse("ws://127.0.0.1:8080/"), + "insecure WebSocket endpoint was rejected"); + require( + insecureEndpoint.host == "127.0.0.1" && + insecureEndpoint.port == "8080" && + !insecureEndpoint.useTls, + "insecure WebSocket endpoint was parsed incorrectly"); + FRG::WebsocketEndpoint invalidEndpoint; + require( + !invalidEndpoint.parse("https://api.shadowoverbishkek.com/"), + "non-WebSocket endpoint was accepted"); + FRG::CampaignConfig campaign; campaign.id = "test_lockstep"; campaign.networkPlayerSlots = {"player", "player2"}; diff --git a/src/Game.cpp b/src/Game.cpp index 712ec8c..24d7122 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -564,6 +564,10 @@ namespace FRG if (!endpoint.parse(kMultiplayerServerUrl)) { logger() << "[network] Invalid multiplayer server URL: " << kMultiplayerServerUrl << std::endl; + remoteCampaignActive = false; + gameServer.use(internalGameServer); + gameServerHost.use(localGameServerHost); + return; } websocketGameClient = std::make_unique(endpoint); remoteGameServerHost = diff --git a/src/game_server/WebsocketGameClient.cpp b/src/game_server/WebsocketGameClient.cpp index fcb4b6a..0b1f5e7 100644 --- a/src/game_server/WebsocketGameClient.cpp +++ b/src/game_server/WebsocketGameClient.cpp @@ -145,7 +145,7 @@ public: attributes.createOnMainThread = EM_TRUE; socket_ = emscripten_websocket_new(&attributes); if (socket_ <= 0) { - fail("create WebSocket"); + fail("create WebSocket", socket_); return; } @@ -205,11 +205,21 @@ private: } static EM_BOOL onClose( - int, const EmscriptenWebSocketCloseEvent*, void* userData) + int, const EmscriptenWebSocketCloseEvent* event, void* userData) { auto* self = static_cast(userData); self->connected_ = false; - if (!self->closing_) self->fail("WebSocket close"); + if (!self->closing_) { + std::cerr << "[network] WebSocket closed"; + if (event) { + std::cerr << " (code " << event->code; + if (event->reason[0] != '\0') + std::cerr << ", reason: " << event->reason; + std::cerr << ')'; + } + std::cerr << '\n'; + self->connectionFailed_ = true; + } return EM_TRUE; } @@ -226,11 +236,13 @@ private: return EM_TRUE; } - void fail(const char* operation) + void fail(const char* operation, int result = 0) { connected_ = false; connectionFailed_ = true; - std::cerr << "[network] " << operation << " failed" << std::endl; + std::cerr << "[network] " << operation << " failed"; + if (result != 0) std::cerr << " (result " << result << ')'; + std::cerr << std::endl; } std::string host_;