aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--launcher/minecraft/auth/AccountList.cpp9
-rw-r--r--launcher/minecraft/auth/MinecraftAccount.cpp24
-rw-r--r--launcher/minecraft/auth/MinecraftAccount.h9
-rw-r--r--launcher/minecraft/auth/flows/AuthContext.cpp109
-rw-r--r--launcher/minecraft/auth/flows/AuthContext.h7
-rw-r--r--launcher/minecraft/auth/flows/MSAInteractive.cpp3
-rw-r--r--launcher/ui/widgets/VersionListView.cpp1
-rw-r--r--libraries/katabasis/CMakeLists.txt6
-rw-r--r--libraries/katabasis/include/katabasis/Bits.h6
-rw-r--r--libraries/katabasis/include/katabasis/DeviceFlow.h150
-rw-r--r--libraries/katabasis/include/katabasis/OAuth2.h233
-rw-r--r--libraries/katabasis/include/katabasis/Reply.h7
-rw-r--r--libraries/katabasis/include/katabasis/ReplyServer.h53
-rw-r--r--libraries/katabasis/src/DeviceFlow.cpp450
-rw-r--r--libraries/katabasis/src/OAuth2.cpp672
-rw-r--r--libraries/katabasis/src/Reply.cpp17
-rwxr-xr-xlibraries/katabasis/src/ReplyServer.cpp182
17 files changed, 722 insertions, 1216 deletions
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<AccountTask> 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 <Application.h>
-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<OAuth2 *>(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);
+void AuthContext::onOAuthActivityChanged(Katabasis::Activity activity) {
+ 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();
-}
+ 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::onOAuthActivityChanged(Katabasis::Activity activity) {
- // respond to activity change here
+ }
}
void AuthContext::doUserAuth() {
@@ -226,7 +228,7 @@ void AuthContext::doSTSAuthMinecraft() {
void AuthContext::processSTSError(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> 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 <QNetworkReply>
#include <QImage>
-#include <katabasis/OAuth2.h>
+#include <katabasis/DeviceFlow.h>
#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 <QDrag>
#include <QPainter>
#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 <QNetworkAccessManager>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QPair>
+
+#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<QString, QString>);
+
+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 &params, 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 <QNetworkAccessManager>
-#include <QNetworkRequest>
-#include <QNetworkReply>
-#include <QPair>
-
-#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<quint16> 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<QString, QString>);
-
-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<QString, QString> &parameters);
-
- /// Set refresh token.
- void setRefreshToken(const QString &v);
-
- /// Set token expiration time.
- void setExpires(QDateTime v);
-
- /// Start polling authorization server
- void startPollServer(const QVariantMap &params, 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 <QTcpServer>
-#include <QMap>
-#include <QByteArray>
-#include <QString>
-
-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<QString, QString>);
- void serverClosed(bool); // whether it has found parameters
-
-public slots:
- void onIncomingConnection();
- void onBytesReady();
- QMap<QString, QString> 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 <QList>
+#include <QPair>
+#include <QDebug>
+#include <QTcpServer>
+#include <QMap>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QNetworkAccessManager>
+#include <QDateTime>
+#include <QCryptographicHash>
+#include <QTimer>
+#include <QVariantMap>
+#include <QUuid>
+#include <QDataStream>
+
+#include <QUrlQuery>
+
+#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<Katabasis::RequestParameter> &parameters) {
+ 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>("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<RequestParameter> 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<QNetworkReply *>(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 &params, 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<RequestParameter> 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<QString, QString> 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<QString, QString> &parameters) {
+ 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<QString, QString> 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<QNetworkReply *>(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 <QList>
-#include <QPair>
-#include <QDebug>
-#include <QTcpServer>
-#include <QMap>
-#include <QNetworkRequest>
-#include <QNetworkReply>
-#include <QNetworkAccessManager>
-#include <QDateTime>
-#include <QCryptographicHash>
-#include <QTimer>
-#include <QVariantMap>
-#include <QUuid>
-#include <QDataStream>
-
-#include <QUrlQuery>
-
-#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<Katabasis::RequestParameter> &parameters) {
- 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>("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<QPair<QString, QString> > 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<RequestParameter> 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<RequestParameter> 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<QString, QString> 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<QString, QString> 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<QNetworkReply *>(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<QNetworkReply *>(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<QString, QString> &parameters) {
- 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 &params, 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<RequestParameter> 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<QString,QString>)), this, SLOT(onVerificationReceived(QMap<QString,QString>)));
- 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<QString, QString> 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<QNetworkReply *>(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<QNetworkReply *>(sender());
- qWarning() << "OAuth2::onRefreshError: " << error;
- unlink();
- timedReplies_.remove(refreshReply);
- emit refreshFinished(error);
-}
-
-void OAuth2::onDeviceAuthReplyFinished()
-{
- qDebug() << "OAuth2::onDeviceAuthReplyFinished";
- QNetworkReply *tokenReply = qobject_cast<QNetworkReply *>(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 <QTcpServer>
-#include <QTcpSocket>
-#include <QByteArray>
-#include <QString>
-#include <QMap>
-#include <QPair>
-#include <QTimer>
-#include <QStringList>
-#include <QUrl>
-#include <QDebug>
-#include <QUrlQuery>
-
-#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_ = "<HTML></HTML>";
-}
-
-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<QTcpSocket *>(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<QString, QString> 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<QString, QString> 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<QString, QString> > tokens;
- QUrlQuery query(getTokenUrl);
- tokens = query.queryItems();
- QMap<QString, QString> queryParams;
- QPair<QString, QString> 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<QTimer*>(sender());
- if (timer) {
- qWarning() << "O2ReplyServer::closeServer: Closing due to timeout";
- timer->stop();
- socket = qobject_cast<QTcpSocket *>(timer->parent());
- timer->deleteLater();
- }
- }
- if (socket) {
- QTimer *timer = socket->findChild<QTimer*>("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;
-}
-
-}