1
0
forked from 0ad/0ad

Secure lobby authentication - prevent joins as a different player

Reviewed by: Dunedan, elexis, Itms
Fixes #3549
Differential Revision: https://code.wildfiregames.com/D897
This was SVN commit r21520.
This commit is contained in:
Imarok 2018-03-12 00:23:40 +00:00
parent 520f70ab2c
commit 0fd8aa2a77
20 changed files with 318 additions and 56 deletions

View File

@ -409,6 +409,7 @@ server = "lobby.wildfiregames.com" ; Address of lobby server
xpartamupp = "wfgbot23" ; Name of the server-side xmpp client that manage games
buddies = "," ; Comma separated list of playernames that the current user has marked as buddies
rememberpassword = true ; Whether to store the encrypted password in the user config
secureauth = true ; Secure Lobby Authentication: This prevents the impersonation of other players. The lobby server confirms the identity of the player before they join.
[lobby.columns]
gamerating = false ; Show the average rating of the participating players in a column of the gamelist

View File

@ -73,6 +73,7 @@ function getDisconnectReason(id, wasConnected)
case 6: return translate("You have been banned");
case 7: return translate("Playername in use. If you were disconnected, retry in few seconds");
case 8: return translate("Server full");
case 9: return translate("Secure lobby authentication failed. Join via lobby");
default:
warn("Unknown disconnect-reason ID received: " + id);
return sprintf(translate("\\[Invalid value %(id)s]"), { "id": id });

View File

