#include "authservice.h" #include "localization.h" #include #include #include #include #include #include #include #include #include namespace { const QString kAuthServer = QStringLiteral("https://authserver.ely.by"); const QString kInjectorManifest = QStringLiteral("https://authlib-injector.yushi.moe/artifact/latest.json"); const QString kInjectorFileName = QStringLiteral("authlib-injector.jar"); // Ely.by отдаёт UUID без дефисов, вручную введённый мог прийти и с ними. QString normalizeUuid(const QString &uuid) { return QString(uuid).remove(u'-'); } QString describeElyError(const QJsonObject &response, int status) { const QString message = response.value(QStringLiteral("errorMessage")).toString(); if (!message.isEmpty()) return message; const QString error = response.value(QStringLiteral("error")).toString(); if (!error.isEmpty()) return error; return Loc::text("auth.error.serverCode").arg(status); } } // namespace AuthService::AuthService(QObject *parent) : QObject(parent) , m_network(new QNetworkAccessManager(this)) { } AuthResult AuthService::offline(const QString &nickname) { AuthResult result; if (nickname.isEmpty()) { result.error = Loc::text("auth.offline.error.noNickname"); return result; } QByteArray digest = QCryptographicHash::hash( QStringLiteral("OfflinePlayer:%1").arg(nickname).toUtf8(), QCryptographicHash::Md5); digest[6] = static_cast((digest[6] & 0x0f) | 0x30); // версия UUID = 3 digest[8] = static_cast((digest[8] & 0x3f) | 0x80); // вариант RFC 4122 result.ok = true; result.playerName = nickname; result.uuid = QString::fromLatin1(digest.toHex()); result.accessToken = QStringLiteral("0"); result.clientToken = QStringLiteral("0"); result.userType = QStringLiteral("legacy"); return result; } QString AuthService::generateClientToken() { return QUuid::createUuid().toString(QUuid::WithoutBraces); } void AuthService::postJson( const QString &endpoint, const QJsonObject &body, std::function handler) { QNetworkRequest request{QUrl(kAuthServer + endpoint)}; request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json")); request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); QNetworkReply *reply = m_network->post(request, QJsonDocument(body).toJson(QJsonDocument::Compact)); connect(reply, &QNetworkReply::finished, this, [reply, handler = std::move(handler)]() { reply->deleteLater(); const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); const QByteArray payload = reply->readAll(); // Сетевую ошибку отличаем от ответа сервера: при 401 статус есть, а error — есть всегда. QString transportError; if (status == 0 && reply->error() != QNetworkReply::NoError) transportError = reply->errorString(); handler(status, QJsonDocument::fromJson(payload).object(), transportError); }); } void AuthService::loginElyBy(const QString &login, const QString &password, const QString &clientToken, const QString &accessToken, Callback callback) { if (login.isEmpty()) { AuthResult result; result.error = Loc::text("auth.ely.error.noLogin"); callback(result); return; } const QString token = clientToken.isEmpty() ? generateClientToken() : clientToken; // Токена ещё нет — сразу за паролем. if (accessToken.isEmpty()) { authenticate(login, password, token, std::move(callback)); return; } emit progress(Loc::text("auth.ely.progress.checkingToken")); postJson(QStringLiteral("/auth/validate"), {{QStringLiteral("accessToken"), accessToken}}, [this, login, password, token, accessToken, callback]( int status, const QJsonObject &, const QString &transportError) { if (!transportError.isEmpty()) { AuthResult result; result.error = Loc::text("auth.ely.error.unreachable").arg(transportError); callback(result); return; } if (status == 200) { AuthResult result; result.ok = true; result.accessToken = accessToken; result.clientToken = token; result.userType = QStringLiteral("ELYBY"); callback(result); return; } // Токен протух — пробуем продлить его, не спрашивая пароль. emit progress(Loc::text("auth.ely.progress.refreshingToken")); postJson(QStringLiteral("/auth/refresh"), {{QStringLiteral("accessToken"), accessToken}, {QStringLiteral("clientToken"), token}, {QStringLiteral("requestUser"), true}}, [this, login, password, token, callback]( int refreshStatus, const QJsonObject &response, const QString &error) { if (error.isEmpty() && refreshStatus == 200) { AuthResult result; result.ok = true; result.accessToken = response.value(QStringLiteral("accessToken")).toString(); result.clientToken = token; const QJsonObject profile = response.value(QStringLiteral("selectedProfile")).toObject(); result.playerName = profile.value(QStringLiteral("name")).toString(); result.uuid = normalizeUuid(profile.value(QStringLiteral("id")).toString()); result.userType = QStringLiteral("ELYBY"); callback(result); return; } authenticate(login, password, token, callback); }); }); } void AuthService::loginElyByWithTotp(const QString &login, const QString &password, const QString &totp, const QString &clientToken, Callback callback) { // Ely.by ждёт двухфакторный код прямо в пароле, через двоеточие. authenticate(login, password + u':' + totp, clientToken.isEmpty() ? generateClientToken() : clientToken, std::move(callback)); } void AuthService::authenticate(const QString &login, const QString &password, const QString &clientToken, Callback callback) { if (password.isEmpty()) { AuthResult result; result.error = Loc::text("auth.ely.error.noPassword"); callback(result); return; } emit progress(Loc::text("auth.ely.progress.signingIn")); postJson(QStringLiteral("/auth/authenticate"), {{QStringLiteral("username"), login}, {QStringLiteral("password"), password}, {QStringLiteral("clientToken"), clientToken}, {QStringLiteral("requestUser"), true}}, [clientToken, callback](int status, const QJsonObject &response, const QString &transportError) { AuthResult result; result.clientToken = clientToken; if (!transportError.isEmpty()) { result.error = Loc::text("auth.ely.error.unreachable").arg(transportError); callback(result); return; } if (status != 200) { const QString message = describeElyError(response, status); // Единственный случай, когда ошибка лечится вводом кода, а не пароля. result.twoFactorRequired = message.contains(QStringLiteral("two factor"), Qt::CaseInsensitive); result.error = result.twoFactorRequired ? Loc::text("auth.ely.error.twoFactor") : message; callback(result); return; } const QJsonObject profile = response.value(QStringLiteral("selectedProfile")).toObject(); result.playerName = profile.value(QStringLiteral("name")).toString(); result.uuid = normalizeUuid(profile.value(QStringLiteral("id")).toString()); result.accessToken = response.value(QStringLiteral("accessToken")).toString(); result.userType = QStringLiteral("ELYBY"); result.ok = !result.accessToken.isEmpty() && !result.playerName.isEmpty(); if (!result.ok) result.error = Loc::text("auth.ely.error.noGameProfile"); callback(result); }); } void AuthService::ensureAuthlibInjector( const QString &targetDir, std::function callback) { QDir().mkpath(targetDir); const QString jarPath = targetDir + u'/' + kInjectorFileName; if (QFileInfo::exists(jarPath)) { callback(jarPath, {}); return; } emit progress(Loc::text("auth.ely.progress.downloadingInjector")); QNetworkRequest manifestRequest{QUrl(kInjectorManifest)}; manifestRequest.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); QNetworkReply *manifestReply = m_network->get(manifestRequest); connect(manifestReply, &QNetworkReply::finished, this, [this, manifestReply, jarPath, callback]() { manifestReply->deleteLater(); if (manifestReply->error() != QNetworkReply::NoError) { callback({}, Loc::text("auth.ely.error.injectorInfoFailed") .arg(manifestReply->errorString())); return; } const QJsonObject manifest = QJsonDocument::fromJson(manifestReply->readAll()).object(); const QString url = manifest.value(QStringLiteral("download_url")).toString(); const QString expectedSha256 = manifest.value(QStringLiteral("checksums")) .toObject() .value(QStringLiteral("sha256")) .toString(); if (url.isEmpty()) { callback({}, Loc::text("auth.ely.error.injectorNoUrl")); return; } QNetworkRequest jarRequest{QUrl(url)}; jarRequest.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); QNetworkReply *jarReply = m_network->get(jarRequest); connect(jarReply, &QNetworkReply::finished, this, [jarReply, jarPath, expectedSha256, callback]() { jarReply->deleteLater(); if (jarReply->error() != QNetworkReply::NoError) { callback({}, Loc::text("auth.ely.error.injectorDownloadFailed").arg(jarReply->errorString())); return; } const QByteArray payload = jarReply->readAll(); if (!expectedSha256.isEmpty()) { const QString actual = QString::fromLatin1( QCryptographicHash::hash(payload, QCryptographicHash::Sha256).toHex()); if (actual != expectedSha256) { callback({}, Loc::text("auth.ely.error.injectorChecksum")); return; } } QFile file(jarPath); if (!file.open(QIODevice::WriteOnly) || file.write(payload) != payload.size()) { callback({}, Loc::text("common.error.saveFailed").arg(QDir::toNativeSeparators(jarPath))); return; } file.close(); callback(jarPath, {}); }); }); }