Index: binaries/data/mods/public/gui/common/network.js
===================================================================
--- binaries/data/mods/public/gui/common/network.js
+++ binaries/data/mods/public/gui/common/network.js
@@ -76,6 +76,9 @@
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 });
Index: binaries/data/mods/public/gui/gamesetup/NetMessages/GameRegisterStanza.js
===================================================================
--- binaries/data/mods/public/gui/gamesetup/NetMessages/GameRegisterStanza.js
+++ binaries/data/mods/public/gui/gamesetup/NetMessages/GameRegisterStanza.js
@@ -10,8 +10,7 @@
this.mapCache = mapCache;
this.serverName = initData.serverName;
- this.serverPort = initData.serverPort;
- this.stunEndpoint = initData.stunEndpoint;
+ this.password = initData.password;
this.mods = JSON.stringify(Engine.GetEngineInfo().mods);
this.timer = undefined;
@@ -87,7 +86,6 @@
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),
@@ -97,9 +95,8 @@
"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,
+ "password": this.password ? this.password : ""
};
// Only send the stanza if one of these properties changed
Index: binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js
===================================================================
--- binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js
+++ binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js
@@ -12,22 +12,18 @@
* Server title shown in the lobby gamelist.
*/
var g_ServerName = "";
-
/**
- * Cached to pass it to the gamesetup of the controller to report the game to the lobby.
+ * Identifier if server is using password.
*/
-var g_ServerPort;
+var g_ServerRequiresPassword = 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;
@@ -38,7 +34,13 @@
{
if (Engine.HasXmppClient())
{
- if (startJoin(attribs.name, attribs.ip, getValidPort(attribs.port), attribs.useSTUN, attribs.hostJID))
+ if (attribs.password)
+ {
+ g_ServerName = attribs.name;
+ g_ServerId = attribs.hostJID;
+ switchSetupPage("pagePassword");
+ }
+ else if (startJoinFromLobby(attribs.name, attribs.hostJID, ""))
switchSetupPage("pageConnecting");
}
else
@@ -48,12 +50,12 @@
case "host":
{
Engine.GetGUIObjectByName("hostSTUNWrapper").hidden = !Engine.HasXmppClient();
+ Engine.GetGUIObjectByName("hostPasswordWrapper").hidden = !Engine.HasXmppClient();
if (Engine.HasXmppClient())
{
Engine.GetGUIObjectByName("hostPlayerName").caption = attribs.name;
Engine.GetGUIObjectByName("hostServerName").caption =
sprintf(translate("%(name)s's game"), { "name": attribs.name });
-
Engine.GetGUIObjectByName("useSTUN").checked = Engine.ConfigDB_GetValue("user", "lobby.stun.enabled") == "true";
}
@@ -92,6 +94,16 @@
error("cancelSetup: Unrecognised multiplayer game type: " + g_GameType);
}
+function confirmPassword()
+{
+ if (!Engine.GetGUIObjectByName("pagePassword").hidden)
+ {
+ let password = Engine.GetGUIObjectByName("clientPassword").caption;
+ if (startJoinFromLobby(g_ServerName, g_ServerId, password))
+ switchSetupPage("pageConnecting");
+ }
+}
+
function confirmSetup()
{
if (!Engine.GetGUIObjectByName("pageJoin").hidden)
@@ -107,6 +119,7 @@
{
let hostPlayerName = Engine.GetGUIObjectByName("hostPlayerName").caption;
let hostServerName = Engine.GetGUIObjectByName("hostServerName").caption;
+ let hostPassword = Engine.GetGUIObjectByName("hostPassword").caption;
let hostPort = Engine.GetGUIObjectByName("hostPort").caption;
if (!hostServerName)
@@ -124,8 +137,7 @@
});
return;
}
-
- if (startHost(hostPlayerName, hostServerName, getValidPort(hostPort)))
+ if (startHost(hostPlayerName, hostServerName, getValidPort(hostPort), hostPassword))
switchSetupPage("pageConnecting");
}
}
@@ -146,6 +158,28 @@
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 +189,27 @@
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 +259,26 @@
default:
error("Unrecognised net message type: " + message.type);
}
+ }
else
// 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 +295,7 @@
}
Engine.SwitchGuiPage("page_gamesetup.xml", {
"serverName": g_ServerName,
- "serverPort": g_ServerPort,
- "stunEndpoint": g_StunEndpoint
+ "password": g_ServerRequiresPassword
});
return; // don't process any more messages - leave them for the game GUI loop
@@ -255,6 +317,7 @@
error("Unrecognised net message type: " + message.type);
break;
}
+ }
}
}
@@ -273,16 +336,24 @@
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 +372,11 @@
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 +390,7 @@
}
g_ServerName = servername;
- g_ServerPort = port;
+ g_ServerRequiresPassword = !!password;
if (Engine.HasXmppClient())
Engine.LobbySetPlayerPresence("playing");
@@ -370,9 +432,49 @@
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 "";
+}
Index: binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.xml
===================================================================
--- binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.xml
+++ binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.xml
@@ -77,7 +77,6 @@
-
-
+
+
+
+ Server Password:
+ Leave blank to not require it.
+
+
+
+
+ this.caption = getDefaultPassword();
+
+
+
+
+
Engine.ConfigDB_CreateAndWriteValueToFile("user", "lobby.stun.enabled", String(this.checked), "config/user.cfg");
@@ -114,6 +127,7 @@
Use STUN to work around firewalls
+
@@ -129,9 +143,27 @@
-
+
+
+
+
+ Password:
+
+
+
+
+ this.caption = getDefaultPassword();
+
+
+
+
+ Confirm
+ confirmPassword();
+
+
+
Index: binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/JoinButton.js
===================================================================
--- binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/JoinButton.js
+++ binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/JoinButton.js
@@ -67,38 +67,12 @@
if (!game)
return;
- let ip;
- let port;
let stanza = game.stanza;
- if (stanza.stunIP)
- {
- ip = stanza.stunIP;
- port = stanza.stunPort;
- }
- else
- {
- ip = stanza.ip;
- port = stanza.port;
- }
-
- if (ip.split('.').length != 4)
- {
- messageBox(
- 400, 250,
- sprintf(
- translate("This game's address '%(ip)s' does not appear to be valid."),
- { "ip": escapeText(stanza.ip) }),
- translate("Error"));
- return;
- }
-
Engine.PushGuiPage("page_gamesetup_mp.xml", {
"multiplayerGameType": "join",
- "ip": ip,
- "port": port,
"name": g_Nickname,
"rating": this.getRejoinRating(stanza),
- "useSTUN": !!stanza.stunIP,
+ "password": !!stanza.password,
"hostJID": stanza.hostUsername + "@" + Engine.ConfigDB_GetValue("user", "lobby.server") + "/0ad"
});
}
Index: binaries/data/mods/public/gui/lobby/LobbyPage/Game.js
===================================================================
--- binaries/data/mods/public/gui/lobby/LobbyPage/Game.js
+++ binaries/data/mods/public/gui/lobby/LobbyPage/Game.js
@@ -270,10 +270,7 @@
*/
Game.prototype.StanzaKeys = [
"name",
- "ip",
- "port",
- "stunIP",
- "stunPort",
+ "password",
"hostUsername",
"state",
"nbp",
Index: binaries/data/mods/public/gui/lobby/LobbyPage/GameList.js
===================================================================
--- binaries/data/mods/public/gui/lobby/LobbyPage/GameList.js
+++ binaries/data/mods/public/gui/lobby/LobbyPage/GameList.js
@@ -97,13 +97,13 @@
let newGames = {};
for (let stanza of gameListData)
{
- let game = this.games[stanza.ip] || undefined;
+ let game = this.games[stanza.hostUsername] || undefined;
let exists = !!game;
if (!exists)
game = new Game(this.mapCache);
game.update(stanza, selectedColumn);
- newGames[stanza.ip] = game;
+ newGames[stanza.hostUsername] = game;
}
this.games = newGames;
Engine.ProfileStop();
@@ -157,8 +157,7 @@
this.list_maxnbp[i] = displayData.playerCount;
this.list_gameRating[i] = game.gameRating;
this.list[i] = "";
-
- if (selectedGame && game.stanza.ip == selectedGame.stanza.ip && game.stanza.port == selectedGame.stanza.port)
+ if (selectedGame && game.hostUsername == selectedGame.hostUsername && game.stanza.gameName == selectedGame.stanza.gameName)
selectedGameIndex = i;
});
Engine.ProfileStop();
Index: build/premake/premake5.lua
===================================================================
--- build/premake/premake5.lua
+++ build/premake/premake5.lua
@@ -585,6 +585,7 @@
extern_libs = {
"spidermonkey",
"enet",
+ "sdl",
"boost", -- dragged in via server->simulation.h->random
"fmt",
}
Index: source/lobby/IXmppClient.h
===================================================================
--- source/lobby/IXmppClient.h
+++ source/lobby/IXmppClient.h
@@ -39,6 +39,7 @@
virtual void SendIqGetProfile(const std::string& player) = 0;
virtual void SendIqGameReport(const ScriptInterface& scriptInterface, JS::HandleValue data) = 0;
virtual void SendIqRegisterGame(const ScriptInterface& scriptInterface, JS::HandleValue data) = 0;
+ virtual void SendIqGetConnectionData(const std::string& jid, const std::string& password) = 0;
virtual void SendIqUnregisterGame() = 0;
virtual void SendIqChangeStateGame(const std::string& nbp, const std::string& players) = 0;
virtual void SendIqLobbyAuth(const std::string& to, const std::string& token) = 0;
Index: source/lobby/StanzaExtensions.h
===================================================================
--- source/lobby/StanzaExtensions.h
+++ source/lobby/StanzaExtensions.h
@@ -41,6 +41,30 @@
#define EXTLOBBYAUTH 1407
#define XMLNS_LOBBYAUTH "jabber:iq:lobbyauth"
+#define EXTCONNECTIONDATA 1408
+#define XMLNS_CONNECTIONDATA "jabber:iq:connectiondata"
+
+class ConnectionData : public glooxwrapper::StanzaExtension
+{
+public:
+ ConnectionData(const glooxwrapper::Tag* tag = 0);
+
+ // Following four methods are all required by gloox
+ virtual StanzaExtension* newInstance(const glooxwrapper::Tag* tag) const
+ {
+ return new ConnectionData(tag);
+ }
+ virtual const glooxwrapper::string& filterString() const;
+ virtual glooxwrapper::Tag* tag() const;
+ virtual glooxwrapper::StanzaExtension* clone() const;
+
+ glooxwrapper::string m_Ip;
+ glooxwrapper::string m_Port;
+ glooxwrapper::string m_UseSTUN;
+ glooxwrapper::string m_Password;
+ glooxwrapper::string m_Error;
+};
+
class GameReport : public glooxwrapper::StanzaExtension
{
public:
Index: source/lobby/StanzaExtensions.cpp
===================================================================
--- source/lobby/StanzaExtensions.cpp
+++ source/lobby/StanzaExtensions.cpp
@@ -1,4 +1,4 @@
-/* Copyright (C) 2018 Wildfire Games.
+/* Copyright (C) 2020 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -283,3 +283,71 @@
{
return new LobbyAuth();
}
+
+/******************************************************
+ * ConnectionData, a custom IQ Stanza, used to send and
+ * receive a ip and port of the server.
+ */
+ConnectionData::ConnectionData(const glooxwrapper::Tag* tag)
+ : StanzaExtension(EXTCONNECTIONDATA)
+{
+ if (!tag || tag->name() != "connectiondata" || tag->xmlns() != XMLNS_CONNECTIONDATA)
+ return;
+
+ const glooxwrapper::Tag* c = tag->findTag_clone("connectiondata/ip");
+ if (c)
+ m_Ip = c->cdata();
+ const glooxwrapper::Tag* p= tag->findTag_clone("connectiondata/port");
+ if (p)
+ m_Port = p->cdata();
+ const glooxwrapper::Tag* s = tag->findTag_clone("connectiondata/useSTUN");
+ if (s)
+ m_UseSTUN = s->cdata();
+ const glooxwrapper::Tag* pw = tag->findTag_clone("connectiondata/password");
+ if (pw)
+ m_Password = pw->cdata();
+ const glooxwrapper::Tag* e = tag->findTag_clone("connectiondata/error");
+ if (e)
+ m_Error= e->cdata();
+
+ glooxwrapper::Tag::free(c);
+ glooxwrapper::Tag::free(p);
+ glooxwrapper::Tag::free(s);
+ glooxwrapper::Tag::free(pw);
+ glooxwrapper::Tag::free(e);
+}
+
+/**
+ * Required by gloox, used to find the LobbyAuth element in a received IQ.
+ */
+const glooxwrapper::string& ConnectionData::filterString() const
+{
+ static const glooxwrapper::string filter = "/iq/connectiondata[@xmlns='" XMLNS_CONNECTIONDATA "']";
+ return filter;
+}
+
+/**
+ * Required by gloox, used to serialize the auth object into XML for sending.
+ */
+glooxwrapper::Tag* ConnectionData::tag() const
+{
+ glooxwrapper::Tag* t = glooxwrapper::Tag::allocate("connectiondata");
+ t->setXmlns(XMLNS_CONNECTIONDATA);
+
+ if (!m_Ip.empty())
+ t->addChild(glooxwrapper::Tag::allocate("ip", m_Ip));
+ if (!m_Port.empty())
+ t->addChild(glooxwrapper::Tag::allocate("port", m_Port));
+ if (!m_UseSTUN.empty())
+ t->addChild(glooxwrapper::Tag::allocate("useSTUN", m_UseSTUN));
+ if (!m_Password.empty())
+ t->addChild(glooxwrapper::Tag::allocate("password", m_Password));
+ if (!m_Error.empty())
+ t->addChild(glooxwrapper::Tag::allocate("error", m_Error));
+ return t;
+}
+
+glooxwrapper::StanzaExtension* ConnectionData::clone() const
+{
+ return new ConnectionData();
+}
Index: source/lobby/XmppClient.h
===================================================================
--- source/lobby/XmppClient.h
+++ source/lobby/XmppClient.h
@@ -55,6 +55,10 @@
std::string m_xpartamuppId;
std::string m_echelonId;
+ // Security
+ std::string m_connectionDataJid;
+ std::string m_connectionDataIqId;
+
// State
gloox::CertStatus m_certStatus;
bool m_initialLoadComplete;
@@ -82,6 +86,7 @@
void SendIqGetProfile(const std::string& player);
void SendIqGameReport(const ScriptInterface& scriptInterface, JS::HandleValue data);
void SendIqRegisterGame(const ScriptInterface& scriptInterface, JS::HandleValue data);
+ void SendIqGetConnectionData(const std::string& jid, const std::string& password);
void SendIqUnregisterGame();
void SendIqChangeStateGame(const std::string& nbp, const std::string& players);
void SendIqLobbyAuth(const std::string& to, const std::string& token);
Index: source/lobby/XmppClient.cpp
===================================================================
--- source/lobby/XmppClient.cpp
+++ source/lobby/XmppClient.cpp
@@ -28,6 +28,7 @@
#include "lib/external_libraries/enet.h"
#include "lib/utf8.h"
#include "network/NetServer.h"
+#include "network/NetClient.h"
#include "network/StunClient.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
@@ -94,7 +95,9 @@
m_isConnected(false),
m_sessionManager(nullptr),
m_certStatus(gloox::CertStatus::CertOk),
- m_PlayerMapUpdate(false)
+ m_PlayerMapUpdate(false),
+ m_connectionDataJid(""),
+ m_connectionDataIqId("")
{
if (m_ScriptInterface)
JS_AddExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), XmppClient::Trace, this);
@@ -148,6 +151,9 @@
m_client->registerStanzaExtension(new LobbyAuth());
m_client->registerIqHandler(this, EXTLOBBYAUTH);
+ m_client->registerStanzaExtension(new ConnectionData());
+ m_client->registerIqHandler(this, EXTCONNECTIONDATA);
+
m_client->registerMessageHandler(this);
// Uncomment to see the raw stanzas
@@ -361,6 +367,20 @@
m_client->send(iq);
}
+void XmppClient::SendIqGetConnectionData(const std::string& jid, const std::string& password)
+{
+ glooxwrapper::JID targetJID(jid);
+
+ ConnectionData* b = new ConnectionData();
+ b->m_Password = password;
+ glooxwrapper::IQ iq(gloox::IQ::Get, targetJID, m_client->getID());
+ iq.addExtension(b);
+ m_connectionDataJid = jid;
+ m_connectionDataIqId = iq.id().to_string();
+ DbgXMPP("SendIqGetConnectionData []");
+ m_client->send(iq);
+}
+
/**
* Send game report containing numerous game properties to the server.
*
@@ -573,7 +593,7 @@
ScriptInterface::CreateArray(rq, ret);
int j = 0;
- const char* stats[] = { "name", "ip", "port", "stunIP", "stunPort", "hostUsername", "state",
+ const char* stats[] = { "name", "hostUsername", "state", "password",
"nbp", "maxnbp", "players", "mapName", "niceMapName", "mapSize", "mapType",
"victoryConditions", "startTime", "mods" };
@@ -811,6 +831,28 @@
const GameListQuery* gq = iq.findExtension(EXTGAMELISTQUERY);
const BoardListQuery* bq = iq.findExtension(EXTBOARDLISTQUERY);
const ProfileQuery* pq = iq.findExtension(EXTPROFILEQUERY);
+ const ConnectionData* cd = iq.findExtension(EXTCONNECTIONDATA);
+ if (cd)
+ {
+ if (g_NetServer || !g_NetClient)
+ return true;
+
+ if (!m_connectionDataJid.empty() && m_connectionDataJid.compare(iq.from().full()) != 0)
+ return true;
+
+ if (!m_connectionDataIqId.empty() && m_connectionDataIqId.compare(iq.id().to_string()) != 0)
+ return true;
+
+ if (!cd->m_Error.empty())
+ {
+ g_NetClient->OnGetServerDataFailed(cd->m_Error.c_str());
+ return true;
+ }
+
+ g_NetClient->SetupServerData(cd->m_Ip.to_string(), stoi(cd->m_Port.to_string()), !cd->m_UseSTUN.empty());
+ g_NetClient->TryToConnect(iq.from().full());
+
+ }
if (gq)
{
for (const glooxwrapper::Tag* const& t : m_GameList)
@@ -877,6 +919,47 @@
LOGERROR("Received lobby authentication request, but not hosting currently!");
}
}
+ else if (iq.subtype() == gloox::IQ::Get)
+ {
+ const ConnectionData* cd = iq.findExtension(EXTCONNECTIONDATA);
+ if (cd)
+ {
+ LOGMESSAGE("XmppClient: Recieved request for connection data from %s", iq.from().username());
+ if (!g_NetServer)
+ {
+ glooxwrapper::IQ response(gloox::IQ::Result, iq.from(), iq.id());
+ ConnectionData* connectionData = new ConnectionData();
+ connectionData->m_Error = "not_server";
+
+ response.addExtension(connectionData);
+
+ m_client->send(response);
+ return true;
+ }
+ if (!g_NetServer->CheckPassword(CStr(cd->m_Password.c_str())))
+ {
+ glooxwrapper::IQ response(gloox::IQ::Result, iq.from(), iq.id());
+ ConnectionData* connectionData = new ConnectionData();
+ connectionData->m_Error = "invalid_password";
+
+ response.addExtension(connectionData);
+
+ m_client->send(response);
+ return true;
+ }
+
+ glooxwrapper::IQ response(gloox::IQ::Result, iq.from(), iq.id());
+ ConnectionData* connectionData = new ConnectionData();
+ connectionData->m_Ip = g_NetServer->GetPublicIp();;
+ connectionData->m_Port = std::to_string(g_NetServer->GetPublicPort());
+ connectionData->m_UseSTUN = g_NetServer->GetUseSTUN() ? "true" : "";
+
+ response.addExtension(connectionData);
+
+ m_client->send(response);
+ }
+
+ }
else if (iq.subtype() == gloox::IQ::Error)
CreateGUIMessage("system", "error", std::time(nullptr), "text", iq.error_error());
else
Index: source/network/NetClient.h
===================================================================
--- source/network/NetClient.h
+++ source/network/NetClient.h
@@ -108,7 +108,11 @@
* @param server IP address or host name to connect to
* @return true on success, false on connection failure
*/
- bool SetupConnection(const CStr& server, const u16 port, ENetHost* enetClient);
+ bool SetupConnection(ENetHost* enetClient);
+
+ void SetupServerData(CStr address, u16 port, bool stun);
+
+ bool TryToConnect(const CStr& hostJID);
/**
* Destroy the connection to the server.
@@ -232,6 +236,8 @@
*/
void SendPausedMessage(bool pause);
+ void OnGetServerDataFailed(const CStr& error);
+
private:
void SendAuthenticateMessage();
@@ -271,6 +277,9 @@
CGame *m_Game;
CStrW m_UserName;
CStr m_HostingPlayerName;
+ CStr m_ServerAddress;
+ u16 m_ServerPort;
+ bool m_UseSTUN;
/// Current network session (or NULL if not connected)
CNetClientSession* m_Session;
Index: source/network/NetClient.cpp
===================================================================
--- source/network/NetClient.cpp
+++ source/network/NetClient.cpp
@@ -25,6 +25,7 @@
#include "lib/byte_order.h"
#include "lib/external_libraries/enet.h"
+#include "lib/external_libraries/libsdl.h"
#include "lib/sysdep/sysdep.h"
#include "lobby/IXmppClient.h"
#include "ps/CConsole.h"
@@ -35,6 +36,7 @@
#include "ps/Loader.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
+#include "network/StunClient.h"
CNetClient *g_NetClient = NULL;
@@ -74,6 +76,8 @@
m_GameAttributes(game->GetSimulation2()->GetScriptInterface().GetGeneralJSContext()),
m_IsLocalClient(isLocalClient),
m_LastConnectionCheck(0),
+ m_ServerAddress(""),
+ m_ServerPort(0),
m_Rejoin(false)
{
m_Game->SetTurnManager(NULL); // delete the old local turn manager so we don't accidentally use it
@@ -165,14 +169,101 @@
m_HostingPlayerName = hostingPlayerName;
}
-bool CNetClient::SetupConnection(const CStr& server, const u16 port, ENetHost* enetClient)
+bool CNetClient::SetupConnection(ENetHost* enetClient)
{
CNetClientSession* session = new CNetClientSession(*this);
- bool ok = session->Connect(server, port, m_IsLocalClient, enetClient);
+ bool ok = session->Connect(m_ServerAddress, m_ServerPort, m_IsLocalClient, enetClient);
SetAndOwnSession(session);
return ok;
}
+void CNetClient::SetupServerData(CStr address, u16 port, bool stun)
+{
+ if (m_Session)
+ return;
+
+ m_ServerAddress = address;
+ m_ServerPort = port;
+ m_UseSTUN = stun;
+}
+
+void CNetClient::OnGetServerDataFailed(const CStr& error)
+{
+ if (m_Session)
+ return;
+
+ PushGuiMessage(
+ "type", "serverdata",
+ "status", "failed",
+ "reason", error
+ );
+}
+
+bool CNetClient::TryToConnect(const CStr& hostJID)
+{
+ if (m_Session)
+ return false;
+
+ if (m_ServerAddress.empty())
+ {
+ PushGuiMessage(
+ "type", "netstatus",
+ "status", "disconnected",
+ "reason", static_cast(NDR_SERVER_REFUSED));
+ return false;
+ }
+
+ ENetHost* enetClient = nullptr;
+ if (g_XmppClient && m_UseSTUN)
+ {
+ // Find an unused port
+ for (int i = 0; i < 5 && !enetClient; ++i)
+ {
+ // Ports below 1024 are privileged on unix
+ u16 port = 1024 + rand() % (UINT16_MAX - 1024);
+ ENetAddress hostAddr{ ENET_HOST_ANY, port };
+ enetClient = enet_host_create(&hostAddr, 1, 1, 0, 0);
+ ++hostAddr.port;
+ }
+
+ if (!enetClient)
+ {
+ PushGuiMessage(
+ "type", "netstatus",
+ "status", "disconnected",
+ "reason", static_cast(NDR_STUN_PORT_FAILED));
+ return false;
+ }
+
+ StunClient::StunEndpoint stunEndpoint;
+ if (!StunClient::FindStunEndpointJoin(*enetClient, stunEndpoint))
+ {
+ PushGuiMessage(
+ "type", "netstatus",
+ "status", "disconnected",
+ "reason", static_cast(NDR_STUN_ENDPOINT_FAILED));
+ return false;
+ }
+
+ g_XmppClient->SendStunEndpointToHost(stunEndpoint, hostJID);
+
+ SDL_Delay(1000);
+
+ StunClient::SendHolePunchingMessages(*enetClient, m_ServerAddress, m_ServerPort);
+ }
+
+ if (!g_NetClient->SetupConnection(enetClient))
+ {
+ PushGuiMessage(
+ "type", "netstatus",
+ "status", "disconnected",
+ "reason", static_cast(NDR_UNKNOWN));
+ return false;
+ }
+
+ return true;
+}
+
void CNetClient::SetAndOwnSession(CNetClientSession* session)
{
delete m_Session;
Index: source/network/NetHost.h
===================================================================
--- source/network/NetHost.h
+++ source/network/NetHost.h
@@ -73,7 +73,10 @@
NDR_LOBBY_AUTH_FAILED,
NDR_GUID_FAILED,
NDR_INCORRECT_READY_TURN_COMMANDS,
- NDR_INCORRECT_READY_TURN_SIMULATED
+ NDR_INCORRECT_READY_TURN_SIMULATED,
+ NDR_SERVER_REFUSED,
+ NDR_STUN_PORT_FAILED,
+ NDR_STUN_ENDPOINT_FAILED
};
class CNetHost
Index: source/network/NetServer.h
===================================================================
--- source/network/NetServer.h
+++ source/network/NetServer.h
@@ -147,9 +147,25 @@
void SendHolePunchingMessage(const CStr& ip, u16 port);
+ void SetConnectionData(const CStr& ip, u16 port, bool useSTUN);
+
+ bool GetUseSTUN() const;
+
+ CStr GetPublicIp() const;
+
+ u16 GetPublicPort() const;
+
+ bool CheckPassword(const CStr& password);
+
+ void SetPassword(const CStr& password);
+
private:
CNetServerWorker* m_Worker;
const bool m_LobbyAuth;
+ bool m_UseSTUN;
+ u16 m_PublicPort;
+ CStr m_PublicIp;
+ CStr m_Password;
};
/**
Index: source/network/NetServer.cpp
===================================================================
--- source/network/NetServer.cpp
+++ source/network/NetServer.cpp
@@ -1581,7 +1581,7 @@
CNetServer::CNetServer(bool useLobbyAuth, int autostartPlayers) :
m_Worker(new CNetServerWorker(useLobbyAuth, autostartPlayers)),
- m_LobbyAuth(useLobbyAuth)
+ m_LobbyAuth(useLobbyAuth), m_UseSTUN(false), m_PublicIp(""), m_PublicPort(2565) ,m_Password("")
{
}
@@ -1590,6 +1590,11 @@
delete m_Worker;
}
+bool CNetServer::GetUseSTUN() const
+{
+ return m_UseSTUN;
+}
+
bool CNetServer::UseLobbyAuth() const
{
return m_LobbyAuth;
@@ -1600,6 +1605,33 @@
return m_Worker->SetupConnection(port);
}
+u16 CNetServer::GetPublicPort() const
+{
+ return m_PublicPort;
+}
+
+CStr CNetServer::GetPublicIp() const
+{
+ return m_PublicIp;
+}
+
+void CNetServer::SetConnectionData(const CStr& ip, const u16 port, bool useSTUN)
+{
+ m_PublicIp = ip;
+ m_PublicPort = port;
+ m_UseSTUN = useSTUN;
+}
+
+bool CNetServer::CheckPassword(const CStr& password)
+{
+ return m_Password == password;
+}
+
+void CNetServer::SetPassword(const CStr& password)
+{
+ m_Password = password;
+}
+
void CNetServer::StartGame()
{
std::lock_guard lock(m_Worker->m_WorkerMutex);
Index: source/network/StunClient.h
===================================================================
--- source/network/StunClient.h
+++ source/network/StunClient.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2019 Wildfire Games.
+/* Copyright (C) 2020 Wildfire Games.
* Copyright (C) 2013-2016 SuperTuxKart-Team.
* This file is part of 0 A.D.
*
@@ -19,11 +19,11 @@
#ifndef STUNCLIENT_H
#define STUNCLIENT_H
-#include "scriptinterface/ScriptInterface.h"
-
#include
typedef struct _ENetHost ENetHost;
+class ScriptInterface;
+class CStr8;
namespace StunClient
{
@@ -35,12 +35,13 @@
void SendStunRequest(ENetHost& transactionHost, u32 targetIp, u16 targetPort);
-JS::Value FindStunEndpointHost(const ScriptInterface& scriptInterface, int port);
+bool FindStunEndpointHost(CStr8& ip, u16& port);
bool FindStunEndpointJoin(ENetHost& transactionHost, StunClient::StunEndpoint& stunEndpoint);
void SendHolePunchingMessages(ENetHost& enetClient, const std::string& serverAddress, u16 serverPort);
+bool GetPublicIp(CStr8& ip, u16 port);
}
#endif // STUNCLIENT_H
Index: source/network/StunClient.cpp
===================================================================
--- source/network/StunClient.cpp
+++ source/network/StunClient.cpp
@@ -19,6 +19,7 @@
#include "precompiled.h"
#include "StunClient.h"
+#include "scriptinterface/ScriptInterface.h"
#include
#include
@@ -46,6 +47,7 @@
#include "scriptinterface/ScriptInterface.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
+#include "ps/CStr.h"
unsigned int m_StunServerIP;
int m_StunServerPort;
@@ -369,20 +371,22 @@
ParseStunResponse(buffer);
}
-JS::Value StunClient::FindStunEndpointHost(const ScriptInterface& scriptInterface, int port)
+bool StunClient::GetPublicIp(CStr8& ip, u16 port)
+{
+ return FindStunEndpointHost(ip, port);
+}
+
+bool StunClient::FindStunEndpointHost(CStr8& ip, u16& port)
{
ENetAddress hostAddr{ENET_HOST_ANY, static_cast(port)};
ENetHost* transactionHost = enet_host_create(&hostAddr, 1, 1, 0, 0);
if (!transactionHost)
- {
- LOGERROR("FindStunEndpointHost: Failed to create enet host");
- return JS::UndefinedValue();
- }
+ return false;
bool success = STUNRequestAndResponse(*transactionHost);
enet_host_destroy(transactionHost);
if (!success)
- return JS::UndefinedValue();
+ return false;
// Convert m_IP to string
char ipStr[256] = "(error)";
@@ -390,11 +394,9 @@
addr.host = ntohl(m_IP);
enet_address_get_host_ip(&addr, ipStr, ARRAY_SIZE(ipStr));
- ScriptRequest rq(scriptInterface);
-
- JS::RootedValue stunEndpoint(rq.cx);
- ScriptInterface::CreateObject(rq, &stunEndpoint, "ip", ipStr, "port", m_Port);
- return stunEndpoint;
+ ip = ipStr;
+ port = m_Port;
+ return ip != "(error)";
}
bool StunClient::FindStunEndpointJoin(ENetHost& transactionHost, StunClient::StunEndpoint& stunEndpoint)
Index: source/network/scripting/JSInterface_Network.h
===================================================================
--- source/network/scripting/JSInterface_Network.h
+++ source/network/scripting/JSInterface_Network.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2018 Wildfire Games.
+/* Copyright (C) 2020 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -29,9 +29,14 @@
bool HasNetClient(ScriptInterface::CmptPrivate* pCmptPrivate);
void StartNetworkGame(ScriptInterface::CmptPrivate* pCmptPrivate);
void SetNetworkGameAttributes(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue attribs1);
- void StartNetworkHost(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& playerName, const u16 serverPort, const CStr& hostLobbyName);
+ void StartNetworkHost(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& playerName, const u16 serverPort, const CStr& hostLobbyName, bool useSTUN, const CStr& password);
void StartNetworkJoin(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& playerName, const CStr& serverAddress, u16 serverPort, bool useSTUN, const CStr& hostJID);
- JS::Value FindStunEndpoint(ScriptInterface::CmptPrivate* pCmptPrivate, int port);
+ /**
+ * Requires XmppClient to send iq request to the server to get server's ip and port based on passed password.
+ * This is needed to not force server to share it's public ip with all potential clients in the lobby.
+ * XmppClient will also handle logic after receiving the answer.
+ */
+ void StartNetworkJoinLobby(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& playerName, const CStr& hostJID, const CStr& password);
void DisconnectNetworkGame(ScriptInterface::CmptPrivate* pCmptPrivate);
JS::Value PollNetworkClient(ScriptInterface::CmptPrivate* pCmptPrivate);
CStr GetPlayerGUID(ScriptInterface::CmptPrivate* pCmptPrivate);
Index: source/network/scripting/JSInterface_Network.cpp
===================================================================
--- source/network/scripting/JSInterface_Network.cpp
+++ source/network/scripting/JSInterface_Network.cpp
@@ -46,19 +46,47 @@
return !!g_NetClient;
}
-JS::Value JSI_Network::FindStunEndpoint(ScriptInterface::CmptPrivate* pCmptPrivate, int port)
-{
- return StunClient::FindStunEndpointHost(*(pCmptPrivate->pScriptInterface), port);
-}
-
-void JSI_Network::StartNetworkHost(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& playerName, const u16 serverPort, const CStr& hostLobbyName)
+void JSI_Network::StartNetworkHost(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& playerName, const u16 serverPort, const CStr& hostLobbyName, bool useSTUN, const CStr& password)
{
ENSURE(!g_NetClient);
ENSURE(!g_NetServer);
ENSURE(!g_Game);
// Always use lobby authentication for lobby matches to prevent impersonation and smurfing, in particular through mods that implemented an UI for arbitrary or other players nicknames.
- g_NetServer = new CNetServer(!!g_XmppClient);
+ bool hasLobby = !!g_XmppClient;
+ g_NetServer = new CNetServer(hasLobby);
+ // In lobby, we send our public ip and port on request to the players, who want to connect.
+ // In both cases we need to ping stun server to get our public ip. If we want to host via stun,
+ // we need port as well.
+ if (hasLobby)
+ {
+ CStr ip;
+ if (!useSTUN)
+ {
+ if (!StunClient::GetPublicIp(ip, serverPort))
+ {
+ ScriptRequest rq(pCmptPrivate->pScriptInterface);
+ ScriptException::Raise(rq, "Failed to get public ip.");
+ SAFE_DELETE(g_NetServer);
+ return;
+ }
+ g_NetServer->SetConnectionData(ip, serverPort, false);
+ }
+ else
+ {
+ u16 port = serverPort;
+ // This is using port variable to store return value, do not pass serverPort itself.
+ if (!StunClient::FindStunEndpointHost(ip, port))
+ {
+ ScriptRequest rq(pCmptPrivate->pScriptInterface);
+ ScriptException::Raise(rq, "Failed to host via STUN.");
+ SAFE_DELETE(g_NetServer);
+ return;
+ }
+ g_NetServer->SetConnectionData(ip, port, true);
+ }
+ }
+
if (!g_NetServer->SetupConnection(serverPort))
{
ScriptRequest rq(pCmptPrivate->pScriptInterface);
@@ -66,13 +94,15 @@
SAFE_DELETE(g_NetServer);
return;
}
+ g_NetServer->SetPassword(password);
g_Game = new CGame(true);
g_NetClient = new CNetClient(g_Game, true);
g_NetClient->SetUserName(playerName);
g_NetClient->SetHostingPlayerName(hostLobbyName);
+ g_NetClient->SetupServerData("127.0.0.1", serverPort, false);
- if (!g_NetClient->SetupConnection("127.0.0.1", serverPort, nullptr))
+ if (!g_NetClient->SetupConnection(nullptr))
{
ScriptRequest rq(pCmptPrivate->pScriptInterface);
ScriptException::Raise(rq, "Failed to connect to server");
@@ -124,11 +154,12 @@
g_NetClient = new CNetClient(g_Game, false);
g_NetClient->SetUserName(playerName);
g_NetClient->SetHostingPlayerName(hostJID.substr(0, hostJID.find("@")));
+ g_NetClient->SetupServerData(serverAddress, serverPort, useSTUN);
if (g_XmppClient && useSTUN)
StunClient::SendHolePunchingMessages(*enetClient, serverAddress, serverPort);
- if (!g_NetClient->SetupConnection(serverAddress, serverPort, enetClient))
+ if (!g_NetClient->SetupConnection(enetClient))
{
ScriptRequest rq(pCmptPrivate->pScriptInterface);
ScriptException::Raise(rq, "Failed to connect to server");
@@ -137,6 +168,17 @@
}
}
+void JSI_Network::StartNetworkJoinLobby(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& playerName, const CStr& hostJID, const CStr& password)
+{
+ ENSURE(!!g_XmppClient);
+
+ g_Game = new CGame(true);
+ g_NetClient = new CNetClient(g_Game, false);
+ g_NetClient->SetUserName(playerName);
+ g_NetClient->SetHostingPlayerName(hostJID.substr(0, hostJID.find("@")));
+ g_XmppClient->SendIqGetConnectionData(hostJID, password.c_str());
+}
+
void JSI_Network::DisconnectNetworkGame(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate))
{
// TODO: we ought to do async reliable disconnections
@@ -231,9 +273,9 @@
scriptInterface.RegisterFunction("GetDefaultPort");
scriptInterface.RegisterFunction("HasNetServer");
scriptInterface.RegisterFunction("HasNetClient");
- scriptInterface.RegisterFunction("FindStunEndpoint");
- scriptInterface.RegisterFunction("StartNetworkHost");
+ scriptInterface.RegisterFunction("StartNetworkHost");
scriptInterface.RegisterFunction("StartNetworkJoin");
+ scriptInterface.RegisterFunction("StartNetworkJoinLobby");
scriptInterface.RegisterFunction("DisconnectNetworkGame");
scriptInterface.RegisterFunction("GetPlayerGUID");
scriptInterface.RegisterFunction("PollNetworkClient");
Index: source/network/tests/test_Net.h
===================================================================
--- source/network/tests/test_Net.h
+++ source/network/tests/test_Net.h
@@ -75,7 +75,10 @@
{
TS_ASSERT(server.SetupConnection(PS_DEFAULT_PORT));
for (size_t j = 0; j < clients.size(); ++j)
- TS_ASSERT(clients[j]->SetupConnection("127.0.0.1", PS_DEFAULT_PORT, nullptr));
+ {
+ clients[j]->SetupServerData("127.0.0.1", PS_DEFAULT_PORT, false);
+ TS_ASSERT(clients[j]->SetupConnection(nullptr));
+ }
for (size_t i = 0; ; ++i)
{
@@ -304,7 +307,8 @@
client2B.SetUserName(L"bob");
clients.push_back(&client2B);
- TS_ASSERT(client2B.SetupConnection("127.0.0.1", PS_DEFAULT_PORT, nullptr));
+ client2B.SetupServerData("127.0.0.1", PS_DEFAULT_PORT, false);
+ TS_ASSERT(client2B.SetupConnection(nullptr));
for (size_t i = 0; ; ++i)
{
Index: source/ps/GameSetup/GameSetup.cpp
===================================================================
--- source/ps/GameSetup/GameSetup.cpp
+++ source/ps/GameSetup/GameSetup.cpp
@@ -1558,7 +1558,8 @@
g_NetClient = new CNetClient(g_Game, true);
g_NetClient->SetUserName(userName);
- g_NetClient->SetupConnection("127.0.0.1", PS_DEFAULT_PORT, nullptr);
+ g_NetClient->SetupServerData("127.0.0.1", PS_DEFAULT_PORT, false);
+ g_NetClient->SetupConnection(nullptr);
}
else if (args.Has("autostart-client"))
{
@@ -1571,8 +1572,8 @@
if (ip.empty())
ip = "127.0.0.1";
- bool ok = g_NetClient->SetupConnection(ip, PS_DEFAULT_PORT, nullptr);
- ENSURE(ok);
+ g_NetClient->SetupServerData(ip, PS_DEFAULT_PORT, false);
+ ENSURE(g_NetClient->SetupConnection(nullptr));
}
else
{