Files
minecraft-launcher/authservice.cpp
T

318 lines
14 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "authservice.h"
#include <QCryptographicHash>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonDocument>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QUuid>
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<char>((digest[6] & 0x0f) | 0x30); // версия UUID = 3
digest[8] = static_cast<char>((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<void(int, const QJsonObject &, const QString &)> 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<void(const QString &, const QString &)> 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, {});
});
});
}