2022-03-19 11:46:56 +00:00
|
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
/*
|
|
|
|
* PolyMC - Minecraft Launcher
|
|
|
|
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
|
2021-07-26 20:44:11 +01:00
|
|
|
*
|
2022-03-19 11:46:56 +00:00
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, version 3.
|
2021-07-26 20:44:11 +01:00
|
|
|
*
|
2022-03-19 11:46:56 +00:00
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
2021-07-26 20:44:11 +01:00
|
|
|
*
|
2022-03-19 11:46:56 +00:00
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
2021-07-26 20:44:11 +01:00
|
|
|
*
|
2022-03-19 11:46:56 +00:00
|
|
|
* This file incorporates work covered by the following copyright and
|
|
|
|
* permission notice:
|
|
|
|
*
|
|
|
|
* Copyright 2013-2021 MultiMC Contributors
|
|
|
|
*
|
|
|
|
* Authors: Orochimarufan <orochimarufan.x3@gmail.com>
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
2021-07-26 20:44:11 +01:00
|
|
|
*/
|
|
|
|
|
|
|
|
#include "MinecraftAccount.h"
|
|
|
|
|
2023-07-18 08:06:49 +01:00
|
|
|
#include <QCryptographicHash>
|
2021-07-26 20:44:11 +01:00
|
|
|
#include <QUuid>
|
|
|
|
#include <QJsonObject>
|
|
|
|
#include <QJsonArray>
|
2022-05-02 18:48:37 +01:00
|
|
|
#include <QRegularExpression>
|
2021-07-26 20:44:11 +01:00
|
|
|
#include <QStringList>
|
|
|
|
#include <QJsonDocument>
|
|
|
|
|
|
|
|
#include <QDebug>
|
|
|
|
|
|
|
|
#include <QPainter>
|
|
|
|
|
2021-12-04 00:18:05 +00:00
|
|
|
#include "flows/MSA.h"
|
|
|
|
#include "flows/Mojang.h"
|
2022-01-17 11:08:10 +00:00
|
|
|
#include "flows/Offline.h"
|
2021-07-26 20:44:11 +01:00
|
|
|
|
2021-11-28 17:42:01 +00:00
|
|
|
MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) {
|
2022-05-02 18:48:37 +01:00
|
|
|
data.internalId = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
|
2021-11-28 17:42:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-07-26 20:44:11 +01:00
|
|
|
MinecraftAccountPtr MinecraftAccount::loadFromJsonV2(const QJsonObject& json) {
|
|
|
|
MinecraftAccountPtr account(new MinecraftAccount());
|
|
|
|
if(account->data.resumeStateFromV2(json)) {
|
|
|
|
return account;
|
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) {
|
|
|
|
MinecraftAccountPtr account(new MinecraftAccount());
|
|
|
|
if(account->data.resumeStateFromV3(json)) {
|
|
|
|
return account;
|
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username)
|
|
|
|
{
|
2023-01-24 19:52:09 +00:00
|
|
|
auto account = makeShared<MinecraftAccount>();
|
2021-07-26 20:44:11 +01:00
|
|
|
account->data.type = AccountType::Mojang;
|
|
|
|
account->data.yggdrasilToken.extra["userName"] = username;
|
2022-05-02 18:48:37 +01:00
|
|
|
account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
|
2021-07-26 20:44:11 +01:00
|
|
|
return account;
|
|
|
|
}
|
|
|
|
|
|
|
|
MinecraftAccountPtr MinecraftAccount::createBlankMSA()
|
|
|
|
{
|
|
|
|
MinecraftAccountPtr account(new MinecraftAccount());
|
|
|
|
account->data.type = AccountType::MSA;
|
|
|
|
return account;
|
|
|
|
}
|
|
|
|
|
2022-01-17 11:08:10 +00:00
|
|
|
MinecraftAccountPtr MinecraftAccount::createOffline(const QString &username)
|
|
|
|
{
|
2023-01-24 19:52:09 +00:00
|
|
|
auto account = makeShared<MinecraftAccount>();
|
2022-01-17 11:08:10 +00:00
|
|
|
account->data.type = AccountType::Offline;
|
|
|
|
account->data.yggdrasilToken.token = "offline";
|
|
|
|
account->data.yggdrasilToken.validity = Katabasis::Validity::Certain;
|
|
|
|
account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc();
|
|
|
|
account->data.yggdrasilToken.extra["userName"] = username;
|
2022-05-02 18:48:37 +01:00
|
|
|
account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
|
2022-01-17 11:08:10 +00:00
|
|
|
account->data.minecraftEntitlement.ownsMinecraft = true;
|
|
|
|
account->data.minecraftEntitlement.canPlayMinecraft = true;
|
2023-07-18 08:06:49 +01:00
|
|
|
account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(QRegularExpression("[{}-]"));
|
2022-01-17 11:08:10 +00:00
|
|
|
account->data.minecraftProfile.name = username;
|
|
|
|
account->data.minecraftProfile.validity = Katabasis::Validity::Certain;
|
|
|
|
return account;
|
|
|
|
}
|
|
|
|
|
2021-07-26 20:44:11 +01:00
|
|
|
|
|
|
|
QJsonObject MinecraftAccount::saveToJson() const
|
|
|
|
{
|
|
|
|
return data.saveState();
|
|
|
|
}
|
|
|
|
|
2021-12-04 00:18:05 +00:00
|
|
|
AccountState MinecraftAccount::accountState() const {
|
|
|
|
return data.accountState;
|
2021-11-28 17:42:01 +00:00
|
|
|
}
|
|
|
|
|
2021-07-26 20:44:11 +01:00
|
|
|
QPixmap MinecraftAccount::getFace() const {
|
|
|
|
QPixmap skinTexture;
|
|
|
|
if(!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) {
|
|
|
|
return QPixmap();
|
|
|
|
}
|
|
|
|
QPixmap skin = QPixmap(8, 8);
|
|
|
|
QPainter painter(&skin);
|
|
|
|
painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8));
|
|
|
|
painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8));
|
|
|
|
return skin.scaled(64, 64, Qt::KeepAspectRatio);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-12-04 00:18:05 +00:00
|
|
|
shared_qobject_ptr<AccountTask> MinecraftAccount::login(QString password) {
|
2021-07-26 20:44:11 +01:00
|
|
|
Q_ASSERT(m_currentTask.get() == nullptr);
|
|
|
|
|
2021-12-04 00:18:05 +00:00
|
|
|
m_currentTask.reset(new MojangLogin(&data, password));
|
2023-04-18 01:51:34 +01:00
|
|
|
connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
|
|
|
|
connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
|
2022-06-22 23:56:24 +01:00
|
|
|
connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); });
|
2021-12-04 00:18:05 +00:00
|
|
|
emit activityChanged(true);
|
2021-07-26 20:44:11 +01:00
|
|
|
return m_currentTask;
|
|
|
|
}
|
|
|
|
|
2021-12-04 00:18:05 +00:00
|
|
|
shared_qobject_ptr<AccountTask> MinecraftAccount::loginMSA() {
|
2021-07-26 20:44:11 +01:00
|
|
|
Q_ASSERT(m_currentTask.get() == nullptr);
|
|
|
|
|
2021-12-04 00:18:05 +00:00
|
|
|
m_currentTask.reset(new MSAInteractive(&data));
|
2023-04-18 01:51:34 +01:00
|
|
|
connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
|
|
|
|
connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
|
2022-06-22 23:56:24 +01:00
|
|
|
connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); });
|
2021-12-04 00:18:05 +00:00
|
|
|
emit activityChanged(true);
|
2021-07-26 20:44:11 +01:00
|
|
|
return m_currentTask;
|
|
|
|
}
|
|
|
|
|
2022-01-17 11:08:10 +00:00
|
|
|
shared_qobject_ptr<AccountTask> MinecraftAccount::loginOffline() {
|
|
|
|
Q_ASSERT(m_currentTask.get() == nullptr);
|
|
|
|
|
|
|
|
m_currentTask.reset(new OfflineLogin(&data));
|
2023-04-18 01:51:34 +01:00
|
|
|
connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
|
|
|
|
connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
|
2022-06-22 23:56:24 +01:00
|
|
|
connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); });
|
2022-01-17 11:08:10 +00:00
|
|
|
emit activityChanged(true);
|
|
|
|
return m_currentTask;
|
|
|
|
}
|
|
|
|
|
2021-12-04 00:18:05 +00:00
|
|
|
shared_qobject_ptr<AccountTask> MinecraftAccount::refresh() {
|
|
|
|
if(m_currentTask) {
|
|
|
|
return m_currentTask;
|
2021-07-26 20:44:11 +01:00
|
|
|
}
|
|
|
|
|
2021-12-04 00:18:05 +00:00
|
|
|
if(data.type == AccountType::MSA) {
|
|
|
|
m_currentTask.reset(new MSASilent(&data));
|
2021-07-26 20:44:11 +01:00
|
|
|
}
|
2022-01-17 11:08:10 +00:00
|
|
|
else if(data.type == AccountType::Offline) {
|
|
|
|
m_currentTask.reset(new OfflineRefresh(&data));
|
|
|
|
}
|
2021-12-04 00:18:05 +00:00
|
|
|
else {
|
|
|
|
m_currentTask.reset(new MojangRefresh(&data));
|
2021-07-26 20:44:11 +01:00
|
|
|
}
|
2021-12-04 00:18:05 +00:00
|
|
|
|
2023-04-18 01:51:34 +01:00
|
|
|
connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded);
|
|
|
|
connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed);
|
2022-06-22 23:56:24 +01:00
|
|
|
connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); });
|
2021-12-04 00:18:05 +00:00
|
|
|
emit activityChanged(true);
|
|
|
|
return m_currentTask;
|
|
|
|
}
|
|
|
|
|
|
|
|
shared_qobject_ptr<AccountTask> MinecraftAccount::currentTask() {
|
2021-07-26 20:44:11 +01:00
|
|
|
return m_currentTask;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void MinecraftAccount::authSucceeded()
|
|
|
|
{
|
|
|
|
m_currentTask.reset();
|
|
|
|
emit changed();
|
2021-11-20 15:22:22 +00:00
|
|
|
emit activityChanged(false);
|
2021-07-26 20:44:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
void MinecraftAccount::authFailed(QString reason)
|
|
|
|
{
|
2021-12-04 00:18:05 +00:00
|
|
|
switch (m_currentTask->taskState()) {
|
|
|
|
case AccountTaskState::STATE_OFFLINE:
|
2022-02-18 11:26:52 +00:00
|
|
|
case AccountTaskState::STATE_DISABLED: {
|
|
|
|
// NOTE: user will need to fix this themselves.
|
|
|
|
}
|
2021-12-04 00:18:05 +00:00
|
|
|
case AccountTaskState::STATE_FAILED_SOFT: {
|
|
|
|
// NOTE: this doesn't do much. There was an error of some sort.
|
2021-07-26 20:44:11 +01:00
|
|
|
}
|
2021-08-29 18:58:35 +01:00
|
|
|
break;
|
2021-12-04 00:18:05 +00:00
|
|
|
case AccountTaskState::STATE_FAILED_HARD: {
|
|
|
|
if(isMSA()) {
|
|
|
|
data.msaToken.token = QString();
|
|
|
|
data.msaToken.refresh_token = QString();
|
|
|
|
data.msaToken.validity = Katabasis::Validity::None;
|
|
|
|
data.validity_ = Katabasis::Validity::None;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
data.yggdrasilToken.token = QString();
|
|
|
|
data.yggdrasilToken.validity = Katabasis::Validity::None;
|
|
|
|
data.validity_ = Katabasis::Validity::None;
|
2021-08-27 21:35:17 +01:00
|
|
|
}
|
2021-12-04 00:18:05 +00:00
|
|
|
emit changed();
|
2021-08-29 18:58:35 +01:00
|
|
|
}
|
|
|
|
break;
|
2021-12-04 00:18:05 +00:00
|
|
|
case AccountTaskState::STATE_FAILED_GONE: {
|
2021-08-29 18:58:35 +01:00
|
|
|
data.validity_ = Katabasis::Validity::None;
|
|
|
|
emit changed();
|
|
|
|
}
|
|
|
|
break;
|
2021-12-04 00:18:05 +00:00
|
|
|
case AccountTaskState::STATE_CREATED:
|
|
|
|
case AccountTaskState::STATE_WORKING:
|
|
|
|
case AccountTaskState::STATE_SUCCEEDED: {
|
2021-08-29 18:58:35 +01:00
|
|
|
// Not reachable here, as they are not failures.
|
2021-07-26 20:44:11 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
m_currentTask.reset();
|
2021-11-20 15:22:22 +00:00
|
|
|
emit activityChanged(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool MinecraftAccount::isActive() const {
|
2022-08-04 17:58:30 +01:00
|
|
|
return !m_currentTask.isNull();
|
2021-11-20 15:22:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool MinecraftAccount::shouldRefresh() const {
|
|
|
|
/*
|
|
|
|
* Never refresh accounts that are being used by the game, it breaks the game session.
|
|
|
|
* Always refresh accounts that have not been refreshed yet during this session.
|
|
|
|
* Don't refresh broken accounts.
|
|
|
|
* Refresh accounts that would expire in the next 12 hours (fresh token validity is 24 hours).
|
|
|
|
*/
|
|
|
|
if(isInUse()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
switch(data.validity_) {
|
|
|
|
case Katabasis::Validity::Certain: {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case Katabasis::Validity::None: {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
case Katabasis::Validity::Assumed: {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
auto now = QDateTime::currentDateTimeUtc();
|
|
|
|
auto issuedTimestamp = data.yggdrasilToken.issueInstant;
|
|
|
|
auto expiresTimestamp = data.yggdrasilToken.notAfter;
|
|
|
|
|
|
|
|
if(!expiresTimestamp.isValid()) {
|
|
|
|
expiresTimestamp = issuedTimestamp.addSecs(24 * 3600);
|
|
|
|
}
|
2021-12-04 01:10:14 +00:00
|
|
|
if (now.secsTo(expiresTimestamp) < (12 * 3600)) {
|
2021-11-20 15:22:22 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
2021-07-26 20:44:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
void MinecraftAccount::fillSession(AuthSessionPtr session)
|
|
|
|
{
|
2021-12-04 00:18:05 +00:00
|
|
|
if(ownsMinecraft() && !hasProfile()) {
|
|
|
|
session->status = AuthSession::RequiresProfileSetup;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
if(session->wants_online) {
|
|
|
|
session->status = AuthSession::PlayableOnline;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
session->status = AuthSession::PlayableOffline;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-26 20:44:11 +01:00
|
|
|
// the user name. you have to have an user name
|
|
|
|
// FIXME: not with MSA
|
|
|
|
session->username = data.userName();
|
|
|
|
// volatile auth token
|
|
|
|
session->access_token = data.accessToken();
|
|
|
|
// the semi-permanent client token
|
|
|
|
session->client_token = data.clientToken();
|
|
|
|
// profile name
|
|
|
|
session->player_name = data.profileName();
|
|
|
|
// profile ID
|
|
|
|
session->uuid = data.profileId();
|
|
|
|
// 'legacy' or 'mojang', depending on account type
|
|
|
|
session->user_type = typeString();
|
|
|
|
if (!session->access_token.isEmpty())
|
|
|
|
{
|
|
|
|
session->session = "token:" + data.accessToken() + ":" + data.profileId();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
session->session = "-";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void MinecraftAccount::decrementUses()
|
|
|
|
{
|
|
|
|
Usable::decrementUses();
|
|
|
|
if(!isInUse())
|
|
|
|
{
|
|
|
|
emit changed();
|
|
|
|
// FIXME: we now need a better way to identify accounts...
|
|
|
|
qWarning() << "Profile" << data.profileId() << "is no longer in use.";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void MinecraftAccount::incrementUses()
|
|
|
|
{
|
|
|
|
bool wasInUse = isInUse();
|
|
|
|
Usable::incrementUses();
|
|
|
|
if(!wasInUse)
|
|
|
|
{
|
|
|
|
emit changed();
|
|
|
|
// FIXME: we now need a better way to identify accounts...
|
|
|
|
qWarning() << "Profile" << data.profileId() << "is now in use.";
|
|
|
|
}
|
|
|
|
}
|
2023-07-18 08:06:49 +01:00
|
|
|
|
|
|
|
QUuid MinecraftAccount::uuidFromUsername(QString username) {
|
|
|
|
auto input = QString("OfflinePlayer:%1").arg(username).toUtf8();
|
|
|
|
|
|
|
|
// basically a reimplementation of Java's UUID#nameUUIDFromBytes
|
|
|
|
QByteArray digest = QCryptographicHash::hash(input, QCryptographicHash::Md5);
|
|
|
|
|
|
|
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
|
|
|
auto bOr = [](QByteArray& array, int index, char value) {
|
|
|
|
array[index] = array.at(index) | value;
|
|
|
|
};
|
|
|
|
auto bAnd = [](QByteArray& array, int index, char value) {
|
|
|
|
array[index] = array.at(index) & value;
|
|
|
|
};
|
|
|
|
#else
|
|
|
|
auto bOr = [](QByteArray& array, qsizetype index, char value) {
|
|
|
|
array[index] |= value;
|
|
|
|
};
|
|
|
|
auto bAnd = [](QByteArray& array, qsizetype index, char value) {
|
|
|
|
array[index] &= value;
|
|
|
|
};
|
|
|
|
#endif
|
|
|
|
bAnd(digest, 6, (char) 0x0f); // clear version
|
|
|
|
bOr(digest, 6, (char) 0x30); // set to version 3
|
|
|
|
bAnd(digest, 8, (char) 0x3f); // clear variant
|
|
|
|
bOr(digest, 8, (char) 0x80); // set to IETF variant
|
|
|
|
|
|
|
|
return QUuid::fromRfc4122(digest);
|
|
|
|
}
|