diff options
author | Petr Mrázek <peterix@gmail.com> | 2021-07-22 20:15:20 +0200 |
---|---|---|
committer | Petr Mrázek <peterix@gmail.com> | 2021-07-25 19:50:44 +0200 |
commit | dd133680858351e3e07690e286882327a4f42ba5 (patch) | |
tree | 42ac11eb0d6cc58d3785c57f8571046cafe4b769 /libraries/katabasis/src | |
parent | 2568752af57258a33f27f4c2d0b8fc486fb3d100 (diff) | |
download | PrismLauncher-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.cpp | 26 | ||||
-rw-r--r-- | libraries/katabasis/src/JsonResponse.h | 12 | ||||
-rw-r--r-- | libraries/katabasis/src/OAuth2.cpp | 668 | ||||
-rw-r--r-- | libraries/katabasis/src/PollServer.cpp | 123 | ||||
-rw-r--r-- | libraries/katabasis/src/Reply.cpp | 62 | ||||
-rwxr-xr-x | libraries/katabasis/src/ReplyServer.cpp | 182 | ||||
-rw-r--r-- | libraries/katabasis/src/Requestor.cpp | 304 |
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> ¶meters) { + QByteArray ret; + bool first = true; + for( auto & h: parameters) { + if (first) { + first = false; + } else { + ret.append("&"); + } + ret.append(QUrl::toPercentEncoding(h.name) + "=" + QUrl::toPercentEncoding(h.value)); + } + return ret; +} +} + +namespace Katabasis { + +OAuth2::OAuth2(Options & opts, Token & token, QObject *parent, QNetworkAccessManager *manager) : QObject(parent), token_(token) { + manager_ = manager ? manager : new QNetworkAccessManager(this); + grantFlow_ = GrantFlowAuthorizationCode; + qRegisterMetaType<QNetworkReply::NetworkError>("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> ¶meters) { + QByteArray body; + bool first = true; + foreach (QString key, parameters.keys()) { + if (first) { + first = false; + } else { + body.append("&"); + } + QString value = parameters.value(key); + body.append(QUrl::toPercentEncoding(key) + QString("=").toUtf8() + QUrl::toPercentEncoding(value)); + } + return body; +} + +QDateTime OAuth2::expires() { + return token_.notAfter; +} +void OAuth2::setExpires(QDateTime v) { + token_.notAfter = v; +} + +void OAuth2::startPollServer(const QVariantMap ¶ms) +{ + 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))); +} + +} |