Index: binaries/data/config/default.cfg =================================================================== --- binaries/data/config/default.cfg +++ binaries/data/config/default.cfg @@ -377,6 +377,11 @@ xpartamupp = "wfgbot22" ; Name of the server-side xmpp client that manage games buddies = "," ; Comma separated list of playernames that the current user has marked as buddies +[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 = "stun.ekiga.net" ; Address of STUN server + [mod] enabledmods = "mod public" Index: binaries/data/mods/public/gui/gamesetup/gamesetup.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/gamesetup.js +++ binaries/data/mods/public/gui/gamesetup/gamesetup.js @@ -206,6 +206,11 @@ var g_ServerPort; /** + * IP address of the STUN endpoint. + */ +var g_StunEndpoint; + +/** * 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. */ @@ -293,6 +298,11 @@ var g_LastViewedAIPlayer = -1; /** + * Current username. Cannot contain whitespace. + */ +var g_Username = Engine.LobbyGetNick(); + +/** * Order in which the GUI elements will be shown. * All valid options are required to appear here. * The ones under "map" are shown in the map selection panel, @@ -867,6 +877,7 @@ g_IsController = attribs.type != "client"; g_ServerName = attribs.serverName; g_ServerPort = attribs.serverPort; + g_StunEndpoint = attribs.stunEndpoint; if (!g_IsNetworked) g_PlayerAssignments = { @@ -2106,6 +2117,7 @@ let stanza = { "name": g_ServerName, "port": g_ServerPort, + "hostUsername": g_Username, "mapName": g_GameAttributes.map, "niceMapName": getMapDisplayName(g_GameAttributes.map), "mapSize": g_GameAttributes.mapType == "random" ? g_GameAttributes.settings.Size : "Default", @@ -2116,6 +2128,12 @@ "players": clients.list, }; + if (g_StunEndpoint !== undefined) + { + stanza.stunIP = g_StunEndpoint.ip; + stanza.stunPort = g_StunEndpoint.port; + } + // Only send the stanza if the relevant settings actually changed if (g_LastGameStanza && Object.keys(stanza).every(prop => g_LastGameStanza[prop] == stanza[prop])) return; Index: binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js =================================================================== --- binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js +++ binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js @@ -23,6 +23,11 @@ var g_PlayerAssignments; // used when rejoining var g_UserRating; +/** + * Object containing the IP address and port of the stun server. + */ +var g_StunEndpoint; + function init(attribs) { g_UserRating = attribs.rating; @@ -33,7 +38,7 @@ { if (Engine.HasXmppClient()) { - if (startJoin(attribs.name, attribs.ip, getValidPort(attribs.port))) + if (startJoin(attribs.name, attribs.ip, getValidPort(attribs.port), attribs.hostJID)) switchSetupPage("pageConnecting"); } else @@ -47,6 +52,9 @@ Engine.GetGUIObjectByName("hostPlayerName").caption = attribs.name; Engine.GetGUIObjectByName("hostServerName").caption = sprintf(translate("%(name)s's game"), { "name": attribs.name }); + + Engine.GetGUIObjectByName("hostSTUNWrapper").hidden = false; + Engine.GetGUIObjectByName("useSTUN").checked = Engine.ConfigDB_GetValue("user", "stun.enabled") == "true"; } switchSetupPage("pageHost"); @@ -230,7 +238,8 @@ Engine.SwitchGuiPage("page_gamesetup.xml", { "type": g_GameType, "serverName": g_ServerName, - "serverPort": g_ServerPort + "serverPort": g_ServerPort, + "stunEndpoint": g_StunEndpoint }); return; // don't process any more messages - leave them for the game GUI loop } @@ -271,6 +280,12 @@ Engine.GetGUIObjectByName("continueButton").hidden = newPage == "pageConnecting"; } +function saveSTUNSetting(enabled) +{ + Engine.ConfigDB_CreateValue("user", "stun.enabled", enabled); + Engine.ConfigDB_WriteValueToFile("user", "stun.enabled", enabled, "config/user.cfg"); +} + function startHost(playername, servername, port) { startConnectionStatus("server"); @@ -293,6 +308,9 @@ return false; } + if (Engine.HasXmppClient() && Engine.GetGUIObjectByName("useSTUN").checked) + g_StunEndpoint = Engine.FindStunEndpoint(port); + try { if (g_UserRating) @@ -320,14 +338,11 @@ return true; } -function startJoin(playername, ip, port) +function startJoin(playername, ip, port, 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, hostJID); } catch (e) { Index: binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.xml =================================================================== --- binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.xml +++ binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.xml @@ -94,7 +94,7 @@ - + Server Port: @@ -108,6 +108,15 @@ + + + + saveSTUNSetting(String(this.checked)); + + + Use STUN to work around firewalls + + Index: binaries/data/mods/public/gui/lobby/lobby.js =================================================================== --- binaries/data/mods/public/gui/lobby/lobby.js +++ binaries/data/mods/public/gui/lobby/lobby.js @@ -29,6 +29,17 @@ const g_Username = Engine.LobbyGetNick(); /** + * If STUN is enabled for both host and client, we are using it + * to discovered NAT-mapped host endpoint + */ +const g_StunEnabled = Engine.ConfigDB_GetValue("user", "stun.enabled") == "true"; + +/** + * Lobby server address to construct host JID + */ +const g_LobbyServer = Engine.ConfigDB_GetValue("user", "lobby.server"); + +/** * Current games will be listed in these colors. */ const g_GameColors = { @@ -988,7 +999,20 @@ if (!game) return; - if (game.ip.split('.').length != 4) + let ip; + let port; + if (g_StunEnabled && game.stunIP) + { + ip = game.stunIP; + port = game.stunPort; + } + else + { + ip = game.ip; + port = game.port; + } + + if (ip.split('.').length != 4) { addChatMessage({ "from": "system", @@ -1000,12 +1024,15 @@ return; } + let hostJID = game.hostUsername + "@" + g_LobbyServer + "/0ad"; + 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, + "hostJID": hostJID }); } Index: build/premake/premake4.lua =================================================================== --- build/premake/premake4.lua +++ build/premake/premake4.lua @@ -623,6 +623,7 @@ extern_libs = { "spidermonkey", "boost", + "enet", "gloox", "icu", "iconv", @@ -771,6 +772,7 @@ "sdl", -- key definitions "opengl", "boost", + "enet", "tinygettext", "icu", "iconv", Index: source/gui/scripting/ScriptFunctions.cpp =================================================================== --- source/gui/scripting/ScriptFunctions.cpp +++ source/gui/scripting/ScriptFunctions.cpp @@ -30,15 +30,18 @@ #include "gui/scripting/JSInterface_GUITypes.h" #include "i18n/L10n.h" #include "i18n/scripting/JSInterface_L10n.h" +#include "lib/external_libraries/enet.h" #include "lib/svn_revision.h" #include "lib/sysdep/sysdep.h" #include "lib/timer.h" #include "lib/utf8.h" #include "lobby/scripting/JSInterface_Lobby.h" +#include "lobby/IXmppClient.h" #include "maths/FixedVector3D.h" #include "network/NetClient.h" #include "network/NetMessage.h" #include "network/NetServer.h" +#include "network/StunClient.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/Errors.h" @@ -237,6 +240,11 @@ return SavedGames::GetEngineInfo(*(pCxPrivate->pScriptInterface)); } +JS::Value FindStunEndpoint(ScriptInterface::CxPrivate* pCxPrivate, int port) +{ + return StunClient::FindStunEndpoint(*(pCxPrivate->pScriptInterface), port); +} + void StartNetworkGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { ENSURE(g_NetClient); @@ -360,16 +368,45 @@ } } -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, const std::string& hostJID) { ENSURE(!g_NetClient); ENSURE(!g_NetServer); ENSURE(!g_Game); + bool stunEnabled(false); + CFG_GET_VAL("stun.enabled", stunEnabled); + ENetHost* enetClient = nullptr; + if (stunEnabled) + { + // By default we are binding client to the same port as host (20595), + // if there are multiple 0ad instances running on the same machine + // (host + client or multiple STUN-enabled clients), this will fail + // (with enet_host_create returning null as a result), + // so we are also trying a couple of subsequent ports. + ENetAddress hostAddr{ENET_HOST_ANY, 20595}; + for (int i = 0; i < 3 && enetClient == nullptr; ++i) + { + enetClient = enet_host_create(&hostAddr, 1, 1, 0, 0); + hostAddr.port++; + } + + StunClient::StunEndpoint stunEndpoint = StunClient::FindStunEndpoint(enetClient); + g_XmppClient->SendStunEndpointToHost(stunEndpoint, hostJID); + // Note: we are sending endpoint and starting to connect right away + // we may actually need to wait for host's response (this needs + // to be checked) + 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 (stunEnabled) + 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); @@ -1027,7 +1064,7 @@ scriptInterface.RegisterFunction("StartGame"); scriptInterface.RegisterFunction("EndGame"); scriptInterface.RegisterFunction("StartNetworkHost"); - scriptInterface.RegisterFunction("StartNetworkJoin"); + scriptInterface.RegisterFunction("StartNetworkJoin"); scriptInterface.RegisterFunction("GetDefaultPort"); scriptInterface.RegisterFunction("DisconnectNetworkGame"); scriptInterface.RegisterFunction("GetPlayerGUID"); @@ -1040,6 +1077,7 @@ scriptInterface.RegisterFunction("SendNetworkReady"); scriptInterface.RegisterFunction("GetAIs"); scriptInterface.RegisterFunction("GetEngineInfo"); + scriptInterface.RegisterFunction("FindStunEndpoint"); // Saved games scriptInterface.RegisterFunction("StartSavedGame"); Index: source/lobby/IXmppClient.h =================================================================== --- source/lobby/IXmppClient.h +++ source/lobby/IXmppClient.h @@ -21,6 +21,9 @@ #include "scriptinterface/ScriptTypes.h" class ScriptInterface; +namespace StunClient { +class StunEndpoint; +} class IXmppClient { @@ -54,6 +57,8 @@ virtual void GuiPollMessage(ScriptInterface& scriptInterface, JS::MutableHandleValue ret) = 0; virtual void SendMUCMessage(const std::string& message) = 0; + + virtual void SendStunEndpointToHost(StunClient::StunEndpoint& stunEndpoint, const std::string& hostJID) = 0; }; extern IXmppClient *g_XmppClient; Index: source/lobby/XmppClient.h =================================================================== --- source/lobby/XmppClient.h +++ source/lobby/XmppClient.h @@ -33,7 +33,7 @@ struct CertInfo; } -class XmppClient : public IXmppClient, public glooxwrapper::ConnectionListener, public glooxwrapper::MUCRoomHandler, public glooxwrapper::IqHandler, public glooxwrapper::RegistrationHandler, public glooxwrapper::MessageHandler +class XmppClient : public IXmppClient, public glooxwrapper::ConnectionListener, public glooxwrapper::MUCRoomHandler, public glooxwrapper::IqHandler, public glooxwrapper::RegistrationHandler, public glooxwrapper::MessageHandler, public glooxwrapper::Jingle::SessionHandler { NONCOPYABLE(XmppClient); @@ -42,6 +42,7 @@ glooxwrapper::Client* m_client; glooxwrapper::MUCRoom* m_mucRoom; glooxwrapper::Registration* m_registration; + glooxwrapper::SessionManager* m_sessionManager; // Account infos std::string m_username; @@ -81,6 +82,8 @@ void GUIGetBoardList(ScriptInterface& scriptInterface, JS::MutableHandleValue ret); void GUIGetProfile(ScriptInterface& scriptInterface, JS::MutableHandleValue ret); + void SendStunEndpointToHost(StunClient::StunEndpoint& stunEndpoint, const std::string& hostJID); + //Script ScriptInterface& GetScriptInterface(); @@ -119,6 +122,9 @@ /* 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); + // Helpers void GetPresenceString(const gloox::Presence::PresenceType p, std::string& presence) const; void GetRoleString(const gloox::MUCRoomRole r, std::string& role) const; @@ -126,6 +132,8 @@ std::string ConnectionErrorToString(gloox::ConnectionError err) const; std::string RegistrationResultToString(gloox::RegistrationResult res) const; + void ProcessJingleData(const glooxwrapper::Jingle::Session::Jingle *jingle); + public: /* Messages */ struct GUIMessage Index: source/lobby/XmppClient.cpp =================================================================== --- source/lobby/XmppClient.cpp +++ source/lobby/XmppClient.cpp @@ -19,9 +19,16 @@ #include "XmppClient.h" #include "StanzaExtensions.h" +#ifdef WIN32 +# include +#endif + #include "glooxwrapper/glooxwrapper.h" #include "i18n/L10n.h" +#include "lib/external_libraries/enet.h" #include "lib/utf8.h" +#include "network/NetServer.h" +#include "network/StunClient.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Pyrogenesis.h" @@ -68,7 +75,7 @@ * @param regOpt If we are just registering or not. */ XmppClient::XmppClient(const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize, bool regOpt) - : m_client(NULL), m_mucRoom(NULL), m_registration(NULL), m_username(sUsername), m_password(sPassword), m_nick(sNick), m_initialLoadComplete(false) + : m_client(NULL), m_mucRoom(NULL), m_registration(NULL), m_username(sUsername), m_password(sPassword), m_nick(sNick), m_initialLoadComplete(false), m_sessionManager() { // Read lobby configuration from default.cfg std::string sServer; @@ -129,6 +136,11 @@ m_registration = new glooxwrapper::Registration(m_client); m_registration->registerRegistrationHandler(this); } + + m_sessionManager = new glooxwrapper::SessionManager(m_client, this); + // Register plugins to allow gloox parse them in incoming sessions + m_sessionManager->registerPlugins(); + } /** @@ -472,7 +484,7 @@ JSAutoRequest rq(cx); scriptInterface.Eval("([])", ret); - const char* stats[] = { "name", "ip", "port", "state", "nbp", "maxnbp", "players", "mapName", "niceMapName", "mapSize", "mapType", "victoryCondition", "startTime" }; + const char* stats[] = { "name", "ip", "port", "stunIP", "stunPort", "hostUsername", "state", "nbp", "maxnbp", "players", "mapName", "niceMapName", "mapSize", "mapType", "victoryCondition", "startTime" }; for(const glooxwrapper::Tag* const& t : m_GameList) { JS::RootedValue game(cx); @@ -1084,3 +1096,31 @@ #undef DEBUG_CASE #undef CASE } + + +void XmppClient::SendStunEndpointToHost(StunClient::StunEndpoint& stunEndpoint, const std::string& hostJIDStr) +{ + glooxwrapper::JID hostJID(hostJIDStr); + glooxwrapper::Jingle::Session session = m_sessionManager->createSession(hostJID); + + char ipStr[256] = "(error)"; + ENetAddress addr; + addr.host = ntohl(stunEndpoint.ip); + enet_address_get_host_ip(&addr, ipStr, ARRAY_SIZE(ipStr)); + + 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) + return; + + ProcessJingleData(jingle); +} + +void XmppClient::ProcessJingleData(const glooxwrapper::Jingle::Session::Jingle *jingle) +{ + glooxwrapper::Jingle::ICEUDP::Candidate candidate = jingle->getCandidate(); + g_NetServer->SendHolePunchingMessage(candidate.ip.to_string(), candidate.port); +} Index: source/lobby/glooxwrapper/glooxwrapper.h =================================================================== --- source/lobby/glooxwrapper/glooxwrapper.h +++ source/lobby/glooxwrapper/glooxwrapper.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2014 Wildfire Games. +/* Copyright (C) 2017 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -72,6 +72,10 @@ #include #include #include +#include +#include +#include +#include #include @@ -101,6 +105,7 @@ class ClientImpl; class MUCRoomHandlerWrapper; + class SessionHandlerWrapper; GLOOXWRAPPER_API void* glooxwrapper_alloc(size_t size); GLOOXWRAPPER_API void glooxwrapper_free(void* p); @@ -256,6 +261,11 @@ } m_Head = m_Tail = NULL; } + + const T& front() const + { + return *begin(); + } }; typedef glooxwrapper::list TagList; @@ -571,6 +581,98 @@ const Tag* findTag_clone(const string& expression) const; // like findTag but must be Tag::free()d ConstTagList findTagList_clone(const string& expression) const; // like findTagList but each tag must be Tag::free()d }; + + namespace Jingle + { + + class GLOOXWRAPPER_API Plugin + { + protected: + const gloox::Jingle::Plugin* m_Wrapped; + bool m_Owned; + + public: + Plugin(const gloox::Jingle::Plugin* wrapped, bool owned) : m_Wrapped(wrapped), m_Owned(owned) {} + + virtual ~Plugin(); + + const Plugin findPlugin(int type) const; + const gloox::Jingle::Plugin* getWrapped() const { return m_Wrapped; } + }; + + typedef list PluginList; + + class GLOOXWRAPPER_API Content : public Plugin + { + public: + Content(const string& name, const PluginList& plugins); + Content(); + }; + + class GLOOXWRAPPER_API ICEUDP : public Plugin + { + public: + struct Candidate { + string ip; + int port; + }; + + typedef std::list CandidateList; + + ICEUDP(CandidateList& candidates); + ICEUDP(); + + const CandidateList candidates() const; + }; + + class GLOOXWRAPPER_API Session + { + protected: + gloox::Jingle::Session* m_Wrapped; + bool m_Owned; + + public: + class GLOOXWRAPPER_API Jingle + { + private: + const gloox::Jingle::Session::Jingle* m_Wrapped; + bool m_Owned; + public: + Jingle(const gloox::Jingle::Session::Jingle* wrapped, bool owned) : m_Wrapped(wrapped), m_Owned(owned) {} + + const PluginList plugins() const; + + ICEUDP::Candidate getCandidate() const; + }; + + Session(gloox::Jingle::Session* wrapped, bool owned) : m_Wrapped(wrapped), m_Owned(owned) {} + + bool sessionInitiate(char* ipStr, uint16_t port); + }; + + class GLOOXWRAPPER_API SessionHandler + { + public: + virtual ~SessionHandler() {} + virtual void handleSessionAction(gloox::Jingle::Action action, Session *session, const Session::Jingle *jingle) = 0; + }; + + } + + class GLOOXWRAPPER_API SessionManager + { + private: + gloox::Jingle::SessionManager* m_Wrapped; + SessionHandlerWrapper* m_HandlerWrapper; + + public: + SessionManager(Client* parent, Jingle::SessionHandler* sh); + ~SessionManager(); + void registerPlugins(); + Jingle::Session createSession(const JID& callee); + }; + + } #endif // INCLUDED_GLOOXWRAPPER_H Index: source/lobby/glooxwrapper/glooxwrapper.cpp =================================================================== --- source/lobby/glooxwrapper/glooxwrapper.cpp +++ source/lobby/glooxwrapper/glooxwrapper.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2015 Wildfire Games. +/* Copyright (C) 2017 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -277,6 +277,29 @@ } }; +class SessionHandlerWrapper : public gloox::Jingle::SessionHandler +{ +public: + glooxwrapper::Jingle::SessionHandler* m_Wrapped; + bool m_Owned; + + SessionHandlerWrapper(glooxwrapper::Jingle::SessionHandler* wrapped, bool owned) + : m_Wrapped(wrapped), m_Owned(owned) {} + + virtual void handleSessionAction(gloox::Jingle::Action action, gloox::Jingle::Session* session, const gloox::Jingle::Session::Jingle* jingle) + { + m_Wrapped->handleSessionAction(action, new glooxwrapper::Jingle::Session(session, false), new glooxwrapper::Jingle::Session::Jingle(jingle, false)); + } + + virtual void handleSessionActionError(gloox::Jingle::Action UNUSED(action), gloox::Jingle::Session* UNUSED(session), const gloox::Error* UNUSED(error)) + { + } + + virtual void handleIncomingSession(gloox::Jingle::Session* UNUSED(session)) + { + } +}; + class ClientImpl { public: @@ -286,6 +309,39 @@ std::list > m_IqHandlers; }; +static const std::string XMLNS = "xmlns"; +static const std::string XMLNS_JINGLE_0AD_GAME = "urn:xmpp:jingle:apps:0ad-game:1"; + +class ZeroADGameData : public gloox::Jingle::Plugin +{ +public: + ZeroADGameData() : Plugin(gloox::Jingle::PluginUser) {} + + ZeroADGameData(const gloox::Tag* UNUSED(tag)) : Plugin(gloox::Jingle::PluginUser) + { + } + + const std::string& filterString() const { + static const std::string filter = "content/description[@xmlns='" + XMLNS_JINGLE_0AD_GAME + "']"; + return filter; + } + + gloox::Tag* tag() const { + gloox::Tag* r = new gloox::Tag("description", XMLNS, XMLNS_JINGLE_0AD_GAME); + return r; + } + + Plugin* newInstance(const gloox::Tag* tag) const + { + return new ZeroADGameData(tag); + } + + Plugin* clone() const + { + return new ZeroADGameData(*this); + } +}; + } // namespace glooxwrapper @@ -737,3 +793,163 @@ tagListWrapper.push_back(new glooxwrapper::Tag(const_cast(t), false)); return tagListWrapper; } + +glooxwrapper::Jingle::Plugin::~Plugin() +{ + if (m_Owned) + delete m_Wrapped; +} + +const glooxwrapper::Jingle::Plugin glooxwrapper::Jingle::Plugin::findPlugin(int type) const +{ + return glooxwrapper::Jingle::Plugin(m_Wrapped->findPlugin(type), false); +} + +glooxwrapper::Jingle::Content::Content(const string& name, const PluginList& plugins) + : glooxwrapper::Jingle::Plugin(NULL, false) +{ + gloox::Jingle::PluginList glooxPluginList; + for (const glooxwrapper::Jingle::Plugin* const& plugin: plugins) + glooxPluginList.push_back(plugin->getWrapped()); + + m_Wrapped = new gloox::Jingle::Content(name.to_string(), glooxPluginList); + m_Owned = true; +} + +glooxwrapper::Jingle::Content::Content() + : glooxwrapper::Jingle::Plugin(NULL, false) +{ + m_Wrapped = new gloox::Jingle::Content(); + m_Owned = true; +} + +const glooxwrapper::Jingle::PluginList glooxwrapper::Jingle::Session::Jingle::plugins() const +{ + glooxwrapper::Jingle::PluginList pluginListWrapper; + for (const gloox::Jingle::Plugin* const& plugin : m_Wrapped->plugins()) + pluginListWrapper.push_back(new glooxwrapper::Jingle::Plugin(const_cast(plugin), false)); + return pluginListWrapper; +} + +glooxwrapper::Jingle::ICEUDP::Candidate glooxwrapper::Jingle::Session::Jingle::getCandidate() const +{ + const gloox::Jingle::Content *content = static_cast(m_Wrapped->plugins().front()); + if (content == NULL) + { + printf("Failed to retrieve Jingle content\n"); + return glooxwrapper::Jingle::ICEUDP::Candidate(); + } + + const ZeroADGameData *gameData = static_cast(content->findPlugin(gloox::Jingle::PluginUser)); + if (gameData == NULL) + { + printf("Failed to retrieve Jingle game data\n"); + return glooxwrapper::Jingle::ICEUDP::Candidate(); + } + + const gloox::Jingle::ICEUDP *iceUDP = static_cast(content->findPlugin(gloox::Jingle::PluginICEUDP)); + if (iceUDP == NULL) + { + printf("Failed to retrieve Jingle ICE-UDP data\n"); + 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, uint16_t port) +{ + ZeroADGameData *gameData = new ZeroADGameData(); + + gloox::Jingle::ICEUDP::CandidateList *candidateList = new gloox::Jingle::ICEUDP::CandidateList(); + + candidateList->push_back(gloox::Jingle::ICEUDP::Candidate + { + /*component_id*/ "1", + /*foundation*/ "1", + /*candidate_generation*/ "0", + /*candidate_id*/ "1", + ipStr, + /*network*/ "", + port, + /*priotiry*/ 0, + "udp", + /*base_ip*/ "", + /*base_port*/ 0, + /*type*/ gloox::Jingle::ICEUDP::ServerReflexive + }); + + gloox::Jingle::ICEUDP *iceUDP = new gloox::Jingle::ICEUDP(/*local_pwd*/"", /*local_ufrag*/"", *candidateList); + + gloox::Jingle::PluginList *pluginList = new gloox::Jingle::PluginList(); + pluginList->push_back(gameData); + pluginList->push_back(iceUDP); + gloox::Jingle::Content *content = new gloox::Jingle::Content(std::string("game-data"), *pluginList); + + return m_Wrapped->sessionInitiate(content); +} + +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 + { + /*component_id*/ "1", + /*foundation*/ "1", + /*candidate_generation*/ "0", + /*candidate_id*/ "1", + candidate.ip.to_string(), + /*network*/ "", + candidate.port, + /*priotiry*/0, "udp", + /*base_ip*/ "", + /*base_port*/ 0, + /*type*/ gloox::Jingle::ICEUDP::ServerReflexive + }); + + m_Wrapped = new gloox::Jingle::ICEUDP(/*local_pwd*/"", /*local_ufrag*/"", glooxCandidates); + m_Owned = true; +} + +glooxwrapper::Jingle::ICEUDP::ICEUDP() + : glooxwrapper::Jingle::Plugin(NULL, false) +{ + m_Wrapped = new gloox::Jingle::ICEUDP(); + m_Owned = true; +} + +const glooxwrapper::Jingle::ICEUDP::CandidateList glooxwrapper::Jingle::ICEUDP::candidates() const +{ + glooxwrapper::Jingle::ICEUDP::CandidateList candidateListWrapper; + for (const gloox::Jingle::ICEUDP::Candidate candidate : static_cast(m_Wrapped)->candidates()) + candidateListWrapper.push_back(glooxwrapper::Jingle::ICEUDP::Candidate{candidate.ip, candidate.port}); + return candidateListWrapper; +} + +glooxwrapper::SessionManager::SessionManager(Client* parent, Jingle::SessionHandler* sh) +{ + m_HandlerWrapper = new SessionHandlerWrapper(sh, false); + m_Wrapped = new gloox::Jingle::SessionManager(parent->getWrapped(), m_HandlerWrapper); +} + +glooxwrapper::SessionManager::~SessionManager() +{ + delete m_Wrapped; + delete m_HandlerWrapper; +} + +void glooxwrapper::SessionManager::registerPlugins() +{ + m_Wrapped->registerPlugin(new gloox::Jingle::Content()); + m_Wrapped->registerPlugin(new gloox::Jingle::ICEUDP()); + m_Wrapped->registerPlugin(new ZeroADGameData()); +} + +glooxwrapper::Jingle::Session glooxwrapper::SessionManager::createSession(const JID& callee) +{ + gloox::Jingle::Session* glooxSession = m_Wrapped->createSession(callee.getWrapped(), m_HandlerWrapper); + return glooxwrapper::Jingle::Session(glooxSession, false); +} Index: source/network/NetClient.h =================================================================== --- source/network/NetClient.h +++ source/network/NetClient.h @@ -33,6 +33,8 @@ class CNetServer; class ScriptInterface; +typedef struct _ENetHost ENetHost; + // NetClient session FSM states enum { @@ -99,7 +101,7 @@ * @param server IP address or host name to connect to * @return true on success, false on connection failure */ - bool SetupConnection(const CStr& server, const u16 port); + bool SetupConnection(const CStr& server, const u16 port, ENetHost* enetClient = NULL); /** * Destroy the connection to the server. Index: source/network/NetClient.cpp =================================================================== --- source/network/NetClient.cpp +++ source/network/NetClient.cpp @@ -24,6 +24,7 @@ #include "NetSession.h" #include "lib/byte_order.h" +#include "lib/external_libraries/enet.h" #include "lib/sysdep/sysdep.h" #include "ps/CConsole.h" #include "ps/CLogger.h" @@ -158,10 +159,10 @@ m_UserName = username; } -bool CNetClient::SetupConnection(const CStr& server, const u16 port) +bool CNetClient::SetupConnection(const CStr& server, const u16 port, ENetHost* enetClient) { CNetClientSession* session = new CNetClientSession(*this); - bool ok = session->Connect(server, port, m_IsLocalClient); + bool ok = session->Connect(server, port, m_IsLocalClient, enetClient); SetAndOwnSession(session); return ok; } Index: source/network/NetServer.h =================================================================== --- source/network/NetServer.h +++ source/network/NetServer.h @@ -134,6 +134,8 @@ */ void SetTurnLength(u32 msecs); + void SendHolePunchingMessage(const CStr& ip, u16 port); + private: CNetServerWorker* m_Worker; }; @@ -271,6 +273,8 @@ */ void CheckClientConnections(); + void SendHolePunchingMessage(const CStr& ip, u16 port); + /** * Internal script context for (de)serializing script messages, * and for storing game attributes. Index: source/network/NetServer.cpp =================================================================== --- source/network/NetServer.cpp +++ source/network/NetServer.cpp @@ -26,6 +26,7 @@ #include "NetStats.h" #include "lib/external_libraries/enet.h" +#include "network/StunClient.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Profile.h" @@ -1445,6 +1446,11 @@ } } +void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port) +{ + StunClient::SendHolePunchingMessages(m_Host, ipStr.c_str(), port); +} + @@ -1484,3 +1490,8 @@ CScopeLock lock(m_Worker->m_WorkerMutex); m_Worker->m_TurnLengthQueue.push_back(msecs); } + +void CNetServer::SendHolePunchingMessage(const CStr& ip, u16 port) +{ + m_Worker->SendHolePunchingMessage(ip, port); +} Index: source/network/NetSession.h =================================================================== --- source/network/NetSession.h +++ source/network/NetSession.h @@ -39,6 +39,8 @@ class CNetStatsTable; +typedef struct _ENetHost ENetHost; + /** * @file * Network client/server sessions. @@ -70,7 +72,7 @@ CNetClientSession(CNetClient& client); ~CNetClientSession(); - bool Connect(const CStr& server, const u16 port, const bool isLocalClient); + bool Connect(const CStr& server, const u16 port, const bool isLocalClient, ENetHost* enetClient); /** * Process queued incoming messages. Index: source/network/NetSession.cpp =================================================================== --- source/network/NetSession.cpp +++ source/network/NetSession.cpp @@ -52,13 +52,18 @@ } } -bool CNetClientSession::Connect(const CStr& server, const u16 port, const bool isLocalClient) +bool CNetClientSession::Connect(const CStr& server, const u16 port, const bool isLocalClient, ENetHost* enetClient) { ENSURE(!m_Host); ENSURE(!m_Server); // Create ENet host - ENetHost* host = enet_host_create(NULL, 1, CHANNEL_COUNT, 0, 0); + ENetHost* host; + if (enetClient != NULL) + host = enetClient; + else + host = enet_host_create(NULL, 1, CHANNEL_COUNT, 0, 0); + if (!host) return false; Index: source/network/StunClient.h =================================================================== --- /dev/null +++ source/network/StunClient.h @@ -0,0 +1,43 @@ +// SuperTuxKart - a fun racing game with go-kart +// Copyright (C) 2013-2016 SuperTuxKart-Team +// +// This program 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. +// +// This program 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 this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +#ifndef STUNCLIENT_H +#define STUNCLIENT_H + +#include "scriptinterface/ScriptInterface.h" + +typedef struct _ENetHost ENetHost; + +namespace StunClient +{ + +struct StunEndpoint { + uint32_t ip; + uint16_t port; +}; + +void SendStunRequest(ENetHost* transactionHost, uint32_t targetIp, uint16_t targetPort); + +JS::Value FindStunEndpoint(ScriptInterface& scriptInterface, int port); + +StunEndpoint FindStunEndpoint(ENetHost* transactionHost); + +void SendHolePunchingMessages(ENetHost* enetClient, const char* serverAddress, u16 serverPort); + +} + +#endif // STUNCLIENT_H Index: source/network/StunClient.cpp =================================================================== --- /dev/null +++ source/network/StunClient.cpp @@ -0,0 +1,364 @@ +// SuperTuxKart - a fun racing game with go-kart +// Copyright (C) 2013-2016 SuperTuxKart-Team +// +// This program 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. +// +// This program 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 this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +#include "precompiled.h" + +#include "StunClient.h" + +#include +#include +#include + +#include +#include + +#include +#ifdef WIN32 +# include +# include +#else +# include +# include +#endif + +#include + +#include "lib/external_libraries/enet.h" +#include "scriptinterface/ScriptInterface.h" +#include "ps/CLogger.h" +#include "ps/ConfigDB.h" + +unsigned int m_StunServerIP; +static const int m_StunServerPort = 3478; +const uint32_t m_StunMagicCookie = 0x2112A442; +uint8_t m_StunTransactionID[12]; + +/** + * Discovered STUN endpoint + */ +uint32_t m_IP; +uint16_t m_Port; + +void AddUInt16(std::vector& m_buffer, const uint16_t value) +{ + m_buffer.push_back((value >> 8) & 0xff); + m_buffer.push_back(value & 0xff); +} + +void AddUInt32(std::vector& m_buffer, const uint32_t& value) +{ + m_buffer.push_back((value >> 24) & 0xff); + m_buffer.push_back((value >> 16) & 0xff); + m_buffer.push_back((value >> 8) & 0xff); + m_buffer.push_back( value & 0xff); +} + +template +T GetFromBuffer(std::vector m_buffer, int& m_current_offset) +{ + int a = n; + T result = 0; + m_current_offset += n; + int offset = m_current_offset -1; + while (a--) + { + result <<= 8; + result += m_buffer[offset - a]; + } + return result; +} + +/** + * Creates a STUN request and sends it to a STUN server. + * See https://tools.ietf.org/html/rfc5389#section-6 + * for details on the message structure. + * The request is send through m_transaction_host, from which the answer + * will be retrieved by ParseStunResponse() + */ +void CreateStunRequest(ENetHost* transactionHost) +{ + std::string server_name; + CFG_GET_VAL("stun.server", server_name); + LOGMESSAGERENDER("GetPublicAddress: Using STUN server %s", server_name.c_str()); + + 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; + } + + // documentation says it points to "one or more addrinfo structures" + ENSURE(res != nullptr); + struct sockaddr_in* current_interface = (struct sockaddr_in*)(res->ai_addr); + m_StunServerIP = ntohl(current_interface->sin_addr.s_addr); + + if (transactionHost == nullptr) + { + LOGERROR("Failed to create enet host"); + return; + } + + StunClient::SendStunRequest(transactionHost, m_StunServerIP, m_StunServerPort); + + freeaddrinfo(res); +} + +void StunClient::SendStunRequest(ENetHost* transactionHost, uint32_t targetIp, uint16_t targetPort) +{ + // Assemble the message for the stun server + std::vector m_buffer; + + // bytes 0-1: the type of the message + // bytes 2-3: message length added to header (attributes) + uint16_t message_type = 0x0001; // binding request + uint16_t message_length = 0x0000; + AddUInt16(m_buffer, message_type); + AddUInt16(m_buffer, message_length); + AddUInt32(m_buffer, 0x2112A442); + + // bytes 8-19: the transaction id + for (int i = 0; i < 12; i++) + { + uint8_t random_byte = rand() % 256; + m_buffer.push_back(random_byte); + m_StunTransactionID[i] = random_byte; + } + //m_buffer.push_back(0); -- this breaks STUN message + + // sendRawPacket + struct 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); + + LOGMESSAGERENDER("GetPublicAddress: Sending STUN request to: %d.%d.%d.%d:%d", + (targetIp >> 24) & 0xff, + (targetIp >> 16) & 0xff, + (targetIp >> 8) & 0xff, + (targetIp >> 0) & 0xff, + targetPort); + + int send_result = sendto(transactionHost->socket, (char*)(m_buffer.data()), (int)m_buffer.size(), 0, (sockaddr*)&to, to_len); + LOGMESSAGERENDER("GetPublicAddress: sendto result: %d", send_result); +} + +/** + * Gets the response from the STUN server, checks it for its validity and + * then parses the answer into address and port + * \return "" if the address could be parsed or an error message +*/ +std::string ParseStunResponse(ENetHost* transactionHost) +{ + // TransportAddress sender; + const int LEN = 2048; + char buffer[LEN]; + + // receiveRawPacket + // int len = m_transaction_host->receiveRawPacket(buffer, LEN, &sender, 2000); + int max_tries = 2000; + + memset(buffer, 0, LEN); + + struct sockaddr_in addr; + socklen_t from_len = sizeof(addr); + + int err; + int len = recvfrom(transactionHost->socket, buffer, LEN, 0, (struct sockaddr*)(&addr), &from_len); + + int count = 0; + // wait to receive the message because enet sockets are non-blocking + while (len < 0 && (countsocket, buffer, LEN, 0, (struct sockaddr*)(&addr), &from_len); + } + + if (len == -1) + err = errno; + LOGERROR("GetPublicAddress: recvfrom result: %d", len); + + if (len == -1) + LOGERROR("GetPublicAddress: recvfrom error: %d", err); + + if (len < 0) + return "No message received"; + + uint32_t sender_ip = ntohl((uint32_t)(addr.sin_addr.s_addr)); + uint16_t sender_port = ntohs(addr.sin_port); + + if (sender_ip != m_StunServerIP) + LOGMESSAGERENDER("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, buffer); + + if (len < 0) + return "STUN response contains no data at all"; + + // Convert to network string. + // NetworkString datas((uint8_t*)buffer, len); + std::vector m_buffer; + int m_current_offset; + + m_buffer.resize(len); + memcpy(m_buffer.data(), (uint8_t*)buffer, len); + + m_current_offset = 0; + // m_current_offset = 5; // ignore type and token -- this breaks STUN response processing + + // check that the stun response is a response, contains the magic cookie + // and the transaction ID + if (GetFromBuffer(m_buffer, m_current_offset) != 0x0101) + return "STUN response has incorrect type"; + + int message_size = GetFromBuffer(m_buffer, m_current_offset); + if (GetFromBuffer(m_buffer, m_current_offset) != m_StunMagicCookie) + return "STUN response doesn't contain the magic cookie"; + + for (int i = 0; i < 12; ++i) + if (m_buffer[m_current_offset++] != m_StunTransactionID[i]) + return "STUN response doesn't contain the transaction ID"; + + LOGERROR("GetPublicAddress: The STUN server responded with a valid answer"); + + // The stun message is valid, so we parse it now: + if (message_size == 0) + return "STUN response does not contain any information."; + + if (message_size < 4) + return "STUN response is too short."; + + // Those are the port and the address to be detected + while (true) + { + int type = GetFromBuffer(m_buffer, m_current_offset); + int size = GetFromBuffer(m_buffer, m_current_offset); + if (type == 0 || type == 1) + { + ENSURE(size == 8); + ++m_current_offset; + + // Check address family + char address_family = m_buffer[m_current_offset++]; + if (address_family != 0x01) + return "Unsupported address family, IPv4 is expected"; + + m_Port = GetFromBuffer(m_buffer, m_current_offset); + m_IP = GetFromBuffer(m_buffer, m_current_offset); + + // finished parsing, we know our public transport address + LOGMESSAGERENDER("GetPublicAddress: The public address has been found: %d.%d.%d.%d:%d", + (m_IP >> 24) & 0xff, + (m_IP >> 16) & 0xff, + (m_IP >> 8) & 0xff, + (m_IP >> 0) & 0xff, + m_Port); + break; + } + + m_current_offset += 4 + size; + ENSURE(m_current_offset >=0 && m_current_offset < (int)m_buffer.size()); + + message_size -= 4 + size; + + if (message_size < 4) + return "STUN response is invalid."; + } + + return ""; +} + +JS::Value StunClient::FindStunEndpoint(ScriptInterface& scriptInterface, int port) +{ + ENetAddress hostAddr; + hostAddr.host = ENET_HOST_ANY; + hostAddr.port = port; + + ENetHost* transactionHost = enet_host_create(&hostAddr, 1, 1, 0, 0); + + CreateStunRequest(transactionHost); + std::string parse_result = ParseStunResponse(transactionHost); + enet_host_destroy(transactionHost); + + if (!parse_result.empty()) + LOGERROR("Parse error: %s", parse_result.c_str()); + + // 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", std::string(ipStr)); + scriptInterface.SetProperty(stunEndpoint, "port", m_Port); + return stunEndpoint; +} + +StunClient::StunEndpoint StunClient::FindStunEndpoint(ENetHost* transactionHost) +{ + CreateStunRequest(transactionHost); + std::string parse_result = ParseStunResponse(transactionHost); + if (!parse_result.empty()) + LOGERROR("Parse error: %s", parse_result.c_str()); + + // 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)); + + StunEndpoint stunEndpoint; + stunEndpoint.ip = m_IP; + stunEndpoint.port = m_Port; + return stunEndpoint; +} + +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); + + // Send a UDP message from enet host to ip:port + LOGMESSAGERENDER("Sending STUN request to %s:%d", serverAddress, serverPort); + for (int i = 0; i < 3; ++i) + { + StunClient::SendStunRequest(enetClient, htonl(addr.host), serverPort); + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + } +}