#include "authservice.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 QObject::tr("Сервер авторизации вернул код %1").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 = tr("Для офлайн-запуска нужен никнейм"); 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 = tr("Укажите логин Ely.by в настройках профиля"); callback(result); return; } const QString token = clientToken.isEmpty() ? generateClientToken() : clientToken; // Токена ещё нет — сразу за паролем. if (accessToken.isEmpty()) { authenticate(login, password, token, std::move(callback)); return; } emit progress(tr("Проверка сохранённого токена Ely.by…")); 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 = tr("Нет связи с authserver.ely.by: %1").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(tr("Обновление токена Ely.by…")); 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 = tr("Укажите пароль Ely.by в настройках профиля"); callback(result); return; } emit progress(tr("Авторизация на Ely.by…")); 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 = AuthService::tr("Нет связи с authserver.ely.by: %1") .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 ? AuthService::tr( "Аккаунт защищён двухфакторной аутентификацией") : 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 = AuthService::tr( "Ely.by не вернул игровой профиль для этого аккаунта"); 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(tr("Загрузка authlib-injector…")); 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({}, tr("Не удалось получить сведения об authlib-injector: %1") .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({}, tr("В манифесте authlib-injector нет ссылки на файл")); 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({}, AuthService::tr("Не удалось скачать authlib-injector: %1") .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({}, AuthService::tr( "Контрольная сумма authlib-injector не совпала")); return; } } QFile file(jarPath); if (!file.open(QIODevice::WriteOnly) || file.write(payload) != payload.size()) { callback({}, AuthService::tr("Не удалось сохранить %1") .arg(QDir::toNativeSeparators(jarPath))); return; } file.close(); callback(jarPath, {}); }); }); }