From 1dee5e84aebd365a7e5a2ae8874bb4d209c26dfa Mon Sep 17 00:00:00 2001
From: MultiMote <sporeon@yandex.ua>
Date: Tue, 15 Aug 2017 20:55:25 +0300
Subject: [PATCH] First commit

---
 .gitignore             |   2 +
 QBERcon.pro            |  19 ++++
 README.md              |  29 +++++
 example/examplegui.cpp |  85 ++++++++++++++
 example/examplegui.h   |  38 +++++++
 example/examplegui.ui  |  99 ++++++++++++++++
 example/main.cpp       |  12 ++
 src/QBERcon.cpp        | 253 +++++++++++++++++++++++++++++++++++++++++
 src/QBERcon.h          |  72 ++++++++++++
 9 files changed, 609 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 QBERcon.pro
 create mode 100644 README.md
 create mode 100644 example/examplegui.cpp
 create mode 100644 example/examplegui.h
 create mode 100644 example/examplegui.ui
 create mode 100644 example/main.cpp
 create mode 100644 src/QBERcon.cpp
 create mode 100644 src/QBERcon.h

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ece90c6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+bin
+QBERcon.pro.user
\ No newline at end of file
diff --git a/QBERcon.pro b/QBERcon.pro
new file mode 100644
index 0000000..4ea2040
--- /dev/null
+++ b/QBERcon.pro
@@ -0,0 +1,19 @@
+QT += core widgets network
+
+TARGET = QBERcon
+
+TEMPLATE = app
+
+INCLUDEPATH += src example
+
+SOURCES += \
+    example/main.cpp \
+    example/examplegui.cpp \
+    src/QBERcon.cpp
+
+HEADERS += \
+    example/examplegui.h \
+    src/QBERcon.h
+
+FORMS += \
+    example/examplegui.ui
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..678d0b6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,29 @@
+QBERcon - BattlEye Rcon connector for Qt5 C++
+=============================================
+
+## Public functions:
+```c++
+    void connectToServer(QString password, QString hostname, quint16 port = 2302);
+    void disconnectFromServer();
+    bool isConnected() const;
+    void setKeepAliveInterval(int value);   // In milliseconds. Default interval is 5 seconds
+    quint8 sendCommand(QString cmd);        // Send command and return command sequence number    
+```
+
+## Signals:
+```c++
+    void messageReceived(QString &message); // Emitted when server broadcasts message
+    void commandReceived(QString message, quint8 seqNumber); // Emitted when server replies command with sequence number
+    void connected();                       // Emitted after successful login
+    void disconnected();                    // Emitted after disconnect, timeout, etc.
+    void error(QBERcon::RconError err);     // Emitted when error thrown
+```
+
+## Errors:
+```c++
+    QBERcon::ERROR_LOGIN_FAILED             // Wrong password
+    QBERcon::ERROR_KEEPALIVE_EXCEEDED       // Timeout
+    QBERcon::ERROR_MISSING_LOGIN_DATA       // No login/password specified
+```
+
+Not fully tested. Use at your own risk. 
\ No newline at end of file
diff --git a/example/examplegui.cpp b/example/examplegui.cpp
new file mode 100644
index 0000000..e61c58b
--- /dev/null
+++ b/example/examplegui.cpp
@@ -0,0 +1,85 @@
+#include "examplegui.h"
+#include "ui_examplegui.h"
+
+#define Q(x) #x
+#define QUOTE(x) Q(x)
+
+ExampleGui::ExampleGui(QWidget *parent) :
+    QWidget(parent),
+    ui(new Ui::ExampleGui) {
+    ui->setupUi(this);
+    be = new QBERcon::Client(this);
+
+
+#ifdef PASSWORD
+    ui->passwordInput->setText(QUOTE(PASSWORD));
+#endif
+#ifdef SERVER_IP
+    ui->hostInput->setText(QUOTE(SERVER_IP));
+#endif
+#ifdef SERVER_PORT
+    ui->portInput->setValue(SERVER_PORT);
+#endif
+
+
+    connect(be, SIGNAL(connected()), this, SLOT(connected()));
+    connect(be, SIGNAL(disconnected()), this, SLOT(disconnected()));
+    connect(be, SIGNAL(error(QBERcon::RconError)), this, SLOT(error(QBERcon::RconError)));
+    connect(be, SIGNAL(messageReceived(QString&)), this, SLOT(messageReceived(QString&)));
+    connect(be, SIGNAL(commandReceived(QString, quint8)), this, SLOT(commandReceived(QString, quint8)));
+}
+
+ExampleGui::~ExampleGui() {
+    delete ui;
+}
+
+
+void ExampleGui::messageReceived(QString &message) {
+    ui->logOutput->append(QString("<font color=\"#67983E\">[MSG]: %1</font>")
+                              .arg(message));
+    qDebug() << __PRETTY_FUNCTION__ << message;
+}
+
+void ExampleGui::commandReceived(QString message, quint8 seqNumber) {
+    ui->logOutput->append(QString("<font color=\"#E19B3E\">[CMD ID=%1]: %2</font>")
+                              .arg(QString::number(seqNumber), message));
+    qDebug() << __PRETTY_FUNCTION__ << "number =" << seqNumber << "Data:" << message;
+}
+
+void ExampleGui::connected() {
+    ui->logOutput->append(QString("<font color=\"#42B555\">[CONNECTED]</font>"));
+    qDebug() << __PRETTY_FUNCTION__;
+}
+
+void ExampleGui::disconnected() {
+    ui->logOutput->append(QString("<font color=\"#D82672\">[DISCONNECTED]</font>"));
+    qDebug() << __PRETTY_FUNCTION__;
+    ui->connectButton->setEnabled(true);
+    ui->disconnectButton->setEnabled(false);
+}
+
+void ExampleGui::error(QBERcon::RconError err) {
+    ui->logOutput->append(QString("<font color=\"#8E1717\">[ERR]: %1</font>")
+                              .arg(QString::number(err)));
+    qDebug() << __PRETTY_FUNCTION__ << err;
+}
+
+void ExampleGui::on_connectButton_clicked() {
+    be->connectToServer(ui->passwordInput->text(), ui->hostInput->text(), ui->portInput->value());
+    ui->connectButton->setEnabled(false);
+    ui->disconnectButton->setEnabled(true);
+    ui->commandButton->setEnabled(true);
+}
+
+void ExampleGui::on_disconnectButton_clicked() {
+    be->disconnectFromServer();
+    ui->connectButton->setEnabled(true);
+    ui->disconnectButton->setEnabled(false);
+}
+
+void ExampleGui::on_commandButton_clicked() {
+    QString command = ui->commandInput->text();
+    quint8 number = be->sendCommand(command);
+    ui->logOutput->append(QString("<font color=\"#5D5815\">[CMD]: Sent %1 with ID=%2</font>")
+                              .arg(command, QString::number(number)));
+}
diff --git a/example/examplegui.h b/example/examplegui.h
new file mode 100644
index 0000000..2a7af31
--- /dev/null
+++ b/example/examplegui.h
@@ -0,0 +1,38 @@
+#ifndef EXAMPLEGUI_H
+#define EXAMPLEGUI_H
+
+#include "QBERcon.h"
+
+#include <QWidget>
+
+namespace Ui {
+class ExampleGui;
+}
+
+class ExampleGui : public QWidget
+{
+    Q_OBJECT
+
+public:
+    explicit ExampleGui(QWidget *parent = 0);
+    ~ExampleGui();
+
+
+public slots:
+    void messageReceived(QString &message);
+    void commandReceived(QString message, quint8 seqNumber);
+    void connected();
+    void disconnected();
+    void error(QBERcon::RconError err);
+
+private slots:
+    void on_connectButton_clicked();
+    void on_disconnectButton_clicked();
+    void on_commandButton_clicked();
+
+private:
+    QBERcon::Client *be;
+    Ui::ExampleGui *ui;
+};
+
+#endif // EXAMPLEGUI_H
diff --git a/example/examplegui.ui b/example/examplegui.ui
new file mode 100644
index 0000000..c12a3f6
--- /dev/null
+++ b/example/examplegui.ui
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ExampleGui</class>
+ <widget class="QWidget" name="ExampleGui">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>600</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>RCON Test</string>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="4" column="0" colspan="2">
+    <widget class="QPushButton" name="disconnectButton">
+     <property name="enabled">
+      <bool>false</bool>
+     </property>
+     <property name="text">
+      <string>Disconnect</string>
+     </property>
+    </widget>
+   </item>
+   <item row="3" column="0" colspan="2">
+    <widget class="QPushButton" name="connectButton">
+     <property name="text">
+      <string>Connect</string>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="1">
+    <widget class="QSpinBox" name="portInput">
+     <property name="maximum">
+      <number>65535</number>
+     </property>
+     <property name="value">
+      <number>2302</number>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="0">
+    <widget class="QLabel" name="label_2">
+     <property name="text">
+      <string>Port</string>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="0">
+    <widget class="QLabel" name="label_3">
+     <property name="text">
+      <string>Password</string>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="1">
+    <widget class="QLineEdit" name="passwordInput">
+     <property name="echoMode">
+      <enum>QLineEdit::Password</enum>
+     </property>
+    </widget>
+   </item>
+   <item row="7" column="0">
+    <widget class="QPushButton" name="commandButton">
+     <property name="enabled">
+      <bool>false</bool>
+     </property>
+     <property name="text">
+      <string>Command</string>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="0">
+    <widget class="QLabel" name="label">
+     <property name="text">
+      <string>IP/Host</string>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="1">
+    <widget class="QLineEdit" name="hostInput"/>
+   </item>
+   <item row="7" column="1">
+    <widget class="QLineEdit" name="commandInput">
+     <property name="text">
+      <string>players</string>
+     </property>
+    </widget>
+   </item>
+   <item row="6" column="0" colspan="2">
+    <widget class="QTextBrowser" name="logOutput"/>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/example/main.cpp b/example/main.cpp
new file mode 100644
index 0000000..5cfcfb7
--- /dev/null
+++ b/example/main.cpp
@@ -0,0 +1,12 @@
+#include <QApplication>
+#include <QTimer>
+#include <examplegui.h>
+
+
+int main(int argc, char *argv[])
+{
+    QApplication a(argc, argv);
+    ExampleGui *example = new ExampleGui();
+    example->show();
+    return a.exec();
+}
diff --git a/src/QBERcon.cpp b/src/QBERcon.cpp
new file mode 100644
index 0000000..7864142
--- /dev/null
+++ b/src/QBERcon.cpp
@@ -0,0 +1,253 @@
+#include "QBERcon.h"
+#include <QDebug>
+
+QBERcon::Client::Client(QObject *parent) : QObject(parent) {
+    this->port = 2302;
+    commandSequenceNumber = 0;
+    keepAliveInterval = 5000;
+    keepAliveTimer = new QTimer(this);
+    socket = new QUdpSocket(this);
+
+    dns = new QDnsLookup();
+    dns->setType(QDnsLookup::A);
+
+    connect(socket, SIGNAL(connected()), SLOT(socketConnected()));
+    connect(socket, SIGNAL(disconnected()), SLOT(socketDisconnected()));
+    connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), SLOT(socketError(QAbstractSocket::SocketError)));
+    connect(socket, SIGNAL(readyRead()), SLOT(read()));
+    connect(keepAliveTimer, SIGNAL(timeout()), this, SLOT(keepAliveTimerTimeout()));
+    connect(dns, SIGNAL(finished()), this, SLOT(hostLookupFinished()));
+}
+
+QBERcon::Client::~Client() {
+    if(isConnected())
+        disconnectFromServer();
+}
+
+void QBERcon::Client::connectToServer(QString password, QString hostname, quint16 port) {
+    if(hostname.isEmpty() || password.isEmpty()) {
+        emit error(QBERcon::ERROR_MISSING_LOGIN_DATA);
+        return;
+    }
+    this->hostname = hostname;
+    this->port = port;
+    this->password = password;
+
+    dns->setName(hostname);
+    dns->lookup();
+}
+
+void QBERcon::Client::hostLookupFinished() {
+    if (dns->error() != QDnsLookup::NoError) {
+        qDebug() << "DNS Lookup failed" << dns->error() << dns->errorString();
+        return;
+    }
+    if(dns->hostAddressRecords().size() > 0) {
+        host = dns->hostAddressRecords().first().value();
+        qDebug() << "DNS Lookup OK:" << host;
+        socket->connectToHost(host, port, QAbstractSocket::ReadWrite);
+    } else {
+        qDebug() << "DNS Lookup failed, no records";
+    }
+}
+
+void QBERcon::Client::keepAliveTimerTimeout() {
+    if(keepAliveReceived) {
+        keepAliveReceived = false;
+        sendCommand("");
+    } else {
+        emit error(QBERcon::ERROR_KEEPALIVE_EXCEEDED);
+        disconnectFromServer();
+    }
+}
+
+void QBERcon::Client::read() {
+    QByteArray data;
+    data.resize(socket->pendingDatagramSize());
+    socket->readDatagram(data.data(), data.size());
+    handleData(data);
+}
+
+void QBERcon::Client::socketConnected() {
+    commandSequenceNumber = 0;
+    keepAliveReceived = true;
+    keepAliveTimer->start(keepAliveInterval);
+    sendPacket(QBERcon::PACKET_LOGIN);
+}
+
+void QBERcon::Client::disconnectFromServer() {
+    connectedToServer = false;
+    keepAliveTimer->stop();
+    socket->disconnectFromHost();
+}
+
+quint8 QBERcon::Client::sendCommand(QString cmd) {
+    if(!connectedToServer) return 0;
+    sendPacket(QBERcon::PACKET_COMMAND, cmd);
+    return commandSequenceNumber - 1;
+}
+
+void QBERcon::Client::socketDisconnected() {
+    connectedToServer = false;
+    emit disconnected();
+}
+
+void QBERcon::Client::socketError(QAbstractSocket::SocketError err) {
+    qDebug() << "QAbstractSocket::SocketError:" << err;
+    disconnectFromServer();
+}
+
+void QBERcon::Client::addHeaderToPacket(QByteArray &dst) {
+    QByteArray result;
+    quint32 crc = qcrc32(dst);
+    result.append("BE");
+    for(int i = 0; i < 4; ++i) {
+        result.append((quint8)((crc >> (i * 8)) & 0xFF));
+    }
+    result.append(dst);
+    dst = result;
+
+}
+
+void QBERcon::Client::handleData(QByteArray &data) {
+    if(!(data.size() > 7 && data.at(0) == 'B' && data.at(1) == 'E')) {
+        qDebug() << "Not a BE packet, ignoring";
+        return;
+    }
+
+    quint32 crc_msg = 0;
+    for(int i = 0; i < 4; ++i) {
+        quint8 b = data.at(2 + i);
+        crc_msg |= b << (i * 8);
+    }
+
+    data.remove(0, 6); // cut header and keep payload
+
+    quint32 crc_computed = qcrc32(data);
+
+    if(crc_msg != crc_computed) {
+        qDebug() << "Packet CRC missmatch, ignoring";
+        return;
+    }
+
+    quint8 packetType = data.at(1);
+    //qDebug() << "Packet type" << packetType;
+
+    switch (packetType) {
+    case QBERcon::PACKET_LOGIN: {
+        quint8 result = data.at(2);
+        if(result == 0x01) {
+            connectedToServer = true;
+            emit connected();
+        } else {
+            emit error(QBERcon::ERROR_LOGIN_FAILED);
+            disconnectFromServer();
+        }
+        break;
+    }
+    case QBERcon::PACKET_COMMAND: {
+        quint8 seqNumber = data.at(2);
+        if(data.length() < 4) { // ACK
+            keepAliveReceived = true;
+        } else {
+            //  3                   4                                        5
+            // 0x00 | number of packets for this response | 0-based index of the current packet
+            if(data.at(3) == 0x00) { // multipart
+                //qDebug() << "Multipart";
+                quint8 messages_total = data.at(4);
+                quint8 message_current = data.at(5);
+                if(message_current == 0x00) {
+                    multipartMessageData.clear();
+                }
+                if(messages_total > message_current) {
+                    multipartMessageData.append(QString::fromUtf8(data.remove(0, 6)));
+                    if((messages_total - 1) == message_current) {
+                        emit commandReceived(multipartMessageData, seqNumber);
+                        multipartMessageData.clear();
+                    }
+                }
+            } else {
+                QString msg = QString::fromUtf8(data.remove(0, 3));
+                emit commandReceived(msg, seqNumber);
+            }
+        }
+        break;
+    }
+    case QBERcon::PACKET_MESSAGE: {
+        quint8 ret = data.at(2);
+        QString msg = QString::fromUtf8(data.remove(0, 3));
+        emit messageReceived(msg);
+        sendPacket(QBERcon::PACKET_MESSAGE, ret);
+        break;
+    }
+    default:
+        break;
+    }
+}
+
+void QBERcon::Client::sendPacket(QBERcon::PacketType type, QVariant data) {
+    QByteArray p;
+    p.append(0xFF);
+    p.append(type);
+    //qDebug() << "Sending packet type" << type;
+    switch (type) {
+    case QBERcon::PACKET_LOGIN:
+        p.append(password);
+        break;
+    case QBERcon::PACKET_MESSAGE:
+        p.append(data.toChar());
+        break;
+    case QBERcon::PACKET_COMMAND:
+        p.append(commandSequenceNumber++);
+        p.append(data.toByteArray());
+        break;
+    default:
+        break;
+    }
+    addHeaderToPacket(p);
+    socket->writeDatagram(p, host, port);
+}
+
+bool QBERcon::Client::isConnected() const {
+    return connectedToServer;
+}
+
+void QBERcon::Client::setKeepAliveInterval(int value) {
+    keepAliveInterval = value;
+}
+
+
+// http://www.hackersdelight.org/hdcodetxt/crc.c.txt
+/* This is derived from crc32b but does table lookup. First the table
+itself is calculated, if it has not yet been set up.
+Not counting the table setup (which would probably be a separate
+function), when compiled to Cyclops with GCC, this function executes in
+7 + 13n instructions, where n is the number of bytes in the input
+message. It should be doable in 4 + 9n instructions. In any case, two
+of the 13 or 9 instrucions are load byte.
+   This is Figure 14-7 in the text. */
+quint32 QBERcon::Client::qcrc32(QByteArray data) {
+    int j;
+    quint32 byte, crc, mask;
+    static quint32 table[256];
+    /* Set up the table, if necessary. */
+    if (table[1] == 0) {
+        for (byte = 0; byte <= 255; byte++) {
+            crc = byte;
+            for (j = 7; j >= 0; j--) {    // Do eight times.
+                mask = -(crc & 1);
+                crc = (crc >> 1) ^ (0xEDB88320 & mask);
+            }
+            table[byte] = crc;
+        }
+    }
+    /* Through with table setup, now calculate the CRC. */
+    crc = 0xFFFFFFFF;
+    QByteArray::Iterator it = data.begin();
+    while (it != data.end()) {
+        byte = *it;
+        crc = (crc >> 8) ^ table[(crc ^ byte) & 0xFF];
+        ++it;
+    }
+    return ~crc;
+}
diff --git a/src/QBERcon.h b/src/QBERcon.h
new file mode 100644
index 0000000..d1d7167
--- /dev/null
+++ b/src/QBERcon.h
@@ -0,0 +1,72 @@
+#ifndef QTBERCON_H
+#define QTBERCON_H
+
+#include <QObject>
+#include <QTimer>
+#include <QUdpSocket>
+#include <QDnsLookup> // QHostInfo::lookupHost slow for me some reason, so QT5
+
+namespace QBERcon {
+enum PacketType {
+    PACKET_LOGIN = 0x00,
+    PACKET_COMMAND = 0x01,
+    PACKET_MESSAGE = 0x02,
+};
+
+enum RconError {
+    ERROR_NONE = 0,
+    ERROR_LOGIN_FAILED,
+    ERROR_KEEPALIVE_EXCEEDED,
+    ERROR_MISSING_LOGIN_DATA,
+};
+
+
+class Client : public QObject {
+    Q_OBJECT
+public:
+    explicit Client(QObject *parent = 0);
+    ~Client();
+    void connectToServer(QString password, QString hostname, quint16 port = 2302);
+    void disconnectFromServer();
+    quint8 sendCommand(QString cmd);
+    bool isConnected() const;
+    void setKeepAliveInterval(int value);
+
+signals:
+    void messageReceived(QString &message);
+    void commandReceived(QString message, quint8 seqNumber);
+    void connected();
+    void disconnected();
+    void error(QBERcon::RconError err);
+
+public slots:    
+    void keepAliveTimerTimeout();
+    void read();
+    void socketConnected();
+    void socketDisconnected();
+    void socketError(QAbstractSocket::SocketError err);
+    void hostLookupFinished();
+private:
+    void addHeaderToPacket(QByteArray &dst);
+    void handleData(QByteArray &data);
+    void sendPacket(QBERcon::PacketType type, QVariant data = QVariant());
+    quint32 qcrc32(QByteArray data);
+
+    QTimer *keepAliveTimer;
+    QUdpSocket *socket;
+    QDnsLookup *dns;
+
+    QString password;
+    quint16 port;
+    QString hostname;
+    QHostAddress host;
+
+    quint8 commandSequenceNumber;    
+    bool connectedToServer;
+    int keepAliveInterval;
+    bool keepAliveReceived;
+    QString multipartMessageData;
+};
+
+}
+#endif // QTBERCON_H