aboutsummaryrefslogtreecommitdiff
path: root/src/UdpTasks
diff options
context:
space:
mode:
Diffstat (limited to 'src/UdpTasks')
-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
-rw-r--r--src/UdpTasks/StatusChecker/getnextserver.cpp22
-rw-r--r--src/UdpTasks/StatusChecker/oncheckerresponseread.cpp112
-rw-r--r--src/UdpTasks/StatusChecker/onticker.cpp81
-rw-r--r--src/UdpTasks/StatusChecker/playerinfoinsert.cpp60
-rw-r--r--src/UdpTasks/StatusChecker/serverinfoinsert.cpp22
-rw-r--r--src/UdpTasks/StatusChecker/serverinfoupdate.cpp54
-rw-r--r--src/UdpTasks/StatusChecker/statuschecker.cpp7
-rw-r--r--src/UdpTasks/StatusChecker/statuschecker.h79
-rw-r--r--src/UdpTasks/StatusChecker/statusticker.cpp47
-rw-r--r--src/UdpTasks/udpdatastructure.h30
19 files changed, 1024 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;
+}
diff --git a/src/UdpTasks/StatusChecker/getnextserver.cpp b/src/UdpTasks/StatusChecker/getnextserver.cpp
new file mode 100644
index 0000000..5b9bb20
--- /dev/null
+++ b/src/UdpTasks/StatusChecker/getnextserver.cpp
@@ -0,0 +1,22 @@
+#include "statuschecker.h"
+
+QSqlQuery StatusChecker::getNextServer(const int &currentServerId)
+{
+ // select server with >id
+ QSqlQuery q;
+ QString selectString = "SELECT id, ip, queryport, gamename, f_auth FROM serverlist "
+ "WHERE id > :currentServerID "
+ "AND (dt_updated > :timestamp OR dt_sync > :timestamp)"
+ "LIMIT 1";
+
+ // bind values and execute
+ q.prepare(selectString);
+ q.bindValue(":currentServerID", currentServerId);
+ q.bindValue(":timestamp", QDateTime::currentDateTime()
+ .addSecs(-_coreObject->Settings.ListenServerSettings.serverttl_s)
+ .toSecsSinceEpoch());
+ if ( ! q.exec() )
+ reportQuery(q);
+
+ return q;
+}
diff --git a/src/UdpTasks/StatusChecker/oncheckerresponseread.cpp b/src/UdpTasks/StatusChecker/oncheckerresponseread.cpp
new file mode 100644
index 0000000..098058a
--- /dev/null
+++ b/src/UdpTasks/StatusChecker/oncheckerresponseread.cpp
@@ -0,0 +1,112 @@
+#include "statuschecker.h"
+
+void StatusChecker::onUdpResponseRead()
+{
+ // read now called, reset workaround counter
+ _missedReadCalls = 0;
+
+ while ( _udpSocket.hasPendingDatagrams() )
+ {
+ // get sender and payload
+ QNetworkDatagram datagram = _udpSocket.receiveDatagram();
+ QString senderAddress = QHostAddress( datagram.senderAddress().toIPv4Address() ).toString();
+ unsigned short 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;
+
+ // buffer complete? else wait for data to be complete
+ _dataBuffer[senderAddressLabel] += receiveBuffer;
+
+ // status response or validate response? (either status or secure, not both)
+ if ( receiveBuffer.contains("\\validate\\") )
+ {
+ // parse key/value pairs and QHash label
+ QMultiHash<QString, QString> receiveData = parseGameSpy0Buffer(_dataBuffer[senderAddressLabel]);
+
+ // load existing information
+ QString secure = _secureBuffer.value(senderAddressLabel).secure;
+ QString gamename = _secureBuffer.value(senderAddressLabel).gamename;
+
+ // if entry, secure, gamename and/or cipher do not exist, AuthResult will be false+invalid
+ AuthResult authResult = validateGamename(false, // this is not a beacon
+ gamename,
+ receiveData.value("validate",""),
+ _coreObject->SupportedGames.value(gamename).cipher,
+ 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 for %2")
+ .arg(senderAddressLabel, gamename));
+
+ // update the existing entry
+ updateServer(senderAddress, senderPort, gamename, false, true);
+
+ // remove from secure buffer
+ _secureBuffer.remove(senderAddressLabel);
+ }
+ else // log failed validate
+ {
+ // set validate false (but update last response time)
+ updateServer(senderAddress, senderPort, gamename, false, false);
+ _coreObject->Log.logEvent("secure", QStringLiteral("failed validate from %1 for %2")
+ .arg(senderAddressLabel, gamename));
+ _coreObject->Log.logEvent("secure", QStringLiteral("secure: '%1', gamename: '%2', validate: '%3', expected: '%4'")
+ .arg(secure, gamename, receiveData.value("validate", "null"), authResult.validate ));
+ }
+
+ // clear receive buffer
+ _dataBuffer.remove(senderAddressLabel);
+
+ // there should be no further data (ignore)
+ continue;
+ }
+
+ // all status query data received?
+ if (receiveBuffer.contains("\\final\\"))
+ {
+
+ // parse key/value pairs and QHash label
+ QMultiHash<QString, QString> receiveData = parseGameSpy0Buffer(_dataBuffer[senderAddressLabel]);
+
+ // update or insert primary details
+ if ( ! updateServer(senderAddress, senderPort, receiveData.value("gamename", "unknown"), false, false) )
+ {
+ // add to database
+ insertServer(senderAddress, senderPort, receiveData.value("gamename", "unknown"), false);
+ }
+
+ // then update detailed information
+ if ( ! updateServerInfo(senderAddress, senderPort, receiveData) )
+ {
+ // insert and update new entry (this does NOT insert to the serverlist, only info)
+ // this assumes that an entry with this ip/port exists in the serverlist (fails silently)
+ insertServerInfo(senderAddress, senderPort);
+ updateServerInfo(senderAddress, senderPort, receiveData);
+ }
+
+ // update player info (removes old playerdata entries)
+ insertPlayerInfo(senderAddress, senderPort, receiveData);
+
+ // clear receive buffer
+ _dataBuffer.remove(senderAddressLabel);
+
+ } // if final
+ }
+}
diff --git a/src/UdpTasks/StatusChecker/onticker.cpp b/src/UdpTasks/StatusChecker/onticker.cpp
new file mode 100644
index 0000000..e4a8fb2
--- /dev/null
+++ b/src/UdpTasks/StatusChecker/onticker.cpp
@@ -0,0 +1,81 @@
+#include "statuschecker.h"
+
+void StatusChecker::onTicker()
+{
+
+ // get next recent item from database (id > index)
+ QSqlQuery q = getNextServer(_dbIndex);
+
+ if ( q.next() )
+ {
+ // get serverinfo from query
+ _dbIndex = q.value("id").toInt();
+ QString remoteAddress = q.value("ip").toString();
+ unsigned short remotePort = static_cast<unsigned short>(q.value("queryport").toInt());
+ QString gamename = q.value("gamename").toString();
+ bool authenticated = static_cast<unsigned short>(q.value("f_auth").toBool());
+
+ // if this particular server was added through SYNC, it may not yet have been authenticated.
+ // send secure/validate challenge in that case
+ if ( ! authenticated )
+ {
+ // shorthand label
+ QString remoteAddressLabel = QStringLiteral("%1:%2").arg(remoteAddress, QString::number(remotePort));
+ QString secure = genChallengeString(6, false);
+
+ // use HeartBeat struct to cache gamename+secure
+ // TODO optimise the HeartBeat struct in BeaconServer to cater both purposes
+ UdpData secureInfo;
+ secureInfo.secure = secure;
+ secureInfo.gamename = gamename;
+ _secureBuffer.insert(remoteAddressLabel, secureInfo);
+
+ // send secure challenge
+ QString udpSecure = QStringLiteral("\\secure\\%1").arg(secure);
+ QNetworkDatagram udpSecureDatagram(udpSecure.toUtf8(), QHostAddress(remoteAddress), remotePort);
+ _udpSocket.writeDatagram(udpSecureDatagram);
+ }
+
+ // create datagram with info- or status request
+ QNetworkDatagram udpDatagram(_udpRequest.toUtf8(), QHostAddress(remoteAddress), remotePort);
+
+ // and send
+ _udpSocket.writeDatagram(udpDatagram);
+
+ // readyRead miss workaround
+ if ( _udpSocket.hasPendingDatagrams() )
+ {
+ /* WORKAROUND INFO
+ * _udpSocket.bytesAvailable() > 0 and _udpSocket.hasPendingDatagrams(),
+ * but apparently no readyRead signal was emitted. As a result, incoming data
+ * is no longer processed. Temporary fix until the cause has been found:
+ * emit readyRead manually when datagrams are available. It is still possible
+ * that the ticker is faster than the readyRead-bound function and that readyRead
+ * was issued correctly, so we count two ticks of missed read calls. If the
+ * readyRead signal was correctly issued, the onUdpResponseRead function will
+ * reset the missed readcall counter. Even when botched, it is better to emit the
+ * same signal twice, rather than not at all (onUdpResponseRead has a failsaife
+ * for duplicate/redundant calls).
+ *
+ * Other info:
+ * is this related to https://www.qtcentre.org/threads/64370-QUdpSocket-readyRead-failure ?
+ *
+ * Displaying readyread state and available data:
+ qDebug() << "bytes available: "
+ << _udpSocket.bytesAvailable()
+ << "pending datagrams: "
+ << _udpSocket.hasPendingDatagrams()
+ ;
+ */
+ _missedReadCalls++;
+
+ // two missed calls in 2 ticks, emit signal manually
+ if ( _missedReadCalls >= 2)
+ {
+ _coreObject->Log.logEvent("warning", "checker udp data not signalled: emitting readyread manually");
+ emit _udpSocket.readyRead();
+ }
+ } // end workaround
+
+ } // end next
+}
diff --git a/src/UdpTasks/StatusChecker/playerinfoinsert.cpp b/src/UdpTasks/StatusChecker/playerinfoinsert.cpp
new file mode 100644
index 0000000..ce21422
--- /dev/null
+++ b/src/UdpTasks/StatusChecker/playerinfoinsert.cpp
@@ -0,0 +1,60 @@
+#include "statuschecker.h"
+
+bool StatusChecker::insertPlayerInfo(const QString &serverAddress,
+ const unsigned short &serverPort,
+ const QHash<QString, QString> &serverInfo)
+{
+ QSqlQuery q;
+ int serverID = -1;
+
+ // TODO: combine from 3 to 2 queries?
+ // (query optimisation)
+
+ // get server ID first
+ QString selectString = "SELECT id FROM serverlist "
+ "WHERE ip = :ip AND queryport = :queryport ";
+
+ q.prepare(selectString);
+ q.bindValue(":ip", serverAddress);
+ q.bindValue(":queryport", serverPort);
+
+ if ( ! q.exec() )
+ return reportQuery(q);
+
+ if ( q.next() ) // else serverID remains -1
+ serverID = q.value(0).toUInt();
+
+ // remove old player entries
+ QString deleteString = "DELETE FROM playerinfo WHERE sid = :sid";
+ q.prepare(deleteString);
+ q.bindValue(":sid", serverID);
+ if ( ! q.exec() )
+ return reportQuery(q);
+
+ // iterate through serverInfo player data
+ int playerIndex = 0;
+ while ( serverInfo.contains( QStringLiteral("player_%1").arg(playerIndex) ) )
+ {
+ QString insertString = "INSERT INTO playerinfo "
+ "(sid, name, team, frags, mesh, skin, face, ping, misc, dt_player) "
+ "VALUES (:sid, :name, :team, :frags, :mesh, :skin, :face, :ping, :misc, :dt_player)";
+ q.prepare(insertString);
+ q.bindValue(":sid", serverID);
+ q.bindValue(":name", serverInfo.value(QStringLiteral("player_%1").arg(playerIndex), "Player"));
+ q.bindValue(":team", serverInfo.value(QStringLiteral( "team_%1").arg(playerIndex), "0"));
+ q.bindValue(":frags", serverInfo.value(QStringLiteral( "frags_%1").arg(playerIndex), "0"));
+ q.bindValue(":mesh", serverInfo.value(QStringLiteral( "mesh_%1").arg(playerIndex), "default"));
+ q.bindValue(":skin", serverInfo.value(QStringLiteral( "skin_%1").arg(playerIndex), "default"));
+ q.bindValue(":face", serverInfo.value(QStringLiteral( "face_%1").arg(playerIndex), "default"));
+ q.bindValue(":ping", serverInfo.value(QStringLiteral( "ping_%1").arg(playerIndex), "0"));
+ q.bindValue(":misc", ""); // reserved for additional query info
+ q.bindValue(":dt_player", QDateTime::currentSecsSinceEpoch() );
+ if ( ! q.exec() )
+ return reportQuery(q);
+
+ // successfull insert, increase player index
+ playerIndex++;
+ }
+
+ return true;
+}
diff --git a/src/UdpTasks/StatusChecker/serverinfoinsert.cpp b/src/UdpTasks/StatusChecker/serverinfoinsert.cpp
new file mode 100644
index 0000000..e2e1f55
--- /dev/null
+++ b/src/UdpTasks/StatusChecker/serverinfoinsert.cpp
@@ -0,0 +1,22 @@
+#include "statuschecker.h"
+
+bool StatusChecker::insertServerInfo(const QString &serverAddress,
+ const unsigned short &serverPort)
+{
+ // insert query string
+ QSqlQuery q;
+ QString insertString = "INSERT INTO serverinfo (sid) "
+ "SELECT id FROM serverlist "
+ "WHERE ip = :ip AND queryport = :queryport";
+
+ // bind values and execute
+ q.prepare(insertString);
+ q.bindValue(":ip", serverAddress);
+ q.bindValue(":queryport", serverPort);
+
+ if ( ! q.exec() )
+ return reportQuery(q);
+
+ // was a row updated?
+ return (q.numRowsAffected() > 0);
+}
diff --git a/src/UdpTasks/StatusChecker/serverinfoupdate.cpp b/src/UdpTasks/StatusChecker/serverinfoupdate.cpp
new file mode 100644
index 0000000..1f0c0ed
--- /dev/null
+++ b/src/UdpTasks/StatusChecker/serverinfoupdate.cpp
@@ -0,0 +1,54 @@
+#include "statuschecker.h"
+
+bool StatusChecker::updateServerInfo(const QString &serverAddress,
+ const unsigned short &serverPort,
+ const QHash<QString, QString> &serverInfo)
+{
+ // start query string and query
+ QString updateString = "UPDATE serverinfo SET ";
+
+ { // add parameter placeholders
+ QStringListIterator updateField(_updateFields);
+ while ( updateField.hasNext() )
+ {
+ QString field = updateField.next();
+ if ( ! serverInfo.value(field).isEmpty() )
+ updateString += QStringLiteral("%1 = :%1, ").arg( field );
+ }
+ }
+
+ // complete update string
+ updateString += "mutators = :mutators, "
+ "dt_serverinfo = :dt_serverinfo "
+ "WHERE sid IN ("
+ "SELECT id FROM serverlist "
+ "WHERE ip = :ip AND queryport = :queryport"
+ ")";
+
+ // bind parameters
+ QSqlQuery q;
+ q.prepare(updateString);
+
+ { // add parameter bindings
+ QStringListIterator updateField(_updateFields);
+ while ( updateField.hasNext() )
+ {
+ QString field = updateField.next();
+ if ( ! serverInfo.value(field).isEmpty() )
+ q.bindValue( QStringLiteral(":%1").arg(field),
+ serverInfo.value(field));
+ }
+ }
+
+ // bind remaining values and execute
+ q.bindValue(":mutators", serverInfo.value("mutators", "None"));
+ q.bindValue(":dt_serverinfo", QDateTime::currentSecsSinceEpoch() );
+ q.bindValue(":ip", serverAddress);
+ q.bindValue(":queryport", serverPort);
+
+ if ( ! q.exec() )
+ return reportQuery(q);
+
+ // was a row updated?
+ return (q.numRowsAffected() > 0);
+}
diff --git a/src/UdpTasks/StatusChecker/statuschecker.cpp b/src/UdpTasks/StatusChecker/statuschecker.cpp
new file mode 100644
index 0000000..4806a98
--- /dev/null
+++ b/src/UdpTasks/StatusChecker/statuschecker.cpp
@@ -0,0 +1,7 @@
+#include "statuschecker.h"
+
+StatusChecker::StatusChecker(const QSharedPointer<CoreObject> &coreObject)
+{
+ // create local access
+ this->_coreObject = coreObject;
+}
diff --git a/src/UdpTasks/StatusChecker/statuschecker.h b/src/UdpTasks/StatusChecker/statuschecker.h
new file mode 100644
index 0000000..bf3c4c2
--- /dev/null
+++ b/src/UdpTasks/StatusChecker/statuschecker.h
@@ -0,0 +1,79 @@
+#ifndef STATUSCHECKER_H
+#define STATUSCHECKER_H
+
+#include <QTimer>
+#include <QUdpSocket>
+#include <QNetworkDatagram>
+
+#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 StatusChecker: public QObject
+{
+ Q_OBJECT
+public:
+ StatusChecker(const QSharedPointer<CoreObject> &coreObject);
+
+ // activate server checker/ticker
+ bool startTicker();
+
+private:
+ QSharedPointer<CoreObject> _coreObject;
+
+ // udp socket
+ QUdpSocket _udpSocket;
+ QString _udpRequest = "\\info\\";
+
+ // ticker and reset timers
+ QTimer _tickTicker;
+ QTimer _resetTimer;
+
+ // ticker helpers
+ int _dbIndex = -1;
+
+ // data buffer for received data.
+ QHash<QString, QString> _dataBuffer;
+
+ // secure/validate challenge buffer
+ QHash<QString, UdpData> _secureBuffer;
+
+ // temporary fix for readyRead signal not being emitted
+ int _missedReadCalls = 0;
+
+ // (defined) database fields to be updated in updateServerInfo(...)
+ const QStringList _updateFields
+ {
+ "hostname", "gamever", "minnetver", "location", "listenserver",
+ "adminname", "adminemail", "password", "gametype", "gamestyle",
+ "changelevels", "maptitle", "mapname", "numplayers", "maxplayers",
+ "minplayers", "botskill", "balanceteams", "playersbalanceteams",
+ "friendlyfire", "maxteams", "timelimit", "goalteamscore", "fraglimit",
+ "misc", "hostport"
+ };
+
+private slots:
+ // udp- and timer events
+ void onUdpResponseRead();
+ void onTicker(); // udp query to remote gameserver
+
+private:
+ // get next server from database
+ QSqlQuery getNextServer (const int &currentServerId);
+
+ // server info to database
+ bool insertServerInfo(const QString &serverAddress,
+ const unsigned short &serverPort);
+
+ bool updateServerInfo(const QString &serverAddress,
+ const unsigned short &serverPort,
+ const QHash<QString, QString> &serverInfo);
+
+ bool insertPlayerInfo(const QString &serverAddress,
+ const unsigned short &serverPort,
+ const QHash<QString, QString> &serverInfo);
+};
+
+#endif // STATUSCHECKER_H
diff --git a/src/UdpTasks/StatusChecker/statusticker.cpp b/src/UdpTasks/StatusChecker/statusticker.cpp
new file mode 100644
index 0000000..351a7ee
--- /dev/null
+++ b/src/UdpTasks/StatusChecker/statusticker.cpp
@@ -0,0 +1,47 @@
+#include "statuschecker.h"
+
+bool StatusChecker::startTicker()
+{
+
+ /* UDP Query Checking
+ *
+ * Send a \info\ or \status\ query to the remote gameserver. The
+ * server will respond with its serverdata, which is processed
+ * and stored in the database. Required for display on the website.
+ */
+
+ // check type (extended or simple)
+ _udpRequest = _coreObject->Settings.CheckerSettings.getExtendedInfo ? "\\status\\" : "\\info\\";
+
+ // set udp datagram handler
+ connect(&_udpSocket, &QUdpSocket::readyRead, this, &StatusChecker::onUdpResponseRead);
+
+ // set UDP+1 port for outgoing status checking (avoid querying from random port)
+ _udpSocket.bind(QHostAddress::Any, _coreObject->Settings.BeaconServerSettings.beaconPort+1);
+
+ // attach timer function to tick through servers (from db)
+ connect(&_tickTicker, &QTimer::timeout, this, &StatusChecker::onTicker);
+
+ // set reset timer and event ("cycle time")
+ connect(&_resetTimer, &QTimer::timeout, [this]
+ {
+ // reset indices and clear udp cache
+ _dbIndex = -1;
+ _dataBuffer.clear();
+ _secureBuffer.clear();
+ });
+ _resetTimer.setInterval( _coreObject->Settings.CheckerSettings.timeCheckerReset_s * 1000);
+ _resetTimer.start();
+
+ // ticker enable (keeps running, also when there are no new servers to be queried-- keeps firing every 1/serversPerSecond)
+ _tickTicker.setInterval( _coreObject->Settings.CheckerSettings.timeServerInterval_ms );
+ _tickTicker.start();
+
+ // announce servers per second (calculate from interval)
+ double serversPerSecond = (1000.0 / _coreObject->Settings.CheckerSettings.timeServerInterval_ms);
+ _coreObject->Log.logEvent("info", QStringLiteral("server status every %1 seconds (%2 servers/s)")
+ .arg( QString::number(_coreObject->Settings.CheckerSettings.timeCheckerReset_s),
+ QString::number(serversPerSecond, 'f', 1) ) );
+
+ return true;
+}
diff --git a/src/UdpTasks/udpdatastructure.h b/src/UdpTasks/udpdatastructure.h
new file mode 100644
index 0000000..16bd1f9
--- /dev/null
+++ b/src/UdpTasks/udpdatastructure.h
@@ -0,0 +1,30 @@
+#ifndef UDPDATASTRUCTURE_H
+#define UDPDATASTRUCTURE_H
+
+#include <QHostAddress>
+#include <QDateTime>
+
+// heartbeat struct for incoming udp heartbeats
+struct UdpData
+{
+ // update time or creation time
+ qint64 time = QDateTime::currentSecsSinceEpoch();
+
+ // address information
+ QString ip = "";
+ unsigned short port = 0;
+
+ // gamename (beacons)
+ QString gamename = "";
+
+ // secure / validate challenge data
+ QString secure = "";
+
+ // raw data from socket
+ QString rawData = "";
+
+ // parsed data as key->value
+ QMultiHash<QString, QString> serverData;
+};
+
+#endif // UDPDATASTRUCTURE_H