diff --git a/src/Main/fg_init.cxx b/src/Main/fg_init.cxx index ea9d9b5ef..4072ae9ba 100644 --- a/src/Main/fg_init.cxx +++ b/src/Main/fg_init.cxx @@ -131,6 +131,7 @@ #include <Instrumentation/HUD/HUD.hxx> #include <Cockpit/cockpitDisplayManager.hxx> #include <Network/HTTPClient.hxx> +#include <Network/DNSClient.hxx> #include <Network/fgcom.hxx> #include <Network/http/httpd.hxx> #include <Include/version.h> @@ -735,6 +736,7 @@ void fgCreateSubsystems(bool duringReset) { if (!globals->get_subsystem<FGHTTPClient>()) { globals->add_new_subsystem<FGHTTPClient>(); } + globals->add_new_subsystem<FGDNSClient>(); //////////////////////////////////////////////////////////////////// // Initialize the flight model subsystem. diff --git a/src/MultiPlayer/CMakeLists.txt b/src/MultiPlayer/CMakeLists.txt index 1d5c7e992..c2d0f59b5 100644 --- a/src/MultiPlayer/CMakeLists.txt +++ b/src/MultiPlayer/CMakeLists.txt @@ -3,11 +3,13 @@ include(FlightGearComponent) set(SOURCES multiplaymgr.cxx tiny_xdr.cxx + MPServerResolver.cxx ) set(HEADERS multiplaymgr.hxx tiny_xdr.hxx + MPServerResolver.hxx ) -flightgear_component(MultiPlayer "${SOURCES}" "${HEADERS}") \ No newline at end of file +flightgear_component(MultiPlayer "${SOURCES}" "${HEADERS}") diff --git a/src/MultiPlayer/MPServerResolver.cxx b/src/MultiPlayer/MPServerResolver.cxx new file mode 100644 index 000000000..dae17975d --- /dev/null +++ b/src/MultiPlayer/MPServerResolver.cxx @@ -0,0 +1,194 @@ + +/* + MPServerResolver.cxx - mpserver names lookup via DNS + Written and copyright by Torsten Dreyer - November 2016 + + This file is part of FlightGear. + + FlightGear 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. + + FlightGear 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 FlightGear. If not, see <http://www.gnu.org/licenses/>. + */#include "MPServerResolver.hxx" + +#include <Network/DNSClient.hxx> +#include <Main/fg_props.hxx> +#include <3rdparty/cjson/cJSON.h> +#include <cstdlib> + +using namespace simgear; + +/** + * Build a name=value map from base64 encoded JSON string + */ +class MPServerProperties : public std::map<string, string> { +public: + MPServerProperties (string b64) + { + std::vector<unsigned char> b64dec; + simgear::strutils::decodeBase64 (b64, b64dec); + auto jsonString = string ((char*) b64dec.data (), b64dec.size ()); + cJSON * json = ::cJSON_Parse (jsonString.c_str ()); + if (json) { + for (int i = 0; i < ::cJSON_GetArraySize (json); i++) { + cJSON * cj = ::cJSON_GetArrayItem (json, i); + if (cj->string && cj->valuestring) + emplace (cj->string, cj->valuestring); + } + ::cJSON_Delete (json); + } else { + SG_LOG(SG_NETWORK,SG_WARN, "MPServerResolver: Can't parse JSON string '" << jsonString << "'" ); + } + } +}; + +class MPServerResolver::MPServerResolver_priv { +public: + enum { + INIT, LOADING_SRV_RECORDS, LOAD_NEXT_TXT_RECORD, LOADING_TXT_RECORDS, DONE, + } _state = INIT; + + FGDNSClient * _dnsClient = globals->get_subsystem<FGDNSClient> (); + DNS::Request_ptr _dnsRequest; + PropertyList _serverNodes; + PropertyList::const_iterator _serverNodes_it; +}; + +MPServerResolver::~MPServerResolver () +{ + delete _priv; +} + +MPServerResolver::MPServerResolver () : + _priv (new MPServerResolver_priv ()) +{ +} + +void +MPServerResolver::run () +{ + //SG_LOG(SG_NETWORK, SG_DEBUG, "MPServerResolver::run() with state=" << _priv->_state ); + switch (_priv->_state) { + // First call - fire DNS lookup for SRV records + case MPServerResolver_priv::INIT: + if (!_priv->_dnsClient) { + SG_LOG(SG_NETWORK, SG_WARN, "MPServerResolver: DNS subsystem not available."); + onFailure (); + return; + } + + _priv->_dnsRequest = new DNS::SRVRequest (_dnsName, _service, _protocol); + SG_LOG(SG_NETWORK, SG_INFO, "MPServerResolver: sending DNS request for " << _priv->_dnsRequest->getDn()); + _priv->_dnsClient->makeRequest (_priv->_dnsRequest); + _priv->_state = MPServerResolver_priv::LOADING_SRV_RECORDS; + break; + + // Check if response from SRV Query + case MPServerResolver_priv::LOADING_SRV_RECORDS: + if (_priv->_dnsRequest->isTimeout ()) { + SG_LOG(SG_NETWORK, SG_WARN, "Timeout waiting for DNS response. Query was: " << _priv->_dnsRequest->getDn()); + onFailure (); + return; + } + if (_priv->_dnsRequest->isComplete ()) { + // Create a child node under _targetNode for each SRV entry of the response + SG_LOG(SG_NETWORK, SG_INFO, "MPServerResolver: got DNS response for " << _priv->_dnsRequest->getDn()); + int idx = 0; + for (DNS::SRVRequest::SRV_ptr entry : dynamic_cast<DNS::SRVRequest*> (_priv->_dnsRequest.get ())->entries) { + SG_LOG(SG_NETWORK, SG_DEBUG, + "MPServerResolver: SRV " << entry->priority << " " << entry->weight << " " << entry->port << " " << entry->target); + if( 0 == entry->port ) { + SG_LOG(SG_NETWORK, SG_INFO, "MPServerResolver: Skipping offline host " << entry->target ); + continue; + } + SGPropertyNode * serverNode = _targetNode->getNode ("server", idx++, true); + serverNode->getNode ("hostname", true)->setStringValue (entry->target); + serverNode->getNode ("priority", true)->setIntValue (entry->priority); + serverNode->getNode ("weight", true)->setIntValue (entry->weight); + serverNode->getNode ("port", true)->setIntValue (entry->port); + } + + // prepare an iterator over the server-nodes to be used later when loading the TXT records + _priv->_serverNodes = _targetNode->getChildren ("server"); + _priv->_serverNodes_it = _priv->_serverNodes.begin (); + if (_priv->_serverNodes_it == _priv->_serverNodes.end ()) { + // No SRV records found - flag failure + SG_LOG(SG_NETWORK, SG_WARN, "MPServerResolver: no multiplayer servers defined via DNS"); + onFailure (); + return; + } + _priv->_state = MPServerResolver_priv::LOAD_NEXT_TXT_RECORD; + break; + } + break; + + // get the next TXT record + case MPServerResolver_priv::LOAD_NEXT_TXT_RECORD: + if (_priv->_serverNodes_it == _priv->_serverNodes.end ()) { + // we are done with all servers + _priv->_state = MPServerResolver_priv::DONE; + break; + } + + // send the DNS query for the hostnames TXT record + _priv->_dnsRequest = new DNS::TXTRequest ((*_priv->_serverNodes_it)->getStringValue ("hostname")); + SG_LOG(SG_NETWORK, SG_INFO, "MPServerResolver: sending DNS request for " << _priv->_dnsRequest->getDn()); + _priv->_dnsClient->makeRequest (_priv->_dnsRequest); + _priv->_state = MPServerResolver_priv::LOADING_TXT_RECORDS; + break; + + // check if response for TXT query + case MPServerResolver_priv::LOADING_TXT_RECORDS: + if (_priv->_dnsRequest->isTimeout ()) { + // on timeout, try proceeding with next server + SG_LOG(SG_NETWORK, SG_WARN, "Timeout waiting for DNS response. Query was: " << _priv->_dnsRequest->getDn()); + _priv->_state = MPServerResolver_priv::LOAD_NEXT_TXT_RECORD; + ++_priv->_serverNodes_it; + break; + } + if (_priv->_dnsRequest->isComplete ()) { + SG_LOG(SG_NETWORK, SG_INFO, "MPServerResolver: got DNS response for " << _priv->_dnsRequest->getDn()); + // DNS::TXTRequest automatically extracts name=value entries for us, lets retrieve them + auto attributes = dynamic_cast<DNS::TXTRequest*> (_priv->_dnsRequest.get ())->attributes; + auto mpserverAttribute = attributes["flightgear-mpserver"]; + if (!mpserverAttribute.empty ()) { + // we are only interested in the 'flightgear-mpserver=something' entry, this is a base64 encoded + // JSON string, convert this into a map<string,string> + MPServerProperties mpserverProperties (mpserverAttribute); + for (auto prop : mpserverProperties) { + // and store each as a node under our servers node. + SG_LOG(SG_NETWORK, SG_DEBUG, "MPServerResolver: TXT record attribute " << prop.first << "=" << prop.second); + // sanitize property name, don't allow dots or forward slash + auto propertyName = prop.first; + std::replace( propertyName.begin(), propertyName.end(), '.', '_'); + std::replace( propertyName.begin(), propertyName.end(), '/', '_'); + (*_priv->_serverNodes_it)->setStringValue (propertyName, prop.second); + } + } else { + SG_LOG(SG_NETWORK, SG_INFO, "MPServerResolver: TXT record attributes empty"); + } + + // procede with the net node + ++_priv->_serverNodes_it; + _priv->_state = MPServerResolver_priv::LOAD_NEXT_TXT_RECORD; + break; + } + break; + + case MPServerResolver_priv::DONE: + onSuccess (); + return; + } + + // Relinguish control, call me back on the next frame + globals->get_event_mgr ()->addEvent ("MPServerResolver_update", this, &MPServerResolver::run, .0); +} + diff --git a/src/MultiPlayer/MPServerResolver.hxx b/src/MultiPlayer/MPServerResolver.hxx new file mode 100644 index 000000000..942426dba --- /dev/null +++ b/src/MultiPlayer/MPServerResolver.hxx @@ -0,0 +1,83 @@ +/* + MPServerResolver.hxx - mpserver names lookup via DNS + Written and copyright by Torsten Dreyer - November 2016 + + This file is part of FlightGear. + + FlightGear 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. + + FlightGear 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 FlightGear. If not, see <http://www.gnu.org/licenses/>. + */ +#ifndef __FG_MPSERVERRESOLVER_HXX +#define __FG_MPSERVERRESOLVER_HXX + +#include <string> +#include <simgear/props/props.hxx> + +class MPServerResolver { +public: + MPServerResolver(); + virtual ~MPServerResolver(); + void run(); + + /** + * Set the target property where the server-list gets stored + * + * \param value the property node to use as a target + */ + void setTarget( SGPropertyNode_ptr value ) { _targetNode = value; } + + /** + * Set the dns domain name to query. This could be either a full qualified name including the + * service and the protocol like _fgms._udp.flightgear.org or just the domain name like + * flightgear.org. Use setService() and setProtocol() in the latter case. + * + * \param value the dnsname to use for the query. + */ + void setDnsName( const std::string & value ) { _dnsName = value; } + + /** Set the service name to use for the query. Don't add the underscore, this gets added + * automatically. This builds the fully qualified DNS name to query, together with + * setProtocol() and setDnsName(). + * + * \param value the service name to use for the query sans the leading underscore + */ + void setService( const std::string & value ) { _service = value; } + + /** Set the protocol name to use for the query. Don't add the underscore, this gets added + * automatically. This builds the fully qualified DNS name to query, together with + * setService() and setDnsName(). + * + * \param value the protocol name to use for the query sans the leading underscore + */ + void setProtocol( const std::string & value ) { _protocol = value; } + + /** Handler to be called if the resolver process finishes with success. Does nothing by + * default and should be overridden be the user. + */ + virtual void onSuccess() {}; + + /** Handler to be called if the resolver process terminates with an error. Does nothing by + * default and should be overridden be the user. + */ + virtual void onFailure() {}; + +private: + class MPServerResolver_priv; + std::string _dnsName; + std::string _service; + std::string _protocol; + SGPropertyNode_ptr _targetNode; + MPServerResolver_priv * _priv; +}; + +#endif // __FG_MPSERVERRESOLVER_HXX diff --git a/src/MultiPlayer/multiplaymgr.cxx b/src/MultiPlayer/multiplaymgr.cxx index b78c452a0..64cf5ec5f 100644 --- a/src/MultiPlayer/multiplaymgr.cxx +++ b/src/MultiPlayer/multiplaymgr.cxx @@ -41,14 +41,14 @@ #include <simgear/debug/logstream.hxx> #include <simgear/props/props.hxx> #include <simgear/structure/commands.hxx> +#include <simgear/structure/event_mgr.hxx> #include <AIModel/AIManager.hxx> #include <AIModel/AIMultiplayer.hxx> #include <Main/fg_props.hxx> -#include <Network/RemoteXMLRequest.hxx> -#include <Network/HTTPClient.hxx> #include "multiplaymgr.hxx" #include "mpmessages.hxx" +#include "MPServerResolver.hxx" #include <FDM/flightProperties.hxx> using namespace std; @@ -428,40 +428,60 @@ static bool do_multiplayer_disconnect(const SGPropertyNode * arg) { // none // ////////////////////////////////////////////////////////////////////// -static bool do_multiplayer_refreshserverlist(const SGPropertyNode * arg) { - FGMultiplayMgr * self = (FGMultiplayMgr*) globals->get_subsystem("mp"); - if (!self) { - SG_LOG(SG_NETWORK, SG_WARN, "Multiplayer subsystem not available."); - return false; - } - FGHTTPClient* http = globals->get_subsystem<FGHTTPClient>(); - if (!http) { - SG_LOG(SG_IO, SG_ALERT, - "do_multiplayer.refreshserverlist: HTTP client not running"); - return false; - } +static bool +do_multiplayer_refreshserverlist (const SGPropertyNode * arg) +{ + using namespace simgear; - string url( - fgGetString("/sim/multiplay/serverlist-url", - "http://liveries.flightgear.org/mpstatus/mpservers.xml")); + FGMultiplayMgr * self = (FGMultiplayMgr*) globals->get_subsystem ("mp"); + if (!self) { + SG_LOG(SG_NETWORK, SG_WARN, "Multiplayer subsystem not available."); + return false; + } - if (url.empty()) { - SG_LOG(SG_IO, SG_ALERT, - "do_multiplayer.refreshserverlist: no URL given"); - return false; - } + // MPServerResolver implementation to fill the mp server list + // deletes itself when done + class MyMPServerResolver : public MPServerResolver { + public: + MyMPServerResolver () : + MPServerResolver () + { + setTarget (fgGetNode ("/sim/multiplay/server-list", true)); + setDnsName (fgGetString ("/sim/multiplay/dns/query-dn", "flightgear.org")); + setService (fgGetString ("/sim/multiplay/dns/query-srv-service", "fgms")); + setProtocol (fgGetString ("/sim/multiplay/dns/query-srv-protocol", "udp")); + _completeNode->setBoolValue (false); + _failureNode->setBoolValue (false); + } - SGPropertyNode *targetnode = fgGetNode("/sim/multiplay/server-list", true); - SGPropertyNode *completeNode = fgGetNode("/sim/multiplay/got-servers", true); - SGPropertyNode *failureNode = fgGetNode("/sim/multiplay/get-servers-failure", true); - RemoteXMLRequest* req = new RemoteXMLRequest(url, targetnode); - req->setCompletionProp(completeNode); - req->setFailedProp(failureNode); - completeNode->setBoolValue(false); - failureNode->setBoolValue(false); - http->makeRequest(req); - return true; + ~MyMPServerResolver () + { + } + + virtual void + onSuccess () + { + SG_LOG(SG_NETWORK, SG_DEBUG, "MyMPServerResolver: trigger success"); + _completeNode->setBoolValue (true); + delete this; + } + virtual void + onFailure () + { + SG_LOG(SG_NETWORK, SG_DEBUG, "MyMPServerResolver: trigger failure"); + _failureNode->setBoolValue (true); + delete this; + } + + private: + SGPropertyNode *_completeNode = fgGetNode ("/sim/multiplay/got-servers", true); + SGPropertyNode *_failureNode = fgGetNode ("/sim/multiplay/get-servers-failure", true); + }; + + MyMPServerResolver * mpServerResolver = new MyMPServerResolver (); + mpServerResolver->run (); + return true; } ////////////////////////////////////////////////////////////////////// diff --git a/src/Network/CMakeLists.txt b/src/Network/CMakeLists.txt index 3a53cbfd7..f36cfafe5 100644 --- a/src/Network/CMakeLists.txt +++ b/src/Network/CMakeLists.txt @@ -11,6 +11,7 @@ set(SOURCES garmin.cxx generic.cxx HTTPClient.cxx + DNSClient.cxx igc.cxx joyclient.cxx jsclient.cxx @@ -39,6 +40,7 @@ set(HEADERS garmin.hxx generic.hxx HTTPClient.hxx + DNSClient.hxx igc.hxx joyclient.hxx jsclient.hxx diff --git a/src/Network/DNSClient.cxx b/src/Network/DNSClient.cxx new file mode 100644 index 000000000..fff953a20 --- /dev/null +++ b/src/Network/DNSClient.cxx @@ -0,0 +1,71 @@ +// HDNSClient.cxx -- Singleton DNS client object +// +// Written by James Turner, started April 2012. +// Based on HTTPClient from James Turner +// Copyright (C) 2012 James Turner +// +// 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#include "DNSClient.hxx" + +#include <cassert> + +#include <Main/fg_props.hxx> +#include <Include/version.h> + +#include <simgear/sg_inlines.h> + + +using namespace simgear; + +FGDNSClient::FGDNSClient() : + _inited(false) +{ +} + +FGDNSClient::~FGDNSClient() +{ +} + +void FGDNSClient::init() +{ + if (_inited) { + return; + } + + _dns.reset( new simgear::DNS::Client() ); + + _inited = true; +} + +void FGDNSClient::postinit() +{ +} + +void FGDNSClient::shutdown() +{ + _dns.reset(); + _inited = false; +} + +void FGDNSClient::update(double) +{ + _dns->update(); +} + +void FGDNSClient::makeRequest(const simgear::DNS::Request_ptr& req) +{ + _dns->makeRequest(req); +} diff --git a/src/Network/DNSClient.hxx b/src/Network/DNSClient.hxx new file mode 100644 index 000000000..cdccb4e79 --- /dev/null +++ b/src/Network/DNSClient.hxx @@ -0,0 +1,52 @@ +// DNSClient.hxx -- Singleton DNS client object +// +// Written by Torsten Dreyer, started November 2016. +// Based on HTTPClient from James Turner +// Copyright (C) 2012 James Turner +// +// 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifndef FG_DNS_CLIENT_HXX +#define FG_DNS_CLIENT_HXX + +#include <simgear/structure/subsystem_mgr.hxx> +#include <simgear/io/DNSClient.hxx> +#include <memory> + +class FGDNSClient : public SGSubsystem +{ +public: + FGDNSClient(); + virtual ~FGDNSClient(); + + void makeRequest(const simgear::DNS::Request_ptr& req); + +// simgear::HTTP::Client* client() { return _http.get(); } +// simgear::HTTP::Client const* client() const { return _http.get(); } + + virtual void init(); + virtual void postinit(); + virtual void shutdown(); + virtual void update(double); + + static const char* subsystemName() { return "dns"; } +private: + bool _inited; + std::unique_ptr<simgear::DNS::Client> _dns; +}; + +#endif // FG_DNS_CLIENT_HXX + +