From ff8057d34bb4e9cf12f5d39b5c1b79ac092b387f Mon Sep 17 00:00:00 2001 From: Tannin Date: Sun, 9 Aug 2015 12:40:57 +0200 Subject: - MO now validates session cookie on startup - It also retrieves account status - rewrote how MO decides when to log-in/when to give up on logging in --- src/main.cpp | 3 + src/nexusinterface.cpp | 14 +--- src/nxmaccessmanager.cpp | 171 +++++++++++++++++++++++++++++++++++++++-------- src/nxmaccessmanager.h | 40 ++++++++--- src/organizercore.cpp | 51 +++++++++----- src/organizercore.h | 2 +- 6 files changed, 215 insertions(+), 66 deletions(-) (limited to 'src') diff --git a/src/main.cpp b/src/main.cpp index ae3bbb4c..8e9b7c9e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -62,6 +62,7 @@ along with Mod Organizer. If not, see . #include "selectiondialog.h" #include "moapplication.h" #include "tutorialmanager.h" +#include "nxmaccessmanager.h" #include #include #include @@ -534,6 +535,8 @@ int main(int argc, char *argv[]) } } + NexusInterface::instance()->getAccessManager()->startLoginCheck(); + qDebug("initializing tutorials"); TutorialManager::init(qApp->applicationDirPath() + "/" + QString::fromStdWString(AppConfig::tutorialsPath()) + "/", &organizer); diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 5e29e8ee..dc027be4 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -391,8 +391,7 @@ void NexusInterface::nextRequest() return; } - if (!getAccessManager()->loggedIn() - && requiresLogin(m_RequestQueue.head())) { + if (requiresLogin(m_RequestQueue.head()) && !getAccessManager()->loggedIn()) { if (!getAccessManager()->loginAttempted()) { emit needLogin(); return; @@ -438,16 +437,7 @@ void NexusInterface::nextRequest() } QNetworkRequest request(url); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/xml"); - QStringList comments; - comments << "compatible to Nexus Client v" + m_NMMVersion; - if (!info.m_SubModule.isEmpty()) { - comments << "module: " + info.m_SubModule; - } - - QString userAgent = QString("Mod Organizer v%1 (%2)") - .arg(m_MOVersion.displayString()) - .arg(comments.join("; ")); - request.setRawHeader("User-Agent", userAgent.toUtf8()); + request.setRawHeader("User-Agent", m_AccessManager->userAgent(info.m_SubModule).toUtf8()); info.m_Reply = m_AccessManager->get(request); diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp index c99d2d8d..0786a254 100644 --- a/src/nxmaccessmanager.cpp +++ b/src/nxmaccessmanager.cpp @@ -24,6 +24,8 @@ along with Mod Organizer. If not, see . #include "selfupdater.h" #include "persistentcookiejar.h" #include "settings.h" +#include +#include #include #include #include @@ -32,24 +34,29 @@ along with Mod Organizer. If not, see . #include #include #include -#include - -#if QT_VERSION >= QT_VERSION_CHECK(5,0,0) #include -#endif +#include +#include +#include + using namespace MOBase; using namespace MOShared; +// unfortunately Nexus doesn't seem to document these states, all I know is all these listed +// are considered premium (27 should be lifetime premium) +const std::set NXMAccessManager::s_PremiumAccountStates { 4, 6, 13, 27, 31, 32 }; + + NXMAccessManager::NXMAccessManager(QObject *parent, const QString &moVersion) : QNetworkAccessManager(parent) , m_LoginReply(nullptr) , m_ProgressDialog() , m_MOVersion(moVersion) - , m_LoginAttempted(false) { - + m_LoginTimeout.setSingleShot(true); + m_LoginTimeout.setInterval(30000); setCookieJar(new PersistentCookieJar( QDir::fromNativeSeparators(Settings::instance().getCacheDirectory() + "/nexus_cookies.dat"))); } @@ -88,20 +95,96 @@ QNetworkReply *NXMAccessManager::createRequest( } -void NXMAccessManager::showCookies() +void NXMAccessManager::showCookies() const { - QList cookies = cookieJar()->cookiesForUrl(QUrl(ToQString(GameInfo::instance().getNexusPage()) + "/")); - foreach (QNetworkCookie cookie, cookies) { - qDebug("%s - %s", cookie.name().constData(), cookie.value().constData()); + QUrl url(ToQString(GameInfo::instance().getNexusPage()) + "/"); + + for (const QNetworkCookie &cookie : cookieJar()->cookiesForUrl(url)) { + qDebug("%s - %s (expires: %s)", + cookie.name().constData(), cookie.value().constData(), + qPrintable(cookie.expirationDate().toString())); + } +} + + +void NXMAccessManager::startLoginCheck() +{ + if (hasLoginCookies()) { + QNetworkRequest request(ToQString(GameInfo::instance().getNexusPage()) + "/Sessions/?Validate"); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + request.setRawHeader("User-Agent", userAgent().toUtf8()); + + m_LoginReply = get(request); + m_LoginTimeout.start(); + m_LoginState = LOGIN_CHECKING; + connect(m_LoginReply, SIGNAL(finished()), this, SLOT(loginChecked())); + connect(m_LoginReply, SIGNAL(error(QNetworkReply::NetworkError)), + this, SLOT(loginError(QNetworkReply::NetworkError))); } } +void NXMAccessManager::retrieveCredentials() +{ + QNetworkRequest request(ToQString(GameInfo::instance().getNexusPage()) + + QString("/Core/Libs/Flamework/Entities/User?GetCredentials&game_id=%1" + ).arg(GameInfo::instance().getNexusGameID())); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + request.setRawHeader("User-Agent", userAgent().toUtf8()); + + QNetworkReply *reply = get(request); + QTimer timeout; + connect(&timeout, &QTimer::timeout, [reply] () { + reply->deleteLater(); + }); + timeout.start(); + + connect(reply, &QNetworkReply::finished, [reply, this] () { + QJsonDocument jdoc = QJsonDocument::fromJson(reply->readAll()); + QJsonArray credentialsData = jdoc.array(); + emit credentialsReceived(credentialsData.at(2).toString(), + s_PremiumAccountStates.find(credentialsData.at(1).toInt()) + != s_PremiumAccountStates.end()); + reply->deleteLater(); + }); + + connect(reply, static_cast(&QNetworkReply::error), + [=] (QNetworkReply::NetworkError) { + qDebug("failed to retrieve account credentials: %s", qPrintable(reply->errorString())); + reply->deleteLater(); + }); +} + + bool NXMAccessManager::loggedIn() const { - return hasLoginCookies(); + if (m_LoginState == LOGIN_CHECKING) { + QProgressDialog progress; + progress.setLabelText(tr("Verifying Nexus login")); + progress.show(); + while (m_LoginState == LOGIN_CHECKING) { + QCoreApplication::processEvents(); + QThread::msleep(100); + } + progress.hide(); + } + + return m_LoginState == LOGIN_VALID; +} + + +void NXMAccessManager::refuseLogin() +{ + m_LoginState = LOGIN_REFUSED; +} + + +bool NXMAccessManager::loginAttempted() const +{ + return m_LoginState != LOGIN_NOT_CHECKED; } + bool NXMAccessManager::loginWaiting() const { return m_LoginReply != nullptr; @@ -114,19 +197,29 @@ void NXMAccessManager::login(const QString &username, const QString &password) return; } - if (hasLoginCookies()) { + if (m_LoginState == LOGIN_VALID) { emit loginSuccessful(false); return; } - m_LoginAttempted = true; - m_Username = username; m_Password = password; pageLogin(); } +QString NXMAccessManager::userAgent(const QString &subModule) const +{ + QStringList comments; + comments << "compatible to Nexus Client v" + m_NMMVersion; + if (!subModule.isEmpty()) { + comments << "module: " + subModule; + } + + return QString("Mod Organizer v%1 (%2)").arg(m_MOVersion, comments.join("; ")); +} + + void NXMAccessManager::pageLogin() { QString requestString = (ToQString(GameInfo::instance().getNexusPage()) + "/Sessions/?Login&uri=%1") @@ -148,8 +241,7 @@ void NXMAccessManager::pageLogin() postDataQuery = postData.encodedQuery(); #endif - QString userAgent = QString("Mod Organizer v%1 (compatible to Nexus Client v%2)").arg(m_MOVersion).arg(m_NMMVersion); - request.setRawHeader("User-Agent", userAgent.toUtf8()); + request.setRawHeader("User-Agent", userAgent().toUtf8()); m_ProgressDialog.setLabelText(tr("Logging into Nexus")); QList buttons = m_ProgressDialog.findChildren(); @@ -159,6 +251,7 @@ void NXMAccessManager::pageLogin() m_LoginReply = post(request, postDataQuery); m_LoginTimeout.start(); + m_LoginState = LOGIN_CHECKING; connect(m_LoginReply, SIGNAL(finished()), this, SLOT(loginFinished())); connect(m_LoginReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(loginError(QNetworkReply::NetworkError))); } @@ -166,37 +259,41 @@ void NXMAccessManager::pageLogin() void NXMAccessManager::loginTimeout() { - emit loginFailed(tr("timeout")); m_LoginReply->deleteLater(); m_LoginReply = nullptr; m_LoginAttempted = false; // this usually means we might have success later - m_LoginTimeout.stop(); m_Username.clear(); m_Password.clear(); + m_LoginState = LOGIN_NOT_VALID; + + emit loginFailed(tr("timeout")); } void NXMAccessManager::loginError(QNetworkReply::NetworkError) { + qDebug("login error"); m_ProgressDialog.hide(); - m_LoginTimeout.stop(); + m_Username.clear(); + m_Password.clear(); + m_LoginState = LOGIN_NOT_VALID; + if (m_LoginReply != nullptr) { - emit loginFailed(m_LoginReply->errorString()); m_LoginReply->deleteLater(); m_LoginReply = nullptr; + emit loginFailed(m_LoginReply->errorString()); } else { emit loginFailed(tr("Unknown error")); } - m_Username.clear(); - m_Password.clear(); } bool NXMAccessManager::hasLoginCookies() const { bool sidCookie = false; - QList cookies = cookieJar()->cookiesForUrl(QUrl(ToQString(GameInfo::instance().getNexusPage()) + "/")); - foreach (QNetworkCookie cookie, cookies) { + QUrl url(ToQString(GameInfo::instance().getNexusPage()) + "/"); + QList cookies = cookieJar()->cookiesForUrl(url); + for (const QNetworkCookie &cookie : cookies) { if (cookie.name() == "sid") { sidCookie = true; } @@ -208,15 +305,35 @@ bool NXMAccessManager::hasLoginCookies() const void NXMAccessManager::loginFinished() { m_ProgressDialog.hide(); + + m_LoginReply->deleteLater(); + m_LoginReply = nullptr; + m_Username.clear(); + m_Password.clear(); + if (hasLoginCookies()) { + m_LoginState = LOGIN_VALID; + retrieveCredentials(); emit loginSuccessful(true); } else { + m_LoginState = LOGIN_NOT_VALID; emit loginFailed(tr("Please check your password")); } +} + - m_LoginTimeout.stop(); +void NXMAccessManager::loginChecked() +{ + QNetworkReply *reply = static_cast(sender()); + QByteArray data = reply->readAll(); + m_LoginState = data == "null" ? LOGIN_NOT_VALID + : LOGIN_VALID; + if (m_LoginState == LOGIN_VALID) { + retrieveCredentials(); + } else { + qDebug("cookies seem to be invalid"); + } m_LoginReply->deleteLater(); m_LoginReply = nullptr; - m_Username.clear(); - m_Password.clear(); } + diff --git a/src/nxmaccessmanager.h b/src/nxmaccessmanager.h index 9d692bba..2b615cdc 100644 --- a/src/nxmaccessmanager.h +++ b/src/nxmaccessmanager.h @@ -25,6 +25,7 @@ along with Mod Organizer. If not, see . #include #include #include +#include /** @@ -43,12 +44,18 @@ public: bool loggedIn() const; - bool loginAttempted() const { return m_LoginAttempted; } + bool loginAttempted() const; bool loginWaiting() const; void login(const QString &username, const QString &password); - void showCookies(); + void showCookies() const; + + QString userAgent(const QString &subModule = QString()) const; + + void startLoginCheck(); + + void refuseLogin(); signals: @@ -68,11 +75,14 @@ signals: void loginFailed(const QString &message); - private slots: + void credentialsReceived(const QString &userName, bool premium); + +private slots: - void loginFinished(); - void loginError(QNetworkReply::NetworkError errorCode); - void loginTimeout(); + void loginChecked(); + void loginFinished(); + void loginError(QNetworkReply::NetworkError errorCode); + void loginTimeout(); public slots: @@ -84,10 +94,16 @@ protected: private: - void pageLogin(); + void pageLogin(); // void dlLogin(); - bool hasLoginCookies() const; + bool hasLoginCookies() const; + + void retrieveCredentials(); + +private: + + static const std::set s_PremiumAccountStates; private: @@ -102,6 +118,14 @@ private: QString m_Password; bool m_LoginAttempted; + enum { + LOGIN_NOT_CHECKED, + LOGIN_CHECKING, + LOGIN_NOT_VALID, + LOGIN_ATTEMPT_FAILED, + LOGIN_REFUSED, + LOGIN_VALID + } m_LoginState = LOGIN_NOT_CHECKED; }; diff --git a/src/organizercore.cpp b/src/organizercore.cpp index 1a81d33b..50c95f0c 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -373,11 +373,13 @@ void OrganizerCore::setUserInterface(IUserInterface *userInterface, QWidget *wid m_InstallationManager.setParentWidget(widget); m_Updater.setUserInterface(widget); - // this currently wouldn't work reliably if the ui isn't initialized yet to display the result - if (isOnline() && !m_Settings.offlineMode()) { - m_Updater.testForUpdate(); - } else { - qDebug("user doesn't seem to be connected to the internet"); + if (userInterface != nullptr) { + // this currently wouldn't work reliably if the ui isn't initialized yet to display the result + if (isOnline() && !m_Settings.offlineMode()) { + m_Updater.testForUpdate(); + } else { + qDebug("user doesn't seem to be connected to the internet"); + } } } @@ -421,21 +423,28 @@ Settings &OrganizerCore::settings() return m_Settings; } -bool OrganizerCore::nexusLogin() +bool OrganizerCore::nexusLogin(bool retry) { - QString username, password; - NXMAccessManager *accessManager = NexusInterface::instance()->getAccessManager(); - if (!accessManager->loginAttempted() - && !accessManager->loggedIn() - && (m_Settings.getNexusLogin(username, password) - || (m_AskForNexusPW - && queryLogin(username, password)))) { - accessManager->login(username, password); - return true; - } else { + if ((accessManager->loginAttempted() + || accessManager->loggedIn()) + && !retry) { + // previous attempt, maybe even successful return false; + } else { + QString username, password; + if ((!retry && m_Settings.getNexusLogin(username, password)) + || (m_AskForNexusPW && queryLogin(username, password))) { + // credentials stored or user entered them manually + qDebug("attempt login with username %s", qPrintable(username)); + accessManager->login(username, password); + return true; + } else { + // no credentials stored and user didn't enter them + accessManager->refuseLogin(); + return false; + } } } @@ -1433,9 +1442,15 @@ void OrganizerCore::loginSuccessfulUpdate(bool necessary) void OrganizerCore::loginFailed(const QString &message) { + if (QMessageBox::question(qApp->activeWindow(), tr("Login failed"), tr("Login failed, try again?")) == QMessageBox::Yes) { + if (nexusLogin(true)) { + return; + } + } + if (!m_PendingDownloads.isEmpty()) { - MessageDialog::showMessage(tr("login failed: %1. Trying to download anyway").arg(message), qApp->activeWindow()); - foreach (QString url, m_PendingDownloads) { + MessageDialog::showMessage(tr("login failed: %1. Download will not be associated with an account").arg(message), qApp->activeWindow()); + for (QString url : m_PendingDownloads) { downloadRequestedNXM(url); } m_PendingDownloads.clear(); diff --git a/src/organizercore.h b/src/organizercore.h index d60ce2ab..014a4ab8 100644 --- a/src/organizercore.h +++ b/src/organizercore.h @@ -182,7 +182,7 @@ public slots: void requestDownload(const QUrl &url, QNetworkReply *reply); void downloadRequestedNXM(const QString &url); - bool nexusLogin(); + bool nexusLogin(bool retry = false); signals: -- cgit v1.3.1