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:
elexis 2017-06-01 06:33:52 +00:00
parent cc2440c00b
commit 61261d14fc
20 changed files with 976 additions and 31 deletions

View File

@ -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"

View File

@ -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

View File

@ -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)
{

View File

@ -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>

View File

@ -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"
});
}

View File

@ -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",

View File

@ -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");

View File

@ -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;

View File

@ -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);
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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

View File

@ -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;
}

View File

@ -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.

View File

@ -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);
}

View File

@ -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.

View File

@ -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;

View File

@ -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.

View 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);
}
}

View 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