STUN + XMPP ICE implementation.
Allows lobby players to host games without having to configure their router. Differential Revision: https://code.wildfiregames.com/D364 Fixes #2305 Patch By: fcxSanya. StunClient based on code by SuperTuxKart, relicensed with approval of the according authors hilnius, hiker, Auria, deveee, Flakebi, leper, konstin and KroArtem. Added rfc5245 (ejabberd) support, a GUI option, refactoring and segfault fixes by myself. Tested By: user1, Sandarac, Sestroretsk1714, Vladislav, Grugnas, javiergodas Partially Reviewed By: leper, Philip, echotangoecho This was SVN commit r19703.
This commit is contained in:
parent
cc2440c00b
commit
61261d14fc
@ -383,6 +383,14 @@ buddies = "," ; Comma separated list of playernames that t
|
||||
[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"
|
||||
|
||||
|
@ -218,6 +218,16 @@ var g_IsTutorial;
|
||||
var g_ServerName;
|
||||
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 @@ function init(attribs)
|
||||
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 @@ function sendRegisterGameStanzaImmediate()
|
||||
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 @@ function sendRegisterGameStanzaImmediate()
|
||||
"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
|
||||
|
@ -23,6 +23,11 @@ var g_GameAttributes; // used when rejoining
|
||||
var g_PlayerAssignments; // used when rejoining
|
||||
var g_UserRating;
|
||||
|
||||
/**
|
||||
* Object containing the IP address and port of the STUN server.
|
||||
*/
|
||||
var g_StunEndpoint;
|
||||
|
||||
function init(attribs)
|
||||
{
|
||||
g_UserRating = attribs.rating;
|
||||
@ -33,7 +38,7 @@ function init(attribs)
|
||||
{
|
||||
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 @@ function init(attribs)
|
||||
}
|
||||
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 @@ function confirmSetup()
|
||||
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 @@ function pollAndHandleNetworkClient()
|
||||
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 @@ function switchSetupPage(newPage)
|
||||
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 @@ function startHost(playername, servername, port)
|
||||
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 @@ function startHost(playername, servername, port)
|
||||
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)
|
||||
{
|
||||
|
@ -94,7 +94,7 @@
|
||||
</object>
|
||||
|
||||
<object>
|
||||
<object type="text" size="20 66 50% 116" style="ModernLabelText" text_align="right">
|
||||
<object type="text" size="20 66 50% 104" style="ModernLabelText" text_align="right">
|
||||
<translatableAttribute id="caption">Server Port:</translatableAttribute>
|
||||
</object>
|
||||
|
||||
@ -108,9 +108,18 @@
|
||||
</action>
|
||||
</object>
|
||||
</object>
|
||||
|
||||
<object name="hostSTUNWrapper">
|
||||
<object name="useSTUN" size="120 116 152 146" type="checkbox" style="ModernTickBox">
|
||||
<action on="Press">saveSTUNSetting(String(this.checked));</action>
|
||||
</object>
|
||||
<object type="text" size="146 106 100% 146" style="ModernLabelText" text_align="left">
|
||||
<translatableAttribute id="caption">Use STUN to work around firewalls</translatableAttribute>
|
||||
</object>
|
||||
</object>
|
||||
</object>
|
||||
|
||||
<object name="hostFeedback" type="text" style="ModernLabelText" size="50 100%-90 100%-50 100%-50" textcolor="red" />
|
||||
<object name="hostFeedback" type="text" style="ModernLabelText" size="50 100%-80 100%-50 100%-50" 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>
|
||||
|
@ -18,6 +18,11 @@ const g_ModeratorPrefix = "@";
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@ -1035,7 +1040,20 @@ function joinSelectedGame()
|
||||
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 @@ function joinSelectedGame()
|
||||
|
||||
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"
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -623,6 +623,7 @@ function setup_all_libs ()
|
||||
extern_libs = {
|
||||
"spidermonkey",
|
||||
"boost",
|
||||
"enet",
|
||||
"gloox",
|
||||
"icu",
|
||||
"iconv",
|
||||
@ -771,6 +772,7 @@ function setup_all_libs ()
|
||||
"sdl", -- key definitions
|
||||
"opengl",
|
||||
"boost",
|
||||
"enet",
|
||||
"tinygettext",
|
||||
"icu",
|
||||
"iconv",
|
||||
|
@ -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 @@ JS::Value GetEngineInfo(ScriptInterface::CxPrivate* pCxPrivate)
|
||||
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 StartNetworkHost(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& playe
|
||||
}
|
||||
}
|
||||
|
||||
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 @@ void GuiScriptingInit(ScriptInterface& scriptInterface)
|
||||
scriptInterface.RegisterFunction<void, JS::HandleValue, int, &StartGame>("StartGame");
|
||||
scriptInterface.RegisterFunction<void, &Script_EndGame>("EndGame");
|
||||
scriptInterface.RegisterFunction<void, CStrW, u16, &StartNetworkHost>("StartNetworkHost");
|
||||
scriptInterface.RegisterFunction<void, CStrW, CStr, u16, &StartNetworkJoin>("StartNetworkJoin");
|
||||
scriptInterface.RegisterFunction<void, CStrW, CStr, u16, bool, std::string, &StartNetworkJoin>("StartNetworkJoin");
|
||||
scriptInterface.RegisterFunction<u16, &GetDefaultPort>("GetDefaultPort");
|
||||
scriptInterface.RegisterFunction<void, &DisconnectNetworkGame>("DisconnectNetworkGame");
|
||||
scriptInterface.RegisterFunction<std::string, &GetPlayerGUID>("GetPlayerGUID");
|
||||
@ -1047,6 +1091,7 @@ void GuiScriptingInit(ScriptInterface& scriptInterface)
|
||||
scriptInterface.RegisterFunction<void, int, &SendNetworkReady>("SendNetworkReady");
|
||||
scriptInterface.RegisterFunction<JS::Value, &GetAIs>("GetAIs");
|
||||
scriptInterface.RegisterFunction<JS::Value, &GetEngineInfo>("GetEngineInfo");
|
||||
scriptInterface.RegisterFunction<JS::Value, int, &FindStunEndpoint>("FindStunEndpoint");
|
||||
|
||||
// Saved games
|
||||
scriptInterface.RegisterFunction<JS::Value, std::wstring, &StartSavedGame>("StartSavedGame");
|
||||
|
@ -21,6 +21,9 @@
|
||||
#include "scriptinterface/ScriptTypes.h"
|
||||
|
||||
class ScriptInterface;
|
||||
namespace StunClient {
|
||||
class StunEndpoint;
|
||||
}
|
||||
|
||||
class IXmppClient
|
||||
{
|
||||
@ -54,6 +57,8 @@ public:
|
||||
|
||||
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;
|
||||
|
@ -19,9 +19,16 @@
|
||||
#include "XmppClient.h"
|
||||
#include "StanzaExtensions.h"
|
||||
|
||||
#ifdef WIN32
|
||||
# include <winsock2.h>
|
||||
#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 @@ 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_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 @@ XmppClient::XmppClient(const std::string& sUsername, const std::string& sPasswor
|
||||
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 @@ void XmppClient::GUIGetGameList(ScriptInterface& scriptInterface, JS::MutableHan
|
||||
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 @@ std::string XmppClient::RegistrationResultToString(gloox::RegistrationResult res
|
||||
#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);
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ namespace glooxwrapper
|
||||
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 @@ private:
|
||||
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 @@ public:
|
||||
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 @@ protected:
|
||||
/* 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;
|
||||
|
@ -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 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
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 @@ public:
|
||||
std::list<shared_ptr<gloox::IqHandler> > 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 @@ glooxwrapper::ConstTagList glooxwrapper::Tag::findTagList_clone(const string& ex
|
||||
tagListWrapper.push_back(new glooxwrapper::Tag(const_cast<gloox::Tag*>(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<gloox::Jingle::Plugin*>(plugin), false));
|
||||
return pluginListWrapper;
|
||||
}
|
||||
|
||||
glooxwrapper::Jingle::ICEUDP::Candidate glooxwrapper::Jingle::Session::Jingle::getCandidate() const
|
||||
{
|
||||
const gloox::Jingle::Content *content = static_cast<const gloox::Jingle::Content*>(m_Wrapped->plugins().front());
|
||||
if (!content)
|
||||
return glooxwrapper::Jingle::ICEUDP::Candidate();
|
||||
|
||||
const gloox::Jingle::ICEUDP *iceUDP = static_cast<const gloox::Jingle::ICEUDP*>(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<const gloox::Jingle::ICEUDP*>(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);
|
||||
}
|
||||
|
@ -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 @@ General design and rules:
|
||||
#include <gloox/mucroom.h>
|
||||
#include <gloox/registration.h>
|
||||
#include <gloox/message.h>
|
||||
#include <gloox/jinglecontent.h>
|
||||
#include <gloox/jingleiceudp.h>
|
||||
#include <gloox/jinglesessionhandler.h>
|
||||
#include <gloox/jinglesessionmanager.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
@ -101,6 +105,7 @@ namespace glooxwrapper
|
||||
|
||||
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 @@ namespace glooxwrapper
|
||||
}
|
||||
m_Head = m_Tail = NULL;
|
||||
}
|
||||
|
||||
const T& front() const
|
||||
{
|
||||
return *begin();
|
||||
}
|
||||
};
|
||||
|
||||
typedef glooxwrapper::list<Tag*> TagList;
|
||||
@ -571,6 +581,98 @@ namespace glooxwrapper
|
||||
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<const Plugin*> 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<Candidate> 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
|
||||
|
@ -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 @@ void CNetClient::SetUserName(const CStrW& username)
|
||||
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;
|
||||
}
|
||||
|
@ -33,6 +33,8 @@ class CNetClientTurnManager;
|
||||
class CNetServer;
|
||||
class ScriptInterface;
|
||||
|
||||
typedef struct _ENetHost ENetHost;
|
||||
|
||||
// NetClient session FSM states
|
||||
enum
|
||||
{
|
||||
@ -99,7 +101,7 @@ public:
|
||||
* @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.
|
||||
|
@ -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 @@ CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original)
|
||||
}
|
||||
}
|
||||
|
||||
void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port)
|
||||
{
|
||||
StunClient::SendHolePunchingMessages(m_Host, ipStr.c_str(), port);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1506,3 +1512,8 @@ void CNetServer::SetTurnLength(u32 msecs)
|
||||
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);
|
||||
}
|
||||
|
@ -134,6 +134,8 @@ public:
|
||||
*/
|
||||
void SetTurnLength(u32 msecs);
|
||||
|
||||
void SendHolePunchingMessage(const CStr& ip, u16 port);
|
||||
|
||||
private:
|
||||
CNetServerWorker* m_Worker;
|
||||
};
|
||||
@ -271,6 +273,8 @@ private:
|
||||
*/
|
||||
void CheckClientConnections();
|
||||
|
||||
void SendHolePunchingMessage(const CStr& ip, u16 port);
|
||||
|
||||
/**
|
||||
* Internal script context for (de)serializing script messages,
|
||||
* and for storing game attributes.
|
||||
|
@ -52,13 +52,18 @@ CNetClientSession::~CNetClientSession()
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
@ -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 CNetServerWorker;
|
||||
|
||||
class CNetStatsTable;
|
||||
|
||||
typedef struct _ENetHost ENetHost;
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Network client/server sessions.
|
||||
@ -70,7 +72,7 @@ public:
|
||||
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.
|
||||
|
426
source/network/StunClient.cpp
Normal file
426
source/network/StunClient.cpp
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "precompiled.h"
|
||||
|
||||
#include "StunClient.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <sys/types.h>
|
||||
#ifdef WIN32
|
||||
# include <winsock2.h>
|
||||
# include <ws2tcpip.h>
|
||||
#else
|
||||
# include <sys/socket.h>
|
||||
# include <netdb.h>
|
||||
#endif
|
||||
|
||||
#include <vector>
|
||||
|
||||
#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<u8>& buffer, const u16 value)
|
||||
{
|
||||
buffer.push_back((value >> 8) & 0xff);
|
||||
buffer.push_back(value & 0xff);
|
||||
}
|
||||
|
||||
void AddUInt32(std::vector<u8>& 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<typename T, size_t n>
|
||||
bool GetFromBuffer(std::vector<u8> 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<u8> 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<u8>& 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<u8>& buffer)
|
||||
{
|
||||
u32 offset = 0;
|
||||
|
||||
u16 responseType = 0;
|
||||
if (!GetFromBuffer<u16, 2>(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<u32, 4>(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<u8, 1>(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<u16, 2>(buffer, offset, type) ||
|
||||
!GetFromBuffer<u16, 2>(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<u8, 1>(buffer, offset, ipFamily) || ipFamily != m_IPAddressFamilyIPv4)
|
||||
{
|
||||
LOGERROR("Unsupported address family, IPv4 is expected");
|
||||
return false;
|
||||
}
|
||||
|
||||
u16 port = 0;
|
||||
u32 ip = 0;
|
||||
if (!GetFromBuffer<u16, 2>(buffer, offset, port) ||
|
||||
!GetFromBuffer<u32, 4>(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<u8> 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);
|
||||
}
|
||||
}
|
44
source/network/StunClient.h
Normal file
44
source/network/StunClient.h
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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
|
Loading…
Reference in New Issue
Block a user