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

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