aboutsummaryrefslogtreecommitdiff
path: root/src/UdpTasks/BeaconServer
diff options
context:
space:
mode:
Diffstat (limited to 'src/UdpTasks/BeaconServer')
-rw-r--r--src/UdpTasks/BeaconServer/Receive/heartbeatgamespy0.cpp186
-rw-r--r--src/UdpTasks/BeaconServer/Receive/replyquery.cpp113
-rw-r--r--src/UdpTasks/BeaconServer/Receive/udponread.cpp31
-rw-r--r--src/UdpTasks/BeaconServer/Receive/udpontimeout.cpp27
-rw-r--r--src/UdpTasks/BeaconServer/Uplink/onuplinktimer.cpp41
-rw-r--r--src/UdpTasks/BeaconServer/Uplink/uplink.cpp21
-rw-r--r--src/UdpTasks/BeaconServer/beaconserver.cpp7
-rw-r--r--src/UdpTasks/BeaconServer/beaconserver.h66
-rw-r--r--src/UdpTasks/BeaconServer/udplisten.cpp18
9 files changed, 510 insertions, 0 deletions
diff --git a/src/UdpTasks/BeaconServer/Receive/heartbeatgamespy0.cpp b/src/UdpTasks/BeaconServer/Receive/heartbeatgamespy0.cpp
new file mode 100644
index 0000000..657330d
--- /dev/null
+++ b/src/UdpTasks/BeaconServer/Receive/heartbeatgamespy0.cpp
@@ -0,0 +1,186 @@
+#include "../beaconserver.h"
+
+// heartbeat processing for different protocol types
+void BeaconServer::processHeartbeatGamespy0(const QNetworkDatagram &datagram,
+ const QString &senderAddress,
+ const unsigned short &senderPort,
+ const QString &receiveBuffer)
+{
+ // parse key/value pairs and create a readable server label
+ QMultiHash<QString, QString> receiveData = parseGameSpy0Buffer(receiveBuffer);
+ QString senderAddressLabel = QStringLiteral("%1:%2").arg(senderAddress, QString::number(senderPort));
+
+ /*
+ * Receive a heartbeat.
+ * Update or insert the server in the database. If not authenticated, send a secure/validate challenge.
+ */
+ if ( receiveData.contains("heartbeat") )
+ {
+ // store heartbeat and request authentication
+ UdpData newBeacon;
+ newBeacon.ip = senderAddress;
+ newBeacon.port = receiveData.value("heartbeat", "0").toUShort();
+ newBeacon.gamename = receiveData.value("gamename", "unknown");
+
+ // sanity check: known game(name)
+ if ( _coreObject->SupportedGames.contains( newBeacon.gamename ) )
+ {
+ // valid port and/or default port available?
+ if (newBeacon.port == 0)
+ {
+ // override with default port if possible
+ if ( _coreObject->SupportedGames.value(newBeacon.gamename).port > 0 )
+ {
+ newBeacon.port = _coreObject->SupportedGames.value( newBeacon.gamename ).port;
+ }
+ else // no valid port available. log and abort.
+ {
+ _coreObject->Log.logEvent("heartbeat", QStringLiteral("%1 invalid port for %2")
+ .arg(newBeacon.ip, newBeacon.gamename) );
+ return;
+ }
+ }
+ }
+ else // unknown game. log and abort.
+ {
+ _coreObject->Log.logEvent("unsupported", QStringLiteral("%1:%2 for unsupported game %3")
+ .arg(newBeacon.ip, QString::number(newBeacon.port), newBeacon.gamename) );
+ return;
+ }
+
+
+ // if this server already exists, update it
+ if ( updateServer(newBeacon.ip, newBeacon.port, newBeacon.gamename, true, false) )
+ {
+ // log update
+ _coreObject->Log.logEvent("heartbeat", QStringLiteral("%1:%2 for %3")
+ .arg(newBeacon.ip, QString::number(newBeacon.port), newBeacon.gamename) );
+
+ // no further tasks for this datagram
+ return;
+ }
+
+ // else:
+
+ // add to database
+ insertServer(newBeacon.ip, newBeacon.port, newBeacon.gamename, true);
+
+ // log type "uplink" for first heartbeat. from now on, this server is logged with type "heartbeat"
+ _coreObject->Log.logEvent("new", QStringLiteral("%1:%2 for %3")
+ .arg(newBeacon.ip, QString::number(newBeacon.port),newBeacon.gamename) );
+
+ /*
+ * Set up secure/validate challenge
+ */
+
+ // some games are incompatible with secure/validate (within this protocol)
+ if ( _overrideValidateBeacon.contains(newBeacon.gamename) )
+ return;
+
+ // generate new challenge
+ newBeacon.secure = genChallengeString(6, false);
+
+ // store heartbeat in temporary list
+ _beaconList.remove(senderAddressLabel); // remove potential old challenges first
+ _beaconList.insert(senderAddressLabel, newBeacon);
+
+ // request authentication of remote server
+ _udpSocket.writeDatagram( datagram.makeReply( QStringLiteral("\\secure\\%1").arg(newBeacon.secure).toLatin1() ) );
+
+ // no further tasks for this datagram
+ return;
+ }
+
+ /*
+ * received response to authentication request
+ */
+ if ( receiveData.contains("validate") )
+ {
+ // load existing information
+ UdpData valBeacon = _beaconList.value(senderAddressLabel);
+
+ // empty heartbeat UdpData? then received validate timed out and previous UdpData was removed
+ if (QHostAddress(valBeacon.ip).isNull() || valBeacon.gamename.length() <= 0)
+ {
+ _coreObject->Log.logEvent("secure", QStringLiteral("unexpected validate from %1").arg(senderAddress));
+ return;
+ }
+
+ // get response
+ AuthResult authResult = validateGamename(true, // this is a beacon
+ valBeacon.gamename,
+ receiveData.value("validate",""),
+ _coreObject->SupportedGames.value(valBeacon.gamename).cipher,
+ valBeacon.secure,
+ receiveData.value("enctype", "0").toInt() );
+
+ // compare with received response
+ if ( authResult.auth )
+ {
+ // server authenticated - log and add to database
+ _coreObject->Log.logEvent("secure", QStringLiteral("successful validate from %1:%2 for %3")
+ .arg(valBeacon.ip, QString::number(valBeacon.port),valBeacon.gamename) );
+
+ // update the existing entry that was already added in the initial heartbeat
+ updateServer(valBeacon.ip, valBeacon.port, valBeacon.gamename, false, true);
+
+ // remove from temporary/pending list
+ _beaconList.remove(senderAddressLabel);
+ }
+ else // log failed validate
+ {
+ // set validate false (but update last response time)
+ updateServer(valBeacon.ip, valBeacon.port, valBeacon.gamename, false, false);
+ _coreObject->Log.logEvent("secure", QStringLiteral("failed validate from %1:%2 for %3")
+ .arg(valBeacon.ip, QString::number(valBeacon.port),valBeacon.gamename) );
+ _coreObject->Log.logEvent("secure", QStringLiteral("secure: '%1', gamename: '%2', validate: '%3', expected: '%4'")
+ .arg(valBeacon.secure, valBeacon.gamename, receiveData.value("validate", "null"), authResult.validate ));
+ }
+
+ return;
+ }
+
+ /*
+ * status queries directed at masterserver
+ */
+ if (receiveData.contains("secure") or
+ receiveData.contains("basic") or
+ receiveData.contains("info") or
+ receiveData.contains("rules") or
+ receiveData.contains("status") or
+ receiveData.contains("echo") )
+ {
+ // parse response query
+ QStringList response = replyQuery(receiveData);
+
+ // return response
+ _udpSocket.writeDatagram( datagram.makeReply( response.join("").toLatin1() ) );
+
+ // log incoming query
+ _coreObject->Log.logEvent("query", QStringLiteral("%1 queried us with %2")
+ .arg(senderAddress, receiveData.keys().join(", ") ) );
+
+ // log echo separately
+ if ( receiveData.contains("echo") )
+ _coreObject->Log.logEvent("echo", QStringLiteral("%1: '%2'")
+ .arg(senderAddress, receiveData.value("echo") ) );
+
+ return;
+ }
+
+ // ignore trailing queryid+final
+ if (receiveData.size() > 0)
+ {
+ // receive queryid and final from the query
+ receiveData.remove("queryid");
+ receiveData.remove("final");
+
+ // nothing remains? then ignore. otherwise, proceed to "unknown query"
+ if ( receiveData.size() <= 0)
+ return;
+ }
+
+ // received another type of query?
+ _coreObject->Log.logEvent("unknown", QStringLiteral("received unknown udp uplink %1 from %2")
+ .arg(receiveBuffer, senderAddress) );
+}
diff --git a/src/UdpTasks/BeaconServer/Receive/replyquery.cpp b/src/UdpTasks/BeaconServer/Receive/replyquery.cpp
new file mode 100644
index 0000000..7df6549
--- /dev/null
+++ b/src/UdpTasks/BeaconServer/Receive/replyquery.cpp
@@ -0,0 +1,113 @@
+#include "../beaconserver.h"
+
+QStringList BeaconServer::replyQuery(const QMultiHash<QString, QString> &query)
+{
+ // initialise output
+ QStringList queryResponse;
+
+ // gamespy uses incrementing query ids in the messages
+ _queryId = ( (_queryId > 99) ? 1 : _queryId + 1 );
+ int querySubId = 1;
+
+ // secure response
+ if ( query.contains("secure") and _coreObject->SupportedGames.contains( TYPE_GAMENAME))
+ {
+ // sanity checks and cast to byte array
+ QByteArray secure = query.value("secure", "").toLatin1();
+ QByteArray cipher = _coreObject->SupportedGames.value(TYPE_GAMENAME).cipher.toLatin1();
+ int enctype = query.value("enctype", "0").toInt();
+ QString validate = returnValidate(cipher, secure, enctype);
+
+ queryResponse.append(
+ QStringLiteral("\\validate\\%1\\queryid\\%2.%3")
+ .arg(validate, QString::number(_queryId), QString::number(querySubId++))
+ );
+ }
+
+ // basic
+ if ( query.contains("basic") or query.contains("status") )
+ {
+ queryResponse.append(
+ QStringLiteral("\\gamename\\%1"
+ "\\gamever\\%2"
+ "\\location\\0"
+ "\\queryid\\%3.%4")
+ .arg(TYPE_GAMENAME, SHORT_VER, QString::number(_queryId), QString::number(querySubId++))
+ );
+ }
+
+ // info
+ if ( query.contains("info") or query.contains("status") )
+ {
+ // cast server statistics as player info
+ QString selectStats = "SELECT SUM(num_direct) AS serv_direct, "
+ "SUM(num_total) AS serv_total "
+ "FROM gameinfo ";
+ QSqlQuery statQuery;
+ statQuery.prepare(selectStats);
+ if ( ! statQuery.exec() )
+ reportQuery(statQuery); // do NOT return/die!
+
+ // get values
+ int serv_direct = -1;
+ int serv_total = -1;
+ if ( statQuery.next() )
+ {
+ serv_direct = statQuery.value("serv_direct").toInt();
+ serv_total = statQuery.value("serv_total").toInt();
+ }
+
+ queryResponse.append(
+ QStringLiteral("\\hostname\\%1"
+ "\\hostport\\%2"
+ "\\gametype\\%3"
+ "\\mapname\\333networks"
+ "\\numplayers\\%4"
+ "\\maxplayers\\%5"
+ "\\gamemode\\openplaying"
+ "\\queryid\\%6.%7")
+ .arg( _coreObject->Settings.PublicInformationSettings.hostname,
+ QString::number(_coreObject->Settings.ListenServerSettings.listenPort),
+ "Masterserver", // replace with "bare masterserver" or "integrated beacon/website checker", based on the settings
+ QString::number(serv_direct),
+ QString::number(serv_total),
+ QString::number(_queryId), QString::number(querySubId++))
+ );
+ }
+
+ // rules
+ if ( query.contains("rules") or query.contains("status") )
+ {
+ // compile information about the specific software
+ QString mutators;
+ mutators.append( QStringLiteral("buildtype: %1, buildtime: %2, Qt version: %3, author: %4")
+ .arg(BUILD_TYPE,
+ BUILD_TIME,
+ qVersion(),
+ BUILD_AUTHOR) );
+
+ queryResponse.append(
+ QStringLiteral("\\mutators\\%1"
+ "\\AdminName\\%2"
+ "\\AdminEMail\\%3"
+ "\\queryid\\%4.%5")
+ .arg( mutators,
+ _coreObject->Settings.PublicInformationSettings.adminName,
+ _coreObject->Settings.PublicInformationSettings.contact,
+ QString::number(_queryId), QString::number(querySubId++))
+ );
+ }
+
+ // echo: reply with echo_reply
+ if (query.contains("echo"))
+ {
+ queryResponse.append(QStringLiteral("\\echo_reply\\%1\\queryid\\%2.%3")
+ .arg(query.value("echo"), QString::number(_queryId), QString::number(querySubId++))
+ );
+ }
+
+ // end query with final
+ queryResponse.append("\\final\\");
+
+ return queryResponse;
+}
diff --git a/src/UdpTasks/BeaconServer/Receive/udponread.cpp b/src/UdpTasks/BeaconServer/Receive/udponread.cpp
new file mode 100644
index 0000000..b6d93d5
--- /dev/null
+++ b/src/UdpTasks/BeaconServer/Receive/udponread.cpp
@@ -0,0 +1,31 @@
+#include "../beaconserver.h"
+
+void BeaconServer::onUdpRead()
+{
+ while ( _udpSocket.hasPendingDatagrams() )
+ {
+ // get sender and payload
+ QNetworkDatagram datagram = _udpSocket.receiveDatagram();
+ QString senderAddress = QHostAddress( datagram.senderAddress().toIPv4Address() ).toString();
+ int senderPort = datagram.senderPort();
+ QString receiveBuffer = datagram.data();
+ receiveBuffer = receiveBuffer.toLatin1();
+
+ // shorthand label
+ QString senderAddressLabel = QStringLiteral("%1:%2").arg(senderAddress, QString::number(senderPort));
+ _coreObject->Log.logEvent("udp", QStringLiteral("%1 sent '%2'").arg(senderAddressLabel, receiveBuffer ) );
+
+ // ignore empty data packets (query port forwarded to a game port)
+ if (receiveBuffer.length() <= 0)
+ continue;
+
+ // determine protocol and response based on the first character (backslash, byte value, ... )
+ unsigned short protocol_chooser = receiveBuffer.at(0).unicode();
+ if (protocol_chooser != 92)
+ continue;
+
+ // Protocol is GameSpy v0 (\key\value format) used by Unreal/UT, Postal 2, Rune, Deus Ex, Serious Sam, others
+ processHeartbeatGamespy0(datagram, senderAddress, senderPort, receiveBuffer);
+
+ } // while pending datagrams
+}
diff --git a/src/UdpTasks/BeaconServer/Receive/udpontimeout.cpp b/src/UdpTasks/BeaconServer/Receive/udpontimeout.cpp
new file mode 100644
index 0000000..7767029
--- /dev/null
+++ b/src/UdpTasks/BeaconServer/Receive/udpontimeout.cpp
@@ -0,0 +1,27 @@
+#include "../beaconserver.h"
+
+// soft timeout. every <timeout>, remove all servers older than <timeout> from the list.
+// that means a server has a maximum wait/response time of 2*<timeout>
+void BeaconServer::onUdpTimedOut()
+{
+ // iterate through the server list
+ QHashIterator<QString, UdpData> list(_beaconList);
+ while (list.hasNext())
+ {
+ // select
+ list.next();
+
+ // check passed time: add date < remove date?
+ qint64 currentTime = QDateTime::currentSecsSinceEpoch();
+ if ( list.value().time < currentTime - (_timeOutTime_ms / 1000) )
+ {
+ // if timeout has passed, remove the server from the list
+ _coreObject->Log.logEvent("udp", QStringLiteral("%1 timed out").arg(list.key()));
+ _beaconList.remove(list.key());
+ }
+ }
+
+ // periodically emit readyread signal to avoid issues similar to StatusChecker
+ // readyread function will handle any inconsistencies
+ emit _udpSocket.readyRead();
+}
diff --git a/src/UdpTasks/BeaconServer/Uplink/onuplinktimer.cpp b/src/UdpTasks/BeaconServer/Uplink/onuplinktimer.cpp
new file mode 100644
index 0000000..7b62bc4
--- /dev/null
+++ b/src/UdpTasks/BeaconServer/Uplink/onuplinktimer.cpp
@@ -0,0 +1,41 @@
+#include "../beaconserver.h"
+
+void BeaconServer::onUplinkTimer()
+{
+ // get uplinks from settings
+ QListIterator<SyncServer> syncServers(_coreObject->Settings.SyncerSettings.syncServers);
+ while ( syncServers.hasNext() )
+ {
+ // get next item
+ SyncServer thisUplink = syncServers.next();
+ QString remoteHostname = thisUplink.remoteAddress;
+ unsigned short remotePort = thisUplink.beaconPort;
+
+ // resolve (async) uplink address and let callback perform the uplink
+ QHostInfo::lookupHost(remoteHostname, this, [this, remoteHostname, remotePort] (const QHostInfo &host)
+ {
+ // errors during lookup?
+ if ( host.error() == QHostInfo::NoError and ! host.addresses().empty() )
+ {
+ // create and send heartbeat
+ QNetworkDatagram udpDatagram( _uplinkData.toLatin1(), host.addresses().first(), remotePort );
+ _udpSocket.writeDatagram( udpDatagram );
+
+ // add to log
+ _coreObject->Log.logEvent("uplink", QStringLiteral("sending uplink to %1 (%2:%3)")
+ .arg(remoteHostname,
+ host.addresses().first().toString(),
+ QString::number(remotePort)));
+
+ // function this->onUdpRead() will handle any responses (secure/basic/status)
+ }
+ else
+ {
+ // log failure to resolve
+ _coreObject->Log.logEvent("uplink", QStringLiteral("cannot resolve %1: %2")
+ .arg( remoteHostname, host.errorString()));
+ }
+ }); // end QHostInfo
+
+ } // end while
+}
diff --git a/src/UdpTasks/BeaconServer/Uplink/uplink.cpp b/src/UdpTasks/BeaconServer/Uplink/uplink.cpp
new file mode 100644
index 0000000..7799dbd
--- /dev/null
+++ b/src/UdpTasks/BeaconServer/Uplink/uplink.cpp
@@ -0,0 +1,21 @@
+#include "../beaconserver.h"
+
+bool BeaconServer::uplink()
+{
+ // set uplink message (port and gamename)
+ _uplinkData = QStringLiteral("\\heartbeat\\%1\\gamename\\%2")
+ .arg( QString::number( _coreObject->Settings.BeaconServerSettings.beaconPort),
+ TYPE_GAMENAME );
+
+ // connect timer with events
+ connect(&_uplinkTimer, &QTimer::timeout, this, &BeaconServer::onUplinkTimer);
+
+ // start timer
+ _uplinkTimer.start( _broadcastInterval_s * 1000 );
+
+ // complete startup
+ _coreObject->Log.logEvent("info", QStringLiteral("broadcasting UDP uplinks every %1 seconds")
+ .arg( _broadcastInterval_s ));
+
+ return true;
+}
diff --git a/src/UdpTasks/BeaconServer/beaconserver.cpp b/src/UdpTasks/BeaconServer/beaconserver.cpp
new file mode 100644
index 0000000..fbd6d89
--- /dev/null
+++ b/src/UdpTasks/BeaconServer/beaconserver.cpp
@@ -0,0 +1,7 @@
+#include "beaconserver.h"
+
+BeaconServer::BeaconServer(const QSharedPointer<CoreObject> &coreObject)
+{
+ // create local access
+ this->_coreObject = coreObject;
+}
diff --git a/src/UdpTasks/BeaconServer/beaconserver.h b/src/UdpTasks/BeaconServer/beaconserver.h
new file mode 100644
index 0000000..fd8532d
--- /dev/null
+++ b/src/UdpTasks/BeaconServer/beaconserver.h
@@ -0,0 +1,66 @@
+#ifndef BEACONSERVER_H
+#define BEACONSERVER_H
+
+#include <QTimer>
+#include <QUdpSocket>
+#include <QNetworkDatagram>
+#include <QHostInfo>
+#include "Core/CoreObject/coreobject.h"
+#include "Database/Common/commonactions.h"
+#include "Protocols/GameSpy0/gamespy0.h"
+#include "Protocols/GameSpy0/securevalidate.h"
+#include "UdpTasks/udpdatastructure.h"
+
+class BeaconServer : public QObject
+{
+ Q_OBJECT
+public:
+ BeaconServer(const QSharedPointer<CoreObject> &coreObject);
+
+ // activate listener and broadcast
+ bool listen();
+ bool uplink();
+
+private: // general udp task handles
+ QSharedPointer<CoreObject> _coreObject;
+ const int _timeOutTime_ms = 15000; // 15 second soft timeout
+ const int _broadcastInterval_s = 60; // 1 min between beacons
+
+ // udp socket
+ QUdpSocket _udpSocket;
+
+ // determine reply to incoming requests
+ QStringList replyQuery(const QMultiHash<QString, QString> &query);
+
+private: // udp beacon server
+
+ // heartbeat processing for different protocol types
+ void processHeartbeatGamespy0(const QNetworkDatagram &datagram,
+ const QString &senderAddress,
+ const unsigned short &senderPort,
+ const QString &receiveBuffer);
+
+ // timer to sweep up abandoned beacons (timeouts)
+ QTimer _sweepTimer;
+
+ // store information about unverified beacons
+ QHash<QString, UdpData> _beaconList;
+
+ // helper for replyQuery()
+ int _queryId;
+
+private slots: // udp beacon server event slots
+ void onUdpRead();
+ void onUdpTimedOut();
+
+private: // broadcast heartbeat
+
+ // outbound heartbeat timer and content
+ QTimer _uplinkTimer;
+ QString _uplinkData;
+
+private slots: // broadcast heartbeat events
+ void onUplinkTimer();
+};
+
+#endif // BEACONSERVER_H
diff --git a/src/UdpTasks/BeaconServer/udplisten.cpp b/src/UdpTasks/BeaconServer/udplisten.cpp
new file mode 100644
index 0000000..da09a74
--- /dev/null
+++ b/src/UdpTasks/BeaconServer/udplisten.cpp
@@ -0,0 +1,18 @@
+#include "beaconserver.h"
+
+bool BeaconServer::listen()
+{
+ // connect socket, timer with events
+ connect(&_udpSocket, &QUdpSocket::readyRead, this, &BeaconServer::onUdpRead);
+ connect(&_sweepTimer, &QTimer::timeout, this, &BeaconServer::onUdpTimedOut);
+
+ // bind socket and timeout
+ _udpSocket.bind(QHostAddress::Any, _coreObject->Settings.BeaconServerSettings.beaconPort);
+ _sweepTimer.start( _timeOutTime_ms );
+
+ // complete startup
+ _coreObject->Log.logEvent("info", QStringLiteral("start listening for UDP beacons on port %1")
+ .arg(_coreObject->Settings.BeaconServerSettings.beaconPort));
+
+ return true;
+}