aboutsummaryrefslogtreecommitdiff
path: root/src/UdpTasks/StatusChecker
diff options
context:
space:
mode:
Diffstat (limited to 'src/UdpTasks/StatusChecker')
-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
9 files changed, 484 insertions, 0 deletions
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;
+}