diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index 97ba48f4..d7537345 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -244,8 +244,13 @@ QVariant AccountList::data(const QModelIndex &index, int role) const } case StatusColumn: { - auto isActive = account->isActive(); - return isActive ? "Working" : "Ready"; + if(account->isActive()) { + return tr("Working", "Account status"); + } + if(account->isExpired()) { + return tr("Expired", "Account status"); + } + return tr("Ready", "Account status"); } case ProfileNameColumn: { diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index 5cfec49d..30ed6afe 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -34,6 +34,11 @@ #include "flows/MojangRefresh.h" #include "flows/MojangLogin.h" +MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) { + m_internalId = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); +} + + MinecraftAccountPtr MinecraftAccount::loadFromJsonV2(const QJsonObject& json) { MinecraftAccountPtr account(new MinecraftAccount()); if(account->data.resumeStateFromV2(json)) { @@ -52,7 +57,7 @@ MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) { MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username) { - MinecraftAccountPtr account(new MinecraftAccount()); + MinecraftAccountPtr account = new MinecraftAccount(); account->data.type = AccountType::Mojang; account->data.yggdrasilToken.extra["userName"] = username; account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); @@ -91,6 +96,23 @@ AccountStatus MinecraftAccount::accountStatus() const { } } +bool MinecraftAccount::isExpired() const { + switch(data.type) { + case AccountType::Mojang: { + return data.accessToken().isEmpty(); + } + break; + case AccountType::MSA: { + return data.msaToken.validity == Katabasis::Validity::None; + } + break; + default: { + return true; + } + } +} + + QPixmap MinecraftAccount::getFace() const { QPixmap skinTexture; if(!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) { diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index 928d3742..459ef903 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -72,7 +72,7 @@ public: /* construction */ explicit MinecraftAccount(const MinecraftAccount &other, QObject *parent) = delete; //! Default constructor - explicit MinecraftAccount(QObject *parent = 0) : QObject(parent) {}; + explicit MinecraftAccount(QObject *parent = 0); static MinecraftAccountPtr createFromUsername(const QString &username); @@ -97,6 +97,10 @@ public: /* manipulation */ shared_qobject_ptr refresh(AuthSessionPtr session); public: /* queries */ + QString internalId() const { + return m_internalId; + } + QString accountDisplayString() const { return data.accountDisplayString(); } @@ -119,6 +123,8 @@ public: /* queries */ bool isActive() const; + bool isExpired() const; + bool canMigrate() const { return data.canMigrateToMSA; } @@ -168,6 +174,7 @@ signals: // TODO: better signalling for the various possible state changes - especially errors protected: /* variables */ + QString m_internalId; AccountData data; // current task we are executing here diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp index 34e2bea8..00957fd4 100644 --- a/launcher/minecraft/auth/flows/AuthContext.cpp +++ b/launcher/minecraft/auth/flows/AuthContext.cpp @@ -18,7 +18,7 @@ #include -using OAuth2 = Katabasis::OAuth2; +using OAuth2 = Katabasis::DeviceFlow; using Activity = Katabasis::Activity; AuthContext::AuthContext(AccountData * data, QObject *parent) : @@ -50,21 +50,17 @@ void AuthContext::initMSA() { return; } - Katabasis::OAuth2::Options opts; + OAuth2::Options opts; opts.scope = "XboxLive.signin offline_access"; opts.clientIdentifier = APPLICATION->msaClientId(); 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}; // FIXME: OAuth2 is not aware of our fancy shared pointers m_oauth2 = new OAuth2(opts, m_data->msaToken, this, APPLICATION->network().get()); - 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::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode); connect(m_oauth2, &OAuth2::activityChanged, this, &AuthContext::onOAuthActivityChanged); + connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode); } void AuthContext::initMojang() { @@ -78,7 +74,7 @@ void AuthContext::initMojang() { } void AuthContext::onMojangSucceeded() { - doEntitlements(); + doMinecraftProfile(); } @@ -89,50 +85,56 @@ void AuthContext::onMojangFailed() { changeState(m_yggdrasil->accountState(), tr("Mojang user authentication failed.")); } -/* -bool AuthContext::signOut() { - if(isBusy()) { - return false; - } - - start(); - - beginActivity(Activity::LoggingOut); - m_oauth2->unlink(); - m_account = AccountData(); - finishActivity(); - return true; -} -*/ - -void AuthContext::onOAuthLinkingFailed() { - emit hideVerificationUriAndCode(); - finishActivity(); - changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed.")); -} - -void AuthContext::onOAuthLinkingSucceeded() { - emit hideVerificationUriAndCode(); - auto *o2t = qobject_cast(sender()); - if (!o2t->linked()) { - finishActivity(); - changeState(STATE_FAILED_HARD, tr("Microsoft user authentication ended with an impossible state (succeeded, but not succeeded at the same time).")); - 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(); -} - void AuthContext::onOAuthActivityChanged(Katabasis::Activity activity) { - // respond to activity change here + switch(activity) { + case Katabasis::Activity::Idle: + case Katabasis::Activity::LoggingIn: + case Katabasis::Activity::Refreshing: + case Katabasis::Activity::LoggingOut: { + // We asked it to do something, it's doing it. Nothing to act upon. + return; + } + case Katabasis::Activity::Succeeded: { + // Succeeded or did not invalidate tokens + emit hideVerificationUriAndCode(); + if (!m_oauth2->linked()) { + finishActivity(); + changeState(STATE_FAILED_HARD, tr("Microsoft user authentication ended with an impossible state (succeeded, but not succeeded at the same time).")); + return; + } + QVariantMap extraTokens = m_oauth2->extraTokens(); +#ifndef NDEBUG + if (!extraTokens.isEmpty()) { + qDebug() << "Extra tokens in response:"; + foreach (QString key, extraTokens.keys()) { + qDebug() << "\t" << key << ":" << extraTokens.value(key); + } + } +#endif + doUserAuth(); + return; + } + case Katabasis::Activity::FailedSoft: { + emit hideVerificationUriAndCode(); + finishActivity(); + changeState(STATE_FAILED_SOFT, tr("Microsoft user authentication failed with a soft error.")); + return; + } + case Katabasis::Activity::FailedGone: + case Katabasis::Activity::FailedHard: { + emit hideVerificationUriAndCode(); + finishActivity(); + changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed.")); + return; + } + default: { + emit hideVerificationUriAndCode(); + finishActivity(); + changeState(STATE_FAILED_HARD, tr("Microsoft user authentication completed with an unrecognized result.")); + return; + } + + } } void AuthContext::doUserAuth() { @@ -226,7 +228,7 @@ void AuthContext::doSTSAuthMinecraft() { void AuthContext::processSTSError(QNetworkReply::NetworkError error, QByteArray data, QList headers) { if(error == QNetworkReply::AuthenticationRequiredError) { - QJsonParseError jsonError; + QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if(jsonError.error) { qWarning() << "Cannot parse error XSTS response as JSON: " << jsonError.errorString(); @@ -543,6 +545,10 @@ void AuthContext::onMinecraftProfileDone( #endif if (error == QNetworkReply::ContentNotFoundError) { // NOTE: Succeed even if we do not have a profile. This is a valid account state. + if(m_data->type == AccountType::Mojang) { + m_data->minecraftEntitlement.canPlayMinecraft = false; + m_data->minecraftEntitlement.ownsMinecraft = false; + } m_data->minecraftProfile = MinecraftProfile(); succeed(); return; @@ -560,6 +566,9 @@ void AuthContext::onMinecraftProfileDone( } if(m_data->type == AccountType::Mojang) { + auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain; + m_data->minecraftEntitlement.canPlayMinecraft = validProfile; + m_data->minecraftEntitlement.ownsMinecraft = validProfile; doMigrationEligibilityCheck(); } else { diff --git a/launcher/minecraft/auth/flows/AuthContext.h b/launcher/minecraft/auth/flows/AuthContext.h index dcb91613..5e4e9edc 100644 --- a/launcher/minecraft/auth/flows/AuthContext.h +++ b/launcher/minecraft/auth/flows/AuthContext.h @@ -7,7 +7,7 @@ #include #include -#include +#include #include "Yggdrasil.h" #include "../AccountData.h" #include "../AccountTask.h" @@ -35,9 +35,6 @@ signals: private slots: // OAuth-specific callbacks - void onOAuthLinkingSucceeded(); - void onOAuthLinkingFailed(); - void onOAuthActivityChanged(Katabasis::Activity activity); // Yggdrasil specific callbacks @@ -87,7 +84,7 @@ protected: void clearTokens(); protected: - Katabasis::OAuth2 *m_oauth2 = nullptr; + Katabasis::DeviceFlow *m_oauth2 = nullptr; Yggdrasil *m_yggdrasil = nullptr; int m_requestsDone = 0; diff --git a/launcher/minecraft/auth/flows/MSAInteractive.cpp b/launcher/minecraft/auth/flows/MSAInteractive.cpp index 6c597cf7..525aaf88 100644 --- a/launcher/minecraft/auth/flows/MSAInteractive.cpp +++ b/launcher/minecraft/auth/flows/MSAInteractive.cpp @@ -17,7 +17,6 @@ void MSAInteractive::executeTask() { m_oauth2->setExtraRequestParams(extraOpts); beginActivity(Katabasis::Activity::LoggingIn); - m_oauth2->unlink(); *m_data = AccountData(); - m_oauth2->link(); + m_oauth2->login(); } diff --git a/launcher/ui/widgets/VersionListView.cpp b/launcher/ui/widgets/VersionListView.cpp index 8424fedd..aba0b1a1 100644 --- a/launcher/ui/widgets/VersionListView.cpp +++ b/launcher/ui/widgets/VersionListView.cpp @@ -19,7 +19,6 @@ #include #include #include "VersionListView.h" -#include "Common.h" VersionListView::VersionListView(QWidget *parent) :QTreeView ( parent ) diff --git a/libraries/katabasis/CMakeLists.txt b/libraries/katabasis/CMakeLists.txt index 2f9cb66d..c6115881 100644 --- a/libraries/katabasis/CMakeLists.txt +++ b/libraries/katabasis/CMakeLists.txt @@ -27,20 +27,18 @@ set(CMAKE_C_STANDARD 11) find_package(Qt5 COMPONENTS Core Network REQUIRED) set( katabasis_PRIVATE - src/OAuth2.cpp + src/DeviceFlow.cpp src/JsonResponse.cpp src/JsonResponse.h src/PollServer.cpp src/Reply.cpp - src/ReplyServer.cpp ) set( katabasis_PUBLIC - include/katabasis/OAuth2.h + include/katabasis/DeviceFlow.h include/katabasis/Globals.h include/katabasis/PollServer.h include/katabasis/Reply.h - include/katabasis/ReplyServer.h include/katabasis/RequestParameter.h ) diff --git a/libraries/katabasis/include/katabasis/Bits.h b/libraries/katabasis/include/katabasis/Bits.h index 3fd2f530..f11f25d2 100644 --- a/libraries/katabasis/include/katabasis/Bits.h +++ b/libraries/katabasis/include/katabasis/Bits.h @@ -10,7 +10,11 @@ enum class Activity { Idle, LoggingIn, LoggingOut, - Refreshing + Refreshing, + FailedSoft, //!< soft failure. this generally means the user auth details haven't been invalidated + FailedHard, //!< hard failure. auth is invalid + FailedGone, //!< hard failure. auth is invalid, and the account no longer exists + Succeeded }; enum class Validity { diff --git a/libraries/katabasis/include/katabasis/DeviceFlow.h b/libraries/katabasis/include/katabasis/DeviceFlow.h new file mode 100644 index 00000000..b68c92e0 --- /dev/null +++ b/libraries/katabasis/include/katabasis/DeviceFlow.h @@ -0,0 +1,150 @@ +#pragma once + +#include +#include +#include +#include + +#include "Reply.h" +#include "RequestParameter.h" +#include "Bits.h" + +namespace Katabasis { + +class ReplyServer; +class PollServer; + +/// Simple OAuth2 Device Flow authenticator. +class DeviceFlow: public QObject +{ + Q_OBJECT +public: + Q_ENUMS(GrantFlow) + +public: + + struct Options { + QString userAgent = QStringLiteral("Katabasis/1.0"); + QString responseType = QStringLiteral("code"); + QString scope; + QString clientIdentifier; + QString clientSecret; + QUrl authorizationUrl; + QUrl accessTokenUrl; + }; + +public: + /// Are we authenticated? + bool linked(); + + /// Authentication token. + QString token(); + + /// Provider-specific extra tokens, available after a successful authentication + QVariantMap extraTokens(); + +public: + // TODO: put in `Options` + /// User-defined extra parameters to append to request URL + QVariantMap extraRequestParams(); + void setExtraRequestParams(const QVariantMap &value); + + // TODO: split up the class into multiple, each implementing one OAuth2 flow + /// Grant type (if non-standard) + QString grantType(); + void setGrantType(const QString &value); + +public: + /// Constructor. + /// @param parent Parent object. + explicit DeviceFlow(Options & opts, Token & token, QObject *parent = 0, QNetworkAccessManager *manager = 0); + + /// Get refresh token. + QString refreshToken(); + + /// Get token expiration time + QDateTime expires(); + +public slots: + /// Authenticate. + void login(); + + /// De-authenticate. + void logout(); + + /// Refresh token. + bool refresh(); + + /// Handle situation where reply server has opted to close its connection + void serverHasClosed(bool paramsfound = false); + +signals: + /// Emitted when client needs to open a web browser window, with the given URL. + void openBrowser(const QUrl &url); + + /// Emitted when client can close the browser window. + void closeBrowser(); + + /// Emitted when client needs to show a verification uri and user code + void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn); + + /// Emitted when the internal state changes + void activityChanged(Activity activity); + +public slots: + /// Handle verification response. + void onVerificationReceived(QMap); + +protected slots: + /// Handle completion of a Device Authorization Request + void onDeviceAuthReplyFinished(); + + /// Handle completion of a refresh request. + void onRefreshFinished(); + + /// Handle failure of a refresh request. + void onRefreshError(QNetworkReply::NetworkError error, QNetworkReply *reply); + +protected: + /// Set refresh token. + void setRefreshToken(const QString &v); + + /// Set token expiration time. + void setExpires(QDateTime v); + + /// Start polling authorization server + void startPollServer(const QVariantMap ¶ms, int expiresIn); + + /// Set authentication token. + void setToken(const QString &v); + + /// Set the linked state + void setLinked(bool v); + + /// Set extra tokens found in OAuth response + void setExtraTokens(QVariantMap extraTokens); + + /// Set local poll server + void setPollServer(PollServer *server); + + PollServer * pollServer() const; + + void updateActivity(Activity activity); + +protected: + Options options_; + + QVariantMap extraReqParams_; + QNetworkAccessManager *manager_ = nullptr; + ReplyList timedReplies_; + QString grantType_; + +protected: + Token &token_; + +private: + PollServer *pollServer_ = nullptr; + Activity activity_ = Activity::Idle; +}; + +} diff --git a/libraries/katabasis/include/katabasis/OAuth2.h b/libraries/katabasis/include/katabasis/OAuth2.h deleted file mode 100644 index 9dbe5c71..00000000 --- a/libraries/katabasis/include/katabasis/OAuth2.h +++ /dev/null @@ -1,233 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "Reply.h" -#include "RequestParameter.h" -#include "Bits.h" - -namespace Katabasis { - -class ReplyServer; -class PollServer; - - -/* - * FIXME: this is not as simple as it should be. it squishes 4 different grant flows into one big ball of mud - * This serves no practical purpose and simply makes the code less readable / maintainable. - * - * Therefore: Split this into the 4 different OAuth2 flows that people can use as authentication steps. Write tests/examples for all of them. - */ - -/// Simple OAuth2 authenticator. -class OAuth2: public QObject -{ - Q_OBJECT -public: - Q_ENUMS(GrantFlow) - -public: - - struct Options { - QString userAgent = QStringLiteral("Katabasis/1.0"); - QString redirectionUrl = QStringLiteral("http://localhost:%1"); - QString responseType = QStringLiteral("code"); - QString scope; - QString clientIdentifier; - QString clientSecret; - QUrl authorizationUrl; - QUrl accessTokenUrl; - QVector listenerPorts = { 0 }; - }; - - /// Authorization flow types. - enum GrantFlow { - GrantFlowAuthorizationCode, ///< @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1 - GrantFlowImplicit, ///< @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.2 - GrantFlowResourceOwnerPasswordCredentials, - GrantFlowDevice ///< @see https://tools.ietf.org/html/rfc8628#section-1 - }; - - /// Authorization flow. - GrantFlow grantFlow(); - void setGrantFlow(GrantFlow value); - -public: - /// Are we authenticated? - bool linked(); - - /// Authentication token. - QString token(); - - /// Provider-specific extra tokens, available after a successful authentication - QVariantMap extraTokens(); - - /// Page content on local host after successful oauth. - /// Provide it in case you do not want to close the browser, but display something - QByteArray replyContent() const; - void setReplyContent(const QByteArray &value); - -public: - - // TODO: remove - /// Resource owner username. - /// instances with the same (username, password) share the same "linked" and "token" properties. - QString username(); - void setUsername(const QString &value); - - // TODO: remove - /// Resource owner password. - /// instances with the same (username, password) share the same "linked" and "token" properties. - QString password(); - void setPassword(const QString &value); - - // TODO: remove - /// API key. - QString apiKey(); - void setApiKey(const QString &value); - - // TODO: remove - /// Allow ignoring SSL errors? - /// E.g. SurveyMonkey fails on Mac due to SSL error. Ignoring the error circumvents the problem - bool ignoreSslErrors(); - void setIgnoreSslErrors(bool ignoreSslErrors); - - // TODO: put in `Options` - /// User-defined extra parameters to append to request URL - QVariantMap extraRequestParams(); - void setExtraRequestParams(const QVariantMap &value); - - // TODO: split up the class into multiple, each implementing one OAuth2 flow - /// Grant type (if non-standard) - QString grantType(); - void setGrantType(const QString &value); - -public: - /// Constructor. - /// @param parent Parent object. - explicit OAuth2(Options & opts, Token & token, QObject *parent = 0, QNetworkAccessManager *manager = 0); - - /// Get refresh token. - QString refreshToken(); - - /// Get token expiration time - QDateTime expires(); - -public slots: - /// Authenticate. - virtual void link(); - - /// De-authenticate. - virtual void unlink(); - - /// Refresh token. - bool refresh(); - - /// Handle situation where reply server has opted to close its connection - void serverHasClosed(bool paramsfound = false); - -signals: - /// Emitted when a token refresh has been completed or failed. - void refreshFinished(QNetworkReply::NetworkError error); - - /// Emitted when client needs to open a web browser window, with the given URL. - void openBrowser(const QUrl &url); - - /// Emitted when client can close the browser window. - void closeBrowser(); - - /// Emitted when client needs to show a verification uri and user code - void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn); - - /// Emitted when authentication/deauthentication succeeded. - void linkingSucceeded(); - - /// Emitted when authentication/deauthentication failed. - void linkingFailed(); - - void activityChanged(Activity activity); - -public slots: - /// Handle verification response. - virtual void onVerificationReceived(QMap); - -protected slots: - /// Handle completion of a token request. - virtual void onTokenReplyFinished(); - - /// Handle failure of a token request. - virtual void onTokenReplyError(QNetworkReply::NetworkError error); - - /// Handle completion of a refresh request. - virtual void onRefreshFinished(); - - /// Handle failure of a refresh request. - virtual void onRefreshError(QNetworkReply::NetworkError error); - - /// Handle completion of a Device Authorization Request - virtual void onDeviceAuthReplyFinished(); - -protected: - /// Build HTTP request body. - QByteArray buildRequestBody(const QMap ¶meters); - - /// Set refresh token. - void setRefreshToken(const QString &v); - - /// Set token expiration time. - void setExpires(QDateTime v); - - /// Start polling authorization server - void startPollServer(const QVariantMap ¶ms, int expiresIn); - - /// Set authentication token. - void setToken(const QString &v); - - /// Set the linked state - void setLinked(bool v); - - /// Set extra tokens found in OAuth response - void setExtraTokens(QVariantMap extraTokens); - - /// Set local reply server - void setReplyServer(ReplyServer *server); - - ReplyServer * replyServer() const; - - /// Set local poll server - void setPollServer(PollServer *server); - - PollServer * pollServer() const; - - void updateActivity(Activity activity); - -protected: - QString username_; - QString password_; - - Options options_; - - QVariantMap extraReqParams_; - QString apiKey_; - QNetworkAccessManager *manager_ = nullptr; - ReplyList timedReplies_; - GrantFlow grantFlow_; - QString grantType_; - -protected: - QString redirectUri_; - Token &token_; - - // this should be part of the reply server impl - QByteArray replyContent_; - -private: - ReplyServer *replyServer_ = nullptr; - PollServer *pollServer_ = nullptr; - Activity activity_ = Activity::Idle; -}; - -} diff --git a/libraries/katabasis/include/katabasis/Reply.h b/libraries/katabasis/include/katabasis/Reply.h index 3af1d49f..415cf4ec 100644 --- a/libraries/katabasis/include/katabasis/Reply.h +++ b/libraries/katabasis/include/katabasis/Reply.h @@ -9,12 +9,14 @@ namespace Katabasis { +constexpr int defaultTimeout = 30 * 1000; + /// A network request/reply pair that can time out. class Reply: public QTimer { Q_OBJECT public: - Reply(QNetworkReply *reply, int timeOut = 60 * 1000, QObject *parent = 0); + Reply(QNetworkReply *reply, int timeOut = defaultTimeout, QObject *parent = 0); signals: void error(QNetworkReply::NetworkError); @@ -25,6 +27,7 @@ public slots: public: QNetworkReply *reply; + bool timedOut = false; }; /// List of O2Replies. @@ -37,7 +40,7 @@ public: virtual ~ReplyList(); /// Create a new O2Reply from a QNetworkReply, and add it to this list. - void add(QNetworkReply *reply); + void add(QNetworkReply *reply, int timeOut = defaultTimeout); /// Add an O2Reply to the list, while taking ownership of it. void add(Reply *reply); diff --git a/libraries/katabasis/include/katabasis/ReplyServer.h b/libraries/katabasis/include/katabasis/ReplyServer.h deleted file mode 100644 index bf47df69..00000000 --- a/libraries/katabasis/include/katabasis/ReplyServer.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -namespace Katabasis { - -/// HTTP server to process authentication response. -class ReplyServer: public QTcpServer { - Q_OBJECT - -public: - explicit ReplyServer(QObject *parent = 0); - - /// Page content on local host after successful oauth - in case you do not want to close the browser, but display something - Q_PROPERTY(QByteArray replyContent READ replyContent WRITE setReplyContent) - QByteArray replyContent(); - void setReplyContent(const QByteArray &value); - - /// Seconds to keep listening *after* first response for a callback with token content - Q_PROPERTY(int timeout READ timeout WRITE setTimeout) - int timeout(); - void setTimeout(int timeout); - - /// Maximum number of callback tries to accept, in case some don't have token content (favicons, etc.) - Q_PROPERTY(int callbackTries READ callbackTries WRITE setCallbackTries) - int callbackTries(); - void setCallbackTries(int maxtries); - - QString uniqueState(); - void setUniqueState(const QString &state); - -signals: - void verificationReceived(QMap); - void serverClosed(bool); // whether it has found parameters - -public slots: - void onIncomingConnection(); - void onBytesReady(); - QMap parseQueryParams(QByteArray *data); - void closeServer(QTcpSocket *socket = 0, bool hasparameters = false); - -protected: - QByteArray replyContent_; - int timeout_; - int maxtries_; - int tries_; - QString uniqueState_; -}; - -} diff --git a/libraries/katabasis/src/DeviceFlow.cpp b/libraries/katabasis/src/DeviceFlow.cpp new file mode 100644 index 00000000..5efd5e7b --- /dev/null +++ b/libraries/katabasis/src/DeviceFlow.cpp @@ -0,0 +1,450 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "katabasis/DeviceFlow.h" +#include "katabasis/PollServer.h" +#include "katabasis/Globals.h" + +#include "JsonResponse.h" + +namespace { +// ref: https://tools.ietf.org/html/rfc8628#section-3.2 +// Exception: Google sign-in uses "verification_url" instead of "*_uri" - we'll accept both. +bool hasMandatoryDeviceAuthParams(const QVariantMap& params) +{ + if (!params.contains(Katabasis::OAUTH2_DEVICE_CODE)) + return false; + + if (!params.contains(Katabasis::OAUTH2_USER_CODE)) + return false; + + if (!(params.contains(Katabasis::OAUTH2_VERIFICATION_URI) || params.contains(Katabasis::OAUTH2_VERIFICATION_URL))) + return false; + + if (!params.contains(Katabasis::OAUTH2_EXPIRES_IN)) + return false; + + return true; +} + +QByteArray createQueryParameters(const QList ¶meters) { + QByteArray ret; + bool first = true; + for( auto & h: parameters) { + if (first) { + first = false; + } else { + ret.append("&"); + } + ret.append(QUrl::toPercentEncoding(h.name) + "=" + QUrl::toPercentEncoding(h.value)); + } + return ret; +} +} + +namespace Katabasis { + +DeviceFlow::DeviceFlow(Options & opts, Token & token, QObject *parent, QNetworkAccessManager *manager) : QObject(parent), token_(token) { + manager_ = manager ? manager : new QNetworkAccessManager(this); + qRegisterMetaType("QNetworkReply::NetworkError"); + options_ = opts; +} + +bool DeviceFlow::linked() { + return token_.validity != Validity::None; +} +void DeviceFlow::setLinked(bool v) { + qDebug() << "DeviceFlow::setLinked:" << (v? "true": "false"); + token_.validity = v ? Validity::Certain : Validity::None; +} + +void DeviceFlow::updateActivity(Activity activity) +{ + if(activity_ == activity) { + return; + } + + activity_ = activity; + switch(activity) { + case Katabasis::Activity::Idle: + case Katabasis::Activity::LoggingIn: + case Katabasis::Activity::LoggingOut: + case Katabasis::Activity::Refreshing: + // non-terminal states... + break; + case Katabasis::Activity::FailedSoft: + // terminal state, tokens did not change + break; + case Katabasis::Activity::FailedHard: + case Katabasis::Activity::FailedGone: + // terminal state, tokens are invalid + token_ = Token(); + break; + case Katabasis::Activity::Succeeded: + setLinked(true); + break; + } + emit activityChanged(activity_); +} + +QString DeviceFlow::token() { + return token_.token; +} +void DeviceFlow::setToken(const QString &v) { + token_.token = v; +} + +QVariantMap DeviceFlow::extraTokens() { + return token_.extra; +} + +void DeviceFlow::setExtraTokens(QVariantMap extraTokens) { + token_.extra = extraTokens; +} + +void DeviceFlow::setPollServer(PollServer *server) +{ + if (pollServer_) + pollServer_->deleteLater(); + + pollServer_ = server; +} + +PollServer *DeviceFlow::pollServer() const +{ + return pollServer_; +} + +QVariantMap DeviceFlow::extraRequestParams() +{ + return extraReqParams_; +} + +void DeviceFlow::setExtraRequestParams(const QVariantMap &value) +{ + extraReqParams_ = value; +} + +QString DeviceFlow::grantType() +{ + if (!grantType_.isEmpty()) + return grantType_; + + return OAUTH2_GRANT_TYPE_DEVICE; +} + +void DeviceFlow::setGrantType(const QString &value) +{ + grantType_ = value; +} + +// First get the URL and token to display to the user +void DeviceFlow::login() { + qDebug() << "DeviceFlow::link"; + + updateActivity(Activity::LoggingIn); + setLinked(false); + setToken(""); + setExtraTokens(QVariantMap()); + setRefreshToken(QString()); + setExpires(QDateTime()); + + QList parameters; + parameters.append(RequestParameter(OAUTH2_CLIENT_ID, options_.clientIdentifier.toUtf8())); + parameters.append(RequestParameter(OAUTH2_SCOPE, options_.scope.toUtf8())); + QByteArray payload = createQueryParameters(parameters); + + QUrl url(options_.authorizationUrl); + QNetworkRequest deviceRequest(url); + deviceRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + QNetworkReply *tokenReply = manager_->post(deviceRequest, payload); + + connect(tokenReply, &QNetworkReply::finished, this, &DeviceFlow::onDeviceAuthReplyFinished, Qt::QueuedConnection); +} + +// Then, once we get them, present them to the user +void DeviceFlow::onDeviceAuthReplyFinished() +{ + qDebug() << "DeviceFlow::onDeviceAuthReplyFinished"; + QNetworkReply *tokenReply = qobject_cast(sender()); + if (!tokenReply) + { + qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: reply is null"; + return; + } + if (tokenReply->error() == QNetworkReply::NoError) { + QByteArray replyData = tokenReply->readAll(); + + // Dump replyData + // SENSITIVE DATA in RelWithDebInfo or Debug builds + //qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: replyData\n"; + //qDebug() << QString( replyData ); + + QVariantMap params = parseJsonResponse(replyData); + + // Dump tokens + qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: Tokens returned:\n"; + foreach (QString key, params.keys()) { + // SENSITIVE DATA in RelWithDebInfo or Debug builds, so it is truncated first + qDebug() << key << ": "<< params.value( key ).toString(); + } + + // Check for mandatory parameters + if (hasMandatoryDeviceAuthParams(params)) { + qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: Device auth request response"; + + const QString userCode = params.take(OAUTH2_USER_CODE).toString(); + QUrl uri = params.take(OAUTH2_VERIFICATION_URI).toUrl(); + if (uri.isEmpty()) + uri = params.take(OAUTH2_VERIFICATION_URL).toUrl(); + + if (params.contains(OAUTH2_VERIFICATION_URI_COMPLETE)) + emit openBrowser(params.take(OAUTH2_VERIFICATION_URI_COMPLETE).toUrl()); + + bool ok = false; + int expiresIn = params[OAUTH2_EXPIRES_IN].toInt(&ok); + if (!ok) { + qWarning() << "DeviceFlow::startPollServer: No expired_in parameter"; + updateActivity(Activity::FailedHard); + return; + } + + emit showVerificationUriAndCode(uri, userCode, expiresIn); + + startPollServer(params, expiresIn); + } else { + qWarning() << "DeviceFlow::onDeviceAuthReplyFinished: Mandatory parameters missing from response"; + updateActivity(Activity::FailedHard); + } + } + tokenReply->deleteLater(); +} + +// Spin up polling for the user completing the login flow out of band +void DeviceFlow::startPollServer(const QVariantMap ¶ms, int expiresIn) +{ + qDebug() << "DeviceFlow::startPollServer: device_ and user_code expires in" << expiresIn << "seconds"; + + QUrl url(options_.accessTokenUrl); + QNetworkRequest authRequest(url); + authRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + const QString deviceCode = params[OAUTH2_DEVICE_CODE].toString(); + const QString grantType = grantType_.isEmpty() ? OAUTH2_GRANT_TYPE_DEVICE : grantType_; + + QList parameters; + parameters.append(RequestParameter(OAUTH2_CLIENT_ID, options_.clientIdentifier.toUtf8())); + if ( !options_.clientSecret.isEmpty() ) { + parameters.append(RequestParameter(OAUTH2_CLIENT_SECRET, options_.clientSecret.toUtf8())); + } + parameters.append(RequestParameter(OAUTH2_CODE, deviceCode.toUtf8())); + parameters.append(RequestParameter(OAUTH2_GRANT_TYPE, grantType.toUtf8())); + QByteArray payload = createQueryParameters(parameters); + + 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); + } + } + connect(pollServer, &PollServer::verificationReceived, this, &DeviceFlow::onVerificationReceived); + connect(pollServer, &PollServer::serverClosed, this, &DeviceFlow::serverHasClosed); + setPollServer(pollServer); + pollServer->startPolling(); +} + +// Once the user completes the flow, update the internal state and report it to observers +void DeviceFlow::onVerificationReceived(const QMap response) { + qDebug() << "DeviceFlow::onVerificationReceived: Emitting closeBrowser()"; + emit closeBrowser(); + + if (response.contains("error")) { + qWarning() << "DeviceFlow::onVerificationReceived: Verification failed:" << response; + updateActivity(Activity::FailedHard); + return; + } + + // Check for mandatory tokens + if (response.contains(OAUTH2_ACCESS_TOKEN)) { + qDebug() << "DeviceFlow::onVerificationReceived: Access token returned for implicit or device flow"; + setToken(response.value(OAUTH2_ACCESS_TOKEN)); + if (response.contains(OAUTH2_EXPIRES_IN)) { + bool ok = false; + int expiresIn = response.value(OAUTH2_EXPIRES_IN).toInt(&ok); + if (ok) { + qDebug() << "DeviceFlow::onVerificationReceived: Token expires in" << expiresIn << "seconds"; + setExpires(QDateTime::currentDateTimeUtc().addSecs(expiresIn)); + } + } + if (response.contains(OAUTH2_REFRESH_TOKEN)) { + setRefreshToken(response.value(OAUTH2_REFRESH_TOKEN)); + } + updateActivity(Activity::Succeeded); + } else { + qWarning() << "DeviceFlow::onVerificationReceived: Access token missing from response for implicit or device flow"; + updateActivity(Activity::FailedHard); + } +} + +// Or if the flow fails or the polling times out, update the internal state with error and report it to observers +void DeviceFlow::serverHasClosed(bool paramsfound) +{ + if ( !paramsfound ) { + // server has probably timed out after receiving first response + updateActivity(Activity::FailedHard); + } + // poll server is not re-used for later auth requests + setPollServer(NULL); +} + +void DeviceFlow::logout() { + qDebug() << "DeviceFlow::unlink"; + updateActivity(Activity::LoggingOut); + // FIXME: implement logout flows... if they exist + token_ = Token(); + updateActivity(Activity::FailedHard); +} + +QDateTime DeviceFlow::expires() { + return token_.notAfter; +} +void DeviceFlow::setExpires(QDateTime v) { + token_.notAfter = v; +} + +QString DeviceFlow::refreshToken() { + return token_.refresh_token; +} + +void DeviceFlow::setRefreshToken(const QString &v) { +#ifndef NDEBUG + qDebug() << "DeviceFlow::setRefreshToken" << v << "..."; +#endif + token_.refresh_token = v; +} + +namespace { +QByteArray buildRequestBody(const QMap ¶meters) { + QByteArray body; + bool first = true; + foreach (QString key, parameters.keys()) { + if (first) { + first = false; + } else { + body.append("&"); + } + QString value = parameters.value(key); + body.append(QUrl::toPercentEncoding(key) + QString("=").toUtf8() + QUrl::toPercentEncoding(value)); + } + return body; +} +} + +bool DeviceFlow::refresh() { + qDebug() << "DeviceFlow::refresh: Token: ..." << refreshToken().right(7); + + updateActivity(Activity::Refreshing); + + if (refreshToken().isEmpty()) { + qWarning() << "DeviceFlow::refresh: No refresh token"; + onRefreshError(QNetworkReply::AuthenticationRequiredError, nullptr); + return false; + } + if (options_.accessTokenUrl.isEmpty()) { + qWarning() << "DeviceFlow::refresh: Refresh token URL not set"; + onRefreshError(QNetworkReply::AuthenticationRequiredError, nullptr); + return false; + } + + QNetworkRequest refreshRequest(options_.accessTokenUrl); + refreshRequest.setHeader(QNetworkRequest::ContentTypeHeader, MIME_TYPE_XFORM); + QMap parameters; + parameters.insert(OAUTH2_CLIENT_ID, options_.clientIdentifier); + if ( !options_.clientSecret.isEmpty() ) { + parameters.insert(OAUTH2_CLIENT_SECRET, options_.clientSecret); + } + parameters.insert(OAUTH2_REFRESH_TOKEN, refreshToken()); + parameters.insert(OAUTH2_GRANT_TYPE, OAUTH2_REFRESH_TOKEN); + + QByteArray data = buildRequestBody(parameters); + QNetworkReply *refreshReply = manager_->post(refreshRequest, data); + timedReplies_.add(refreshReply); + connect(refreshReply, &QNetworkReply::finished, this, &DeviceFlow::onRefreshFinished, Qt::QueuedConnection); + return true; +} + +void DeviceFlow::onRefreshFinished() { + QNetworkReply *refreshReply = qobject_cast(sender()); + + auto networkError = refreshReply->error(); + if (networkError == QNetworkReply::NoError) { + QByteArray reply = refreshReply->readAll(); + QVariantMap tokens = parseJsonResponse(reply); + setToken(tokens.value(OAUTH2_ACCESS_TOKEN).toString()); + setExpires(QDateTime::currentDateTimeUtc().addSecs(tokens.value(OAUTH2_EXPIRES_IN).toInt())); + QString refreshToken = tokens.value(OAUTH2_REFRESH_TOKEN).toString(); + if(!refreshToken.isEmpty()) { + setRefreshToken(refreshToken); + } + else { + qDebug() << "No new refresh token. Keep the old one."; + } + timedReplies_.remove(refreshReply); + refreshReply->deleteLater(); + updateActivity(Activity::Succeeded); + qDebug() << "New token expires in" << expires() << "seconds"; + } else { + // FIXME: differentiate the error more here + onRefreshError(networkError, refreshReply); + } +} + +void DeviceFlow::onRefreshError(QNetworkReply::NetworkError error, QNetworkReply *refreshReply) { + QString errorString = "No Reply"; + if(refreshReply) { + timedReplies_.remove(refreshReply); + errorString = refreshReply->errorString(); + } + + switch (error) + { + // used for invalid credentials and similar errors. Fall through. + case QNetworkReply::AuthenticationRequiredError: + case QNetworkReply::ContentAccessDenied: + case QNetworkReply::ContentOperationNotPermittedError: + updateActivity(Activity::FailedHard); + break; + case QNetworkReply::ContentGoneError: { + updateActivity(Activity::FailedGone); + break; + } + case QNetworkReply::TimeoutError: + case QNetworkReply::OperationCanceledError: + case QNetworkReply::SslHandshakeFailedError: + default: + updateActivity(Activity::FailedSoft); + return; + } + if(refreshReply) { + refreshReply->deleteLater(); + } + qDebug() << "DeviceFlow::onRefreshFinished: Error" << (int)error << " - " << errorString; +} + +} diff --git a/libraries/katabasis/src/OAuth2.cpp b/libraries/katabasis/src/OAuth2.cpp deleted file mode 100644 index 260aa9c1..00000000 --- a/libraries/katabasis/src/OAuth2.cpp +++ /dev/null @@ -1,672 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "katabasis/OAuth2.h" -#include "katabasis/PollServer.h" -#include "katabasis/ReplyServer.h" -#include "katabasis/Globals.h" - -#include "JsonResponse.h" - -namespace { -// ref: https://tools.ietf.org/html/rfc8628#section-3.2 -// Exception: Google sign-in uses "verification_url" instead of "*_uri" - we'll accept both. -bool hasMandatoryDeviceAuthParams(const QVariantMap& params) -{ - if (!params.contains(Katabasis::OAUTH2_DEVICE_CODE)) - return false; - - if (!params.contains(Katabasis::OAUTH2_USER_CODE)) - return false; - - if (!(params.contains(Katabasis::OAUTH2_VERIFICATION_URI) || params.contains(Katabasis::OAUTH2_VERIFICATION_URL))) - return false; - - if (!params.contains(Katabasis::OAUTH2_EXPIRES_IN)) - return false; - - return true; -} - -QByteArray createQueryParameters(const QList ¶meters) { - QByteArray ret; - bool first = true; - for( auto & h: parameters) { - if (first) { - first = false; - } else { - ret.append("&"); - } - ret.append(QUrl::toPercentEncoding(h.name) + "=" + QUrl::toPercentEncoding(h.value)); - } - return ret; -} -} - -namespace Katabasis { - -OAuth2::OAuth2(Options & opts, Token & token, QObject *parent, QNetworkAccessManager *manager) : QObject(parent), token_(token) { - manager_ = manager ? manager : new QNetworkAccessManager(this); - grantFlow_ = GrantFlowAuthorizationCode; - qRegisterMetaType("QNetworkReply::NetworkError"); - options_ = opts; -} - -bool OAuth2::linked() { - return token_.validity != Validity::None; -} -void OAuth2::setLinked(bool v) { - qDebug() << "OAuth2::setLinked:" << (v? "true": "false"); - token_.validity = v ? Validity::Certain : Validity::None; -} - -QString OAuth2::token() { - return token_.token; -} -void OAuth2::setToken(const QString &v) { - token_.token = v; -} - -QByteArray OAuth2::replyContent() const { - return replyContent_; -} - -void OAuth2::setReplyContent(const QByteArray &value) { - replyContent_ = value; - if (replyServer_) { - replyServer_->setReplyContent(replyContent_); - } -} - -QVariantMap OAuth2::extraTokens() { - return token_.extra; -} - -void OAuth2::setExtraTokens(QVariantMap extraTokens) { - token_.extra = extraTokens; -} - -void OAuth2::setReplyServer(ReplyServer * server) -{ - delete replyServer_; - - replyServer_ = server; - replyServer_->setReplyContent(replyContent_); -} - -ReplyServer * OAuth2::replyServer() const -{ - return replyServer_; -} - -void OAuth2::setPollServer(PollServer *server) -{ - if (pollServer_) - pollServer_->deleteLater(); - - pollServer_ = server; -} - -PollServer *OAuth2::pollServer() const -{ - return pollServer_; -} - -OAuth2::GrantFlow OAuth2::grantFlow() { - return grantFlow_; -} - -void OAuth2::setGrantFlow(OAuth2::GrantFlow value) { - grantFlow_ = value; -} - -QString OAuth2::username() { - return username_; -} - -void OAuth2::setUsername(const QString &value) { - username_ = value; -} - -QString OAuth2::password() { - return password_; -} - -void OAuth2::setPassword(const QString &value) { - password_ = value; -} - -QVariantMap OAuth2::extraRequestParams() -{ - return extraReqParams_; -} - -void OAuth2::setExtraRequestParams(const QVariantMap &value) -{ - extraReqParams_ = value; -} - -QString OAuth2::grantType() -{ - if (!grantType_.isEmpty()) - return grantType_; - - switch (grantFlow_) { - case GrantFlowAuthorizationCode: - return OAUTH2_GRANT_TYPE_CODE; - case GrantFlowImplicit: - return OAUTH2_GRANT_TYPE_TOKEN; - case GrantFlowResourceOwnerPasswordCredentials: - return OAUTH2_GRANT_TYPE_PASSWORD; - case GrantFlowDevice: - return OAUTH2_GRANT_TYPE_DEVICE; - } - - return QString(); -} - -void OAuth2::setGrantType(const QString &value) -{ - grantType_ = value; -} - -void OAuth2::updateActivity(Activity activity) -{ - if(activity_ != activity) { - activity_ = activity; - emit activityChanged(activity_); - } -} - -void OAuth2::link() { - qDebug() << "OAuth2::link"; - - // Create the reply server if it doesn't exist - if(replyServer() == NULL) { - ReplyServer * replyServer = new ReplyServer(this); - connect(replyServer, &ReplyServer::verificationReceived, this, &OAuth2::onVerificationReceived); - connect(replyServer, &ReplyServer::serverClosed, this, &OAuth2::serverHasClosed); - setReplyServer(replyServer); - } - - if (linked()) { - qDebug() << "OAuth2::link: Linked already"; - emit linkingSucceeded(); - return; - } - - setLinked(false); - setToken(""); - setExtraTokens(QVariantMap()); - setRefreshToken(QString()); - setExpires(QDateTime()); - - if (grantFlow_ == GrantFlowAuthorizationCode || grantFlow_ == GrantFlowImplicit) { - - QString uniqueState = QUuid::createUuid().toString().remove(QRegExp("([^a-zA-Z0-9]|[-])")); - - // FIXME: this should be part of a 'redirection handler' that would get injected into O2 - { - quint16 foundPort = 0; - // Start listening to authentication replies - if (!replyServer()->isListening()) { - auto ports = options_.listenerPorts; - for(auto & port: ports) { - if (replyServer()->listen(QHostAddress::Any, port)) { - foundPort = replyServer()->serverPort(); - qDebug() << "OAuth2::link: Reply server listening on port " << foundPort; - break; - } - } - if(foundPort == 0) { - qWarning() << "OAuth2::link: Reply server failed to start listening on any port out of " << ports; - emit linkingFailed(); - return; - } - } - - // Save redirect URI, as we have to reuse it when requesting the access token - redirectUri_ = options_.redirectionUrl.arg(foundPort); - replyServer()->setUniqueState(uniqueState); - } - - // Assemble intial authentication URL - QUrl url(options_.authorizationUrl); - QUrlQuery query(url); - QList > parameters; - query.addQueryItem(OAUTH2_RESPONSE_TYPE, (grantFlow_ == GrantFlowAuthorizationCode)? OAUTH2_GRANT_TYPE_CODE: OAUTH2_GRANT_TYPE_TOKEN); - query.addQueryItem(OAUTH2_CLIENT_ID, options_.clientIdentifier); - query.addQueryItem(OAUTH2_REDIRECT_URI, redirectUri_); - query.addQueryItem(OAUTH2_SCOPE, options_.scope.replace( " ", "+" )); - query.addQueryItem(OAUTH2_STATE, uniqueState); - if (!apiKey_.isEmpty()) { - query.addQueryItem(OAUTH2_API_KEY, apiKey_); - } - for(auto iter = extraReqParams_.begin(); iter != extraReqParams_.end(); iter++) { - query.addQueryItem(iter.key(), iter.value().toString()); - } - url.setQuery(query); - - // Show authentication URL with a web browser - qDebug() << "OAuth2::link: Emit openBrowser" << url.toString(); - emit openBrowser(url); - updateActivity(Activity::LoggingIn); - } else if (grantFlow_ == GrantFlowResourceOwnerPasswordCredentials) { - QList parameters; - parameters.append(RequestParameter(OAUTH2_CLIENT_ID, options_.clientIdentifier.toUtf8())); - if ( !options_.clientSecret.isEmpty() ) { - parameters.append(RequestParameter(OAUTH2_CLIENT_SECRET, options_.clientSecret.toUtf8())); - } - parameters.append(RequestParameter(OAUTH2_USERNAME, username_.toUtf8())); - parameters.append(RequestParameter(OAUTH2_PASSWORD, password_.toUtf8())); - parameters.append(RequestParameter(OAUTH2_GRANT_TYPE, OAUTH2_GRANT_TYPE_PASSWORD)); - parameters.append(RequestParameter(OAUTH2_SCOPE, options_.scope.toUtf8())); - if ( !apiKey_.isEmpty() ) - parameters.append(RequestParameter(OAUTH2_API_KEY, apiKey_.toUtf8())); - foreach (QString key, extraRequestParams().keys()) { - parameters.append(RequestParameter(key.toUtf8(), extraRequestParams().value(key).toByteArray())); - } - QByteArray payload = createQueryParameters(parameters); - - qDebug() << "OAuth2::link: Sending token request for resource owner flow"; - QUrl url(options_.accessTokenUrl); - QNetworkRequest tokenRequest(url); - tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - QNetworkReply *tokenReply = manager_->post(tokenRequest, payload); - - connect(tokenReply, SIGNAL(finished()), this, SLOT(onTokenReplyFinished()), Qt::QueuedConnection); - connect(tokenReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onTokenReplyError(QNetworkReply::NetworkError)), Qt::QueuedConnection); - updateActivity(Activity::LoggingIn); - } - else if (grantFlow_ == GrantFlowDevice) { - QList parameters; - parameters.append(RequestParameter(OAUTH2_CLIENT_ID, options_.clientIdentifier.toUtf8())); - parameters.append(RequestParameter(OAUTH2_SCOPE, options_.scope.toUtf8())); - QByteArray payload = createQueryParameters(parameters); - - QUrl url(options_.authorizationUrl); - QNetworkRequest deviceRequest(url); - deviceRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - QNetworkReply *tokenReply = manager_->post(deviceRequest, payload); - - connect(tokenReply, SIGNAL(finished()), this, SLOT(onDeviceAuthReplyFinished()), Qt::QueuedConnection); - connect(tokenReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onTokenReplyError(QNetworkReply::NetworkError)), Qt::QueuedConnection); - updateActivity(Activity::LoggingIn); - } -} - -void OAuth2::unlink() { - qDebug() << "OAuth2::unlink"; - updateActivity(Activity::LoggingOut); - // FIXME: implement logout flows... if they exist - token_ = Token(); - updateActivity(Activity::Idle); -} - -void OAuth2::onVerificationReceived(const QMap response) { - qDebug() << "OAuth2::onVerificationReceived: Emitting closeBrowser()"; - emit closeBrowser(); - - if (response.contains("error")) { - qWarning() << "OAuth2::onVerificationReceived: Verification failed:" << response; - emit linkingFailed(); - updateActivity(Activity::Idle); - return; - } - - if (grantFlow_ == GrantFlowAuthorizationCode) { - // NOTE: access code is temporary and should never be saved anywhere! - auto access_code = response.value(QString(OAUTH2_GRANT_TYPE_CODE)); - - // Exchange access code for access/refresh tokens - QString query; - if(!apiKey_.isEmpty()) - query = QString("?" + QString(OAUTH2_API_KEY) + "=" + apiKey_); - QNetworkRequest tokenRequest(QUrl(options_.accessTokenUrl.toString() + query)); - tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, MIME_TYPE_XFORM); - tokenRequest.setRawHeader("Accept", MIME_TYPE_JSON); - QMap parameters; - parameters.insert(OAUTH2_GRANT_TYPE_CODE, access_code); - parameters.insert(OAUTH2_CLIENT_ID, options_.clientIdentifier); - if ( !options_.clientSecret.isEmpty() ) { - parameters.insert(OAUTH2_CLIENT_SECRET, options_.clientSecret); - } - parameters.insert(OAUTH2_REDIRECT_URI, redirectUri_); - parameters.insert(OAUTH2_GRANT_TYPE, AUTHORIZATION_CODE); - QByteArray data = buildRequestBody(parameters); - - qDebug() << QString("OAuth2::onVerificationReceived: Exchange access code data:\n%1").arg(QString(data)); - - QNetworkReply *tokenReply = manager_->post(tokenRequest, data); - timedReplies_.add(tokenReply); - connect(tokenReply, SIGNAL(finished()), this, SLOT(onTokenReplyFinished()), Qt::QueuedConnection); - connect(tokenReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onTokenReplyError(QNetworkReply::NetworkError)), Qt::QueuedConnection); - } else if (grantFlow_ == GrantFlowImplicit || grantFlow_ == GrantFlowDevice) { - // Check for mandatory tokens - if (response.contains(OAUTH2_ACCESS_TOKEN)) { - qDebug() << "OAuth2::onVerificationReceived: Access token returned for implicit or device flow"; - setToken(response.value(OAUTH2_ACCESS_TOKEN)); - if (response.contains(OAUTH2_EXPIRES_IN)) { - bool ok = false; - int expiresIn = response.value(OAUTH2_EXPIRES_IN).toInt(&ok); - if (ok) { - qDebug() << "OAuth2::onVerificationReceived: Token expires in" << expiresIn << "seconds"; - setExpires(QDateTime::currentDateTimeUtc().addSecs(expiresIn)); - } - } - if (response.contains(OAUTH2_REFRESH_TOKEN)) { - setRefreshToken(response.value(OAUTH2_REFRESH_TOKEN)); - } - setLinked(true); - emit linkingSucceeded(); - } else { - qWarning() << "OAuth2::onVerificationReceived: Access token missing from response for implicit or device flow"; - emit linkingFailed(); - } - updateActivity(Activity::Idle); - } else { - setToken(response.value(OAUTH2_ACCESS_TOKEN)); - setRefreshToken(response.value(OAUTH2_REFRESH_TOKEN)); - updateActivity(Activity::Idle); - } -} - -void OAuth2::onTokenReplyFinished() { - qDebug() << "OAuth2::onTokenReplyFinished"; - QNetworkReply *tokenReply = qobject_cast(sender()); - if (!tokenReply) - { - qDebug() << "OAuth2::onTokenReplyFinished: reply is null"; - return; - } - if (tokenReply->error() == QNetworkReply::NoError) { - QByteArray replyData = tokenReply->readAll(); - - // Dump replyData - // SENSITIVE DATA in RelWithDebInfo or Debug builds - //qDebug() << "OAuth2::onTokenReplyFinished: replyData\n"; - //qDebug() << QString( replyData ); - - QVariantMap tokens = parseJsonResponse(replyData); - - // Dump tokens - qDebug() << "OAuth2::onTokenReplyFinished: Tokens returned:\n"; - foreach (QString key, tokens.keys()) { - // SENSITIVE DATA in RelWithDebInfo or Debug builds, so it is truncated first - qDebug() << key << ": "<< tokens.value( key ).toString(); - } - - // Check for mandatory tokens - if (tokens.contains(OAUTH2_ACCESS_TOKEN)) { - qDebug() << "OAuth2::onTokenReplyFinished: Access token returned"; - setToken(tokens.take(OAUTH2_ACCESS_TOKEN).toString()); - bool ok = false; - int expiresIn = tokens.take(OAUTH2_EXPIRES_IN).toInt(&ok); - if (ok) { - qDebug() << "OAuth2::onTokenReplyFinished: Token expires in" << expiresIn << "seconds"; - setExpires(QDateTime::currentDateTimeUtc().addSecs(expiresIn)); - } - setRefreshToken(tokens.take(OAUTH2_REFRESH_TOKEN).toString()); - setExtraTokens(tokens); - timedReplies_.remove(tokenReply); - setLinked(true); - emit linkingSucceeded(); - } else { - qWarning() << "OAuth2::onTokenReplyFinished: Access token missing from response"; - emit linkingFailed(); - } - } - tokenReply->deleteLater(); - updateActivity(Activity::Idle); -} - -void OAuth2::onTokenReplyError(QNetworkReply::NetworkError error) { - QNetworkReply *tokenReply = qobject_cast(sender()); - if (!tokenReply) - { - qDebug() << "OAuth2::onTokenReplyError: reply is null"; - } else { - qWarning() << "OAuth2::onTokenReplyError: " << error << ": " << tokenReply->errorString(); - qDebug() << "OAuth2::onTokenReplyError: " << tokenReply->readAll(); - timedReplies_.remove(tokenReply); - } - - setToken(QString()); - setRefreshToken(QString()); - emit linkingFailed(); -} - -QByteArray OAuth2::buildRequestBody(const QMap ¶meters) { - QByteArray body; - bool first = true; - foreach (QString key, parameters.keys()) { - if (first) { - first = false; - } else { - body.append("&"); - } - QString value = parameters.value(key); - body.append(QUrl::toPercentEncoding(key) + QString("=").toUtf8() + QUrl::toPercentEncoding(value)); - } - return body; -} - -QDateTime OAuth2::expires() { - return token_.notAfter; -} -void OAuth2::setExpires(QDateTime v) { - token_.notAfter = v; -} - -void OAuth2::startPollServer(const QVariantMap ¶ms, int expiresIn) -{ - qDebug() << "OAuth2::startPollServer: device_ and user_code expires in" << expiresIn << "seconds"; - - QUrl url(options_.accessTokenUrl); - QNetworkRequest authRequest(url); - authRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - - const QString deviceCode = params[OAUTH2_DEVICE_CODE].toString(); - const QString grantType = grantType_.isEmpty() ? OAUTH2_GRANT_TYPE_DEVICE : grantType_; - - QList parameters; - parameters.append(RequestParameter(OAUTH2_CLIENT_ID, options_.clientIdentifier.toUtf8())); - if ( !options_.clientSecret.isEmpty() ) { - parameters.append(RequestParameter(OAUTH2_CLIENT_SECRET, options_.clientSecret.toUtf8())); - } - parameters.append(RequestParameter(OAUTH2_CODE, deviceCode.toUtf8())); - parameters.append(RequestParameter(OAUTH2_GRANT_TYPE, grantType.toUtf8())); - QByteArray payload = createQueryParameters(parameters); - - 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); - } - connect(pollServer, SIGNAL(verificationReceived(QMap)), this, SLOT(onVerificationReceived(QMap))); - connect(pollServer, SIGNAL(serverClosed(bool)), this, SLOT(serverHasClosed(bool))); - setPollServer(pollServer); - pollServer->startPolling(); -} - -QString OAuth2::refreshToken() { - return token_.refresh_token; -} -void OAuth2::setRefreshToken(const QString &v) { -#ifndef NDEBUG - qDebug() << "OAuth2::setRefreshToken" << v << "..."; -#endif - token_.refresh_token = v; -} - -bool OAuth2::refresh() { - qDebug() << "OAuth2::refresh: Token: ..." << refreshToken().right(7); - - if (refreshToken().isEmpty()) { - qWarning() << "OAuth2::refresh: No refresh token"; - onRefreshError(QNetworkReply::AuthenticationRequiredError); - return false; - } - if (options_.accessTokenUrl.isEmpty()) { - qWarning() << "OAuth2::refresh: Refresh token URL not set"; - onRefreshError(QNetworkReply::AuthenticationRequiredError); - return false; - } - - updateActivity(Activity::Refreshing); - - QNetworkRequest refreshRequest(options_.accessTokenUrl); - refreshRequest.setHeader(QNetworkRequest::ContentTypeHeader, MIME_TYPE_XFORM); - QMap parameters; - parameters.insert(OAUTH2_CLIENT_ID, options_.clientIdentifier); - if ( !options_.clientSecret.isEmpty() ) { - parameters.insert(OAUTH2_CLIENT_SECRET, options_.clientSecret); - } - parameters.insert(OAUTH2_REFRESH_TOKEN, refreshToken()); - parameters.insert(OAUTH2_GRANT_TYPE, OAUTH2_REFRESH_TOKEN); - - QByteArray data = buildRequestBody(parameters); - QNetworkReply *refreshReply = manager_->post(refreshRequest, data); - timedReplies_.add(refreshReply); - connect(refreshReply, SIGNAL(finished()), this, SLOT(onRefreshFinished()), Qt::QueuedConnection); - connect(refreshReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRefreshError(QNetworkReply::NetworkError)), Qt::QueuedConnection); - return true; -} - -void OAuth2::onRefreshFinished() { - QNetworkReply *refreshReply = qobject_cast(sender()); - - if (refreshReply->error() == QNetworkReply::NoError) { - QByteArray reply = refreshReply->readAll(); - QVariantMap tokens = parseJsonResponse(reply); - setToken(tokens.value(OAUTH2_ACCESS_TOKEN).toString()); - setExpires(QDateTime::currentDateTimeUtc().addSecs(tokens.value(OAUTH2_EXPIRES_IN).toInt())); - QString refreshToken = tokens.value(OAUTH2_REFRESH_TOKEN).toString(); - if(!refreshToken.isEmpty()) { - setRefreshToken(refreshToken); - } - else { - qDebug() << "No new refresh token. Keep the old one."; - } - timedReplies_.remove(refreshReply); - setLinked(true); - emit linkingSucceeded(); - emit refreshFinished(QNetworkReply::NoError); - qDebug() << "New token expires in" << expires() << "seconds"; - } else { - emit linkingFailed(); - qDebug() << "OAuth2::onRefreshFinished: Error" << (int)refreshReply->error() << refreshReply->errorString(); - } - refreshReply->deleteLater(); - updateActivity(Activity::Idle); -} - -void OAuth2::onRefreshError(QNetworkReply::NetworkError error) { - QNetworkReply *refreshReply = qobject_cast(sender()); - qWarning() << "OAuth2::onRefreshError: " << error; - unlink(); - timedReplies_.remove(refreshReply); - emit refreshFinished(error); -} - -void OAuth2::onDeviceAuthReplyFinished() -{ - qDebug() << "OAuth2::onDeviceAuthReplyFinished"; - QNetworkReply *tokenReply = qobject_cast(sender()); - if (!tokenReply) - { - qDebug() << "OAuth2::onDeviceAuthReplyFinished: reply is null"; - return; - } - if (tokenReply->error() == QNetworkReply::NoError) { - QByteArray replyData = tokenReply->readAll(); - - // Dump replyData - // SENSITIVE DATA in RelWithDebInfo or Debug builds - //qDebug() << "OAuth2::onDeviceAuthReplyFinished: replyData\n"; - //qDebug() << QString( replyData ); - - QVariantMap params = parseJsonResponse(replyData); - - // Dump tokens - qDebug() << "OAuth2::onDeviceAuthReplyFinished: Tokens returned:\n"; - foreach (QString key, params.keys()) { - // SENSITIVE DATA in RelWithDebInfo or Debug builds, so it is truncated first - qDebug() << key << ": "<< params.value( key ).toString(); - } - - // Check for mandatory parameters - if (hasMandatoryDeviceAuthParams(params)) { - qDebug() << "OAuth2::onDeviceAuthReplyFinished: Device auth request response"; - - const QString userCode = params.take(OAUTH2_USER_CODE).toString(); - QUrl uri = params.take(OAUTH2_VERIFICATION_URI).toUrl(); - if (uri.isEmpty()) - uri = params.take(OAUTH2_VERIFICATION_URL).toUrl(); - - if (params.contains(OAUTH2_VERIFICATION_URI_COMPLETE)) - emit openBrowser(params.take(OAUTH2_VERIFICATION_URI_COMPLETE).toUrl()); - - 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, expiresIn); - } else { - qWarning() << "OAuth2::onDeviceAuthReplyFinished: Mandatory parameters missing from response"; - emit linkingFailed(); - updateActivity(Activity::Idle); - } - } - tokenReply->deleteLater(); -} - -void OAuth2::serverHasClosed(bool paramsfound) -{ - if ( !paramsfound ) { - // server has probably timed out after receiving first response - emit linkingFailed(); - } - // poll server is not re-used for later auth requests - setPollServer(NULL); -} - -QString OAuth2::apiKey() { - return apiKey_; -} - -void OAuth2::setApiKey(const QString &value) { - apiKey_ = value; -} - -bool OAuth2::ignoreSslErrors() { - return timedReplies_.ignoreSslErrors(); -} - -void OAuth2::setIgnoreSslErrors(bool ignoreSslErrors) { - timedReplies_.setIgnoreSslErrors(ignoreSslErrors); -} - -} diff --git a/libraries/katabasis/src/Reply.cpp b/libraries/katabasis/src/Reply.cpp index 775b9202..3e27a7e6 100644 --- a/libraries/katabasis/src/Reply.cpp +++ b/libraries/katabasis/src/Reply.cpp @@ -7,25 +7,28 @@ namespace Katabasis { Reply::Reply(QNetworkReply *r, int timeOut, QObject *parent): QTimer(parent), reply(r) { setSingleShot(true); - connect(this, SIGNAL(error(QNetworkReply::NetworkError)), reply, SIGNAL(error(QNetworkReply::NetworkError)), Qt::QueuedConnection); - connect(this, SIGNAL(timeout()), this, SLOT(onTimeOut()), Qt::QueuedConnection); + connect(this, &Reply::timeout, this, &Reply::onTimeOut, Qt::QueuedConnection); start(timeOut); } void Reply::onTimeOut() { - emit error(QNetworkReply::TimeoutError); + timedOut = true; + reply->abort(); } +// ---------------------------- + ReplyList::~ReplyList() { foreach (Reply *timedReply, replies_) { delete timedReply; } } -void ReplyList::add(QNetworkReply *reply) { - if (reply && ignoreSslErrors()) - reply->ignoreSslErrors(); - add(new Reply(reply)); +void ReplyList::add(QNetworkReply *reply, int timeOut) { + if (reply && ignoreSslErrors()) { + reply->ignoreSslErrors(); + } + add(new Reply(reply, timeOut)); } void ReplyList::add(Reply *reply) { diff --git a/libraries/katabasis/src/ReplyServer.cpp b/libraries/katabasis/src/ReplyServer.cpp deleted file mode 100755 index 4598b18a..00000000 --- a/libraries/katabasis/src/ReplyServer.cpp +++ /dev/null @@ -1,182 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "katabasis/Globals.h" -#include "katabasis/ReplyServer.h" - -namespace Katabasis { - -ReplyServer::ReplyServer(QObject *parent): QTcpServer(parent), - timeout_(15), maxtries_(3), tries_(0) { - qDebug() << "O2ReplyServer: Starting"; - connect(this, SIGNAL(newConnection()), this, SLOT(onIncomingConnection())); - replyContent_ = ""; -} - -void ReplyServer::onIncomingConnection() { - qDebug() << "O2ReplyServer::onIncomingConnection: Receiving..."; - QTcpSocket *socket = nextPendingConnection(); - connect(socket, SIGNAL(readyRead()), this, SLOT(onBytesReady()), Qt::UniqueConnection); - connect(socket, SIGNAL(disconnected()), socket, SLOT(deleteLater())); - - // Wait for a bit *after* first response, then close server if no useable data has arrived - // Helps with implicit flow, where a URL fragment may need processed by local user-agent and - // sent as secondary query string callback, or additional requests make it through first, - // like for favicons, etc., before such secondary callbacks are fired - QTimer *timer = new QTimer(socket); - timer->setObjectName("timeoutTimer"); - connect(timer, SIGNAL(timeout()), this, SLOT(closeServer())); - timer->setSingleShot(true); - timer->setInterval(timeout() * 1000); - connect(socket, SIGNAL(readyRead()), timer, SLOT(start())); -} - -void ReplyServer::onBytesReady() { - if (!isListening()) { - // server has been closed, stop processing queued connections - return; - } - qDebug() << "O2ReplyServer::onBytesReady: Processing request"; - // NOTE: on first call, the timeout timer is started - QTcpSocket *socket = qobject_cast(sender()); - if (!socket) { - qWarning() << "O2ReplyServer::onBytesReady: No socket available"; - return; - } - QByteArray reply; - reply.append("HTTP/1.0 200 OK \r\n"); - reply.append("Content-Type: text/html; charset=\"utf-8\"\r\n"); - reply.append(QString("Content-Length: %1\r\n\r\n").arg(replyContent_.size()).toLatin1()); - reply.append(replyContent_); - socket->write(reply); - qDebug() << "O2ReplyServer::onBytesReady: Sent reply"; - - QByteArray data = socket->readAll(); - QMap queryParams = parseQueryParams(&data); - if (queryParams.isEmpty()) { - if (tries_ < maxtries_ ) { - qDebug() << "O2ReplyServer::onBytesReady: No query params found, waiting for more callbacks"; - ++tries_; - return; - } else { - tries_ = 0; - qWarning() << "O2ReplyServer::onBytesReady: No query params found, maximum callbacks received"; - closeServer(socket, false); - return; - } - } - if (!uniqueState_.isEmpty() && !queryParams.contains(QString(OAUTH2_STATE))) { - qDebug() << "O2ReplyServer::onBytesReady: Malicious or service request"; - closeServer(socket, true); - return; // Malicious or service (e.g. favicon.ico) request - } - qDebug() << "O2ReplyServer::onBytesReady: Query params found, closing server"; - closeServer(socket, true); - emit verificationReceived(queryParams); -} - -QMap ReplyServer::parseQueryParams(QByteArray *data) { - qDebug() << "O2ReplyServer::parseQueryParams"; - - //qDebug() << QString("O2ReplyServer::parseQueryParams data:\n%1").arg(QString(*data)); - - QString splitGetLine = QString(*data).split("\r\n").first(); - splitGetLine.remove("GET "); - splitGetLine.remove("HTTP/1.1"); - splitGetLine.remove("\r\n"); - splitGetLine.prepend("http://localhost"); - QUrl getTokenUrl(splitGetLine); - - QList< QPair > tokens; - QUrlQuery query(getTokenUrl); - tokens = query.queryItems(); - QMap queryParams; - QPair tokenPair; - foreach (tokenPair, tokens) { - // FIXME: We are decoding key and value again. This helps with Google OAuth, but is it mandated by the standard? - QString key = QUrl::fromPercentEncoding(QByteArray().append(tokenPair.first.trimmed().toLatin1())); - QString value = QUrl::fromPercentEncoding(QByteArray().append(tokenPair.second.trimmed().toLatin1())); - queryParams.insert(key, value); - } - return queryParams; -} - -void ReplyServer::closeServer(QTcpSocket *socket, bool hasparameters) -{ - if (!isListening()) { - return; - } - - qDebug() << "O2ReplyServer::closeServer: Initiating"; - int port = serverPort(); - - if (!socket && sender()) { - QTimer *timer = qobject_cast(sender()); - if (timer) { - qWarning() << "O2ReplyServer::closeServer: Closing due to timeout"; - timer->stop(); - socket = qobject_cast(timer->parent()); - timer->deleteLater(); - } - } - if (socket) { - QTimer *timer = socket->findChild("timeoutTimer"); - if (timer) { - qDebug() << "O2ReplyServer::closeServer: Stopping socket's timeout timer"; - timer->stop(); - } - socket->disconnectFromHost(); - } - close(); - qDebug() << "O2ReplyServer::closeServer: Closed, no longer listening on port" << port; - emit serverClosed(hasparameters); -} - -QByteArray ReplyServer::replyContent() { - return replyContent_; -} - -void ReplyServer::setReplyContent(const QByteArray &value) { - replyContent_ = value; -} - -int ReplyServer::timeout() -{ - return timeout_; -} - -void ReplyServer::setTimeout(int timeout) -{ - timeout_ = timeout; -} - -int ReplyServer::callbackTries() -{ - return maxtries_; -} - -void ReplyServer::setCallbackTries(int maxtries) -{ - maxtries_ = maxtries; -} - -QString ReplyServer::uniqueState() -{ - return uniqueState_; -} - -void ReplyServer::setUniqueState(const QString &state) -{ - uniqueState_ = state; -} - -}