NOISSUE bulk addition of code from Katabasis

This commit is contained in:
Petr Mrázek
2021-07-22 20:15:20 +02:00
parent 2568752af5
commit dd13368085
31 changed files with 3687 additions and 18 deletions

2
libraries/katabasis/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
build/
*.kdev4

View File

@ -0,0 +1,60 @@
cmake_minimum_required(VERSION 3.20)
string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_BUILD_DIR}" IS_IN_SOURCE_BUILD)
if(IS_IN_SOURCE_BUILD)
message(FATAL_ERROR "You are building Katabasis in-source. Please separate the build tree from the source tree.")
endif()
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
if(CMAKE_HOST_SYSTEM_VERSION MATCHES ".*[Mm]icrosoft.*" OR
CMAKE_HOST_SYSTEM_VERSION MATCHES ".*WSL.*"
)
message(FATAL_ERROR "Building Katabasis is not supported in Linux-on-Windows distributions. Use a real Linux machine, not a fraudulent one.")
endif()
endif()
project(Katabasis)
enable_testing()
set(CMAKE_AUTOMOC ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_CXX_STANDARD_REQUIRED true)
set(CMAKE_C_STANDARD_REQUIRED true)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_C_STANDARD 11)
find_package(Qt5 COMPONENTS Core Network REQUIRED)
set( katabasis_PRIVATE
src/OAuth2.cpp
src/JsonResponse.cpp
src/JsonResponse.h
src/PollServer.cpp
src/Reply.cpp
src/ReplyServer.cpp
src/Requestor.cpp
)
set( katabasis_PUBLIC
include/katabasis/OAuth2.h
include/katabasis/Globals.h
include/katabasis/PollServer.h
include/katabasis/Reply.h
include/katabasis/ReplyServer.h
include/katabasis/Requestor.h
include/katabasis/RequestParameter.h
)
add_library( Katabasis STATIC ${katabasis_PRIVATE} ${katabasis_PUBLIC} )
target_link_libraries(Katabasis Qt5::Core Qt5::Network)
# needed for statically linked Katabasis in shared libs on x86_64
set_target_properties(Katabasis
PROPERTIES POSITION_INDEPENDENT_CODE TRUE
)
target_include_directories(Katabasis PUBLIC include PRIVATE src include/katabasis)

View File

