From 1a8de6d2b8930004e3eeb67dd785bbc2d8521a41 Mon Sep 17 00:00:00 2001 From: Angen Date: Wed, 20 Jan 2021 18:31:39 +0000 Subject: [PATCH] Hide ip and port from users until they want to join, add optional password Current issue with the lobby, is that we make ips of hosts public for anyone to read. This patch consists of 3 parts. 1.) Removing ips and ports from lobby javascript 2.) Removing need of script on the server to attach public ips to game stanza by asking the host using xmppclient as proxy. 3.) Implementing password protected matches, to deny this information to not trusted players. Further description: Do not send ports and stunip to the bots. Removed from stanza. Do not send ip to the lobby. Removed from mapping gamelist from backend to gui (still on the backend side, because it is done by script on 0ad server). Get ip and ports on request when trying to connect. On the host side, ask stun server what is host's public ip and remember it. On the client side, send iq through xmppclient to the hosting player and ask for ip, port and if Stun is used, then if answer is success, continue with connecting, else fail. Add optional password for matches. Add password required identifier to the stanza. Allow host to setup password for the match. Hash it on the host side and store inside Netserver. If no password is given, matches will behave as it is not required. On the client side, if password for the match is required, show additional window before trying to connect and ask for password, then hash it and send with iq request for ip, port and stun. Server will answer with ip, port and stun only if passwords matches, else will asnwer with error string. Some security: Passwords are hashed before sending, so it is not easy to guess what users typed. (per wraitii) Hashes are using different salt as lobby hashing and not using usernames as salt (as that is not doable), so they are different even typing the same password as for the lobby account. Client remembers which user was asked for connection data and iq's id of request. If answer doesn't match these things, it is ignored. (thnx user1) Every request for connection data is logged with hostname of the requester to the mainlog file (no ips). If user gets iq to send connection data and is not hosting the match, will respond with error string "not_server". If server gets iq::result with connection data, request is ignored. Differential revision: D3184 Reviewed by: @wraitii Comments by: @Stan, @bb, @Imarok, @vladislavbelov Tested in lobby This was SVN commit r24728. --- .../data/mods/public/gui/common/network.js | 3 + .../NetMessages/GameRegisterStanza.js | 9 +- .../public/gui/gamesetup_mp/gamesetup_mp.js | 177 ++++++++++++++---- .../public/gui/gamesetup_mp/gamesetup_mp.xml | 35 +++- .../gui/lobby/LobbyPage/Buttons/JoinButton.js | 28 +-- .../mods/public/gui/lobby/LobbyPage/Game.js | 5 +- .../public/gui/lobby/LobbyPage/GameList.js | 7 +- build/premake/premake5.lua | 1 + source/lobby/IXmppClient.h | 3 +- source/lobby/StanzaExtensions.cpp | 70 ++++++- source/lobby/StanzaExtensions.h | 24 +++ source/lobby/XmppClient.cpp | 91 ++++++++- source/lobby/XmppClient.h | 7 +- source/network/NetClient.cpp | 95 +++++++++- source/network/NetClient.h | 27 ++- source/network/NetHost.h | 5 +- source/network/NetServer.cpp | 34 +++- source/network/NetServer.h | 16 ++ source/network/StunClient.cpp | 28 +-- source/network/StunClient.h | 9 +- .../network/scripting/JSInterface_Network.cpp | 133 ++++++++----- .../network/scripting/JSInterface_Network.h | 12 +- source/network/tests/test_Net.h | 10 +- source/ps/GameSetup/GameSetup.cpp | 7 +- 24 files changed, 672 insertions(+), 164 deletions(-) diff --git a/binaries/data/mods/public/gui/common/network.js b/binaries/data/mods/public/gui/common/network.js index 1f63d5a65a..2ad5ea5621 100644 --- a/binaries/data/mods/public/gui/common/network.js +++ b/binaries/data/mods/public/gui/common/network.js @@ -76,6 +76,9 @@ function getDisconnectReason(id, wasConnected) case 10: return translate("Error: Server failed to allocate a unique client identifier."); case 11: return translate("Error: Client commands were ready for an unexpected game turn."); case 12: return translate("Error: Client simulated an unexpected game turn."); + case 13: return translate("Password is invalid."); + case 14: return translate("Could not find an unused port for the enet STUN client."); + case 15: return translate("Could not find the STUN endpoint."); default: warn("Unknown disconnect-reason ID received: " + id); return sprintf(translate("\\[Invalid value %(id)s]"), { "id": id }); diff --git a/binaries/data/mods/public/gui/gamesetup/NetMessages/GameRegisterStanza.js b/binaries/data/mods/public/gui/gamesetup/NetMessages/GameRegisterStanza.js index ad571c8888..e4a70f817b 100644 --- a/binaries/data/mods/public/gui/gamesetup/NetMessages/GameRegisterStanza.js +++ b/binaries/data/mods/public/gui/gamesetup/NetMessages/GameRegisterStanza.js @@ -10,8 +10,7 @@ class GameRegisterStanza this.mapCache = mapCache; this.serverName = initData.serverName; - this.serverPort = initData.serverPort; - this.stunEndpoint = initData.stunEndpoint; + this.hasPassword = initData.hasPassword; this.mods = JSON.stringify(Engine.GetEngineInfo().mods); this.timer = undefined; @@ -83,7 +82,6 @@ class GameRegisterStanza let stanza = { "name": this.serverName, - "port": this.serverPort, "hostUsername": Engine.LobbyGetNick(), "mapName": g_GameAttributes.map, "niceMapName": this.mapCache.getTranslatableMapName(g_GameAttributes.mapType, g_GameAttributes.map), @@ -93,9 +91,8 @@ class GameRegisterStanza "nbp": clients.connectedPlayers, "maxnbp": g_GameAttributes.settings.PlayerData.length, "players": clients.list, - "stunIP": this.stunEndpoint ? this.stunEndpoint.ip : "", - "stunPort": this.stunEndpoint ? this.stunEndpoint.port : "", - "mods": this.mods + "mods": this.mods, + "hasPassword": this.hasPassword || "" }; // Only send the stanza if one of these properties changed diff --git a/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js b/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js index 128e0fea05..3dfbfb955d 100644 --- a/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js +++ b/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js @@ -14,20 +14,17 @@ var g_GameType; var g_ServerName = ""; /** - * Cached to pass it to the game setup of the controller to report the game to the lobby. + * Identifier if server is using password. */ -var g_ServerPort; +var g_ServerHasPassword = false; + +var g_ServerId; var g_IsRejoining = false; var g_GameAttributes; // used when rejoining var g_PlayerAssignments; // used when rejoining var g_UserRating; -/** - * Object containing the IP address and port of the STUN server. - */ -var g_StunEndpoint; - function init(attribs) { g_UserRating = attribs.rating; @@ -36,19 +33,27 @@ function init(attribs) { case "join": { - if (Engine.HasXmppClient()) + if (!Engine.HasXmppClient()) { - if (startJoin(attribs.name, attribs.ip, getValidPort(attribs.port), attribs.useSTUN, attribs.hostJID)) - switchSetupPage("pageConnecting"); - } - else switchSetupPage("pageJoin"); + break; + } + if (attribs.hasPassword) + { + g_ServerName = attribs.name; + g_ServerId = attribs.hostJID; + switchSetupPage("pagePassword"); + } + else if (startJoinFromLobby(attribs.name, attribs.hostJID, "")) + switchSetupPage("pageConnecting"); break; } case "host": { - Engine.GetGUIObjectByName("hostSTUNWrapper").hidden = !Engine.HasXmppClient(); - if (Engine.HasXmppClient()) + let hasXmppClient = Engine.HasXmppClient(); + Engine.GetGUIObjectByName("hostSTUNWrapper").hidden = !hasXmppClient; + Engine.GetGUIObjectByName("hostPasswordWrapper").hidden = !hasXmppClient; + if (hasXmppClient) { Engine.GetGUIObjectByName("hostPlayerName").caption = attribs.name; Engine.GetGUIObjectByName("hostServerName").caption = @@ -92,6 +97,14 @@ function cancelSetup() error("cancelSetup: Unrecognised multiplayer game type: " + g_GameType); } +function confirmPassword() +{ + if (Engine.GetGUIObjectByName("pagePassword").hidden) + return; + if (startJoinFromLobby(g_ServerName, g_ServerId, Engine.GetGUIObjectByName("clientPassword").caption)) + switchSetupPage("pageConnecting"); +} + function confirmSetup() { if (!Engine.GetGUIObjectByName("pageJoin").hidden) @@ -105,16 +118,14 @@ function confirmSetup() } else if (!Engine.GetGUIObjectByName("pageHost").hidden) { - let hostPlayerName = Engine.GetGUIObjectByName("hostPlayerName").caption; let hostServerName = Engine.GetGUIObjectByName("hostServerName").caption; - let hostPort = Engine.GetGUIObjectByName("hostPort").caption; - if (!hostServerName) { Engine.GetGUIObjectByName("hostFeedback").caption = translate("Please enter a valid server name."); return; } + let hostPort = Engine.GetGUIObjectByName("hostPort").caption; if (getValidPort(hostPort) != +hostPort) { Engine.GetGUIObjectByName("hostFeedback").caption = sprintf( @@ -125,7 +136,9 @@ function confirmSetup() return; } - if (startHost(hostPlayerName, hostServerName, getValidPort(hostPort))) + let hostPlayerName = Engine.GetGUIObjectByName("hostPlayerName").caption; + let hostPassword = Engine.GetGUIObjectByName("hostPassword").caption; + if (startHost(hostPlayerName, hostServerName, getValidPort(hostPort), hostPassword)) switchSetupPage("pageConnecting"); } } @@ -146,6 +159,28 @@ function onTick() pollAndHandleNetworkClient(); } +function getConnectionFailReason(reason) +{ + switch (reason) + { + case "not_server": return translate("Server is not running."); + case "invalid_password": return translate("Password is invalid."); + default: + warn("Unknown connection failure reason: " + reason); + return sprintf(translate("\\[Invalid value %(reason)s]"), { "reason": id }); + } +} + +function reportConnectionFail(reason) +{ + messageBox( + 400, 200, + (translate("Failed to connect to the server.") + ) + "\n\n" + getConnectionFailReason(reason), + translate("Connection failed") + ); +} + function pollAndHandleNetworkClient() { while (true) @@ -155,13 +190,27 @@ function pollAndHandleNetworkClient() break; log(sprintf(translate("Net message: %(message)s"), { "message": uneval(message) })); - // If we're rejoining an active game, we don't want to actually display // the game setup screen, so perform similar processing to gamesetup.js // in this screen if (g_IsRejoining) + { switch (message.type) { + case "serverdata": + switch (message.status) + { + case "failed": + cancelSetup(); + reportConnectionFail(message.reason, false); + return; + + default: + error("Unrecognised netstatus type: " + message.status); + break; + } + break; + case "netstatus": switch (message.status) { @@ -211,11 +260,26 @@ function pollAndHandleNetworkClient() default: error("Unrecognised net message type: " + message.type); } + } else - // Not rejoining - just trying to connect to server - + // Not rejoining - just trying to connect to server. + { switch (message.type) { + case "serverdata": + switch (message.status) + { + case "failed": + cancelSetup(); + reportConnectionFail(message.reason, false); + return; + + default: + error("Unrecognised netstatus type: " + message.status); + break; + } + break; + case "netstatus": switch (message.status) { @@ -232,8 +296,7 @@ function pollAndHandleNetworkClient() } Engine.SwitchGuiPage("page_gamesetup.xml", { "serverName": g_ServerName, - "serverPort": g_ServerPort, - "stunEndpoint": g_StunEndpoint + "hasPassword": g_ServerHasPassword }); return; // don't process any more messages - leave them for the game GUI loop @@ -255,6 +318,7 @@ function pollAndHandleNetworkClient() error("Unrecognised net message type: " + message.type); break; } + } } } @@ -273,16 +337,24 @@ function switchSetupPage(newPage) pageSize.bottom = halfHeight; multiplayerPages.size = pageSize; } + else if (newPage == "pagePassword") + { + let pageSize = multiplayerPages.size; + let halfHeight = 60; + pageSize.top = -halfHeight; + pageSize.bottom = halfHeight; + multiplayerPages.size = pageSize; + } Engine.GetGUIObjectByName(newPage).hidden = false; Engine.GetGUIObjectByName("hostPlayerNameWrapper").hidden = Engine.HasXmppClient(); Engine.GetGUIObjectByName("hostServerNameWrapper").hidden = !Engine.HasXmppClient(); - Engine.GetGUIObjectByName("continueButton").hidden = newPage == "pageConnecting"; + Engine.GetGUIObjectByName("continueButton").hidden = newPage == "pageConnecting" || newPage == "pagePassword"; } -function startHost(playername, servername, port) +function startHost(playername, servername, port, password) { startConnectionStatus("server"); @@ -301,20 +373,11 @@ function startHost(playername, servername, port) return false; } - if (Engine.HasXmppClient() && Engine.GetGUIObjectByName("useSTUN").checked) - { - g_StunEndpoint = Engine.FindStunEndpoint(port); - if (!g_StunEndpoint) - { - cancelSetup(); - hostFeedback.caption = translate("Failed to host via STUN."); - return false; - } - } + let useSTUN = Engine.HasXmppClient() && Engine.GetGUIObjectByName("useSTUN").checked; try { - Engine.StartNetworkHost(playername + (g_UserRating ? " (" + g_UserRating + ")" : ""), port, playername); + Engine.StartNetworkHost(playername + (g_UserRating ? " (" + g_UserRating + ")" : ""), port, playername, useSTUN, password); } catch (e) { @@ -328,7 +391,7 @@ function startHost(playername, servername, port) } g_ServerName = servername; - g_ServerPort = port; + g_ServerHasPassword = !!password; if (Engine.HasXmppClient()) Engine.LobbySetPlayerPresence("playing"); @@ -370,9 +433,49 @@ function startJoin(playername, ip, port, useSTUN, hostJID) return true; } +function startJoinFromLobby(playername, hostJID, password) +{ + if (!Engine.HasXmppClient()) + { + cancelSetup(); + messageBox( + 400, 200, + sprintf("You cannot join a lobby game without logging in to the lobby."), + translate("Error") + ); + return false; + } + + try + { + Engine.StartNetworkJoinLobby(playername + (g_UserRating ? " (" + g_UserRating + ")" : ""), hostJID, password); + } + catch (e) + { + cancelSetup(); + messageBox( + 400, 200, + sprintf(translate("Cannot join game: %(message)s."), { "message": e.message }), + translate("Error") + ); + return false; + } + + startConnectionStatus("client"); + + Engine.LobbySetPlayerPresence("playing"); + + return true; +} + function getDefaultGameName() { return sprintf(translate("%(playername)s's game"), { "playername": multiplayerName() }); } + +function getDefaultPassword() +{ + return ""; +} diff --git a/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.xml b/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.xml index c57469c8bc..98cab62d88 100644 --- a/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.xml +++ b/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.xml @@ -106,7 +106,21 @@ - + + + + Engine.ConfigDB_CreateAndWriteValueToFile("user", "lobby.stun.enabled", String(this.checked), "config/user.cfg"); @@ -129,9 +143,26 @@