From 43a54cafef95a3b4c2181f4d3d1e2d3876b8e7f9 Mon Sep 17 00:00:00 2001 From: iTrooz Date: Fri, 15 Nov 2024 19:52:06 +0100 Subject: [PATCH] add my classes --- launcher/ui/pages/instance/McClient.hpp | 169 ++++++++++++++++++++++ launcher/ui/pages/instance/McResolver.hpp | 85 +++++++++++ 2 files changed, 254 insertions(+) create mode 100644 launcher/ui/pages/instance/McClient.hpp create mode 100644 launcher/ui/pages/instance/McResolver.hpp diff --git a/launcher/ui/pages/instance/McClient.hpp b/launcher/ui/pages/instance/McClient.hpp new file mode 100644 index 000000000..611e0c143 --- /dev/null +++ b/launcher/ui/pages/instance/McClient.hpp @@ -0,0 +1,169 @@ +#include +#include +#include +#include + +#define SEGMENT_BITS 0x7F +#define CONTINUE_BIT 0x80 + +// Client for the Minecraft protocol +class McClient : public QObject { + Q_OBJECT + + QString domain; + QString ip; + short port; + 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 std::runtime_error("Failed to connect to socket"); + } + qDebug() << "Connected to socket successfully"; + sendRequest(); + + if (!socket.waitForReadyRead(3000)) { + throw std::runtime_error("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 std::runtime_error("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()) { + printf("Warning: Packet length doesn't match actual packet size (%d expected vs %d received)\n", length, resp.size()); + } + qDebug() << "Received response successfully"; + + int packetID = readVarInt(resp); + if (packetID != 0x00) { + throw std::runtime_error( + QString("Packet ID doesn't match expected value (0x00 vs 0x%1)") + .arg(packetID, 0, 16).toStdString() + ); + } + + int jsonLength = readVarInt(resp); + std::string json = resp.toStdString(); + + QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(json)); + return doc.object(); + } + + void close() { + socket.close(); + } + +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 std::runtime_error("VarInt is too big"); + } + + return value; + } + + char readByte(QByteArray &data) { + if (data.isEmpty()) { + throw std::runtime_error("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 writeFixedInt(QByteArray &data, int value, int size) { + for (int i = size - 1; i >= 0; i--) { + data.append((value >> (i * 8)) & 0xFF); + } + } + + void writeString(QByteArray &data, std::string value) { + data.append(value); + } + + 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(); + } +}; diff --git a/launcher/ui/pages/instance/McResolver.hpp b/launcher/ui/pages/instance/McResolver.hpp new file mode 100644 index 000000000..29968f26b --- /dev/null +++ b/launcher/ui/pages/instance/McResolver.hpp @@ -0,0 +1,85 @@ +#include +#include +#include +#include + +// resolve the IP and port of a Minecraft server +class MCResolver : public QObject { + Q_OBJECT + + std::string constrDomain; + int constrPort; + +public: + explicit MCResolver(QObject *parent, std::string domain, int port): QObject(parent), constrDomain(domain), constrPort(port) {} + + void ping() { + pingWithDomainSRV(QString::fromStdString(constrDomain), constrPort); + } + +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) { + printf("Warning: SRV record lookup failed (%v), trying A record lookup\n", lookup->errorString().toStdString()); + pingWithDomainA(domain, port); + return; + } + + auto records = lookup->serviceRecords(); + if (records.isEmpty()) { + printf("Warning: no SRV entries found for domain, trying A record lookup\n"); + 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) { + printf("Ping error: %s\n", error.c_str()); + emit fail(); + } + + void emitSucceed(QString ip, int port) { + emit succeed(ip, port); + } + +signals: + void succeed(QString ip, int port); + void fail(); +};