/* Copyright (C) 2012 Sebastian Herbord. All rights reserved. This file is part of Mod Organizer. Mod Organizer 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, either version 3 of the License, or (at your option) any later version. Mod Organizer 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. You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ #include "nxmaccessmanager.h" #include "iplugingame.h" #include "nexusinterface.h" #include "nxmurl.h" #include "report.h" #include "utility.h" #include "selfupdater.h" #include "persistentcookiejar.h" #include "settings.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace MOBase; using namespace std::chrono_literals; const QString NexusBaseUrl("https://api.nexusmods.com/v1"); const std::chrono::seconds NXMAccessManager::ValidationTimeout = 10s; const QString NexusSSO("wss://sso.nexusmods.com"); const QString NexusSSOPage("https://www.nexusmods.com/sso?id=%1&application=modorganizer2"); ValidationProgressDialog::ValidationProgressDialog(std::chrono::seconds t) : m_timeout(t), m_bar(nullptr), m_buttons(nullptr), m_timer(nullptr), m_first(true) { m_bar = new QProgressBar; m_bar->setTextVisible(false); auto* label = new QLabel(tr("Validating Nexus Connection")); label->setAlignment(Qt::AlignHCenter); auto* vbox = new QVBoxLayout(this); vbox->addWidget(label); vbox->addWidget(m_bar); m_buttons = new QDialogButtonBox; m_buttons->addButton(tr("Hide"), QDialogButtonBox::RejectRole); connect(m_buttons, &QDialogButtonBox::clicked, [&](auto* b){ onButton(b); }); vbox->addWidget(m_buttons); } void ValidationProgressDialog::setParentWidget(QWidget* w) { const auto wasVisible = isVisible(); hide(); setParent(w, windowFlags() | Qt::Dialog); setModal(false); setVisible(wasVisible); } void ValidationProgressDialog::start() { if (!m_timer) { m_timer = new QTimer(this); connect(m_timer, &QTimer::timeout, [&]{ onTimer(); }); m_timer->setInterval(100ms); } m_bar->setRange(0, m_timeout.count()); m_bar->setValue(0); m_elapsed.start(); m_timer->start(); show(); } void ValidationProgressDialog::stop() { if (m_timer) { m_timer->stop(); } hide(); } void ValidationProgressDialog::showEvent(QShowEvent* e) { if (m_first) { Settings::instance().geometry().centerOnMainWindowMonitor(this); m_first = false; } } void ValidationProgressDialog::closeEvent(QCloseEvent* e) { hide(); e->ignore(); } void ValidationProgressDialog::onButton(QAbstractButton* b) { if (m_buttons->buttonRole(b) == QDialogButtonBox::RejectRole) { hide(); } else { qCritical() << "validation dialog: unknown button pressed"; } } void ValidationProgressDialog::onTimer() { m_bar->setValue(m_elapsed.elapsed() / 1000); } NexusSSOLogin::NexusSSOLogin() : m_keyReceived(false), m_active(false) { m_timeout.setInterval(NXMAccessManager::ValidationTimeout); m_timeout.setSingleShot(true); QObject::connect( &m_socket, &QWebSocket::connected, [&]{ onConnected(); }); QObject::connect( &m_socket, qOverload(&QWebSocket::error), [&](auto&& e){ onError(e); }); QObject::connect( &m_socket, &QWebSocket::sslErrors, [&](auto&& errors){ onSslErrors(errors); }); QObject::connect( &m_socket, &QWebSocket::textMessageReceived, [&](auto&& s){ onMessage(s); }); QObject::connect( &m_socket, &QWebSocket::disconnected, [&]{ onDisconnected(); }); QObject::connect(&m_timeout, &QTimer::timeout, [&]{ onTimeout(); }); } QString NexusSSOLogin::stateToString(States s, const QString& e) { switch (s) { case ConnectingToSSO: return QObject::tr("Connecting to Nexus..."); case WaitingForToken: return QObject::tr("Waiting for Nexus..."); case WaitingForBrowser: return QObject::tr( "Opened Nexus in browser.\n" "Switch to your browser and accept the request."); case Finished: return QObject::tr("Finished."); case Timeout: return QObject::tr( "No answer from Nexus.\n" "A firewall might be blocking Mod Organizer."); case ClosedByRemote: return QObject::tr("Nexus closed the connection."); case Cancelled: return QObject::tr("Cancelled."); case Error: // fall-through default: { if (e.isEmpty()) { return QString("%1").arg(s); } else { return e; } } } } void NexusSSOLogin::start() { m_active = true; setState(ConnectingToSSO); m_timeout.start(); m_socket.open(NexusSSO); } void NexusSSOLogin::cancel() { if (m_active) { abort(); setState(Cancelled); } } void NexusSSOLogin::close() { if (m_active) { m_active = false; m_timeout.stop(); m_socket.close(); } } void NexusSSOLogin::abort() { m_active = false; m_timeout.stop(); m_socket.abort(); } bool NexusSSOLogin::isActive() const { return m_active; } void NexusSSOLogin::setState(States s, const QString& error) { if (stateChanged) { stateChanged(s, error); } } void NexusSSOLogin::onConnected() { setState(WaitingForToken); m_keyReceived = false; boost::uuids::random_generator generator; boost::uuids::uuid sessionId = generator(); m_guid = boost::uuids::to_string(sessionId).c_str(); QJsonObject data; data.insert(QString("id"), QJsonValue(m_guid)); data.insert(QString("protocol"), 2); const QString message = QJsonDocument(data).toJson(); m_socket.sendTextMessage(message); } void NexusSSOLogin::onMessage(const QString& s) { const QJsonDocument doc = QJsonDocument::fromJson(s.toUtf8()); const QVariantMap root = doc.object().toVariantMap(); if (!root["success"].toBool()) { close(); setState(Error, QString("There was a problem with SSO initialization: %1") .arg(root["error"].toString())); return; } const QVariantMap data = root["data"].toMap(); if (data.contains("connection_token")) { // first answer // open browser const auto url = NexusSSOPage.arg(m_guid); shell::OpenLink(url); m_timeout.stop(); setState(WaitingForBrowser); } else { // second answer const auto key = data["api_key"].toString(); close(); if (keyChanged) { keyChanged(key); } setState(Finished); } } void NexusSSOLogin::onDisconnected() { if (m_active) { m_active = false; if (!m_keyReceived) { setState(ClosedByRemote); } } } void NexusSSOLogin::onError(QAbstractSocket::SocketError e) { if (m_active) { setState(Error, m_socket.errorString()); close(); } } void NexusSSOLogin::onSslErrors(const QList& errors) { if (m_active) { for (const auto& e : errors) { setState(Error, e.errorString()); } } } void NexusSSOLogin::onTimeout() { abort(); setState(Timeout); } NexusKeyValidator::NexusKeyValidator(NXMAccessManager& am) : m_manager(am), m_reply(nullptr), m_active(false) { m_timeout.setInterval(NXMAccessManager::ValidationTimeout); m_timeout.setSingleShot(true); QObject::connect(&m_timeout, &QTimer::timeout, [&]{ onTimeout(); }); } NexusKeyValidator::~NexusKeyValidator() { abort(); } QString NexusKeyValidator::stateToString(States s, const QString& e) { switch (s) { case NexusKeyValidator::Connecting: return QObject::tr("Connecting to Nexus..."); case NexusKeyValidator::Finished: return QObject::tr("Finished."); case NexusKeyValidator::InvalidJson: return QObject::tr("Invalid JSON"); case NexusKeyValidator::BadResponse: return QObject::tr("Bad response"); case NexusKeyValidator::Timeout: return QObject::tr("There was a timeout during the request"); case NexusKeyValidator::Cancelled: return QObject::tr("Cancelled"); case NexusKeyValidator::Error: // fall-through default: { if (e.isEmpty()) { return QString("%1").arg(s); } else { return e; } } } } void NexusKeyValidator::start(const QString& key) { if (m_reply) { abort(); return; } m_active = true; setState(Connecting); const QString requestUrl(NexusBaseUrl + "/users/validate"); QNetworkRequest request(requestUrl); request.setRawHeader("APIKEY", key.toUtf8()); request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, m_manager.userAgent().toUtf8()); request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); request.setRawHeader("Protocol-Version", "1.0.0"); request.setRawHeader("Application-Name", "MO2"); request.setRawHeader("Application-Version", m_manager.MOVersion().toUtf8()); m_reply = m_manager.get(request); if (!m_reply) { close(); setState(Error, QObject::tr("Failed to request %1").arg(requestUrl)); return; } m_timeout.start(NXMAccessManager::ValidationTimeout); QObject::connect( m_reply, &QNetworkReply::finished, [&]{ onFinished(); }); QObject::connect( m_reply, &QNetworkReply::sslErrors, [&](auto&& errors){ onSslErrors(errors); }); } void NexusKeyValidator::cancel() { if (m_active) { abort(); setState(Cancelled); } } bool NexusKeyValidator::isActive() const { return m_active; } void NexusKeyValidator::close() { m_active = false; m_timeout.stop(); if (m_reply) { m_reply->disconnect(); m_reply->deleteLater(); m_reply = nullptr; } } void NexusKeyValidator::abort() { m_active = false; m_timeout.stop(); if (m_reply) { m_reply->disconnect(); m_reply->abort(); m_reply->deleteLater(); m_reply = nullptr; } } void NexusKeyValidator::setState(States s, const QString& error) { if (stateChanged) { stateChanged(s, error); } } void NexusKeyValidator::onFinished() { if (!m_reply) { // shouldn't happen return; } m_timeout.stop(); const auto code = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); const auto doc = QJsonDocument::fromJson(m_reply->readAll()); const auto headers = m_reply->rawHeaderPairs(); const auto error = m_reply->errorString(); close(); const QJsonObject data = doc.object(); if (code != 200) { handleError(code, data.value("message").toString(), error); return; } if (doc.isNull()) { setState(InvalidJson); return; } if (!data.contains("user_id")) { setState(BadResponse); return; } const int id = data.value("user_id").toInt(); const QString key = data.value("key").toString(); const QString name = data.value("name").toString(); const bool premium = data.value("is_premium").toBool(); const auto user = APIUserAccount() .apiKey(key) .id(QString("%1").arg(id)) .name(name) .type(premium ? APIUserAccountTypes::Premium : APIUserAccountTypes::Regular) .limits(NexusInterface::parseLimits(headers)); if (finished) { setState(Finished); finished(user); } } void NexusKeyValidator::onSslErrors(const QList& errors) { if (m_active) { for (const auto& e : errors) { setState(Error, e.errorString()); } } } void NexusKeyValidator::onTimeout() { abort(); setState(Timeout); } void NexusKeyValidator::handleError( int code, const QString& nexusMessage, const QString& httpError) { QString s = httpError; if (!nexusMessage.isEmpty()) { if (!s.isEmpty()) { s += ", "; } s += nexusMessage; } if (code != 0) { if (s.isEmpty()) { s = QString("HTTP code %1").arg(code); } else { s += QString(" (%1)").arg(code); } } setState(Error, s); } NXMAccessManager::NXMAccessManager(QObject *parent, const QString &moVersion) : QNetworkAccessManager(parent) , m_ProgressDialog(new ValidationProgressDialog(ValidationTimeout)) , m_MOVersion(moVersion) , m_validator(*this) , m_validationState(NotChecked) { m_validator.stateChanged = [&](auto&& s, auto&& e){ onValidatorState(s, e); }; m_validator.finished = [&](auto&& user){ onValidatorFinished(user); }; setCookieJar(new PersistentCookieJar(QDir::fromNativeSeparators( Settings::instance().paths().cache() + "/nexus_cookies.dat"))); if (networkAccessible() == QNetworkAccessManager::UnknownAccessibility) { // why is this necessary all of a sudden? setNetworkAccessible(QNetworkAccessManager::Accessible); } } void NXMAccessManager::setTopLevelWidget(QWidget* w) { m_ProgressDialog->setParentWidget(w); } QNetworkReply *NXMAccessManager::createRequest( QNetworkAccessManager::Operation operation, const QNetworkRequest &request, QIODevice *device) { if (request.url().scheme() != "nxm") { return QNetworkAccessManager::createRequest(operation, request, device); } if (operation == GetOperation) { emit requestNXMDownload(request.url().toString()); // eat the request, everything else will be done by the download manager return QNetworkAccessManager::createRequest(QNetworkAccessManager::GetOperation, QNetworkRequest(QUrl())); } else if (operation == PostOperation) { return QNetworkAccessManager::createRequest(operation, request, device);; } else { return QNetworkAccessManager::createRequest(operation, request, device); } } void NXMAccessManager::showCookies() const { QUrl url(NexusBaseUrl + "/"); for (const QNetworkCookie &cookie : cookieJar()->cookiesForUrl(url)) { log::debug("{} - {} (expires: {})", cookie.name().constData(), cookie.value().constData(), cookie.expirationDate().toString()); } } void NXMAccessManager::clearCookies() { PersistentCookieJar *jar = qobject_cast(cookieJar()); if (jar != nullptr) { jar->clear(); } else { log::warn("failed to clear cookies, invalid cookie jar"); } } void NXMAccessManager::startValidationCheck(const QString& key) { m_validationState = NotChecked; m_validator.start(key); m_ProgressDialog->start(); } void NXMAccessManager::onValidatorState( NexusKeyValidator::States s, const QString& e) { if (s == NexusKeyValidator::Connecting || s == NexusKeyValidator::Finished) { // no-op, success is handled in onValidatorFinished() return; } m_ProgressDialog->stop(); m_validationState = Invalid; emit validateFailed(NexusKeyValidator::stateToString(s, e)); } void NXMAccessManager::onValidatorFinished(const APIUserAccount& user) { m_ProgressDialog->stop(); m_validationState = Valid; emit credentialsReceived(user); emit validateSuccessful(true); } bool NXMAccessManager::validated() const { if (m_validator.isActive()) { m_ProgressDialog->show(); } return (m_validationState == Valid); } void NXMAccessManager::refuseValidation() { m_validationState = Invalid; } bool NXMAccessManager::validateAttempted() const { return (m_validationState != NotChecked); } bool NXMAccessManager::validateWaiting() const { return m_validator.isActive(); } void NXMAccessManager::apiCheck(const QString &apiKey, bool force) { if (m_validator.isActive()) { return; } if (force) { m_validationState = NotChecked; } if (m_validationState == Valid) { emit validateSuccessful(false); return; } startValidationCheck(apiKey); } const QString& NXMAccessManager::MOVersion() const { return m_MOVersion; } QString NXMAccessManager::userAgent(const QString &subModule) const { QStringList comments; QString os; if (QSysInfo::productType() == "windows") comments << ((QSysInfo::kernelType() == "winnt") ? "Windows_NT " : "Windows ") + QSysInfo::kernelVersion(); else comments << QSysInfo::kernelType().left(1).toUpper() + QSysInfo::kernelType().mid(1) << QSysInfo::productType().left(1).toUpper() + QSysInfo::kernelType().mid(1) + " " + QSysInfo::productVersion(); if (!subModule.isEmpty()) { comments << "module: " + subModule; } comments << ((QSysInfo::buildCpuArchitecture() == "x86_64") ? "x64" : "x86"); return QString("Mod Organizer/%1 (%2) Qt/%3").arg(m_MOVersion, comments.join("; "), qVersion()); } void NXMAccessManager::clearApiKey() { m_validator.cancel(); emit credentialsReceived(APIUserAccount()); }