diff options
| author | Dark1-dev <shansarkar272@gmail.com> | 2023-03-01 21:30:57 +0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-03-01 21:30:57 +0600 |
| commit | 60a301a93b6057bb2c54ac04a7c38c38389037b3 (patch) | |
| tree | b09c5f8bc0045828c660654d8ed6744663856202 /src/UdpTasks | |
| parent | c784240d1af68dbd8d0466822b34fd05d6ccdda1 (diff) | |
| download | Masterserver-Qt5-60a301a93b6057bb2c54ac04a7c38c38389037b3.tar.gz Masterserver-Qt5-60a301a93b6057bb2c54ac04a7c38c38389037b3.zip | |
Add files via upload
Diffstat (limited to 'src/UdpTasks')
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 ¤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<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 ¤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<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 |
