From 60a301a93b6057bb2c54ac04a7c38c38389037b3 Mon Sep 17 00:00:00 2001 From: Dark1-dev Date: Wed, 1 Mar 2023 21:30:57 +0600 Subject: Add files via upload --- src/UdpTasks/StatusChecker/getnextserver.cpp | 22 ++++ .../StatusChecker/oncheckerresponseread.cpp | 112 +++++++++++++++++++++ src/UdpTasks/StatusChecker/onticker.cpp | 81 +++++++++++++++ src/UdpTasks/StatusChecker/playerinfoinsert.cpp | 60 +++++++++++ src/UdpTasks/StatusChecker/serverinfoinsert.cpp | 22 ++++ src/UdpTasks/StatusChecker/serverinfoupdate.cpp | 54 ++++++++++ src/UdpTasks/StatusChecker/statuschecker.cpp | 7 ++ src/UdpTasks/StatusChecker/statuschecker.h | 79 +++++++++++++++ src/UdpTasks/StatusChecker/statusticker.cpp | 47 +++++++++ 9 files changed, 484 insertions(+) create mode 100644 src/UdpTasks/StatusChecker/getnextserver.cpp create mode 100644 src/UdpTasks/StatusChecker/oncheckerresponseread.cpp create mode 100644 src/UdpTasks/StatusChecker/onticker.cpp create mode 100644 src/UdpTasks/StatusChecker/playerinfoinsert.cpp create mode 100644 src/UdpTasks/StatusChecker/serverinfoinsert.cpp create mode 100644 src/UdpTasks/StatusChecker/serverinfoupdate.cpp create mode 100644 src/UdpTasks/StatusChecker/statuschecker.cpp create mode 100644 src/UdpTasks/StatusChecker/statuschecker.h create mode 100644 src/UdpTasks/StatusChecker/statusticker.cpp (limited to 'src/UdpTasks/StatusChecker') 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 ¤tServerId) +{ + // 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 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 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(q.value("queryport").toInt()); + QString gamename = q.value("gamename").toString(); + bool authenticated = static_cast(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 &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 &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) +{ + // 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 +#include +#include + +#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); + + // activate server checker/ticker + bool startTicker(); + +private: + QSharedPointer _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 _dataBuffer; + + // secure/validate challenge buffer + QHash _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 ¤tServerId); + + // server info to database + bool insertServerInfo(const QString &serverAddress, + const unsigned short &serverPort); + + bool updateServerInfo(const QString &serverAddress, + const unsigned short &serverPort, + const QHash &serverInfo); + + bool insertPlayerInfo(const QString &serverAddress, + const unsigned short &serverPort, + const QHash &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; +} -- cgit v1.2.3