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