diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 4fdadba6f..e05b0fafc 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -918,7 +918,9 @@ SET(LAUNCHER_SOURCES ui/pages/instance/ServersPage.h ui/pages/instance/WorldListPage.cpp ui/pages/instance/WorldListPage.h + ui/pages/instance/McClient.cpp ui/pages/instance/McClient.hpp + ui/pages/instance/McResolver.cpp ui/pages/instance/McResolver.hpp # GUI - global settings pages diff --git a/launcher/ui/pages/instance/McClient.cpp b/launcher/ui/pages/instance/McClient.cpp new file mode 100644 index 000000000..f49d4c8ad --- /dev/null +++ b/launcher/ui/pages/instance/McClient.cpp @@ -0,0 +1,156 @@ +#include +#include +#include +#include + +#include +#include "McClient.hpp" + +#define SEGMENT_BITS 0x7F +#define CONTINUE_BIT 0x80 + +McClient::McClient(QObject *parent, QString domain, QString ip, short port): QObject(parent), domain(domain), ip(ip), port(port) {} + +QJsonObject McClient::getStatusData() { + qDebug() << "Connecting to socket.."; + socket.connectToHost(ip, port); + + if (!socket.waitForConnected(3000)) { + throw Exception("Failed to connect to socket"); + } + qDebug() << "Connected to socket successfully"; + sendRequest(); + + if (!socket.waitForReadyRead(3000)) { + throw Exception("Socket didn't send anything to read"); + } + return readResponse(); +} + +int McClient::getOnlinePlayers() { + auto status = getStatusData(); + return status.value("players").toObject().value("online").toInt(); +} + +void McClient::sendRequest() { + QByteArray data; + writeVarInt(data, 0x00); // packet ID + writeVarInt(data, 0x760); // protocol version + writeVarInt(data, domain.size()); // server address length + writeString(data, domain.toStdString()); // server address + writeFixedInt(data, port, 2); // server port + writeVarInt(data, 0x01); // next state + writePacketToSocket(data); // send handshake packet + + data.clear(); + + writeVarInt(data, 0x00); // packet ID + writePacketToSocket(data); // send status packet +} + +void McClient::readBytesExactFromSocket(QByteArray &resp, int bytesToRead) { + while (bytesToRead > 0) { + qDebug() << bytesToRead << " bytes left to read"; + if (!socket.waitForReadyRead()) { + throw Exception("Read timeout or error"); + } + + QByteArray chunk = socket.read(qMin(bytesToRead, socket.bytesAvailable())); + resp.append(chunk); + bytesToRead -= chunk.size(); + } +} + +QJsonObject McClient::readResponse() { + auto resp = socket.readAll(); + int length = readVarInt(resp); + + // finish ready response + readBytesExactFromSocket(resp, length-resp.size()); + + if (length != resp.size()) { + qDebug() << "Warning: Packet length doesn't match actual packet size (" << length << " expected vs " << resp.size() << " received)"; + } + qDebug() << "Received response successfully"; + + int packetID = readVarInt(resp); + if (packetID != 0x00) { + throw Exception( + QString("Packet ID doesn't match expected value (0x00 vs 0x%1)") + .arg(packetID, 0, 16) + ); + } + + Q_UNUSED(readVarInt(resp)); // json length + + // 'resp' should now be the JSON string + QJsonDocument doc = QJsonDocument::fromJson(resp); + return doc.object(); +} + +// From https://wiki.vg/Protocol#VarInt_and_VarLong +void McClient::writeVarInt(QByteArray &data, int value) { + while (true) { + if ((value & ~SEGMENT_BITS) == 0) { + data.append(value); + return; + } + + data.append((value & SEGMENT_BITS) | CONTINUE_BIT); + + // Note: >>> means that the sign bit is shifted with the rest of the number rather than being left alone + value >>= 7; + } +} + +// From https://wiki.vg/Protocol#VarInt_and_VarLong +int McClient::readVarInt(QByteArray &data) { + int value = 0; + int position = 0; + char currentByte; + + while (true) { + currentByte = readByte(data); + value |= (currentByte & SEGMENT_BITS) << position; + + if ((currentByte & CONTINUE_BIT) == 0) break; + + position += 7; + + if (position >= 32) throw Exception("VarInt is too big"); + } + + return value; +} + +char McClient::readByte(QByteArray &data) { + if (data.isEmpty()) { + throw Exception("No more bytes to read"); + } + + char byte = data.at(0); + data.remove(0, 1); + return byte; +} + +// write number with specified size in big endian format +void McClient::writeFixedInt(QByteArray &data, int value, int size) { + for (int i = size - 1; i >= 0; i--) { + data.append((value >> (i * 8)) & 0xFF); + } +} + +void McClient::writeString(QByteArray &data, const std::string &value) { + data.append(value.c_str()); +} + +void McClient::writePacketToSocket(QByteArray &data) { + // we prefix the packet with its length + QByteArray dataWithSize; + writeVarInt(dataWithSize, data.size()); + dataWithSize.append(data); + + // write it to the socket + socket.write(dataWithSize); + socket.flush(); +} diff --git a/launcher/ui/pages/instance/McClient.hpp b/launcher/ui/pages/instance/McClient.hpp index afbb64739..e3da1fbcf 100644 --- a/launcher/ui/pages/instance/McClient.hpp +++ b/launcher/ui/pages/instance/McClient.hpp @@ -18,150 +18,19 @@ class McClient : public QObject { QTcpSocket socket; public: - explicit McClient(QObject *parent, QString domain, QString ip, short port): QObject(parent), domain(domain), ip(ip), port(port) {} - - QJsonObject getStatusData() { - qDebug() << "Connecting to socket.."; - socket.connectToHost(ip, port); - - if (!socket.waitForConnected(3000)) { - throw Exception("Failed to connect to socket"); - } - qDebug() << "Connected to socket successfully"; - sendRequest(); - - if (!socket.waitForReadyRead(3000)) { - throw Exception("Socket didn't send anything to read"); - } - return readResponse(); - } - - int getOnlinePlayers() { - auto status = getStatusData(); - return status.value("players").toObject().value("online").toInt(); - } - - void sendRequest() { - QByteArray data; - writeVarInt(data, 0x00); // packet ID - writeVarInt(data, 0x760); // protocol version - writeVarInt(data, domain.size()); // server address length - writeString(data, domain.toStdString()); // server address - writeFixedInt(data, port, 2); // server port - writeVarInt(data, 0x01); // next state - writePacketToSocket(data); // send handshake packet - - data.clear(); - - writeVarInt(data, 0x00); // packet ID - writePacketToSocket(data); // send status packet - } - - void readBytesExactFromSocket(QByteArray &resp, int bytesToRead) { - while (bytesToRead > 0) { - qDebug() << bytesToRead << " bytes left to read"; - if (!socket.waitForReadyRead()) { - throw Exception("Read timeout or error"); - } - - QByteArray chunk = socket.read(qMin(bytesToRead, socket.bytesAvailable())); - resp.append(chunk); - bytesToRead -= chunk.size(); - } - } - - QJsonObject readResponse() { - auto resp = socket.readAll(); - int length = readVarInt(resp); - - // finish ready response - readBytesExactFromSocket(resp, length-resp.size()); - - if (length != resp.size()) { - qDebug() << "Warning: Packet length doesn't match actual packet size (" << length << " expected vs " << resp.size() << " received)"; - } - qDebug() << "Received response successfully"; - - int packetID = readVarInt(resp); - if (packetID != 0x00) { - throw Exception( - QString("Packet ID doesn't match expected value (0x00 vs 0x%1)") - .arg(packetID, 0, 16) - ); - } - - Q_UNUSED(readVarInt(resp)); // json length - - // 'resp' should now be the JSON string - QJsonDocument doc = QJsonDocument::fromJson(resp); - return doc.object(); - } - + explicit McClient(QObject *parent, QString domain, QString ip, short port); + QJsonObject getStatusData(); + int getOnlinePlayers(); + void sendRequest(); + void readBytesExactFromSocket(QByteArray &resp, int bytesToRead); + QJsonObject readResponse(); private: - // From https://wiki.vg/Protocol#VarInt_and_VarLong - void writeVarInt(QByteArray &data, int value) { - while (true) { - if ((value & ~SEGMENT_BITS) == 0) { - data.append(value); - return; - } - - data.append((value & SEGMENT_BITS) | CONTINUE_BIT); - - // Note: >>> means that the sign bit is shifted with the rest of the number rather than being left alone - value >>= 7; - } - } - - // From https://wiki.vg/Protocol#VarInt_and_VarLong - int readVarInt(QByteArray &data) { - int value = 0; - int position = 0; - char currentByte; - - while (true) { - currentByte = readByte(data); - value |= (currentByte & SEGMENT_BITS) << position; - - if ((currentByte & CONTINUE_BIT) == 0) break; - - position += 7; - - if (position >= 32) throw Exception("VarInt is too big"); - } - - return value; - } - - char readByte(QByteArray &data) { - if (data.isEmpty()) { - throw Exception("No more bytes to read"); - } - - char byte = data.at(0); - data.remove(0, 1); - return byte; - } - + void writeVarInt(QByteArray &data, int value); + int readVarInt(QByteArray &data); + char readByte(QByteArray &data); // write number with specified size in big endian format - void writeFixedInt(QByteArray &data, int value, int size) { - for (int i = size - 1; i >= 0; i--) { - data.append((value >> (i * 8)) & 0xFF); - } - } + void writeFixedInt(QByteArray &data, int value, int size); + void writeString(QByteArray &data, const std::string &value); - void writeString(QByteArray &data, const std::string &value) { - data.append(value.c_str()); - } - - void writePacketToSocket(QByteArray &data) { - // we prefix the packet with its length - QByteArray dataWithSize; - writeVarInt(dataWithSize, data.size()); - dataWithSize.append(data); - - // write it to the socket - socket.write(dataWithSize); - socket.flush(); - } + void writePacketToSocket(QByteArray &data); }; diff --git a/launcher/ui/pages/instance/McResolver.cpp b/launcher/ui/pages/instance/McResolver.cpp new file mode 100644 index 000000000..e7103e211 --- /dev/null +++ b/launcher/ui/pages/instance/McResolver.cpp @@ -0,0 +1,72 @@ +#include +#include +#include +#include + +#include "McResolver.hpp" + +MCResolver::MCResolver(QObject *parent, QString domain, int port): QObject(parent), constrDomain(domain), constrPort(port) {} + +void MCResolver::ping() { + pingWithDomainSRV(constrDomain, constrPort); +} + +void MCResolver::pingWithDomainSRV(QString domain, int port) { + QDnsLookup *lookup = new QDnsLookup(this); + lookup->setName(QString("_minecraft._tcp.%1").arg(domain)); + lookup->setType(QDnsLookup::SRV); + + connect(lookup, &QDnsLookup::finished, this, [&, domain, port]() { + QDnsLookup *lookup = qobject_cast(sender()); + + lookup->deleteLater(); + + if (lookup->error() != QDnsLookup::NoError) { + qDebug() << QString("Warning: SRV record lookup failed (%1), trying A record lookup").arg(lookup->errorString()); + pingWithDomainA(domain, port); + return; + } + + auto records = lookup->serviceRecords(); + if (records.isEmpty()) { + qDebug() << "Warning: no SRV entries found for domain, trying A record lookup"; + pingWithDomainA(domain, port); + return; + } + + + const auto& firstRecord = records.at(0); + QString domain = firstRecord.target(); + int port = firstRecord.port(); + pingWithDomainA(domain, port); + }); + + lookup->lookup(); +} + +void MCResolver::pingWithDomainA(QString domain, int port) { + QHostInfo::lookupHost(domain, this, [&, port](const QHostInfo &hostInfo){ + if (hostInfo.error() != QHostInfo::NoError) { + emitFail("A record lookup failed"); + return; + } else { + auto records = hostInfo.addresses(); + if (records.isEmpty()) { + emitFail("No A entries found for domain"); + return; + } + + const auto& firstRecord = records.at(0); + emitSucceed(firstRecord.toString(), port); + } + }); +} + +void MCResolver::emitFail(std::string error) { + qDebug() << "Ping error:" << QString::fromStdString(error); + emit fail(); +} + +void MCResolver::emitSucceed(QString ip, int port) { + emit succeed(ip, port); +} diff --git a/launcher/ui/pages/instance/McResolver.hpp b/launcher/ui/pages/instance/McResolver.hpp index 4bb767200..e7800d539 100644 --- a/launcher/ui/pages/instance/McResolver.hpp +++ b/launcher/ui/pages/instance/McResolver.hpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -11,73 +12,14 @@ class MCResolver : public QObject { int constrPort; public: - explicit MCResolver(QObject *parent, QString domain, int port): QObject(parent), constrDomain(domain), constrPort(port) {} - - void ping() { - pingWithDomainSRV(constrDomain, constrPort); - } + explicit MCResolver(QObject *parent, QString domain, int port); + void ping(); private: - - void pingWithDomainSRV(QString domain, int port) { - QDnsLookup *lookup = new QDnsLookup(this); - lookup->setName(QString("_minecraft._tcp.%1").arg(domain)); - lookup->setType(QDnsLookup::SRV); - - connect(lookup, &QDnsLookup::finished, this, [&, domain, port]() { - QDnsLookup *lookup = qobject_cast(sender()); - - lookup->deleteLater(); - - if (lookup->error() != QDnsLookup::NoError) { - qDebug() << QString("Warning: SRV record lookup failed (%1), trying A record lookup").arg(lookup->errorString()); - pingWithDomainA(domain, port); - return; - } - - auto records = lookup->serviceRecords(); - if (records.isEmpty()) { - qDebug() << "Warning: no SRV entries found for domain, trying A record lookup"; - pingWithDomainA(domain, port); - return; - } - - - const auto& firstRecord = records.at(0); - QString domain = firstRecord.target(); - int port = firstRecord.port(); - pingWithDomainA(domain, port); - }); - - lookup->lookup(); - } - - void pingWithDomainA(QString domain, int port) { - QHostInfo::lookupHost(domain, this, [&, port](const QHostInfo &hostInfo){ - if (hostInfo.error() != QHostInfo::NoError) { - emitFail("A record lookup failed"); - return; - } else { - auto records = hostInfo.addresses(); - if (records.isEmpty()) { - emitFail("No A entries found for domain"); - return; - } - - const auto& firstRecord = records.at(0); - emitSucceed(firstRecord.toString(), port); - } - }); - } - - void emitFail(std::string error) { - qDebug() << "Ping error:" << QString::fromStdString(error); - emit fail(); - } - - void emitSucceed(QString ip, int port) { - emit succeed(ip, port); - } + void pingWithDomainSRV(QString domain, int port); + void pingWithDomainA(QString domain, int port); + void emitFail(std::string error); + void emitSucceed(QString ip, int port); signals: void succeed(QString ip, int port);