NOISSUE bulk addition of code from Katabasis
This commit is contained in:
26
libraries/katabasis/src/JsonResponse.cpp
Normal file
26
libraries/katabasis/src/JsonResponse.cpp
Normal 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();
|
||||
}
|
||||
|
||||
}
|
12
libraries/katabasis/src/JsonResponse.h
Normal file
12
libraries/katabasis/src/JsonResponse.h
Normal file
@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <QVariantMap>
|
||||
|
||||
class QByteArray;
|
||||
|
||||
namespace Katabasis {
|
||||
|
||||
/// Parse JSON data into a QVariantMap
|
||||
QVariantMap parseJsonResponse(const QByteArray &data);
|
||||
|
||||
}
|
668
libraries/katabasis/src/OAuth2.cpp
Normal file
668
libraries/katabasis/src/OAuth2.cpp
Normal 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> ¶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);
|
||||
}
|
||||
|
||||
}
|
123
libraries/katabasis/src/PollServer.cpp
Normal file
123
libraries/katabasis/src/PollServer.cpp
Normal 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();
|
||||
}
|
||||
|
||||
}
|
62
libraries/katabasis/src/Reply.cpp
Normal file
62
libraries/katabasis/src/Reply.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
182
libraries/katabasis/src/ReplyServer.cpp
Executable file
182
libraries/katabasis/src/ReplyServer.cpp
Executable 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;
|
||||
}
|
||||
|
||||
}
|
304
libraries/katabasis/src/Requestor.cpp
Normal file
304
libraries/katabasis/src/Requestor.cpp
Normal 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)));
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user