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.
This commit is contained in:
parent
a30c4a69f7
commit
1a8de6d2b8
@ -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 });
|
||||
|
@ -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
|
||||
|
@ -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 "";
|
||||
}
|
||||
|
@ -106,7 +106,21 @@
|
||||
</object>
|
||||
</object>
|
||||
|
||||
<object name="hostSTUNWrapper" size="120 106 100% 146">
|
||||
<!-- Host password is only used on games started through the lobby. -->
|
||||
<object name="hostPasswordWrapper" size="0 116 100% 126" hidden="true">
|
||||
<object type="text" size="20 0 50% 22" style="ModernRightLabelText">
|
||||
<translatableAttribute id="caption">Server Password:</translatableAttribute>
|
||||
<translatableAttribute id="tooltip">Leave blank to not require it.</translatableAttribute>
|
||||
</object>
|
||||
|
||||
<object name="hostPassword" type="input" size="50%+10 0 100%-20 22" style="ModernInput" mask="true" mask_char="*" max_length="256">
|
||||
<action on="Load">
|
||||
this.caption = getDefaultPassword();
|
||||
</action>
|
||||
</object>
|
||||
</object>
|
||||
|
||||
<object name="hostSTUNWrapper" size="120 136 100% 180">
|
||||
<object name="useSTUN" size="0 10 32 100%" type="checkbox" style="ModernTickBox">
|
||||
<action on="Press">Engine.ConfigDB_CreateAndWriteValueToFile("user", "lobby.stun.enabled", String(this.checked), "config/user.cfg");</action>
|
||||
</object>
|
||||
@ -129,9 +143,26 @@
|
||||
</object>
|
||||
|
||||
<object name="pageConnecting" hidden="true">
|
||||
<object name="connectionStatus" type="text" style="ModernLabelText" size="0 100 100% 120"/>
|
||||
<object name="connectionStatus" type="text" style="ModernLabelText" size="0 50%-10 100% 50%+10"/>
|
||||
</object>
|
||||
|
||||
<object name="pagePassword" size="0 32 100% 100%" hidden="true">
|
||||
<object name="clientPasswordWrapper" size="0 0 100% 40">
|
||||
<object type="text" size="20 0 50% 22" style="ModernRightLabelText">
|
||||
<translatableAttribute id="caption">Password:</translatableAttribute>
|
||||
</object>
|
||||
|
||||
<object name="clientPassword" type="input" size="50%+10 0 100%-20 22" style="ModernInput" mask="true" mask_char="*" max_length="256">
|
||||
<action on="Load">
|
||||
this.caption = getDefaultPassword();
|
||||
</action>
|
||||
</object>
|
||||
</object>
|
||||
<object name="confirmPasswordButton" hotkey="confirm" type="button" size="50%+5 100%-45 100%-18 100%-17" style="ModernButtonRed">
|
||||
<translatableAttribute id="caption">Confirm</translatableAttribute>
|
||||
<action on="Press">confirmPassword();</action>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
|
||||
</objects>
|
||||
|
@ -67,38 +67,12 @@ class JoinButton
|
||||
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,
|
||||
"hasPassword": !!stanza.hasPassword,
|
||||
"hostJID": stanza.hostUsername + "@" + Engine.ConfigDB_GetValue("user", "lobby.server") + "/0ad"
|
||||
});
|
||||
}
|
||||
|
@ -270,10 +270,7 @@ class Game
|
||||
*/
|
||||
Game.prototype.StanzaKeys = [
|
||||
"name",
|
||||
"ip",
|
||||
"port",
|
||||
"stunIP",
|
||||
"stunPort",
|
||||
"hasPassword",
|
||||
"hostUsername",
|
||||
"state",
|
||||
"nbp",
|
||||
|
@ -97,13 +97,13 @@ class GameList
|
||||
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 @@ class GameList
|
||||
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();
|
||||
|
@ -594,6 +594,7 @@ function setup_all_libs ()
|
||||
extern_libs = {
|
||||
"spidermonkey",
|
||||
"enet",
|
||||
"sdl",
|
||||
"boost", -- dragged in via server->simulation.h->random and NetSession.h->lockfree
|
||||
"fmt",
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2019 Wildfire Games.
|
||||
/* Copyright (C) 2021 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
@ -39,6 +39,7 @@ public:
|
||||
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;
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2018 Wildfire Games.
|
||||
/* Copyright (C) 2021 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 @@ glooxwrapper::StanzaExtension* LobbyAuth::clone() const
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2020 Wildfire Games.
|
||||
/* Copyright (C) 2021 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
@ -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 @@ XmppClient::XmppClient(const ScriptInterface* scriptInterface, const std::string
|
||||
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 @@ XmppClient::XmppClient(const ScriptInterface* scriptInterface, const std::string
|
||||
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,23 @@ void XmppClient::SendIqGetProfile(const std::string& player)
|
||||
m_client->send(iq);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the Connection data (ip, port...) from the server.
|
||||
*/
|
||||
void XmppClient::SendIqGetConnectionData(const std::string& jid, const std::string& password)
|
||||
{
|
||||
glooxwrapper::JID targetJID(jid);
|
||||
|
||||
ConnectionData* connectionData = new ConnectionData();
|
||||
connectionData->m_Password = password;
|
||||
glooxwrapper::IQ iq(gloox::IQ::Get, targetJID, m_client->getID());
|
||||
iq.addExtension(connectionData);
|
||||
m_connectionDataJid = jid;
|
||||
m_connectionDataIqId = iq.id().to_string();
|
||||
DbgXMPP("SendIqGetConnectionData [" << tag_xml(iq) << "]");
|
||||
m_client->send(iq);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send game report containing numerous game properties to the server.
|
||||
*
|
||||
@ -573,7 +596,7 @@ void XmppClient::GUIGetGameList(const ScriptInterface& scriptInterface, JS::Muta
|
||||
ScriptInterface::CreateArray(rq, ret);
|
||||
int j = 0;
|
||||
|
||||
const char* stats[] = { "name", "ip", "port", "stunIP", "stunPort", "hostUsername", "state",
|
||||
const char* stats[] = { "name", "hostUsername", "state", "hasPassword",
|
||||
"nbp", "maxnbp", "players", "mapName", "niceMapName", "mapSize", "mapType",
|
||||
"victoryConditions", "startTime", "mods" };
|
||||
|
||||
@ -811,6 +834,27 @@ bool XmppClient::handleIq(const glooxwrapper::IQ& iq)
|
||||
const GameListQuery* gq = iq.findExtension<GameListQuery>(EXTGAMELISTQUERY);
|
||||
const BoardListQuery* bq = iq.findExtension<BoardListQuery>(EXTBOARDLISTQUERY);
|
||||
const ProfileQuery* pq = iq.findExtension<ProfileQuery>(EXTPROFILEQUERY);
|
||||
const ConnectionData* cd = iq.findExtension<ConnectionData>(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->HandleGetServerDataFailed(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 +921,47 @@ bool XmppClient::handleIq(const glooxwrapper::IQ& iq)
|
||||
LOGERROR("Received lobby authentication request, but not hosting currently!");
|
||||
}
|
||||
}
|
||||
else if (iq.subtype() == gloox::IQ::Get)
|
||||
{
|
||||
const ConnectionData* cd = iq.findExtension<ConnectionData>(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
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2020 Wildfire Games.
|
||||
/* Copyright (C) 2021 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
@ -55,6 +55,10 @@ private:
|
||||
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 @@ public:
|
||||
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);
|
||||
|
@ -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"
|
||||
@ -37,6 +38,7 @@
|
||||
#include "ps/Threading.h"
|
||||
#include "scriptinterface/ScriptInterface.h"
|
||||
#include "simulation2/Simulation2.h"
|
||||
#include "network/StunClient.h"
|
||||
|
||||
CNetClient *g_NetClient = NULL;
|
||||
|
||||
@ -76,6 +78,8 @@ CNetClient::CNetClient(CGame* game, bool isLocalClient) :
|
||||
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
|
||||
@ -171,15 +175,102 @@ void CNetClient::SetHostingPlayerName(const CStr& hostingPlayerName)
|
||||
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);
|
||||
m_PollingThread = std::thread(Threading::HandleExceptions<CNetClientSession::RunNetLoop>::Wrapper, m_Session);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void CNetClient::SetupServerData(CStr address, u16 port, bool stun)
|
||||
{
|
||||
ENSURE(!m_Session);
|
||||
|
||||
m_ServerAddress = address;
|
||||
m_ServerPort = port;
|
||||
m_UseSTUN = stun;
|
||||
}
|
||||
|
||||
void CNetClient::HandleGetServerDataFailed(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<i32>(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<i32>(NDR_STUN_PORT_FAILED));
|
||||
return false;
|
||||
}
|
||||
|
||||
StunClient::StunEndpoint stunEndpoint;
|
||||
if (!StunClient::FindStunEndpointJoin(*enetClient, stunEndpoint))
|
||||
{
|
||||
PushGuiMessage(
|
||||
"type", "netstatus",
|
||||
"status", "disconnected",
|
||||
"reason", static_cast<i32>(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<i32>(NDR_UNKNOWN));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void CNetClient::SetAndOwnSession(CNetClientSession* session)
|
||||
{
|
||||
delete m_Session;
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2020 Wildfire Games.
|
||||
/* Copyright (C) 2021 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
@ -105,12 +105,25 @@ public:
|
||||
*/
|
||||
CStr GetGUID() const { return m_GUID; }
|
||||
|
||||
/**
|
||||
* Set connection data to the remote networked server.
|
||||
* @param address IP address or host name to connect to
|
||||
*/
|
||||
void SetupServerData(CStr address, u16 port, bool stun);
|
||||
|
||||
/**
|
||||
* Set up a connection to the remote networked server.
|
||||
* @param server IP address or host name to connect to
|
||||
* Must call SetupServerData first.
|
||||
* @return true on success, false on connection failure
|
||||
*/
|
||||
bool SetupConnection(const CStr& server, const u16 port, ENetHost* enetClient);
|
||||
bool SetupConnection(ENetHost* enetClient);
|
||||
|
||||
/**
|
||||
* Connect to the remote networked server using lobby.
|
||||
* Push netstatus messages on failure.
|
||||
* @return true on success, false on connection failure
|
||||
*/
|
||||
bool TryToConnect(const CStr& hostJID);
|
||||
|
||||
/**
|
||||
* Destroy the connection to the server.
|
||||
@ -232,6 +245,11 @@ public:
|
||||
* @return Whether the NetClient is shutting down.
|
||||
*/
|
||||
bool ShouldShutdown() const;
|
||||
|
||||
/**
|
||||
* Called when fetching connection data from the host failed, to inform JS code.
|
||||
*/
|
||||
void HandleGetServerDataFailed(const CStr& error);
|
||||
private:
|
||||
|
||||
void SendAuthenticateMessage();
|
||||
@ -271,6 +289,9 @@ private:
|
||||
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;
|
||||
|
@ -73,7 +73,10 @@ enum NetDisconnectReason
|
||||
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
|
||||
|
@ -1573,7 +1573,7 @@ void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port)
|
||||
|
||||
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(20595), m_Password()
|
||||
{
|
||||
}
|
||||
|
||||
@ -1582,6 +1582,11 @@ CNetServer::~CNetServer()
|
||||
delete m_Worker;
|
||||
}
|
||||
|
||||
bool CNetServer::GetUseSTUN() const
|
||||
{
|
||||
return m_UseSTUN;
|
||||
}
|
||||
|
||||
bool CNetServer::UseLobbyAuth() const
|
||||
{
|
||||
return m_LobbyAuth;
|
||||
@ -1592,6 +1597,33 @@ bool CNetServer::SetupConnection(const u16 port)
|
||||
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) const
|
||||
{
|
||||
return m_Password == password;
|
||||
}
|
||||
|
||||
void CNetServer::SetPassword(const CStr& password)
|
||||
{
|
||||
m_Password = password;
|
||||
}
|
||||
|
||||
void CNetServer::StartGame()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex);
|
||||
|
@ -147,9 +147,25 @@ public:
|
||||
|
||||
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) const;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2020 Wildfire Games.
|
||||
/* Copyright (C) 2021 Wildfire Games.
|
||||
* Copyright (C) 2013-2016 SuperTuxKart-Team.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
@ -19,6 +19,7 @@
|
||||
#include "precompiled.h"
|
||||
|
||||
#include "StunClient.h"
|
||||
#include "scriptinterface/ScriptInterface.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
@ -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,32 +371,32 @@ bool STUNRequestAndResponse(ENetHost& transactionHost)
|
||||
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<u16>(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)";
|
||||
ENetAddress addr;
|
||||
addr.host = ntohl(m_IP);
|
||||
enet_address_get_host_ip(&addr, ipStr, ARRAY_SIZE(ipStr));
|
||||
int result = 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 result == 0;
|
||||
}
|
||||
|
||||
bool StunClient::FindStunEndpointJoin(ENetHost& transactionHost, StunClient::StunEndpoint& stunEndpoint)
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2019 Wildfire Games.
|
||||
/* Copyright (C) 2021 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 <string>
|
||||
|
||||
typedef struct _ENetHost ENetHost;
|
||||
class ScriptInterface;
|
||||
class CStr8;
|
||||
|
||||
namespace StunClient
|
||||
{
|
||||
@ -35,12 +35,13 @@ struct StunEndpoint {
|
||||
|
||||
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
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2020 Wildfire Games.
|
||||
/* Copyright (C) 2021 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
@ -29,8 +29,11 @@
|
||||
#include "network/StunClient.h"
|
||||
#include "ps/CLogger.h"
|
||||
#include "ps/Game.h"
|
||||
#include "ps/Util.h"
|
||||
#include "scriptinterface/ScriptInterface.h"
|
||||
|
||||
#include "third_party/encryption/pkcs5_pbkdf2.h"
|
||||
|
||||
u16 JSI_Network::GetDefaultPort(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate))
|
||||
{
|
||||
return PS_DEFAULT_PORT;
|
||||
@ -46,19 +49,76 @@ bool JSI_Network::HasNetClient(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate
|
||||
return !!g_NetClient;
|
||||
}
|
||||
|
||||
JS::Value JSI_Network::FindStunEndpoint(ScriptInterface::CmptPrivate* pCmptPrivate, int port)
|
||||
CStr JSI_Network::HashPassword(const CStr& password)
|
||||
{
|
||||
return StunClient::FindStunEndpointHost(*(pCmptPrivate->pScriptInterface), port);
|
||||
if (password.empty())
|
||||
return password;
|
||||
|
||||
ENSURE(sodium_init() >= 0);
|
||||
const int DIGESTSIZE = crypto_hash_sha256_BYTES;
|
||||
constexpr int ITERATIONS = 1737;
|
||||
|
||||
cassert(DIGESTSIZE == 32);
|
||||
|
||||
static const unsigned char salt_base[DIGESTSIZE] = {
|
||||
244, 243, 249, 244, 32, 33, 19, 35, 16, 11, 12, 13, 14, 15, 16, 17,
|
||||
18, 19, 20, 32, 33, 244, 224, 127, 129, 130, 140, 153, 88, 123, 234, 123 };
|
||||
|
||||
// initialize the salt buffer
|
||||
unsigned char salt_buffer[DIGESTSIZE] = { 0 };
|
||||
crypto_hash_sha256_state state;
|
||||
crypto_hash_sha256_init(&state);
|
||||
crypto_hash_sha256_update(&state, salt_base, sizeof(salt_base));
|
||||
|
||||
crypto_hash_sha256_final(&state, salt_buffer);
|
||||
|
||||
// PBKDF2 to create the buffer
|
||||
unsigned char encrypted[DIGESTSIZE];
|
||||
pbkdf2(encrypted, (unsigned char*)password.c_str(), password.length(), salt_buffer, DIGESTSIZE, ITERATIONS);
|
||||
return CStr(Hexify(encrypted, DIGESTSIZE)).UpperCase();
|
||||
}
|
||||
|
||||
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);
|
||||
@ -67,12 +127,16 @@ void JSI_Network::StartNetworkHost(ScriptInterface::CmptPrivate* pCmptPrivate, c
|
||||
return;
|
||||
}
|
||||
|
||||
// We will get hashed password from clients, so hash it once for server
|
||||
g_NetServer->SetPassword(HashPassword(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");
|
||||
@ -87,48 +151,13 @@ void JSI_Network::StartNetworkJoin(ScriptInterface::CmptPrivate* pCmptPrivate, c
|
||||
ENSURE(!g_NetServer);
|
||||
ENSURE(!g_Game);
|
||||
|
||||
ENetHost* enetClient = nullptr;
|
||||
if (g_XmppClient && 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)
|
||||
{
|
||||
ScriptRequest rq(pCmptPrivate->pScriptInterface);
|
||||
ScriptException::Raise(rq, "Could not find an unused port for the enet STUN client");
|
||||
return;
|
||||
}
|
||||
|
||||
StunClient::StunEndpoint stunEndpoint;
|
||||
if (!StunClient::FindStunEndpointJoin(*enetClient, stunEndpoint))
|
||||
{
|
||||
ScriptRequest rq(pCmptPrivate->pScriptInterface);
|
||||
ScriptException::Raise(rq, "Could not find the STUN endpoint");
|
||||
return;
|
||||
}
|
||||
|
||||
g_XmppClient->SendStunEndpointToHost(stunEndpoint, hostJID);
|
||||
|
||||
SDL_Delay(1000);
|
||||
}
|
||||
|
||||
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_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(nullptr))
|
||||
{
|
||||
ScriptRequest rq(pCmptPrivate->pScriptInterface);
|
||||
ScriptException::Raise(rq, "Failed to connect to server");
|
||||
@ -137,6 +166,20 @@ void JSI_Network::StartNetworkJoin(ScriptInterface::CmptPrivate* pCmptPrivate, c
|
||||
}
|
||||
}
|
||||
|
||||
void JSI_Network::StartNetworkJoinLobby(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& playerName, const CStr& hostJID, const CStr& password)
|
||||
{
|
||||
ENSURE(!!g_XmppClient);
|
||||
ENSURE(!g_NetClient);
|
||||
ENSURE(!g_NetServer);
|
||||
ENSURE(!g_Game);
|
||||
|
||||
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, HashPassword(password).c_str());
|
||||
}
|
||||
|
||||
void JSI_Network::DisconnectNetworkGame(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate))
|
||||
{
|
||||
// TODO: we ought to do async reliable disconnections
|
||||
@ -231,9 +274,9 @@ void JSI_Network::RegisterScriptFunctions(const ScriptInterface& scriptInterface
|
||||
scriptInterface.RegisterFunction<u16, &GetDefaultPort>("GetDefaultPort");
|
||||
scriptInterface.RegisterFunction<bool, &HasNetServer>("HasNetServer");
|
||||
scriptInterface.RegisterFunction<bool, &HasNetClient>("HasNetClient");
|
||||
scriptInterface.RegisterFunction<JS::Value, int, &FindStunEndpoint>("FindStunEndpoint");
|
||||
scriptInterface.RegisterFunction<void, CStrW, u16, CStr, &StartNetworkHost>("StartNetworkHost");
|
||||
scriptInterface.RegisterFunction<void, CStrW, u16, CStr, bool, CStr, &StartNetworkHost>("StartNetworkHost");
|
||||
scriptInterface.RegisterFunction<void, CStrW, CStr, u16, bool, CStr, &StartNetworkJoin>("StartNetworkJoin");
|
||||
scriptInterface.RegisterFunction<void, CStrW, CStr, CStr, &StartNetworkJoinLobby>("StartNetworkJoinLobby");
|
||||
scriptInterface.RegisterFunction<void, &DisconnectNetworkGame>("DisconnectNetworkGame");
|
||||
scriptInterface.RegisterFunction<CStr, &GetPlayerGUID>("GetPlayerGUID");
|
||||
scriptInterface.RegisterFunction<JS::Value, &PollNetworkClient>("PollNetworkClient");
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2018 Wildfire Games.
|
||||
/* Copyright (C) 2021 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 @@ namespace JSI_Network
|
||||
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);
|
||||
@ -42,6 +47,7 @@ namespace JSI_Network
|
||||
void SendNetworkReady(ScriptInterface::CmptPrivate* pCmptPrivate, int message);
|
||||
void SetTurnLength(ScriptInterface::CmptPrivate* pCmptPrivate, int length);
|
||||
|
||||
CStr HashPassword(const CStr& password);
|
||||
void RegisterScriptFunctions(const ScriptInterface& scriptInterface);
|
||||
}
|
||||
|
||||
|
@ -74,8 +74,11 @@ public:
|
||||
void connect(CNetServer& server, const std::vector<CNetClient*>& clients)
|
||||
{
|
||||
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));
|
||||
for (CNetClient* client: clients)
|
||||
{
|
||||
client->SetupServerData("127.0.0.1", PS_DEFAULT_PORT, false);
|
||||
TS_ASSERT(client->SetupConnection(nullptr));
|
||||
}
|
||||
|
||||
for (size_t i = 0; ; ++i)
|
||||
{
|
||||
@ -304,7 +307,8 @@ public:
|
||||
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)
|
||||
{
|
||||
|
@ -1565,7 +1565,8 @@ bool Autostart(const CmdLineArgs& args)
|
||||
|
||||
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"))
|
||||
{
|
||||
@ -1578,8 +1579,8 @@ bool Autostart(const CmdLineArgs& args)
|
||||
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
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user