@ -48,6 +48,7 @@ function init(attribs)
case "host":
{
Engine.GetGUIObjectByName("hostSTUNWrapper").hidden = !Engine.HasXmppClient();
Engine.GetGUIObjectByName("hostLobbyAuthWrapper").hidden = !Engine.HasXmppClient();
if (Engine.HasXmppClient())
{
Engine.GetGUIObjectByName("hostPlayerName").caption = attribs.name;
@ -55,6 +56,7 @@ function init(attribs)
sprintf(translate("%(name)s's game"), { "name": attribs.name });
Engine.GetGUIObjectByName("useSTUN").checked = Engine.ConfigDB_GetValue("user", "lobby.stun.enabled") == "true";
Engine.GetGUIObjectByName("useLobbyAuth").checked = Engine.ConfigDB_GetValue("user", "lobby.secureauth") == "true";
}
switchSetupPage("pageHost");
@ -260,10 +262,20 @@ function pollAndHandleNetworkClient()
function switchSetupPage(newPage)
{
for (let page of Engine.GetGUIObjectByName("multiplayerPages").children)
if (page.name.substr(0, 4) == "page")
let multiplayerPages = Engine.GetGUIObjectByName("multiplayerPages");
for (let page of multiplayerPages.children)
if (page.name.startsWith("page"))
page.hidden = true;
if (newPage == "pageJoin" || newPage == "pageHost")
{
let pageSize = multiplayerPages.size;
let halfHeight = newPage == "pageJoin" ? 130 : Engine.HasXmppClient() ? 145 : 110;
pageSize.top = -halfHeight;
pageSize.bottom = halfHeight;
multiplayerPages.size = pageSize;
}
Engine.GetGUIObjectByName(newPage).hidden = false;
Engine.GetGUIObjectByName("hostPlayerNameWrapper").hidden = Engine.HasXmppClient();
@ -302,12 +314,10 @@ function startHost(playername, servername, port)
}
}
let useLobbyAuth = Engine.HasXmppClient() && Engine.GetGUIObjectByName("useLobbyAuth").checked;
try
{
if (g_UserRating)
Engine.StartNetworkHost(playername + " (" + g_UserRating + ")", port);
else
Engine.StartNetworkHost(playername, port);
Engine.StartNetworkHost(playername + (g_UserRating ? " (" + g_UserRating + ")" : ""), port, playername, useLobbyAuth);
}
catch (e)
{

View File

@ -106,17 +106,27 @@
</object>
</object>
<object name="hostSTUNWrapper">
<object name="useSTUN" size="120 116 152 146" type="checkbox" style="ModernTickBox">
<object name="hostSTUNWrapper" size="120 106 100% 146">
<object name="useSTUN" size="0 10 32 100%" type="checkbox" style="ModernTickBox">
<action on="Press">saveSettingAndWriteToUserConfig("lobby.stun.enabled", String(this.checked));</action>
</object>
<object type="text" size="146 106 100% 146" style="ModernLeftLabelText">
<object type="text" size="26 0 100% 100%" style="ModernLeftLabelText">
<translatableAttribute id="caption">Use STUN to work around firewalls</translatableAttribute>
</object>
</object>
<object name="hostLobbyAuthWrapper" size="120 141 100% 181">
<object name="useLobbyAuth" size="0 10 32 100%" type="checkbox" style="ModernTickBox">
<action on="Press">saveSettingAndWriteToUserConfig("lobby.secureauth", String(this.checked));</action>
</object>
<object type="text" size="26 0 100% 100%" style="ModernLeftLabelText">
<translatableAttribute id="caption">Require Lobby Authentication</translatableAttribute>
<translatableAttribute id="tooltip">This prevents the impersonation of other players. The lobby server confirms the identity of the player before they join.</translatableAttribute>
</object>
</object>
</object>
<object name="hostFeedback" type="text" style="ModernLabelText" size="50 100%-80 100%-50 100%-50" textcolor="red"/>
<object name="hostFeedback" type="text" style="ModernLabelText" size="50 100%-80 100%-50 100%-45" textcolor="red"/>
<object name="continueButton" hotkey="confirm" type="button" size="50%+5 100%-45 100%-18 100%-17" style="ModernButtonRed">
<translatableAttribute id="caption">Continue</translatableAttribute>

View File

@ -41,6 +41,7 @@ public:
virtual void SendIqRegisterGame(const ScriptInterface& scriptInterface, JS::HandleValue data) = 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 void GetNick(std::string& nick) = 0;
virtual void kick(const std::string& nick, const std::string& reason) = 0;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015 Wildfire Games.
/* Copyright (C) 2018 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -123,13 +123,13 @@ BoardListQuery::~BoardListQuery()
* the listing of games from the server, and register/
* unregister/changestate games on the server.
*/
GameListQuery::GameListQuery( const glooxwrapper::Tag* tag )
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/game" );
const glooxwrapper::Tag* c = tag->findTag_clone("query/game");
if (c)
m_Command = c->cdata();
glooxwrapper::Tag::free(c);
@ -238,3 +238,48 @@ ProfileQuery::~ProfileQuery()
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();
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015 Wildfire Games.
/* Copyright (C) 2018 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -35,6 +35,10 @@
#define EXTPROFILEQUERY 1406
#define XMLNS_PROFILE "jabber:iq:profile"
/// Global Lobby Authentication Extension
#define EXTLOBBYAUTH 1407
#define XMLNS_LOBBYAUTH "jabber:iq:lobbyauth"
class GameReport : public glooxwrapper::StanzaExtension
{
public:
@ -111,4 +115,21 @@ public:
glooxwrapper::string m_Command;
std::vector<const glooxwrapper::Tag*> 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

View File

@ -75,24 +75,23 @@ IXmppClient* IXmppClient::create(const std::string& sUsername, const std::string
* @param regOpt If we are just registering or not.
*/
XmppClient::XmppClient(const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize, bool regOpt)
: m_client(NULL), m_mucRoom(NULL), m_registration(NULL), m_username(sUsername), m_password(sPassword), m_nick(sNick), m_initialLoadComplete(false), m_isConnected(false), m_sessionManager()
: m_client(NULL), m_mucRoom(NULL), m_registration(NULL), m_username(sUsername), m_password(sPassword), m_room(sRoom), m_nick(sNick), m_initialLoadComplete(false), m_isConnected(false), m_sessionManager()
{
// Read lobby configuration from default.cfg
std::string sServer;
std::string sXpartamupp;
CFG_GET_VAL("lobby.server", sServer);
CFG_GET_VAL("lobby.server", m_server);
CFG_GET_VAL("lobby.xpartamupp", sXpartamupp);
m_xpartamuppId = sXpartamupp + "@" + sServer + "/CC";
glooxwrapper::JID clientJid(sUsername + "@" + sServer + "/0ad");
glooxwrapper::JID roomJid(sRoom + "@conference." + sServer + "/" + sNick);
m_xpartamuppId = sXpartamupp + "@" + m_server + "/CC";
glooxwrapper::JID clientJid(sUsername + "@" + m_server + "/0ad");
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(sServer);
m_client = new glooxwrapper::Client(m_server);
// Disable TLS as we haven't set a certificate on the server yet
m_client->setTls(gloox::TLSDisabled);
@ -118,6 +117,9 @@ XmppClient::XmppClient(const std::string& sUsername, const std::string& sPasswor
m_client->registerStanzaExtension(new ProfileQuery());
m_client->registerIqHandler(this, EXTPROFILEQUERY);
m_client->registerStanzaExtension(new LobbyAuth());
m_client->registerIqHandler(this, EXTLOBBYAUTH);
m_client->registerMessageHandler(this);
// Uncomment to see the raw stanzas
@ -416,6 +418,25 @@ void XmppClient::SendIqChangeStateGame(const std::string& nbp, const std::string
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 + "@" + m_server + "/0ad");
glooxwrapper::IQ iq(gloox::IQ::Set, clientJid, m_client->getID());
iq.addExtension(auth);
DbgXMPP("SendIqLobbyAuth [" << tag_xml(iq) << "]");
m_client->send(iq);
}
/*****************************************************
* Account registration *
*****************************************************/
@ -742,6 +763,20 @@ bool XmppClient::handleIq(const glooxwrapper::IQ& iq)
CreateGUIMessage("game", "profile");
}
}
else if (iq.subtype() == gloox::IQ::Set)
{
const LobbyAuth* lobbyAuth = iq.findExtension<LobbyAuth>(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 if (iq.subtype() == gloox::IQ::Error)
CreateGUIMessage("system", "error", "text", StanzaErrorToString(iq.error_error()));
else

View File

@ -47,6 +47,8 @@ private:
// 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;
@ -70,6 +72,7 @@ public:
void SendIqRegisterGame(const ScriptInterface& scriptInterface, JS::HandleValue data);
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);
void GetNick(std::string& nick);
void kick(const std::string& nick, const std::string& reason);

View File

@ -480,6 +480,15 @@ gloox::IQ::IqType glooxwrapper::IQ::subtype() const
return m_Wrapped->subtype();
}
const glooxwrapper::string glooxwrapper::IQ::id() const
{
return m_Wrapped->id();
}
const gloox::JID& glooxwrapper::IQ::from() const
{
return m_Wrapped->from();
}
glooxwrapper::JID::JID()
{

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2017 Wildfire Games.
/* Copyright (C) 2018 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -462,6 +462,8 @@ namespace glooxwrapper
}
gloox::IQ::IqType subtype() const;
const string id() const;
const gloox::JID& from() const;
gloox::StanzaError error_error() const; // wrapper for ->error()->error()
Tag* tag() const;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2017 Wildfire Games.
/* Copyright (C) 2018 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -26,6 +26,7 @@
#include "lib/byte_order.h"
#include "lib/external_libraries/enet.h"
#include "lib/sysdep/sysdep.h"
#include "lobby/IXmppClient.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/Compress.h"
@ -88,6 +89,7 @@ CNetClient::CNetClient(CGame* game, bool isLocalClient) :
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_INITIAL_GAMESETUP, (void*)&OnAuthenticate, context);
AddTransition(NCS_INITIAL_GAMESETUP, (uint)NMT_GAME_SETUP, NCS_PREGAME, (void*)&OnGameSetup, context);
@ -158,6 +160,11 @@ void CNetClient::SetUserName(const CStrW& username)
m_UserName = username;
}
void CNetClient::SetHostingPlayerName(const CStr& hostingPlayerName)
{
m_HostingPlayerName = hostingPlayerName;
}
bool CNetClient::SetupConnection(const CStr& server, const u16 port, ENetHost* enetClient)
{
CNetClientSession* session = new CNetClientSession(*this);
@ -476,6 +483,15 @@ void CNetClient::LoadFinished()
SendMessage(&loaded);
}
void CNetClient::SendAuthenticateMessage()
{
CAuthenticateMessage authenticate;
authenticate.m_Name = m_UserName;
authenticate.m_Password = L""; // TODO
authenticate.m_IsLocalClient = m_IsLocalClient;
SendMessage(&authenticate);
}
bool CNetClient::OnConnect(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CONNECT_COMPLETE);
@ -516,12 +532,34 @@ bool CNetClient::OnHandshakeResponse(void* context, CFsmEvent* event)
CSrvHandshakeResponseMessage* message = (CSrvHandshakeResponseMessage*)event->GetParamRef();
client->m_GUID = message->m_GUID;
CAuthenticateMessage authenticate;
authenticate.m_Name = client->m_UserName;
authenticate.m_Password = L""; // TODO
authenticate.m_IsLocalClient = client->m_IsLocalClient;
client->SendMessage(&authenticate);
if (message->m_Flags & PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH)
{
if (g_XmppClient && !client->m_HostingPlayerName.empty())
g_XmppClient->SendIqLobbyAuth(client->m_HostingPlayerName, client->m_GUID);
else
{
JSContext* cx = client->GetScriptInterface().GetContext();
JSAutoRequest rq(cx);
JS::RootedValue msg(cx);
client->GetScriptInterface().Eval("({'type':'netstatus','status':'disconnected'})", &msg);
client->GetScriptInterface().SetProperty(msg, "reason", (int)NDR_LOBBY_AUTH_FAILED, false);
client->PushGuiMessage(msg);
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 = (CNetClient*)context;
client->SendAuthenticateMessage();
return true;
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2017 Wildfire Games.
/* Copyright (C) 2018 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -90,6 +90,12 @@ public:
*/
void SetUserName(const CStrW& username);
/**
* Set the name of the hosting player.
* This is needed for the secure lobby authentication.
*/
void SetHostingPlayerName(const CStr& hostingPlayerName);
/**
* Returns the GUID of the local client.
* Used for distinguishing observers.
@ -218,10 +224,14 @@ public:
void SendPausedMessage(bool pause);
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);
@ -251,6 +261,7 @@ private:
CGame *m_Game;
CStrW m_UserName;
CStr m_HostingPlayerName;
/// Current network session (or NULL if not connected)
CNetClientSession* m_Session;

View File

@ -67,7 +67,8 @@ enum NetDisconnectReason
NDR_KICKED,
NDR_BANNED,
NDR_PLAYERNAME_IN_USE,
NDR_SERVER_FULL
NDR_SERVER_FULL,
NDR_LOBBY_AUTH_FAILED
};
class CNetHost

View File

@ -26,10 +26,13 @@
#include "ps/CStr.h"
#include "scriptinterface/ScriptVal.h"
#define PS_PROTOCOL_MAGIC 0x5073013f // 'P', 's', 0x01, '?'
#define PS_PROTOCOL_MAGIC_RESPONSE 0x50630121 // 'P', 'c', 0x01, '!'
#define PS_PROTOCOL_VERSION 0x01010015 // Arbitrary protocol
#define PS_DEFAULT_PORT 0x5073 // 'P', 's'
#define PS_PROTOCOL_MAGIC 0x5073013f // 'P', 's', 0x01, '?'
#define PS_PROTOCOL_MAGIC_RESPONSE 0x50630121 // 'P', 'c', 0x01, '!'
#define PS_PROTOCOL_VERSION 0x01010015 // Arbitrary protocol
#define PS_DEFAULT_PORT 0x5073 // 'P', 's'
// Set when lobby authentication is required. Used in the SrvHandshakeResponseMessage.
#define PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH 0x1
// Defines the list of message types. The order of the list must not change.
// The message types having a negative value are used internally and not sent

View File

@ -127,8 +127,9 @@ private:
* See http://trac.wildfiregames.com/ticket/654
*/
CNetServerWorker::CNetServerWorker(int autostartPlayers) :
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_HostGUID(), m_Stats(NULL),
@ -407,6 +408,7 @@ bool CNetServerWorker::RunStep()
std::vector<bool> newStartGame;
std::vector<std::string> newGameAttributes;
std::vector<std::pair<CStr, CStr>> newLobbyAuths;
std::vector<u32> newTurnLength;
{
@ -417,6 +419,7 @@ bool CNetServerWorker::RunStep()
newStartGame.swap(m_StartGameQueue);
newGameAttributes.swap(m_GameAttributesQueue);
newLobbyAuths.swap(m_LobbyAuthQueue);
newTurnLength.swap(m_TurnLengthQueue);
}
@ -434,6 +437,13 @@ bool CNetServerWorker::RunStep()
if (!newStartGame.empty())
StartGame();
while (!newLobbyAuths.empty())
{
const std::pair<CStr, CStr>& auth = newLobbyAuths.back();
ProcessLobbyAuth(auth.first, auth.second);
newLobbyAuths.pop_back();
}
// Perform file transfers
for (CNetServerSession* session : m_Sessions)
session->GetFileTransferer().Poll();
@ -632,6 +642,9 @@ void CNetServerWorker::SetupSession(CNetServerSession* session)
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);
@ -856,6 +869,24 @@ void CNetServerWorker::SetTurnLength(u32 msecs)
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<CNetServerSession*>::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);
@ -892,6 +923,13 @@ bool CNetServerWorker::OnClientHandshake(void* context, CFsmEvent* event)
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;
@ -914,20 +952,36 @@ bool CNetServerWorker::OnAuthenticate(void* context, CFsmEvent* event)
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",
usernameWithoutRating.ToUTF8(),
session->GetUserName().ToUTF8());
session->Disconnect(NDR_LOBBY_AUTH_FAILED);
return true;
}
// Either deduplicate or prohibit join if name is in use
bool duplicatePlayernames = false;
CFG_GET_VAL("network.duplicateplayernames", duplicatePlayernames);
if (duplicatePlayernames)
username = server.DeduplicatePlayerName(username);
else if (std::find_if(
else
{
std::vector<CNetServerSession*>::iterator it = std::find_if(
server.m_Sessions.begin(), server.m_Sessions.end(),
[&username] (const CNetServerSession* session)
{ return session->GetUserName() == username; })
!= server.m_Sessions.end())
{
session->Disconnect(NDR_PLAYERNAME_IN_USE);
return true;
{ return session->GetUserName() == username; });
if (it != server.m_Sessions.end() && (*it) != session)
{
session->Disconnect(NDR_PLAYERNAME_IN_USE);
return true;
}
}
// Disconnect banned usernames
@ -984,7 +1038,6 @@ bool CNetServerWorker::OnAuthenticate(void* context, CFsmEvent* event)
}
else if (observerLateJoin == "buddies")
{
CStrW usernameWithoutRating(username.substr(0, username.find(L" (")));
CStr buddies;
CFG_GET_VAL("lobby.buddies", buddies);
std::wstringstream buddiesStream(wstring_from_utf8(buddies));
@ -1481,8 +1534,8 @@ void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port)
CNetServer::CNetServer(int autostartPlayers) :
m_Worker(new CNetServerWorker(autostartPlayers))
CNetServer::CNetServer(bool useLobbyAuth, int autostartPlayers) :
m_Worker(new CNetServerWorker(useLobbyAuth, autostartPlayers))
{
}
@ -1512,6 +1565,12 @@ void CNetServer::UpdateGameAttributes(JS::MutableHandleValue attrs, const Script
m_Worker->m_GameAttributesQueue.push_back(attrsJSON);
}
void CNetServer::OnLobbyAuth(const CStr& name, const CStr& token)
{
CScopeLock lock(m_Worker->m_WorkerMutex);
m_Worker->m_LobbyAuthQueue.push_back(std::make_pair(name, token));
}
void CNetServer::SetTurnLength(u32 msecs)
{
CScopeLock lock(m_Worker->m_WorkerMutex);

View File

@ -71,6 +71,9 @@ enum NetServerSessionState
// 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,
@ -104,7 +107,7 @@ public:
* @param autostartPlayers if positive then StartGame will be called automatically
* once this many players are connected (intended for the command-line testing mode).
*/
CNetServer(int autostartPlayers = -1);
CNetServer(bool useLobbyAuth = false, int autostartPlayers = -1);
~CNetServer();
@ -134,6 +137,8 @@ public:
*/
void SetTurnLength(u32 msecs);
void OnLobbyAuth(const CStr& name, const CStr& token);
void SendHolePunchingMessage(const CStr& ip, u16 port);
private:
@ -178,7 +183,7 @@ private:
friend class CNetServer;
friend class CNetFileReceiveTask_ServerRejoin;
CNetServerWorker(int autostartPlayers);
CNetServerWorker(bool useLobbyAuth, int autostartPlayers);
~CNetServerWorker();
/**
@ -229,6 +234,8 @@ private:
*/
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();
@ -291,6 +298,7 @@ private:
JS::PersistentRootedValue m_GameAttributes;
int m_AutostartPlayers;
bool m_LobbyAuth;
ENetHost* m_Host;
std::vector<CNetServerSession*> m_Sessions;
@ -351,12 +359,14 @@ private:
pthread_t m_WorkerThread;
CMutex m_WorkerMutex;
bool m_Shutdown; // protected by m_WorkerMutex
// protected by m_WorkerMutex
bool m_Shutdown;
// Queues for messages sent by the game thread:
std::vector<bool> m_StartGameQueue; // protected by m_WorkerMutex
std::vector<std::string> m_GameAttributesQueue; // protected by m_WorkerMutex
std::vector<u32> m_TurnLengthQueue; // protected by m_WorkerMutex
// Queues for messages sent by the game thread (protected by m_WorkerMutex):
std::vector<bool> m_StartGameQueue;
std::vector<std::string> m_GameAttributesQueue;
std::vector<std::pair<CStr, CStr>> m_LobbyAuthQueue;
std::vector<u32> m_TurnLengthQueue;
};
/// Global network server for the standard game