@ -0,0 +1,23 @@
Copyright (c) 2012, Akos Polster
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,36 @@
# Katabasis - MS-flavoerd OAuth for Qt, derived from the O2 library
This library's sole purpose is to make interacting with MSA and various MSA and XBox authenticated services less painful.
It may be possible to backport some of the changes to O2 in the future, but for the sake of going fast, all compatibility concerns have been ignored.
[You can find the original library's git repository here.](https://github.com/pipacs/o2)
Notes to contributors:
* Please follow the coding style of the existing source, where reasonable
* Code contributions are released under Simplified BSD License, as specified in LICENSE. Do not contribute if this license does not suit your code
* If you are interested in working on this, come to the MultiMC Discord server and talk first
## Installation
Clone the Github repository, integrate the it into your CMake build system.
The library is static only, dynamic linking and system-wide installation are out of scope and undesirable.
## Usage
At this stage, don't, unless you want to help with the library itself.
This is an experimental fork of the O2 library and is undergoing a big design/architecture shift in order to support different features:
* Multiple accounts
* Multi-stage authentication/authorization schemes
* Tighter control over token chains and their storage
* Talking to complex APIs and individually authorized microservices
* Token lifetime management, 'offline mode' and resilience in face of network failures
* Token and claims/entitlements validation
* Caching of some API results
* XBox magic
* Mojang magic
* Generally, magic that you would spend weeks on researching while getting confused by contradictory/incomplete documentation (if any is available)

View File

@ -0,0 +1,110 @@
# O2 library by Akos Polster and contributors
[The origin of this fork.](https://github.com/pipacs/o2)
> Copyright (c) 2012, Akos Polster
> All rights reserved.
>
> Redistribution and use in source and binary forms, with or without
> modification, are permitted provided that the following conditions are met:
>
> * Redistributions of source code must retain the above copyright notice, this
> list of conditions and the following disclaimer.
>
> * Redistributions in binary form must reproduce the above copyright notice,
> this list of conditions and the following disclaimer in the documentation
> and/or other materials provided with the distribution.
>
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
> AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
> IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
> DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
> FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
> DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
> SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
> OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
> OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# SimpleCrypt by Andre Somers
Cryptographic methods for Qt.
> Copyright (c) 2011, Andre Somers
> All rights reserved.
>
> Redistribution and use in source and binary forms, with or without
> modification, are permitted provided that the following conditions are met:
>
> * Redistributions of source code must retain the above copyright
> notice, this list of conditions and the following disclaimer.
> * Redistributions in binary form must reproduce the above copyright
> notice, this list of conditions and the following disclaimer in the
> documentation and/or other materials provided with the distribution.
> * Neither the name of the Rathenau Instituut, Andre Somers nor the
> names of its contributors may be used to endorse or promote products
> derived from this software without specific prior written permission.
>
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
> ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
> WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
> DISCLAIMED. IN NO EVENT SHALL ANDRE SOMERS BE LIABLE FOR ANY
> DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
> (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
> ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
> (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
> SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# Mandeep Sandhu <mandeepsandhu.chd@gmail.com>
Configurable settings storage, Twitter XAuth specialization, new demos, cleanups.
> "Hi Akos,
>
> I'm writing this mail to confirm that my contributions to the O2 library, available here https://github.com/pipacs/o2, can be freely distributed according to the project's license (as shown in the LICENSE file).
>
> Regards,
> -mandeep"
# Sergey Gavrushkin <https://github.com/ncux>
FreshBooks specialization
# Theofilos Intzoglou <https://github.com/parapente>
Hubic specialization
# Dimitar
SurveyMonkey specialization
# David Brooks <https://github.com/dbrnz>
CMake related fixes and improvements.
# Lukas Vogel <https://github.com/lukedirtwalker>
Spotify support
# Alan Garny <https://github.com/agarny>
Windows DLL build support
# MartinMikita <https://github.com/MartinMikita>
Bug fixes
# Larry Shaffer <https://github.com/dakcarto>
Versioning, shared lib, install target and header support
# Gilmanov Ildar <https://github.com/gilmanov-ildar>
Bug fixes, support for ```qml``` module
# Fabian Vogt <https://github.com/Vogtinator>
Bug fixes, support for building without Qt keywords enabled

View File

@ -0,0 +1,33 @@
#pragma once
#include <QString>
#include <QDateTime>
#include <QMap>
#include <QVariantMap>
namespace Katabasis {
enum class Activity {
Idle,
LoggingIn,
LoggingOut,
Refreshing
};
enum class Validity {
None,
Assumed,
Certain
};
struct Token {
QDateTime issueInstant;
QDateTime notAfter;
QString token;
QString refresh_token;
QVariantMap extra;
Validity validity = Validity::None;
bool persistent = true;
};
}

View File

@ -0,0 +1,59 @@
#pragma once
namespace Katabasis {
// Common constants
const char ENCRYPTION_KEY[] = "12345678";
const char MIME_TYPE_XFORM[] = "application/x-www-form-urlencoded";
const char MIME_TYPE_JSON[] = "application/json";
// OAuth 1/1.1 Request Parameters
const char OAUTH_CALLBACK[] = "oauth_callback";
const char OAUTH_CONSUMER_KEY[] = "oauth_consumer_key";
const char OAUTH_NONCE[] = "oauth_nonce";
const char OAUTH_SIGNATURE[] = "oauth_signature";
const char OAUTH_SIGNATURE_METHOD[] = "oauth_signature_method";
const char OAUTH_TIMESTAMP[] = "oauth_timestamp";
const char OAUTH_VERSION[] = "oauth_version";
// OAuth 1/1.1 Response Parameters
const char OAUTH_TOKEN[] = "oauth_token";
const char OAUTH_TOKEN_SECRET[] = "oauth_token_secret";
const char OAUTH_CALLBACK_CONFIRMED[] = "oauth_callback_confirmed";
const char OAUTH_VERFIER[] = "oauth_verifier";
// OAuth 2 Request Parameters
const char OAUTH2_RESPONSE_TYPE[] = "response_type";
const char OAUTH2_CLIENT_ID[] = "client_id";
const char OAUTH2_CLIENT_SECRET[] = "client_secret";
const char OAUTH2_USERNAME[] = "username";
const char OAUTH2_PASSWORD[] = "password";
const char OAUTH2_REDIRECT_URI[] = "redirect_uri";
const char OAUTH2_SCOPE[] = "scope";
const char OAUTH2_GRANT_TYPE_CODE[] = "code";
const char OAUTH2_GRANT_TYPE_TOKEN[] = "token";
const char OAUTH2_GRANT_TYPE_PASSWORD[] = "password";
const char OAUTH2_GRANT_TYPE_DEVICE[] = "urn:ietf:params:oauth:grant-type:device_code";
const char OAUTH2_GRANT_TYPE[] = "grant_type";
const char OAUTH2_API_KEY[] = "api_key";
const char OAUTH2_STATE[] = "state";
const char OAUTH2_CODE[] = "code";
// OAuth 2 Response Parameters
const char OAUTH2_ACCESS_TOKEN[] = "access_token";
const char OAUTH2_REFRESH_TOKEN[] = "refresh_token";
const char OAUTH2_EXPIRES_IN[] = "expires_in";
const char OAUTH2_DEVICE_CODE[] = "device_code";
const char OAUTH2_USER_CODE[] = "user_code";
const char OAUTH2_VERIFICATION_URI[] = "verification_uri";
const char OAUTH2_VERIFICATION_URL[] = "verification_url"; // Google sign-in
const char OAUTH2_VERIFICATION_URI_COMPLETE[] = "verification_uri_complete";
const char OAUTH2_INTERVAL[] = "interval";
// Parameter values
const char AUTHORIZATION_CODE[] = "authorization_code";
// Standard HTTP headers
const char HTTP_HTTP_HEADER[] = "HTTP";
const char HTTP_AUTHORIZATION_HEADER[] = "Authorization";
}

View File

@ -0,0 +1,233 @@
#pragma once
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QPair>
#include "Reply.h"
#include "RequestParameter.h"
#include "Bits.h"
namespace Katabasis {
class ReplyServer;
class PollServer;
/*
* FIXME: this is not as simple as it should be. it squishes 4 different grant flows into one big ball of mud
* This serves no practical purpose and simply makes the code less readable / maintainable.
*
* Therefore: Split this into the 4 different OAuth2 flows that people can use as authentication steps. Write tests/examples for all of them.
*/
/// Simple OAuth2 authenticator.
class OAuth2: public QObject
{
Q_OBJECT
public:
Q_ENUMS(GrantFlow)
public:
struct Options {
QString userAgent = QStringLiteral("Katabasis/1.0");
QString redirectionUrl = QStringLiteral("http://localhost:%1");
QString responseType = QStringLiteral("code");
QString scope;
QString clientIdentifier;
QString clientSecret;
QUrl authorizationUrl;
QUrl accessTokenUrl;
QVector<quint16> listenerPorts = { 0 };
};
/// Authorization flow types.
enum GrantFlow {
GrantFlowAuthorizationCode, ///< @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1
GrantFlowImplicit, ///< @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.2
GrantFlowResourceOwnerPasswordCredentials,
GrantFlowDevice ///< @see https://tools.ietf.org/html/rfc8628#section-1
};
/// Authorization flow.
GrantFlow grantFlow();
void setGrantFlow(GrantFlow value);
public:
/// Are we authenticated?
bool linked();
/// Authentication token.
QString token();
/// Provider-specific extra tokens, available after a successful authentication
QVariantMap extraTokens();
/// Page content on local host after successful oauth.
/// Provide it in case you do not want to close the browser, but display something
QByteArray replyContent() const;
void setReplyContent(const QByteArray &value);
public:
// TODO: remove
/// Resource owner username.
/// instances with the same (username, password) share the same "linked" and "token" properties.
QString username();
void setUsername(const QString &value);
// TODO: remove
/// Resource owner password.
/// instances with the same (username, password) share the same "linked" and "token" properties.
QString password();
void setPassword(const QString &value);
// TODO: remove
/// API key.
QString apiKey();
void setApiKey(const QString &value);
// TODO: remove
/// Allow ignoring SSL errors?
/// E.g. SurveyMonkey fails on Mac due to SSL error. Ignoring the error circumvents the problem
bool ignoreSslErrors();
void setIgnoreSslErrors(bool ignoreSslErrors);
// TODO: put in `Options`
/// User-defined extra parameters to append to request URL
QVariantMap extraRequestParams();
void setExtraRequestParams(const QVariantMap &value);
// TODO: split up the class into multiple, each implementing one OAuth2 flow
/// Grant type (if non-standard)
QString grantType();
void setGrantType(const QString &value);
public:
/// Constructor.
/// @param parent Parent object.
explicit OAuth2(Options & opts, Token & token, QObject *parent = 0, QNetworkAccessManager *manager = 0);
/// Get refresh token.
QString refreshToken();
/// Get token expiration time
QDateTime expires();
public slots:
/// Authenticate.
virtual void link();
/// De-authenticate.
virtual void unlink();
/// Refresh token.
bool refresh();
/// Handle situation where reply server has opted to close its connection
void serverHasClosed(bool paramsfound = false);
signals:
/// Emitted when a token refresh has been completed or failed.
void refreshFinished(QNetworkReply::NetworkError error);
/// Emitted when client needs to open a web browser window, with the given URL.
void openBrowser(const QUrl &url);
/// Emitted when client can close the browser window.
void closeBrowser();
/// Emitted when client needs to show a verification uri and user code
void showVerificationUriAndCode(const QUrl &uri, const QString &code);
/// Emitted when authentication/deauthentication succeeded.
void linkingSucceeded();
/// Emitted when authentication/deauthentication failed.
void linkingFailed();
void activityChanged(Activity activity);
public slots:
/// Handle verification response.
virtual void onVerificationReceived(QMap<QString, QString>);
protected slots:
/// Handle completion of a token request.
virtual void onTokenReplyFinished();
/// Handle failure of a token request.
virtual void onTokenReplyError(QNetworkReply::NetworkError error);
/// Handle completion of a refresh request.
virtual void onRefreshFinished();
/// Handle failure of a refresh request.
virtual void onRefreshError(QNetworkReply::NetworkError error);
/// Handle completion of a Device Authorization Request
virtual void onDeviceAuthReplyFinished();
protected:
/// Build HTTP request body.
QByteArray buildRequestBody(const QMap<QString, QString> &parameters);
/// Set refresh token.
void setRefreshToken(const QString &v);
/// Set token expiration time.
void setExpires(QDateTime v);
/// Start polling authorization server
void startPollServer(const QVariantMap &params);
/// Set authentication token.
void setToken(const QString &v);
/// Set the linked state
void setLinked(bool v);
/// Set extra tokens found in OAuth response
void setExtraTokens(QVariantMap extraTokens);
/// Set local reply server
void setReplyServer(ReplyServer *server);
ReplyServer * replyServer() const;
/// Set local poll server
void setPollServer(PollServer *server);
PollServer * pollServer() const;
void updateActivity(Activity activity);
protected:
QString username_;
QString password_;
Options options_;
QVariantMap extraReqParams_;
QString apiKey_;
QNetworkAccessManager *manager_ = nullptr;
ReplyList timedReplies_;
GrantFlow grantFlow_;
QString grantType_;
protected:
QString redirectUri_;
Token &token_;
// this should be part of the reply server impl
QByteArray replyContent_;
private:
ReplyServer *replyServer_ = nullptr;
PollServer *pollServer_ = nullptr;
Activity activity_ = Activity::Idle;
};
}

View File

@ -0,0 +1,48 @@
#pragma once
#include <QByteArray>
#include <QMap>
#include <QNetworkRequest>
#include <QObject>
#include <QString>
#include <QTimer>
class QNetworkAccessManager;
namespace Katabasis {
/// Poll an authorization server for token
class PollServer : public QObject
{
Q_OBJECT
public:
explicit PollServer(QNetworkAccessManager * manager, const QNetworkRequest &request, const QByteArray & payload, int expiresIn, QObject *parent = 0);
/// Seconds to wait between polling requests
Q_PROPERTY(int interval READ interval WRITE setInterval)
int interval() const;
void setInterval(int interval);
signals:
void verificationReceived(QMap<QString, QString>);
void serverClosed(bool); // whether it has found parameters
public slots:
void startPolling();
protected slots:
void onPollTimeout();
void onExpiration();
void onReplyFinished();
protected:
QNetworkAccessManager *manager_;
const QNetworkRequest request_;
const QByteArray payload_;
const int expiresIn_;
QTimer expirationTimer;
QTimer pollTimer;
};
}

View File

@ -0,0 +1,60 @@
#pragma once
#include <QList>
#include <QTimer>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QNetworkAccessManager>
#include <QByteArray>
namespace Katabasis {
/// A network request/reply pair that can time out.
class Reply: public QTimer {
Q_OBJECT
public:
Reply(QNetworkReply *reply, int timeOut = 60 * 1000, QObject *parent = 0);
signals:
void error(QNetworkReply::NetworkError);
public slots:
/// When time out occurs, the QNetworkReply's error() signal is triggered.
void onTimeOut();
public:
QNetworkReply *reply;
};
/// List of O2Replies.
class ReplyList {
public:
ReplyList() { ignoreSslErrors_ = false; }
/// Destructor.
/// Deletes all O2Reply instances in the list.
virtual ~ReplyList();
/// Create a new O2Reply from a QNetworkReply, and add it to this list.
void add(QNetworkReply *reply);
/// Add an O2Reply to the list, while taking ownership of it.
void add(Reply *reply);
/// Remove item from the list that corresponds to a QNetworkReply.
void remove(QNetworkReply *reply);
/// Find an O2Reply in the list, corresponding to a QNetworkReply.
/// @return Matching O2Reply or NULL.
Reply *find(QNetworkReply *reply);
bool ignoreSslErrors();
void setIgnoreSslErrors(bool ignoreSslErrors);
protected:
QList<Reply *> replies_;
bool ignoreSslErrors_;
};
}

View File

@ -0,0 +1,53 @@
#pragma once
#include <QTcpServer>
#include <QMap>
#include <QByteArray>
#include <QString>
namespace Katabasis {
/// HTTP server to process authentication response.
class ReplyServer: public QTcpServer {
Q_OBJECT
public:
explicit ReplyServer(QObject *parent = 0);
/// Page content on local host after successful oauth - in case you do not want to close the browser, but display something
Q_PROPERTY(QByteArray replyContent READ replyContent WRITE setReplyContent)
QByteArray replyContent();
void setReplyContent(const QByteArray &value);
/// Seconds to keep listening *after* first response for a callback with token content
Q_PROPERTY(int timeout READ timeout WRITE setTimeout)
int timeout();
void setTimeout(int timeout);
/// Maximum number of callback tries to accept, in case some don't have token content (favicons, etc.)
Q_PROPERTY(int callbackTries READ callbackTries WRITE setCallbackTries)
int callbackTries();
void setCallbackTries(int maxtries);
QString uniqueState();
void setUniqueState(const QString &state);
signals:
void verificationReceived(QMap<QString, QString>);
void serverClosed(bool); // whether it has found parameters
public slots:
void onIncomingConnection();
void onBytesReady();
QMap<QString, QString> parseQueryParams(QByteArray *data);
void closeServer(QTcpSocket *socket = 0, bool hasparameters = false);
protected:
QByteArray replyContent_;
int timeout_;
int maxtries_;
int tries_;
QString uniqueState_;
};
}

View File

@ -0,0 +1,15 @@
#pragma once
namespace Katabasis {
/// Request parameter (name-value pair) participating in authentication.
struct RequestParameter {
RequestParameter(const QByteArray &n, const QByteArray &v): name(n), value(v) {}
bool operator <(const RequestParameter &other) const {
return (name == other.name)? (value < other.value): (name < other.name);
}
QByteArray name;
QByteArray value;
};
}

View File

@ -0,0 +1,116 @@
#pragma once
#include <QObject>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QNetworkAccessManager>
#include <QUrl>
#include <QByteArray>
#include <QHttpMultiPart>
#include "Reply.h"
namespace Katabasis {
class OAuth2;
/// Makes authenticated requests.
class Requestor: public QObject {
Q_OBJECT
public:
explicit Requestor(QNetworkAccessManager *manager, OAuth2 *authenticator, QObject *parent = 0);
~Requestor();
/// Some services require the access token to be sent as a Authentication HTTP header
/// and refuse requests with the access token in the query.
/// This function allows to use or ignore the access token in the query.
/// The default value of `true` means that the query will contain the access token.
/// By setting the value to false, the query will not contain the access token.
/// See:
/// https://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-16#section-4.3
/// https://tools.ietf.org/html/rfc6750#section-2.3
void setAddAccessTokenInQuery(bool value);
/// Some services require the access token to be sent as a Authentication HTTP header.
/// This is the case for Twitch and Mixer.
/// When the access token expires and is refreshed, O2Requestor::retry() needs to update the Authentication HTTP header.
/// In order to do so, O2Requestor needs to know the format of the Authentication HTTP header.
void setAccessTokenInAuthenticationHTTPHeaderFormat(const QString &value);
public slots:
/// Make a GET request.
/// @return Request ID or -1 if there are too many requests in the queue.
int get(const QNetworkRequest &req, int timeout = 60*1000);
/// Make a POST request.
/// @return Request ID or -1 if there are too many requests in the queue.
int post(const QNetworkRequest &req, const QByteArray &data, int timeout = 60*1000);
int post(const QNetworkRequest &req, QHttpMultiPart* data, int timeout = 60*1000);
/// Make a PUT request.
/// @return Request ID or -1 if there are too many requests in the queue.
int put(const QNetworkRequest &req, const QByteArray &data, int timeout = 60*1000);
int put(const QNetworkRequest &req, QHttpMultiPart* data, int timeout = 60*1000);
/// Make a HEAD request.
/// @return Request ID or -1 if there are too many requests in the queue.
int head(const QNetworkRequest &req, int timeout = 60*1000);
/// Make a custom request.
/// @return Request ID or -1 if there are too many requests in the queue.
int customRequest(const QNetworkRequest &req, const QByteArray &verb, const QByteArray &data, int timeout = 60*1000);
signals:
/// Emitted when a request has been completed or failed.
void finished(int id, QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers);
/// Emitted when an upload has progressed.
void uploadProgress(int id, qint64 bytesSent, qint64 bytesTotal);
protected slots:
/// Handle refresh completion.
void onRefreshFinished(QNetworkReply::NetworkError error);
/// Handle request finished.
void onRequestFinished();
/// Handle request error.
void onRequestError(QNetworkReply::NetworkError error);
/// Re-try request (after successful token refresh).
void retry();
/// Finish the request, emit finished() signal.
void finish();
/// Handle upload progress.
void onUploadProgress(qint64 uploaded, qint64 total);
protected:
int setup(const QNetworkRequest &request, QNetworkAccessManager::Operation operation, const QByteArray &verb = QByteArray());
enum Status {
Idle, Requesting, ReRequesting
};
QNetworkAccessManager *manager_;
OAuth2 *authenticator_;
QNetworkRequest request_;
QByteArray data_;
QHttpMultiPart* multipartData_;
QNetworkReply *reply_;
Status status_;
int id_;
QNetworkAccessManager::Operation operation_;
QUrl url_;
ReplyList timedReplies_;
QNetworkReply::NetworkError error_;
bool addAccessTokenInQuery_;
QString accessTokenInAuthenticationHTTPHeaderFormat_;
bool rawData_;
};
}

View File

@ -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();
}
}

View File

@ -0,0 +1,12 @@
#pragma once
#include <QVariantMap>
class QByteArray;
namespace Katabasis {
/// Parse JSON data into a QVariantMap
QVariantMap parseJsonResponse(const QByteArray &data);
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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)));
}
}