diff options
-rw-r--r-- | launcher/CMakeLists.txt | 2 | ||||
-rw-r--r-- | launcher/dialogs/LoginDialog.cpp | 12 | ||||
-rw-r--r-- | launcher/dialogs/MSALoginDialog.cpp | 47 | ||||
-rw-r--r-- | launcher/dialogs/MSALoginDialog.h | 10 | ||||
-rw-r--r-- | launcher/dialogs/MSALoginDialog.ui | 11 | ||||
-rw-r--r-- | launcher/dialogs/SkinUploadDialog.cpp | 44 | ||||
-rw-r--r-- | launcher/dialogs/SkinUploadDialog.ui | 170 | ||||
-rw-r--r-- | launcher/minecraft/auth/AccountData.cpp | 78 | ||||
-rw-r--r-- | launcher/minecraft/auth/AccountData.h | 4 | ||||
-rw-r--r-- | launcher/minecraft/auth/AccountTask.h | 4 | ||||
-rw-r--r-- | launcher/minecraft/auth/flows/AuthContext.cpp | 123 | ||||
-rw-r--r-- | launcher/minecraft/auth/flows/AuthContext.h | 14 | ||||
-rw-r--r-- | launcher/minecraft/services/CapeChange.cpp | 67 | ||||
-rw-r--r-- | launcher/minecraft/services/CapeChange.h | 32 | ||||
-rw-r--r-- | libraries/katabasis/include/katabasis/OAuth2.h | 4 | ||||
-rw-r--r-- | libraries/katabasis/include/katabasis/Requestor.h | 3 | ||||
-rw-r--r-- | libraries/katabasis/src/OAuth2.cpp | 27 | ||||
-rw-r--r-- | libraries/katabasis/src/Requestor.cpp | 18 |
18 files changed, 472 insertions, 198 deletions
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 3c140ede..84a03895 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -329,6 +329,8 @@ set(MINECRAFT_SOURCES minecraft/AssetsUtils.cpp # Minecraft services + minecraft/services/CapeChange.cpp + minecraft/services/CapeChange.h minecraft/services/SkinUpload.cpp minecraft/services/SkinUpload.h minecraft/services/SkinDelete.cpp diff --git a/launcher/dialogs/LoginDialog.cpp b/launcher/dialogs/LoginDialog.cpp index 1dee9920..bf0806e1 100644 --- a/launcher/dialogs/LoginDialog.cpp +++ b/launcher/dialogs/LoginDialog.cpp @@ -73,7 +73,17 @@ void LoginDialog::on_passTextBox_textEdited(const QString &newText) void LoginDialog::onTaskFailed(const QString &reason) { // Set message - ui->label->setText("<span style='color:red'>" + reason + "</span>"); + auto lines = reason.split('\n'); + QString processed; + for(auto line: lines) { + if(line.size()) { + processed += "<font color='red'>" + line + "</font><br />"; + } + else { + processed += "<br />"; + } + } + ui->label->setText(processed); // Re-enable user-interaction setUserInputsEnabled(true); diff --git a/launcher/dialogs/MSALoginDialog.cpp b/launcher/dialogs/MSALoginDialog.cpp index 778b379d..15c04061 100644 --- a/launcher/dialogs/MSALoginDialog.cpp +++ b/launcher/dialogs/MSALoginDialog.cpp @@ -19,6 +19,7 @@ #include "minecraft/auth/AccountTask.h" #include <QtWidgets/QPushButton> +#include <QUrl> MSALoginDialog::MSALoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::MSALoginDialog) { @@ -41,6 +42,9 @@ int MSALoginDialog::exec() { connect(m_loginTask.get(), &Task::succeeded, this, &MSALoginDialog::onTaskSucceeded); connect(m_loginTask.get(), &Task::status, this, &MSALoginDialog::onTaskStatus); connect(m_loginTask.get(), &Task::progress, this, &MSALoginDialog::onTaskProgress); + connect(m_loginTask.get(), &AccountTask::showVerificationUriAndCode, this, &MSALoginDialog::showVerificationUriAndCode); + connect(m_loginTask.get(), &AccountTask::hideVerificationUriAndCode, this, &MSALoginDialog::hideVerificationUriAndCode); + connect(&m_externalLoginTimer, &QTimer::timeout, this, &MSALoginDialog::externalLoginTick); m_loginTask->start(); return QDialog::exec(); @@ -52,6 +56,37 @@ MSALoginDialog::~MSALoginDialog() delete ui; } +void MSALoginDialog::externalLoginTick() { + m_externalLoginElapsed++; + ui->progressBar->setValue(m_externalLoginElapsed); + ui->progressBar->repaint(); + + if(m_externalLoginElapsed >= m_externalLoginTimeout) { + m_externalLoginTimer.stop(); + } +} + + +void MSALoginDialog::showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn) { + m_externalLoginElapsed = 0; + m_externalLoginTimeout = expiresIn; + + m_externalLoginTimer.setInterval(1000); + m_externalLoginTimer.setSingleShot(false); + m_externalLoginTimer.start(); + + ui->progressBar->setMaximum(expiresIn); + ui->progressBar->setValue(m_externalLoginElapsed); + + QString urlString = uri.toString(); + QString linkString = QString("<a href=\"%1\">%2</a>").arg(urlString, urlString); + ui->label->setText(tr("<p>Please open up %1 in a browser and put in the code <b>%2</b> to proceed with login.</p>").arg(linkString, code)); +} + +void MSALoginDialog::hideVerificationUriAndCode() { + m_externalLoginTimer.stop(); +} + void MSALoginDialog::setUserInputsEnabled(bool enable) { ui->buttonBox->setEnabled(enable); @@ -60,7 +95,17 @@ void MSALoginDialog::setUserInputsEnabled(bool enable) void MSALoginDialog::onTaskFailed(const QString &reason) { // Set message - ui->label->setText("<span style='color:red'>" + reason + "</span>"); + auto lines = reason.split('\n'); + QString processed; + for(auto line: lines) { + if(line.size()) { + processed += "<font color='red'>" + line + "</font><br />"; + } + else { + processed += "<br />"; + } + } + ui->label->setText(processed); // Re-enable user-interaction setUserInputsEnabled(true); diff --git a/launcher/dialogs/MSALoginDialog.h b/launcher/dialogs/MSALoginDialog.h index 402180ee..3d26a0dd 100644 --- a/launcher/dialogs/MSALoginDialog.h +++ b/launcher/dialogs/MSALoginDialog.h @@ -17,6 +17,7 @@ #include <QtWidgets/QDialog> #include <QtCore/QEventLoop> +#include <QTimer> #include "minecraft/auth/MinecraftAccount.h" @@ -46,10 +47,17 @@ slots: void onTaskSucceeded(); void onTaskStatus(const QString &status); void onTaskProgress(qint64 current, qint64 total); + void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn); + void hideVerificationUriAndCode(); + + void externalLoginTick(); private: Ui::MSALoginDialog *ui; MinecraftAccountPtr m_account; - std::shared_ptr<Task> m_loginTask; + std::shared_ptr<AccountTask> m_loginTask; + QTimer m_externalLoginTimer; + int m_externalLoginElapsed = 0; + int m_externalLoginTimeout = 0; }; diff --git a/launcher/dialogs/MSALoginDialog.ui b/launcher/dialogs/MSALoginDialog.ui index 4ae8085a..78cbfb26 100644 --- a/launcher/dialogs/MSALoginDialog.ui +++ b/launcher/dialogs/MSALoginDialog.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>421</width> - <height>114</height> + <width>491</width> + <height>143</height> </rect> </property> <property name="sizePolicy"> @@ -23,11 +23,16 @@ <item> <widget class="QLabel" name="label"> <property name="text"> - <string notr="true">Message label placeholder.</string> + <string notr="true">Message label placeholder. + +aaaaa</string> </property> <property name="textFormat"> <enum>Qt::RichText</enum> </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> <property name="textInteractionFlags"> <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> </property> diff --git a/launcher/dialogs/SkinUploadDialog.cpp b/launcher/dialogs/SkinUploadDialog.cpp index 19bfac4d..97478f4b 100644 --- a/launcher/dialogs/SkinUploadDialog.cpp +++ b/launcher/dialogs/SkinUploadDialog.cpp @@ -1,11 +1,16 @@ #include <QFileInfo> #include <QFileDialog> +#include <QPainter> + #include <FileSystem.h> #include <minecraft/services/SkinUpload.h> +#include <tasks/SequentialTask.h> + #include "SkinUploadDialog.h" #include "ui_SkinUploadDialog.h" #include "ProgressDialog.h" #include "CustomMessageBox.h" +#include <minecraft/services/CapeChange.h> void SkinUploadDialog::on_buttonBox_rejected() { @@ -85,8 +90,13 @@ void SkinUploadDialog::on_buttonBox_accepted() { model = SkinUpload::ALEX; } - SkinUploadPtr upload = std::make_shared<SkinUpload>(this, session, FS::read(fileName), model); - if (prog.execWithTask((Task*)upload.get()) != QDialog::Accepted) + SequentialTask skinUpload; + skinUpload.addTask(std::make_shared<SkinUpload>(this, session, FS::read(fileName), model)); + auto selectedCape = ui->capeCombo->currentData().toString(); + if(selectedCape != session->m_accountPtr->accountData()->minecraftProfile.currentCape) { + skinUpload.addTask(std::make_shared<CapeChange>(this, session, selectedCape)); + } + if (prog.execWithTask(&skinUpload) != QDialog::Accepted) { CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to upload skin!"), QMessageBox::Warning)->exec(); close(); @@ -111,4 +121,34 @@ SkinUploadDialog::SkinUploadDialog(MinecraftAccountPtr acct, QWidget *parent) :QDialog(parent), m_acct(acct), ui(new Ui::SkinUploadDialog) { ui->setupUi(this); + + // FIXME: add a model for this, download/refresh the capes on demand + auto &data = *acct->accountData(); + int index = 0; + ui->capeCombo->addItem(tr("No Cape"), QVariant()); + auto currentCape = data.minecraftProfile.currentCape; + if(currentCape.isEmpty()) { + ui->capeCombo->setCurrentIndex(index); + } + + for(auto & cape: data.minecraftProfile.capes) { + index++; + if(cape.data.size()) { + QPixmap capeImage; + if(capeImage.loadFromData(cape.data, "PNG")) { + QPixmap preview = QPixmap(10, 16); + QPainter painter(&preview); + painter.drawPixmap(0, 0, capeImage.copy(1, 1, 10, 16)); + ui->capeCombo->addItem(capeImage, cape.alias, cape.id); + if(currentCape == cape.id) { + ui->capeCombo->setCurrentIndex(index); + } + continue; + } + } + ui->capeCombo->addItem(cape.alias, cape.id); + if(currentCape == cape.id) { + ui->capeCombo->setCurrentIndex(index); + } + } } diff --git a/launcher/dialogs/SkinUploadDialog.ui b/launcher/dialogs/SkinUploadDialog.ui index 6f5307e3..f4b0ed0a 100644 --- a/launcher/dialogs/SkinUploadDialog.ui +++ b/launcher/dialogs/SkinUploadDialog.ui @@ -1,85 +1,97 @@ <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> - <class>SkinUploadDialog</class> - <widget class="QDialog" name="SkinUploadDialog"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>413</width> - <height>300</height> - </rect> + <class>SkinUploadDialog</class> + <widget class="QDialog" name="SkinUploadDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>394</width> + <height>360</height> + </rect> + </property> + <property name="windowTitle"> + <string>Skin Upload</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QGroupBox" name="fileBox"> + <property name="title"> + <string>Skin File</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLineEdit" name="skinPathTextBox"/> + </item> + <item> + <widget class="QPushButton" name="skinBrowseBtn"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> </property> - <property name="windowTitle"> - <string>Skin Upload</string> + <property name="maximumSize"> + <size> + <width>28</width> + <height>16777215</height> + </size> </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <widget class="QGroupBox" name="fileBox"> - <property name="title"> - <string>Skin File</string> - </property> - <layout class="QHBoxLayout" name="horizontalLayout"> - <item> - <widget class="QLineEdit" name="skinPathTextBox"/> - </item> - <item> - <widget class="QPushButton" name="skinBrowseBtn"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="maximumSize"> - <size> - <width>28</width> - <height>16777215</height> - </size> - </property> - <property name="text"> - <string notr="true">...</string> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item> - <widget class="QGroupBox" name="modelBox"> - <property name="title"> - <string>Player Model</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_1"> - <item> - <widget class="QRadioButton" name="steveBtn"> - <property name="text"> - <string>Steve Model</string> - </property> - <property name="checked"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <widget class="QRadioButton" name="alexBtn"> - <property name="text"> - <string>Alex Model</string> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item> - <widget class="QDialogButtonBox" name="buttonBox"> - <property name="standardButtons"> - <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> - </property> - </widget> - </item> - </layout> + <property name="text"> + <string notr="true">...</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="modelBox"> + <property name="title"> + <string>Player Model</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_1"> + <item> + <widget class="QRadioButton" name="steveBtn"> + <property name="text"> + <string>Steve Model</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="alexBtn"> + <property name="text"> + <string>Alex Model</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="capeBox"> + <property name="title"> + <string>Cape</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QComboBox" name="capeCombo"/> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> </widget> - <resources/> - <connections/> + </item> + </layout> + </widget> + <resources/> + <connections/> </ui> diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index 77c73c1b..5c6de9df 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -78,8 +78,8 @@ void profileToJSONV3(QJsonObject &parent, MinecraftProfile p, const char * token QJsonObject out; out["id"] = QJsonValue(p.id); out["name"] = QJsonValue(p.name); - if(p.currentCape != -1) { - out["cape"] = p.capes[p.currentCape].id; + if(!p.currentCape.isEmpty()) { + out["cape"] = p.currentCape; } { @@ -155,41 +155,53 @@ MinecraftProfile profileFromJSONV3(const QJsonObject &parent, const char * token } } - auto capesV = tokenObject.value("capes"); - if(!capesV.isArray()) { - qWarning() << "capes is not an array!"; - return MinecraftProfile(); - } - auto capesArray = capesV.toArray(); - for(auto capeV: capesArray) { - if(!capeV.isObject()) { - qWarning() << "cape is not an object!"; - return MinecraftProfile(); - } - auto capeObj = capeV.toObject(); - auto idV = capeObj.value("id"); - auto urlV = capeObj.value("url"); - auto aliasV = capeObj.value("alias"); - if(!idV.isString() || !urlV.isString() || !aliasV.isString()) { - qWarning() << "mandatory skin attributes are missing or of unexpected type"; + { + auto capesV = tokenObject.value("capes"); + if(!capesV.isArray()) { + qWarning() << "capes is not an array!"; return MinecraftProfile(); } - Cape cape; - cape.id = idV.toString(); - cape.url = urlV.toString(); - cape.alias = aliasV.toString(); - - // data for cape is optional. - auto dataV = capeObj.value("data"); - if(dataV.isString()) { - // TODO: validate base64 - cape.data = QByteArray::fromBase64(dataV.toString().toLatin1()); + auto capesArray = capesV.toArray(); + for(auto capeV: capesArray) { + if(!capeV.isObject()) { + qWarning() << "cape is not an object!"; + return MinecraftProfile(); + } + auto capeObj = capeV.toObject(); + auto idV = capeObj.value("id"); + auto urlV = capeObj.value("url"); + auto aliasV = capeObj.value("alias"); + if(!idV.isString() || !urlV.isString() || !aliasV.isString()) { + qWarning() << "mandatory skin attributes are missing or of unexpected type"; + return MinecraftProfile(); + } + Cape cape; + cape.id = idV.toString(); + cape.url = urlV.toString(); + cape.alias = aliasV.toString(); + + // data for cape is optional. + auto dataV = capeObj.value("data"); + if(dataV.isString()) { + // TODO: validate base64 + cape.data = QByteArray::fromBase64(dataV.toString().toLatin1()); + } + else if (!dataV.isUndefined()) { + qWarning() << "cape data is something unexpected"; + return MinecraftProfile(); + } + out.capes[cape.id] = cape; } - else if (!dataV.isUndefined()) { - qWarning() << "cape data is something unexpected"; - return MinecraftProfile(); + } + // current cape + { + auto capeV = tokenObject.value("cape"); + if(capeV.isString()) { + auto currentCape = capeV.toString(); + if(out.capes.contains(currentCape)) { + out.currentCape = currentCape; + } } - out.capes.push_back(cape); } out.validity = Katabasis::Validity::Assumed; return out; diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index b2d09cb0..cf58fb76 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -25,8 +25,8 @@ struct MinecraftProfile { QString id; QString name; Skin skin; - int currentCape = -1; - QVector<Cape> capes; + QString currentCape; + QMap<QString, Cape> capes; Katabasis::Validity validity = Katabasis::Validity::None; }; diff --git a/launcher/minecraft/auth/AccountTask.h b/launcher/minecraft/auth/AccountTask.h index 3f08096f..fc3488eb 100644 --- a/launcher/minecraft/auth/AccountTask.h +++ b/launcher/minecraft/auth/AccountTask.h @@ -83,6 +83,10 @@ public: return m_accountState; } +signals: + void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn); + void hideVerificationUriAndCode(); + protected: /** diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp index 859060d6..ecd7e310 100644 --- a/launcher/minecraft/auth/flows/AuthContext.cpp +++ b/launcher/minecraft/auth/flows/AuthContext.cpp @@ -43,7 +43,7 @@ void AuthContext::finishActivity() { throw 0; } m_activity = Katabasis::Activity::Idle; - m_stage = MSAStage::Idle; + setStage(AuthStage::Complete); m_data->validity_ = m_data->minecraftProfile.validity; emit activityChanged(m_activity); } @@ -55,16 +55,16 @@ void AuthContext::initMSA() { Katabasis::OAuth2::Options opts; opts.scope = "XboxLive.signin offline_access"; opts.clientIdentifier = BuildConfig.MSA_CLIENT_ID; - opts.authorizationUrl = "https://login.live.com/oauth20_authorize.srf"; - opts.accessTokenUrl = "https://login.live.com/oauth20_token.srf"; + opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"; + opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; opts.listenerPorts = {28562, 28563, 28564, 28565, 28566}; m_oauth2 = new OAuth2(opts, m_data->msaToken, this, mgr); + m_oauth2->setGrantFlow(Katabasis::OAuth2::GrantFlowDevice); connect(m_oauth2, &OAuth2::linkingFailed, this, &AuthContext::onOAuthLinkingFailed); connect(m_oauth2, &OAuth2::linkingSucceeded, this, &AuthContext::onOAuthLinkingSucceeded); - connect(m_oauth2, &OAuth2::openBrowser, this, &AuthContext::onOpenBrowser); - connect(m_oauth2, &OAuth2::closeBrowser, this, &AuthContext::onCloseBrowser); + connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode); connect(m_oauth2, &OAuth2::activityChanged, this, &AuthContext::onOAuthActivityChanged); } @@ -106,20 +106,14 @@ bool AuthContext::signOut() { } */ -void AuthContext::onOpenBrowser(const QUrl &url) { - QDesktopServices::openUrl(url); -} - -void AuthContext::onCloseBrowser() { - -} - void AuthContext::onOAuthLinkingFailed() { + emit hideVerificationUriAndCode(); finishActivity(); changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed.")); } void AuthContext::onOAuthLinkingSucceeded() { + emit hideVerificationUriAndCode(); auto *o2t = qobject_cast<OAuth2 *>(sender()); if (!o2t->linked()) { finishActivity(); @@ -127,12 +121,14 @@ void AuthContext::onOAuthLinkingSucceeded() { return; } QVariantMap extraTokens = o2t->extraTokens(); +#ifndef NDEBUG if (!extraTokens.isEmpty()) { qDebug() << "Extra tokens in response:"; foreach (QString key, extraTokens.keys()) { qDebug() << "\t" << key << ":" << extraTokens.value(key); } } +#endif doUserAuth(); } @@ -141,7 +137,7 @@ void AuthContext::onOAuthActivityChanged(Katabasis::Activity activity) { } void AuthContext::doUserAuth() { - m_stage = MSAStage::UserAuth; + setStage(AuthStage::UserAuth); changeState(STATE_WORKING, tr("Starting user authentication")); QString xbox_auth_template = R"XXX( @@ -219,35 +215,34 @@ bool getNumber(QJsonValue value, double & out) { // 2148916238 = child account not linked to a family */ -bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output) { +bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, const char * name) { + qDebug() << "Parsing" << name <<":"; +#ifndef NDEBUG + qDebug() << data; +#endif QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if(jsonError.error) { qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); - qDebug() << data; return false; } auto obj = doc.object(); if(!getDateTime(obj.value("IssueInstant"), output.issueInstant)) { qWarning() << "User IssueInstant is not a timestamp"; - qDebug() << data; return false; } if(!getDateTime(obj.value("NotAfter"), output.notAfter)) { qWarning() << "User NotAfter is not a timestamp"; - qDebug() << data; return false; } if(!getString(obj.value("Token"), output.token)) { qWarning() << "User Token is not a timestamp"; - qDebug() << data; return false; } auto arrayVal = obj.value("DisplayClaims").toObject().value("xui"); if(!arrayVal.isArray()) { qWarning() << "Missing xui claims array"; - qDebug() << data; return false; } bool foundUHS = false; @@ -266,7 +261,6 @@ bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output) { QString claim; if(!getString(obj.value(iter.key()), claim)) { qWarning() << "display claim " << iter.key() << " is not a string..."; - qDebug() << data; return false; } output.extra[iter.key()] = claim; @@ -276,11 +270,10 @@ bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output) { } if(!foundUHS) { qWarning() << "Missing uhs"; - qDebug() << data; return false; } output.validity = Katabasis::Validity::Certain; - qDebug() << data; + qDebug() << name << "is valid."; return true; } @@ -300,7 +293,7 @@ void AuthContext::onUserAuthDone( } Katabasis::Token temp; - if(!parseXTokenResponse(replyData, temp)) { + if(!parseXTokenResponse(replyData, temp, "UToken")) { qWarning() << "Could not parse user authentication response..."; finishActivity(); changeState(STATE_FAILED_HARD, tr("XBox user authentication response could not be understood.")); @@ -308,7 +301,7 @@ void AuthContext::onUserAuthDone( } m_data->userToken = temp; - m_stage = MSAStage::XboxAuth; + setStage(AuthStage::XboxAuth); changeState(STATE_WORKING, tr("Starting XBox authentication")); doSTSAuthMinecraft(); @@ -349,7 +342,7 @@ void AuthContext::doSTSAuthMinecraft() { connect(requestor, &Requestor::finished, this, &AuthContext::onSTSAuthMinecraftDone); requestor->post(request, xbox_auth_data.toUtf8()); - qDebug() << "Second layer of XBox auth ... commencing."; + qDebug() << "Getting Minecraft services STS token..."; } void AuthContext::onSTSAuthMinecraftDone( @@ -365,7 +358,7 @@ void AuthContext::onSTSAuthMinecraftDone( } Katabasis::Token temp; - if(!parseXTokenResponse(replyData, temp)) { + if(!parseXTokenResponse(replyData, temp, "STSAuthMinecraft")) { qWarning() << "Could not parse authorization response for access to mojang services..."; m_requestsDone ++; return; @@ -405,7 +398,7 @@ void AuthContext::doSTSAuthGeneric() { connect(requestor, &Requestor::finished, this, &AuthContext::onSTSAuthGenericDone); requestor->post(request, xbox_auth_data.toUtf8()); - qDebug() << "Second layer of XBox auth ... commencing."; + qDebug() << "Getting generic STS token..."; } void AuthContext::onSTSAuthGenericDone( @@ -421,7 +414,7 @@ void AuthContext::onSTSAuthGenericDone( } Katabasis::Token temp; - if(!parseXTokenResponse(replyData, temp)) { + if(!parseXTokenResponse(replyData, temp, "STSAuthGaneric")) { qWarning() << "Could not parse authorization response for access to xbox API..."; m_requestsDone ++; return; @@ -461,10 +454,13 @@ void AuthContext::doMinecraftAuth() { namespace { bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) { QJsonParseError jsonError; + qDebug() << "Parsing Mojang response..."; +#ifndef NDEBUG + qDebug() << data; +#endif QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if(jsonError.error) { - qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); - qDebug() << data; + qWarning() << "Failed to parse response from api.minecraftservices.com/authentication/login_with_xbox as JSON: " << jsonError.errorString(); return false; } @@ -472,7 +468,6 @@ bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) { double expires_in = 0; if(!getNumber(obj.value("expires_in"), expires_in)) { qWarning() << "expires_in is not a valid number"; - qDebug() << data; return false; } auto currentTime = QDateTime::currentDateTimeUtc(); @@ -482,18 +477,16 @@ bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) { QString username; if(!getString(obj.value("username"), username)) { qWarning() << "username is not valid"; - qDebug() << data; return false; } // TODO: it's a JWT... validate it? if(!getString(obj.value("access_token"), output.token)) { qWarning() << "access_token is not valid"; - qDebug() << data; return false; } output.validity = Katabasis::Validity::Certain; - qDebug() << data; + qDebug() << "Mojang response is valid."; return true; } } @@ -508,13 +501,17 @@ void AuthContext::onMinecraftAuthDone( if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; +#ifndef NDEBUG qDebug() << replyData; +#endif return; } if(!parseMojangResponse(replyData, m_data->yggdrasilToken)) { qWarning() << "Could not parse login_with_xbox response..."; +#ifndef NDEBUG qDebug() << replyData; +#endif return; } m_mcAuthSucceeded = true; @@ -558,18 +555,24 @@ void AuthContext::onXBoxProfileDone( if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; +#ifndef NDEBUG qDebug() << replyData; +#endif return; } +#ifndef NDEBUG qDebug() << "XBox profile: " << replyData; +#endif m_xboxProfileSucceeded = true; checkResult(); } void AuthContext::checkResult() { + qDebug() << "AuthContext::checkResult called"; if(m_requestsDone != 2) { + qDebug() << "Number of ready results:" << m_requestsDone; return; } if(m_mcAuthSucceeded && m_xboxProfileSucceeded) { @@ -583,24 +586,26 @@ void AuthContext::checkResult() { namespace { bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { + qDebug() << "Parsing Minecraft profile..."; +#ifndef NDEBUG + qDebug() << data; +#endif + QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if(jsonError.error) { qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); - qDebug() << data; return false; } auto obj = doc.object(); if(!getString(obj.value("id"), output.id)) { - qWarning() << "minecraft profile id is not a string"; - qDebug() << data; + qWarning() << "Minecraft profile id is not a string"; return false; } if(!getString(obj.value("name"), output.name)) { - qWarning() << "minecraft profile name is not a string"; - qDebug() << data; + qWarning() << "Minecraft profile name is not a string"; return false; } @@ -629,10 +634,9 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { break; } auto capesArray = obj.value("capes").toArray(); - int i = -1; - int currentCape = -1; + + QString currentCape; for(auto cape: capesArray) { - i++; auto capeObj = cape.toObject(); Cape capeOut; if(!getString(capeObj.value("id"), capeOut.id)) { @@ -643,7 +647,7 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { continue; } if(state == "ACTIVE") { - currentCape = i; + currentCape = capeOut.id; } if(!getString(capeObj.value("url"), capeOut.url)) { continue; @@ -652,8 +656,7 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { continue; } - // we deal with only the active skin - output.capes.push_back(capeOut); + output.capes[capeOut.id] = capeOut; } output.currentCape = currentCape; output.validity = Katabasis::Validity::Certain; @@ -662,7 +665,7 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { } void AuthContext::doMinecraftProfile() { - m_stage = MSAStage::MinecraftProfile; + setStage(AuthStage::MinecraftProfile); changeState(STATE_WORKING, tr("Starting minecraft profile acquisition")); auto url = QUrl("https://api.minecraftservices.com/minecraft/profile"); @@ -683,25 +686,25 @@ void AuthContext::onMinecraftProfileDone(int, QNetworkReply::NetworkError error, if (error == QNetworkReply::ContentNotFoundError) { m_data->minecraftProfile = MinecraftProfile(); finishActivity(); - changeState(STATE_FAILED_HARD, tr("Account is missing a profile")); + changeState(STATE_FAILED_HARD, tr("Account is missing a Minecraft Java profile.\n\nWhile the Microsoft account is valid, it does not own the game.\n\nYou might own Bedrock on this account, but that does not give you access to Java currently.")); return; } if (error != QNetworkReply::NoError) { finishActivity(); - changeState(STATE_FAILED_HARD, tr("Profile acquisition failed")); + changeState(STATE_FAILED_HARD, tr("Minecraft Java profile acquisition failed.")); return; } if(!parseMinecraftProfile(data, m_data->minecraftProfile)) { m_data->minecraftProfile = MinecraftProfile(); finishActivity(); - changeState(STATE_FAILED_HARD, tr("Profile response could not be parsed")); + changeState(STATE_FAILED_HARD, tr("Minecraft Java profile response could not be parsed")); return; } doGetSkin(); } void AuthContext::doGetSkin() { - m_stage = MSAStage::Skin; + setStage(AuthStage::Skin); changeState(STATE_WORKING, tr("Fetching player skin")); auto url = QUrl(m_data->minecraftProfile.skin.url); @@ -721,12 +724,18 @@ void AuthContext::onSkinDone(int, QNetworkReply::NetworkError error, QByteArray changeState(STATE_SUCCEEDED, tr("Finished all authentication steps")); } +void AuthContext::setStage(AuthContext::AuthStage stage) { + m_stage = stage; + emit progress((int)m_stage, (int)AuthStage::Complete); +} + + QString AuthContext::getStateMessage() const { switch (m_accountState) { case STATE_WORKING: switch(m_stage) { - case MSAStage::Idle: { + case AuthStage::Initial: { QString loginMessage = tr("Logging in as %1 user"); if(m_data->type == AccountType::MSA) { return loginMessage.arg("Microsoft"); @@ -735,14 +744,16 @@ QString AuthContext::getStateMessage() const { return loginMessage.arg("Mojang"); } } - case MSAStage::UserAuth: + case AuthStage::UserAuth: return tr("Logging in as XBox user"); - case MSAStage::XboxAuth: + case AuthStage::XboxAuth: return tr("Logging in with XBox and Mojang services"); - case MSAStage::MinecraftProfile: + case AuthStage::MinecraftProfile: return tr("Getting Minecraft profile"); - case MSAStage::Skin: + case AuthStage::Skin: return tr("Getting Minecraft skin"); + case AuthStage::Complete: + return tr("Finished"); default: break; } diff --git a/launcher/minecraft/auth/flows/AuthContext.h b/launcher/minecraft/auth/flows/AuthContext.h index 5f99dba3..1d9f8f72 100644 --- a/launcher/minecraft/auth/flows/AuthContext.h +++ b/launcher/minecraft/auth/flows/AuthContext.h @@ -36,8 +36,7 @@ private slots: // OAuth-specific callbacks void onOAuthLinkingSucceeded(); void onOAuthLinkingFailed(); - void onOpenBrowser(const QUrl &url); - void onCloseBrowser(); + void onOAuthActivityChanged(Katabasis::Activity activity); // Yggdrasil specific callbacks @@ -82,13 +81,16 @@ protected: bool m_xboxProfileSucceeded = false; bool m_mcAuthSucceeded = false; Katabasis::Activity m_activity = Katabasis::Activity::Idle; - enum class MSAStage { - Idle, + enum class AuthStage { + Initial, UserAuth, XboxAuth, MinecraftProfile, - Skin - } m_stage = MSAStage::Idle; + Skin, + Complete + } m_stage = AuthStage::Initial; + + void setStage(AuthStage stage); QNetworkAccessManager *mgr = nullptr; }; diff --git a/launcher/minecraft/services/CapeChange.cpp b/launcher/minecraft/services/CapeChange.cpp new file mode 100644 index 00000000..c1d88d14 --- /dev/null +++ b/launcher/minecraft/services/CapeChange.cpp @@ -0,0 +1,67 @@ +#include "CapeChange.h" +#include <QNetworkRequest> +#include <QHttpMultiPart> +#include <Env.h> + +CapeChange::CapeChange(QObject *parent, AuthSessionPtr session, QString cape) + : Task(parent), m_capeId(cape), m_session(session) +{ +} + +void CapeChange::setCape(QString& cape) { + QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active")); + auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit()); + QNetworkReply *rep = ENV.qnam().put(request, requestString.toUtf8()); + + setStatus(tr("Equipping cape")); + + m_reply = std::shared_ptr<QNetworkReply>(rep); + connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); +} + +void CapeChange::clearCape() { + QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active")); + auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit()); + QNetworkReply *rep = ENV.qnam().deleteResource(request); + + setStatus(tr("Removing cape")); + + m_reply = std::shared_ptr<QNetworkReply>(rep); + connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); +} + + +void CapeChange::executeTask() +{ + if(m_capeId.isEmpty()) { + clearCape(); + } + else { + setCape(m_capeId); + } +} + +void CapeChange::downloadError(QNetworkReply::NetworkError error) +{ + // error happened during download. + qCritical() << "Network error: " << error; + emitFailed(m_reply->errorString()); +} + +void CapeChange::downloadFinished() +{ + // if the download failed + if (m_reply->error() != QNetworkReply::NetworkError::NoError) + { + emitFailed(QString("Network error: %1").arg(m_reply->errorString())); + m_reply.reset(); + return; + } + emitSucceeded(); +} diff --git a/launcher/minecraft/services/CapeChange.h b/launcher/minecraft/services/CapeChange.h new file mode 100644 index 00000000..1b6f2f72 --- /dev/null +++ b/launcher/minecraft/services/CapeChange.h @@ -0,0 +1,32 @@ +#pragma once + +#include <QFile> +#include <QtNetwork/QtNetwork> +#include <memory> +#include <minecraft/auth/AuthSession.h> +#include "tasks/Task.h" + +class CapeChange : public Task +{ + Q_OBJECT +public: + CapeChange(QObject *parent, AuthSessionPtr session, QString capeId); + virtual ~CapeChange() {} + +private: + void setCape(QString & cape); + void clearCape(); + +private: + QString m_capeId; + AuthSessionPtr m_session; + std::shared_ptr<QNetworkReply> m_reply; + +protected: + virtual void executeTask(); + +public slots: + void downloadError(QNetworkReply::NetworkError); + void downloadFinished(); +}; + diff --git a/libraries/katabasis/include/katabasis/OAuth2.h b/libraries/katabasis/include/katabasis/OAuth2.h index 4361691c..9dbe5c71 100644 --- a/libraries/katabasis/include/katabasis/OAuth2.h +++ b/libraries/katabasis/include/katabasis/OAuth2.h @@ -140,7 +140,7 @@ signals: void closeBrowser(); /// Emitted when client needs to show a verification uri and user code - void showVerificationUriAndCode(const QUrl &uri, const QString &code); + void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn); /// Emitted when authentication/deauthentication succeeded. void linkingSucceeded(); @@ -181,7 +181,7 @@ protected: void setExpires(QDateTime v); /// Start polling authorization server - void startPollServer(const QVariantMap ¶ms); + void startPollServer(const QVariantMap ¶ms, int expiresIn); /// Set authentication token. void setToken(const QString &v); diff --git a/libraries/katabasis/include/katabasis/Requestor.h b/libraries/katabasis/include/katabasis/Requestor.h index 61437f76..4bc0256a 100644 --- a/libraries/katabasis/include/katabasis/Requestor.h +++ b/libraries/katabasis/include/katabasis/Requestor.h @@ -80,6 +80,9 @@ protected slots: /// Handle request error. void onRequestError(QNetworkReply::NetworkError error); + /// Handle ssl errors. + void onSslErrors(QList<QSslError> errors); + /// Re-try request (after successful token refresh). void retry(); diff --git a/libraries/katabasis/src/OAuth2.cpp b/libraries/katabasis/src/OAuth2.cpp index 6cc03a0d..9756d377 100644 --- a/libraries/katabasis/src/OAuth2.cpp +++ b/libraries/katabasis/src/OAuth2.cpp @@ -472,16 +472,8 @@ void OAuth2::setExpires(QDateTime v) { token_.notAfter = v; } -void OAuth2::startPollServer(const QVariantMap ¶ms) +void OAuth2::startPollServer(const QVariantMap ¶ms, int expiresIn) { - bool ok = false; - int expiresIn = params[OAUTH2_EXPIRES_IN].toInt(&ok); - if (!ok) { - qWarning() << "OAuth2::startPollServer: No expired_in parameter"; - emit linkingFailed(); - return; - } - qDebug() << "OAuth2::startPollServer: device_ and user_code expires in" << expiresIn << "seconds"; QUrl url(options_.accessTokenUrl); @@ -502,6 +494,7 @@ void OAuth2::startPollServer(const QVariantMap ¶ms) PollServer * pollServer = new PollServer(manager_, authRequest, payload, expiresIn, this); if (params.contains(OAUTH2_INTERVAL)) { + bool ok = false; int interval = params[OAUTH2_INTERVAL].toInt(&ok); if (ok) pollServer->setInterval(interval); @@ -516,7 +509,9 @@ QString OAuth2::refreshToken() { return token_.refresh_token; } void OAuth2::setRefreshToken(const QString &v) { +#ifndef NDEBUG qDebug() << "OAuth2::setRefreshToken" << v << "..."; +#endif token_.refresh_token = v; } @@ -573,7 +568,7 @@ void OAuth2::onRefreshFinished() { setLinked(true); emit linkingSucceeded(); emit refreshFinished(QNetworkReply::NoError); - qDebug() << " New token expires in" << expires() << "seconds"; + qDebug() << "New token expires in" << expires() << "seconds"; } else { qDebug() << "OAuth2::onRefreshFinished: Error" << (int)refreshReply->error() << refreshReply->errorString(); } @@ -627,9 +622,17 @@ void OAuth2::onDeviceAuthReplyFinished() if (params.contains(OAUTH2_VERIFICATION_URI_COMPLETE)) emit openBrowser(params.take(OAUTH2_VERIFICATION_URI_COMPLETE).toUrl()); - emit showVerificationUriAndCode(uri, userCode); + bool ok = false; + int expiresIn = params[OAUTH2_EXPIRES_IN].toInt(&ok); + if (!ok) { + qWarning() << "OAuth2::startPollServer: No expired_in parameter"; + emit linkingFailed(); + return; + } + + emit showVerificationUriAndCode(uri, userCode, expiresIn); - startPollServer(params); + startPollServer(params, expiresIn); } else { qWarning() << "OAuth2::onDeviceAuthReplyFinished: Mandatory parameters missing from response"; emit linkingFailed(); diff --git a/libraries/katabasis/src/Requestor.cpp b/libraries/katabasis/src/Requestor.cpp index 7b6d2679..917f2c07 100644 --- a/libraries/katabasis/src/Requestor.cpp +++ b/libraries/katabasis/src/Requestor.cpp @@ -40,6 +40,7 @@ int Requestor::get(const QNetworkRequest &req, int timeout/* = 60*1000*/) { timedReplies_.add(new Reply(reply_, timeout)); connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); + connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); return id_; } @@ -53,6 +54,7 @@ int Requestor::post(const QNetworkRequest &req, const QByteArray &data, int time timedReplies_.add(new Reply(reply_, timeout)); connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); + connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); return id_; } @@ -69,6 +71,7 @@ int Requestor::post(const QNetworkRequest & req, QHttpMultiPart* data, int timeo timedReplies_.add(new Reply(reply_, timeout)); connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); + connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); return id_; } @@ -83,6 +86,7 @@ int Requestor::put(const QNetworkRequest &req, const QByteArray &data, int timeo timedReplies_.add(new Reply(reply_, timeout)); connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); + connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); return id_; } @@ -99,6 +103,7 @@ int Requestor::put(const QNetworkRequest & req, QHttpMultiPart* data, int timeou timedReplies_.add(new Reply(reply_, timeout)); connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); + connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); return id_; } @@ -118,6 +123,7 @@ int Requestor::customRequest(const QNetworkRequest &req, const QByteArray &verb, timedReplies_.add(new Reply(reply_)); connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); + connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); return id_; } @@ -131,6 +137,7 @@ int Requestor::head(const QNetworkRequest &req, int timeout/* = 60*1000*/) timedReplies_.add(new Reply(reply_, timeout)); connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); + connect(reply_, &QNetworkReply::sslErrors, this, &Requestor::onSslErrors); return id_; } @@ -167,6 +174,7 @@ void Requestor::onRequestError(QNetworkReply::NetworkError error) { if (reply_ != qobject_cast<QNetworkReply *>(sender())) { return; } + qWarning() << "O2Requestor::onRequestError: Error string: " << reply_->errorString(); int httpStatus = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); qWarning() << "O2Requestor::onRequestError: HTTP status" << httpStatus << reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); if ((status_ == Requesting) && (httpStatus == 401)) { @@ -180,6 +188,16 @@ void Requestor::onRequestError(QNetworkReply::NetworkError error) { QTimer::singleShot(10, this, SLOT(finish())); } +void Requestor::onSslErrors(QList<QSslError> errors) { + int i = 1; + for (auto error : errors) { + qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString(); + auto cert = error.certificate(); + qCritical() << "Certificate in question:\n" << cert.toText(); + i++; + } +} + void Requestor::onUploadProgress(qint64 uploaded, qint64 total) { if (status_ == Idle) { qWarning() << "O2Requestor::onUploadProgress: No pending request"; |