View File

@ -39,13 +39,13 @@ JS::Value JSI_Network::FindStunEndpoint(ScriptInterface::CxPrivate* pCxPrivate,
return StunClient::FindStunEndpointHost(*(pCxPrivate->pScriptInterface), port);
}
void JSI_Network::StartNetworkHost(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& playerName, const u16 serverPort)
void JSI_Network::StartNetworkHost(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& playerName, const u16 serverPort, const CStr& hostLobbyName, bool useLobbyAuth)
{
ENSURE(!g_NetClient);
ENSURE(!g_NetServer);
ENSURE(!g_Game);
g_NetServer = new CNetServer();
g_NetServer = new CNetServer(useLobbyAuth);
if (!g_NetServer->SetupConnection(serverPort))
{
pCxPrivate->pScriptInterface->ReportError("Failed to start server");
@ -56,6 +56,7 @@ void JSI_Network::StartNetworkHost(ScriptInterface::CxPrivate* pCxPrivate, const
g_Game = new CGame();
g_NetClient = new CNetClient(g_Game, true);
g_NetClient->SetUserName(playerName);
g_NetClient->SetHostingPlayerName(hostLobbyName);
if (!g_NetClient->SetupConnection("127.0.0.1", serverPort))
{
@ -106,6 +107,7 @@ void JSI_Network::StartNetworkJoin(ScriptInterface::CxPrivate* pCxPrivate, const
g_Game = new CGame();
g_NetClient = new CNetClient(g_Game, false);
g_NetClient->SetUserName(playerName);
g_NetClient->SetHostingPlayerName(hostJID.substr(0, hostJID.find("@")));
if (g_XmppClient && useSTUN)
StunClient::SendHolePunchingMessages(enetClient, serverAddress.c_str(), serverPort);
@ -213,7 +215,7 @@ void JSI_Network::RegisterScriptFunctions(const ScriptInterface& scriptInterface
{
scriptInterface.RegisterFunction<u16, &GetDefaultPort>("GetDefaultPort");
scriptInterface.RegisterFunction<JS::Value, int, &FindStunEndpoint>("FindStunEndpoint");
scriptInterface.RegisterFunction<void, CStrW, u16, &StartNetworkHost>("StartNetworkHost");
scriptInterface.RegisterFunction<void, CStrW, u16, CStr, bool, &StartNetworkHost>("StartNetworkHost");
scriptInterface.RegisterFunction<void, CStrW, CStr, u16, bool, CStr, &StartNetworkJoin>("StartNetworkJoin");
scriptInterface.RegisterFunction<void, &DisconnectNetworkGame>("DisconnectNetworkGame");
scriptInterface.RegisterFunction<CStr, &GetPlayerGUID>("GetPlayerGUID");

View File

@ -26,7 +26,7 @@ namespace JSI_Network
u16 GetDefaultPort(ScriptInterface::CxPrivate* pCxPrivate);
void StartNetworkGame(ScriptInterface::CxPrivate* pCxPrivate);
void SetNetworkGameAttributes(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue attribs1);
void StartNetworkHost(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& playerName, const u16 serverPort);
void StartNetworkHost(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& playerName, const u16 serverPort, const CStr& hostLobbyName, bool useLobbyAuth);
void StartNetworkJoin(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& playerName, const CStr& serverAddress, u16 serverPort, bool useSTUN, const CStr& hostJID);
JS::Value FindStunEndpoint(ScriptInterface::CxPrivate* pCxPrivate, int port);
void DisconnectNetworkGame(ScriptInterface::CxPrivate* pCxPrivate);

View File

@ -1516,7 +1516,7 @@ bool Autostart(const CmdLineArgs& args)
if (args.Has("autostart-host-players"))
maxPlayers = args.Get("autostart-host-players").ToUInt();
g_NetServer = new CNetServer(maxPlayers);
g_NetServer = new CNetServer(false, maxPlayers);
g_NetServer->UpdateGameAttributes(&attrs, scriptInterface);