diff options
53 files changed, 2771 insertions, 0 deletions
diff --git a/src/MasterServer-Qt5.pro b/src/MasterServer-Qt5.pro new file mode 100644 index 0000000..1ba6bdc --- /dev/null +++ b/src/MasterServer-Qt5.pro @@ -0,0 +1,115 @@ +QT += core network sql +QT -= gui + +CONFIG += c++11 console +CONFIG -= app_bundle + +# The following define makes your compiler emit warnings if you use +# any Qt feature that has been marked deprecated (the exact warnings +# depend on your compiler). Please consult the documentation of the +# deprecated API in order to know how to port your code away from it. +#DEFINES += QT_DEPRECATED_WARNINGS + +# You can also make your code fail to compile if it uses deprecated APIs. +# In order to do so, uncomment the following line. +# You can also select to disable deprecated APIs only up to a certain version of Qt. +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + Core/CoreObject/coreobject.cpp \ + Database/Common/existserver.cpp \ + Database/Common/getgamenames.cpp \ + Database/Common/getnumgames.cpp \ + Database/Common/insertserver.cpp \ + Database/Common/selectserverlist.cpp \ + Database/Common/updateserver.cpp \ + Database/closedatabase.cpp \ + Database/createtables.cpp \ + Database/initdatabase.cpp \ + Database/reportquery.cpp \ + Core/GameInfo/loadsupportedgames.cpp \ + Logger/cyclelogfile.cpp \ + Logger/initlog.cpp \ + Logger/logevent.cpp \ + Logger/logger.cpp \ + Logger/openlogfile.cpp \ + Logger/writelogfile.cpp \ + Settings/loadsettings.cpp \ + Settings/writesettings.cpp \ + Core/core.cpp \ + Core/corerun.cpp \ + Maintenance/maintenance.cpp \ + Maintenance/onmaintenancetimeraction.cpp \ + Maintenance/prunebeacons.cpp \ + Maintenance/pruneplayers.cpp \ + Maintenance/pruneserverinfo.cpp \ + Maintenance/schedulemaintenance.cpp \ + Maintenance/updatestats.cpp \ + Protocols/GameSpy0/algorithm.cpp \ + Protocols/GameSpy0/gamespy0.cpp \ + Protocols/GameSpy0/securevalidate.cpp \ + Protocols/overrides.cpp \ + TcpTasks/ListenClientHandler/compileserverlist.cpp \ + TcpTasks/ListenClientHandler/compilesynclist.cpp \ + TcpTasks/ListenClientHandler/listenclientdisconnect.cpp \ + TcpTasks/ListenClientHandler/listenclienthandler.cpp \ + TcpTasks/ListenClientHandler/onlistenclientread.cpp \ + TcpTasks/ListenServer/listenserver.cpp \ + TcpTasks/ListenServer/onlistenconnection.cpp \ + TcpTasks/ListenServer/tcplisten.cpp \ + TcpTasks/SyncClient/onsyncconnect.cpp \ + TcpTasks/SyncClient/onsyncdisconnect.cpp \ + TcpTasks/SyncClient/onsyncread.cpp \ + TcpTasks/SyncClient/syncclient.cpp \ + TcpTasks/SyncClient/syncreplyquery.cpp \ + TcpTasks/SyncClient/updatesyncedserver.cpp \ + UdpTasks/BeaconServer/Receive/heartbeatgamespy0.cpp \ + UdpTasks/BeaconServer/Receive/replyquery.cpp \ + UdpTasks/BeaconServer/Receive/udponread.cpp \ + UdpTasks/BeaconServer/Receive/udpontimeout.cpp \ + UdpTasks/BeaconServer/Uplink/onuplinktimer.cpp \ + UdpTasks/BeaconServer/Uplink/uplink.cpp \ + UdpTasks/BeaconServer/beaconserver.cpp \ + UdpTasks/BeaconServer/udplisten.cpp \ + UdpTasks/StatusChecker/getnextserver.cpp \ + UdpTasks/StatusChecker/oncheckerresponseread.cpp \ + UdpTasks/StatusChecker/onticker.cpp \ + UdpTasks/StatusChecker/playerinfoinsert.cpp \ + UdpTasks/StatusChecker/serverinfoinsert.cpp \ + UdpTasks/StatusChecker/serverinfoupdate.cpp \ + UdpTasks/StatusChecker/statuschecker.cpp \ + UdpTasks/StatusChecker/statusticker.cpp \ + TcpTasks/Updater/onsynctickeraction.cpp \ + TcpTasks/Updater/scheduleupdater.cpp \ + TcpTasks/Updater/syncupdater.cpp \ + main.cpp + +# Default rules for deployment. +qnx: target.path = /tmp/$${TARGET}/bin +else: unix:!android: target.path = /opt/$${TARGET}/bin +!isEmpty(target.path): INSTALLS += target + +HEADERS += \ + Core/CoreObject/coreobject.h \ + Core/CoreObject/serverinfostructure.h \ + Core/GameInfo/gameinfostructure.h \ + Database/Common/commonactions.h \ + Database/databaseinterface.h \ + Core/GameInfo/loadsupportedgames.h \ + Logger/logger.h \ + Logger/logprimitive.h \ + Settings/loadsettings.h \ + Settings/settingstructure.h \ + Core/core.h \ + Core/version.h \ + Maintenance/maintenance.h \ + Protocols/GameSpy0/gamespy0.h \ + Protocols/GameSpy0/securevalidate.h \ + Protocols/overrides.h \ + TcpTasks/ListenClientHandler/listenclienthandler.h \ + TcpTasks/ListenServer/listenserver.h \ + TcpTasks/SyncClient/syncclient.h \ + UdpTasks/BeaconServer/beaconserver.h \ + UdpTasks/StatusChecker/statuschecker.h \ + UdpTasks/udpdatastructure.h \ + TcpTasks/Updater/syncupdater.h diff --git a/src/Protocols/GameSpy0/algorithm.cpp b/src/Protocols/GameSpy0/algorithm.cpp new file mode 100644 index 0000000..5250079 --- /dev/null +++ b/src/Protocols/GameSpy0/algorithm.cpp @@ -0,0 +1,96 @@ +#include "securevalidate.h" + +/* This algorithm is based on Luigi Auriemma's gsmsalg 0.3.3, + * https://aluigi.altervista.org/papers/gsmsalg.h + * under GNU General Public License v2. + */ +QString generateValidateString(const QByteArray &cipher, + const QByteArray &secure, + const int &enctype) +{ + unsigned char cipherLength = static_cast<unsigned char>( cipher.length() ); + unsigned char secureLength = static_cast<unsigned char>( secure.length() ); + + // fill array with ascii characters + unsigned char enc[256]; + for (unsigned short j = 0; j < 256; j++) + { + enc[j] = static_cast<unsigned char>(j); + } + + // cipher shuffle + unsigned char a = 0, x; + for (unsigned short j = 0; j < 256; j++) + { + a += enc[j] + cipher[j % cipherLength]; + x = enc[a]; + enc[a] = enc[j]; + enc[j] = x; + } + + // secure shuffle + unsigned char tmp[66]; + unsigned char i = 0, + y = 0, + b = 0; + a = 0; // reset a + for (i = 0; i < cipherLength; i++) + { + a += secure.at(i) + 1; + x = enc[a]; + b += x; + y = enc[b]; + enc[b] = x; + enc[a] = y; + tmp[i] = static_cast<unsigned char>( secure.at(i) ^ enc[ (x+y) & 0xff ] ); + } + + // part of the enctype 1-2 process (uses i from previous loop) + for (secureLength = i; secureLength % 3; secureLength++) + { + tmp[secureLength] = 0; + } + + // enctype 1 shuffle + if (enctype == 1) + { + for (i = 0; i < secureLength; i++) + { + tmp[i] = enctype1_data[ tmp[i] ]; + } + } + else if (enctype == 2 ) + { + for (i = 0; i < secureLength; i++) + { + tmp[i] = static_cast<unsigned char>(tmp[i] ^ cipher[i % cipherLength] ); + } + } + + // final shuffle and stitch validate response together + unsigned char z = 0; + QString validate; + for (i = 0; i < secureLength; i += 3) + { + x = (tmp[i]); + y = (tmp[i+1]); + z = (tmp[i+2]); + + validate.append( charshift (x >> 2) ); + validate.append( charshift (static_cast<unsigned char>(((x & 3) << 4) | (y >> 4)) )); + validate.append( charshift (static_cast<unsigned char>(((y & 15) << 2) | (z >> 6)) )); + validate.append( charshift (z & 63)); + } + return validate; +} + +// part of gsmsalg 0.3.3 and license +unsigned char charshift ( const unsigned char ® ) +{ + if (reg < 26) return (reg + 'A'); + if (reg < 52) return (reg + 'G'); + if (reg < 62) return (reg - 4); + if (reg == 62) return ('+'); + if (reg == 63) return ('/'); + return (0); +} diff --git a/src/Protocols/GameSpy0/gamespy0.cpp b/src/Protocols/GameSpy0/gamespy0.cpp new file mode 100644 index 0000000..6595d4a --- /dev/null +++ b/src/Protocols/GameSpy0/gamespy0.cpp @@ -0,0 +1,51 @@ +#include "gamespy0.h" + +QMultiHash<QString, QString> parseGameSpy0Buffer(const QString &bufferString) +{ + // initialise output hash + QMultiHash<QString, QString> queryStringHash; + + // split on backslash + QStringList bufferStringList = bufferString.split('\\', QString::KeepEmptyParts); + + // iterate through all items + QListIterator<QString> property (bufferStringList); + + // the first element is always empty -- skip it + if ( property.hasNext() ) + { + property.next(); + } + + // store as key -> value + while ( property.hasNext() ) + { + // unify valid keys + QString key = overrideKey( property.next().trimmed() ); + + // see if a value for this key exists + if ( ! property.hasNext() ) + break; + + // get value + QString value = property.next().trimmed(); + + // insert to return hash + queryStringHash.insert(key, value); + } + + // override gamename + if ( queryStringHash.contains("gamename") ) + { + QList<QString> gn = queryStringHash.values("gamename"); + queryStringHash.remove("gamename"); + + // read backwards to preserve element order + for (int i = gn.size()-1; i >= 0; i--) + { + queryStringHash.insert("gamename", overrideGamename( gn.value(i) ) ); + } + } + + return queryStringHash; +} diff --git a/src/Protocols/GameSpy0/gamespy0.h b/src/Protocols/GameSpy0/gamespy0.h new file mode 100644 index 0000000..9a28fa8 --- /dev/null +++ b/src/Protocols/GameSpy0/gamespy0.h @@ -0,0 +1,10 @@ +#ifndef GAMESPY0_H +#define GAMESPY0_H + +#include <QHash> +#include <QMultiHash> +#include "Protocols/overrides.h" + +QMultiHash<QString, QString> parseGameSpy0Buffer(const QString &bufferString); + +#endif // GAMESPY0_H diff --git a/src/Protocols/GameSpy0/securevalidate.cpp b/src/Protocols/GameSpy0/securevalidate.cpp new file mode 100644 index 0000000..0532640 --- /dev/null +++ b/src/Protocols/GameSpy0/securevalidate.cpp @@ -0,0 +1,90 @@ +#include "securevalidate.h" + +AuthResult validateGamename(const bool &isBeacon, + const QString &gamename, + const QString &validate, + const QString &cipherIn, + const QString &secureIn, + const int &enctypeIn) +{ + // output result + AuthResult authResult; + + // override certain cases for UDP beacon + if ( isBeacon and _overrideValidateBeacon.contains(gamename) ) + { + authResult.auth = true; + authResult.validate = "override"; + return authResult; + } + + // override certain cases for TCP client + if ( ! isBeacon and _overrideValidateClient.contains(gamename) ) + { + authResult.auth = true; + authResult.validate = "override"; + return authResult; + } + + // inputs + checks + QByteArray secure = secureIn.toLatin1(); + QByteArray cipher = cipherIn.toLatin1(); + + // get validate value + if ( 6 <= cipher.length() and cipher.length() < 16 and + 6 <= secure.length() and secure.length() < 16 ) + { + // safe to call validation. proceed. + authResult.validate = generateValidateString(cipher, secure, enctypeIn); + } + else + { + // incorrect input, not safe to calculate validation + authResult.auth = false; + authResult.validate = "invalid!"; + } + + // correct validation provided? + authResult.auth = (authResult.validate.compare(validate) == 0); + + // return result as boolean and string + return authResult; +} + +QString returnValidate(const QByteArray &cipher, + const QByteArray &secure, + const int &enctype) +{ + // get validate value + if ( 6 <= cipher.length() and cipher.length() < 16 and + 6 <= secure.length() and secure.length() < 16 ) + { + return generateValidateString(cipher, secure, enctype); + } + else + { + return "invalid!"; + } +} + +QString genChallengeString(const int len, const bool moreChars) +{ + QString randomString; + if (moreChars) + { + // use A-Za-z0-9 + for(unsigned char i = 0; i < len; ++i) + { + randomString += moreCharacters[qrand() % moreCharacters.length()]; + } + } + else + { + // use A-Z only + for(unsigned char i = 0; i < len; ++i) + { + randomString += possibleCharacters[qrand() % possibleCharacters.length()]; + } + } + return randomString; +} diff --git a/src/Protocols/GameSpy0/securevalidate.h b/src/Protocols/GameSpy0/securevalidate.h new file mode 100644 index 0000000..b767bda --- /dev/null +++ b/src/Protocols/GameSpy0/securevalidate.h @@ -0,0 +1,62 @@ +#ifndef SECUREVALIDATE_H +#define SECUREVALIDATE_H + +#include <QString> +#include "Protocols/overrides.h" + +// return both status and result string +struct AuthResult { + bool auth = false; + QString validate = ""; +}; + +// authenticate the beacon/client and return the validate string +AuthResult validateGamename(const bool &isBeacon, + const QString &gamename, + const QString &validate, + const QString &cipherIn, + const QString &secureIn, + const int &enctypeIn); + +// return validate string (acccepts unsanitised inputs) +QString returnValidate(const QByteArray &cipher, + const QByteArray &secure, + const int &enctype); + +// generate random challenge strings +QString genChallengeString(const int len, const bool moreChars); + + +// algorithm for the secure/validate challenge +unsigned char charshift ( const unsigned char ® ); +QString generateValidateString(const QByteArray &cipher, + const QByteArray &secure, + const int &enctype); + +// pre-built characters for generating validate string +const QString possibleCharacters("ABCDEFGHIJKLMNOPQRSTUVWXYZ"); +const QString moreCharacters("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"); + +/* Pre-built algorithm data for enctype 1 from Luigi Auriemma's + * gsmsalg 0.3.3, https://aluigi.altervista.org/papers/gsmsalg.h + * under GNU General Public License v2. + */ +const unsigned char enctype1_data[] = ( +"\x01\xba\xfa\xb2\x51\x00\x54\x80\x75\x16\x8e\x8e\x02\x08\x36\xa5" +"\x2d\x05\x0d\x16\x52\x07\xb4\x22\x8c\xe9\x09\xd6\xb9\x26\x00\x04" +"\x06\x05\x00\x13\x18\xc4\x1e\x5b\x1d\x76\x74\xfc\x50\x51\x06\x16" +"\x00\x51\x28\x00\x04\x0a\x29\x78\x51\x00\x01\x11\x52\x16\x06\x4a" +"\x20\x84\x01\xa2\x1e\x16\x47\x16\x32\x51\x9a\xc4\x03\x2a\x73\xe1" +"\x2d\x4f\x18\x4b\x93\x4c\x0f\x39\x0a\x00\x04\xc0\x12\x0c\x9a\x5e" +"\x02\xb3\x18\xb8\x07\x0c\xcd\x21\x05\xc0\xa9\x41\x43\x04\x3c\x52" +"\x75\xec\x98\x80\x1d\x08\x02\x1d\x58\x84\x01\x4e\x3b\x6a\x53\x7a" +"\x55\x56\x57\x1e\x7f\xec\xb8\xad\x00\x70\x1f\x82\xd8\xfc\x97\x8b" +"\xf0\x83\xfe\x0e\x76\x03\xbe\x39\x29\x77\x30\xe0\x2b\xff\xb7\x9e" +"\x01\x04\xf8\x01\x0e\xe8\x53\xff\x94\x0c\xb2\x45\x9e\x0a\xc7\x06" +"\x18\x01\x64\xb0\x03\x98\x01\xeb\x02\xb0\x01\xb4\x12\x49\x07\x1f" +"\x5f\x5e\x5d\xa0\x4f\x5b\xa0\x5a\x59\x58\xcf\x52\x54\xd0\xb8\x34" +"\x02\xfc\x0e\x42\x29\xb8\xda\x00\xba\xb1\xf0\x12\xfd\x23\xae\xb6" +"\x45\xa9\xbb\x06\xb8\x88\x14\x24\xa9\x00\x14\xcb\x24\x12\xae\xcc" +"\x57\x56\xee\xfd\x08\x30\xd9\xfd\x8b\x3e\x0a\x84\x46\xfa\x77\xb8"); + +#endif // SECUREVALIDATE_H diff --git a/src/Protocols/overrides.cpp b/src/Protocols/overrides.cpp new file mode 100644 index 0000000..957c489 --- /dev/null +++ b/src/Protocols/overrides.cpp @@ -0,0 +1,44 @@ +#include "overrides.h" + +// some games use different key/value pairs. override with known key. +QString overrideKey(const QString &rawKey) +{ + // convert to lowercase + QString key = rawKey.toLower(); + + // some keys are indexed, like "player_0". + if ( _index_match.indexIn(key) >= 0 ) + { + // find keyword without index and match key + if ( _validKeys.contains( _index_match.cap(1) ) ) // group starts at 1, not 0 + { + // concat the index back and return found override + return _validKeys.value( _index_match.cap(1), key ) + _index_match.cap(2); + } + } + else + { + // non-indexed key + if ( _validKeys.contains( key ) ) + { + // return found override + return _validKeys.value(key, key); + } + } + + // no override found. return original. + return key; +} + +// some games deviate from the gamename protocol. override with correct gamename +QString overrideGamename(const QString &gamenameIn) +{ + // convert to lowercase + QString gamename = gamenameIn.toLower(); + + if ( _validGamenames.contains(gamename ) ) + { + return _validGamenames.value(gamename, gamename); + } + return gamename; +} diff --git a/src/Protocols/overrides.h b/src/Protocols/overrides.h new file mode 100644 index 0000000..eb6f8b2 --- /dev/null +++ b/src/Protocols/overrides.h @@ -0,0 +1,70 @@ +#ifndef OVERRIDES_H +#define OVERRIDES_H + +#include <QHash> +#include <QString> +#include <QRegExp> + +// parse to a valid key/value pair +QString overrideKey(const QString &rawKey); + +// parse to a valid gamename +QString overrideGamename(const QString &gamenameIn); + +// record of gamenames that need to be overridden +const QHash<QString, QString> _validGamenames +{ + {"JetFighter IV", "jetfighter4"}, + {"igi2" , "projectigi2r"}, +}; + +// record of query keys that need to be overridden +const QHash<QString, QString> _validKeys +{ + {"version", "gamever"}, + {"mingamever", "minnetver"}, + {"admin", "adminname"}, + {"adminname", "adminname"}, + {"admin e-mail", "adminemail"}, + {"friendly fire", "friendlyfire"}, + {"friendly fire?", "friendlyfire"}, + {"mapfilename", "mapname"}, + {"mapid", "mapname"}, + {"skill", "botskill"}, + {"num teams", "maxteams"}, + {"time limit", "timelimit"}, + {"time_limit", "timelimit"}, + {"timetowin", "timelimit"}, + {"roundtime", "timelimit"}, + {"active_mods", "mutators"}, + {"activemod", "mutators"}, + {"Mutator", "mutators"}, + {"Mutator101", "mutators"}, + {"Mutator102", "mutators"}, + {"Mutator103", "mutators"}, + {"Mutator104", "mutators"}, + {"playername", "player"}, + {"teamname", "team"}, + {"score", "score"}, + {"kills", "score"}, +}; + +// some games do not (fully) support secure/validate +const QStringList _overrideValidateBeacon +{ + "deusex", + "rune", + "wot", +}; + +const QStringList _overrideValidateClient +{ + "deusex", + "wot", +}; + +// matching regexes +// TODO: replace with QRegularExpression +const QRegExp _index_match{"^(\\w+)(_\\d+)$"}; + +#endif // OVERRIDES_H diff --git a/src/Settings/loadsettings.cpp b/src/Settings/loadsettings.cpp new file mode 100644 index 0000000..943311b --- /dev/null +++ b/src/Settings/loadsettings.cpp @@ -0,0 +1,144 @@ +#include "loadsettings.h" + +SettingStructure loadSettings (const QString &applicationPath) +{ + // return object + SettingStructure settings; + + // determine if file exists + QString settingsFilePath = applicationPath + "/" + _settingsPath; + if ( ! QFile(settingsFilePath).exists() ) + { + // no settings file exists. ask to generate clean settings file. + logPrimitive() << "No valid settings file was found at " << settingsFilePath << endl + << "Do you want to generate a clean settings file? [y/N]" << endl; + + // if opted for new config, create a new config file at the provided location + if ( QTextStream(stdin).readLine().startsWith("y") ) + { + // generate new settings file + writeSettings(settingsFilePath); + + // inform that a file was written + logPrimitive() << "A new settings file was generated. Please update your " + << "settings in " << settingsFilePath << endl + << "and restart the application." << endl; + } + // do NOT init = true, the application is now intended to shut down due to init==false + return settings; + } + + // open settings + QSettings settingsFile(settingsFilePath, QSettings::IniFormat); + if ( settingsFile.status() != QSettings::NoError ) + { + // error occurred. report and quit. + logPrimitive() << "An error occurred while loading the configuration (" << QString::number( settingsFile.status() ) << ")."; + + // do NOT init = true, the application is now intended to shut down due to init==false + return settings; + } + + // logging settings + settings.LoggingSettings.cycle = settingsFile.value("Logging/CycleLogs", settings.LoggingSettings.cycle).toString(); + settings.LoggingSettings.suppressLog = settingsFile.value("Logging/SuppressLog", settings.LoggingSettings.suppressLog).toString(); + settings.LoggingSettings.suppressDisplay = settingsFile.value("Logging/SuppressDisplay", settings.LoggingSettings.suppressDisplay).toString(); + + // beacon server settings (udp server) + settings.BeaconServerSettings.beaconPort = static_cast<unsigned short>( settingsFile.value("BeaconServer/BeaconPort", settings.BeaconServerSettings.beaconPort).toInt() ); + settings.BeaconServerSettings.doUplink = settingsFile.value("BeaconServer/DoUplink", settings.BeaconServerSettings.doUplink).toBool(); + + // listen server settings (tcp server) + settings.ListenServerSettings.listenPort = static_cast<unsigned short>( settingsFile.value("ListenServer/ListenPort", settings.ListenServerSettings.listenPort).toInt() ); + settings.ListenServerSettings.serverttl_s = settingsFile.value("ListenServer/ServerLifeTime_s", settings.ListenServerSettings.serverttl_s).toInt(); + + // syncer settings (tcp client) + settings.SyncerSettings.doSync = settingsFile.value("Syncer/DoSync", settings.SyncerSettings.doSync).toBool(); + settings.SyncerSettings.syncGames = settingsFile.value("Syncer/SyncGames", settings.SyncerSettings.syncGames).toString().toLower(); + settings.SyncerSettings.syncInterval_s = settingsFile.value("Syncer/SyncInterval_s", settings.SyncerSettings.syncInterval_s).toInt(); + + // error and number of items + int parseError = -1; + int len = settingsFile.beginReadArray("Syncer"); + + // read list of syncer items + for (int i = 0; i < len; i++) + { + settingsFile.setArrayIndex(i); + SyncServer syncServer; + QStringList strServer = settingsFile.value("SyncServer").toString().split(","); + + if ( strServer.length() >= 3 ) + { + // parse + syncServer.remoteAddress = strServer.value(0).trimmed(); + syncServer.beaconPort = strServer.value(1).toUShort(); + syncServer.listenPort = strServer.value(2).toUShort(); + + // sanity checks + if (! syncServer.remoteAddress.isEmpty() and + syncServer.beaconPort > 0 and + syncServer.listenPort > 0) + { + // add + settings.SyncerSettings.syncServers.append(syncServer); + continue; + } + } + + // else input error, do not continue parsing + parseError = i; + break; + } + settingsFile.endArray(); + + // server checker (udp client ticker) + settings.CheckerSettings.doCheck = settingsFile.value("Checker/DoCheck", settings.CheckerSettings.doCheck).toBool(); + settings.CheckerSettings.getExtendedInfo = settingsFile.value("Checker/GetExtendedInformation", settings.CheckerSettings.getExtendedInfo).toBool(); + settings.CheckerSettings.timeServerInterval_ms = settingsFile.value("Checker/ServerCheckInterval_ms", settings.CheckerSettings.timeServerInterval_ms).toInt(); + settings.CheckerSettings.timeCheckerReset_s = settingsFile.value("Checker/CycleInterval_s", settings.CheckerSettings.timeCheckerReset_s).toInt(); + + // maintenance settings + settings.MaintenanceSettings.doMaintenance = settingsFile.value("Maintenance/DoMaintenance", settings.MaintenanceSettings.doMaintenance).toBool(); + settings.MaintenanceSettings.timeMaintenanceInterval_s = settingsFile.value("Maintenance/MaintainRate_s", settings.MaintenanceSettings.timeMaintenanceInterval_s).toInt(); + + // public details + settings.PublicInformationSettings.hostname = settingsFile.value("PublicDetails/Hostname", "").toString(); + settings.PublicInformationSettings.adminName = settingsFile.value("PublicDetails/AdminName", "").toString(); + settings.PublicInformationSettings.contact = settingsFile.value("PublicDetails/Contact", "").toString(); + + // sanity checks + try { + // beacon / udp server + if ( settings.BeaconServerSettings.beaconPort <= 0 ) throw QString("BeaconServer/BeaconPort"); + + // listen / tcp server + if ( settings.ListenServerSettings.listenPort <= 0 ) throw QString("ListenServer/ListenPort"); + if ( settings.ListenServerSettings.serverttl_s < 0) throw QString("ListenServer/ServerLifeTimeSeconds"); + + // syncer / tcp client + if ( settings.SyncerSettings.syncGames.isEmpty() ) throw QString("Syncer/SyncGames"); + if ( settings.SyncerSettings.syncInterval_s <= 0 ) throw QString(); + if (parseError >= 0) throw QStringLiteral("Syncer/SyncServer[%1]").arg(QString::number(parseError+1)); + + // server checker / udp client + if ( settings.CheckerSettings.timeServerInterval_ms < 10 ) throw QString("Checker/ServerCheckInterval_ms"); + if ( settings.CheckerSettings.timeCheckerReset_s < 1 ) throw QString("Checker/CycleInterval_s"); + + // make sure that details are filled in + if (settings.PublicInformationSettings.hostname.isEmpty()) throw QString("PublicDetails/Hostname"); + if (settings.PublicInformationSettings.adminName.isEmpty()) throw QString("PublicDetails/AdminName"); + if (settings.PublicInformationSettings.contact.isEmpty()) throw QString("PublicDetails/Contact"); + + } + catch (QString error) + { + logPrimitive() << "One or more settings are incorrect: setting \"" << error << "\" has an incorrect value! " + << "Please correct the value and restart the application." << endl; + return settings; + } + + // loading settings complete + settings.init = true; + return settings; +} diff --git a/src/Settings/loadsettings.h b/src/Settings/loadsettings.h new file mode 100644 index 0000000..540e1a9 --- /dev/null +++ b/src/Settings/loadsettings.h @@ -0,0 +1,19 @@ +#ifndef LOADSETTINGS_H +#define LOADSETTINGS_H + +#include <QFile> +#include <QSettings> + +#include "Logger/logprimitive.h" +#include "settingstructure.h" + +// settings path (following README structure) +const QString _settingsPath = "../data/MasterServer-Settings.ini"; + +// load all settings from the config file +SettingStructure loadSettings(const QString &applicationPath); + +// write all settings to the config file +void writeSettings(const QString &settingsFilePath); + +#endif // LOADSETTINGS_H diff --git a/src/Settings/settingstructure.h b/src/Settings/settingstructure.h new file mode 100644 index 0000000..b480164 --- /dev/null +++ b/src/Settings/settingstructure.h @@ -0,0 +1,119 @@ +#ifndef SETTINGSTRUCTURE_H +#define SETTINGSTRUCTURE_H + +#include <QString> +#include <QList> + +// masterservers sync options +struct SyncServer +{ + // domain name string, not QHostAddress + QString remoteAddress; + + // udp port + unsigned short int beaconPort = 27900; + + // tcp port + unsigned short int listenPort = 28900; +}; + +// cascaded struct with setting structure +struct SettingStructure +{ + // initialisation check + bool init = false; + + // log settings + struct LoggingSettings + { + // never, yearly, monthly, weekly, daily + QString cycle = "weekly"; + + // suppress type: [timestamp][type] <message> + QString suppressLog = "debug udp tcp"; + QString suppressDisplay = "debug udp tcp"; + } + LoggingSettings; + + // udp beacon server settings + struct BeaconServerSettings + { + // default port 27900 + unsigned short int beaconPort = 27900; + + // uplink settings enabled by default + bool doUplink = true; + } + BeaconServerSettings; + + // tcp listen server settings + struct ListenServerSettings + { + // default port 28900 + unsigned short int listenPort = 28900; + + // server time to live for client list + int serverttl_s = 1800; + } + ListenServerSettings; + + // synchronisation settings (works only with 333networks-compatible masterservers) + struct SyncerSettings + { + // syncer settings enabled by default + bool doSync = true; + + // sync games (which games to sync) + QString syncGames = "all"; + + // list of servers to sync + QList<SyncServer> syncServers; + + // sync event interval + int syncInterval_s = 1800; + } + SyncerSettings; + + // checker settings (query all individual servers to determine their state) + struct CheckerSettings + { + // check individual remote servers? + bool doCheck = true; + + // get information for the website too? + bool getExtendedInfo = true; + + // time between servers (ticker) + int timeServerInterval_ms = 250; + + // cycle time before a reset takes place + int timeCheckerReset_s = 900; // every 15 minutes + } CheckerSettings; + + // maintenance settings + struct MaintenanceSettings + { + // do maintenance? + bool doMaintenance = true; + + // interval + int timeMaintenanceInterval_s = 300; // every 5 minutes + } + MaintenanceSettings; + + // contact information + struct PublicInformationSettings + { + // your website, domain name, brand name or identity + QString hostname = ""; + + // your (nick)name + QString adminName = ""; + + // your e-mailaddress (format not checked, TODO) + QString contact = ""; + } + PublicInformationSettings; +}; + +#endif // SETTINGSTRUCTURE_H diff --git a/src/Settings/writesettings.cpp b/src/Settings/writesettings.cpp new file mode 100644 index 0000000..22d72af --- /dev/null +++ b/src/Settings/writesettings.cpp @@ -0,0 +1,76 @@ +#include "loadsettings.h" + +void writeSettings (const QString &settingsFilePath) +{ + // user already specified that config is to be written + QSettings settingsFile(settingsFilePath, QSettings::IniFormat); + + // initialise default settings to write to config + SettingStructure settings; + + // logging settings + settingsFile.setValue("Logging/CycleLogs", settings.LoggingSettings.cycle); + settingsFile.setValue("Logging/SuppressLog", settings.LoggingSettings.suppressLog); + settingsFile.setValue("Logging/SuppressDisplay", settings.LoggingSettings.suppressDisplay); + + // beacon server settings (udp server) + settingsFile.setValue("BeaconServer/BeaconPort", settings.BeaconServerSettings.beaconPort); + settingsFile.setValue("BeaconServer/DoUplink", settings.BeaconServerSettings.doUplink); + + // listen server settings (tcp server) + settingsFile.setValue("ListenServer/ListenPort", settings.ListenServerSettings.listenPort); + settingsFile.setValue("ListenServer/ServerLifeTime_s", settings.ListenServerSettings.serverttl_s); + + // syncer settings (tcp client) + settingsFile.setValue("Syncer/DoSync", settings.SyncerSettings.doSync); + settingsFile.setValue("Syncer/SyncGames", settings.SyncerSettings.syncGames); + settingsFile.setValue("Syncer/SyncInterval_s", settings.SyncerSettings.syncInterval_s); + + // if sync is set, but no servers are listed, generate default line with 333networks + if ( settings.SyncerSettings.doSync and settings.SyncerSettings.syncServers.size() <= 0 ) + { + SyncServer defaultServer; + defaultServer.remoteAddress = "master.333networks.com"; // default settings are correct + settings.SyncerSettings.syncServers.append(defaultServer); + } + + // write list of servers + QListIterator<SyncServer> syncRecordIterator(settings.SyncerSettings.syncServers); + int i = 0; + + settingsFile.beginWriteArray("Syncer"); + while ( syncRecordIterator.hasNext() ) + { + // next list item + SyncServer syncServer = syncRecordIterator.next(); + if ( syncServer.remoteAddress.size() > 0 ) + { + // write to settings file + settingsFile.setArrayIndex(i++); + + // output format 1\SyncServer=master.333networks.com, 27900, 28900 + QString strServer = QStringLiteral("%1, %2, %3") + .arg(syncServer.remoteAddress, + QString::number(syncServer.beaconPort), + QString::number(syncServer.listenPort) ); + settingsFile.setValue("SyncServer", strServer); + } + + } + settingsFile.endArray(); + + // server checker (udp client ticker) + settingsFile.setValue("Checker/DoCheck", settings.CheckerSettings.doCheck); + settingsFile.setValue("Checker/GetExtendedInformation", settings.CheckerSettings.getExtendedInfo); + settingsFile.setValue("Checker/ServerCheckInterval_ms", settings.CheckerSettings.timeServerInterval_ms); + settingsFile.setValue("Checker/CycleInterval_s", settings.CheckerSettings.timeCheckerReset_s); + + // maintenance settings + settingsFile.setValue("Maintenance/DoMaintenance", settings.MaintenanceSettings.doMaintenance); + settingsFile.setValue("Maintenance/MaintainRate_s", settings.MaintenanceSettings.timeMaintenanceInterval_s); + + // public details + settingsFile.setValue("PublicDetails/Hostname", settings.PublicInformationSettings.hostname); + settingsFile.setValue("PublicDetails/AdminName", settings.PublicInformationSettings.adminName); + settingsFile.setValue("PublicDetails/Contact", settings.PublicInformationSettings.contact); +} diff --git a/src/TcpTasks/ListenClientHandler/compileserverlist.cpp b/src/TcpTasks/ListenClientHandler/compileserverlist.cpp new file mode 100644 index 0000000..5423fbf --- /dev/null +++ b/src/TcpTasks/ListenClientHandler/compileserverlist.cpp @@ -0,0 +1,39 @@ +#include "listenclienthandler.h" + +QByteArray ListenClientHandler::compileServerlist (const QString &gamename, + const int &serverAge_s, + const bool &cmp) +{ + // retrieve servers from database (both direct and sync) + QSqlQuery q = selectServerList( gamename, serverAge_s, true); + + // output array + QByteArray compiledList; + QDataStream dsList(&compiledList,QIODevice::WriteOnly); + + // iterate through resulting queries and add to compile list + while (q.next()) + { + QString ip = q.value(0).toString(); + unsigned short port = q.value(1).value<unsigned short>(); + + // add server to list (compressed/cmp or plaintext) + if (cmp) + { + // QHostAddress.toIPv4Address() provides the correct ABCD format, append EF port bytes + dsList << QHostAddress(ip).toIPv4Address() << port; + } + else // plaintext output + { + // ip in database is plaintext already + compiledList += QStringLiteral("\\ip\\%1:%2").arg(ip, QString::number(port)); + } + + } // while next + + // terminator after list + compiledList.append("\\final\\"); + + // list compiled + return compiledList; +} diff --git a/src/TcpTasks/ListenClientHandler/compilesynclist.cpp b/src/TcpTasks/ListenClientHandler/compilesynclist.cpp new file mode 100644 index 0000000..4d333b3 --- /dev/null +++ b/src/TcpTasks/ListenClientHandler/compilesynclist.cpp @@ -0,0 +1,35 @@ +#include "listenclienthandler.h" + +QByteArray ListenClientHandler::compileSyncList(const QStringList &gamenameList, + const int &serverAge_s) +{ + // output list in \\gamename1\\ip:port ip:port\\gamename2\\ip:port ip:port\\final\\ format + QByteArray compiledList; + + // go through list of gamenames + QStringListIterator gamenameListNames(gamenameList); + while ( gamenameListNames.hasNext() ) + { + // retrieve servers from database (only verified, not from other syncs) + QString gamename = gamenameListNames.next(); + QSqlQuery q = selectServerList( gamename, serverAge_s, false); + + // identifier + compiledList += QStringLiteral("\\%1\\").arg(gamename); + + // iterate through resulting queries and add to compile list + // ip-addresses are stored as text and do not need to be converted back and forth + while (q.next()) + { + // add to list -> ip:port + compiledList += QStringLiteral("%1:%2 ").arg(q.value(0).toString(), q.value(1).toString()); + + } // while next + } + + // terminator after list + compiledList.append("\\final\\"); + + // list compiled + return compiledList; +} diff --git a/src/TcpTasks/ListenClientHandler/listenclientdisconnect.cpp b/src/TcpTasks/ListenClientHandler/listenclientdisconnect.cpp new file mode 100644 index 0000000..55e5f9f --- /dev/null +++ b/src/TcpTasks/ListenClientHandler/listenclientdisconnect.cpp @@ -0,0 +1,21 @@ +#include "listenclienthandler.h" + +void ListenClientHandler::disconnect() +{ + _timeOut.stop(); + _tcpSocket->disconnectFromHost(); +} + +void ListenClientHandler::onListenClientDisconnect() +{ + _timeOut.stop(); + _coreObject->Log.logEvent("tcp", QStringLiteral("%1 disconnected").arg(_clientLabel) ); + this->deleteLater(); +} + +void ListenClientHandler::onListenClientTimeOut() +{ + _timeOut.stop(); + _coreObject->Log.logEvent("tcp", QStringLiteral("%1 timed out").arg(_clientLabel) ); + this->disconnect(); +} diff --git a/src/TcpTasks/ListenClientHandler/listenclienthandler.cpp b/src/TcpTasks/ListenClientHandler/listenclienthandler.cpp new file mode 100644 index 0000000..bc0afb4 --- /dev/null +++ b/src/TcpTasks/ListenClientHandler/listenclienthandler.cpp @@ -0,0 +1,28 @@ +#include "listenclienthandler.h" + +ListenClientHandler::ListenClientHandler(const QSharedPointer<CoreObject> &coreObject, + QTcpSocket *tcpSocket) + : _tcpSocket(tcpSocket) +{ + // create local access + this->_coreObject = coreObject; + + // connect read, timeout and disconnect events + connect(tcpSocket, &QTcpSocket::readyRead, this, &ListenClientHandler::onListenClientRead); + connect(tcpSocket, &QTcpSocket::disconnected, this, &ListenClientHandler::onListenClientDisconnect); + connect(&_timeOut, &QTimer::timeout, this, &ListenClientHandler::onListenClientTimeOut); + + // timeout + _timeOut.setInterval( _timeOutTime_ms ); + _timeOut.start(); + + // challenge + _clientLabel = QStringLiteral("%1:%2").arg(tcpSocket->peerAddress().toString(), QString::number(tcpSocket->peerPort())); + _secure = genChallengeString(6, false); + + _tcpSocket->write( QStringLiteral("\\basic\\\\secure\\%1").arg(_secure).toUtf8() ); + _tcpSocket->flush(); + + // log new connections + _coreObject->Log.logEvent("tcp", QStringLiteral("%1 connected").arg(_clientLabel) ); +} diff --git a/src/TcpTasks/ListenClientHandler/listenclienthandler.h b/src/TcpTasks/ListenClientHandler/listenclienthandler.h new file mode 100644 index 0000000..f7c300b --- /dev/null +++ b/src/TcpTasks/ListenClientHandler/listenclienthandler.h @@ -0,0 +1,49 @@ +#ifndef LISTENCLIENTHANDLER_H +#define LISTENCLIENTHANDLER_H + +#include <QTimer> +#include <QTcpSocket> +#include <QHostAddress> + +#include "Core/CoreObject/coreobject.h" +#include "Database/Common/commonactions.h" + +#include "Protocols/GameSpy0/gamespy0.h" +#include "Protocols/GameSpy0/securevalidate.h" + +class ListenClientHandler : public QObject +{ + Q_OBJECT +public: + ListenClientHandler(const QSharedPointer<CoreObject> &coreObject, + QTcpSocket *tcpSocket); + +private: + const int _timeOutTime_ms = 7500; + + QSharedPointer<CoreObject> _coreObject; + QScopedPointer<QTcpSocket> _tcpSocket; + QTimer _timeOut; + QString _rxBuffer = ""; + bool _hasValidated = false; + + QString _clientLabel; + QString _secure; +private: + void disconnect(); + + // database functions + QByteArray compileServerlist (const QString &gamename, + const int &serverAge_s, + const bool &cmp); + QByteArray compileSyncList(const QStringList &gamenameList, + const int &serverAge_s); + +private slots: + void onListenClientRead(); + void onListenClientTimeOut(); + void onListenClientDisconnect(); + +}; + +#endif // LISTENCLIENTHANDLER_H diff --git a/src/TcpTasks/ListenClientHandler/onlistenclientread.cpp b/src/TcpTasks/ListenClientHandler/onlistenclientread.cpp new file mode 100644 index 0000000..83cc822 --- /dev/null +++ b/src/TcpTasks/ListenClientHandler/onlistenclientread.cpp @@ -0,0 +1,185 @@ +#include "listenclienthandler.h" + +void ListenClientHandler::onListenClientRead() +{ + // clear buffer from previous intereactions + if ( _rxBuffer.contains("\\final\\") ) + { + // if the keyword "\\final\\" is already found, a previous interaction was completed + // it is now safe to clear the buffer + _rxBuffer = ""; + } + + // read incoming data + QByteArray receiveBuffer = _tcpSocket->readAll(); + _coreObject->Log.logEvent("tcp", QStringLiteral("%1 sent '%2'") + .arg(_clientLabel, receiveBuffer.data()) ); + + // reset timeout when data is received + _timeOut.start(); + + // add data to buffer and wait for the interaction to be complete + _rxBuffer += receiveBuffer; + if ( ! receiveBuffer.contains("\\final\\") ) + { + // wait for data to be complete + return; + } + + // process data according to \key\value formatting + QMultiHash<QString, QString> receiveData = parseGameSpy0Buffer(_rxBuffer.toLatin1()); + + // part 1: validation challenge. + if ( receiveData.contains("validate") ) + { + // combined queries provide two gamenames: one for authentication and one for list request. + QString gamename = ( receiveData.values("gamename").size() >= 2 + ? receiveData.values("gamename").takeLast() // removes duplicate gamename + : receiveData.value("gamename","") ); + + // sanity check + if ( _coreObject->SupportedGames.contains(gamename) ) + { + // get response + AuthResult authResult = validateGamename(false, // tcp client, not a beacon + gamename, + receiveData.value("validate",""), + _coreObject->SupportedGames.value(gamename).cipher, + _secure, + receiveData.value("enctype", "0").toInt() ); + + // authenticate + if ( authResult.auth ) + { + _hasValidated = true; + _coreObject->Log.logEvent("secure", QStringLiteral("%1 passed validation for %2").arg(_clientLabel, gamename)); + } + else + { + _coreObject->Log.logEvent("secure", QStringLiteral("%1 failed validation for %2").arg(_clientLabel, gamename)); + + // log detailed information on failure + _coreObject->Log.logEvent("secure", QStringLiteral("secure: '%1', gamename: '%2', validate: '%3', expected: '%4'") + .arg(_secure, gamename, receiveData.value("validate", "null"), authResult.validate )); + + // kick client after unsuccessful validation + this->disconnect(); + } + } + else + { + _coreObject->Log.logEvent("secure", QStringLiteral("%1 tried to validate for unknown game %2").arg(_clientLabel, gamename)); + + // log detailed information on failure + _coreObject->Log.logEvent("secure", QStringLiteral("secure: '%1', gamename: '%2', validate: '%3'") + .arg(_secure, gamename, receiveData.value("validate", "NULL") )); + // kick client after unsuccessful validation + this->disconnect(); + } + + } // hasValidated ? + + // part 2: after validation, send serverlist + if ( receiveData.contains("list") ) + { + // list request is only granted after validation + if ( ! _hasValidated ) + { + // kick client after unsuccessful validation + _coreObject->Log.logEvent("secure", QStringLiteral("%1 requested a list for %2 without validating").arg(_clientLabel, receiveData.value("gamename", "")) ); + this->disconnect(); + return; + } + + // get list from db and send it + QByteArray writeBuffer = compileServerlist( + receiveData.value("gamename", ""), + _coreObject->Settings.ListenServerSettings.serverttl_s, + (receiveData.value("list","").compare("cmp", Qt::CaseInsensitive) == 0 )); + _tcpSocket->write(writeBuffer); + _coreObject->Log.logEvent("list", QStringLiteral("%1 received the list for %2").arg(_clientLabel, receiveData.value("gamename", ""))); + + // all done + this->disconnect(); + + } // list + + // part 2b: 333networks synchronisation + if ( receiveData.contains("sync") ) + { + // list request is only granted after validation + if ( ! _hasValidated ) + { + // kick client after unsuccessful validation + _coreObject->Log.logEvent("secure", QStringLiteral("%1 requested to sync for %2 without validating").arg(_clientLabel, receiveData.value("gamename", "")) ); + this->disconnect(); + return; + } + + // do not sync with ourself + QString remoteIdentity = receiveData.value("msid", ""); + if ( _coreObject->masterserverIdentity == remoteIdentity ) + { + // msid match -- skip syncing + _tcpSocket->write("\\final\\"); + _coreObject->Log.logEvent("list", QStringLiteral("skipping sync for self at %1").arg(_clientLabel) ); + this->disconnect(); + return; + } + + // sync request has options "all" or "gamename_1 gamename_2 gamename_n" + QStringList gamenameList; + if ( receiveData.value("sync","").toLower().compare("all") == 0 ) + { + // get list of gamenames from database + gamenameList = getGamenames(_coreObject->Settings.ListenServerSettings.serverttl_s); + } + else + { + // only load gamenames that are in our list of available games + QStringListIterator rawListNames( receiveData.value("sync","").toLower().split(" ") ); + while ( rawListNames.hasNext() ) + { + QString rawGamename = overrideGamename( rawListNames.next() ); + if ( _coreObject->SupportedGames.contains(rawGamename) ) + { + gamenameList.append(rawGamename); + } + } + } + + // get compiled sync list for all requested gamenames + QByteArray writeBuffer = compileSyncList( + gamenameList, + _coreObject->Settings.ListenServerSettings.serverttl_s); + _tcpSocket->write(writeBuffer); + + // log (displays all the gamenames that were synced or empty when no gamenames/servers were available) + _coreObject->Log.logEvent("list", QStringLiteral("%1 received the sync list for %2").arg(_clientLabel, gamenameList.join(",")) ); + + // all done + this->disconnect(); + + } // sync + + // optional: echo + if ( receiveData.contains("echo") ) + { + _coreObject->Log.logEvent("echo", QStringLiteral("%1 echoed %2").arg(_clientLabel, receiveData.value("echo", "")) ); + _tcpSocket->write("\\echo_reply\\" + receiveData.value("echo", "").toLatin1() ); + } + + // else + + // unknown, log as error + if ( ! receiveData.contains("validate") and + ! receiveData.contains("list") and + ! receiveData.contains("sync") and + ! receiveData.contains("echo") ) + { + _coreObject->Log.logEvent("unknown", QStringLiteral("%1 with unknown request %2").arg(_clientLabel, _rxBuffer ) ); + this->disconnect(); + } + + +} diff --git a/src/TcpTasks/ListenServer/listenserver.cpp b/src/TcpTasks/ListenServer/listenserver.cpp new file mode 100644 index 0000000..1269f21 --- /dev/null +++ b/src/TcpTasks/ListenServer/listenserver.cpp @@ -0,0 +1,7 @@ +#include "listenserver.h" + +ListenServer::ListenServer(const QSharedPointer<CoreObject> &coreObject) +{ + // create local access + this->_coreObject = coreObject; +} diff --git a/src/TcpTasks/ListenServer/listenserver.h b/src/TcpTasks/ListenServer/listenserver.h new file mode 100644 index 0000000..bac0d66 --- /dev/null +++ b/src/TcpTasks/ListenServer/listenserver.h @@ -0,0 +1,31 @@ +#ifndef LISTENSERVER_H +#define LISTENSERVER_H + +#include <QTimer> +#include <QTcpServer> +#include <QTcpSocket> + +#include "Core/CoreObject/coreobject.h" +#include "TcpTasks/ListenClientHandler/listenclienthandler.h" + +class ListenServer: public QObject +{ + Q_OBJECT +public: + ListenServer(const QSharedPointer<CoreObject> &coreObject); + bool listen(); + +private: + QSharedPointer<CoreObject> _coreObject; + + // tcp server socket + QTcpServer _tcpServer; + +signals: + +private slots: + void onListenConnection(); + +}; + +#endif // LISTENSERVER_H diff --git a/src/TcpTasks/ListenServer/onlistenconnection.cpp b/src/TcpTasks/ListenServer/onlistenconnection.cpp new file mode 100644 index 0000000..12d830a --- /dev/null +++ b/src/TcpTasks/ListenServer/onlistenconnection.cpp @@ -0,0 +1,7 @@ +#include "listenserver.h" + +void ListenServer::onListenConnection() +{ + // handle client in its own class + new ListenClientHandler(_coreObject, _tcpServer.nextPendingConnection()); +} diff --git a/src/TcpTasks/ListenServer/tcplisten.cpp b/src/TcpTasks/ListenServer/tcplisten.cpp new file mode 100644 index 0000000..309adbc --- /dev/null +++ b/src/TcpTasks/ListenServer/tcplisten.cpp @@ -0,0 +1,25 @@ +#include "listenserver.h" + +bool ListenServer::listen() +{ + // connect socket + connect(&_tcpServer, &QTcpServer::newConnection, this, &ListenServer::onListenConnection); + + // start listening for new TCP connections + bool error = _tcpServer.listen(QHostAddress::Any, _coreObject->Settings.ListenServerSettings.listenPort); + + // error notification: + if ( ! error ) + { + // complete startup + _coreObject->Log.logEvent("fatal", QStringLiteral("error starting tcp Listen server: %1") + .arg(_tcpServer.errorString())); + return false; + } + + // complete startup + _coreObject->Log.logEvent("info", QStringLiteral("start listening for TCP beacons on port %1") + .arg(_coreObject->Settings.ListenServerSettings.listenPort)); + + return true; +} diff --git a/src/TcpTasks/SyncClient/onsyncconnect.cpp b/src/TcpTasks/SyncClient/onsyncconnect.cpp new file mode 100644 index 0000000..3e05864 --- /dev/null +++ b/src/TcpTasks/SyncClient/onsyncconnect.cpp @@ -0,0 +1,10 @@ +#include "syncclient.h" + +void SyncClient::onSyncConnect() +{ + // reset timeout + _timeOut.start(); + + // log + _coreObject->Log.logEvent("tcp", QStringLiteral("connected to %1").arg(_clientLabel) ); +} diff --git a/src/TcpTasks/SyncClient/onsyncdisconnect.cpp b/src/TcpTasks/SyncClient/onsyncdisconnect.cpp new file mode 100644 index 0000000..4bc8eac --- /dev/null +++ b/src/TcpTasks/SyncClient/onsyncdisconnect.cpp @@ -0,0 +1,49 @@ +#include "syncclient.h" + +void SyncClient::onSyncDisconnect() +{ + // remote host closed the connection (typically occurs after receiving the list from remote host) + if (_tcpSocket.error() == QAbstractSocket::RemoteHostClosedError ) + { + _coreObject->Log.logEvent("tcp", QStringLiteral("disconnected from %1").arg(_clientLabel) ); + } + + // timer already stopped or a timeout occurred + if ( ! _timeOut.isActive() or _tcpSocket.error() == QAbstractSocket::SocketTimeoutError) + { + _coreObject->Log.logEvent("warning", QStringLiteral("timeout while attempting to sync with %1 (1)").arg(_clientLabel)); + } + + // an error occured and is reported, excluding... + if ( _tcpSocket.error() != QAbstractSocket::RemoteHostClosedError and // ...regular disconnect caught above + _tcpSocket.error() != QAbstractSocket::SocketTimeoutError and // ...timeout caught above + _tcpSocket.error() != QAbstractSocket::UnknownSocketError ) // ...QTimer timeout does not leave an errorcode (defaults to unknown error) + { + _coreObject->Log.logEvent("warning", QStringLiteral("error while syncing with %1: %2").arg(_clientLabel, _tcpSocket.errorString())); + } + + // stop timer if necessary and delete this client + _timeOut.stop(); + this->deleteLater(); +} + +void SyncClient::onSyncTimeOut() +{ + // if no error was specified while timer expired, there was a connection timeout + if ( _tcpSocket.error() == QAbstractSocket::UnknownSocketError ) + { + _coreObject->Log.logEvent("warning", QStringLiteral("timeout while attempting to sync with %1 (2)").arg(_clientLabel)); + } + + // other errors, like establishing connection/refused + if ( _tcpSocket.error() != QAbstractSocket::UnknownSocketError ) + { + _coreObject->Log.logEvent("warning", QStringLiteral("error while syncing with %1: %2").arg(_clientLabel, _tcpSocket.errorString())); + } + + // stop timer and close socket + _timeOut.stop(); + _coreObject->Log.logEvent("tcp", QStringLiteral("%1 scheduled for deletion").arg(_clientLabel) ); + _tcpSocket.disconnectFromHost(); + this->deleteLater(); +} diff --git a/src/TcpTasks/SyncClient/onsyncread.cpp b/src/TcpTasks/SyncClient/onsyncread.cpp new file mode 100644 index 0000000..c0f0b73 --- /dev/null +++ b/src/TcpTasks/SyncClient/onsyncread.cpp @@ -0,0 +1,111 @@ +#include "syncclient.h" + +void SyncClient::onSyncRead() +{ + // reset timeout after receiving (any) data + _timeOut.start(); + + // read from tcp connection and append to buffer + QByteArray receiveBuffer = _tcpSocket.readAll(); + _rxBuffer.append( receiveBuffer ); + + // prevent large heaps of text -- log only relevant message, not masterserver data + if ( receiveBuffer.length() > 30 ) + { + // log size of data + _coreObject->Log.logEvent("tcp", QStringLiteral("%1 sent %2 characters") + .arg(_clientLabel, QString::number(receiveBuffer.length()) ) ); + } + else + { + // log message + _coreObject->Log.logEvent("tcp", QStringLiteral("%1 sent '%2'") + .arg(_clientLabel, receiveBuffer.data()) ); + } + + // remote masterserver opens with secure challenge + if ( _rxBuffer.contains("secure") ) + { + // parse to hash + QMultiHash<QString, QString> receiveData = parseGameSpy0Buffer(_rxBuffer.toLatin1()); + + // generate response + QStringList response = replyQuery(receiveData); + + // return response + _tcpSocket.write(response.join("").toLatin1()); + + // sync request + QString request = QStringLiteral("\\sync\\%1\\msid\\%2") + .arg(_coreObject->Settings.SyncerSettings.syncGames, + _coreObject->masterserverIdentity); + _tcpSocket.write(request.toLatin1()); + + // all relevant information received. clear buffer for next interaction + _rxBuffer = ""; + return; + } + + if ( _rxBuffer.contains("final") ) + { + // parse to hash: receivedData format is {gamename} => {string of addresses} + QMultiHash<QString, QString> receiveData = parseGameSpy0Buffer(_rxBuffer.toLatin1()); + receiveData.remove("final"); // prevent "final" from registering as gamename + + // count number of addresses for logging + int totalServerCount = 0; + + // use transaction for SQLite + QSqlDatabase::database().transaction(); + + // parse to list of list of <ServerInfo> + QHashIterator<QString,QString> receiveDataIterator(receiveData); + while ( receiveDataIterator.hasNext() ) + { + // {gamename} => {string of addresses} + receiveDataIterator.next(); + QString gamename = receiveDataIterator.key(); + QString addressBufferList = receiveDataIterator.value(); + + // split address list in single addresses + QStringListIterator listIterator( addressBufferList.split(" ", QString::SkipEmptyParts) ); + while ( listIterator.hasNext() ) + { + // address (ip:port) + QString addr = listIterator.next(); + + // older Qt5 masterservers sync in ::ffff:127.0.0.1 format, trim the '::ffff:' + addr.remove("::ffff:"); + + // (address cont.) + QStringList address = addr.split(':'); + unsigned short remotePort = address.takeLast().toUShort(); + QString remoteAddr = address.join(":"); // IPv4 has only 1 element, IPv6 has 4 that need joining with ':' + + // valid address? + if ( ! QHostAddress(remoteAddr).isNull() and remotePort > 0 ) + { + // if it does not exist in the db, insert + if ( ! updateSyncedServer(remoteAddr, remotePort) ) + { + // add + insertServer(remoteAddr, remotePort, gamename, false); + } + } + totalServerCount++; + } // has next address + } // has next game + + // commit SQLite + QSqlDatabase::database().commit(); + + // report in log + _coreObject->Log.logEvent("sync", QStringLiteral("received %1 servers in %2 games from %3") + .arg( QString::number(totalServerCount), + QString::number(receiveData.count()), + _clientLabel) + ); + } // if final + + // else keep appending data until \\final\\ is received +} diff --git a/src/TcpTasks/SyncClient/syncclient.cpp b/src/TcpTasks/SyncClient/syncclient.cpp new file mode 100644 index 0000000..a5a1531 --- /dev/null +++ b/src/TcpTasks/SyncClient/syncclient.cpp @@ -0,0 +1,27 @@ +#include "syncclient.h" + +SyncClient::SyncClient(const QSharedPointer<CoreObject> &coreObject, + const QString &remoteHost, + const unsigned short int &remotePort) +{ + // create local access + this->_coreObject = coreObject; + + // connect events and functions + connect(&_tcpSocket, &QTcpSocket::connected, this, &SyncClient::onSyncConnect); + connect(&_tcpSocket, &QTcpSocket::readyRead, this, &SyncClient::onSyncRead); + connect(&_tcpSocket, &QTcpSocket::disconnected, this, &SyncClient::onSyncDisconnect); + connect(&_timeOut, &QTimer::timeout, this, &SyncClient::onSyncTimeOut); + + // convenience label to found + _clientLabel = QStringLiteral("%1:%2") + .arg(remoteHost, QString::number(remotePort)); + + // set timeout + _timeOut.setInterval( _timeOutTime_ms ); + _timeOut.start(); // connection timout time === read timeout time + + // connect to remote masterserver + _tcpSocket.connectToHost(remoteHost, remotePort); + _coreObject->Log.logEvent("tcp", QStringLiteral("connecting to %1").arg(_clientLabel)); +} diff --git a/src/TcpTasks/SyncClient/syncclient.h b/src/TcpTasks/SyncClient/syncclient.h new file mode 100644 index 0000000..8cf253f --- /dev/null +++ b/src/TcpTasks/SyncClient/syncclient.h @@ -0,0 +1,50 @@ +#ifndef SYNCCLIENT_H +#define SYNCCLIENT_H + +#include <QTimer> +#include <QTcpSocket> +#include <QHostAddress> + +#include "Core/CoreObject/coreobject.h" +#include "Database/Common/commonactions.h" +#include "Protocols/GameSpy0/gamespy0.h" +#include "Protocols/GameSpy0/securevalidate.h" + +class SyncClient: public QObject +{ + Q_OBJECT +public: + SyncClient(const QSharedPointer<CoreObject> &coreObject, + const QString &remoteHost, + const unsigned short int &remotePort); + +private: + QSharedPointer<CoreObject> _coreObject; + const int _timeOutTime_ms = 7500; + + // tcp client handles + QTcpSocket _tcpSocket; + QTimer _timeOut; + QString _rxBuffer = ""; + QString _clientLabel; + + // helpers + int _queryId = 0; + + // functions + QStringList replyQuery(const QMultiHash<QString, QString> &query); + +private: // update sync time in database + bool updateSyncedServer(const QString &serverAddress, + const unsigned short &serverPort); + +private slots: + void onSyncConnect(); + void onSyncRead(); + void onSyncDisconnect(); + void onSyncTimeOut(); + +signals: +}; + +#endif // SYNCCLIENT_H diff --git a/src/TcpTasks/SyncClient/syncreplyquery.cpp b/src/TcpTasks/SyncClient/syncreplyquery.cpp new file mode 100644 index 0000000..d36637f --- /dev/null +++ b/src/TcpTasks/SyncClient/syncreplyquery.cpp @@ -0,0 +1,49 @@ +#include "syncclient.h" + +QStringList SyncClient::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 + 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") ) + { + queryResponse.append( + QStringLiteral("\\gamename\\%1" + "\\gamever\\%2" + "\\location\\0" + "\\queryid\\%3.%4") + .arg(TYPE_GAMENAME, + SHORT_VER, + QString::number(_queryId), + QString::number(querySubId++) + ) + ); + } + + + + // end query with final + queryResponse.append("\\final\\"); + + return queryResponse; +} diff --git a/src/TcpTasks/SyncClient/updatesyncedserver.cpp b/src/TcpTasks/SyncClient/updatesyncedserver.cpp new file mode 100644 index 0000000..354c6a5 --- /dev/null +++ b/src/TcpTasks/SyncClient/updatesyncedserver.cpp @@ -0,0 +1,27 @@ +#include "syncclient.h" + +bool SyncClient::updateSyncedServer(const QString &serverAddress, + const unsigned short &serverPort) +{ + // update existing entry, but do not insert. + QSqlQuery q; + QString updateString; + + // update with available values + updateString = "UPDATE serverlist SET " + "dt_sync = :timestamp " + "WHERE ip = :ip " + "AND queryport = :queryport"; + + // bind values and execute + q.prepare(updateString); + q.bindValue(":ip", serverAddress); + q.bindValue(":queryport", serverPort); + q.bindValue(":timestamp", QDateTime::currentSecsSinceEpoch() ); + + if ( ! q.exec() ) + return reportQuery(q); + + // was a row updated? + return (q.numRowsAffected() > 0); +} diff --git a/src/TcpTasks/Updater/onsynctickeraction.cpp b/src/TcpTasks/Updater/onsynctickeraction.cpp new file mode 100644 index 0000000..53214ce --- /dev/null +++ b/src/TcpTasks/Updater/onsynctickeraction.cpp @@ -0,0 +1,20 @@ +#include "syncupdater.h" + +void SyncUpdater::onSyncTickerAction() +{ + // exists? + if ( _syncIndex < _coreObject->Settings.SyncerSettings.syncServers.length() ) + { + // retrieve sync settings from config + SyncServer syncServ = _coreObject->Settings.SyncerSettings.syncServers.at(_syncIndex); + _syncIndex++; + + // execute sync with remote masterserver + new SyncClient(_coreObject, syncServ.remoteAddress, syncServ.listenPort); + } + else + { + // last entry. turn off timer + _syncTicker.stop(); + } +} diff --git a/src/TcpTasks/Updater/scheduleupdater.cpp b/src/TcpTasks/Updater/scheduleupdater.cpp new file mode 100644 index 0000000..7a96d6e --- /dev/null +++ b/src/TcpTasks/Updater/scheduleupdater.cpp @@ -0,0 +1,29 @@ +#include "syncupdater.h" + +bool SyncUpdater::scheduleUpdater() +{ + // schedule sync X seconds apart (prevent network spikes) + connect(&_syncTicker, &QTimer::timeout, this, &SyncUpdater::onSyncTickerAction); + _syncTicker.setInterval( _graceTime_ms ); + + // set update timer + connect(&_updaterTimer, &QTimer::timeout, [this] + { + // reset and start ticker + _syncIndex = 0; + _syncTicker.start(); + }); + + _updaterTimer.setInterval( _coreObject->Settings.SyncerSettings.syncInterval_s * 1000); + _updaterTimer.start(); + + // complete startup + _coreObject->Log.logEvent("info", QStringLiteral("sync with other masterservers every %1 seconds") + .arg(_coreObject->Settings.SyncerSettings.syncInterval_s)); + + // run immediately at startup + _syncIndex = 0; + _syncTicker.start(); + + return true; +} diff --git a/src/TcpTasks/Updater/syncupdater.cpp b/src/TcpTasks/Updater/syncupdater.cpp new file mode 100644 index 0000000..da52f12 --- /dev/null +++ b/src/TcpTasks/Updater/syncupdater.cpp @@ -0,0 +1,7 @@ +#include "syncupdater.h" + +SyncUpdater::SyncUpdater(const QSharedPointer<CoreObject> &coreObject) +{ + // create local access + this->_coreObject = coreObject; +} diff --git a/src/TcpTasks/Updater/syncupdater.h b/src/TcpTasks/Updater/syncupdater.h new file mode 100644 index 0000000..9bd2299 --- /dev/null +++ b/src/TcpTasks/Updater/syncupdater.h @@ -0,0 +1,33 @@ +#ifndef SYNCUPDATER_H +#define SYNCUPDATER_H + +#include <QTimer> +#include <QHostInfo> + +#include "Core/CoreObject/coreobject.h" +#include "Database/Common/commonactions.h" +#include "TcpTasks/SyncClient/syncclient.h" + +class SyncUpdater: public QObject +{ + Q_OBJECT +public: + SyncUpdater(const QSharedPointer<CoreObject> &coreObject); + bool scheduleUpdater(); + +private: + QSharedPointer<CoreObject> _coreObject; + const int _graceTime_ms = 5000; + + // update/ticker timer + QTimer _updaterTimer; + QTimer _syncTicker; + + // index + int _syncIndex = 0; + +private slots: + void onSyncTickerAction(); +}; + +#endif // SYNCUPDATER_H 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 diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..10eddb7 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,12 @@ +#include <QCoreApplication> +#include <Core/core.h> + +int main(int argc, char *argv[]) +{ + QCoreApplication QApplication(argc, argv); + + // run the masterserver + Core MasterServerApplication( QApplication.applicationDirPath() ); + MasterServerApplication.run(); + return QApplication.exec(); +} |
