Index: ps/trunk/binaries/data/config/default.cfg
===================================================================
--- ps/trunk/binaries/data/config/default.cfg
+++ ps/trunk/binaries/data/config/default.cfg
@@ -383,6 +383,14 @@
[lobby.columns]
gamerating = false ; Show the average rating of the participating players in a column of the gamelist
+[lobby.stun]
+enabled = true ; The STUN protocol allows hosting games without configuring the firewall and router.
+ ; If STUN is disabled, the game relies on direct connection, UPnP and port forwarding.
+server = "lobby.wildfiregames.com" ; Address of the STUN server.
+port = 3478 ; Port of the STUN server.
+delay = 200 ; Duration in milliseconds that is waited between STUN messages.
+ ; Smaller numbers speed up joins but also become less stable.
+
[mod]
enabledmods = "mod public"
Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js
+++ ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js
@@ -219,6 +219,16 @@
var g_ServerPort;
/**
+ * IP address and port of the STUN endpoint.
+ */
+var g_StunEndpoint;
+
+/**
+ * Current username. Cannot contain whitespace.
+ */
+var g_Username = Engine.LobbyGetNick();
+
+/**
* States whether the GUI is currently updated in response to network messages instead of user input
* and therefore shouldn't send further messages to the network.
*/
@@ -930,6 +940,7 @@
g_IsTutorial = attribs.tutorial && attribs.tutorial == true;
g_ServerName = attribs.serverName;
g_ServerPort = attribs.serverPort;
+ g_StunEndpoint = attribs.stunEndpoint;
if (!g_IsNetworked)
g_PlayerAssignments = {
@@ -2231,6 +2242,7 @@
let stanza = {
"name": g_ServerName,
"port": g_ServerPort,
+ "hostUsername": g_Username,
"mapName": g_GameAttributes.map,
"niceMapName": getMapDisplayName(g_GameAttributes.map),
"mapSize": g_GameAttributes.mapType == "random" ? g_GameAttributes.settings.Size : "Default",
@@ -2239,6 +2251,8 @@
"nbp": clients.connectedPlayers,
"maxnbp": g_GameAttributes.settings.PlayerData.length,
"players": clients.list,
+ "stunIP": g_StunEndpoint ? g_StunEndpoint.ip : "",
+ "stunPort": g_StunEndpoint ? g_StunEndpoint.port : "",
};
// Only send the stanza if the relevant settings actually changed
Index: ps/trunk/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js
+++ ps/trunk/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js
@@ -23,6 +23,11 @@
var g_PlayerAssignments; // used when rejoining
var g_UserRating;
+/**
+ * Object containing the IP address and port of the STUN server.
+ */
+var g_StunEndpoint;
+
function init(attribs)
{
g_UserRating = attribs.rating;
@@ -33,7 +38,7 @@
{
if (Engine.HasXmppClient())
{
- if (startJoin(attribs.name, attribs.ip, getValidPort(attribs.port)))
+ if (startJoin(attribs.name, attribs.ip, getValidPort(attribs.port), attribs.useSTUN, attribs.hostJID))
switchSetupPage("pageConnecting");
}
else
@@ -42,11 +47,14 @@
}
case "host":
{
+ Engine.GetGUIObjectByName("hostSTUNWrapper").hidden = !Engine.HasXmppClient();
if (Engine.HasXmppClient())
{
Engine.GetGUIObjectByName("hostPlayerName").caption = attribs.name;
Engine.GetGUIObjectByName("hostServerName").caption =
sprintf(translate("%(name)s's game"), { "name": attribs.name });
+
+ Engine.GetGUIObjectByName("useSTUN").checked = Engine.ConfigDB_GetValue("user", "lobby.stun.enabled") == "true";
}
switchSetupPage("pageHost");
@@ -92,7 +100,7 @@
let joinServer = Engine.GetGUIObjectByName("joinServer").caption;
let joinPort = Engine.GetGUIObjectByName("joinPort").caption;
- if (startJoin(joinPlayerName, joinServer, getValidPort(joinPort)))
+ if (startJoin(joinPlayerName, joinServer, getValidPort(joinPort), false))
switchSetupPage("pageConnecting");
}
else if (!Engine.GetGUIObjectByName("pageHost").hidden)
@@ -230,7 +238,8 @@
Engine.SwitchGuiPage("page_gamesetup.xml", {
"type": g_GameType,
"serverName": g_ServerName,
- "serverPort": g_ServerPort
+ "serverPort": g_ServerPort,
+ "stunEndpoint": g_StunEndpoint
});
return; // don't process any more messages - leave them for the game GUI loop
}
@@ -271,6 +280,12 @@
Engine.GetGUIObjectByName("continueButton").hidden = newPage == "pageConnecting";
}
+function saveSTUNSetting(enabled)
+{
+ Engine.ConfigDB_CreateValue("user", "lobby.stun.enabled", enabled);
+ Engine.ConfigDB_WriteValueToFile("user", "lobby.stun.enabled", enabled, "config/user.cfg");
+}
+
function startHost(playername, servername, port)
{
startConnectionStatus("server");
@@ -283,16 +298,28 @@
Engine.ConfigDB_CreateValue("user", "multiplayerhosting.port", port);
Engine.ConfigDB_WriteValueToFile("user", "multiplayerhosting.port", port, "config/user.cfg");
+ let hostFeedback = Engine.GetGUIObjectByName("hostFeedback");
+
// Disallow identically named games in the multiplayer lobby
if (Engine.HasXmppClient() &&
Engine.GetGameList().some(game => game.name == servername))
{
cancelSetup();
- Engine.GetGUIObjectByName("hostFeedback").caption =
- translate("Game name already in use.");
+ hostFeedback.caption = translate("Game name already in use.");
return false;
}
+ if (Engine.HasXmppClient() && Engine.GetGUIObjectByName("useSTUN").checked)
+ {
+ g_StunEndpoint = Engine.FindStunEndpoint(port);
+ if (!g_StunEndpoint)
+ {
+ cancelSetup();
+ hostFeedback.caption = translate("Failed to host via STUN.");
+ return false;
+ }
+ }
+
try
{
if (g_UserRating)
@@ -320,14 +347,14 @@
return true;
}
-function startJoin(playername, ip, port)
+/**
+ * Connects via STUN if the hostJID is given.
+ */
+function startJoin(playername, ip, port, useSTUN, hostJID = "")
{
try
{
- if (g_UserRating)
- Engine.StartNetworkJoin(playername + " (" + g_UserRating + ")", ip, port);
- else
- Engine.StartNetworkJoin(playername, ip, port);
+ Engine.StartNetworkJoin(playername + (g_UserRating ? " (" + g_UserRating + ")" : ""), ip, port, useSTUN, hostJID);
}
catch (e)
{
Index: ps/trunk/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.xml
+++ ps/trunk/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.xml
@@ -94,7 +94,7 @@
+
+
+
+ saveSTUNSetting(String(this.checked));
+
+
+ Use STUN to work around firewalls
+
+
-
+
Continue
Index: ps/trunk/binaries/data/mods/public/gui/lobby/lobby.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/lobby/lobby.js
+++ ps/trunk/binaries/data/mods/public/gui/lobby/lobby.js
@@ -19,6 +19,11 @@
const g_Username = Engine.LobbyGetNick();
/**
+ * Lobby server address to construct host JID.
+ */
+const g_LobbyServer = Engine.ConfigDB_GetValue("user", "lobby.server");
+
+/**
* Current games will be listed in these colors.
*/
const g_GameColors = {
@@ -1035,7 +1040,20 @@
if (!game)
return;
- if (game.ip.split('.').length != 4)
+ let ip;
+ let port;
+ if (game.stunIP)
+ {
+ ip = game.stunIP;
+ port = game.stunPort;
+ }
+ else
+ {
+ ip = game.ip;
+ port = game.port;
+ }
+
+ if (ip.split('.').length != 4)
{
addChatMessage({
"from": "system",
@@ -1049,10 +1067,12 @@
Engine.PushGuiPage("page_gamesetup_mp.xml", {
"multiplayerGameType": "join",
- "ip": game.ip,
- "port": game.port,
+ "ip": ip,
+ "port": port,
"name": g_Username,
- "rating": g_UserRating
+ "rating": g_UserRating,
+ "useSTUN": !!game.stunIP,
+ "hostJID": game.hostUsername + "@" + g_LobbyServer + "/0ad"
});
}
Index: ps/trunk/build/premake/premake4.lua
===================================================================
--- ps/trunk/build/premake/premake4.lua
+++ ps/trunk/build/premake/premake4.lua
@@ -623,6 +623,7 @@
extern_libs = {
"spidermonkey",
"boost",
+ "enet",
"gloox",
"icu",
"iconv",
@@ -771,6 +772,7 @@
"sdl", -- key definitions
"opengl",
"boost",
+ "enet",
"tinygettext",
"icu",
"iconv",
Index: ps/trunk/source/gui/scripting/ScriptFunctions.cpp
===================================================================
--- ps/trunk/source/gui/scripting/ScriptFunctions.cpp
+++ ps/trunk/source/gui/scripting/ScriptFunctions.cpp
@@ -30,15 +30,18 @@
#include "gui/scripting/JSInterface_GUITypes.h"
#include "i18n/L10n.h"
#include "i18n/scripting/JSInterface_L10n.h"
+#include "lib/external_libraries/enet.h"
#include "lib/svn_revision.h"
#include "lib/sysdep/sysdep.h"
#include "lib/timer.h"
#include "lib/utf8.h"
#include "lobby/scripting/JSInterface_Lobby.h"
+#include "lobby/IXmppClient.h"
#include "maths/FixedVector3D.h"
#include "network/NetClient.h"
#include "network/NetMessage.h"
#include "network/NetServer.h"
+#include "network/StunClient.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/Errors.h"
@@ -237,6 +240,11 @@
return SavedGames::GetEngineInfo(*(pCxPrivate->pScriptInterface));
}
+JS::Value FindStunEndpoint(ScriptInterface::CxPrivate* pCxPrivate, int port)
+{
+ return StunClient::FindStunEndpointHost(*(pCxPrivate->pScriptInterface), port);
+}
+
void StartNetworkGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
ENSURE(g_NetClient);
@@ -360,16 +368,52 @@
}
}
-void StartNetworkJoin(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& playerName, const CStr& serverAddress, u16 serverPort)
+void StartNetworkJoin(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& playerName, const CStr& serverAddress, u16 serverPort, bool useSTUN, const std::string& hostJID)
{
ENSURE(!g_NetClient);
ENSURE(!g_NetServer);
ENSURE(!g_Game);
+ ENetHost* enetClient = nullptr;
+ if (g_XmppClient && useSTUN)
+ {
+ // Find an unused port
+ for (int i = 0; i < 5 && !enetClient; ++i)
+ {
+ // Ports below 1024 are privileged on unix
+ u16 port = 1024 + rand() % (UINT16_MAX - 1024);
+ ENetAddress hostAddr{ENET_HOST_ANY, port};
+ enetClient = enet_host_create(&hostAddr, 1, 1, 0, 0);
+ ++hostAddr.port;
+ }
+
+ if (!enetClient)
+ {
+ pCxPrivate->pScriptInterface->ReportError("Could not find an unused port for the enet STUN client");
+ return;
+ }
+
+ StunClient::StunEndpoint* stunEndpoint = StunClient::FindStunEndpointJoin(enetClient);
+ if (!stunEndpoint)
+ {
+ pCxPrivate->pScriptInterface->ReportError("Could not find the STUN endpoint");
+ return;
+ }
+
+ g_XmppClient->SendStunEndpointToHost(stunEndpoint, hostJID);
+ delete stunEndpoint;
+
+ SDL_Delay(1000);
+ }
+
g_Game = new CGame();
g_NetClient = new CNetClient(g_Game, false);
g_NetClient->SetUserName(playerName);
- if (!g_NetClient->SetupConnection(serverAddress, serverPort))
+
+ if (g_XmppClient && useSTUN)
+ StunClient::SendHolePunchingMessages(enetClient, serverAddress.c_str(), serverPort);
+
+ if (!g_NetClient->SetupConnection(serverAddress, serverPort, enetClient))
{
pCxPrivate->pScriptInterface->ReportError("Failed to connect to server");
SAFE_DELETE(g_NetClient);
@@ -1034,7 +1078,7 @@
scriptInterface.RegisterFunction("StartGame");
scriptInterface.RegisterFunction("EndGame");
scriptInterface.RegisterFunction("StartNetworkHost");
- scriptInterface.RegisterFunction("StartNetworkJoin");
+ scriptInterface.RegisterFunction("StartNetworkJoin");
scriptInterface.RegisterFunction("GetDefaultPort");
scriptInterface.RegisterFunction("DisconnectNetworkGame");
scriptInterface.RegisterFunction("GetPlayerGUID");
@@ -1047,6 +1091,7 @@
scriptInterface.RegisterFunction("SendNetworkReady");
scriptInterface.RegisterFunction("GetAIs");
scriptInterface.RegisterFunction("GetEngineInfo");
+ scriptInterface.RegisterFunction("FindStunEndpoint");
// Saved games
scriptInterface.RegisterFunction("StartSavedGame");
Index: ps/trunk/source/lobby/IXmppClient.h
===================================================================
--- ps/trunk/source/lobby/IXmppClient.h
+++ ps/trunk/source/lobby/IXmppClient.h
@@ -21,6 +21,9 @@
#include "scriptinterface/ScriptTypes.h"
class ScriptInterface;
+namespace StunClient {
+class StunEndpoint;
+}
class IXmppClient
{
@@ -54,6 +57,8 @@
virtual void GuiPollMessage(ScriptInterface& scriptInterface, JS::MutableHandleValue ret) = 0;
virtual void SendMUCMessage(const std::string& message) = 0;
+
+ virtual void SendStunEndpointToHost(StunClient::StunEndpoint* stunEndpoint, const std::string& hostJID) = 0;
};
extern IXmppClient *g_XmppClient;
Index: ps/trunk/source/lobby/XmppClient.h
===================================================================
--- ps/trunk/source/lobby/XmppClient.h
+++ ps/trunk/source/lobby/XmppClient.h
@@ -33,7 +33,7 @@
struct CertInfo;
}
-class XmppClient : public IXmppClient, public glooxwrapper::ConnectionListener, public glooxwrapper::MUCRoomHandler, public glooxwrapper::IqHandler, public glooxwrapper::RegistrationHandler, public glooxwrapper::MessageHandler
+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);
@@ -42,6 +42,7 @@
glooxwrapper::Client* m_client;
glooxwrapper::MUCRoom* m_mucRoom;
glooxwrapper::Registration* m_registration;
+ glooxwrapper::SessionManager* m_sessionManager;
// Account infos
std::string m_username;
@@ -81,6 +82,8 @@
void GUIGetBoardList(ScriptInterface& scriptInterface, JS::MutableHandleValue ret);
void GUIGetProfile(ScriptInterface& scriptInterface, JS::MutableHandleValue ret);
+ void SendStunEndpointToHost(StunClient::StunEndpoint* stunEndpoint, const std::string& hostJID);
+
//Script
ScriptInterface& GetScriptInterface();
@@ -119,6 +122,10 @@
/* Message Handler */
virtual void handleMessage(const glooxwrapper::Message& msg, glooxwrapper::MessageSession * session);
+ /* Session Handler */
+ virtual void handleSessionAction(gloox::Jingle::Action action, glooxwrapper::Jingle::Session *UNUSED(session), const glooxwrapper::Jingle::Session::Jingle *jingle);
+ virtual void handleSessionInitiation(const glooxwrapper::Jingle::Session::Jingle *jingle);
+
// Helpers
void GetPresenceString(const gloox::Presence::PresenceType p, std::string& presence) const;
void GetRoleString(const gloox::MUCRoomRole r, std::string& role) const;
Index: ps/trunk/source/lobby/XmppClient.cpp
===================================================================
--- ps/trunk/source/lobby/XmppClient.cpp
+++ ps/trunk/source/lobby/XmppClient.cpp
@@ -19,9 +19,16 @@
#include "XmppClient.h"
#include "StanzaExtensions.h"
+#ifdef WIN32
+# include
+#endif
+
#include "glooxwrapper/glooxwrapper.h"
#include "i18n/L10n.h"
+#include "lib/external_libraries/enet.h"
#include "lib/utf8.h"
+#include "network/NetServer.h"
+#include "network/StunClient.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/Pyrogenesis.h"
@@ -68,7 +75,7 @@
* @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_client(NULL), m_mucRoom(NULL), m_registration(NULL), m_username(sUsername), m_password(sPassword), m_nick(sNick), m_initialLoadComplete(false), m_sessionManager()
{
// Read lobby configuration from default.cfg
std::string sServer;
@@ -129,6 +136,11 @@
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();
+
}
/**
@@ -472,7 +484,7 @@
JSAutoRequest rq(cx);
scriptInterface.Eval("([])", ret);
- const char* stats[] = { "name", "ip", "port", "state", "nbp", "maxnbp", "players", "mapName", "niceMapName", "mapSize", "mapType", "victoryCondition", "startTime" };
+ const char* stats[] = { "name", "ip", "port", "stunIP", "stunPort", "hostUsername", "state", "nbp", "maxnbp", "players", "mapName", "niceMapName", "mapSize", "mapType", "victoryCondition", "startTime" };
for(const glooxwrapper::Tag* const& t : m_GameList)
{
JS::RootedValue game(cx);
@@ -1084,3 +1096,36 @@
#undef DEBUG_CASE
#undef CASE
}
+
+void XmppClient::SendStunEndpointToHost(StunClient::StunEndpoint* stunEndpoint, const std::string& hostJIDStr)
+{
+ ENSURE(stunEndpoint);
+
+ char ipStr[256] = "(error)";
+ ENetAddress addr;
+ addr.host = ntohl(stunEndpoint->ip);
+ enet_address_get_host_ip(&addr, ipStr, ARRAY_SIZE(ipStr));
+
+ glooxwrapper::JID hostJID(hostJIDStr);
+ glooxwrapper::Jingle::Session session = m_sessionManager->createSession(hostJID);
+ session.sessionInitiate(ipStr, stunEndpoint->port);
+}
+
+void XmppClient::handleSessionAction(gloox::Jingle::Action action, glooxwrapper::Jingle::Session *UNUSED(session), const glooxwrapper::Jingle::Session::Jingle *jingle)
+{
+ if (action == gloox::Jingle::SessionInitiate)
+ handleSessionInitiation(jingle);
+}
+
+void XmppClient::handleSessionInitiation(const glooxwrapper::Jingle::Session::Jingle *jingle)
+{
+ glooxwrapper::Jingle::ICEUDP::Candidate candidate = jingle->getCandidate();
+
+ if (candidate.ip.empty())
+ {
+ LOGERROR("Failed to retrieve Jingle candidate");
+ return;
+ }
+
+ g_NetServer->SendHolePunchingMessage(candidate.ip.to_string(), candidate.port);
+}
Index: ps/trunk/source/lobby/glooxwrapper/glooxwrapper.h
===================================================================
--- ps/trunk/source/lobby/glooxwrapper/glooxwrapper.h
+++ ps/trunk/source/lobby/glooxwrapper/glooxwrapper.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2014 Wildfire Games.
+/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -72,6 +72,10 @@
#include
#include
#include
+#include
+#include
+#include
+#include
#include
@@ -101,6 +105,7 @@
class ClientImpl;
class MUCRoomHandlerWrapper;
+ class SessionHandlerWrapper;
GLOOXWRAPPER_API void* glooxwrapper_alloc(size_t size);
GLOOXWRAPPER_API void glooxwrapper_free(void* p);
@@ -256,6 +261,11 @@
}
m_Head = m_Tail = NULL;
}
+
+ const T& front() const
+ {
+ return *begin();
+ }
};
typedef glooxwrapper::list TagList;
@@ -571,6 +581,98 @@
const Tag* findTag_clone(const string& expression) const; // like findTag but must be Tag::free()d
ConstTagList findTagList_clone(const string& expression) const; // like findTagList but each tag must be Tag::free()d
};
+
+ namespace Jingle
+ {
+
+ class GLOOXWRAPPER_API Plugin
+ {
+ protected:
+ const gloox::Jingle::Plugin* m_Wrapped;
+ bool m_Owned;
+
+ public:
+ Plugin(const gloox::Jingle::Plugin* wrapped, bool owned) : m_Wrapped(wrapped), m_Owned(owned) {}
+
+ virtual ~Plugin();
+
+ const Plugin findPlugin(int type) const;
+ const gloox::Jingle::Plugin* getWrapped() const { return m_Wrapped; }
+ };
+
+ typedef list PluginList;
+
+ class GLOOXWRAPPER_API Content : public Plugin
+ {
+ public:
+ Content(const string& name, const PluginList& plugins);
+ Content();
+ };
+
+ class GLOOXWRAPPER_API ICEUDP : public Plugin
+ {
+ public:
+ struct Candidate {
+ string ip;
+ int port;
+ };
+
+ typedef std::list CandidateList;
+
+ ICEUDP(CandidateList& candidates);
+ ICEUDP();
+
+ const CandidateList candidates() const;
+ };
+
+ class GLOOXWRAPPER_API Session
+ {
+ protected:
+ gloox::Jingle::Session* m_Wrapped;
+ bool m_Owned;
+
+ public:
+ class GLOOXWRAPPER_API Jingle
+ {
+ private:
+ const gloox::Jingle::Session::Jingle* m_Wrapped;
+ bool m_Owned;
+ public:
+ Jingle(const gloox::Jingle::Session::Jingle* wrapped, bool owned) : m_Wrapped(wrapped), m_Owned(owned) {}
+
+ const PluginList plugins() const;
+
+ ICEUDP::Candidate getCandidate() const;
+ };
+
+ Session(gloox::Jingle::Session* wrapped, bool owned) : m_Wrapped(wrapped), m_Owned(owned) {}
+
+ bool sessionInitiate(char* ipStr, uint16_t port);
+ };
+
+ class GLOOXWRAPPER_API SessionHandler
+ {
+ public:
+ virtual ~SessionHandler() {}
+ virtual void handleSessionAction(gloox::Jingle::Action action, Session *session, const Session::Jingle *jingle) = 0;
+ };
+
+ }
+
+ class GLOOXWRAPPER_API SessionManager
+ {
+ private:
+ gloox::Jingle::SessionManager* m_Wrapped;
+ SessionHandlerWrapper* m_HandlerWrapper;
+
+ public:
+ SessionManager(Client* parent, Jingle::SessionHandler* sh);
+ ~SessionManager();
+ void registerPlugins();
+ Jingle::Session createSession(const JID& callee);
+ };
+
+
}
#endif // INCLUDED_GLOOXWRAPPER_H
Index: ps/trunk/source/lobby/glooxwrapper/glooxwrapper.cpp
===================================================================
--- ps/trunk/source/lobby/glooxwrapper/glooxwrapper.cpp
+++ ps/trunk/source/lobby/glooxwrapper/glooxwrapper.cpp
@@ -1,4 +1,4 @@
-/* Copyright (C) 2015 Wildfire Games.
+/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -277,6 +277,29 @@
}
};
+class SessionHandlerWrapper : public gloox::Jingle::SessionHandler
+{
+public:
+ glooxwrapper::Jingle::SessionHandler* m_Wrapped;
+ bool m_Owned;
+
+ SessionHandlerWrapper(glooxwrapper::Jingle::SessionHandler* wrapped, bool owned)
+ : m_Wrapped(wrapped), m_Owned(owned) {}
+
+ virtual void handleSessionAction(gloox::Jingle::Action action, gloox::Jingle::Session* session, const gloox::Jingle::Session::Jingle* jingle)
+ {
+ m_Wrapped->handleSessionAction(action, new glooxwrapper::Jingle::Session(session, false), new glooxwrapper::Jingle::Session::Jingle(jingle, false));
+ }
+
+ virtual void handleSessionActionError(gloox::Jingle::Action UNUSED(action), gloox::Jingle::Session* UNUSED(session), const gloox::Error* UNUSED(error))
+ {
+ }
+
+ virtual void handleIncomingSession(gloox::Jingle::Session* UNUSED(session))
+ {
+ }
+};
+
class ClientImpl
{
public:
@@ -286,6 +309,9 @@
std::list > m_IqHandlers;
};
+static const std::string XMLNS = "xmlns";
+static const std::string XMLNS_JINGLE_0AD_GAME = "urn:xmpp:jingle:apps:0ad-game:1";
+
} // namespace glooxwrapper
@@ -737,3 +763,143 @@
tagListWrapper.push_back(new glooxwrapper::Tag(const_cast(t), false));
return tagListWrapper;
}
+
+glooxwrapper::Jingle::Plugin::~Plugin()
+{
+ if (m_Owned)
+ delete m_Wrapped;
+}
+
+const glooxwrapper::Jingle::Plugin glooxwrapper::Jingle::Plugin::findPlugin(int type) const
+{
+ return glooxwrapper::Jingle::Plugin(m_Wrapped->findPlugin(type), false);
+}
+
+glooxwrapper::Jingle::Content::Content(const string& name, const PluginList& plugins)
+ : glooxwrapper::Jingle::Plugin(NULL, false)
+{
+ gloox::Jingle::PluginList glooxPluginList;
+ for (const glooxwrapper::Jingle::Plugin* const& plugin: plugins)
+ glooxPluginList.push_back(plugin->getWrapped());
+
+ m_Wrapped = new gloox::Jingle::Content(name.to_string(), glooxPluginList);
+ m_Owned = true;
+}
+
+glooxwrapper::Jingle::Content::Content()
+ : glooxwrapper::Jingle::Plugin(NULL, false)
+{
+ m_Wrapped = new gloox::Jingle::Content();
+ m_Owned = true;
+}
+
+const glooxwrapper::Jingle::PluginList glooxwrapper::Jingle::Session::Jingle::plugins() const
+{
+ glooxwrapper::Jingle::PluginList pluginListWrapper;
+ for (const gloox::Jingle::Plugin* const& plugin : m_Wrapped->plugins())
+ pluginListWrapper.push_back(new glooxwrapper::Jingle::Plugin(const_cast(plugin), false));
+ return pluginListWrapper;
+}
+
+glooxwrapper::Jingle::ICEUDP::Candidate glooxwrapper::Jingle::Session::Jingle::getCandidate() const
+{
+ const gloox::Jingle::Content *content = static_cast(m_Wrapped->plugins().front());
+ if (!content)
+ return glooxwrapper::Jingle::ICEUDP::Candidate();
+
+ const gloox::Jingle::ICEUDP *iceUDP = static_cast(content->findPlugin(gloox::Jingle::PluginICEUDP));
+ if (!iceUDP)
+ return glooxwrapper::Jingle::ICEUDP::Candidate();
+
+ gloox::Jingle::ICEUDP::Candidate glooxCandidate = iceUDP->candidates().front();
+ return glooxwrapper::Jingle::ICEUDP::Candidate{glooxCandidate.ip, glooxCandidate.port};
+}
+
+bool glooxwrapper::Jingle::Session::sessionInitiate(char* ipStr, u16 port)
+{
+ gloox::Jingle::ICEUDP::CandidateList *candidateList = new gloox::Jingle::ICEUDP::CandidateList();
+
+ candidateList->push_back(gloox::Jingle::ICEUDP::Candidate
+ {
+ "1", // component_id,
+ "1", // foundation
+ "0", // andidate_generation
+ "1", // candidate_id
+ ipStr,
+ "0", // network
+ port,
+ 0, // priotiry
+ "udp",
+ "", // base_ip
+ 0, // base_port
+ gloox::Jingle::ICEUDP::ServerReflexive
+ });
+
+ gloox::Jingle::PluginList *pluginList = new gloox::Jingle::PluginList();
+ pluginList->push_back(new gloox::Jingle::ICEUDP(/*local_pwd*/"", /*local_ufrag*/"", *candidateList));
+ return m_Wrapped->sessionInitiate(new gloox::Jingle::Content(std::string("game-data"), *pluginList));
+}
+
+glooxwrapper::Jingle::ICEUDP::ICEUDP(glooxwrapper::Jingle::ICEUDP::CandidateList& candidates)
+ : glooxwrapper::Jingle::Plugin(NULL, false)
+{
+ gloox::Jingle::ICEUDP::CandidateList glooxCandidates;
+ for (const glooxwrapper::Jingle::ICEUDP::Candidate candidate : candidates)
+ glooxCandidates.push_back(gloox::Jingle::ICEUDP::Candidate
+ {
+ "1", // component_id,
+ "1", // foundation
+ "0", // candidate_generation
+ "1", // candidate_id
+ candidate.ip.to_string(),
+ "0", // network
+ candidate.port,
+ 0, // priority
+ "udp",
+ "", // base_ip
+ 0, // base_port
+ gloox::Jingle::ICEUDP::ServerReflexive
+ });
+
+ m_Wrapped = new gloox::Jingle::ICEUDP(/*local_pwd*/"", /*local_ufrag*/"", glooxCandidates);
+ m_Owned = true;
+}
+
+glooxwrapper::Jingle::ICEUDP::ICEUDP()
+ : glooxwrapper::Jingle::Plugin(NULL, false)
+{
+ m_Wrapped = new gloox::Jingle::ICEUDP();
+ m_Owned = true;
+}
+
+const glooxwrapper::Jingle::ICEUDP::CandidateList glooxwrapper::Jingle::ICEUDP::candidates() const
+{
+ glooxwrapper::Jingle::ICEUDP::CandidateList candidateListWrapper;
+ for (const gloox::Jingle::ICEUDP::Candidate candidate : static_cast(m_Wrapped)->candidates())
+ candidateListWrapper.push_back(glooxwrapper::Jingle::ICEUDP::Candidate{candidate.ip, candidate.port});
+ return candidateListWrapper;
+}
+
+glooxwrapper::SessionManager::SessionManager(Client* parent, Jingle::SessionHandler* sh)
+{
+ m_HandlerWrapper = new SessionHandlerWrapper(sh, false);
+ m_Wrapped = new gloox::Jingle::SessionManager(parent->getWrapped(), m_HandlerWrapper);
+}
+
+glooxwrapper::SessionManager::~SessionManager()
+{
+ delete m_Wrapped;
+ delete m_HandlerWrapper;
+}
+
+void glooxwrapper::SessionManager::registerPlugins()
+{
+ m_Wrapped->registerPlugin(new gloox::Jingle::Content());
+ m_Wrapped->registerPlugin(new gloox::Jingle::ICEUDP());
+}
+
+glooxwrapper::Jingle::Session glooxwrapper::SessionManager::createSession(const JID& callee)
+{
+ gloox::Jingle::Session* glooxSession = m_Wrapped->createSession(callee.getWrapped(), m_HandlerWrapper);
+ return glooxwrapper::Jingle::Session(glooxSession, false);
+}
Index: ps/trunk/source/network/NetClient.h
===================================================================
--- ps/trunk/source/network/NetClient.h
+++ ps/trunk/source/network/NetClient.h
@@ -33,6 +33,8 @@
class CNetServer;
class ScriptInterface;
+typedef struct _ENetHost ENetHost;
+
// NetClient session FSM states
enum
{
@@ -99,7 +101,7 @@
* @param server IP address or host name to connect to
* @return true on success, false on connection failure
*/
- bool SetupConnection(const CStr& server, const u16 port);
+ bool SetupConnection(const CStr& server, const u16 port, ENetHost* enetClient = NULL);
/**
* Destroy the connection to the server.
Index: ps/trunk/source/network/NetClient.cpp
===================================================================
--- ps/trunk/source/network/NetClient.cpp
+++ ps/trunk/source/network/NetClient.cpp
@@ -24,6 +24,7 @@
#include "NetSession.h"
#include "lib/byte_order.h"
+#include "lib/external_libraries/enet.h"
#include "lib/sysdep/sysdep.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
@@ -158,10 +159,10 @@
m_UserName = username;
}
-bool CNetClient::SetupConnection(const CStr& server, const u16 port)
+bool CNetClient::SetupConnection(const CStr& server, const u16 port, ENetHost* enetClient)
{
CNetClientSession* session = new CNetClientSession(*this);
- bool ok = session->Connect(server, port, m_IsLocalClient);
+ bool ok = session->Connect(server, port, m_IsLocalClient, enetClient);
SetAndOwnSession(session);
return ok;
}
Index: ps/trunk/source/network/NetServer.h
===================================================================
--- ps/trunk/source/network/NetServer.h
+++ ps/trunk/source/network/NetServer.h
@@ -134,6 +134,8 @@
*/
void SetTurnLength(u32 msecs);
+ void SendHolePunchingMessage(const CStr& ip, u16 port);
+
private:
CNetServerWorker* m_Worker;
};
@@ -271,6 +273,8 @@
*/
void CheckClientConnections();
+ void SendHolePunchingMessage(const CStr& ip, u16 port);
+
/**
* Internal script context for (de)serializing script messages,
* and for storing game attributes.
Index: ps/trunk/source/network/NetServer.cpp
===================================================================
--- ps/trunk/source/network/NetServer.cpp
+++ ps/trunk/source/network/NetServer.cpp
@@ -26,6 +26,7 @@
#include "NetStats.h"
#include "lib/external_libraries/enet.h"
+#include "network/StunClient.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/Profile.h"
@@ -1467,6 +1468,11 @@
}
}
+void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port)
+{
+ StunClient::SendHolePunchingMessages(m_Host, ipStr.c_str(), port);
+}
+
@@ -1506,3 +1512,8 @@
CScopeLock 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/NetSession.h
===================================================================
--- ps/trunk/source/network/NetSession.h
+++ ps/trunk/source/network/NetSession.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2016 Wildfire Games.
+/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@@ -39,6 +39,8 @@
class CNetStatsTable;
+typedef struct _ENetHost ENetHost;
+
/**
* @file
* Network client/server sessions.
@@ -70,7 +72,7 @@
CNetClientSession(CNetClient& client);
~CNetClientSession();
- bool Connect(const CStr& server, const u16 port, const bool isLocalClient);
+ bool Connect(const CStr& server, const u16 port, const bool isLocalClient, ENetHost* enetClient);
/**
* Process queued incoming messages.
Index: ps/trunk/source/network/NetSession.cpp
===================================================================
--- ps/trunk/source/network/NetSession.cpp
+++ ps/trunk/source/network/NetSession.cpp
@@ -52,13 +52,18 @@
}
}
-bool CNetClientSession::Connect(const CStr& server, const u16 port, const bool isLocalClient)
+bool CNetClientSession::Connect(const CStr& server, const u16 port, const bool isLocalClient, ENetHost* enetClient)
{
ENSURE(!m_Host);
ENSURE(!m_Server);
// Create ENet host
- ENetHost* host = enet_host_create(NULL, 1, CHANNEL_COUNT, 0, 0);
+ ENetHost* host;
+ if (enetClient != nullptr)
+ host = enetClient;
+ else
+ host = enet_host_create(NULL, 1, CHANNEL_COUNT, 0, 0);
+
if (!host)
return false;
Index: ps/trunk/source/network/StunClient.h
===================================================================
--- ps/trunk/source/network/StunClient.h
+++ ps/trunk/source/network/StunClient.h
@@ -0,0 +1,44 @@
+/* Copyright (C) 2017 Wildfire Games.
+ * Copyright (C) 2013-2016 SuperTuxKart-Team.
+ * 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 STUNCLIENT_H
+#define STUNCLIENT_H
+
+#include "scriptinterface/ScriptInterface.h"
+
+typedef struct _ENetHost ENetHost;
+
+namespace StunClient
+{
+
+struct StunEndpoint {
+ u32 ip;
+ u16 port;
+};
+
+void SendStunRequest(ENetHost* transactionHost, u32 targetIp, u16 targetPort);
+
+JS::Value FindStunEndpointHost(ScriptInterface& scriptInterface, int port);
+
+StunEndpoint* FindStunEndpointJoin(ENetHost* transactionHost);
+
+void SendHolePunchingMessages(ENetHost* enetClient, const char* serverAddress, u16 serverPort);
+
+}
+
+#endif // STUNCLIENT_H
Index: ps/trunk/source/network/StunClient.cpp
===================================================================
--- ps/trunk/source/network/StunClient.cpp
+++ ps/trunk/source/network/StunClient.cpp
@@ -0,0 +1,426 @@
+/* Copyright (C) 2017 Wildfire Games.
+ * Copyright (C) 2013-2016 SuperTuxKart-Team.
+ * 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 "StunClient.h"
+
+#include
+#include
+
+#include
+#include
+
+#include
+#ifdef WIN32
+# include
+# include
+#else
+# include
+# include
+#endif
+
+#include
+
+#include "lib/external_libraries/enet.h"
+
+#if OS_WIN
+#include "lib/sysdep/os/win/wposix/wtime.h"
+#endif
+
+#include "scriptinterface/ScriptInterface.h"
+#include "ps/CLogger.h"
+#include "ps/ConfigDB.h"
+
+unsigned int m_StunServerIP;
+int m_StunServerPort;
+
+/**
+ * These constants are defined in Section 6 of RFC 5389.
+ */
+const u32 m_MagicCookie = 0x2112A442;
+const u32 m_MethodTypeBinding = 0x0001;
+const u32 m_BindingSuccessResponse = 0x0101;
+
+/**
+ * Bit determining whether comprehension of an attribute is optional.
+ * Described in Section 15 of RFC 5389.
+ */
+const u16 m_ComprehensionOptional = 0x1 << 15;
+
+/**
+ * Bit determining whether the bit was assigned by IETF Review.
+ * Described in section 18.1. of RFC 5389.
+ */
+const u16 m_IETFReview = 0x1 << 14;
+
+/**
+ * These constants are defined in Section 15.1 of RFC 5389.
+ */
+const u8 m_IPAddressFamilyIPv4 = 0x01;
+
+/**
+ * These constants are defined in Section 18.2 of RFC 5389.
+ */
+const u16 m_AttrTypeMappedAddress = 0x001;
+const u16 m_AttrTypeXORMappedAddress = 0x0020;
+
+/**
+ * Described in section 3 of RFC 5389.
+ */
+u8 m_TransactionID[12];
+
+/**
+ * Discovered STUN endpoint
+ */
+u32 m_IP;
+u16 m_Port;
+
+void AddUInt16(std::vector& buffer, const u16 value)
+{
+ buffer.push_back((value >> 8) & 0xff);
+ buffer.push_back(value & 0xff);
+}
+
+void AddUInt32(std::vector& buffer, const u32 value)
+{
+ buffer.push_back((value >> 24) & 0xff);
+ buffer.push_back((value >> 16) & 0xff);
+ buffer.push_back((value >> 8) & 0xff);
+ buffer.push_back( value & 0xff);
+}
+
+template
+bool GetFromBuffer(std::vector buffer, u32& offset, T& result)
+{
+ if (offset + n > buffer.size())
+ return false;
+
+ int a = n;
+ offset += n;
+ while (a--)
+ {
+ result <<= 8;
+ result += buffer[offset - 1 - a];
+ }
+ return true;
+}
+
+/**
+ * Creates a STUN request and sends it to a STUN server.
+ * The request is sent through transactionHost, from which the answer
+ * will be retrieved by ReceiveStunResponse and interpreted by ParseStunResponse.
+ */
+bool CreateStunRequest(ENetHost* transactionHost)
+{
+ ENSURE(transactionHost);
+
+ CStr server_name;
+ CFG_GET_VAL("lobby.stun.server", server_name);
+ CFG_GET_VAL("lobby.stun.port", m_StunServerPort);
+
+ debug_printf("GetPublicAddress: Using STUN server %s:%d\n", server_name.c_str(), m_StunServerPort);
+
+ addrinfo hints;
+ addrinfo* res;
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC; // AF_INET or AF_INET6 to force version
+ hints.ai_socktype = SOCK_STREAM;
+
+ // Resolve the stun server name so we can send it a STUN request
+ int status = getaddrinfo(server_name.c_str(), nullptr, &hints, &res);
+ if (status != 0)
+ {
+ LOGERROR("GetPublicAddress: Error in getaddrinfo: %s", gai_strerror(status));
+ return false;
+ }
+
+ ENSURE(res);
+
+ // Documentation says it points to "one or more addrinfo structures"
+ sockaddr_in* current_interface = (sockaddr_in*)(res->ai_addr);
+ m_StunServerIP = ntohl(current_interface->sin_addr.s_addr);
+
+ StunClient::SendStunRequest(transactionHost, m_StunServerIP, m_StunServerPort);
+
+ freeaddrinfo(res);
+ return true;
+}
+
+void StunClient::SendStunRequest(ENetHost* transactionHost, u32 targetIp, u16 targetPort)
+{
+ std::vector buffer;
+ AddUInt16(buffer, m_MethodTypeBinding);
+ AddUInt16(buffer, 0); // length
+ AddUInt32(buffer, m_MagicCookie);
+
+ for (std::size_t i = 0; i < sizeof(m_TransactionID); ++i)
+ {
+ u8 random_byte = rand() % 256;
+ buffer.push_back(random_byte);
+ m_TransactionID[i] = random_byte;
+ }
+
+ sockaddr_in to;
+ int to_len = sizeof(to);
+ memset(&to, 0, to_len);
+
+ to.sin_family = AF_INET;
+ to.sin_port = htons(targetPort);
+ to.sin_addr.s_addr = htonl(targetIp);
+
+ sendto(transactionHost->socket, (char*)(buffer.data()), (int)buffer.size(), 0, (sockaddr*)&to, to_len);
+}
+
+/**
+ * Gets the response from the STUN server and checks it for its validity.
+ */
+bool ReceiveStunResponse(ENetHost* transactionHost, std::vector& buffer)
+{
+ ENSURE(transactionHost);
+
+ // TransportAddress sender;
+ const int LEN = 2048;
+ char input_buffer[LEN];
+
+ memset(input_buffer, 0, LEN);
+
+ sockaddr_in addr;
+ socklen_t from_len = sizeof(addr);
+
+ int len = recvfrom(transactionHost->socket, input_buffer, LEN, 0, (sockaddr*)(&addr), &from_len);
+
+ int delay = 200;
+ CFG_GET_VAL("lobby.stun.delay", delay);
+
+ // Wait to receive the message because enet sockets are non-blocking
+ const int max_tries = 5;
+ for (int count = 0; len < 0 && (count < max_tries || max_tries == -1); ++count)
+ {
+ usleep(delay * 1000);
+ len = recvfrom(transactionHost->socket, input_buffer, LEN, 0, (sockaddr*)(&addr), &from_len);
+ }
+
+ if (len < 0)
+ {
+ LOGERROR("GetPublicAddress: recvfrom error (%d): %s", errno, strerror(errno));
+ return false;
+ }
+
+ u32 sender_ip = ntohl((u32)(addr.sin_addr.s_addr));
+ u16 sender_port = ntohs(addr.sin_port);
+
+ if (sender_ip != m_StunServerIP)
+ LOGERROR("GetPublicAddress: Received stun response from different address: %d:%d (%d.%d.%d.%d:%d) %s",
+ addr.sin_addr.s_addr,
+ addr.sin_port,
+ (sender_ip >> 24) & 0xff,
+ (sender_ip >> 16) & 0xff,
+ (sender_ip >> 8) & 0xff,
+ (sender_ip >> 0) & 0xff,
+ sender_port,
+ input_buffer);
+
+ // Convert to network string.
+ buffer.resize(len);
+ memcpy(buffer.data(), (u8*)input_buffer, len);
+
+ return true;
+}
+
+bool ParseStunResponse(const std::vector& buffer)
+{
+ u32 offset = 0;
+
+ u16 responseType = 0;
+ if (!GetFromBuffer(buffer, offset, responseType) || responseType != m_BindingSuccessResponse)
+ {
+ LOGERROR("STUN response isn't a binding success response");
+ return false;
+ }
+
+ // Ignore message size
+ offset += 2;
+
+ u32 cookie = 0;
+ if (!GetFromBuffer(buffer, offset, cookie) || cookie != m_MagicCookie)
+ {
+ LOGERROR("STUN response doesn't contain the magic cookie");
+ return false;
+ }
+
+ for (std::size_t i = 0; i < sizeof(m_TransactionID); ++i)
+ {
+ u8 transactionChar = 0;
+ if (!GetFromBuffer(buffer, offset, transactionChar) || transactionChar != m_TransactionID[i])
+ {
+ LOGERROR("STUN response doesn't contain the transaction ID");
+ return false;
+ }
+ }
+
+ while (offset < buffer.size())
+ {
+ u16 type = 0;
+ u16 size = 0;
+ if (!GetFromBuffer(buffer, offset, type) ||
+ !GetFromBuffer(buffer, offset, size))
+ {
+ LOGERROR("STUN response contains invalid attribute");
+ return false;
+ }
+
+ // The first two bits are irrelevant to the type
+ type &= ~(m_ComprehensionOptional | m_IETFReview);
+
+ switch (type)
+ {
+ case m_AttrTypeMappedAddress:
+ case m_AttrTypeXORMappedAddress:
+ {
+ if (size != 8)
+ {
+ LOGERROR("Invalid STUN Mapped Address length");
+ return false;
+ }
+
+ // Ignore the first byte as mentioned in Section 15.1 of RFC 5389.
+ ++offset;
+
+ u8 ipFamily = 0;
+ if (!GetFromBuffer(buffer, offset, ipFamily) || ipFamily != m_IPAddressFamilyIPv4)
+ {
+ LOGERROR("Unsupported address family, IPv4 is expected");
+ return false;
+ }
+
+ u16 port = 0;
+ u32 ip = 0;
+ if (!GetFromBuffer(buffer, offset, port) ||
+ !GetFromBuffer(buffer, offset, ip))
+ {
+ LOGERROR("Mapped address doesn't contain IP and port");
+ return false;
+ }
+
+ // Obfuscation is described in Section 15.2 of RFC 5389.
+ if (type == m_AttrTypeXORMappedAddress)
+ {
+ port ^= m_MagicCookie >> 16;
+ ip ^= m_MagicCookie;
+ }
+
+ m_Port = port;
+ m_IP = ip;
+
+ break;
+ }
+ default:
+ {
+ // We don't care about other attributes at all
+
+ // Skip attribute
+ offset += size;
+
+ // Skip padding
+ int padding = size % 4;
+ if (padding)
+ offset += 4 - padding;
+ break;
+ }
+ }
+ }
+
+ return true;
+}
+
+bool STUNRequestAndResponse(ENetHost* transactionHost)
+{
+ if (!CreateStunRequest(transactionHost))
+ return false;
+
+ std::vector buffer;
+ return ReceiveStunResponse(transactionHost, buffer) &&
+ ParseStunResponse(buffer);
+}
+
+JS::Value StunClient::FindStunEndpointHost(ScriptInterface& scriptInterface, int port)
+{
+ ENetAddress hostAddr{ENET_HOST_ANY, (u16)port};
+ ENetHost* transactionHost = enet_host_create(&hostAddr, 1, 1, 0, 0);
+ if (!transactionHost)
+ {
+ LOGERROR("FindStunEndpointHost: Failed to create enet host");
+ return JS::UndefinedValue();
+ }
+
+ bool success = STUNRequestAndResponse(transactionHost);
+ enet_host_destroy(transactionHost);
+ if (!success)
+ return JS::UndefinedValue();
+
+ // Convert m_IP to string
+ char ipStr[256] = "(error)";
+ ENetAddress addr;
+ addr.host = ntohl(m_IP);
+ enet_address_get_host_ip(&addr, ipStr, ARRAY_SIZE(ipStr));
+
+ JSContext* cx = scriptInterface.GetContext();
+ JSAutoRequest rq(cx);
+
+ JS::RootedValue stunEndpoint(cx);
+ scriptInterface.Eval("({})", &stunEndpoint);
+ scriptInterface.SetProperty(stunEndpoint, "ip", CStr(ipStr));
+ scriptInterface.SetProperty(stunEndpoint, "port", m_Port);
+ return stunEndpoint;
+}
+
+StunClient::StunEndpoint* StunClient::FindStunEndpointJoin(ENetHost* transactionHost)
+{
+ ENSURE(transactionHost);
+
+ if (!STUNRequestAndResponse(transactionHost))
+ return nullptr;
+
+ // Convert m_IP to string
+ char ipStr[256] = "(error)";
+ ENetAddress addr;
+ addr.host = ntohl(m_IP);
+ enet_address_get_host_ip(&addr, ipStr, ARRAY_SIZE(ipStr));
+
+ return new StunEndpoint({ m_IP, m_Port });
+}
+
+void StunClient::SendHolePunchingMessages(ENetHost* enetClient, const char* serverAddress, u16 serverPort)
+{
+ // Convert ip string to int64
+ ENetAddress addr;
+ addr.port = serverPort;
+ enet_address_set_host(&addr, serverAddress);
+
+ int delay = 200;
+ CFG_GET_VAL("lobby.stun.delay", delay);
+
+ // Send an UDP message from enet host to ip:port
+ for (int i = 0; i < 3; ++i)
+ {
+ StunClient::SendStunRequest(enetClient, htonl(addr.host), serverPort);
+ usleep(delay * 1000);
+ }
+}