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