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