First commit

This commit is contained in:
MultiMote 2017-08-15 20:55:25 +03:00
commit 1dee5e84ae
9 changed files with 609 additions and 0 deletions

2
.gitignore vendored Normal file

@ -0,0 +1,2 @@
bin
QBERcon.pro.user

19
QBERcon.pro Normal file

@ -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

29
README.md Normal file

@ -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.

85
example/examplegui.cpp Normal file

@ -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)));
}

38
example/examplegui.h Normal file

@ -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

99
example/examplegui.ui Normal file

@ -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>

12
example/main.cpp Normal file

@ -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();
}

253
src/QBERcon.cpp Normal file

@ -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;
}

72
src/QBERcon.h Normal file

@ -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