aboutsummaryrefslogtreecommitdiff
path: root/libraries/katabasis/src
diff options
context:
space:
mode:
authorPetr Mrázek <peterix@gmail.com>2021-07-22 20:15:20 +0200
committerPetr Mrázek <peterix@gmail.com>2021-07-25 19:50:44 +0200
commitdd133680858351e3e07690e286882327a4f42ba5 (patch)
tree42ac11eb0d6cc58d3785c57f8571046cafe4b769 /libraries/katabasis/src
parent2568752af57258a33f27f4c2d0b8fc486fb3d100 (diff)
downloadPrismLauncher-dd133680858351e3e07690e286882327a4f42ba5.tar.gz
PrismLauncher-dd133680858351e3e07690e286882327a4f42ba5.tar.bz2
PrismLauncher-dd133680858351e3e07690e286882327a4f42ba5.zip
NOISSUE bulk addition of code from Katabasis
Diffstat (limited to 'libraries/katabasis/src')
-rw-r--r--libraries/katabasis/src/JsonResponse.cpp26
-rw-r--r--libraries/katabasis/src/JsonResponse.h12
-rw-r--r--libraries/katabasis/src/OAuth2.cpp668
-rw-r--r--libraries/katabasis/src/PollServer.cpp123
-rw-r--r--libraries/katabasis/src/Reply.cpp62
-rwxr-xr-xlibraries/katabasis/src/ReplyServer.cpp182
-rw-r--r--libraries/katabasis/src/Requestor.cpp304
7 files changed, 1377 insertions, 0 deletions
diff --git a/libraries/katabasis/src/JsonResponse.cpp b/libraries/katabasis/src/JsonResponse.cpp
new file mode 100644
index 00000000..63384d12
--- /dev/null
+++ b/libraries/katabasis/src/JsonResponse.cpp
@@ -0,0 +1,26 @@
+#include "JsonResponse.h"
+
+#include <QByteArray>
+#include <QDebug>
+#include <QJsonDocument>
+#include <QJsonObject>
+
+namespace Katabasis {
+
+QVariantMap parseJsonResponse(const QByteArray &data) {
+ QJsonParseError err;
+ QJsonDocument doc = QJsonDocument::fromJson(data, &err);
+ if (err.error != QJsonParseError::NoError) {
+ qWarning() << "parseTokenResponse: Failed to parse token response due to err:" << err.errorString();
+ return QVariantMap();
+ }
+
+ if (!doc.isObject()) {
+ qWarning() << "parseTokenResponse: Token response is not an object";
+ return QVariantMap();
+ }
+
+ return doc.object().toVariantMap();
+}
+
+}
diff --git a/libraries/katabasis/src/JsonResponse.h b/libraries/katabasis/src/JsonResponse.h
new file mode 100644
index 00000000..e7fe7e30
--- /dev/null
+++ b/libraries/katabasis/src/JsonResponse.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include <QVariantMap>
+
+class QByteArray;
+
+namespace Katabasis {
+
+ /// Parse JSON data into a QVariantMap
+QVariantMap parseJsonResponse(const QByteArray &data);
+
+}
diff --git a/libraries/katabasis/src/OAuth2.cpp b/libraries/katabasis/src/OAuth2.cpp
new file mode 100644
index 00000000..6cc03a0d
--- /dev/null
+++ b/libraries/katabasis/src/OAuth2.cpp
@@ -0,0 +1,668 @@
+#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)
+{
+ bool ok = false;
+ int expiresIn = params[OAUTH2_EXPIRES_IN].toInt(&ok);
+ if (!ok) {
+ qWarning() << "OAuth2::startPollServer: No expired_in parameter";
+ emit linkingFailed();
+ return;
+ }
+
+ qDebug() << "OAuth2::startPollServer: device_ and user_code expires in" << expiresIn << "seconds";
+
+ QUrl url(options_.accessTokenUrl);
+ 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)) {
+ 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) {
+ qDebug() << "OAuth2::setRefreshToken" << v << "...";
+ 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 {
+ 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());
+
+ emit showVerificationUriAndCode(uri, userCode);
+
+ startPollServer(params);
+ } 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/PollServer.cpp b/libraries/katabasis/src/PollServer.cpp
new file mode 100644
index 00000000..1083c599
--- /dev/null
+++ b/libraries/katabasis/src/PollServer.cpp
@@ -0,0 +1,123 @@
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+
+#include "katabasis/PollServer.h"
+#include "JsonResponse.h"
+
+namespace {
+QMap<QString, QString> toVerificationParams(const QVariantMap &map)
+{
+ QMap<QString, QString> params;
+ for (QVariantMap::const_iterator i = map.constBegin();
+ i != map.constEnd(); ++i)
+ {
+ params[i.key()] = i.value().toString();
+ }
+ return params;
+}
+}
+
+namespace Katabasis {
+
+PollServer::PollServer(QNetworkAccessManager *manager, const QNetworkRequest &request, const QByteArray &payload, int expiresIn, QObject *parent)
+ : QObject(parent)
+ , manager_(manager)
+ , request_(request)
+ , payload_(payload)
+ , expiresIn_(expiresIn)
+{
+ expirationTimer.setTimerType(Qt::VeryCoarseTimer);
+ expirationTimer.setInterval(expiresIn * 1000);
+ expirationTimer.setSingleShot(true);
+ connect(&expirationTimer, SIGNAL(timeout()), this, SLOT(onExpiration()));
+ expirationTimer.start();
+
+ pollTimer.setTimerType(Qt::VeryCoarseTimer);
+ pollTimer.setInterval(5 * 1000);
+ pollTimer.setSingleShot(true);
+ connect(&pollTimer, SIGNAL(timeout()), this, SLOT(onPollTimeout()));
+}
+
+int PollServer::interval() const
+{
+ return pollTimer.interval() / 1000;
+}
+
+void PollServer::setInterval(int interval)
+{
+ pollTimer.setInterval(interval * 1000);
+}
+
+void PollServer::startPolling()
+{
+ if (expirationTimer.isActive()) {
+ pollTimer.start();
+ }
+}
+
+void PollServer::onPollTimeout()
+{
+ qDebug() << "PollServer::onPollTimeout: retrying";
+ QNetworkReply * reply = manager_->post(request_, payload_);
+ connect(reply, SIGNAL(finished()), this, SLOT(onReplyFinished()));
+}
+
+void PollServer::onExpiration()
+{
+ pollTimer.stop();
+ emit serverClosed(false);
+}
+
+void PollServer::onReplyFinished()
+{
+ QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
+
+ if (!reply) {
+ qDebug() << "PollServer::onReplyFinished: reply is null";
+ return;
+ }
+
+ QByteArray replyData = reply->readAll();
+ QMap<QString, QString> params = toVerificationParams(parseJsonResponse(replyData));
+
+ // Dump replyData
+ // SENSITIVE DATA in RelWithDebInfo or Debug builds
+ // qDebug() << "PollServer::onReplyFinished: replyData\n";
+ // qDebug() << QString( replyData );
+
+ if (reply->error() == QNetworkReply::TimeoutError) {
+ // rfc8628#section-3.2
+ // "On encountering a connection timeout, clients MUST unilaterally
+ // reduce their polling frequency before retrying. The use of an
+ // exponential backoff algorithm to achieve this, such as doubling the
+ // polling interval on each such connection timeout, is RECOMMENDED."
+ setInterval(interval() * 2);
+ pollTimer.start();
+ }
+ else {
+ QString error = params.value("error");
+ if (error == "slow_down") {
+ // rfc8628#section-3.2
+ // "A variant of 'authorization_pending', the authorization request is
+ // still pending and polling should continue, but the interval MUST
+ // be increased by 5 seconds for this and all subsequent requests."
+ setInterval(interval() + 5);
+ pollTimer.start();
+ }
+ else if (error == "authorization_pending") {
+ // keep trying - rfc8628#section-3.2
+ // "The authorization request is still pending as the end user hasn't
+ // yet completed the user-interaction steps (Section 3.3)."
+ pollTimer.start();
+ }
+ else {
+ expirationTimer.stop();
+ emit serverClosed(true);
+ // let O2 handle the other cases
+ emit verificationReceived(params);
+ }
+ }
+ reply->deleteLater();
+}
+
+}
diff --git a/libraries/katabasis/src/Reply.cpp b/libraries/katabasis/src/Reply.cpp
new file mode 100644
index 00000000..775b9202
--- /dev/null
+++ b/libraries/katabasis/src/Reply.cpp
@@ -0,0 +1,62 @@
+#include <QTimer>
+#include <QNetworkReply>
+
+#include "katabasis/Reply.h"
+
+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);
+ start(timeOut);
+}
+
+void Reply::onTimeOut() {
+ emit error(QNetworkReply::TimeoutError);
+}
+
+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(Reply *reply) {
+ replies_.append(reply);
+}
+
+void ReplyList::remove(QNetworkReply *reply) {
+ Reply *o2Reply = find(reply);
+ if (o2Reply) {
+ o2Reply->stop();
+ (void)replies_.removeOne(o2Reply);
+ }
+}
+
+Reply *ReplyList::find(QNetworkReply *reply) {
+ foreach (Reply *timedReply, replies_) {
+ if (timedReply->reply == reply) {
+ return timedReply;
+ }
+ }
+ return 0;
+}
+
+bool ReplyList::ignoreSslErrors()
+{
+ return ignoreSslErrors_;
+}
+
+void ReplyList::setIgnoreSslErrors(bool ignoreSslErrors)
+{
+ ignoreSslErrors_ = ignoreSslErrors;
+}
+
+}
diff --git a/libraries/katabasis/src/ReplyServer.cpp b/libraries/katabasis/src/ReplyServer.cpp
new file mode 100755
index 00000000..4598b18a
--- /dev/null
+++ b/libraries/katabasis/src/ReplyServer.cpp
@@ -0,0 +1,182 @@
+#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;
+}
+
+}
diff --git a/libraries/katabasis/src/Requestor.cpp b/libraries/katabasis/src/Requestor.cpp
new file mode 100644
index 00000000..7b6d2679
--- /dev/null
+++ b/libraries/katabasis/src/Requestor.cpp
@@ -0,0 +1,304 @@
+#include <cassert>
+
+#include <QDebug>
+#include <QTimer>
+#include <QBuffer>
+#include <QUrlQuery>
+
+#include "katabasis/Requestor.h"
+#include "katabasis/OAuth2.h"
+#include "katabasis/Globals.h"
+
+namespace Katabasis {
+
+Requestor::Requestor(QNetworkAccessManager *manager, OAuth2 *authenticator, QObject *parent): QObject(parent), reply_(NULL), status_(Idle), addAccessTokenInQuery_(true), rawData_(false) {
+ manager_ = manager;
+ authenticator_ = authenticator;
+ if (authenticator) {
+ timedReplies_.setIgnoreSslErrors(authenticator->ignoreSslErrors());
+ }
+ qRegisterMetaType<QNetworkReply::NetworkError>("QNetworkReply::NetworkError");
+ connect(authenticator, &OAuth2::refreshFinished, this, &Requestor::onRefreshFinished, Qt::QueuedConnection);
+}
+
+Requestor::~Requestor() {
+}
+
+void Requestor::setAddAccessTokenInQuery(bool value) {
+ addAccessTokenInQuery_ = value;
+}
+
+void Requestor::setAccessTokenInAuthenticationHTTPHeaderFormat(const QString &value) {
+ accessTokenInAuthenticationHTTPHeaderFormat_ = value;
+}
+
+int Requestor::get(const QNetworkRequest &req, int timeout/* = 60*1000*/) {
+ if (-1 == setup(req, QNetworkAccessManager::GetOperation)) {
+ return -1;
+ }
+ reply_ = manager_->get(request_);
+ timedReplies_.add(new Reply(reply_, timeout));
+ connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
+ connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection);
+ return id_;
+}
+
+int Requestor::post(const QNetworkRequest &req, const QByteArray &data, int timeout/* = 60*1000*/) {
+ if (-1 == setup(req, QNetworkAccessManager::PostOperation)) {
+ return -1;
+ }
+ rawData_ = true;
+ data_ = data;
+ reply_ = manager_->post(request_, data_);
+ timedReplies_.add(new Reply(reply_, timeout));
+ connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
+ connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection);
+ connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64)));
+ return id_;
+}
+
+int Requestor::post(const QNetworkRequest & req, QHttpMultiPart* data, int timeout/* = 60*1000*/)
+{
+ if (-1 == setup(req, QNetworkAccessManager::PostOperation)) {
+ return -1;
+ }
+ rawData_ = false;
+ multipartData_ = data;
+ reply_ = manager_->post(request_, multipartData_);
+ multipartData_->setParent(reply_);
+ timedReplies_.add(new Reply(reply_, timeout));
+ connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
+ connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection);
+ connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64)));
+ return id_;
+}
+
+int Requestor::put(const QNetworkRequest &req, const QByteArray &data, int timeout/* = 60*1000*/) {
+ if (-1 == setup(req, QNetworkAccessManager::PutOperation)) {
+ return -1;
+ }
+ rawData_ = true;
+ data_ = data;
+ reply_ = manager_->put(request_, data_);
+ timedReplies_.add(new Reply(reply_, timeout));
+ connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
+ connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection);
+ connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64)));
+ return id_;
+}
+
+int Requestor::put(const QNetworkRequest & req, QHttpMultiPart* data, int timeout/* = 60*1000*/)
+{
+ if (-1 == setup(req, QNetworkAccessManager::PutOperation)) {
+ return -1;
+ }
+ rawData_ = false;
+ multipartData_ = data;
+ reply_ = manager_->put(request_, multipartData_);
+ multipartData_->setParent(reply_);
+ timedReplies_.add(new Reply(reply_, timeout));
+ connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
+ connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection);
+ connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64)));
+ return id_;
+}
+
+int Requestor::customRequest(const QNetworkRequest &req, const QByteArray &verb, const QByteArray &data, int timeout/* = 60*1000*/)
+{
+ (void)timeout;
+
+ if (-1 == setup(req, QNetworkAccessManager::CustomOperation, verb)) {
+ return -1;
+ }
+ data_ = data;
+ QBuffer * buffer = new QBuffer;
+ buffer->setData(data_);
+ reply_ = manager_->sendCustomRequest(request_, verb, buffer);
+ buffer->setParent(reply_);
+ timedReplies_.add(new Reply(reply_));
+ connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
+ connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection);
+ connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64)));
+ return id_;
+}
+
+int Requestor::head(const QNetworkRequest &req, int timeout/* = 60*1000*/)
+{
+ if (-1 == setup(req, QNetworkAccessManager::HeadOperation)) {
+ return -1;
+ }
+ reply_ = manager_->head(request_);
+ timedReplies_.add(new Reply(reply_, timeout));
+ connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
+ connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection);
+ return id_;
+}
+
+void Requestor::onRefreshFinished(QNetworkReply::NetworkError error) {
+ if (status_ != Requesting) {
+ qWarning() << "O2Requestor::onRefreshFinished: No pending request";
+ return;
+ }
+ if (QNetworkReply::NoError == error) {
+ QTimer::singleShot(100, this, &Requestor::retry);
+ } else {
+ error_ = error;
+ QTimer::singleShot(10, this, &Requestor::finish);
+ }
+}
+
+void Requestor::onRequestFinished() {
+ if (status_ == Idle) {
+ return;
+ }
+ if (reply_ != qobject_cast<QNetworkReply *>(sender())) {
+ return;
+ }
+ if (reply_->error() == QNetworkReply::NoError) {
+ QTimer::singleShot(10, this, SLOT(finish()));
+ }
+}
+
+void Requestor::onRequestError(QNetworkReply::NetworkError error) {
+ qWarning() << "O2Requestor::onRequestError: Error" << (int)error;
+ if (status_ == Idle) {
+ return;
+ }
+ if (reply_ != qobject_cast<QNetworkReply *>(sender())) {
+ return;
+ }
+ int httpStatus = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ qWarning() << "O2Requestor::onRequestError: HTTP status" << httpStatus << reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
+ if ((status_ == Requesting) && (httpStatus == 401)) {
+ // Call OAuth2::refresh. Note the O2 instance might live in a different thread
+ if (QMetaObject::invokeMethod(authenticator_, "refresh")) {
+ return;
+ }
+ qCritical() << "O2Requestor::onRequestError: Invoking remote refresh failed";
+ }
+ error_ = error;
+ QTimer::singleShot(10, this, SLOT(finish()));
+}
+
+void Requestor::onUploadProgress(qint64 uploaded, qint64 total) {
+ if (status_ == Idle) {
+ qWarning() << "O2Requestor::onUploadProgress: No pending request";
+ return;
+ }
+ if (reply_ != qobject_cast<QNetworkReply *>(sender())) {
+ return;
+ }
+ // Restart timeout because request in progress
+ Reply *o2Reply = timedReplies_.find(reply_);
+ if(o2Reply)
+ o2Reply->start();
+ emit uploadProgress(id_, uploaded, total);
+}
+
+int Requestor::setup(const QNetworkRequest &req, QNetworkAccessManager::Operation operation, const QByteArray &verb) {
+ static int currentId;
+
+ if (status_ != Idle) {
+ qWarning() << "O2Requestor::setup: Another request pending";
+ return -1;
+ }
+
+ request_ = req;
+ operation_ = operation;
+ id_ = currentId++;
+ url_ = req.url();
+
+ QUrl url = url_;
+ if (addAccessTokenInQuery_) {
+ QUrlQuery query(url);
+ query.addQueryItem(OAUTH2_ACCESS_TOKEN, authenticator_->token());
+ url.setQuery(query);
+ }
+
+ request_.setUrl(url);
+
+ // If the service require the access token to be sent as a Authentication HTTP header, we add the access token.
+ if (!accessTokenInAuthenticationHTTPHeaderFormat_.isEmpty()) {
+ request_.setRawHeader(HTTP_AUTHORIZATION_HEADER, accessTokenInAuthenticationHTTPHeaderFormat_.arg(authenticator_->token()).toLatin1());
+ }
+
+ if (!verb.isEmpty()) {
+ request_.setRawHeader(HTTP_HTTP_HEADER, verb);
+ }
+
+ status_ = Requesting;
+ error_ = QNetworkReply::NoError;
+ return id_;
+}
+
+void Requestor::finish() {
+ QByteArray data;
+ if (status_ == Idle) {
+ qWarning() << "O2Requestor::finish: No pending request";
+ return;
+ }
+ data = reply_->readAll();
+ status_ = Idle;
+ timedReplies_.remove(reply_);
+ reply_->disconnect(this);
+ reply_->deleteLater();
+ QList<QNetworkReply::RawHeaderPair> headers = reply_->rawHeaderPairs();
+ emit finished(id_, error_, data, headers);
+}
+
+void Requestor::retry() {
+ if (status_ != Requesting) {
+ qWarning() << "O2Requestor::retry: No pending request";
+ return;
+ }
+ timedReplies_.remove(reply_);
+ reply_->disconnect(this);
+ reply_->deleteLater();
+ QUrl url = url_;
+ if (addAccessTokenInQuery_) {
+ QUrlQuery query(url);
+ query.addQueryItem(OAUTH2_ACCESS_TOKEN, authenticator_->token());
+ url.setQuery(query);
+ }
+ request_.setUrl(url);
+
+ // If the service require the access token to be sent as a Authentication HTTP header,
+ // we update the access token when retrying.
+ if(!accessTokenInAuthenticationHTTPHeaderFormat_.isEmpty()) {
+ request_.setRawHeader(HTTP_AUTHORIZATION_HEADER, accessTokenInAuthenticationHTTPHeaderFormat_.arg(authenticator_->token()).toLatin1());
+ }
+
+ status_ = ReRequesting;
+ switch (operation_) {
+ case QNetworkAccessManager::GetOperation:
+ reply_ = manager_->get(request_);
+ break;
+ case QNetworkAccessManager::PostOperation:
+ reply_ = rawData_ ? manager_->post(request_, data_) : manager_->post(request_, multipartData_);
+ break;
+ case QNetworkAccessManager::CustomOperation:
+ {
+ QBuffer * buffer = new QBuffer;
+ buffer->setData(data_);
+ reply_ = manager_->sendCustomRequest(request_, request_.rawHeader(HTTP_HTTP_HEADER), buffer);
+ buffer->setParent(reply_);
+ }
+ break;
+ case QNetworkAccessManager::PutOperation:
+ reply_ = rawData_ ? manager_->post(request_, data_) : manager_->put(request_, multipartData_);
+ break;
+ case QNetworkAccessManager::HeadOperation:
+ reply_ = manager_->head(request_);
+ break;
+ default:
+ assert(!"Unspecified operation for request");
+ reply_ = manager_->get(request_);
+ break;
+ }
+ timedReplies_.add(reply_);
+ connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
+ connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection);
+ connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64)));
+}
+
+}