transmission/qt/RpcClient.cc

317 lines
8.1 KiB
C++
Raw Normal View History

/*
* This file Copyright (C) 2014-2016 Mnemosyne LLC
*
* It may be used under the GNU GPL versions 2 or 3
* or any future license endorsed by Mnemosyne LLC.
*
*/
#include <cstring>
#include <iostream>
#include <QApplication>
#include <QHostAddress>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <libtransmission/transmission.h>
#include <libtransmission/rpcimpl.h>
#include <libtransmission/utils.h> // tr_free
#include <libtransmission/version.h> // LONG_VERSION_STRING
#include "RpcClient.h"
#include "VariantHelpers.h"
// #define DEBUG_HTTP
#define REQUEST_DATA_PROPERTY_KEY "requestData"
#define REQUEST_FUTUREINTERFACE_PROPERTY_KEY "requestReplyFutureInterface"
using ::trqt::variant_helpers::dictAdd;
using ::trqt::variant_helpers::dictFind;
using ::trqt::variant_helpers::variantInit;
namespace
{
void destroyVariant(tr_variant* json)
{
tr_variantFree(json);
tr_free(json);
}
TrVariantPtr createVariant()
{
return TrVariantPtr(tr_new0(tr_variant, 1), &destroyVariant);
}
} // namespace
RpcClient::RpcClient(QObject* parent) :
QObject(parent)
{
qRegisterMetaType<TrVariantPtr>("TrVariantPtr");
}
void RpcClient::stop()
{
session_ = nullptr;
session_id_.clear();
url_.clear();
if (nam_ != nullptr)
{
nam_->deleteLater();
nam_ = nullptr;
}
}
void RpcClient::start(tr_session* session)
{
session_ = session;
}
void RpcClient::start(QUrl const& url)
{
url_ = url;
}
bool RpcClient::isLocal() const
{
if (session_ != nullptr)
{
return true;
}
if (QHostAddress(url_.host()).isLoopback())
{
return true;
}
return false;
}
QUrl const& RpcClient::url() const
{
return url_;
}
RpcResponseFuture RpcClient::exec(tr_quark method, tr_variant* args)
{
auto len = size_t{};
auto const* str = tr_quark_get_string(method, &len);
return exec(std::string_view(str, len), args);
}
RpcResponseFuture RpcClient::exec(std::string_view method, tr_variant* args)
{
TrVariantPtr json = createVariant();
tr_variantInitDict(json.get(), 3);
dictAdd(json.get(), TR_KEY_method, method);
if (args != nullptr)
{
tr_variantDictSteal(json.get(), TR_KEY_arguments, args);
}
return sendRequest(json);
}
int64_t RpcClient::getNextTag()
{
return next_tag_++;
}
void RpcClient::sendNetworkRequest(TrVariantPtr json, QFutureInterface<RpcResponse> const& promise)
{
QNetworkRequest request;
request.setUrl(url_);
request.setRawHeader("User-Agent", (qApp->applicationName() + QLatin1Char('/') +
QString::fromUtf8(LONG_VERSION_STRING)).toUtf8());
request.setRawHeader("Content-Type", "application/json; charset=UTF-8");
if (!session_id_.isEmpty())
{
request.setRawHeader(TR_RPC_SESSION_ID_HEADER, session_id_.toUtf8());
}
size_t raw_json_data_length;
char* raw_json_data = tr_variantToStr(json.get(), TR_VARIANT_FMT_JSON_LEAN, &raw_json_data_length);
QByteArray json_data(raw_json_data, raw_json_data_length);
tr_free(raw_json_data);
QNetworkReply* reply = networkAccessManager()->post(request, json_data);
reply->setProperty(REQUEST_DATA_PROPERTY_KEY, QVariant::fromValue(json));
reply->setProperty(REQUEST_FUTUREINTERFACE_PROPERTY_KEY, QVariant::fromValue(promise));
connect(reply, SIGNAL(downloadProgress(qint64, qint64)), this, SIGNAL(dataReadProgress()));
connect(reply, SIGNAL(uploadProgress(qint64, qint64)), this, SIGNAL(dataSendProgress()));
#ifdef DEBUG_HTTP
std::cerr << "sending " << "POST " << qPrintable(url_.path()) << std::endl;
for (QByteArray const& b : request.rawHeaderList())
{
std::cerr << b.constData() << ": " << request.rawHeader(b).constData() << std::endl;
}
std::cerr << "Body:\n" << json_data.constData() << std::endl;
#endif
}
void RpcClient::sendLocalRequest(TrVariantPtr json, QFutureInterface<RpcResponse> const& promise, int64_t tag)
{
local_requests_.insert(tag, promise);
tr_rpc_request_exec_json(session_, json.get(), localSessionCallback, this);
}
RpcResponseFuture RpcClient::sendRequest(TrVariantPtr json)
{
int64_t tag = getNextTag();
dictAdd(json.get(), TR_KEY_tag, tag);
QFutureInterface<RpcResponse> promise;
promise.setExpectedResultCount(1);
promise.setProgressRange(0, 1);
promise.setProgressValue(0);
promise.reportStarted();
if (session_ != nullptr)
{
sendLocalRequest(json, promise, tag);
}
else if (!url_.isEmpty())
{
sendNetworkRequest(json, promise);
}
return promise.future();
}
QNetworkAccessManager* RpcClient::networkAccessManager()
{
if (nam_ == nullptr)
{
nam_ = new QNetworkAccessManager();
connect(nam_, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkRequestFinished(QNetworkReply*)));
connect(nam_, SIGNAL(authenticationRequired(QNetworkReply*, QAuthenticator*)), this,
SIGNAL(httpAuthenticationRequired()));
}
return nam_;
}
void RpcClient::localSessionCallback(tr_session* s, tr_variant* response, void* vself)
{
Q_UNUSED(s)
auto* self = static_cast<RpcClient*>(vself);
TrVariantPtr json = createVariant();
*json = *response;
variantInit(response, false);
// this callback is invoked in the libtransmission thread, so we don't want
// to process the response here... let's push it over to the Qt thread.
QMetaObject::invokeMethod(self, "localRequestFinished", Qt::QueuedConnection, Q_ARG(TrVariantPtr, json));
}
void RpcClient::networkRequestFinished(QNetworkReply* reply)
{
reply->deleteLater();
auto promise = reply->property(REQUEST_FUTUREINTERFACE_PROPERTY_KEY).
value<QFutureInterface<RpcResponse>>();
#ifdef DEBUG_HTTP
std::cerr << "http response header: " << std::endl;
for (QByteArray const& b : reply->rawHeaderList())
{
std::cerr << b.constData() << ": " << reply->rawHeader(b).constData() << std::endl;
}
std::cerr << "json:\n" << reply->peek(reply->bytesAvailable()).constData() << std::endl;
#endif
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409 &&
reply->hasRawHeader(TR_RPC_SESSION_ID_HEADER))
{
// we got a 409 telling us our session id has expired.
// update it and resubmit the request.
session_id_ = QString::fromUtf8(reply->rawHeader(TR_RPC_SESSION_ID_HEADER));
sendNetworkRequest(reply->property(REQUEST_DATA_PROPERTY_KEY).value<TrVariantPtr>(), promise);
return;
}
emit networkResponse(reply->error(), reply->errorString());
if (reply->error() != QNetworkReply::NoError)
{
RpcResponse result;
result.networkError = reply->error();
promise.setProgressValueAndText(1, reply->errorString());
promise.reportFinished(&result);
}
else
{
RpcResponse result;
QByteArray const json_data = reply->readAll().trimmed();
TrVariantPtr json = createVariant();
if (tr_variantFromJson(json.get(), json_data.constData(), json_data.size()) == 0)
{
result = parseResponseData(*json);
}
promise.setProgressValue(1);
promise.reportFinished(&result);
}
}
void RpcClient::localRequestFinished(TrVariantPtr response)
{
int64_t tag = parseResponseTag(*response);
RpcResponse result = parseResponseData(*response);
QFutureInterface<RpcResponse> promise = local_requests_.take(tag);
promise.setProgressRange(0, 1);
promise.setProgressValue(1);
promise.reportFinished(&result);
}
int64_t RpcClient::parseResponseTag(tr_variant& json)
{
auto const tag = dictFind<int>(&json, TR_KEY_tag);
return tag ? *tag : -1;
}
RpcResponse RpcClient::parseResponseData(tr_variant& json)
{
RpcResponse ret;
auto const result = dictFind<QString>(&json, TR_KEY_result);
if (result)
{
ret.result = *result;
ret.success = *result == QStringLiteral("success");
}
tr_variant* args;
if (tr_variantDictFindDict(&json, TR_KEY_arguments, &args))
{
ret.args = createVariant();
*ret.args = *args;
variantInit(args, false);
}
return ret;
}