1
Fork 0
mirror of https://github.com/redstrate/Astra.git synced 2025-04-20 11:47:46 +00:00
astra/launcher/src/squarelauncher.cpp

230 lines
9.3 KiB
C++
Raw Normal View History

// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later
2021-11-01 09:54:58 -04:00
#include "squarelauncher.h"
#include <KLocalizedString>
2022-08-15 11:14:37 -04:00
#include <QDesktopServices>
2021-11-01 09:54:58 -04:00
#include <QFile>
2022-08-15 11:14:37 -04:00
#include <QNetworkReply>
#include <QRegularExpressionMatch>
#include <QUrlQuery>
#include <qcoronetworkreply.h>
2021-11-01 09:54:58 -04:00
#include "account.h"
#include "launchercore.h"
2022-01-27 09:25:23 -05:00
SquareLauncher::SquareLauncher(LauncherCore &window, QObject *parent)
: QObject(parent)
, window(window)
{
}
2021-11-01 09:54:58 -04:00
QString getFileHash(const QString &file)
{
2021-11-01 09:54:58 -04:00
auto f = QFile(file);
if (!f.open(QIODevice::ReadOnly))
return {};
2021-11-01 09:54:58 -04:00
QCryptographicHash hash(QCryptographicHash::Sha1);
hash.addData(&f);
return QStringLiteral("%1/%2").arg(QString::number(f.size()), hash.result().toHex());
2021-11-01 09:54:58 -04:00
}
QCoro::Task<std::optional<SquareLauncher::StoredInfo>> SquareLauncher::getStored(const LoginInformation &info)
{
Q_EMIT window.stageChanged(i18n("Logging in..."));
2021-11-01 09:54:58 -04:00
QUrlQuery query;
// en is always used to the top url
query.addQueryItem(QStringLiteral("lng"), QStringLiteral("en"));
// for some reason, we always use region 3. the actual region is acquired later
query.addQueryItem(QStringLiteral("rgn"), QStringLiteral("3"));
query.addQueryItem(QStringLiteral("isft"), info.profile->account()->isFreeTrial() ? QStringLiteral("1") : QStringLiteral("0"));
query.addQueryItem(QStringLiteral("cssmode"), QStringLiteral("1"));
query.addQueryItem(QStringLiteral("isnew"), QStringLiteral("1"));
query.addQueryItem(QStringLiteral("launchver"), QStringLiteral("3"));
if (info.profile->account()->license() == Account::GameLicense::WindowsSteam) {
query.addQueryItem(QStringLiteral("issteam"), QStringLiteral("1"));
// TODO: get steam ticket information from steam api
query.addQueryItem(QStringLiteral("session_ticket"), QStringLiteral("1"));
query.addQueryItem(QStringLiteral("ticket_size"), QStringLiteral("1"));
}
2021-11-01 09:54:58 -04:00
QUrl url;
url.setScheme(QStringLiteral("https"));
url.setHost(QStringLiteral("ffxiv-login.%1").arg(window.squareEnixLoginServer()));
url.setPath(QStringLiteral("/oauth/ffxivarr/login/top"));
2021-11-01 09:54:58 -04:00
url.setQuery(query);
auto request = QNetworkRequest(url);
window.buildRequest(*info.profile, request);
2021-11-01 09:54:58 -04:00
const auto reply = window.mgr->get(request);
co_await reply;
2021-11-01 09:54:58 -04:00
const QString str = reply->readAll();
2021-11-01 09:54:58 -04:00
// fetches Steam username
if (info.profile->account()->license() == Account::GameLicense::WindowsSteam) {
const QRegularExpression re(QStringLiteral(R"lit(<input name=""sqexid"" type=""hidden"" value=""(?<sqexid>.*)""\/>)lit"));
const QRegularExpressionMatch match = re.match(str);
2021-11-01 09:54:58 -04:00
if (match.hasMatch()) {
username = match.captured(1);
2021-11-01 09:54:58 -04:00
} else {
Q_EMIT window.loginError(i18n("Could not get Steam username, have you attached your account?"));
2021-11-01 09:54:58 -04:00
}
} else {
username = info.username;
}
const QRegularExpression re(QStringLiteral(R"lit(\t<\s*input .* name="_STORED_" value="(?<stored>.*)">)lit"));
const QRegularExpressionMatch match = re.match(str);
if (match.hasMatch()) {
co_return StoredInfo{match.captured(1), url};
} else {
Q_EMIT window.loginError(
i18n("Square Enix servers refused to confirm session information. The game may be under maintenance, try the official launcher."));
co_return {};
}
2021-11-01 09:54:58 -04:00
}
QCoro::Task<> SquareLauncher::login(const LoginInformation &info)
{
const auto storedResult = co_await getStored(info);
if (storedResult == std::nullopt) {
co_return;
}
const auto [stored, referer] = *storedResult;
2021-11-01 09:54:58 -04:00
QUrlQuery postData;
postData.addQueryItem(QStringLiteral("_STORED_"), stored);
postData.addQueryItem(QStringLiteral("sqexid"), info.username);
postData.addQueryItem(QStringLiteral("password"), info.password);
postData.addQueryItem(QStringLiteral("otppw"), info.oneTimePassword);
2021-11-01 09:54:58 -04:00
QUrl url;
url.setScheme(QStringLiteral("https"));
url.setHost(QStringLiteral("ffxiv-login.%1").arg(window.squareEnixLoginServer()));
url.setPath(QStringLiteral("/oauth/ffxivarr/login/login.send"));
QNetworkRequest request(url);
window.buildRequest(*info.profile, request);
request.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/x-www-form-urlencoded"));
request.setRawHeader(QByteArrayLiteral("Referer"), referer.toEncoded());
request.setRawHeader(QByteArrayLiteral("Cache-Control"), QByteArrayLiteral("no-cache"));
2021-11-01 09:54:58 -04:00
const auto reply = window.mgr->post(request, postData.toString(QUrl::FullyEncoded).toUtf8());
co_await reply;
2021-11-01 09:54:58 -04:00
const QString str = reply->readAll();
2021-11-01 09:54:58 -04:00
const QRegularExpression re(QStringLiteral(R"lit(window.external.user\("login=auth,ok,(?<launchParams>.*)\);)lit"));
const QRegularExpressionMatch match = re.match(str);
if (match.hasMatch()) {
const auto parts = match.captured(1).split(QLatin1Char(','));
2021-11-01 09:54:58 -04:00
const bool terms = parts[3] == QLatin1String("1");
const bool playable = parts[9] == QLatin1String("1");
if (!playable) {
Q_EMIT window.loginError(i18n("Your account is unplayable. Check that you have the correct license, and a valid subscription."));
co_return;
}
if (!terms) {
Q_EMIT window.loginError(i18n("Your account is unplayable. You need to accept the terms of service from the official launcher first."));
co_return;
}
SID = parts[1];
auth.region = parts[5].toInt();
auth.maxExpansion = parts[13].toInt();
registerSession(info);
} else {
const QRegularExpression re(QStringLiteral(R"lit(window.external.user\("login=auth,ng,err,(?<launchParams>.*)\);)lit"));
const QRegularExpressionMatch match = re.match(str);
// there's a stray quote at the end of the error string, so let's remove that
Q_EMIT window.loginError(match.captured(1).chopped(1));
}
2021-11-01 09:54:58 -04:00
}
QCoro::Task<> SquareLauncher::registerSession(const LoginInformation &info)
{
2021-11-01 09:54:58 -04:00
QUrl url;
url.setScheme(QStringLiteral("https"));
url.setHost(QStringLiteral("patch-gamever.%1").arg(window.squareEnixServer()));
2023-09-17 08:51:26 -04:00
url.setPath(QStringLiteral("/http/win32/ffxivneo_release_game/%1/%2").arg(info.profile->baseGameVersion(), SID));
2021-11-01 09:54:58 -04:00
auto request = QNetworkRequest(url);
window.setSSL(request);
request.setRawHeader(QByteArrayLiteral("X-Hash-Check"), QByteArrayLiteral("enabled"));
request.setRawHeader(QByteArrayLiteral("User-Agent"), QByteArrayLiteral("FFXIV PATCH CLIENT"));
request.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/x-www-form-urlencoded"));
2021-11-01 09:54:58 -04:00
2023-09-17 08:51:26 -04:00
QString report = QStringLiteral("%1=%2").arg(info.profile->bootVersion(), getBootHash(info));
2022-08-15 11:14:37 -04:00
for (int i = 1; i < auth.maxExpansion + 1; i++) {
2023-09-17 08:51:26 -04:00
if (i < static_cast<int>(info.profile->numInstalledExpansions())) {
report += QStringLiteral("\nex%1\t%2").arg(QString::number(i), info.profile->expansionVersion(i));
} else {
report += QStringLiteral("\nex%1\t2012.01.01.0000.0000").arg(QString::number(i));
}
}
2021-11-01 09:54:58 -04:00
2023-09-17 08:51:26 -04:00
qInfo() << report;
const auto reply = window.mgr->post(request, report.toUtf8());
co_await reply;
if (reply->error() == QNetworkReply::NoError) {
if (reply->rawHeaderList().contains(QByteArrayLiteral("X-Patch-Unique-Id"))) {
const QString body = reply->readAll();
2023-09-17 08:51:26 -04:00
patcher = new Patcher(window, info.profile->gamePath() + QStringLiteral("/game"), *info.profile->gameData(), this);
2023-09-16 21:18:50 -04:00
co_await patcher->patch(body);
2023-09-16 20:12:42 -04:00
info.profile->readGameVersion();
2021-11-01 09:54:58 -04:00
2023-09-16 20:12:42 -04:00
auth.SID = reply->rawHeader(QByteArrayLiteral("X-Patch-Unique-Id"));
2023-09-16 20:12:42 -04:00
window.launchGame(*info.profile, auth);
} else {
Q_EMIT window.loginError(i18n("Fatal error, request was successful but X-Patch-Unique-Id was not recieved."));
}
} else {
if (reply->error() == QNetworkReply::SslHandshakeFailedError) {
Q_EMIT window.loginError(
i18n("SSL handshake error detected. If you are using OpenSUSE or Fedora, try running `update-crypto-policies --set LEGACY`."));
} else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 405) {
Q_EMIT window.loginError(i18n("The game failed the anti-tamper check. Restore the game to the original state and try updating again."));
2021-11-01 09:54:58 -04:00
} else {
Q_EMIT window.loginError(i18n("Unknown error when registering the session."));
2021-11-01 09:54:58 -04:00
}
}
2021-11-01 09:54:58 -04:00
}
QString SquareLauncher::getBootHash(const LoginInformation &info)
{
const QList<QString> fileList = {QStringLiteral("ffxivboot.exe"),
QStringLiteral("ffxivboot64.exe"),
QStringLiteral("ffxivlauncher.exe"),
QStringLiteral("ffxivlauncher64.exe"),
QStringLiteral("ffxivupdater.exe"),
QStringLiteral("ffxivupdater64.exe")};
2021-11-01 09:54:58 -04:00
QString result;
for (int i = 0; i < fileList.count(); i++) {
result += fileList[i] + QStringLiteral("/") + getFileHash(info.profile->gamePath() + QStringLiteral("/boot/") + fileList[i]);
2021-11-01 09:54:58 -04:00
if (i != fileList.length() - 1)
result += QStringLiteral(",");
2021-11-01 09:54:58 -04:00
}
return result;
}