/* * 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 #include #include #include #include #include #include #include #include #include #include // tr_free #include // LONG_VERSION_STRING #include "RpcClient.h" // #define DEBUG_HTTP #define REQUEST_DATA_PROPERTY_KEY "requestData" #define REQUEST_FUTUREINTERFACE_PROPERTY_KEY "requestReplyFutureInterface" namespace { void destroyVariant (tr_variant * json) { tr_variantFree (json); tr_free (json); } TrVariantPtr createVariant () { return TrVariantPtr (tr_new0 (tr_variant, 1), &destroyVariant); } } RpcClient::RpcClient (QObject * parent): QObject (parent), mySession (nullptr), myNAM (nullptr), myNextTag (0) { qRegisterMetaType ("TrVariantPtr"); } void RpcClient::stop () { mySession = nullptr; mySessionId.clear (); myUrl.clear (); if (myNAM != nullptr) { myNAM->deleteLater (); myNAM = nullptr; } } void RpcClient::start (tr_session * session) { mySession = session; } void RpcClient::start (const QUrl& url) { myUrl = url; } bool RpcClient::isLocal () const { if (mySession != 0) return true; if (QHostAddress (myUrl.host ()).isLoopback ()) return true; return false; } const QUrl& RpcClient::url () const { return myUrl; } RpcResponseFuture RpcClient::exec (tr_quark method, tr_variant * args) { return exec (tr_quark_get_string (method, nullptr), args); } RpcResponseFuture RpcClient::exec (const char * method, tr_variant * args) { TrVariantPtr json = createVariant (); tr_variantInitDict (json.get (), 3); tr_variantDictAddStr (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 myNextTag++; } void RpcClient::sendNetworkRequest (TrVariantPtr json, const QFutureInterface &promise) { QNetworkRequest request; request.setUrl (myUrl); request.setRawHeader ("User-Agent", (qApp->applicationName () + QLatin1Char ('/') + QString::fromUtf8 (LONG_VERSION_STRING)).toUtf8 ()); request.setRawHeader ("Content-Type", "application/json; charset=UTF-8"); if (!mySessionId.isEmpty ()) request.setRawHeader (TR_RPC_SESSION_ID_HEADER, mySessionId.toUtf8 ()); size_t rawJsonDataLength; char * rawJsonData = tr_variantToStr (json.get (), TR_VARIANT_FMT_JSON_LEAN, &rawJsonDataLength); QByteArray jsonData (rawJsonData, rawJsonDataLength); tr_free (rawJsonData); QNetworkReply * reply = networkAccessManager ()->post (request, jsonData); 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 (myUrl.path ()) << std::endl; for (const QByteArray& b: request.rawHeaderList ()) std::cerr << b.constData () << ": " << request.rawHeader (b).constData () << std::endl; std::cerr << "Body:\n" << jsonData.constData () << std::endl; #endif } void RpcClient::sendLocalRequest (TrVariantPtr json, const QFutureInterface &promise, int64_t tag) { myLocalRequests.insert (tag, promise); tr_rpc_request_exec_json (mySession, json.get (), localSessionCallback, this); } RpcResponseFuture RpcClient::sendRequest (TrVariantPtr json) { int64_t tag = getNextTag (); tr_variantDictAddInt (json.get (), TR_KEY_tag, tag); QFutureInterface promise; promise.setExpectedResultCount (1); promise.setProgressRange (0, 1); promise.setProgressValue (0); promise.reportStarted (); if (mySession != nullptr) sendLocalRequest (json, promise, tag); else if (!myUrl.isEmpty ()) sendNetworkRequest (json, promise); return promise.future (); } QNetworkAccessManager * RpcClient::networkAccessManager () { if (myNAM == 0) { myNAM = new QNetworkAccessManager (); connect (myNAM, SIGNAL (finished (QNetworkReply *)), this, SLOT (networkRequestFinished (QNetworkReply * ))); connect (myNAM, SIGNAL (authenticationRequired (QNetworkReply *,QAuthenticator *)), this, SIGNAL (httpAuthenticationRequired ())); } return myNAM; } void RpcClient::localSessionCallback (tr_session * s, tr_variant * response, void * vself) { Q_UNUSED (s); RpcClient * self = static_cast (vself); TrVariantPtr json = createVariant (); *json = *response; tr_variantInitBool (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 (); QFutureInterface promise = reply->property (REQUEST_FUTUREINTERFACE_PROPERTY_KEY).value> (); #ifdef DEBUG_HTTP std::cerr << "http response header: " << std::endl; for (const QByteArray& 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. mySessionId = QString::fromUtf8 (reply->rawHeader (TR_RPC_SESSION_ID_HEADER)); sendNetworkRequest (reply->property (REQUEST_DATA_PROPERTY_KEY).value (), 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; const QByteArray jsonData = reply->readAll ().trimmed (); TrVariantPtr json = createVariant (); if (tr_variantFromJson (json.get (), jsonData.constData (), jsonData.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 promise = myLocalRequests.take (tag); promise.setProgressRange (0, 1); promise.setProgressValue (1); promise.reportFinished (&result); } int64_t RpcClient::parseResponseTag (tr_variant& json) { int64_t tag; if (!tr_variantDictFindInt (&json, TR_KEY_tag, &tag)) tag = -1; return tag; } RpcResponse RpcClient::parseResponseData (tr_variant& json) { RpcResponse ret; const char * result; if (tr_variantDictFindStr (&json, TR_KEY_result, &result, nullptr)) { ret.result = QString::fromUtf8 (result); ret.success = std::strcmp (result, "success") == 0; } tr_variant * args; if (tr_variantDictFindDict (&json, TR_KEY_arguments, &args)) { ret.args = createVariant (); *ret.args = *args; tr_variantInitBool (args, false); } return ret; }