Index: ps/trunk/source/lobby/IXmppClient.h
===================================================================
--- ps/trunk/source/lobby/IXmppClient.h (revision 25458)
+++ ps/trunk/source/lobby/IXmppClient.h (revision 25459)
@@ -1,69 +1,69 @@
/* 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
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef IXMPPCLIENT_H
#define IXMPPCLIENT_H
#include "scriptinterface/ScriptTypes.h"
class ScriptRequest;
class IXmppClient
{
public:
static IXmppClient* create(const ScriptInterface* scriptInterface, const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize = 0, bool regOpt = false);
virtual ~IXmppClient() {}
virtual void connect() = 0;
virtual void disconnect() = 0;
virtual bool isConnected() = 0;
virtual void recv() = 0;
virtual void SendIqGetBoardList() = 0;
virtual void SendIqGetProfile(const std::string& player) = 0;
virtual void SendIqGameReport(const ScriptRequest& rq, JS::HandleValue data) = 0;
virtual void SendIqRegisterGame(const ScriptRequest& rq, JS::HandleValue data) = 0;
- virtual void SendIqGetConnectionData(const std::string& jid, const std::string& password, bool localIP) = 0;
+ virtual void SendIqGetConnectionData(const std::string& jid, const std::string& password, const std::string& clientSalt, bool localIP) = 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;
virtual void SetNick(const std::string& nick) = 0;
virtual std::string GetNick() const = 0;
virtual std::string GetJID() const = 0;
virtual void kick(const std::string& nick, const std::string& reason) = 0;
virtual void ban(const std::string& nick, const std::string& reason) = 0;
virtual void SetPresence(const std::string& presence) = 0;
virtual const char* GetPresence(const std::string& nickname) = 0;
virtual const char* GetRole(const std::string& nickname) = 0;
virtual std::wstring GetRating(const std::string& nickname) = 0;
virtual const std::wstring& GetSubject() = 0;
virtual JS::Value GUIGetPlayerList(const ScriptRequest& rq) = 0;
virtual JS::Value GUIGetGameList(const ScriptRequest& rq) = 0;
virtual JS::Value GUIGetBoardList(const ScriptRequest& rq) = 0;
virtual JS::Value GUIGetProfile(const ScriptRequest& rq) = 0;
virtual JS::Value GuiPollNewMessages(const ScriptInterface& guiInterface) = 0;
virtual JS::Value GuiPollHistoricMessages(const ScriptInterface& guiInterface) = 0;
virtual bool GuiPollHasPlayerListUpdate() = 0;
virtual void SendMUCMessage(const std::string& message) = 0;
virtual void SendStunEndpointToHost(const std::string& ip, u16 port, const std::string& hostJID) = 0;
};
extern IXmppClient *g_XmppClient;
extern bool g_rankedGame;
#endif // XMPPCLIENT_H
Index: ps/trunk/source/lobby/StanzaExtensions.cpp
===================================================================
--- ps/trunk/source/lobby/StanzaExtensions.cpp (revision 25458)
+++ ps/trunk/source/lobby/StanzaExtensions.cpp (revision 25459)
@@ -1,359 +1,365 @@
/* 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
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "StanzaExtensions.h"
/******************************************************
* GameReport, fairly generic custom stanza extension used
* to report game statistics.
*/
GameReport::GameReport(const glooxwrapper::Tag* tag)
: StanzaExtension(EXTGAMEREPORT)
{
if (!tag || tag->name() != "report" || tag->xmlns() != XMLNS_GAMEREPORT)
return;
// TODO if we want to handle receiving this stanza extension.
};
/**
* Required by gloox, used to serialize the GameReport into XML for sending.
*/
glooxwrapper::Tag* GameReport::tag() const
{
glooxwrapper::Tag* t = glooxwrapper::Tag::allocate("report");
t->setXmlns(XMLNS_GAMEREPORT);
for (const glooxwrapper::Tag* const& tag : m_GameReport)
t->addChild(tag->clone());
return t;
}
/**
* Required by gloox, used to find the GameReport element in a recived IQ.
*/
const glooxwrapper::string& GameReport::filterString() const
{
static const glooxwrapper::string filter = "/iq/report[@xmlns='" XMLNS_GAMEREPORT "']";
return filter;
}
glooxwrapper::StanzaExtension* GameReport::clone() const
{
GameReport* q = new GameReport();
return q;
}
/******************************************************
* BoardListQuery, a flexible custom IQ Stanza useful for anything with ratings, used to
* request and receive leaderboard and rating data from server.
* Example stanza:
* 1200
*/
BoardListQuery::BoardListQuery(const glooxwrapper::Tag* tag)
: StanzaExtension(EXTBOARDLISTQUERY)
{
if (!tag || tag->name() != "query" || tag->xmlns() != XMLNS_BOARDLIST)
return;
const glooxwrapper::Tag* c = tag->findTag_clone("query/command");
if (c)
m_Command = c->cdata();
glooxwrapper::Tag::free(c);
for (const glooxwrapper::Tag* const& t : tag->findTagList_clone("query/board"))
m_StanzaBoardList.emplace_back(t);
}
/**
* Required by gloox, used to find the BoardList element in a received IQ.
*/
const glooxwrapper::string& BoardListQuery::filterString() const
{
static const glooxwrapper::string filter = "/iq/query[@xmlns='" XMLNS_BOARDLIST "']";
return filter;
}
/**
* Required by gloox, used to serialize the BoardList request into XML for sending.
*/
glooxwrapper::Tag* BoardListQuery::tag() const
{
glooxwrapper::Tag* t = glooxwrapper::Tag::allocate("query");
t->setXmlns(XMLNS_BOARDLIST);
// Check for ratinglist or boardlist command
if (!m_Command.empty())
t->addChild(glooxwrapper::Tag::allocate("command", m_Command));
for (const glooxwrapper::Tag* const& tag : m_StanzaBoardList)
t->addChild(tag->clone());
return t;
}
glooxwrapper::StanzaExtension* BoardListQuery::clone() const
{
BoardListQuery* q = new BoardListQuery();
return q;
}
BoardListQuery::~BoardListQuery()
{
for (const glooxwrapper::Tag* const& t : m_StanzaBoardList)
glooxwrapper::Tag::free(t);
m_StanzaBoardList.clear();
}
/******************************************************
* GameListQuery, custom IQ Stanza, used to receive
* the listing of games from the server, and register/
* unregister/changestate games on the server.
*/
GameListQuery::GameListQuery(const glooxwrapper::Tag* tag)
: StanzaExtension(EXTGAMELISTQUERY)
{
if (!tag || tag->name() != "query" || tag->xmlns() != XMLNS_GAMELIST)
return;
const glooxwrapper::Tag* c = tag->findTag_clone("query/command");
if (c)
m_Command = c->cdata();
glooxwrapper::Tag::free(c);
for (const glooxwrapper::Tag* const& t : tag->findTagList_clone("query/game"))
m_GameList.emplace_back(t);
}
/**
* Required by gloox, used to find the GameList element in a received IQ.
*/
const glooxwrapper::string& GameListQuery::filterString() const
{
static const glooxwrapper::string filter = "/iq/query[@xmlns='" XMLNS_GAMELIST "']";
return filter;
}
/**
* Required by gloox, used to serialize the game object into XML for sending.
*/
glooxwrapper::Tag* GameListQuery::tag() const
{
glooxwrapper::Tag* t = glooxwrapper::Tag::allocate("query");
t->setXmlns(XMLNS_GAMELIST);
// Check for register / unregister command
if (!m_Command.empty())
t->addChild(glooxwrapper::Tag::allocate("command", m_Command));
for (const glooxwrapper::Tag* const& tag : m_GameList)
t->addChild(tag->clone());
return t;
}
glooxwrapper::StanzaExtension* GameListQuery::clone() const
{
GameListQuery* q = new GameListQuery();
return q;
}
GameListQuery::~GameListQuery()
{
for (const glooxwrapper::Tag* const & t : m_GameList)
glooxwrapper::Tag::free(t);
m_GameList.clear();
}
/******************************************************
* ProfileQuery, a custom IQ Stanza useful for fetching
* user profiles
* Example stanza:
* foobar
*/
ProfileQuery::ProfileQuery(const glooxwrapper::Tag* tag)
: StanzaExtension(EXTPROFILEQUERY)
{
if (!tag || tag->name() != "query" || tag->xmlns() != XMLNS_PROFILE)
return;
const glooxwrapper::Tag* c = tag->findTag_clone("query/command");
if (c)
m_Command = c->cdata();
glooxwrapper::Tag::free(c);
for (const glooxwrapper::Tag* const& t : tag->findTagList_clone("query/profile"))
m_StanzaProfile.emplace_back(t);
}
/**
* Required by gloox, used to find the Profile element in a received IQ.
*/
const glooxwrapper::string& ProfileQuery::filterString() const
{
static const glooxwrapper::string filter = "/iq/query[@xmlns='" XMLNS_PROFILE "']";
return filter;
}
/**
* Required by gloox, used to serialize the Profile request into XML for sending.
*/
glooxwrapper::Tag* ProfileQuery::tag() const
{
glooxwrapper::Tag* t = glooxwrapper::Tag::allocate("query");
t->setXmlns(XMLNS_PROFILE);
if (!m_Command.empty())
t->addChild(glooxwrapper::Tag::allocate("command", m_Command));
for (const glooxwrapper::Tag* const& tag : m_StanzaProfile)
t->addChild(tag->clone());
return t;
}
glooxwrapper::StanzaExtension* ProfileQuery::clone() const
{
ProfileQuery* q = new ProfileQuery();
return q;
}
ProfileQuery::~ProfileQuery()
{
for (const glooxwrapper::Tag* const& t : m_StanzaProfile)
glooxwrapper::Tag::free(t);
m_StanzaProfile.clear();
}
/******************************************************
* LobbyAuth, a custom IQ Stanza, used to send and
* receive a security token for hosting authentication.
*/
LobbyAuth::LobbyAuth(const glooxwrapper::Tag* tag)
: StanzaExtension(EXTLOBBYAUTH)
{
if (!tag || tag->name() != "auth" || tag->xmlns() != XMLNS_LOBBYAUTH)
return;
const glooxwrapper::Tag* c = tag->findTag_clone("auth/token");
if (c)
m_Token = c->cdata();
glooxwrapper::Tag::free(c);
}
/**
* Required by gloox, used to find the LobbyAuth element in a received IQ.
*/
const glooxwrapper::string& LobbyAuth::filterString() const
{
static const glooxwrapper::string filter = "/iq/auth[@xmlns='" XMLNS_LOBBYAUTH "']";
return filter;
}
/**
* Required by gloox, used to serialize the auth object into XML for sending.
*/
glooxwrapper::Tag* LobbyAuth::tag() const
{
glooxwrapper::Tag* t = glooxwrapper::Tag::allocate("auth");
t->setXmlns(XMLNS_LOBBYAUTH);
// Check for the auth token
if (!m_Token.empty())
t->addChild(glooxwrapper::Tag::allocate("token", m_Token));
return t;
}
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* pip = tag->findTag_clone("connectiondata/isLocalIP");
if (pip)
m_IsLocalIP = pip->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* cs = tag->findTag_clone("connectiondata/clientsalt");
+ if (cs)
+ m_ClientSalt = cs->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(pip);
glooxwrapper::Tag::free(s);
glooxwrapper::Tag::free(pw);
+ glooxwrapper::Tag::free(cs);
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_IsLocalIP.empty())
t->addChild(glooxwrapper::Tag::allocate("isLocalIP", m_IsLocalIP));
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_ClientSalt.empty())
+ t->addChild(glooxwrapper::Tag::allocate("clientsalt", m_ClientSalt));
if (!m_Error.empty())
t->addChild(glooxwrapper::Tag::allocate("error", m_Error));
return t;
}
glooxwrapper::StanzaExtension* ConnectionData::clone() const
{
return new ConnectionData();
}
Index: ps/trunk/source/lobby/StanzaExtensions.h
===================================================================
--- ps/trunk/source/lobby/StanzaExtensions.h (revision 25458)
+++ ps/trunk/source/lobby/StanzaExtensions.h (revision 25459)
@@ -1,162 +1,163 @@
/* 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
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef STANZAEXTENSIONS_H
#define STANZAEXTENSIONS_H
#include "glooxwrapper/glooxwrapper.h"
#include
/// Global Gamelist Extension
#define EXTGAMELISTQUERY 1403
#define XMLNS_GAMELIST "jabber:iq:gamelist"
/// Global Boardlist Extension
#define EXTBOARDLISTQUERY 1404
#define XMLNS_BOARDLIST "jabber:iq:boardlist"
/// Global Gamereport Extension
#define EXTGAMEREPORT 1405
#define XMLNS_GAMEREPORT "jabber:iq:gamereport"
/// Global Profile Extension
#define EXTPROFILEQUERY 1406
#define XMLNS_PROFILE "jabber:iq:profile"
/// Global Lobby Authentication Extension
#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_IsLocalIP;
glooxwrapper::string m_UseSTUN;
glooxwrapper::string m_Password;
+ glooxwrapper::string m_ClientSalt;
glooxwrapper::string m_Error;
};
class GameReport : public glooxwrapper::StanzaExtension
{
public:
GameReport(const glooxwrapper::Tag* tag = 0);
// Following four methods are all required by gloox
virtual StanzaExtension* newInstance(const glooxwrapper::Tag* tag) const
{
return new GameReport(tag);
}
virtual const glooxwrapper::string& filterString() const;
virtual glooxwrapper::Tag* tag() const;
virtual glooxwrapper::StanzaExtension* clone() const;
std::vector m_GameReport;
};
class GameListQuery : public glooxwrapper::StanzaExtension
{
public:
GameListQuery(const glooxwrapper::Tag* tag = 0);
// Following four methods are all required by gloox
virtual StanzaExtension* newInstance(const glooxwrapper::Tag* tag) const
{
return new GameListQuery(tag);
}
virtual const glooxwrapper::string& filterString() const;
virtual glooxwrapper::Tag* tag() const;
virtual glooxwrapper::StanzaExtension* clone() const;
~GameListQuery();
glooxwrapper::string m_Command;
std::vector m_GameList;
};
class BoardListQuery : public glooxwrapper::StanzaExtension
{
public:
BoardListQuery(const glooxwrapper::Tag* tag = 0);
// Following four methods are all required by gloox
virtual StanzaExtension* newInstance(const glooxwrapper::Tag* tag) const
{
return new BoardListQuery(tag);
}
virtual const glooxwrapper::string& filterString() const;
virtual glooxwrapper::Tag* tag() const;
virtual glooxwrapper::StanzaExtension* clone() const;
~BoardListQuery();
glooxwrapper::string m_Command;
std::vector m_StanzaBoardList;
};
class ProfileQuery : public glooxwrapper::StanzaExtension
{
public:
ProfileQuery(const glooxwrapper::Tag* tag = 0);
// Following four methods are all required by gloox
virtual StanzaExtension* newInstance(const glooxwrapper::Tag* tag) const
{
return new ProfileQuery(tag);
}
virtual const glooxwrapper::string& filterString() const;
virtual glooxwrapper::Tag* tag() const;
virtual glooxwrapper::StanzaExtension* clone() const;
~ProfileQuery();
glooxwrapper::string m_Command;
std::vector m_StanzaProfile;
};
class LobbyAuth : public glooxwrapper::StanzaExtension
{
public:
LobbyAuth(const glooxwrapper::Tag* tag = 0);
// Following four methods are all required by gloox
virtual StanzaExtension* newInstance(const glooxwrapper::Tag* tag) const
{
return new LobbyAuth(tag);
}
virtual const glooxwrapper::string& filterString() const;
virtual glooxwrapper::Tag* tag() const;
virtual glooxwrapper::StanzaExtension* clone() const;
glooxwrapper::string m_Token;
};
#endif // STANZAEXTENSIONS_H
Index: ps/trunk/source/lobby/XmppClient.cpp
===================================================================
--- ps/trunk/source/lobby/XmppClient.cpp (revision 25458)
+++ ps/trunk/source/lobby/XmppClient.cpp (revision 25459)
@@ -1,1514 +1,1515 @@
/* 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
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "XmppClient.h"
#include "StanzaExtensions.h"
#include "i18n/L10n.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"
#include "ps/GUID.h"
#include "ps/Pyrogenesis.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/StructuredClone.h"
#include
//debug
#if 1
#define DbgXMPP(x)
#else
#define DbgXMPP(x) std::cout << x << std::endl;
static std::string tag_xml(const glooxwrapper::IQ& iq)
{
std::string ret;
glooxwrapper::Tag* tag = iq.tag();
ret = tag->xml().to_string();
glooxwrapper::Tag::free(tag);
return ret;
}
#endif
static std::string tag_name(const glooxwrapper::IQ& iq)
{
std::string ret;
glooxwrapper::Tag* tag = iq.tag();
ret = tag->name().to_string();
glooxwrapper::Tag::free(tag);
return ret;
}
IXmppClient* IXmppClient::create(const ScriptInterface* scriptInterface, const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize,bool regOpt)
{
return new XmppClient(scriptInterface, sUsername, sPassword, sRoom, sNick, historyRequestSize, regOpt);
}
/**
* Construct the XMPP client.
*
* @param scriptInterface - ScriptInterface to be used for storing GUI messages.
* Can be left blank for non-visual applications.
* @param sUsername Username to login with of register.
* @param sPassword Password to login with or register.
* @param sRoom MUC room to join.
* @param sNick Nick to join with.
* @param historyRequestSize Number of stanzas of room history to request.
* @param regOpt If we are just registering or not.
*/
XmppClient::XmppClient(const ScriptInterface* scriptInterface, const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize, bool regOpt)
: m_ScriptInterface(scriptInterface),
m_client(nullptr),
m_mucRoom(nullptr),
m_registration(nullptr),
m_username(sUsername),
m_password(sPassword),
m_room(sRoom),
m_nick(sNick),
m_initialLoadComplete(false),
m_isConnected(false),
m_sessionManager(nullptr),
m_certStatus(gloox::CertStatus::CertOk),
m_PlayerMapUpdate(false),
m_connectionDataJid(),
m_connectionDataIqId()
{
if (m_ScriptInterface)
JS_AddExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), XmppClient::Trace, this);
// Read lobby configuration from default.cfg
std::string sXpartamupp;
std::string sEchelon;
CFG_GET_VAL("lobby.server", m_server);
CFG_GET_VAL("lobby.xpartamupp", sXpartamupp);
CFG_GET_VAL("lobby.echelon", sEchelon);
m_xpartamuppId = sXpartamupp + "@" + m_server + "/CC";
m_echelonId = sEchelon + "@" + m_server + "/CC";
// Generate a unique, unpredictable resource to allow multiple 0 A.D. instances to connect to the lobby.
glooxwrapper::JID clientJid(sUsername + "@" + m_server + "/0ad-" + ps_generate_guid());
glooxwrapper::JID roomJid(m_room + "@conference." + m_server + "/" + sNick);
// If we are connecting, use the full jid and a password
// If we are registering, only use the server name
if (!regOpt)
m_client = new glooxwrapper::Client(clientJid, sPassword);
else
m_client = new glooxwrapper::Client(m_server);
// Optionally join without a TLS certificate, so a local server can be tested quickly.
// Security risks from malicious JS mods can be mitigated if this option and also the hostname and login are shielded from JS access.
bool tls = true;
CFG_GET_VAL("lobby.tls", tls);
m_client->setTls(tls ? gloox::TLSRequired : gloox::TLSDisabled);
// Disable use of the SASL PLAIN mechanism, to prevent leaking credentials
// if the server doesn't list any supported SASL mechanism or the response
// has been modified to exclude those.
const int mechs = gloox::SaslMechAll ^ gloox::SaslMechPlain;
m_client->setSASLMechanisms(mechs);
m_client->registerConnectionListener(this);
m_client->setPresence(gloox::Presence::Available, -1);
m_client->disco()->setVersion("Pyrogenesis", engine_version);
m_client->disco()->setIdentity("client", "bot");
m_client->setCompression(false);
m_client->registerStanzaExtension(new GameListQuery());
m_client->registerIqHandler(this, EXTGAMELISTQUERY);
m_client->registerStanzaExtension(new BoardListQuery());
m_client->registerIqHandler(this, EXTBOARDLISTQUERY);
m_client->registerStanzaExtension(new ProfileQuery());
m_client->registerIqHandler(this, EXTPROFILEQUERY);
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
//m_client->getWrapped()->logInstance().registerLogHandler( gloox::LogLevelDebug, gloox::LogAreaAll, this );
if (!regOpt)
{
// Create a Multi User Chat Room
m_mucRoom = new glooxwrapper::MUCRoom(m_client, roomJid, this, 0);
// Get room history.
m_mucRoom->setRequestHistory(historyRequestSize, gloox::MUCRoom::HistoryMaxStanzas);
}
else
{
// Registration
m_registration = new glooxwrapper::Registration(m_client);
m_registration->registerRegistrationHandler(this);
}
m_sessionManager = new glooxwrapper::SessionManager(m_client, this);
// Register plugins to allow gloox parse them in incoming sessions
m_sessionManager->registerPlugins();
}
/**
* Destroy the xmpp client
*/
XmppClient::~XmppClient()
{
DbgXMPP("XmppClient destroyed");
delete m_registration;
delete m_mucRoom;
delete m_sessionManager;
// Workaround for memory leak in gloox 1.0/1.0.1
m_client->removePresenceExtension(gloox::ExtCaps);
delete m_client;
for (const glooxwrapper::Tag* const& t : m_GameList)
glooxwrapper::Tag::free(t);
for (const glooxwrapper::Tag* const& t : m_BoardList)
glooxwrapper::Tag::free(t);
for (const glooxwrapper::Tag* const& t : m_Profile)
glooxwrapper::Tag::free(t);
if (m_ScriptInterface)
JS_RemoveExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), XmppClient::Trace, this);
}
void XmppClient::TraceMember(JSTracer* trc)
{
for (JS::Heap& guiMessage : m_GuiMessageQueue)
JS::TraceEdge(trc, &guiMessage, "m_GuiMessageQueue");
for (JS::Heap& guiMessage : m_HistoricGuiMessages)
JS::TraceEdge(trc, &guiMessage, "m_HistoricGuiMessages");
}
/// Network
void XmppClient::connect()
{
m_initialLoadComplete = false;
m_client->connect(false);
}
void XmppClient::disconnect()
{
m_client->disconnect();
}
bool XmppClient::isConnected()
{
return m_isConnected;
}
void XmppClient::recv()
{
m_client->recv(1);
}
/**
* Log (debug) Handler
*/
void XmppClient::handleLog(gloox::LogLevel level, gloox::LogArea area, const std::string& message)
{
std::cout << "log: level: " << level << ", area: " << area << ", message: " << message << std::endl;
}
/*****************************************************
* Connection handlers *
*****************************************************/
/**
* Handle connection
*/
void XmppClient::onConnect()
{
if (m_mucRoom)
{
m_isConnected = true;
CreateGUIMessage("system", "connected", std::time(nullptr));
m_mucRoom->join();
}
if (m_registration)
m_registration->fetchRegistrationFields();
}
/**
* Handle disconnection
*/
void XmppClient::onDisconnect(gloox::ConnectionError error)
{
// Make sure we properly leave the room so that
// everything works if we decide to come back later
if (m_mucRoom)
m_mucRoom->leave();
// Clear game, board and player lists.
for (const glooxwrapper::Tag* const& t : m_GameList)
glooxwrapper::Tag::free(t);
for (const glooxwrapper::Tag* const& t : m_BoardList)
glooxwrapper::Tag::free(t);
for (const glooxwrapper::Tag* const& t : m_Profile)
glooxwrapper::Tag::free(t);
m_BoardList.clear();
m_GameList.clear();
m_PlayerMap.clear();
m_PlayerMapUpdate = true;
m_Profile.clear();
m_HistoricGuiMessages.clear();
m_isConnected = false;
m_initialLoadComplete = false;
CreateGUIMessage(
"system",
"disconnected",
std::time(nullptr),
"reason", error,
"certificate_status", m_certStatus);
}
/**
* Handle TLS connection.
*/
bool XmppClient::onTLSConnect(const glooxwrapper::CertInfo& info)
{
DbgXMPP("onTLSConnect");
DbgXMPP(
"status: " << info.status <<
"\nissuer: " << info.issuer <<
"\npeer: " << info.server <<
"\nprotocol: " << info.protocol <<
"\nmac: " << info.mac <<
"\ncipher: " << info.cipher <<
"\ncompression: " << info.compression );
m_certStatus = static_cast(info.status);
// Optionally accept invalid certificates, see require_tls option.
bool verify_certificate = true;
CFG_GET_VAL("lobby.verify_certificate", verify_certificate);
return info.status == gloox::CertOk || !verify_certificate;
}
/**
* Handle MUC room errors
*/
void XmppClient::handleMUCError(glooxwrapper::MUCRoom& UNUSED(room), gloox::StanzaError err)
{
DbgXMPP("MUC Error " << ": " << StanzaErrorToString(err));
CreateGUIMessage("system", "error", std::time(nullptr), "text", err);
}
/*****************************************************
* Requests to server *
*****************************************************/
/**
* Request the leaderboard data from the server.
*/
void XmppClient::SendIqGetBoardList()
{
glooxwrapper::JID echelonJid(m_echelonId);
// Send IQ
BoardListQuery* b = new BoardListQuery();
b->m_Command = "getleaderboard";
glooxwrapper::IQ iq(gloox::IQ::Get, echelonJid, m_client->getID());
iq.addExtension(b);
DbgXMPP("SendIqGetBoardList [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/**
* Request the profile data from the server.
*/
void XmppClient::SendIqGetProfile(const std::string& player)
{
glooxwrapper::JID echelonJid(m_echelonId);
// Send IQ
ProfileQuery* b = new ProfileQuery();
b->m_Command = player;
glooxwrapper::IQ iq(gloox::IQ::Get, echelonJid, m_client->getID());
iq.addExtension(b);
DbgXMPP("SendIqGetProfile [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/**
* Request the Connection data (ip, port...) from the server.
*/
-void XmppClient::SendIqGetConnectionData(const std::string& jid, const std::string& password, bool localIP)
+void XmppClient::SendIqGetConnectionData(const std::string& jid, const std::string& password, const std::string& clientSalt, bool localIP)
{
glooxwrapper::JID targetJID(jid);
ConnectionData* connectionData = new ConnectionData();
connectionData->m_Password = password;
+ connectionData->m_ClientSalt = clientSalt;
connectionData->m_IsLocalIP = localIP ? "1" : "0";
glooxwrapper::IQ iq(gloox::IQ::Get, targetJID, m_client->getID());
iq.addExtension(connectionData);
m_connectionDataJid = iq.from().full();
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.
*
* @param data A JS array of game statistics
*/
void XmppClient::SendIqGameReport(const ScriptRequest& rq, JS::HandleValue data)
{
glooxwrapper::JID echelonJid(m_echelonId);
// Setup some base stanza attributes
GameReport* game = new GameReport();
glooxwrapper::Tag* report = glooxwrapper::Tag::allocate("game");
// Iterate through all the properties reported and add them to the stanza.
std::vector properties;
Script::EnumeratePropertyNames(rq, data, true, properties);
for (const std::string& p : properties)
{
std::wstring value;
Script::GetProperty(rq, data, p.c_str(), value);
report->addAttribute(p, utf8_from_wstring(value));
}
// Add stanza to IQ
game->m_GameReport.emplace_back(report);
// Send IQ
glooxwrapper::IQ iq(gloox::IQ::Set, echelonJid, m_client->getID());
iq.addExtension(game);
DbgXMPP("SendGameReport [" << tag_xml(iq) << "]");
m_client->send(iq);
};
/**
* Send a request to register a game to the server.
*
* @param data A JS array of game attributes
*/
void XmppClient::SendIqRegisterGame(const ScriptRequest& rq, JS::HandleValue data)
{
glooxwrapper::JID xpartamuppJid(m_xpartamuppId);
// Setup some base stanza attributes
std::unique_ptr g = std::make_unique();
g->m_Command = "register";
glooxwrapper::Tag* game = glooxwrapper::Tag::allocate("game");
// Iterate through all the properties reported and add them to the stanza.
std::vector properties;
Script::EnumeratePropertyNames(rq, data, true, properties);
for (const std::string& p : properties)
{
std::string value;
if (!Script::GetProperty(rq, data, p.c_str(), value))
{
LOGERROR("Could not parse attribute '%s' as string.", p);
return;
}
game->addAttribute(p, value);
}
// Overwrite some attributes to make it slightly less trivial to do bad things,
// and explicit some invariants.
// The JID must point to ourself.
game->addAttribute("hostJID", GetJID());
// Push the stanza onto the IQ
g->m_GameList.emplace_back(game);
// Send IQ
glooxwrapper::IQ iq(gloox::IQ::Set, xpartamuppJid, m_client->getID());
iq.addExtension(g.release());
DbgXMPP("SendIqRegisterGame [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/**
* Send a request to unregister a game to the server.
*/
void XmppClient::SendIqUnregisterGame()
{
glooxwrapper::JID xpartamuppJid(m_xpartamuppId);
// Send IQ
GameListQuery* g = new GameListQuery();
g->m_Command = "unregister";
g->m_GameList.emplace_back(glooxwrapper::Tag::allocate("game"));
glooxwrapper::IQ iq(gloox::IQ::Set, xpartamuppJid, m_client->getID());
iq.addExtension(g);
DbgXMPP("SendIqUnregisterGame [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/**
* Send a request to change the state of a registered game on the server.
*
* A game can either be in the 'running' or 'waiting' state - the server
* decides which - but we need to update the current players that are
* in-game so the server can make the calculation.
*/
void XmppClient::SendIqChangeStateGame(const std::string& nbp, const std::string& players)
{
glooxwrapper::JID xpartamuppJid(m_xpartamuppId);
// Send IQ
GameListQuery* g = new GameListQuery();
g->m_Command = "changestate";
glooxwrapper::Tag* game = glooxwrapper::Tag::allocate("game");
game->addAttribute("nbp", nbp);
game->addAttribute("players", players);
g->m_GameList.emplace_back(game);
glooxwrapper::IQ iq(gloox::IQ::Set, xpartamuppJid, m_client->getID());
iq.addExtension(g);
DbgXMPP("SendIqChangeStateGame [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/*****************************************************
* iq to clients *
*****************************************************/
/**
* Send lobby authentication token.
*/
void XmppClient::SendIqLobbyAuth(const std::string& to, const std::string& token)
{
LobbyAuth* auth = new LobbyAuth();
auth->m_Token = token;
glooxwrapper::JID clientJid(to);
glooxwrapper::IQ iq(gloox::IQ::Set, clientJid, m_client->getID());
iq.addExtension(auth);
DbgXMPP("SendIqLobbyAuth [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/*****************************************************
* Account registration *
*****************************************************/
void XmppClient::handleRegistrationFields(const glooxwrapper::JID&, int fields, glooxwrapper::string)
{
glooxwrapper::RegistrationFields vals;
vals.username = m_username;
vals.password = m_password;
m_registration->createAccount(fields, vals);
}
void XmppClient::handleRegistrationResult(const glooxwrapper::JID&, gloox::RegistrationResult result)
{
if (result == gloox::RegistrationSuccess)
CreateGUIMessage("system", "registered", std::time(nullptr));
else
CreateGUIMessage("system", "error", std::time(nullptr), "text", result);
disconnect();
}
void XmppClient::handleAlreadyRegistered(const glooxwrapper::JID&)
{
DbgXMPP("the account already exists");
}
void XmppClient::handleDataForm(const glooxwrapper::JID&, const glooxwrapper::DataForm&)
{
DbgXMPP("dataForm received");
}
void XmppClient::handleOOB(const glooxwrapper::JID&, const glooxwrapper::OOB&)
{
DbgXMPP("OOB registration requested");
}
/*****************************************************
* Requests from GUI *
*****************************************************/
/**
* Handle requests from the GUI for the list of players.
*
* @return A JS array containing all known players and their presences
*/
JS::Value XmppClient::GUIGetPlayerList(const ScriptRequest& rq)
{
JS::RootedValue ret(rq.cx);
Script::CreateArray(rq, &ret);
int j = 0;
for (const std::pair& p : m_PlayerMap)
{
JS::RootedValue player(rq.cx);
Script::CreateObject(
rq,
&player,
"name", p.first,
"presence", p.second.m_Presence,
"rating", p.second.m_Rating,
"role", p.second.m_Role);
Script::SetPropertyInt(rq, ret, j++, player);
}
return ret;
}
/**
* Handle requests from the GUI for the list of all active games.
*
* @return A JS array containing all known games
*/
JS::Value XmppClient::GUIGetGameList(const ScriptRequest& rq)
{
JS::RootedValue ret(rq.cx);
Script::CreateArray(rq, &ret);
int j = 0;
const char* stats[] = { "name", "hostUsername", "hostJID", "state", "hasPassword",
"nbp", "maxnbp", "players", "mapName", "niceMapName", "mapSize", "mapType",
"victoryConditions", "startTime", "mods" };
for(const glooxwrapper::Tag* const& t : m_GameList)
{
JS::RootedValue game(rq.cx);
Script::CreateObject(rq, &game);
for (size_t i = 0; i < ARRAY_SIZE(stats); ++i)
Script::SetProperty(rq, game, stats[i], t->findAttribute(stats[i]));
Script::SetPropertyInt(rq, ret, j++, game);
}
return ret;
}
/**
* Handle requests from the GUI for leaderboard data.
*
* @return A JS array containing all known leaderboard data
*/
JS::Value XmppClient::GUIGetBoardList(const ScriptRequest& rq)
{
JS::RootedValue ret(rq.cx);
Script::CreateArray(rq, &ret);
int j = 0;
const char* attributes[] = { "name", "rank", "rating" };
for(const glooxwrapper::Tag* const& t : m_BoardList)
{
JS::RootedValue board(rq.cx);
Script::CreateObject(rq, &board);
for (size_t i = 0; i < ARRAY_SIZE(attributes); ++i)
Script::SetProperty(rq, board, attributes[i], t->findAttribute(attributes[i]));
Script::SetPropertyInt(rq, ret, j++, board);
}
return ret;
}
/**
* Handle requests from the GUI for profile data.
*
* @return A JS array containing the specific user's profile data
*/
JS::Value XmppClient::GUIGetProfile(const ScriptRequest& rq)
{
JS::RootedValue ret(rq.cx);
Script::CreateArray(rq, &ret);
int j = 0;
const char* stats[] = { "player", "rating", "totalGamesPlayed", "highestRating", "wins", "losses", "rank" };
for (const glooxwrapper::Tag* const& t : m_Profile)
{
JS::RootedValue profile(rq.cx);
Script::CreateObject(rq, &profile);
for (size_t i = 0; i < ARRAY_SIZE(stats); ++i)
Script::SetProperty(rq, profile, stats[i], t->findAttribute(stats[i]));
Script::SetPropertyInt(rq, ret, j++, profile);
}
return ret;
}
/*****************************************************
* Message interfaces *
*****************************************************/
void SetGUIMessageProperty(const ScriptRequest& UNUSED(rq), JS::HandleObject UNUSED(messageObj))
{
}
template
void SetGUIMessageProperty(const ScriptRequest& rq, JS::HandleObject messageObj, const std::string& propertyName, const T& propertyValue, Args const&... args)
{
JS::RootedValue scriptPropertyValue(rq.cx);
Script::ToJSVal(rq, &scriptPropertyValue, propertyValue);
JS_DefineProperty(rq.cx, messageObj, propertyName.c_str(), scriptPropertyValue, JSPROP_ENUMERATE);
SetGUIMessageProperty(rq, messageObj, args...);
}
template
void XmppClient::CreateGUIMessage(
const std::string& type,
const std::string& level,
const std::time_t time,
Args const&... args)
{
if (!m_ScriptInterface)
return;
ScriptRequest rq(m_ScriptInterface);
JS::RootedValue message(rq.cx);
Script::CreateObject(
rq,
&message,
"type", type,
"level", level,
"historic", false,
"time", static_cast(time));
JS::RootedObject messageObj(rq.cx, message.toObjectOrNull());
SetGUIMessageProperty(rq, messageObj, args...);
Script::FreezeObject(rq, message, true);
m_GuiMessageQueue.push_back(JS::Heap(message));
}
bool XmppClient::GuiPollHasPlayerListUpdate()
{
// The initial playerlist will be received in multiple messages
// Only inform the GUI after all of these playerlist fragments were received.
if (!m_initialLoadComplete)
return false;
bool hasUpdate = m_PlayerMapUpdate;
m_PlayerMapUpdate = false;
return hasUpdate;
}
JS::Value XmppClient::GuiPollNewMessages(const ScriptInterface& guiInterface)
{
if ((m_isConnected && !m_initialLoadComplete) || m_GuiMessageQueue.empty())
return JS::UndefinedValue();
ScriptRequest rq(m_ScriptInterface);
// Optimize for batch message processing that is more
// performance demanding than processing a lone message.
JS::RootedValue messages(rq.cx);
Script::CreateArray(rq, &messages);
int j = 0;
for (const JS::Heap& message : m_GuiMessageQueue)
{
Script::SetPropertyInt(rq, messages, j++, message);
// Store historic chat messages.
// Only store relevant messages to minimize memory footprint.
JS::RootedValue rootedMessage(rq.cx, message);
std::string type;
Script::GetProperty(rq, rootedMessage, "type", type);
if (type != "chat")
continue;
std::string level;
Script::GetProperty(rq, rootedMessage, "level", level);
if (level != "room-message" && level != "private-message")
continue;
JS::RootedValue historicMessage(rq.cx, Script::DeepCopy(rq, rootedMessage));
if (true)
{
Script::SetProperty(rq, historicMessage, "historic", true);
Script::FreezeObject(rq, historicMessage, true);
m_HistoricGuiMessages.push_back(JS::Heap(historicMessage));
}
else
LOGERROR("Could not clone historic lobby GUI message!");
}
m_GuiMessageQueue.clear();
// Copy the messages over to the caller script interface.
return Script::CloneValueFromOtherCompartment(guiInterface, *m_ScriptInterface, messages);
}
JS::Value XmppClient::GuiPollHistoricMessages(const ScriptInterface& guiInterface)
{
if (m_HistoricGuiMessages.empty())
return JS::UndefinedValue();
ScriptRequest rq(m_ScriptInterface);
JS::RootedValue messages(rq.cx);
Script::CreateArray(rq, &messages);
int j = 0;
for (const JS::Heap& message : m_HistoricGuiMessages)
Script::SetPropertyInt(rq, messages, j++, message);
// Copy the messages over to the caller script interface.
return Script::CloneValueFromOtherCompartment(guiInterface, *m_ScriptInterface, messages);
}
/**
* Send a standard MUC textual message.
*/
void XmppClient::SendMUCMessage(const std::string& message)
{
m_mucRoom->send(message);
}
/**
* Handle a room message.
*/
void XmppClient::handleMUCMessage(glooxwrapper::MUCRoom& UNUSED(room), const glooxwrapper::Message& msg, bool priv)
{
DbgXMPP(msg.from().resource() << " said " << msg.body());
CreateGUIMessage(
"chat",
priv ? "private-message" : "room-message",
ComputeTimestamp(msg),
"from", msg.from().resource(),
"text", msg.body());
}
/**
* Handle a private message.
*/
void XmppClient::handleMessage(const glooxwrapper::Message& msg, glooxwrapper::MessageSession*)
{
DbgXMPP("type " << msg.subtype() << ", subject " << msg.subject()
<< ", message " << msg.body() << ", thread id " << msg.thread());
CreateGUIMessage(
"chat",
"private-message",
ComputeTimestamp(msg),
"from", msg.from().resource(),
"text", msg.body());
}
/**
* Handle portions of messages containing custom stanza extensions.
*/
bool XmppClient::handleIq(const glooxwrapper::IQ& iq)
{
DbgXMPP("handleIq [" << tag_xml(iq) << "]");
if (iq.subtype() == gloox::IQ::Result)
{
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) {
LOGMESSAGE("XmppClient: Received connection data from invalid host: %s", iq.from().username());
return true;
}
if (!m_connectionDataIqId.empty() && m_connectionDataIqId.compare(iq.id().to_string()) != 0) {
LOGMESSAGE("XmppClient: Received connection data with invalid id");
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)
{
if (iq.from().full() == m_xpartamuppId && gq->m_Command == "register" && g_NetServer && !g_NetServer->GetUseSTUN())
{
if (gq->m_GameList.empty())
{
LOGWARNING("XmppClient: Received empty game list in response to Game Register");
return true;
}
std::string publicIP = gq->m_GameList.front()->findAttribute("ip").to_string();
if (publicIP.empty())
{
LOGWARNING("XmppClient: Received game with no IP in response to Game Register");
return true;
}
g_NetServer->SetConnectionData(publicIP, g_NetServer->GetPublicPort());
return true;
}
for (const glooxwrapper::Tag* const& t : m_GameList)
glooxwrapper::Tag::free(t);
m_GameList.clear();
for (const glooxwrapper::Tag* const& t : gq->m_GameList)
m_GameList.emplace_back(t->clone());
CreateGUIMessage("game", "gamelist", std::time(nullptr));
}
if (bq)
{
if (bq->m_Command == "boardlist")
{
for (const glooxwrapper::Tag* const& t : m_BoardList)
glooxwrapper::Tag::free(t);
m_BoardList.clear();
for (const glooxwrapper::Tag* const& t : bq->m_StanzaBoardList)
m_BoardList.emplace_back(t->clone());
CreateGUIMessage("game", "leaderboard", std::time(nullptr));
}
else if (bq->m_Command == "ratinglist")
{
for (const glooxwrapper::Tag* const& t : bq->m_StanzaBoardList)
{
const PlayerMap::iterator it = m_PlayerMap.find(t->findAttribute("name"));
if (it != m_PlayerMap.end())
{
it->second.m_Rating = t->findAttribute("rating");
m_PlayerMapUpdate = true;
}
}
CreateGUIMessage("game", "ratinglist", std::time(nullptr));
}
}
if (pq)
{
for (const glooxwrapper::Tag* const& t : m_Profile)
glooxwrapper::Tag::free(t);
m_Profile.clear();
for (const glooxwrapper::Tag* const& t : pq->m_StanzaProfile)
m_Profile.emplace_back(t->clone());
CreateGUIMessage("game", "profile", std::time(nullptr));
}
}
else if (iq.subtype() == gloox::IQ::Set)
{
const LobbyAuth* lobbyAuth = iq.findExtension(EXTLOBBYAUTH);
if (lobbyAuth)
{
LOGMESSAGE("XmppClient: Received lobby auth: %s from %s", lobbyAuth->m_Token.to_string(), iq.from().username());
glooxwrapper::IQ response(gloox::IQ::Result, iq.from(), iq.id());
m_client->send(response);
if (g_NetServer)
g_NetServer->OnLobbyAuth(iq.from().username(), lobbyAuth->m_Token.to_string());
else
LOGMESSAGE("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: Received 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->IsBanned(iq.from().username()))
{
glooxwrapper::IQ response(gloox::IQ::Result, iq.from(), iq.id());
ConnectionData* connectionData = new ConnectionData();
connectionData->m_Error = "banned";
response.addExtension(connectionData);
m_client->send(response);
return true;
}
- if (!g_NetServer->CheckPasswordAndIncrement(CStr(cd->m_Password.to_string()), iq.from().username()))
+ if (!g_NetServer->CheckPasswordAndIncrement(iq.from().username(), cd->m_Password.to_string(), cd->m_ClientSalt.to_string()))
{
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();
if (cd->m_IsLocalIP.to_string() == "0")
{
connectionData->m_Ip = g_NetServer->GetPublicIp();
connectionData->m_Port = std::to_string(g_NetServer->GetPublicPort());
connectionData->m_UseSTUN = g_NetServer->GetUseSTUN() ? "true" : "";
}
else
{
CStr ip;
if (StunClient::FindLocalIP(ip))
{
connectionData->m_Ip = ip;
connectionData->m_Port = std::to_string(g_NetServer->GetLocalPort());
connectionData->m_UseSTUN = "";
}
else
connectionData->m_Error = "local_ip_failed";
}
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
{
CreateGUIMessage("system", "error", std::time(nullptr), "text", wstring_from_utf8(g_L10n.Translate("unknown subtype (see logs)")));
LOGMESSAGE("unknown subtype '%s'", tag_name(iq).c_str());
}
return true;
}
/**
* Update local data when a user changes presence.
*/
void XmppClient::handleMUCParticipantPresence(glooxwrapper::MUCRoom& UNUSED(room), const glooxwrapper::MUCRoomParticipant participant, const glooxwrapper::Presence& presence)
{
const glooxwrapper::string& nick = participant.nick->resource();
if (presence.presence() == gloox::Presence::Unavailable)
{
if (!participant.newNick.empty() && (participant.flags & (gloox::UserNickChanged | gloox::UserSelf)))
{
// we have a nick change
if (m_PlayerMap.find(participant.newNick) == m_PlayerMap.end())
m_PlayerMap.emplace(
std::piecewise_construct,
std::forward_as_tuple(participant.newNick),
std::forward_as_tuple(presence.presence(), participant.role, std::move(m_PlayerMap.at(nick).m_Rating)));
else
LOGERROR("Nickname changed to an existing nick!");
DbgXMPP(nick << " is now known as " << participant.newNick);
CreateGUIMessage(
"chat",
"nick",
std::time(nullptr),
"oldnick", nick,
"newnick", participant.newNick);
}
else if (participant.flags & gloox::UserKicked)
{
DbgXMPP(nick << " was kicked. Reason: " << participant.reason);
CreateGUIMessage(
"chat",
"kicked",
std::time(nullptr),
"nick", nick,
"reason", participant.reason);
}
else if (participant.flags & gloox::UserBanned)
{
DbgXMPP(nick << " was banned. Reason: " << participant.reason);
CreateGUIMessage(
"chat",
"banned",
std::time(nullptr),
"nick", nick,
"reason", participant.reason);
}
else
{
DbgXMPP(nick << " left the room (flags " << participant.flags << ")");
CreateGUIMessage(
"chat",
"leave",
std::time(nullptr),
"nick", nick);
}
m_PlayerMap.erase(nick);
}
else
{
const PlayerMap::iterator it = m_PlayerMap.find(nick);
/* During the initialization process, we receive join messages for everyone
* currently in the room. We don't want to display these, so we filter them
* out. We will always be the last to join during initialization.
*/
if (!m_initialLoadComplete)
{
if (m_mucRoom->nick() == nick)
m_initialLoadComplete = true;
}
else if (it == m_PlayerMap.end())
{
CreateGUIMessage(
"chat",
"join",
std::time(nullptr),
"nick", nick);
}
else if (it->second.m_Role != participant.role)
{
CreateGUIMessage(
"chat",
"role",
std::time(nullptr),
"nick", nick,
"oldrole", it->second.m_Role,
"newrole", participant.role);
}
else
{
// Don't create a GUI message for regular presence changes, because
// several hundreds of them accumulate during a match, impacting performance terribly and
// the only way they are used is to determine whether to update the playerlist.
}
DbgXMPP(
nick << " is in the room, "
"presence: " << GetPresenceString(presence.presence()) << ", "
"role: "<< GetRoleString(participant.role));
if (it == m_PlayerMap.end())
{
m_PlayerMap.emplace(
std::piecewise_construct,
std::forward_as_tuple(nick),
std::forward_as_tuple(presence.presence(), participant.role, std::string()));
}
else
{
it->second.m_Presence = presence.presence();
it->second.m_Role = participant.role;
}
}
m_PlayerMapUpdate = true;
}
/**
* Update local cache when subject changes.
*/
void XmppClient::handleMUCSubject(glooxwrapper::MUCRoom& UNUSED(room), const glooxwrapper::string& nick, const glooxwrapper::string& subject)
{
m_Subject = wstring_from_utf8(subject.to_string());
CreateGUIMessage(
"chat",
"subject",
std::time(nullptr),
"nick", nick,
"subject", m_Subject);
}
/**
* Get current subject.
*/
const std::wstring& XmppClient::GetSubject()
{
return m_Subject;
}
/**
* Request nick change, real change via mucRoomHandler.
*
* @param nick Desired nickname
*/
void XmppClient::SetNick(const std::string& nick)
{
m_mucRoom->setNick(nick);
}
/**
* Get current nickname.
*/
std::string XmppClient::GetNick() const
{
return m_mucRoom->nick().to_string();
}
std::string XmppClient::GetJID() const
{
return m_client->getJID().to_string();
}
/**
* Kick a player from the current room.
*
* @param nick Nickname to be kicked
* @param reason Reason the player was kicked
*/
void XmppClient::kick(const std::string& nick, const std::string& reason)
{
m_mucRoom->kick(nick, reason);
}
/**
* Ban a player from the current room.
*
* @param nick Nickname to be banned
* @param reason Reason the player was banned
*/
void XmppClient::ban(const std::string& nick, const std::string& reason)
{
m_mucRoom->ban(nick, reason);
}
/**
* Change the xmpp presence of the client.
*
* @param presence A string containing the desired presence
*/
void XmppClient::SetPresence(const std::string& presence)
{
#define IF(x,y) if (presence == x) m_mucRoom->setPresence(gloox::Presence::y)
IF("available", Available);
else IF("chat", Chat);
else IF("away", Away);
else IF("playing", DND);
else IF("offline", Unavailable);
// The others are not to be set
#undef IF
else LOGERROR("Unknown presence '%s'", presence.c_str());
}
/**
* Get the current xmpp presence of the given nick.
*/
const char* XmppClient::GetPresence(const std::string& nick)
{
const PlayerMap::iterator it = m_PlayerMap.find(nick);
if (it == m_PlayerMap.end())
return "offline";
return GetPresenceString(it->second.m_Presence);
}
/**
* Get the current xmpp role of the given nick.
*/
const char* XmppClient::GetRole(const std::string& nick)
{
const PlayerMap::iterator it = m_PlayerMap.find(nick);
if (it == m_PlayerMap.end())
return "";
return GetRoleString(it->second.m_Role);
}
/**
* Get the most recent received rating of the given nick.
* Notice that this doesn't request a rating profile if it hasn't been received yet.
*/
std::wstring XmppClient::GetRating(const std::string& nick)
{
const PlayerMap::iterator it = m_PlayerMap.find(nick);
if (it == m_PlayerMap.end())
return std::wstring();
return wstring_from_utf8(it->second.m_Rating.to_string());
}
/*****************************************************
* Utilities *
*****************************************************/
/**
* Parse and return the timestamp of a historic chat message and return the current time for new chat messages.
* Historic chat messages are implement as DelayedDelivers as specified in XEP-0203.
* Hence, their timestamp MUST be in UTC and conform to the DateTime format XEP-0082.
*
* @returns Seconds since the epoch.
*/
std::time_t XmppClient::ComputeTimestamp(const glooxwrapper::Message& msg)
{
// Only historic messages contain a timestamp!
if (!msg.when())
return std::time(nullptr);
// The locale is irrelevant, because the XMPP date format doesn't contain written month names
for (const std::string& format : std::vector{ "Y-M-d'T'H:m:sZ", "Y-M-d'T'H:m:s.SZ" })
{
UDate dateTime = g_L10n.ParseDateTime(msg.when()->stamp().to_string(), format, icu::Locale::getUS());
if (dateTime)
return dateTime / 1000.0;
}
return std::time(nullptr);
}
/**
* Convert a gloox presence type to an untranslated string literal to be used as an identifier by the scripts.
*/
const char* XmppClient::GetPresenceString(const gloox::Presence::PresenceType presenceType)
{
switch (presenceType)
{
#define CASE(X,Y) case gloox::Presence::X: return Y
CASE(Available, "available");
CASE(Chat, "chat");
CASE(Away, "away");
CASE(DND, "playing");
CASE(XA, "away");
CASE(Unavailable, "offline");
CASE(Probe, "probe");
CASE(Error, "error");
CASE(Invalid, "invalid");
default:
LOGERROR("Unknown presence type '%d'", static_cast(presenceType));
return "";
#undef CASE
}
}
/**
* Convert a gloox role type to an untranslated string literal to be used as an identifier by the scripts.
*/
const char* XmppClient::GetRoleString(const gloox::MUCRoomRole role)
{
switch (role)
{
#define CASE(X, Y) case gloox::X: return Y
CASE(RoleNone, "none");
CASE(RoleVisitor, "visitor");
CASE(RoleParticipant, "participant");
CASE(RoleModerator, "moderator");
CASE(RoleInvalid, "invalid");
default:
LOGERROR("Unknown role type '%d'", static_cast(role));
return "";
#undef CASE
}
}
/**
* Translates a gloox certificate error codes, i.e. gloox certificate statuses except CertOk.
* Keep in sync with specifications.
*/
std::string XmppClient::CertificateErrorToString(gloox::CertStatus status)
{
std::map certificateErrorStrings = {
{ gloox::CertInvalid, g_L10n.Translate("The certificate is not trusted.") },
{ gloox::CertSignerUnknown, g_L10n.Translate("The certificate hasn't got a known issuer.") },
{ gloox::CertRevoked, g_L10n.Translate("The certificate has been revoked.") },
{ gloox::CertExpired, g_L10n.Translate("The certificate has expired.") },
{ gloox::CertNotActive, g_L10n.Translate("The certificate is not yet active.") },
{ gloox::CertWrongPeer, g_L10n.Translate("The certificate has not been issued for the peer connected to.") },
{ gloox::CertSignerNotCa, g_L10n.Translate("The certificate signer is not a certificate authority.") }
};
std::string result;
for (std::map::iterator it = certificateErrorStrings.begin(); it != certificateErrorStrings.end(); ++it)
if (status & it->first)
result += "\n" + it->second;
return result;
}
/**
* Convert a gloox stanza error type to string.
* Keep in sync with Gloox documentation
*
* @param err Error to be converted
* @return Converted error string
*/
std::string XmppClient::StanzaErrorToString(gloox::StanzaError err)
{
#define CASE(X, Y) case gloox::X: return Y
#define DEBUG_CASE(X, Y) case gloox::X: return g_L10n.Translate("Error") + " (" + Y + ")"
switch (err)
{
CASE(StanzaErrorUndefined, g_L10n.Translate("No error"));
DEBUG_CASE(StanzaErrorBadRequest, "Server received malformed XML");
CASE(StanzaErrorConflict, g_L10n.Translate("Player already logged in"));
DEBUG_CASE(StanzaErrorFeatureNotImplemented, "Server does not implement requested feature");
CASE(StanzaErrorForbidden, g_L10n.Translate("Forbidden"));
DEBUG_CASE(StanzaErrorGone, "Unable to find message receipiant");
CASE(StanzaErrorInternalServerError, g_L10n.Translate("Internal server error"));
DEBUG_CASE(StanzaErrorItemNotFound, "Message receipiant does not exist");
DEBUG_CASE(StanzaErrorJidMalformed, "JID (XMPP address) malformed");
DEBUG_CASE(StanzaErrorNotAcceptable, "Receipiant refused message. Possible policy issue");
CASE(StanzaErrorNotAllowed, g_L10n.Translate("Not allowed"));
CASE(StanzaErrorNotAuthorized, g_L10n.Translate("Not authorized"));
DEBUG_CASE(StanzaErrorNotModified, "Requested item has not changed since last request");
DEBUG_CASE(StanzaErrorPaymentRequired, "This server requires payment");
CASE(StanzaErrorRecipientUnavailable, g_L10n.Translate("Recipient temporarily unavailable"));
DEBUG_CASE(StanzaErrorRedirect, "Request redirected");
CASE(StanzaErrorRegistrationRequired, g_L10n.Translate("Registration required"));
DEBUG_CASE(StanzaErrorRemoteServerNotFound, "Remote server not found");
DEBUG_CASE(StanzaErrorRemoteServerTimeout, "Remote server timed out");
DEBUG_CASE(StanzaErrorResourceConstraint, "The recipient is unable to process the message due to resource constraints");
CASE(StanzaErrorServiceUnavailable, g_L10n.Translate("Service unavailable"));
DEBUG_CASE(StanzaErrorSubscribtionRequired, "Service requires subscription");
DEBUG_CASE(StanzaErrorUnexpectedRequest, "Attempt to send from invalid stanza address");
DEBUG_CASE(StanzaErrorUnknownSender, "Invalid 'from' address");
default:
return g_L10n.Translate("Unknown error");
}
#undef DEBUG_CASE
#undef CASE
}
/**
* Convert a gloox connection error enum to string
* Keep in sync with Gloox documentation
*
* @param err Error to be converted
* @return Converted error string
*/
std::string XmppClient::ConnectionErrorToString(gloox::ConnectionError err)
{
#define CASE(X, Y) case gloox::X: return Y
#define DEBUG_CASE(X, Y) case gloox::X: return g_L10n.Translate("Error") + " (" + Y + ")"
switch (err)
{
CASE(ConnNoError, g_L10n.Translate("No error"));
CASE(ConnStreamError, g_L10n.Translate("Stream error"));
CASE(ConnStreamVersionError, g_L10n.Translate("The incoming stream version is unsupported"));
CASE(ConnStreamClosed, g_L10n.Translate("The stream has been closed by the server"));
DEBUG_CASE(ConnProxyAuthRequired, "The HTTP/SOCKS5 proxy requires authentication");
DEBUG_CASE(ConnProxyAuthFailed, "HTTP/SOCKS5 proxy authentication failed");
DEBUG_CASE(ConnProxyNoSupportedAuth, "The HTTP/SOCKS5 proxy requires an unsupported authentication mechanism");
CASE(ConnIoError, g_L10n.Translate("An I/O error occurred"));
DEBUG_CASE(ConnParseError, "An XML parse error occurred");
CASE(ConnConnectionRefused, g_L10n.Translate("The connection was refused by the server"));
CASE(ConnDnsError, g_L10n.Translate("Resolving the server's hostname failed"));
CASE(ConnOutOfMemory, g_L10n.Translate("This system is out of memory"));
DEBUG_CASE(ConnNoSupportedAuth, "The authentication mechanisms the server offered are not supported or no authentication mechanisms were available");
CASE(ConnTlsFailed, g_L10n.Translate("The server's certificate could not be verified or the TLS handshake did not complete successfully"));
CASE(ConnTlsNotAvailable, g_L10n.Translate("The server did not offer required TLS encryption"));
DEBUG_CASE(ConnCompressionFailed, "Negotiation/initializing compression failed");
CASE(ConnAuthenticationFailed, g_L10n.Translate("Authentication failed. Incorrect password or account does not exist"));
CASE(ConnUserDisconnected, g_L10n.Translate("The user or system requested a disconnect"));
CASE(ConnNotConnected, g_L10n.Translate("There is no active connection"));
default:
return g_L10n.Translate("Unknown error");
}
#undef DEBUG_CASE
#undef CASE
}
/**
* Convert a gloox registration result enum to string
* Keep in sync with Gloox documentation
*
* @param err Enum to be converted
* @return Converted string
*/
std::string XmppClient::RegistrationResultToString(gloox::RegistrationResult res)
{
#define CASE(X, Y) case gloox::X: return Y
#define DEBUG_CASE(X, Y) case gloox::X: return g_L10n.Translate("Error") + " (" + Y + ")"
switch (res)
{
CASE(RegistrationSuccess, g_L10n.Translate("Your account has been successfully registered"));
CASE(RegistrationNotAcceptable, g_L10n.Translate("Not all necessary information provided"));
CASE(RegistrationConflict, g_L10n.Translate("Username already exists"));
DEBUG_CASE(RegistrationNotAuthorized, "Account removal timeout or insufficiently secure channel for password change");
DEBUG_CASE(RegistrationBadRequest, "Server received an incomplete request");
DEBUG_CASE(RegistrationForbidden, "Registration forbidden");
DEBUG_CASE(RegistrationRequired, "Account cannot be removed as it does not exist");
DEBUG_CASE(RegistrationUnexpectedRequest, "This client is unregistered with the server");
DEBUG_CASE(RegistrationNotAllowed, "Server does not permit password changes");
default:
return "";
}
#undef DEBUG_CASE
#undef CASE
}
void XmppClient::SendStunEndpointToHost(const std::string& ip, u16 port, const std::string& hostJIDStr)
{
DbgXMPP("SendStunEndpointToHost " << hostJIDStr);
glooxwrapper::JID hostJID(hostJIDStr);
glooxwrapper::Jingle::Session session = m_sessionManager->createSession(hostJID);
session.sessionInitiate(ip.c_str(), port);
}
void XmppClient::handleSessionAction(gloox::Jingle::Action action, glooxwrapper::Jingle::Session& session, const glooxwrapper::Jingle::Session::Jingle& jingle)
{
if (action == gloox::Jingle::SessionInitiate)
handleSessionInitiation(session, jingle);
}
void XmppClient::handleSessionInitiation(glooxwrapper::Jingle::Session& UNUSED(session), const glooxwrapper::Jingle::Session::Jingle& jingle)
{
glooxwrapper::Jingle::ICEUDP::Candidate candidate = jingle.getCandidate();
if (candidate.ip.empty())
{
LOGERROR("Failed to retrieve Jingle candidate");
return;
}
if (!g_NetServer)
{
LOGERROR("Received STUN connection request, but not hosting currently!");
return;
}
g_NetServer->SendHolePunchingMessage(candidate.ip.to_string(), candidate.port);
}
Index: ps/trunk/source/lobby/XmppClient.h
===================================================================
--- ps/trunk/source/lobby/XmppClient.h (revision 25458)
+++ ps/trunk/source/lobby/XmppClient.h (revision 25459)
@@ -1,207 +1,207 @@
/* 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
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef XXXMPPCLIENT_H
#define XXXMPPCLIENT_H
#include "IXmppClient.h"
#include "glooxwrapper/glooxwrapper.h"
#include
#include
#include
#include
class ScriptRequest;
namespace glooxwrapper
{
class Client;
struct CertInfo;
}
class XmppClient : public IXmppClient, public glooxwrapper::ConnectionListener, public glooxwrapper::MUCRoomHandler, public glooxwrapper::IqHandler, public glooxwrapper::RegistrationHandler, public glooxwrapper::MessageHandler, public glooxwrapper::Jingle::SessionHandler
{
NONCOPYABLE(XmppClient);
private:
// Components
glooxwrapper::Client* m_client;
glooxwrapper::MUCRoom* m_mucRoom;
glooxwrapper::Registration* m_registration;
glooxwrapper::SessionManager* m_sessionManager;
// Account infos
std::string m_username;
std::string m_password;
std::string m_server;
std::string m_room;
std::string m_nick;
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;
bool m_isConnected;
public:
// Basic
XmppClient(const ScriptInterface* scriptInterface, const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize = 0, const bool regOpt = false);
virtual ~XmppClient();
// JS::Heap is better for GC performance than JS::PersistentRooted
static void Trace(JSTracer *trc, void *data)
{
static_cast(data)->TraceMember(trc);
}
void TraceMember(JSTracer *trc);
// Network
void connect();
void disconnect();
bool isConnected();
void recv();
void SendIqGetBoardList();
void SendIqGetProfile(const std::string& player);
void SendIqGameReport(const ScriptRequest& rq, JS::HandleValue data);
void SendIqRegisterGame(const ScriptRequest& rq, JS::HandleValue data);
- void SendIqGetConnectionData(const std::string& jid, const std::string& password, bool localIP);
+ void SendIqGetConnectionData(const std::string& jid, const std::string& password, const std::string& clientSalt, bool localIP);
void SendIqUnregisterGame();
void SendIqChangeStateGame(const std::string& nbp, const std::string& players);
void SendIqLobbyAuth(const std::string& to, const std::string& token);
void SetNick(const std::string& nick);
std::string GetNick() const;
std::string GetJID() const;
void kick(const std::string& nick, const std::string& reason);
void ban(const std::string& nick, const std::string& reason);
void SetPresence(const std::string& presence);
const char* GetPresence(const std::string& nickname);
const char* GetRole(const std::string& nickname);
std::wstring GetRating(const std::string& nickname);
const std::wstring& GetSubject();
JS::Value GUIGetPlayerList(const ScriptRequest& rq);
JS::Value GUIGetGameList(const ScriptRequest& rq);
JS::Value GUIGetBoardList(const ScriptRequest& rq);
JS::Value GUIGetProfile(const ScriptRequest& rq);
void SendStunEndpointToHost(const std::string& ip, u16 port, const std::string& hostJID);
/**
* Convert gloox values to string or time.
*/
static const char* GetPresenceString(const gloox::Presence::PresenceType presenceType);
static const char* GetRoleString(const gloox::MUCRoomRole role);
static std::string StanzaErrorToString(gloox::StanzaError err);
static std::string RegistrationResultToString(gloox::RegistrationResult res);
static std::string ConnectionErrorToString(gloox::ConnectionError err);
static std::string CertificateErrorToString(gloox::CertStatus status);
static std::time_t ComputeTimestamp(const glooxwrapper::Message& msg);
protected:
/* Xmpp handlers */
/* MUC handlers */
virtual void handleMUCParticipantPresence(glooxwrapper::MUCRoom& room, const glooxwrapper::MUCRoomParticipant, const glooxwrapper::Presence&);
virtual void handleMUCError(glooxwrapper::MUCRoom& room, gloox::StanzaError);
virtual void handleMUCMessage(glooxwrapper::MUCRoom& room, const glooxwrapper::Message& msg, bool priv);
virtual void handleMUCSubject(glooxwrapper::MUCRoom& room, const glooxwrapper::string& nick, const glooxwrapper::string& subject);
/* MUC handlers not supported by glooxwrapper */
// virtual bool handleMUCRoomCreation(glooxwrapper::MUCRoom*) {return false;}
// virtual void handleMUCInviteDecline(glooxwrapper::MUCRoom*, const glooxwrapper::JID&, const std::string&) {}
// virtual void handleMUCInfo(glooxwrapper::MUCRoom*, int, const std::string&, const glooxwrapper::DataForm*) {}
// virtual void handleMUCItems(glooxwrapper::MUCRoom*, const std::list >&) {}
/* Log handler */
virtual void handleLog(gloox::LogLevel level, gloox::LogArea area, const std::string& message);
/* ConnectionListener handlers*/
virtual void onConnect();
virtual void onDisconnect(gloox::ConnectionError e);
virtual bool onTLSConnect(const glooxwrapper::CertInfo& info);
/* Iq Handlers */
virtual bool handleIq(const glooxwrapper::IQ& iq);
virtual void handleIqID(const glooxwrapper::IQ&, int) {}
/* Registration Handlers */
virtual void handleRegistrationFields(const glooxwrapper::JID& /*from*/, int fields, glooxwrapper::string instructions );
virtual void handleRegistrationResult(const glooxwrapper::JID& /*from*/, gloox::RegistrationResult result);
virtual void handleAlreadyRegistered(const glooxwrapper::JID& /*from*/);
virtual void handleDataForm(const glooxwrapper::JID& /*from*/, const glooxwrapper::DataForm& /*form*/);
virtual void handleOOB(const glooxwrapper::JID& /*from*/, const glooxwrapper::OOB& oob);
/* Message Handler */
virtual void handleMessage(const glooxwrapper::Message& msg, glooxwrapper::MessageSession* session);
/* Session Handler */
virtual void handleSessionAction(gloox::Jingle::Action action, glooxwrapper::Jingle::Session& session, const glooxwrapper::Jingle::Session::Jingle& jingle);
virtual void handleSessionInitiation(glooxwrapper::Jingle::Session& session, const glooxwrapper::Jingle::Session::Jingle& jingle);
public:
JS::Value GuiPollNewMessages(const ScriptInterface& guiInterface);
JS::Value GuiPollHistoricMessages(const ScriptInterface& guiInterface);
bool GuiPollHasPlayerListUpdate();
void SendMUCMessage(const std::string& message);
protected:
template
void CreateGUIMessage(
const std::string& type,
const std::string& level,
const std::time_t time,
Args const&... args);
private:
struct SPlayer {
SPlayer(const gloox::Presence::PresenceType presence, const gloox::MUCRoomRole role, const glooxwrapper::string& rating)
: m_Presence(presence), m_Role(role), m_Rating(rating)
{
}
gloox::Presence::PresenceType m_Presence;
gloox::MUCRoomRole m_Role;
glooxwrapper::string m_Rating;
};
using PlayerMap = std::map;
/// Map of players
PlayerMap m_PlayerMap;
/// Whether or not the playermap has changed since the last time the GUI checked.
bool m_PlayerMapUpdate;
/// List of games
std::vector m_GameList;
/// List of rankings
std::vector m_BoardList;
/// Profile data
std::vector m_Profile;
/// ScriptInterface to root the values
const ScriptInterface* m_ScriptInterface;
/// Queue of messages for the GUI
std::deque > m_GuiMessageQueue;
/// Cache of all GUI messages received since the login
std::vector > m_HistoricGuiMessages;
/// Current room subject/topic.
std::wstring m_Subject;
};
#endif // XMPPCLIENT_H
Index: ps/trunk/source/network/NetClient.cpp
===================================================================
--- ps/trunk/source/network/NetClient.cpp (revision 25458)
+++ ps/trunk/source/network/NetClient.cpp (revision 25459)
@@ -1,975 +1,983 @@
/* 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
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "NetClient.h"
#include "NetClientTurnManager.h"
#include "NetMessage.h"
#include "NetSession.h"
#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"
#include "ps/CLogger.h"
#include "ps/Compress.h"
#include "ps/CStr.h"
#include "ps/Game.h"
+#include "ps/Hashing.h"
#include "ps/Loader.h"
#include "ps/Profile.h"
#include "ps/Threading.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/JSON.h"
#include "simulation2/Simulation2.h"
#include "network/StunClient.h"
/**
* Once ping goes above turn length * command delay,
* the game will start 'freezing' for other clients while we catch up.
* Since commands are sent client -> server -> client, divide by 2.
* (duplicated in NetServer.cpp to avoid having to fetch the constants in a header file)
*/
constexpr u32 NETWORK_BAD_PING = DEFAULT_TURN_LENGTH * COMMAND_DELAY_MP / 2;
CNetClient *g_NetClient = NULL;
/**
* Async task for receiving the initial game state when rejoining an
* in-progress network game.
*/
class CNetFileReceiveTask_ClientRejoin : public CNetFileReceiveTask
{
NONCOPYABLE(CNetFileReceiveTask_ClientRejoin);
public:
CNetFileReceiveTask_ClientRejoin(CNetClient& client, const CStr& initAttribs)
: m_Client(client), m_InitAttributes(initAttribs)
{
}
virtual void OnComplete()
{
// We've received the game state from the server
// Save it so we can use it after the map has finished loading
m_Client.m_JoinSyncBuffer = m_Buffer;
// Pretend the server told us to start the game
CGameStartMessage start;
start.m_InitAttributes = m_InitAttributes;
m_Client.HandleMessage(&start);
}
private:
CNetClient& m_Client;
CStr m_InitAttributes;
};
CNetClient::CNetClient(CGame* game) :
m_Session(NULL),
m_UserName(L"anonymous"),
m_HostID((u32)-1), m_ClientTurnManager(NULL), m_Game(game),
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
void* context = this;
JS_AddExtraGCRootsTracer(GetScriptInterface().GetGeneralJSContext(), CNetClient::Trace, this);
// Set up transitions for session
AddTransition(NCS_UNCONNECTED, (uint)NMT_CONNECT_COMPLETE, NCS_CONNECT, (void*)&OnConnect, context);
AddTransition(NCS_CONNECT, (uint)NMT_SERVER_HANDSHAKE, NCS_HANDSHAKE, (void*)&OnHandshake, context);
AddTransition(NCS_HANDSHAKE, (uint)NMT_SERVER_HANDSHAKE_RESPONSE, NCS_AUTHENTICATE, (void*)&OnHandshakeResponse, context);
AddTransition(NCS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NCS_AUTHENTICATE, (void*)&OnAuthenticateRequest, context);
AddTransition(NCS_AUTHENTICATE, (uint)NMT_AUTHENTICATE_RESULT, NCS_PREGAME, (void*)&OnAuthenticate, context);
AddTransition(NCS_PREGAME, (uint)NMT_CHAT, NCS_PREGAME, (void*)&OnChat, context);
AddTransition(NCS_PREGAME, (uint)NMT_READY, NCS_PREGAME, (void*)&OnReady, context);
AddTransition(NCS_PREGAME, (uint)NMT_GAME_SETUP, NCS_PREGAME, (void*)&OnGameSetup, context);
AddTransition(NCS_PREGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_PREGAME, (void*)&OnPlayerAssignment, context);
AddTransition(NCS_PREGAME, (uint)NMT_KICKED, NCS_PREGAME, (void*)&OnKicked, context);
AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_PREGAME, (void*)&OnClientTimeout, context);
AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_PREGAME, (void*)&OnClientPerformance, context);
AddTransition(NCS_PREGAME, (uint)NMT_GAME_START, NCS_LOADING, (void*)&OnGameStart, context);
AddTransition(NCS_PREGAME, (uint)NMT_JOIN_SYNC_START, NCS_JOIN_SYNCING, (void*)&OnJoinSyncStart, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CHAT, NCS_JOIN_SYNCING, (void*)&OnChat, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_SETUP, NCS_JOIN_SYNCING, (void*)&OnGameSetup, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_JOIN_SYNCING, (void*)&OnPlayerAssignment, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_KICKED, NCS_JOIN_SYNCING, (void*)&OnKicked, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_TIMEOUT, NCS_JOIN_SYNCING, (void*)&OnClientTimeout, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_PERFORMANCE, NCS_JOIN_SYNCING, (void*)&OnClientPerformance, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_START, NCS_JOIN_SYNCING, (void*)&OnGameStart, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_SIMULATION_COMMAND, NCS_JOIN_SYNCING, (void*)&OnInGame, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_END_COMMAND_BATCH, NCS_JOIN_SYNCING, (void*)&OnJoinSyncEndCommandBatch, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NCS_INGAME, (void*)&OnLoadedGame, context);
AddTransition(NCS_LOADING, (uint)NMT_CHAT, NCS_LOADING, (void*)&OnChat, context);
AddTransition(NCS_LOADING, (uint)NMT_GAME_SETUP, NCS_LOADING, (void*)&OnGameSetup, context);
AddTransition(NCS_LOADING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_LOADING, (void*)&OnPlayerAssignment, context);
AddTransition(NCS_LOADING, (uint)NMT_KICKED, NCS_LOADING, (void*)&OnKicked, context);
AddTransition(NCS_LOADING, (uint)NMT_CLIENT_TIMEOUT, NCS_LOADING, (void*)&OnClientTimeout, context);
AddTransition(NCS_LOADING, (uint)NMT_CLIENT_PERFORMANCE, NCS_LOADING, (void*)&OnClientPerformance, context);
AddTransition(NCS_LOADING, (uint)NMT_CLIENTS_LOADING, NCS_LOADING, (void*)&OnClientsLoading, context);
AddTransition(NCS_LOADING, (uint)NMT_LOADED_GAME, NCS_INGAME, (void*)&OnLoadedGame, context);
AddTransition(NCS_INGAME, (uint)NMT_REJOINED, NCS_INGAME, (void*)&OnRejoined, context);
AddTransition(NCS_INGAME, (uint)NMT_KICKED, NCS_INGAME, (void*)&OnKicked, context);
AddTransition(NCS_INGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_INGAME, (void*)&OnClientTimeout, context);
AddTransition(NCS_INGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_INGAME, (void*)&OnClientPerformance, context);
AddTransition(NCS_INGAME, (uint)NMT_CLIENTS_LOADING, NCS_INGAME, (void*)&OnClientsLoading, context);
AddTransition(NCS_INGAME, (uint)NMT_CLIENT_PAUSED, NCS_INGAME, (void*)&OnClientPaused, context);
AddTransition(NCS_INGAME, (uint)NMT_CHAT, NCS_INGAME, (void*)&OnChat, context);
AddTransition(NCS_INGAME, (uint)NMT_GAME_SETUP, NCS_INGAME, (void*)&OnGameSetup, context);
AddTransition(NCS_INGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_INGAME, (void*)&OnPlayerAssignment, context);
AddTransition(NCS_INGAME, (uint)NMT_SIMULATION_COMMAND, NCS_INGAME, (void*)&OnInGame, context);
AddTransition(NCS_INGAME, (uint)NMT_SYNC_ERROR, NCS_INGAME, (void*)&OnInGame, context);
AddTransition(NCS_INGAME, (uint)NMT_END_COMMAND_BATCH, NCS_INGAME, (void*)&OnInGame, context);
// Set first state
SetFirstState(NCS_UNCONNECTED);
}
CNetClient::~CNetClient()
{
// Try to flush messages before dying (probably fails).
if (m_ClientTurnManager)
m_ClientTurnManager->OnDestroyConnection();
DestroyConnection();
JS_RemoveExtraGCRootsTracer(GetScriptInterface().GetGeneralJSContext(), CNetClient::Trace, this);
}
void CNetClient::TraceMember(JSTracer *trc)
{
for (JS::Heap& guiMessage : m_GuiMessageQueue)
JS::TraceEdge(trc, &guiMessage, "m_GuiMessageQueue");
}
void CNetClient::SetUserName(const CStrW& username)
{
ENSURE(!m_Session); // must be called before we start the connection
m_UserName = username;
}
void CNetClient::SetHostJID(const CStr& jid)
{
m_HostJID = jid;
}
void CNetClient::SetGamePassword(const CStr& hashedPassword)
{
- m_Password = hashedPassword;
+ // Hash on top with the user's name, to make sure not all
+ // hashing data is in control of the host.
+ m_Password = HashCryptographically(hashedPassword, m_UserName.ToUTF8());
}
void CNetClient::SetControllerSecret(const std::string& secret)
{
m_ControllerSecret = secret;
}
bool CNetClient::SetupConnection(ENetHost* enetClient)
{
CNetClientSession* session = new CNetClientSession(*this);
bool ok = session->Connect(m_ServerAddress, m_ServerPort, enetClient);
SetAndOwnSession(session);
m_PollingThread = std::thread(Threading::HandleExceptions::Wrapper, m_Session);
return ok;
}
+void CNetClient::SetupConnectionViaLobby()
+{
+ g_XmppClient->SendIqGetConnectionData(m_HostJID, m_Password, m_UserName.ToUTF8(), false);
+}
+
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(NDR_SERVER_REFUSED));
return false;
}
ENetHost* enetClient = nullptr;
if (g_XmppClient && m_UseSTUN)
{
ENetAddress hostAddr{ ENET_HOST_ANY, ENET_PORT_ANY };
enetClient = enet_host_create(&hostAddr, 1, 1, 0, 0);
if (!enetClient)
{
PushGuiMessage(
"type", "netstatus",
"status", "disconnected",
"reason", static_cast(NDR_STUN_PORT_FAILED));
return false;
}
CStr ip;
u16 port;
if (!StunClient::FindPublicIP(*enetClient, ip, port))
{
PushGuiMessage(
"type", "netstatus",
"status", "disconnected",
"reason", static_cast(NDR_STUN_ENDPOINT_FAILED));
return false;
}
// If the host is on the same network, we risk failing to connect
// on routers that don't support NAT hairpinning/NAT loopback.
// To work around that, send again a connection data request, but for internal IP this time.
if (ip == m_ServerAddress)
{
- g_XmppClient->SendIqGetConnectionData(m_HostJID, m_Password, true);
+ g_XmppClient->SendIqGetConnectionData(m_HostJID, m_Password, m_UserName.ToUTF8(), true);
// Return true anyways - we're on a success path here.
return true;
}
g_XmppClient->SendStunEndpointToHost(ip, port, 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;
m_Session = session;
}
void CNetClient::DestroyConnection()
{
if (m_Session)
m_Session->Shutdown();
if (m_PollingThread.joinable())
// Use detach() over join() because we don't want to wait for the session
// (which may be polling or trying to send messages).
m_PollingThread.detach();
// The polling thread will cleanup the session on its own,
// mark it as nullptr here so we know we're done using it.
m_Session = nullptr;
}
void CNetClient::Poll()
{
if (!m_Session)
return;
PROFILE3("NetClient::poll");
CheckServerConnection();
m_Session->ProcessPolledMessages();
}
void CNetClient::CheckServerConnection()
{
// Trigger local warnings if the connection to the server is bad.
// At most once per second.
std::time_t now = std::time(nullptr);
if (now <= m_LastConnectionCheck)
return;
m_LastConnectionCheck = now;
// Report if we are losing the connection to the server
u32 lastReceived = m_Session->GetLastReceivedTime();
if (lastReceived > NETWORK_WARNING_TIMEOUT)
{
PushGuiMessage(
"type", "netwarn",
"warntype", "server-timeout",
"lastReceivedTime", lastReceived);
return;
}
// Report if we have a bad ping to the server.
u32 meanRTT = m_Session->GetMeanRTT();
if (meanRTT > NETWORK_BAD_PING)
{
PushGuiMessage(
"type", "netwarn",
"warntype", "server-latency",
"meanRTT", meanRTT);
}
}
void CNetClient::GuiPoll(JS::MutableHandleValue ret)
{
if (m_GuiMessageQueue.empty())
{
ret.setUndefined();
return;
}
ret.set(m_GuiMessageQueue.front());
m_GuiMessageQueue.pop_front();
}
std::string CNetClient::TestReadGuiMessages()
{
ScriptRequest rq(GetScriptInterface());
std::string r;
JS::RootedValue msg(rq.cx);
while (true)
{
GuiPoll(&msg);
if (msg.isUndefined())
break;
r += Script::ToString(rq, &msg) + "\n";
}
return r;
}
const ScriptInterface& CNetClient::GetScriptInterface()
{
return m_Game->GetSimulation2()->GetScriptInterface();
}
void CNetClient::PostPlayerAssignmentsToScript()
{
ScriptRequest rq(GetScriptInterface());
JS::RootedValue newAssignments(rq.cx);
Script::CreateObject(rq, &newAssignments);
for (const std::pair& p : m_PlayerAssignments)
{
JS::RootedValue assignment(rq.cx);
Script::CreateObject(
rq,
&assignment,
"name", p.second.m_Name,
"player", p.second.m_PlayerID,
"status", p.second.m_Status);
Script::SetProperty(rq, newAssignments, p.first.c_str(), assignment);
}
PushGuiMessage(
"type", "players",
"newAssignments", newAssignments);
}
bool CNetClient::SendMessage(const CNetMessage* message)
{
if (!m_Session)
return false;
return m_Session->SendMessage(message);
}
void CNetClient::HandleConnect()
{
Update((uint)NMT_CONNECT_COMPLETE, NULL);
}
void CNetClient::HandleDisconnect(u32 reason)
{
PushGuiMessage(
"type", "netstatus",
"status", "disconnected",
"reason", reason);
DestroyConnection();
// Update the state immediately to UNCONNECTED (don't bother with FSM transitions since
// we'd need one for every single state, and we don't need to use per-state actions)
SetCurrState(NCS_UNCONNECTED);
}
void CNetClient::SendGameSetupMessage(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface)
{
CGameSetupMessage gameSetup(scriptInterface);
gameSetup.m_Data = attrs;
SendMessage(&gameSetup);
}
void CNetClient::SendAssignPlayerMessage(const int playerID, const CStr& guid)
{
CAssignPlayerMessage assignPlayer;
assignPlayer.m_PlayerID = playerID;
assignPlayer.m_GUID = guid;
SendMessage(&assignPlayer);
}
void CNetClient::SendChatMessage(const std::wstring& text)
{
CChatMessage chat;
chat.m_Message = text;
SendMessage(&chat);
}
void CNetClient::SendReadyMessage(const int status)
{
CReadyMessage readyStatus;
readyStatus.m_Status = status;
SendMessage(&readyStatus);
}
void CNetClient::SendClearAllReadyMessage()
{
CClearAllReadyMessage clearAllReady;
SendMessage(&clearAllReady);
}
void CNetClient::SendStartGameMessage(const CStr& initAttribs)
{
CGameStartMessage gameStart;
gameStart.m_InitAttributes = initAttribs;
SendMessage(&gameStart);
}
void CNetClient::SendRejoinedMessage()
{
CRejoinedMessage rejoinedMessage;
SendMessage(&rejoinedMessage);
}
void CNetClient::SendKickPlayerMessage(const CStrW& playerName, bool ban)
{
CKickedMessage kickPlayer;
kickPlayer.m_Name = playerName;
kickPlayer.m_Ban = ban;
SendMessage(&kickPlayer);
}
void CNetClient::SendPausedMessage(bool pause)
{
CClientPausedMessage pausedMessage;
pausedMessage.m_Pause = pause;
SendMessage(&pausedMessage);
}
bool CNetClient::HandleMessage(CNetMessage* message)
{
// Handle non-FSM messages first
Status status = m_Session->GetFileTransferer().HandleMessageReceive(*message);
if (status == INFO::OK)
return true;
if (status != INFO::SKIPPED)
return false;
if (message->GetType() == NMT_FILE_TRANSFER_REQUEST)
{
CFileTransferRequestMessage* reqMessage = static_cast(message);
// TODO: we should support different transfer request types, instead of assuming
// it's always requesting the simulation state
std::stringstream stream;
LOGMESSAGERENDER("Serializing game at turn %u for rejoining player", m_ClientTurnManager->GetCurrentTurn());
u32 turn = to_le32(m_ClientTurnManager->GetCurrentTurn());
stream.write((char*)&turn, sizeof(turn));
bool ok = m_Game->GetSimulation2()->SerializeState(stream);
ENSURE(ok);
// Compress the content with zlib to save bandwidth
// (TODO: if this is still too large, compressing with e.g. LZMA works much better)
std::string compressed;
CompressZLib(stream.str(), compressed, true);
m_Session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, compressed);
return true;
}
// Update FSM
bool ok = Update(message->GetType(), message);
if (!ok)
LOGERROR("Net client: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)GetCurrState());
return ok;
}
void CNetClient::LoadFinished()
{
if (!m_JoinSyncBuffer.empty())
{
// We're rejoining a game, and just finished loading the initial map,
// so deserialize the saved game state now
std::string state;
DecompressZLib(m_JoinSyncBuffer, state, true);
std::stringstream stream(state);
u32 turn;
stream.read((char*)&turn, sizeof(turn));
turn = to_le32(turn);
LOGMESSAGE("Rejoining client deserializing state at turn %u\n", turn);
bool ok = m_Game->GetSimulation2()->DeserializeState(stream);
ENSURE(ok);
m_ClientTurnManager->ResetState(turn, turn);
PushGuiMessage(
"type", "netstatus",
"status", "join_syncing");
}
else
{
// Connecting at the start of a game, so we'll wait for other players to finish loading
PushGuiMessage(
"type", "netstatus",
"status", "waiting_for_players");
}
CLoadedGameMessage loaded;
loaded.m_CurrentTurn = m_ClientTurnManager->GetCurrentTurn();
SendMessage(&loaded);
}
void CNetClient::SendAuthenticateMessage()
{
CAuthenticateMessage authenticate;
authenticate.m_Name = m_UserName;
authenticate.m_Password = m_Password;
authenticate.m_ControllerSecret = m_ControllerSecret;
SendMessage(&authenticate);
}
bool CNetClient::OnConnect(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CONNECT_COMPLETE);
CNetClient* client = static_cast(context);
client->PushGuiMessage(
"type", "netstatus",
"status", "connected");
return true;
}
bool CNetClient::OnHandshake(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_SERVER_HANDSHAKE);
CNetClient* client = static_cast(context);
CCliHandshakeMessage handshake;
handshake.m_MagicResponse = PS_PROTOCOL_MAGIC_RESPONSE;
handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION;
handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION;
client->SendMessage(&handshake);
return true;
}
bool CNetClient::OnHandshakeResponse(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_SERVER_HANDSHAKE_RESPONSE);
CNetClient* client = static_cast(context);
CSrvHandshakeResponseMessage* message = static_cast(event->GetParamRef());
client->m_GUID = message->m_GUID;
if (message->m_Flags & PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH)
{
if (g_XmppClient && !client->m_HostJID.empty())
g_XmppClient->SendIqLobbyAuth(client->m_HostJID, client->m_GUID);
else
{
client->PushGuiMessage(
"type", "netstatus",
"status", "disconnected",
"reason", static_cast(NDR_LOBBY_AUTH_FAILED));
LOGMESSAGE("Net client: Couldn't send lobby auth xmpp message");
}
return true;
}
client->SendAuthenticateMessage();
return true;
}
bool CNetClient::OnAuthenticateRequest(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE);
CNetClient* client = static_cast(context);
client->SendAuthenticateMessage();
return true;
}
bool CNetClient::OnAuthenticate(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE_RESULT);
CNetClient* client = static_cast(context);
CAuthenticateResultMessage* message = static_cast(event->GetParamRef());
LOGMESSAGE("Net: Authentication result: host=%u, %s", message->m_HostID, utf8_from_wstring(message->m_Message));
client->m_HostID = message->m_HostID;
client->m_Rejoin = message->m_Code == ARC_OK_REJOINING;
client->m_IsController = message->m_IsController;
client->PushGuiMessage(
"type", "netstatus",
"status", "authenticated",
"rejoining", client->m_Rejoin);
return true;
}
bool CNetClient::OnChat(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CHAT);
CNetClient* client = static_cast(context);
CChatMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"type", "chat",
"guid", message->m_GUID,
"text", message->m_Message);
return true;
}
bool CNetClient::OnReady(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_READY);
CNetClient* client = static_cast(context);
CReadyMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"type", "ready",
"guid", message->m_GUID,
"status", message->m_Status);
return true;
}
bool CNetClient::OnGameSetup(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_GAME_SETUP);
CNetClient* client = static_cast(context);
CGameSetupMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"type", "gamesetup",
"data", message->m_Data);
return true;
}
bool CNetClient::OnPlayerAssignment(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_PLAYER_ASSIGNMENT);
CNetClient* client = static_cast(context);
CPlayerAssignmentMessage* message = static_cast(event->GetParamRef());
// Unpack the message
PlayerAssignmentMap newPlayerAssignments;
for (size_t i = 0; i < message->m_Hosts.size(); ++i)
{
PlayerAssignment assignment;
assignment.m_Enabled = true;
assignment.m_Name = message->m_Hosts[i].m_Name;
assignment.m_PlayerID = message->m_Hosts[i].m_PlayerID;
assignment.m_Status = message->m_Hosts[i].m_Status;
newPlayerAssignments[message->m_Hosts[i].m_GUID] = assignment;
}
client->m_PlayerAssignments.swap(newPlayerAssignments);
client->PostPlayerAssignmentsToScript();
return true;
}
// This is called either when the host clicks the StartGame button or
// if this client rejoins and finishes the download of the simstate.
bool CNetClient::OnGameStart(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_GAME_START);
CNetClient* client = static_cast(context);
CGameStartMessage* message = static_cast(event->GetParamRef());
// Find the player assigned to our GUID
int player = -1;
if (client->m_PlayerAssignments.find(client->m_GUID) != client->m_PlayerAssignments.end())
player = client->m_PlayerAssignments[client->m_GUID].m_PlayerID;
client->m_ClientTurnManager = new CNetClientTurnManager(
*client->m_Game->GetSimulation2(), *client, client->m_HostID, client->m_Game->GetReplayLogger());
// Parse init attributes.
const ScriptInterface& scriptInterface = client->m_Game->GetSimulation2()->GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedValue initAttribs(rq.cx);
Script::ParseJSON(rq, message->m_InitAttributes, &initAttribs);
client->m_Game->SetPlayerID(player);
client->m_Game->StartGame(&initAttribs, "");
client->PushGuiMessage("type", "start",
"initAttributes", initAttribs);
return true;
}
bool CNetClient::OnJoinSyncStart(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_JOIN_SYNC_START);
CNetClient* client = static_cast(context);
CJoinSyncStartMessage* joinSyncStartMessage = (CJoinSyncStartMessage*)event->GetParamRef();
// The server wants us to start downloading the game state from it, so do so
client->m_Session->GetFileTransferer().StartTask(
shared_ptr(new CNetFileReceiveTask_ClientRejoin(*client, joinSyncStartMessage->m_InitAttributes))
);
return true;
}
bool CNetClient::OnJoinSyncEndCommandBatch(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH);
CNetClient* client = static_cast(context);
CEndCommandBatchMessage* endMessage = (CEndCommandBatchMessage*)event->GetParamRef();
client->m_ClientTurnManager->FinishedAllCommands(endMessage->m_Turn, endMessage->m_TurnLength);
// Execute all the received commands for the latest turn
client->m_ClientTurnManager->UpdateFastForward();
return true;
}
bool CNetClient::OnRejoined(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_REJOINED);
CNetClient* client = static_cast(context);
CRejoinedMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"type", "rejoined",
"guid", message->m_GUID);
return true;
}
bool CNetClient::OnKicked(void *context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_KICKED);
CNetClient* client = static_cast(context);
CKickedMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"username", message->m_Name,
"type", "kicked",
"banned", message->m_Ban != 0);
return true;
}
bool CNetClient::OnClientTimeout(void *context, CFsmEvent* event)
{
// Report the timeout of some other client
ENSURE(event->GetType() == (uint)NMT_CLIENT_TIMEOUT);
CNetClient* client = static_cast(context);
CClientTimeoutMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"type", "netwarn",
"warntype", "client-timeout",
"guid", message->m_GUID,
"lastReceivedTime", message->m_LastReceivedTime);
return true;
}
bool CNetClient::OnClientPerformance(void *context, CFsmEvent* event)
{
// Performance statistics for one or multiple clients
ENSURE(event->GetType() == (uint)NMT_CLIENT_PERFORMANCE);
CNetClient* client = static_cast(context);
CClientPerformanceMessage* message = static_cast(event->GetParamRef());
// Display warnings for other clients with bad ping
for (size_t i = 0; i < message->m_Clients.size(); ++i)
{
if (message->m_Clients[i].m_MeanRTT < NETWORK_BAD_PING || message->m_Clients[i].m_GUID == client->m_GUID)
continue;
client->PushGuiMessage(
"type", "netwarn",
"warntype", "client-latency",
"guid", message->m_Clients[i].m_GUID,
"meanRTT", message->m_Clients[i].m_MeanRTT);
}
return true;
}
bool CNetClient::OnClientsLoading(void *context, CFsmEvent *event)
{
ENSURE(event->GetType() == (uint)NMT_CLIENTS_LOADING);
CNetClient* client = static_cast(context);
CClientsLoadingMessage* message = static_cast(event->GetParamRef());
std::vector guids;
guids.reserve(message->m_Clients.size());
for (const CClientsLoadingMessage::S_m_Clients& mClient : message->m_Clients)
guids.push_back(mClient.m_GUID);
client->PushGuiMessage(
"type", "clients-loading",
"guids", guids);
return true;
}
bool CNetClient::OnClientPaused(void *context, CFsmEvent *event)
{
ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED);
CNetClient* client = static_cast(context);
CClientPausedMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"type", "paused",
"pause", message->m_Pause != 0,
"guid", message->m_GUID);
return true;
}
bool CNetClient::OnLoadedGame(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
CNetClient* client = static_cast(context);
// All players have loaded the game - start running the turn manager
// so that the game begins
client->m_Game->SetTurnManager(client->m_ClientTurnManager);
client->PushGuiMessage(
"type", "netstatus",
"status", "active");
// If we have rejoined an in progress game, send the rejoined message to the server.
if (client->m_Rejoin)
client->SendRejoinedMessage();
return true;
}
bool CNetClient::OnInGame(void *context, CFsmEvent* event)
{
// TODO: should split each of these cases into a separate method
CNetClient* client = static_cast(context);
CNetMessage* message = static_cast(event->GetParamRef());
if (message)
{
if (message->GetType() == NMT_SIMULATION_COMMAND)
{
CSimulationMessage* simMessage = static_cast (message);
client->m_ClientTurnManager->OnSimulationMessage(simMessage);
}
else if (message->GetType() == NMT_SYNC_ERROR)
{
CSyncErrorMessage* syncMessage = static_cast (message);
client->m_ClientTurnManager->OnSyncError(syncMessage->m_Turn, syncMessage->m_HashExpected, syncMessage->m_PlayerNames);
}
else if (message->GetType() == NMT_END_COMMAND_BATCH)
{
CEndCommandBatchMessage* endMessage = static_cast (message);
client->m_ClientTurnManager->FinishedAllCommands(endMessage->m_Turn, endMessage->m_TurnLength);
}
}
return true;
}
Index: ps/trunk/source/network/NetClient.h
===================================================================
--- ps/trunk/source/network/NetClient.h (revision 25458)
+++ ps/trunk/source/network/NetClient.h (revision 25459)
@@ -1,348 +1,354 @@
/* 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
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef NETCLIENT_H
#define NETCLIENT_H
#include "network/fsm.h"
#include "network/NetFileTransfer.h"
#include "network/NetHost.h"
#include "scriptinterface/Object.h"
#include "ps/CStr.h"
#include
#include
#include
class CGame;
class CNetClientSession;
class CNetClientTurnManager;
class CNetServer;
class ScriptInterface;
typedef struct _ENetHost ENetHost;
// NetClient session FSM states
enum
{
NCS_UNCONNECTED,
NCS_CONNECT,
NCS_HANDSHAKE,
NCS_AUTHENTICATE,
NCS_PREGAME,
NCS_LOADING,
NCS_JOIN_SYNCING,
NCS_INGAME
};
/**
* Network client.
* This code is run by every player (including the host, if they are not
* a dedicated server).
* It provides an interface between the GUI, the network (via CNetClientSession),
* and the game (via CGame and CNetClientTurnManager).
*/
class CNetClient : public CFsm
{
NONCOPYABLE(CNetClient);
friend class CNetFileReceiveTask_ClientRejoin;
public:
/**
* Construct a client associated with the given game object.
* The game must exist for the lifetime of this object.
*/
CNetClient(CGame* game);
virtual ~CNetClient();
/**
* We assume that adding a tracing function that's only called
* during GC is better for performance than using a
* PersistentRooted where each value needs to be added to
* the root set.
*/
static void Trace(JSTracer *trc, void *data)
{
reinterpret_cast(data)->TraceMember(trc);
}
void TraceMember(JSTracer *trc);
/**
* Set the user's name that will be displayed to all players.
* This must not be called after the connection setup.
*/
void SetUserName(const CStrW& username);
/**
* Store the JID of the host.
* This is needed for the secure lobby authentication.
*/
void SetHostJID(const CStr& jid);
void SetControllerSecret(const std::string& secret);
bool IsController() const { return m_IsController; }
/**
* Set the game password.
+ * Must be called after SetUserName, as that is used to hash further.
*/
void SetGamePassword(const CStr& hashedPassword);
/**
* Returns the GUID of the local client.
* Used for distinguishing observers.
*/
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.
* Must call SetupServerData first.
* @return true on success, false on connection failure
*/
bool SetupConnection(ENetHost* enetClient);
/**
+ * Request connection information over the lobby.
+ */
+ void SetupConnectionViaLobby();
+
+ /**
* 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.
* This client probably cannot be used again.
*/
void DestroyConnection();
/**
* Poll the connection for messages from the server and process them, and send
* any queued messages.
* This must be called frequently (i.e. once per frame).
*/
void Poll();
/**
* Locally triggers a GUI message if the connection to the server is being lost or has bad latency.
*/
void CheckServerConnection();
/**
* Retrieves the next queued GUI message, and removes it from the queue.
* The returned value is in the GetScriptInterface() JS context.
*
* This is the only mechanism for the networking code to send messages to
* the GUI - it is pull-based (instead of push) so the engine code does not
* need to know anything about the code structure of the GUI scripts.
*
* The structure of the messages is { "type": "...", ... }
.
* The exact types and associated data are not specified anywhere - the
* implementation and GUI scripts must make the same assumptions.
*
* @return next message, or the value 'undefined' if the queue is empty
*/
void GuiPoll(JS::MutableHandleValue);
/**
* Add a message to the queue, to be read by GuiPoll.
* The script value must be in the GetScriptInterface() JS context.
*/
template
void PushGuiMessage(Args const&... args)
{
ScriptRequest rq(GetScriptInterface());
JS::RootedValue message(rq.cx);
Script::CreateObject(rq, &message, args...);
m_GuiMessageQueue.push_back(JS::Heap(message));
}
/**
* Return a concatenation of all messages in the GUI queue,
* for test cases to easily verify the queue contents.
*/
std::string TestReadGuiMessages();
/**
* Get the script interface associated with this network client,
* which is equivalent to the one used by the CGame in the constructor.
*/
const ScriptInterface& GetScriptInterface();
/**
* Send a message to the server.
* @param message message to send
* @return true on success
*/
bool SendMessage(const CNetMessage* message);
/**
* Call when the network connection has been successfully initiated.
*/
void HandleConnect();
/**
* Call when the network connection has been lost.
*/
void HandleDisconnect(u32 reason);
/**
* Call when a message has been received from the network.
*/
bool HandleMessage(CNetMessage* message);
/**
* Call when the game has started and all data files have been loaded,
* to signal to the server that we are ready to begin the game.
*/
void LoadFinished();
void SendGameSetupMessage(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface);
void SendAssignPlayerMessage(const int playerID, const CStr& guid);
void SendChatMessage(const std::wstring& text);
void SendReadyMessage(const int status);
void SendClearAllReadyMessage();
void SendStartGameMessage(const CStr& initAttribs);
/**
* Call when the client has rejoined a running match and finished
* the loading screen.
*/
void SendRejoinedMessage();
/**
* Call to kick/ban a client
*/
void SendKickPlayerMessage(const CStrW& playerName, bool ban);
/**
* Call when the client has paused or unpaused the game.
*/
void SendPausedMessage(bool pause);
/**
* @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();
// Net message / FSM transition handlers
static bool OnConnect(void* context, CFsmEvent* event);
static bool OnHandshake(void* context, CFsmEvent* event);
static bool OnHandshakeResponse(void* context, CFsmEvent* event);
static bool OnAuthenticateRequest(void* context, CFsmEvent* event);
static bool OnAuthenticate(void* context, CFsmEvent* event);
static bool OnChat(void* context, CFsmEvent* event);
static bool OnReady(void* context, CFsmEvent* event);
static bool OnGameSetup(void* context, CFsmEvent* event);
static bool OnPlayerAssignment(void* context, CFsmEvent* event);
static bool OnInGame(void* context, CFsmEvent* event);
static bool OnGameStart(void* context, CFsmEvent* event);
static bool OnJoinSyncStart(void* context, CFsmEvent* event);
static bool OnJoinSyncEndCommandBatch(void* context, CFsmEvent* event);
static bool OnRejoined(void* context, CFsmEvent* event);
static bool OnKicked(void* context, CFsmEvent* event);
static bool OnClientTimeout(void* context, CFsmEvent* event);
static bool OnClientPerformance(void* context, CFsmEvent* event);
static bool OnClientsLoading(void* context, CFsmEvent* event);
static bool OnClientPaused(void* context, CFsmEvent* event);
static bool OnLoadedGame(void* context, CFsmEvent* event);
/**
* Take ownership of a session object, and use it for all network communication.
*/
void SetAndOwnSession(CNetClientSession* session);
/**
* Push a message onto the GUI queue listing the current player assignments.
*/
void PostPlayerAssignmentsToScript();
CGame *m_Game;
CStrW m_UserName;
CStr m_HostJID;
CStr m_ServerAddress;
u16 m_ServerPort;
bool m_UseSTUN;
/**
* Password to join the game.
*/
CStr m_Password;
/// The 'secret' used to identify the controller of the game.
std::string m_ControllerSecret;
/// Note that this is just a "gui hint" with no actual impact on being controller.
bool m_IsController = false;
/// Current network session (or NULL if not connected)
CNetClientSession* m_Session;
std::thread m_PollingThread;
/// Turn manager associated with the current game (or NULL if we haven't started the game yet)
CNetClientTurnManager* m_ClientTurnManager;
/// Unique-per-game identifier of this client, used to identify the sender of simulation commands
u32 m_HostID;
/// True if the player is currently rejoining or has already rejoined the game.
bool m_Rejoin;
/// Latest copy of player assignments heard from the server
PlayerAssignmentMap m_PlayerAssignments;
/// Globally unique identifier to distinguish users beyond the lifetime of a single network session
CStr m_GUID;
/// Queue of messages for GuiPoll
std::deque> m_GuiMessageQueue;
/// Serialized game state received when joining an in-progress game
std::string m_JoinSyncBuffer;
/// Time when the server was last checked for timeouts and bad latency
std::time_t m_LastConnectionCheck;
};
/// Global network client for the standard game
extern CNetClient *g_NetClient;
#endif // NETCLIENT_H
Index: ps/trunk/source/network/NetServer.cpp
===================================================================
--- ps/trunk/source/network/NetServer.cpp (revision 25458)
+++ ps/trunk/source/network/NetServer.cpp (revision 25459)
@@ -1,1743 +1,1751 @@
/* 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
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "NetServer.h"
#include "NetClient.h"
#include "NetMessage.h"
#include "NetSession.h"
#include "NetServerTurnManager.h"
#include "NetStats.h"
#include "lib/external_libraries/enet.h"
#include "lib/types.h"
#include "network/StunClient.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/GUID.h"
+#include "ps/Hashing.h"
#include "ps/Profile.h"
#include "ps/Threading.h"
#include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/JSON.h"
#include "simulation2/Simulation2.h"
#include "simulation2/system/TurnManager.h"
#if CONFIG2_MINIUPNPC
#include
#include
#include
#include
#endif
#include
/**
* Number of peers to allocate for the enet host.
* Limited by ENET_PROTOCOL_MAXIMUM_PEER_ID (4096).
*
* At most 8 players, 32 observers and 1 temporary connection to send the "server full" disconnect-reason.
*/
#define MAX_CLIENTS 41
#define DEFAULT_SERVER_NAME L"Unnamed Server"
constexpr int CHANNEL_COUNT = 1;
constexpr int FAILED_PASSWORD_TRIES_BEFORE_BAN = 3;
/**
* enet_host_service timeout (msecs).
* Smaller numbers may hurt performance; larger numbers will
* hurt latency responding to messages from game thread.
*/
static const int HOST_SERVICE_TIMEOUT = 50;
/**
* Once ping goes above turn length * command delay,
* the game will start 'freezing' for other clients while we catch up.
* Since commands are sent client -> server -> client, divide by 2.
* (duplicated in NetServer.cpp to avoid having to fetch the constants in a header file)
*/
constexpr u32 NETWORK_BAD_PING = DEFAULT_TURN_LENGTH * COMMAND_DELAY_MP / 2;
CNetServer* g_NetServer = NULL;
static CStr DebugName(CNetServerSession* session)
{
if (session == NULL)
return "[unknown host]";
if (session->GetGUID().empty())
return "[unauthed host]";
return "[" + session->GetGUID().substr(0, 8) + "...]";
}
/**
* Async task for receiving the initial game state to be forwarded to another
* client that is rejoining an in-progress network game.
*/
class CNetFileReceiveTask_ServerRejoin : public CNetFileReceiveTask
{
NONCOPYABLE(CNetFileReceiveTask_ServerRejoin);
public:
CNetFileReceiveTask_ServerRejoin(CNetServerWorker& server, u32 hostID)
: m_Server(server), m_RejoinerHostID(hostID)
{
}
virtual void OnComplete()
{
// We've received the game state from an existing player - now
// we need to send it onwards to the newly rejoining player
// Find the session corresponding to the rejoining host (if any)
CNetServerSession* session = NULL;
for (CNetServerSession* serverSession : m_Server.m_Sessions)
{
if (serverSession->GetHostID() == m_RejoinerHostID)
{
session = serverSession;
break;
}
}
if (!session)
{
LOGMESSAGE("Net server: rejoining client disconnected before we sent to it");
return;
}
// Store the received state file, and tell the client to start downloading it from us
// TODO: this will get kind of confused if there's multiple clients downloading in parallel;
// they'll race and get whichever happens to be the latest received by the server,
// which should still work but isn't great
m_Server.m_JoinSyncFile = m_Buffer;
// Send the init attributes alongside - these should be correct since the game should be started.
CJoinSyncStartMessage message;
message.m_InitAttributes = Script::StringifyJSON(ScriptRequest(m_Server.GetScriptInterface()), &m_Server.m_InitAttributes);
session->SendMessage(&message);
}
private:
CNetServerWorker& m_Server;
u32 m_RejoinerHostID;
};
/*
* XXX: We use some non-threadsafe functions from the worker thread.
* See http://trac.wildfiregames.com/ticket/654
*/
CNetServerWorker::CNetServerWorker(bool useLobbyAuth, int autostartPlayers) :
m_AutostartPlayers(autostartPlayers),
m_LobbyAuth(useLobbyAuth),
m_Shutdown(false),
m_ScriptInterface(NULL),
m_NextHostID(1), m_Host(NULL), m_ControllerGUID(), m_Stats(NULL),
m_LastConnectionCheck(0)
{
m_State = SERVER_STATE_UNCONNECTED;
m_ServerTurnManager = NULL;
m_ServerName = DEFAULT_SERVER_NAME;
}
CNetServerWorker::~CNetServerWorker()
{
if (m_State != SERVER_STATE_UNCONNECTED)
{
// Tell the thread to shut down
{
std::lock_guard lock(m_WorkerMutex);
m_Shutdown = true;
}
// Wait for it to shut down cleanly
m_WorkerThread.join();
}
#if CONFIG2_MINIUPNPC
if (m_UPnPThread.joinable())
m_UPnPThread.detach();
#endif
// Clean up resources
delete m_Stats;
for (CNetServerSession* session : m_Sessions)
{
session->DisconnectNow(NDR_SERVER_SHUTDOWN);
delete session;
}
if (m_Host)
enet_host_destroy(m_Host);
delete m_ServerTurnManager;
}
void CNetServerWorker::SetPassword(const CStr& hashedPassword)
{
m_Password = hashedPassword;
}
void CNetServerWorker::SetControllerSecret(const std::string& secret)
{
m_ControllerSecret = secret;
}
+bool CNetServerWorker::CheckPassword(const std::string& password, const std::string& salt) const
+{
+ return HashCryptographically(m_Password, salt) == password;
+}
+
+
bool CNetServerWorker::SetupConnection(const u16 port)
{
ENSURE(m_State == SERVER_STATE_UNCONNECTED);
ENSURE(!m_Host);
// Bind to default host
ENetAddress addr;
addr.host = ENET_HOST_ANY;
addr.port = port;
// Create ENet server
m_Host = enet_host_create(&addr, MAX_CLIENTS, CHANNEL_COUNT, 0, 0);
if (!m_Host)
{
LOGERROR("Net server: enet_host_create failed");
return false;
}
m_Stats = new CNetStatsTable();
if (CProfileViewer::IsInitialised())
g_ProfileViewer.AddRootTable(m_Stats);
m_State = SERVER_STATE_PREGAME;
// Launch the worker thread
m_WorkerThread = std::thread(Threading::HandleExceptions::Wrapper, this);
#if CONFIG2_MINIUPNPC
// Launch the UPnP thread
m_UPnPThread = std::thread(Threading::HandleExceptions::Wrapper);
#endif
return true;
}
#if CONFIG2_MINIUPNPC
void CNetServerWorker::SetupUPnP()
{
debug_SetThreadName("UPnP");
// Values we want to set.
char psPort[6];
sprintf_s(psPort, ARRAY_SIZE(psPort), "%d", PS_DEFAULT_PORT);
const char* leaseDuration = "0"; // Indefinite/permanent lease duration.
const char* description = "0AD Multiplayer";
const char* protocall = "UDP";
char internalIPAddress[64];
char externalIPAddress[40];
// Variables to hold the values that actually get set.
char intClient[40];
char intPort[6];
char duration[16];
// Intermediate variables.
bool allocatedUrls = false;
struct UPNPUrls urls;
struct IGDdatas data;
struct UPNPDev* devlist = NULL;
// Make sure everything is properly freed.
std::function freeUPnP = [&allocatedUrls, &urls, &devlist]()
{
if (allocatedUrls)
FreeUPNPUrls(&urls);
freeUPNPDevlist(devlist);
// IGDdatas does not need to be freed according to UPNP_GetIGDFromUrl
};
// Cached root descriptor URL.
std::string rootDescURL;
CFG_GET_VAL("network.upnprootdescurl", rootDescURL);
if (!rootDescURL.empty())
LOGMESSAGE("Net server: attempting to use cached root descriptor URL: %s", rootDescURL.c_str());
int ret = 0;
// Try a cached URL first
if (!rootDescURL.empty() && UPNP_GetIGDFromUrl(rootDescURL.c_str(), &urls, &data, internalIPAddress, sizeof(internalIPAddress)))
{
LOGMESSAGE("Net server: using cached IGD = %s", urls.controlURL);
ret = 1;
}
// No cached URL, or it did not respond. Try getting a valid UPnP device for 10 seconds.
#if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 14
else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 2, 0)) != NULL)
#else
else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 0)) != NULL)
#endif
{
ret = UPNP_GetValidIGD(devlist, &urls, &data, internalIPAddress, sizeof(internalIPAddress));
allocatedUrls = ret != 0; // urls is allocated on non-zero return values
}
else
{
LOGMESSAGE("Net server: upnpDiscover failed and no working cached URL.");
freeUPnP();
return;
}
switch (ret)
{
case 0:
LOGMESSAGE("Net server: No IGD found");
break;
case 1:
LOGMESSAGE("Net server: found valid IGD = %s", urls.controlURL);
break;
case 2:
LOGMESSAGE("Net server: found a valid, not connected IGD = %s, will try to continue anyway", urls.controlURL);
break;
case 3:
LOGMESSAGE("Net server: found a UPnP device unrecognized as IGD = %s, will try to continue anyway", urls.controlURL);
break;
default:
debug_warn(L"Unrecognized return value from UPNP_GetValidIGD");
}
// Try getting our external/internet facing IP. TODO: Display this on the game-setup page for conviniance.
ret = UPNP_GetExternalIPAddress(urls.controlURL, data.first.servicetype, externalIPAddress);
if (ret != UPNPCOMMAND_SUCCESS)
{
LOGMESSAGE("Net server: GetExternalIPAddress failed with code %d (%s)", ret, strupnperror(ret));
freeUPnP();
return;
}
LOGMESSAGE("Net server: ExternalIPAddress = %s", externalIPAddress);
// Try to setup port forwarding.
ret = UPNP_AddPortMapping(urls.controlURL, data.first.servicetype, psPort, psPort,
internalIPAddress, description, protocall, 0, leaseDuration);
if (ret != UPNPCOMMAND_SUCCESS)
{
LOGMESSAGE("Net server: AddPortMapping(%s, %s, %s) failed with code %d (%s)",
psPort, psPort, internalIPAddress, ret, strupnperror(ret));
freeUPnP();
return;
}
// Check that the port was actually forwarded.
ret = UPNP_GetSpecificPortMappingEntry(urls.controlURL,
data.first.servicetype,
psPort, protocall,
#if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 10
NULL/*remoteHost*/,
#endif
intClient, intPort, NULL/*desc*/,
NULL/*enabled*/, duration);
if (ret != UPNPCOMMAND_SUCCESS)
{
LOGMESSAGE("Net server: GetSpecificPortMappingEntry() failed with code %d (%s)", ret, strupnperror(ret));
freeUPnP();
return;
}
LOGMESSAGE("Net server: External %s:%s %s is redirected to internal %s:%s (duration=%s)",
externalIPAddress, psPort, protocall, intClient, intPort, duration);
// Cache root descriptor URL to try to avoid discovery next time.
g_ConfigDB.SetValueString(CFG_USER, "network.upnprootdescurl", urls.controlURL);
g_ConfigDB.WriteValueToFile(CFG_USER, "network.upnprootdescurl", urls.controlURL);
LOGMESSAGE("Net server: cached UPnP root descriptor URL as %s", urls.controlURL);
freeUPnP();
}
#endif // CONFIG2_MINIUPNPC
bool CNetServerWorker::SendMessage(ENetPeer* peer, const CNetMessage* message)
{
ENSURE(m_Host);
CNetServerSession* session = static_cast(peer->data);
return CNetHost::SendMessage(message, peer, DebugName(session).c_str());
}
bool CNetServerWorker::Broadcast(const CNetMessage* message, const std::vector& targetStates)
{
ENSURE(m_Host);
bool ok = true;
// TODO: this does lots of repeated message serialisation if we have lots
// of remote peers; could do it more efficiently if that's a real problem
for (CNetServerSession* session : m_Sessions)
if (std::find(targetStates.begin(), targetStates.end(), static_cast(session->GetCurrState())) != targetStates.end() &&
!session->SendMessage(message))
ok = false;
return ok;
}
void CNetServerWorker::RunThread(CNetServerWorker* data)
{
debug_SetThreadName("NetServer");
data->Run();
}
void CNetServerWorker::Run()
{
// The script context uses the profiler and therefore the thread must be registered before the context is created
g_Profiler2.RegisterCurrentThread("Net server");
// We create a new ScriptContext for this network thread, with a single ScriptInterface.
shared_ptr netServerContext = ScriptContext::CreateContext();
m_ScriptInterface = new ScriptInterface("Engine", "Net server", netServerContext);
m_InitAttributes.init(m_ScriptInterface->GetGeneralJSContext(), JS::UndefinedValue());
while (true)
{
if (!RunStep())
break;
// Implement autostart mode
if (m_State == SERVER_STATE_PREGAME && (int)m_PlayerAssignments.size() == m_AutostartPlayers)
StartGame(Script::StringifyJSON(ScriptRequest(m_ScriptInterface), &m_InitAttributes));
// Update profiler stats
m_Stats->LatchHostState(m_Host);
}
// Clear roots before deleting their context
m_SavedCommands.clear();
SAFE_DELETE(m_ScriptInterface);
}
bool CNetServerWorker::RunStep()
{
// Check for messages from the game thread.
// (Do as little work as possible while the mutex is held open,
// to avoid performance problems and deadlocks.)
m_ScriptInterface->GetContext()->MaybeIncrementalGC(0.5f);
ScriptRequest rq(m_ScriptInterface);
std::vector newStartGame;
std::vector newGameAttributes;
std::vector> newLobbyAuths;
std::vector newTurnLength;
{
std::lock_guard lock(m_WorkerMutex);
if (m_Shutdown)
return false;
newStartGame.swap(m_StartGameQueue);
newGameAttributes.swap(m_InitAttributesQueue);
newLobbyAuths.swap(m_LobbyAuthQueue);
newTurnLength.swap(m_TurnLengthQueue);
}
if (!newGameAttributes.empty())
{
if (m_State != SERVER_STATE_UNCONNECTED && m_State != SERVER_STATE_PREGAME)
LOGERROR("NetServer: Init Attributes cannot be changed after the server starts loading.");
else
{
JS::RootedValue gameAttributesVal(rq.cx);
Script::ParseJSON(rq, newGameAttributes.back(), &gameAttributesVal);
m_InitAttributes = gameAttributesVal;
}
}
if (!newTurnLength.empty())
SetTurnLength(newTurnLength.back());
while (!newLobbyAuths.empty())
{
const std::pair& auth = newLobbyAuths.back();
ProcessLobbyAuth(auth.first, auth.second);
newLobbyAuths.pop_back();
}
// Perform file transfers
for (CNetServerSession* session : m_Sessions)
session->GetFileTransferer().Poll();
CheckClientConnections();
// Process network events:
ENetEvent event;
int status = enet_host_service(m_Host, &event, HOST_SERVICE_TIMEOUT);
if (status < 0)
{
LOGERROR("CNetServerWorker: enet_host_service failed (%d)", status);
// TODO: notify game that the server has shut down
return false;
}
if (status == 0)
{
// Reached timeout with no events - try again
return true;
}
// Process the event:
switch (event.type)
{
case ENET_EVENT_TYPE_CONNECT:
{
// Report the client address
char hostname[256] = "(error)";
enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname));
LOGMESSAGE("Net server: Received connection from %s:%u", hostname, (unsigned int)event.peer->address.port);
// Set up a session object for this peer
CNetServerSession* session = new CNetServerSession(*this, event.peer);
m_Sessions.push_back(session);
SetupSession(session);
ENSURE(event.peer->data == NULL);
event.peer->data = session;
HandleConnect(session);
break;
}
case ENET_EVENT_TYPE_DISCONNECT:
{
// If there is an active session with this peer, then reset and delete it
CNetServerSession* session = static_cast(event.peer->data);
if (session)
{
LOGMESSAGE("Net server: Disconnected %s", DebugName(session).c_str());
// Remove the session first, so we won't send player-update messages to it
// when updating the FSM
m_Sessions.erase(remove(m_Sessions.begin(), m_Sessions.end(), session), m_Sessions.end());
session->Update((uint)NMT_CONNECTION_LOST, NULL);
delete session;
event.peer->data = NULL;
}
if (m_State == SERVER_STATE_LOADING)
CheckGameLoadStatus(NULL);
break;
}
case ENET_EVENT_TYPE_RECEIVE:
{
// If there is an active session with this peer, then process the message
CNetServerSession* session = static_cast(event.peer->data);
if (session)
{
// Create message from raw data
CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, GetScriptInterface());
if (msg)
{
LOGMESSAGE("Net server: Received message %s of size %lu from %s", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength(), DebugName(session).c_str());
HandleMessageReceive(msg, session);
delete msg;
}
}
// Done using the packet
enet_packet_destroy(event.packet);
break;
}
case ENET_EVENT_TYPE_NONE:
break;
}
return true;
}
void CNetServerWorker::CheckClientConnections()
{
// Send messages at most once per second
std::time_t now = std::time(nullptr);
if (now <= m_LastConnectionCheck)
return;
m_LastConnectionCheck = now;
for (size_t i = 0; i < m_Sessions.size(); ++i)
{
u32 lastReceived = m_Sessions[i]->GetLastReceivedTime();
u32 meanRTT = m_Sessions[i]->GetMeanRTT();
CNetMessage* message = nullptr;
// Report if we didn't hear from the client since few seconds
if (lastReceived > NETWORK_WARNING_TIMEOUT)
{
CClientTimeoutMessage* msg = new CClientTimeoutMessage();
msg->m_GUID = m_Sessions[i]->GetGUID();
msg->m_LastReceivedTime = lastReceived;
message = msg;
}
// Report if the client has bad ping
else if (meanRTT > NETWORK_BAD_PING)
{
CClientPerformanceMessage* msg = new CClientPerformanceMessage();
CClientPerformanceMessage::S_m_Clients client;
client.m_GUID = m_Sessions[i]->GetGUID();
client.m_MeanRTT = meanRTT;
msg->m_Clients.push_back(client);
message = msg;
}
// Send to all clients except the affected one
// (since that will show the locally triggered warning instead).
// Also send it to clients that finished the loading screen while
// the game is still waiting for other clients to finish the loading screen.
if (message)
for (size_t j = 0; j < m_Sessions.size(); ++j)
{
if (i != j && (
(m_Sessions[j]->GetCurrState() == NSS_PREGAME && m_State == SERVER_STATE_PREGAME) ||
m_Sessions[j]->GetCurrState() == NSS_INGAME))
{
m_Sessions[j]->SendMessage(message);
}
}
SAFE_DELETE(message);
}
}
void CNetServerWorker::HandleMessageReceive(const CNetMessage* message, CNetServerSession* session)
{
// Handle non-FSM messages first
Status status = session->GetFileTransferer().HandleMessageReceive(*message);
if (status != INFO::SKIPPED)
return;
if (message->GetType() == NMT_FILE_TRANSFER_REQUEST)
{
CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message;
// Rejoining client got our JoinSyncStart after we received the state from
// another client, and has now requested that we forward it to them
ENSURE(!m_JoinSyncFile.empty());
session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, m_JoinSyncFile);
return;
}
// Update FSM
if (!session->Update(message->GetType(), (void*)message))
LOGERROR("Net server: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)session->GetCurrState());
}
void CNetServerWorker::SetupSession(CNetServerSession* session)
{
void* context = session;
// Set up transitions for session
session->AddTransition(NSS_UNCONNECTED, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CLIENT_HANDSHAKE, NSS_AUTHENTICATE, (void*)&OnClientHandshake, context);
session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&OnAuthenticate, context);
session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&OnAuthenticate, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_CHAT, NSS_PREGAME, (void*)&OnChat, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_READY, NSS_PREGAME, (void*)&OnReady, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_CLEAR_ALL_READY, NSS_PREGAME, (void*)&OnClearAllReady, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_SETUP, NSS_PREGAME, (void*)&OnGameSetup, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_ASSIGN_PLAYER, NSS_PREGAME, (void*)&OnAssignPlayer, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_KICKED, NSS_PREGAME, (void*)&OnKickPlayer, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_START, NSS_PREGAME, (void*)&OnGameStart, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnLoadedGame, context);
session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_KICKED, NSS_JOIN_SYNCING, (void*)&OnKickPlayer, context);
session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnJoinSyncingLoadedGame, context);
session->AddTransition(NSS_INGAME, (uint)NMT_REJOINED, NSS_INGAME, (void*)&OnRejoined, context);
session->AddTransition(NSS_INGAME, (uint)NMT_KICKED, NSS_INGAME, (void*)&OnKickPlayer, context);
session->AddTransition(NSS_INGAME, (uint)NMT_CLIENT_PAUSED, NSS_INGAME, (void*)&OnClientPaused, context);
session->AddTransition(NSS_INGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
session->AddTransition(NSS_INGAME, (uint)NMT_CHAT, NSS_INGAME, (void*)&OnChat, context);
session->AddTransition(NSS_INGAME, (uint)NMT_SIMULATION_COMMAND, NSS_INGAME, (void*)&OnSimulationCommand, context);
session->AddTransition(NSS_INGAME, (uint)NMT_SYNC_CHECK, NSS_INGAME, (void*)&OnSyncCheck, context);
session->AddTransition(NSS_INGAME, (uint)NMT_END_COMMAND_BATCH, NSS_INGAME, (void*)&OnEndCommandBatch, context);
// Set first state
session->SetFirstState(NSS_HANDSHAKE);
}
bool CNetServerWorker::HandleConnect(CNetServerSession* session)
{
if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), session->GetIPAddress()) != m_BannedIPs.end())
{
session->Disconnect(NDR_BANNED);
return false;
}
CSrvHandshakeMessage handshake;
handshake.m_Magic = PS_PROTOCOL_MAGIC;
handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION;
handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION;
return session->SendMessage(&handshake);
}
void CNetServerWorker::OnUserJoin(CNetServerSession* session)
{
AddPlayer(session->GetGUID(), session->GetUserName());
CPlayerAssignmentMessage assignMessage;
ConstructPlayerAssignmentMessage(assignMessage);
session->SendMessage(&assignMessage);
}
void CNetServerWorker::OnUserLeave(CNetServerSession* session)
{
std::vector::iterator pausing = std::find(m_PausingPlayers.begin(), m_PausingPlayers.end(), session->GetGUID());
if (pausing != m_PausingPlayers.end())
m_PausingPlayers.erase(pausing);
RemovePlayer(session->GetGUID());
if (m_ServerTurnManager && session->GetCurrState() != NSS_JOIN_SYNCING)
m_ServerTurnManager->UninitialiseClient(session->GetHostID());
// TODO: ought to switch the player controlled by that client
// back to AI control, or something?
}
void CNetServerWorker::AddPlayer(const CStr& guid, const CStrW& name)
{
// Find all player IDs in active use; we mustn't give them to a second player (excluding the unassigned ID: -1)
std::set usedIDs;
for (const std::pair& p : m_PlayerAssignments)
if (p.second.m_Enabled && p.second.m_PlayerID != -1)
usedIDs.insert(p.second.m_PlayerID);
// If the player is rejoining after disconnecting, try to give them
// back their old player ID
i32 playerID = -1;
// Try to match GUID first
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
{
if (!it->second.m_Enabled && it->first == guid && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
{
playerID = it->second.m_PlayerID;
m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
goto found;
}
}
// Try to match username next
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
{
if (!it->second.m_Enabled && it->second.m_Name == name && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
{
playerID = it->second.m_PlayerID;
m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
goto found;
}
}
// Otherwise leave the player ID as -1 (observer) and let gamesetup change it as needed.
found:
PlayerAssignment assignment;
assignment.m_Enabled = true;
assignment.m_Name = name;
assignment.m_PlayerID = playerID;
assignment.m_Status = 0;
m_PlayerAssignments[guid] = assignment;
// Send the new assignments to all currently active players
// (which does not include the one that's just joining)
SendPlayerAssignments();
}
void CNetServerWorker::RemovePlayer(const CStr& guid)
{
m_PlayerAssignments[guid].m_Enabled = false;
SendPlayerAssignments();
}
void CNetServerWorker::ClearAllPlayerReady()
{
for (std::pair& p : m_PlayerAssignments)
if (p.second.m_Status != 2)
p.second.m_Status = 0;
SendPlayerAssignments();
}
void CNetServerWorker::KickPlayer(const CStrW& playerName, const bool ban)
{
// Find the user with that name
std::vector::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(),
[&](CNetServerSession* session) { return session->GetUserName() == playerName; });
// and return if no one or the host has that name
if (it == m_Sessions.end() || (*it)->GetGUID() == m_ControllerGUID)
return;
if (ban)
{
// Remember name
if (std::find(m_BannedPlayers.begin(), m_BannedPlayers.end(), playerName) == m_BannedPlayers.end())
m_BannedPlayers.push_back(m_LobbyAuth ? CStrW(playerName.substr(0, playerName.find(L" ("))) : playerName);
// Remember IP address
u32 ipAddress = (*it)->GetIPAddress();
if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), ipAddress) == m_BannedIPs.end())
m_BannedIPs.push_back(ipAddress);
}
// Disconnect that user
(*it)->Disconnect(ban ? NDR_BANNED : NDR_KICKED);
// Send message notifying other clients
CKickedMessage kickedMessage;
kickedMessage.m_Name = playerName;
kickedMessage.m_Ban = ban;
Broadcast(&kickedMessage, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME });
}
void CNetServerWorker::AssignPlayer(int playerID, const CStr& guid)
{
// Remove anyone who's already assigned to this player
for (std::pair& p : m_PlayerAssignments)
{
if (p.second.m_PlayerID == playerID)
p.second.m_PlayerID = -1;
}
// Update this host's assignment if it exists
if (m_PlayerAssignments.find(guid) != m_PlayerAssignments.end())
m_PlayerAssignments[guid].m_PlayerID = playerID;
SendPlayerAssignments();
}
void CNetServerWorker::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message)
{
for (const std::pair& p : m_PlayerAssignments)
{
if (!p.second.m_Enabled)
continue;
CPlayerAssignmentMessage::S_m_Hosts h;
h.m_GUID = p.first;
h.m_Name = p.second.m_Name;
h.m_PlayerID = p.second.m_PlayerID;
h.m_Status = p.second.m_Status;
message.m_Hosts.push_back(h);
}
}
void CNetServerWorker::SendPlayerAssignments()
{
CPlayerAssignmentMessage message;
ConstructPlayerAssignmentMessage(message);
Broadcast(&message, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME });
}
const ScriptInterface& CNetServerWorker::GetScriptInterface()
{
return *m_ScriptInterface;
}
void CNetServerWorker::SetTurnLength(u32 msecs)
{
if (m_ServerTurnManager)
m_ServerTurnManager->SetTurnLength(msecs);
}
void CNetServerWorker::ProcessLobbyAuth(const CStr& name, const CStr& token)
{
LOGMESSAGE("Net Server: Received lobby auth message from %s with %s", name, token);
// Find the user with that guid
std::vector::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(),
[&](CNetServerSession* session)
{ return session->GetGUID() == token; });
if (it == m_Sessions.end())
return;
(*it)->SetUserName(name.FromUTF8());
// Send an empty message to request the authentication message from the client
// after its identity has been confirmed via the lobby
CAuthenticateMessage emptyMessage;
(*it)->SendMessage(&emptyMessage);
}
bool CNetServerWorker::OnClientHandshake(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CLIENT_HANDSHAKE);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CCliHandshakeMessage* message = (CCliHandshakeMessage*)event->GetParamRef();
if (message->m_ProtocolVersion != PS_PROTOCOL_VERSION)
{
session->Disconnect(NDR_INCORRECT_PROTOCOL_VERSION);
return false;
}
CStr guid = ps_generate_guid();
int count = 0;
// Ensure unique GUID
while(std::find_if(
server.m_Sessions.begin(), server.m_Sessions.end(),
[&guid] (const CNetServerSession* session)
{ return session->GetGUID() == guid; }) != server.m_Sessions.end())
{
if (++count > 100)
{
session->Disconnect(NDR_GUID_FAILED);
return true;
}
guid = ps_generate_guid();
}
session->SetGUID(guid);
CSrvHandshakeResponseMessage handshakeResponse;
handshakeResponse.m_UseProtocolVersion = PS_PROTOCOL_VERSION;
handshakeResponse.m_GUID = guid;
handshakeResponse.m_Flags = 0;
if (server.m_LobbyAuth)
{
handshakeResponse.m_Flags |= PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH;
session->SetNextState(NSS_LOBBY_AUTHENTICATE);
}
session->SendMessage(&handshakeResponse);
return true;
}
bool CNetServerWorker::OnAuthenticate(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
// Prohibit joins while the game is loading
if (server.m_State == SERVER_STATE_LOADING)
{
LOGMESSAGE("Refused connection while the game is loading");
session->Disconnect(NDR_SERVER_LOADING);
return true;
}
CAuthenticateMessage* message = (CAuthenticateMessage*)event->GetParamRef();
CStrW username = SanitisePlayerName(message->m_Name);
CStrW usernameWithoutRating(username.substr(0, username.find(L" (")));
// Compare the lowercase names as specified by https://xmpp.org/extensions/xep-0029.html#sect-idm139493404168176
// "[...] comparisons will be made in case-normalized canonical form."
if (server.m_LobbyAuth && usernameWithoutRating.LowerCase() != session->GetUserName().LowerCase())
{
LOGERROR("Net server: lobby auth: %s tried joining as %s",
session->GetUserName().ToUTF8(),
usernameWithoutRating.ToUTF8());
session->Disconnect(NDR_LOBBY_AUTH_FAILED);
return true;
}
// Check the password before anything else.
- if (server.m_Password != message->m_Password)
+ // NB: m_Name must match the client's salt, @see CNetClient::SetGamePassword
+ if (!server.CheckPassword(message->m_Password, message->m_Name.ToUTF8()))
{
// Noisy logerror because players are not supposed to be able to get the IP,
// so this might be someone targeting the host for some reason
// (or TODO a dedicated server and we do want to log anyways)
LOGERROR("Net server: user %s tried joining with the wrong password",
session->GetUserName().ToUTF8());
session->Disconnect(NDR_SERVER_REFUSED);
return true;
}
// Either deduplicate or prohibit join if name is in use
bool duplicatePlayernames = false;
CFG_GET_VAL("network.duplicateplayernames", duplicatePlayernames);
// If lobby authentication is enabled, the clients playername has already been registered.
// There also can't be any duplicated names.
if (!server.m_LobbyAuth && duplicatePlayernames)
username = server.DeduplicatePlayerName(username);
else
{
std::vector::iterator it = std::find_if(
server.m_Sessions.begin(), server.m_Sessions.end(),
[&username] (const CNetServerSession* session)
{ return session->GetUserName() == username; });
if (it != server.m_Sessions.end() && (*it) != session)
{
session->Disconnect(NDR_PLAYERNAME_IN_USE);
return true;
}
}
// Disconnect banned usernames
if (std::find(server.m_BannedPlayers.begin(), server.m_BannedPlayers.end(), server.m_LobbyAuth ? usernameWithoutRating : username) != server.m_BannedPlayers.end())
{
session->Disconnect(NDR_BANNED);
return true;
}
int maxObservers = 0;
CFG_GET_VAL("network.observerlimit", maxObservers);
bool isRejoining = false;
bool serverFull = false;
if (server.m_State == SERVER_STATE_PREGAME)
{
// Don't check for maxObservers in the gamesetup, as we don't know yet who will be assigned
serverFull = server.m_Sessions.size() >= MAX_CLIENTS;
}
else
{
bool isObserver = true;
int disconnectedPlayers = 0;
int connectedPlayers = 0;
// (TODO: if GUIDs were stable, we should use them instead)
for (const std::pair& p : server.m_PlayerAssignments)
{
const PlayerAssignment& assignment = p.second;
if (!assignment.m_Enabled && assignment.m_Name == username)
{
isObserver = assignment.m_PlayerID == -1;
isRejoining = true;
}
if (assignment.m_PlayerID == -1)
continue;
if (assignment.m_Enabled)
++connectedPlayers;
else
++disconnectedPlayers;
}
// Optionally allow everyone or only buddies to join after the game has started
if (!isRejoining)
{
CStr observerLateJoin;
CFG_GET_VAL("network.lateobservers", observerLateJoin);
if (observerLateJoin == "everyone")
{
isRejoining = true;
}
else if (observerLateJoin == "buddies")
{
CStr buddies;
CFG_GET_VAL("lobby.buddies", buddies);
std::wstringstream buddiesStream(wstring_from_utf8(buddies));
CStrW buddy;
while (std::getline(buddiesStream, buddy, L','))
{
if (buddy == usernameWithoutRating)
{
isRejoining = true;
break;
}
}
}
}
if (!isRejoining)
{
LOGMESSAGE("Refused connection after game start from not-previously-known user \"%s\"", utf8_from_wstring(username));
session->Disconnect(NDR_SERVER_ALREADY_IN_GAME);
return true;
}
// Ensure all players will be able to rejoin
serverFull = isObserver && (
(int) server.m_Sessions.size() - connectedPlayers > maxObservers ||
(int) server.m_Sessions.size() + disconnectedPlayers >= MAX_CLIENTS);
}
if (serverFull)
{
session->Disconnect(NDR_SERVER_FULL);
return true;
}
u32 newHostID = server.m_NextHostID++;
session->SetUserName(username);
session->SetHostID(newHostID);
CAuthenticateResultMessage authenticateResult;
authenticateResult.m_Code = isRejoining ? ARC_OK_REJOINING : ARC_OK;
authenticateResult.m_HostID = newHostID;
authenticateResult.m_Message = L"Logged in";
authenticateResult.m_IsController = 0;
if (message->m_ControllerSecret == server.m_ControllerSecret)
{
if (server.m_ControllerGUID.empty())
{
server.m_ControllerGUID = session->GetGUID();
authenticateResult.m_IsController = 1;
}
// TODO: we could probably handle having several controllers, or swapping?
}
session->SendMessage(&authenticateResult);
server.OnUserJoin(session);
if (isRejoining)
{
ENSURE(server.m_State != SERVER_STATE_UNCONNECTED && server.m_State != SERVER_STATE_PREGAME);
// Request a copy of the current game state from an existing player,
// so we can send it on to the new player
// Assume session 0 is most likely the local player, so they're
// the most efficient client to request a copy from
CNetServerSession* sourceSession = server.m_Sessions.at(0);
sourceSession->GetFileTransferer().StartTask(
shared_ptr(new CNetFileReceiveTask_ServerRejoin(server, newHostID))
);
session->SetNextState(NSS_JOIN_SYNCING);
}
return true;
}
bool CNetServerWorker::OnSimulationCommand(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_SIMULATION_COMMAND);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CSimulationMessage* message = (CSimulationMessage*)event->GetParamRef();
// Ignore messages sent by one player on behalf of another player
// unless cheating is enabled
bool cheatsEnabled = false;
const ScriptInterface& scriptInterface = server.GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedValue settings(rq.cx);
Script::GetProperty(rq, server.m_InitAttributes, "settings", &settings);
if (Script::HasProperty(rq, settings, "CheatsEnabled"))
Script::GetProperty(rq, settings, "CheatsEnabled", cheatsEnabled);
PlayerAssignmentMap::iterator it = server.m_PlayerAssignments.find(session->GetGUID());
// When cheating is disabled, fail if the player the message claims to
// represent does not exist or does not match the sender's player name
if (!cheatsEnabled && (it == server.m_PlayerAssignments.end() || it->second.m_PlayerID != message->m_Player))
return true;
// Send it back to all clients that have finished
// the loading screen (and the synchronization when rejoining)
server.Broadcast(message, { NSS_INGAME });
// Save all the received commands
if (server.m_SavedCommands.size() < message->m_Turn + 1)
server.m_SavedCommands.resize(message->m_Turn + 1);
server.m_SavedCommands[message->m_Turn].push_back(*message);
// TODO: we shouldn't send the message back to the client that first sent it
return true;
}
bool CNetServerWorker::OnSyncCheck(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_SYNC_CHECK);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CSyncCheckMessage* message = (CSyncCheckMessage*)event->GetParamRef();
server.m_ServerTurnManager->NotifyFinishedClientUpdate(*session, message->m_Turn, message->m_Hash);
return true;
}
bool CNetServerWorker::OnEndCommandBatch(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CEndCommandBatchMessage* message = (CEndCommandBatchMessage*)event->GetParamRef();
// The turn-length field is ignored
server.m_ServerTurnManager->NotifyFinishedClientCommands(*session, message->m_Turn);
return true;
}
bool CNetServerWorker::OnChat(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CHAT);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CChatMessage* message = (CChatMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
server.Broadcast(message, { NSS_PREGAME, NSS_INGAME });
return true;
}
bool CNetServerWorker::OnReady(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_READY);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
// Occurs if a client presses not-ready
// in the very last moment before the hosts starts the game
if (server.m_State == SERVER_STATE_LOADING)
return true;
CReadyMessage* message = (CReadyMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
server.Broadcast(message, { NSS_PREGAME });
server.m_PlayerAssignments[message->m_GUID].m_Status = message->m_Status;
return true;
}
bool CNetServerWorker::OnClearAllReady(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CLEAR_ALL_READY);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
if (session->GetGUID() == server.m_ControllerGUID)
server.ClearAllPlayerReady();
return true;
}
bool CNetServerWorker::OnGameSetup(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_GAME_SETUP);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
// Changing the settings after gamestart is not implemented and would cause an Out-of-sync error.
// This happened when doubleclicking on the startgame button.
if (server.m_State != SERVER_STATE_PREGAME)
return true;
// Only the controller is allowed to send game setup updates.
// TODO: it would be good to allow other players to request changes to some settings,
// e.g. their civilisation.
// Possibly this should use another message, to enforce a single source of truth.
if (session->GetGUID() == server.m_ControllerGUID)
{
CGameSetupMessage* message = (CGameSetupMessage*)event->GetParamRef();
server.Broadcast(message, { NSS_PREGAME });
}
return true;
}
bool CNetServerWorker::OnAssignPlayer(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_ASSIGN_PLAYER);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
if (session->GetGUID() == server.m_ControllerGUID)
{
CAssignPlayerMessage* message = (CAssignPlayerMessage*)event->GetParamRef();
server.AssignPlayer(message->m_PlayerID, message->m_GUID);
}
return true;
}
bool CNetServerWorker::OnGameStart(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_GAME_START);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
if (session->GetGUID() != server.m_ControllerGUID)
return true;
CGameStartMessage* message = (CGameStartMessage*)event->GetParamRef();
server.StartGame(message->m_InitAttributes);
return true;
}
bool CNetServerWorker::OnLoadedGame(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
CNetServerSession* loadedSession = (CNetServerSession*)context;
CNetServerWorker& server = loadedSession->GetServer();
// We're in the loading state, so wait until every client has loaded
// before starting the game
ENSURE(server.m_State == SERVER_STATE_LOADING);
if (server.CheckGameLoadStatus(loadedSession))
return true;
CClientsLoadingMessage message;
// We always send all GUIDs of clients in the loading state
// so that we don't have to bother about switching GUI pages
for (CNetServerSession* session : server.m_Sessions)
if (session->GetCurrState() != NSS_INGAME && loadedSession->GetGUID() != session->GetGUID())
{
CClientsLoadingMessage::S_m_Clients client;
client.m_GUID = session->GetGUID();
message.m_Clients.push_back(client);
}
// Send to the client who has loaded the game but did not reach the NSS_INGAME state yet
loadedSession->SendMessage(&message);
server.Broadcast(&message, { NSS_INGAME });
return true;
}
bool CNetServerWorker::OnJoinSyncingLoadedGame(void* context, CFsmEvent* event)
{
// A client rejoining an in-progress game has now finished loading the
// map and deserialized the initial state.
// The simulation may have progressed since then, so send any subsequent
// commands to them and set them as an active player so they can participate
// in all future turns.
//
// (TODO: if it takes a long time for them to receive and execute all these
// commands, the other players will get frozen for that time and may be unhappy;
// we could try repeating this process a few times until the client converges
// on the up-to-date state, before setting them as active.)
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CLoadedGameMessage* message = (CLoadedGameMessage*)event->GetParamRef();
u32 turn = message->m_CurrentTurn;
u32 readyTurn = server.m_ServerTurnManager->GetReadyTurn();
// Send them all commands received since their saved state,
// and turn-ended messages for any turns that have already been processed
for (size_t i = turn + 1; i < std::max(readyTurn+1, (u32)server.m_SavedCommands.size()); ++i)
{
if (i < server.m_SavedCommands.size())
for (size_t j = 0; j < server.m_SavedCommands[i].size(); ++j)
session->SendMessage(&server.m_SavedCommands[i][j]);
if (i <= readyTurn)
{
CEndCommandBatchMessage endMessage;
endMessage.m_Turn = i;
endMessage.m_TurnLength = server.m_ServerTurnManager->GetSavedTurnLength(i);
session->SendMessage(&endMessage);
}
}
// Tell the turn manager to expect commands from this new client
// Special case: the controller shouldn't be treated as an observer in any case.
bool isObserver = server.m_PlayerAssignments[session->GetGUID()].m_PlayerID == -1 && server.m_ControllerGUID != session->GetGUID();
server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn, isObserver);
// Tell the client that everything has finished loading and it should start now
CLoadedGameMessage loaded;
loaded.m_CurrentTurn = readyTurn;
session->SendMessage(&loaded);
return true;
}
bool CNetServerWorker::OnRejoined(void* context, CFsmEvent* event)
{
// A client has finished rejoining and the loading screen disappeared.
ENSURE(event->GetType() == (uint)NMT_REJOINED);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
// Inform everyone of the client having rejoined
CRejoinedMessage* message = (CRejoinedMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
server.Broadcast(message, { NSS_INGAME });
// Send all pausing players to the rejoined client.
for (const CStr& guid : server.m_PausingPlayers)
{
CClientPausedMessage pausedMessage;
pausedMessage.m_GUID = guid;
pausedMessage.m_Pause = true;
session->SendMessage(&pausedMessage);
}
return true;
}
bool CNetServerWorker::OnKickPlayer(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_KICKED);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
if (session->GetGUID() == server.m_ControllerGUID)
{
CKickedMessage* message = (CKickedMessage*)event->GetParamRef();
server.KickPlayer(message->m_Name, message->m_Ban);
}
return true;
}
bool CNetServerWorker::OnDisconnect(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CONNECTION_LOST);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
server.OnUserLeave(session);
return true;
}
bool CNetServerWorker::OnClientPaused(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CClientPausedMessage* message = (CClientPausedMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
// Update the list of pausing players.
std::vector::iterator player = std::find(server.m_PausingPlayers.begin(), server.m_PausingPlayers.end(), session->GetGUID());
if (message->m_Pause)
{
if (player != server.m_PausingPlayers.end())
return true;
server.m_PausingPlayers.push_back(session->GetGUID());
}
else
{
if (player == server.m_PausingPlayers.end())
return true;
server.m_PausingPlayers.erase(player);
}
// Send messages to clients that are in game, and are not the client who paused.
for (CNetServerSession* netSession : server.m_Sessions)
if (netSession->GetCurrState() == NSS_INGAME && message->m_GUID != netSession->GetGUID())
netSession->SendMessage(message);
return true;
}
bool CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession)
{
for (const CNetServerSession* session : m_Sessions)
if (session != changedSession && session->GetCurrState() != NSS_INGAME)
return false;
// Inform clients that everyone has loaded the map and that the game can start
CLoadedGameMessage loaded;
loaded.m_CurrentTurn = 0;
// Notice the changedSession is still in the NSS_PREGAME state
Broadcast(&loaded, { NSS_PREGAME, NSS_INGAME });
m_State = SERVER_STATE_INGAME;
return true;
}
void CNetServerWorker::StartGame(const CStr& initAttribs)
{
for (std::pair& player : m_PlayerAssignments)
if (player.second.m_Enabled && player.second.m_PlayerID != -1 && player.second.m_Status == 0)
{
LOGERROR("Tried to start the game without player \"%s\" being ready!", utf8_from_wstring(player.second.m_Name).c_str());
return;
}
m_ServerTurnManager = new CNetServerTurnManager(*this);
for (CNetServerSession* session : m_Sessions)
{
// Special case: the controller shouldn't be treated as an observer in any case.
bool isObserver = m_PlayerAssignments[session->GetGUID()].m_PlayerID == -1 && m_ControllerGUID != session->GetGUID();
m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0, isObserver);
}
m_State = SERVER_STATE_LOADING;
// Remove players and observers that are not present when the game starts
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end();)
if (it->second.m_Enabled)
++it;
else
it = m_PlayerAssignments.erase(it);
SendPlayerAssignments();
// Update init attributes. They should no longer change.
Script::ParseJSON(ScriptRequest(m_ScriptInterface), initAttribs, &m_InitAttributes);
CGameStartMessage gameStart;
gameStart.m_InitAttributes = initAttribs;
Broadcast(&gameStart, { NSS_PREGAME });
}
CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original)
{
const size_t MAX_LENGTH = 32;
CStrW name = original;
name.Replace(L"[", L"{"); // remove GUI tags
name.Replace(L"]", L"}"); // remove for symmetry
// Restrict the length
if (name.length() > MAX_LENGTH)
name = name.Left(MAX_LENGTH);
// Don't allow surrounding whitespace
name.Trim(PS_TRIM_BOTH);
// Don't allow empty name
if (name.empty())
name = L"Anonymous";
return name;
}
CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original)
{
CStrW name = original;
// Try names "Foo", "Foo (2)", "Foo (3)", etc
size_t id = 2;
while (true)
{
bool unique = true;
for (const CNetServerSession* session : m_Sessions)
{
if (session->GetUserName() == name)
{
unique = false;
break;
}
}
if (unique)
return name;
name = original + L" (" + CStrW::FromUInt(id++) + L")";
}
}
void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port)
{
if (m_Host)
StunClient::SendHolePunchingMessages(*m_Host, ipStr, port);
}
CNetServer::CNetServer(bool useLobbyAuth, int autostartPlayers) :
m_Worker(new CNetServerWorker(useLobbyAuth, autostartPlayers)),
m_LobbyAuth(useLobbyAuth), m_UseSTUN(false), m_PublicIp(""), m_PublicPort(20595), m_Password()
{
}
CNetServer::~CNetServer()
{
delete m_Worker;
}
bool CNetServer::GetUseSTUN() const
{
return m_UseSTUN;
}
bool CNetServer::UseLobbyAuth() const
{
return m_LobbyAuth;
}
bool CNetServer::SetupConnection(const u16 port)
{
return m_Worker->SetupConnection(port);
}
CStr CNetServer::GetPublicIp() const
{
return m_PublicIp;
}
u16 CNetServer::GetPublicPort() const
{
return m_PublicPort;
}
u16 CNetServer::GetLocalPort() const
{
std::lock_guard lock(m_Worker->m_WorkerMutex);
if (!m_Worker->m_Host)
return 0;
return m_Worker->m_Host->address.port;
}
void CNetServer::SetConnectionData(const CStr& ip, const u16 port)
{
m_PublicIp = ip;
m_PublicPort = port;
m_UseSTUN = false;
}
bool CNetServer::SetConnectionDataViaSTUN()
{
m_UseSTUN = true;
std::lock_guard lock(m_Worker->m_WorkerMutex);
if (!m_Worker->m_Host)
return false;
return StunClient::FindPublicIP(*m_Worker->m_Host, m_PublicIp, m_PublicPort);
}
-bool CNetServer::CheckPasswordAndIncrement(const CStr& password, const std::string& username)
+bool CNetServer::CheckPasswordAndIncrement(const std::string& username, const std::string& password, const std::string& salt)
{
std::unordered_map::iterator it = m_FailedAttempts.find(username);
- if (m_Password == password)
+ if (m_Worker->CheckPassword(password, salt))
{
if (it != m_FailedAttempts.end())
it->second = 0;
return true;
}
if (it == m_FailedAttempts.end())
m_FailedAttempts.emplace(username, 1);
else
it->second++;
return false;
}
bool CNetServer::IsBanned(const std::string& username) const
{
std::unordered_map::const_iterator it = m_FailedAttempts.find(username);
return it != m_FailedAttempts.end() && it->second >= FAILED_PASSWORD_TRIES_BEFORE_BAN;
}
void CNetServer::SetPassword(const CStr& password)
{
m_Password = password;
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->SetPassword(password);
}
void CNetServer::SetControllerSecret(const std::string& secret)
{
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->SetControllerSecret(secret);
}
void CNetServer::StartGame()
{
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->m_StartGameQueue.push_back(true);
}
void CNetServer::UpdateInitAttributes(JS::MutableHandleValue attrs, const ScriptRequest& rq)
{
// Pass the attributes as JSON, since that's the easiest safe
// cross-thread way of passing script data
std::string attrsJSON = Script::StringifyJSON(rq, attrs, false);
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->m_InitAttributesQueue.push_back(attrsJSON);
}
void CNetServer::OnLobbyAuth(const CStr& name, const CStr& token)
{
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->m_LobbyAuthQueue.push_back(std::make_pair(name, token));
}
void CNetServer::SetTurnLength(u32 msecs)
{
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->m_TurnLengthQueue.push_back(msecs);
}
void CNetServer::SendHolePunchingMessage(const CStr& ip, u16 port)
{
m_Worker->SendHolePunchingMessage(ip, port);
}
Index: ps/trunk/source/network/NetServer.h
===================================================================
--- ps/trunk/source/network/NetServer.h (revision 25458)
+++ ps/trunk/source/network/NetServer.h (revision 25459)
@@ -1,440 +1,442 @@
/* 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
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#ifndef NETSERVER_H
#define NETSERVER_H
#include "NetFileTransfer.h"
#include "NetHost.h"
#include "lib/config2.h"
#include "lib/types.h"
#include "scriptinterface/ScriptTypes.h"
#include
#include
#include
#include
#include
#include
#include
class CNetServerSession;
class CNetServerTurnManager;
class CFsmEvent;
class CPlayerAssignmentMessage;
class CNetStatsTable;
class CSimulationMessage;
class ScriptInterface;
class ScriptRequest;
class CNetServerWorker;
enum NetServerState
{
// We haven't opened the port yet, we're just setting some stuff up.
// The worker thread has not been started.
SERVER_STATE_UNCONNECTED,
// The server is open and accepting connections. This is the screen where
// rules are set up by the operator and where players join and select civs
// and stuff.
SERVER_STATE_PREGAME,
// All the hosts are connected and are loading the game
SERVER_STATE_LOADING,
// The one with all the killing ;-)
SERVER_STATE_INGAME,
// The game is over and someone has won. Players might linger to chat or
// download the replay log.
SERVER_STATE_POSTGAME
};
/**
* Server session representation of client state
*/
enum NetServerSessionState
{
// The client has disconnected or been disconnected
NSS_UNCONNECTED,
// The client has just connected and we're waiting for its handshake message,
// to agree on the protocol version
NSS_HANDSHAKE,
// The client has handshook and we're waiting for its lobby authentication message
NSS_LOBBY_AUTHENTICATE,
// The client has handshook and we're waiting for its authentication message,
// to find its name and check its password etc
NSS_AUTHENTICATE,
// The client has fully joined, and is in the pregame setup stage
// or is loading the game.
// Server must be in SERVER_STATE_PREGAME or SERVER_STATE_LOADING.
NSS_PREGAME,
// The client has authenticated but the game was already started,
// so it's synchronising with the game state from other clients
NSS_JOIN_SYNCING,
// The client is running the game.
// Server must be in SERVER_STATE_LOADING or SERVER_STATE_INGAME.
NSS_INGAME
};
/**
* Network server interface. Handles all the coordination between players.
* One person runs this object, and every player (including the host) connects their CNetClient to it.
*
* The actual work is performed by CNetServerWorker in a separate thread.
*/
class CNetServer
{
NONCOPYABLE(CNetServer);
public:
/**
* Construct a new network server.
* @param autostartPlayers - if positive then StartGame will be called automatically
* once this many players are connected (intended for the command-line testing mode).
*/
CNetServer(bool useLobbyAuth = false, int autostartPlayers = -1);
~CNetServer();
/**
* Begin listening for network connections.
* This function is synchronous (it won't return until the connection is established).
* @return true on success, false on error (e.g. port already in use)
*/
bool SetupConnection(const u16 port);
/**
* Call from the GUI to asynchronously notify all clients that they should start loading the game.
* UpdateInitAttributes must be called at least once.
*/
void StartGame();
/**
* Call from the GUI to update the game setup attributes.
* The changes won't be propagated to clients until game start.
* @param attrs init attributes, in the script context of rq
*/
void UpdateInitAttributes(JS::MutableHandleValue attrs, const ScriptRequest& rq);
/**
* Set the turn length to a fixed value.
* TODO: we should replace this with some adapative lag-dependent computation.
*/
void SetTurnLength(u32 msecs);
bool UseLobbyAuth() const;
void OnLobbyAuth(const CStr& name, const CStr& token);
void SendHolePunchingMessage(const CStr& ip, u16 port);
void SetConnectionData(const CStr& ip, u16 port);
bool SetConnectionDataViaSTUN();
bool GetUseSTUN() const;
/**
* Return the externally accessible IP.
*/
CStr GetPublicIp() const;
/**
* Return the externally accessible port.
*/
u16 GetPublicPort() const;
/**
* Return the serving port on the local machine.
*/
u16 GetLocalPort() const;
/**
* Check if password is valid. If is not, increase number of failed attempts of the lobby user.
* This is used without established direct session with the client, to prevent brute force attacks
* when guessing password trying to get connection data from the host.
* @return true iff password is valid
*/
- bool CheckPasswordAndIncrement(const CStr& password, const std::string& username);
+ bool CheckPasswordAndIncrement(const std::string& username, const std::string& password, const std::string& salt);
/**
* Check if user reached certain number of failed attempts.
* @see m_BanAfterNumberOfTries
* @see CheckPasswordAndBan
*/
bool IsBanned(const std::string& username) const;
void SetPassword(const CStr& password);
void SetControllerSecret(const std::string& secret);
private:
CNetServerWorker* m_Worker;
const bool m_LobbyAuth;
bool m_UseSTUN;
u16 m_PublicPort;
CStr m_PublicIp;
CStr m_Password;
std::unordered_map m_FailedAttempts;
};
/**
* Network server worker thread.
* (This is run in a thread so that client/server communication is not delayed
* by the host player's framerate - the only delay should be the network latency.)
*
* Thread-safety:
* - SetupConnection and constructor/destructor must be called from the main thread.
* - The main thread may push commands onto the Queue members,
* while holding the m_WorkerMutex lock.
* - Public functions (SendMessage, Broadcast) must be called from the network
* server thread.
*/
class CNetServerWorker
{
NONCOPYABLE(CNetServerWorker);
public:
// Public functions for CNetSession/CNetServerTurnManager to use:
/**
* Send a message to the given network peer.
*/
bool SendMessage(ENetPeer* peer, const CNetMessage* message);
/**
* Disconnects a player from gamesetup or session.
*/
void KickPlayer(const CStrW& playerName, const bool ban);
/**
* Send a message to all clients who match one of the given states.
*/
bool Broadcast(const CNetMessage* message, const std::vector& targetStates);
private:
friend class CNetServer;
friend class CNetFileReceiveTask_ServerRejoin;
CNetServerWorker(bool useLobbyAuth, int autostartPlayers);
~CNetServerWorker();
+ bool CheckPassword(const std::string& password, const std::string& salt) const;
+
void SetPassword(const CStr& hashedPassword);
void SetControllerSecret(const std::string& secret);
/**
* Begin listening for network connections.
* @return true on success, false on error (e.g. port already in use)
*/
bool SetupConnection(const u16 port);
/**
* The given GUID will be (re)assigned to the given player ID.
* Any player currently using that ID will be unassigned.
*/
void AssignPlayer(int playerID, const CStr& guid);
/**
* Switch in game mode and notify all clients to start the game.
*/
void StartGame(const CStr& initAttribs);
/**
* Make a player name 'nicer' by limiting the length and removing forbidden characters etc.
*/
static CStrW SanitisePlayerName(const CStrW& original);
/**
* Make a player name unique, if it matches any existing session's name.
*/
CStrW DeduplicatePlayerName(const CStrW& original);
/**
* Get the script context used for init attributes.
*/
const ScriptInterface& GetScriptInterface();
/**
* Set the turn length to a fixed value.
* TODO: we should replace this with some adaptive lag-dependent computation.
*/
void SetTurnLength(u32 msecs);
void ProcessLobbyAuth(const CStr& name, const CStr& token);
void AddPlayer(const CStr& guid, const CStrW& name);
void RemovePlayer(const CStr& guid);
void SendPlayerAssignments();
void ClearAllPlayerReady();
void SetupSession(CNetServerSession* session);
bool HandleConnect(CNetServerSession* session);
void OnUserJoin(CNetServerSession* session);
void OnUserLeave(CNetServerSession* session);
static bool OnClientHandshake(void* context, CFsmEvent* event);
static bool OnAuthenticate(void* context, CFsmEvent* event);
static bool OnSimulationCommand(void* context, CFsmEvent* event);
static bool OnSyncCheck(void* context, CFsmEvent* event);
static bool OnEndCommandBatch(void* context, CFsmEvent* event);
static bool OnChat(void* context, CFsmEvent* event);
static bool OnReady(void* context, CFsmEvent* event);
static bool OnClearAllReady(void* context, CFsmEvent* event);
static bool OnGameSetup(void* context, CFsmEvent* event);
static bool OnAssignPlayer(void* context, CFsmEvent* event);
static bool OnGameStart(void* context, CFsmEvent* event);
static bool OnLoadedGame(void* context, CFsmEvent* event);
static bool OnJoinSyncingLoadedGame(void* context, CFsmEvent* event);
static bool OnRejoined(void* context, CFsmEvent* event);
static bool OnKickPlayer(void* context, CFsmEvent* event);
static bool OnDisconnect(void* context, CFsmEvent* event);
static bool OnClientPaused(void* context, CFsmEvent* event);
/**
* Checks if all clients have finished loading.
* If so informs the clients about that and change the server state.
*
* Returns if all clients finished loading.
*/
bool CheckGameLoadStatus(CNetServerSession* changedSession);
void ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message);
void HandleMessageReceive(const CNetMessage* message, CNetServerSession* session);
/**
* Send a network warning if the connection to a client is being lost or has bad latency.
*/
void CheckClientConnections();
void SendHolePunchingMessage(const CStr& ip, u16 port);
/**
* Internal script context for (de)serializing script messages,
* and for storing init attributes.
* (TODO: we shouldn't bother deserializing (except for debug printing of messages),
* we should just forward messages blindly and efficiently.)
*/
ScriptInterface* m_ScriptInterface;
PlayerAssignmentMap m_PlayerAssignments;
/**
* Stores the most current init attributes.
* NB: this is not guaranteed to be up-to-date until the server is LOADING or INGAME.
* At that point, the settings are frozen and ought to be identical to the simulation Init Attributes.
*/
JS::PersistentRootedValue m_InitAttributes;
int m_AutostartPlayers;
/**
* Whether this match requires lobby authentication.
*/
const bool m_LobbyAuth;
ENetHost* m_Host;
std::vector m_Sessions;
CNetStatsTable* m_Stats;
NetServerState m_State;
CStrW m_ServerName;
std::vector m_BannedIPs;
std::vector m_BannedPlayers;
CStr m_Password;
/**
* Holds the GUIDs of all currently paused players.
*/
std::vector m_PausingPlayers;
u32 m_NextHostID;
CNetServerTurnManager* m_ServerTurnManager;
/**
* The GUID of the client in control of the game (the 'host' from the players' perspective).
*/
CStr m_ControllerGUID;
/**
* The 'secret' used to identify the controller of the game.
*/
std::string m_ControllerSecret;
/**
* A copy of all simulation commands received so far, indexed by
* turn number, to simplify support for rejoining etc.
* TODO: verify this doesn't use too much RAM.
*/
std::vector> m_SavedCommands;
/**
* The latest copy of the simulation state, received from an existing
* client when a new client has asked to rejoin the game.
*/
std::string m_JoinSyncFile;
/**
* Time when the clients connections were last checked for timeouts and latency.
*/
std::time_t m_LastConnectionCheck;
private:
// Thread-related stuff:
#if CONFIG2_MINIUPNPC
/**
* Try to find a UPnP root on the network and setup port forwarding.
*/
static void SetupUPnP();
std::thread m_UPnPThread;
#endif
static void RunThread(CNetServerWorker* data);
void Run();
bool RunStep();
std::thread m_WorkerThread;
std::mutex m_WorkerMutex;
// protected by m_WorkerMutex
bool m_Shutdown;
// Queues for messages sent by the game thread (protected by m_WorkerMutex):
std::vector m_StartGameQueue;
std::vector m_InitAttributesQueue;
std::vector> m_LobbyAuthQueue;
std::vector m_TurnLengthQueue;
};
/// Global network server for the standard game
extern CNetServer *g_NetServer;
#endif // NETSERVER_H
Index: ps/trunk/source/network/scripting/JSInterface_Network.cpp
===================================================================
--- ps/trunk/source/network/scripting/JSInterface_Network.cpp (revision 25458)
+++ ps/trunk/source/network/scripting/JSInterface_Network.cpp (revision 25459)
@@ -1,305 +1,298 @@
/* 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
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "JSInterface_Network.h"
#include "lib/external_libraries/enet.h"
#include "lib/external_libraries/libsdl.h"
#include "lib/types.h"
#include "lobby/IXmppClient.h"
#include "network/NetClient.h"
#include "network/NetMessage.h"
#include "network/NetServer.h"
#include "network/StunClient.h"
#include "ps/CLogger.h"
+#include "ps/CStr.h"
#include "ps/Game.h"
#include "ps/GUID.h"
+#include "ps/Hashing.h"
+#include "ps/Pyrogenesis.h"
#include "ps/Util.h"
#include "scriptinterface/FunctionWrapper.h"
#include "scriptinterface/StructuredClone.h"
#include "scriptinterface/JSON.h"
#include "third_party/encryption/pkcs5_pbkdf2.h"
namespace JSI_Network
{
u16 GetDefaultPort()
{
return PS_DEFAULT_PORT;
}
bool IsNetController()
{
return !!g_NetClient && g_NetClient->IsController();
}
bool HasNetServer()
{
return !!g_NetServer;
}
bool HasNetClient()
{
return !!g_NetClient;
}
-CStr HashPassword(const CStr& password)
-{
- 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 StartNetworkHost(const ScriptRequest& rq, const CStrW& playerName, const u16 serverPort, 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.
bool hasLobby = !!g_XmppClient;
g_NetServer = new CNetServer(hasLobby);
if (!g_NetServer->SetupConnection(serverPort))
{
ScriptException::Raise(rq, "Failed to start server");
SAFE_DELETE(g_NetServer);
return;
}
// In lobby, we send our public ip and port on request to the players who want to connect.
// Thus we need to know our public IP. Use STUN if that's available,
// otherwise, the lobby's reponse to the game registration stanza will tell us our public IP.
if (hasLobby)
{
if (!useSTUN)
// Don't store IP - the lobby bot will send it later.
// (if a client tries to connect before it's setup, they'll be disconnected)
g_NetServer->SetConnectionData("", serverPort);
else if (!g_NetServer->SetConnectionDataViaSTUN())
{
ScriptException::Raise(rq, "Failed to host via STUN.");
SAFE_DELETE(g_NetServer);
return;
}
}
// Generate a secret to identify the host client.
std::string secret = ps_generate_guid();
-
- // We will get hashed password from clients, so hash it once for server
- CStr hashedPass = HashPassword(password);
- g_NetServer->SetPassword(hashedPass);
g_NetServer->SetControllerSecret(secret);
g_Game = new CGame(true);
g_NetClient = new CNetClient(g_Game);
g_NetClient->SetUserName(playerName);
+
if (hasLobby)
- g_NetClient->SetHostJID(g_XmppClient->GetJID());
- g_NetClient->SetGamePassword(hashedPass);
+ {
+ CStr hostJID = g_XmppClient->GetJID();
+
+ /**
+ * Password security - we want 0 A.D. to protect players from malicious hosts. We assume that clients
+ * might mistakenly send a personal password instead of the game password (e.g. enter their mail account's password on autopilot).
+ * Malicious dedicated servers might be set up to farm these failed logins and possibly obtain user credentials.
+ * Therefore, we hash the passwords on the client side before sending them to the server.
+ * This still makes the passwords potentially recoverable, but makes it much harder at scale.
+ * To prevent the creation of rainbow tables, hash with:
+ * - the host name
+ * - the client name (this makes rainbow tables completely unworkable unless a specific user is targeted,
+ * but that would require both computing the matching rainbow table _and_ for that specific user to mistype a personal password,
+ * at which point we assume the attacker would/could probably just rather use another means of obtaining the password).
+ * - the password itself
+ * - the engine version (so that the hashes change periodically)
+ * TODO: it should be possible to implement SRP or something along those lines to completely protect from this,
+ * but the cost/benefit ratio is probably not worth it.
+ */
+ CStr hashedPass = HashCryptographically(password, hostJID + password + engine_version);
+ g_NetServer->SetPassword(hashedPass);
+ g_NetClient->SetHostJID(hostJID);
+ g_NetClient->SetGamePassword(hashedPass);
+ }
+
g_NetClient->SetupServerData("127.0.0.1", serverPort, false);
g_NetClient->SetControllerSecret(secret);
if (!g_NetClient->SetupConnection(nullptr))
{
ScriptException::Raise(rq, "Failed to connect to server");
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_Game);
}
}
void StartNetworkJoin(const ScriptRequest& rq, const CStrW& playerName, const CStr& serverAddress, u16 serverPort)
{
ENSURE(!g_NetClient);
ENSURE(!g_NetServer);
ENSURE(!g_Game);
g_Game = new CGame(true);
g_NetClient = new CNetClient(g_Game);
g_NetClient->SetUserName(playerName);
g_NetClient->SetupServerData(serverAddress, serverPort, false);
if (!g_NetClient->SetupConnection(nullptr))
{
ScriptException::Raise(rq, "Failed to connect to server");
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_Game);
}
}
/**
* 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(const CStrW& playerName, const CStr& hostJID, const CStr& password)
{
ENSURE(!!g_XmppClient);
ENSURE(!g_NetClient);
ENSURE(!g_NetServer);
ENSURE(!g_Game);
- CStr hashedPass = HashPassword(password);
+ CStr hashedPass = HashCryptographically(password, hostJID + password + engine_version);
g_Game = new CGame(true);
g_NetClient = new CNetClient(g_Game);
g_NetClient->SetUserName(playerName);
g_NetClient->SetHostJID(hostJID);
g_NetClient->SetGamePassword(hashedPass);
- g_XmppClient->SendIqGetConnectionData(hostJID, hashedPass.c_str(), false);
+ g_NetClient->SetupConnectionViaLobby();
}
void DisconnectNetworkGame()
{
// TODO: we ought to do async reliable disconnections
SAFE_DELETE(g_NetServer);
SAFE_DELETE(g_NetClient);
SAFE_DELETE(g_Game);
}
CStr GetPlayerGUID()
{
if (!g_NetClient)
return "local";
return g_NetClient->GetGUID();
}
JS::Value PollNetworkClient(const ScriptInterface& guiInterface)
{
if (!g_NetClient)
return JS::UndefinedValue();
// Convert from net client context to GUI script context
ScriptRequest rqNet(g_NetClient->GetScriptInterface());
JS::RootedValue pollNet(rqNet.cx);
g_NetClient->GuiPoll(&pollNet);
return Script::CloneValueFromOtherCompartment(guiInterface, g_NetClient->GetScriptInterface(), pollNet);
}
void SendGameSetupMessage(const ScriptInterface& scriptInterface, JS::HandleValue attribs1)
{
ENSURE(g_NetClient);
// TODO: This is a workaround because we need to pass a MutableHandle to a JSAPI functions somewhere (with no obvious reason).
ScriptRequest rq(scriptInterface);
JS::RootedValue attribs(rq.cx, attribs1);
g_NetClient->SendGameSetupMessage(&attribs, scriptInterface);
}
void AssignNetworkPlayer(int playerID, const CStr& guid)
{
ENSURE(g_NetClient);
g_NetClient->SendAssignPlayerMessage(playerID, guid);
}
void KickPlayer(const CStrW& playerName, bool ban)
{
ENSURE(g_NetClient);
g_NetClient->SendKickPlayerMessage(playerName, ban);
}
void SendNetworkChat(const CStrW& message)
{
ENSURE(g_NetClient);
g_NetClient->SendChatMessage(message);
}
void SendNetworkReady(int message)
{
ENSURE(g_NetClient);
g_NetClient->SendReadyMessage(message);
}
void ClearAllPlayerReady ()
{
ENSURE(g_NetClient);
g_NetClient->SendClearAllReadyMessage();
}
void StartNetworkGame(const ScriptInterface& scriptInterface, JS::HandleValue attribs1)
{
ENSURE(g_NetClient);
// TODO: This is a workaround because we need to pass a MutableHandle to a JSAPI functions somewhere (with no obvious reason).
ScriptRequest rq(scriptInterface);
JS::RootedValue attribs(rq.cx, attribs1);
g_NetClient->SendStartGameMessage(Script::StringifyJSON(rq, &attribs));
}
void SetTurnLength(int length)
{
if (g_NetServer)
g_NetServer->SetTurnLength(length);
else
LOGERROR("Only network host can change turn length");
}
void RegisterScriptFunctions(const ScriptRequest& rq)
{
ScriptFunction::Register<&GetDefaultPort>(rq, "GetDefaultPort");
ScriptFunction::Register<&IsNetController>(rq, "IsNetController");
ScriptFunction::Register<&HasNetServer>(rq, "HasNetServer");
ScriptFunction::Register<&HasNetClient>(rq, "HasNetClient");
ScriptFunction::Register<&StartNetworkHost>(rq, "StartNetworkHost");
ScriptFunction::Register<&StartNetworkJoin>(rq, "StartNetworkJoin");
ScriptFunction::Register<&StartNetworkJoinLobby>(rq, "StartNetworkJoinLobby");
ScriptFunction::Register<&DisconnectNetworkGame>(rq, "DisconnectNetworkGame");
ScriptFunction::Register<&GetPlayerGUID>(rq, "GetPlayerGUID");
ScriptFunction::Register<&PollNetworkClient>(rq, "PollNetworkClient");
ScriptFunction::Register<&SendGameSetupMessage>(rq, "SendGameSetupMessage");
ScriptFunction::Register<&AssignNetworkPlayer>(rq, "AssignNetworkPlayer");
ScriptFunction::Register<&KickPlayer>(rq, "KickPlayer");
ScriptFunction::Register<&SendNetworkChat>(rq, "SendNetworkChat");
ScriptFunction::Register<&SendNetworkReady>(rq, "SendNetworkReady");
ScriptFunction::Register<&ClearAllPlayerReady>(rq, "ClearAllPlayerReady");
ScriptFunction::Register<&StartNetworkGame>(rq, "StartNetworkGame");
ScriptFunction::Register<&SetTurnLength>(rq, "SetTurnLength");
}
}
Index: ps/trunk/source/ps/Hashing.cpp
===================================================================
--- ps/trunk/source/ps/Hashing.cpp (nonexistent)
+++ ps/trunk/source/ps/Hashing.cpp (revision 25459)
@@ -0,0 +1,59 @@
+/* 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
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 0 A.D. is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with 0 A.D. If not, see .
+ */
+#include "precompiled.h"
+
+#include "ps/CStr.h"
+#include "ps/Util.h"
+
+#include
+
+CStr8 HashCryptographically(const CStr8& string, const CStr8& salt)
+{
+ if (string.empty())
+ return string;
+
+ ENSURE(sodium_init() >= 0);
+
+ constexpr int SALTSIZE = crypto_pwhash_SALTBYTES;
+ static_assert(SALTSIZE >= crypto_generichash_BYTES_MIN);
+ static_assert(SALTSIZE <= crypto_generichash_BYTES_MAX);
+ static_assert(SALTSIZE >= crypto_generichash_KEYBYTES_MIN);
+ static_assert(SALTSIZE <= crypto_generichash_KEYBYTES_MAX);
+
+ // First generate a fixed-size salt from out variable-sized one (libsodium requires it).
+ unsigned char salt_buffer[SALTSIZE] = {
+ 235, 82, 29, 20, 135, 168, 184, 97, 7, 240, 48, 109, 8, 34, 158, 32,
+ };
+ crypto_generichash_state state;
+ crypto_generichash_init(&state, salt_buffer, SALTSIZE, SALTSIZE);
+ crypto_generichash_update(&state, reinterpret_cast(salt.c_str()), salt.size());
+ crypto_generichash_final(&state, salt_buffer, SALTSIZE);
+
+ constexpr int HASHSIZE = 32;
+ static_assert(HASHSIZE >= crypto_pwhash_BYTES_MIN);
+ static_assert(HASHSIZE <= crypto_pwhash_BYTES_MAX);
+
+ // Now that we have a fixed-length key, use that to hash the password.
+ unsigned char output[HASHSIZE] = { 0 };
+ // For HashCryptographically, we use 'fast' parameters, corresponding to low values.
+ // These parameters must not change, or hashes will change, hence why the #defined values are copied.
+ constexpr size_t memLimit = 8192 * 4; // 4 * crypto_pwhash_argon2id_MEMLIMIT_MIN
+ constexpr size_t opsLimit = 2; // crypto_pwhash_argon2id_OPSLIMIT_INTERACTIVE
+ ENSURE(crypto_pwhash(output, HASHSIZE, string.c_str(), string.size(), salt_buffer, opsLimit, memLimit, crypto_pwhash_ALG_ARGON2ID13) == 0);
+
+ return CStr(Hexify(output, HASHSIZE)).UpperCase();
+}
Property changes on: ps/trunk/source/ps/Hashing.cpp
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/source/ps/Hashing.h
===================================================================
--- ps/trunk/source/ps/Hashing.h (nonexistent)
+++ ps/trunk/source/ps/Hashing.h (revision 25459)
@@ -0,0 +1,32 @@
+/* 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
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 0 A.D. is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with 0 A.D. If not, see .
+ */
+
+#ifndef INCLUDED_HASHING
+#define INCLUDED_HASHING
+
+class CStr8;
+
+/**
+ * Hash a string in a cryptographically secure manner.
+ * This method is intended to be 'somewhat fast' for password hashing,
+ * and should neither be used where a fast real-time hash is wanted,
+ * nor for more sensitive passwords.
+ * @return a hex-encoded string.
+ */
+CStr8 HashCryptographically(const CStr8& password, const CStr8& salt);
+
+#endif
Property changes on: ps/trunk/source/ps/Hashing.h
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/source/ps/tests/test_Hashing.h
===================================================================
--- ps/trunk/source/ps/tests/test_Hashing.h (nonexistent)
+++ ps/trunk/source/ps/tests/test_Hashing.h (revision 25459)
@@ -0,0 +1,57 @@
+/* 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
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 0 A.D. is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with 0 A.D. If not, see .
+ */
+
+#include "lib/self_test.h"
+
+#include "ps/Hashing.h"
+
+class TestHashing : public CxxTest::TestSuite
+{
+public:
+ void test_hash_cryptographically()
+ {
+ // Simple test: these should be deterministic and no collision on these trivial cases
+ TS_ASSERT_EQUALS(HashCryptographically("", ""), "");
+ TS_ASSERT_EQUALS(HashCryptographically("", "foo"), "");
+
+ TS_ASSERT_EQUALS(HashCryptographically("pass", ""), "CFD946EEBCC23A1642BD846FF54B3659765D305D352D6C590DCCE0728BAAE360");
+ TS_ASSERT_EQUALS(HashCryptographically("pass", "foo"), "7E18AD648A3BE29EC513551D54AFD2505F9726EE14750BCC92F979D41A3328D3");
+ TS_ASSERT_EQUALS(HashCryptographically("pass", "foofoo"), "5CCF3FCD4A285A133F19461ADE1819C838E16D9C1BED3B18276B5F5EBF57E172");
+ TS_ASSERT_EQUALS(HashCryptographically("pass", "bar"), "5195DE5588213B1A07BCA48BB43050EE2C1FD99DC37E243B313D3E12A6AAFFD8");
+ TS_ASSERT_EQUALS(HashCryptographically("pass", "foobar"), "E63C16BBE04E806DC54032A0D4BAABB96A99E0DA695357035E23C83A4E20E718");
+ TS_ASSERT_EQUALS(HashCryptographically("pass", ""), "CFD946EEBCC23A1642BD846FF54B3659765D305D352D6C590DCCE0728BAAE360");
+
+ TS_ASSERT_EQUALS(HashCryptographically("passpass", ""), "68FCE509D0B68EC7142D28165E6D697E26FEB929FCC1FE70ED4D0A8C716F7E56");
+ TS_ASSERT_EQUALS(HashCryptographically("passpass", "foo"), "B766D8DB7AD9D110ED6059BAC6E3667486609AE193FF62ADB4EE174AC665F6F8");
+ TS_ASSERT_EQUALS(HashCryptographically("passpass", "foofoo"), "0BDD6EE3B37FB7B6B4AA24F24AD148CED9BC26793B5EDBF68598800F5F53FD77");
+ TS_ASSERT_EQUALS(HashCryptographically("passpass", "bar"), "BB719CDF8E5E0505AFEEABC487BE4A2A2EE83683DEC6BFD5A08C2E6C308A51C2");
+ TS_ASSERT_EQUALS(HashCryptographically("passpass", "foobar"), "6745DD30BAD7B7A78BC0DC559C684CD4A5E13AD538CDE23D75577B61943D3DC1");
+
+ // Test that hashing hashes works.
+ TS_ASSERT_EQUALS(HashCryptographically("A989A9C5BDB02DD91C038661424BE039E2AE727483A30D3F13F995D0AB6C3712", "foobar"), "9509646C4675EED47E9D49AF20456F3F08605E87CA825DD44A846F5E3E3AC02F");
+ TS_ASSERT_EQUALS(HashCryptographically("D9895FDEE287DBEE19907B7329207F388B1708AC4A123CA537603E953885B20F", "foobar"), "8CE4D45113D5A682FE4B6F185C1880F83EEA6CB2F007E815DCA5BF4B8178ECD0");
+ }
+
+ void test_hash_perf_DISABLED()
+ {
+ double t = timer_Time();
+ for (size_t i = 0; i < 100; ++i)
+ HashCryptographically("somePasswordValue", reinterpret_cast(&i));
+ double total = timer_Time() - t;
+ printf("Time: %lfs\n", total);
+ }
+};
Property changes on: ps/trunk/source/ps/tests/test_Hashing.h
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property