commit 1dee5e84aebd365a7e5a2ae8874bb4d209c26dfa Author: MultiMote <sporeon@yandex.ua> Date: Tue Aug 15 20:55:25 2017 +0300 First commit 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