// This file Copyright © Mnemosyne LLC. // It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only), // or any future license endorsed by Mnemosyne LLC. // License text can be found in the licenses/ folder. #include #include #include #include "RpcClient.h" #include #include #include #include #include #include #include #include #include #include // LONG_VERSION_STRING #include "VariantHelpers.h" using ::trqt::variant_helpers::dictAdd; using ::trqt::variant_helpers::dictFind; using ::trqt::variant_helpers::variantInit; namespace api_compat = libtransmission::api_compat; namespace { char constexpr const* const RequestBodyKey{ "requestBody" }; char constexpr const* const RequestFutureinterfacePropertyKey{ "requestReplyFutureInterface" }; [[nodiscard]] int64_t nextId() { static int64_t id = {}; return id++; } [[nodiscard]] std::pair buildRequest(tr_quark const method, tr_variant* params) { auto const id = nextId(); auto req = tr_variant::Map{ 4U }; req.try_emplace(TR_KEY_jsonrpc, tr_variant::unmanaged_string(JsonRpc::Version)); req.try_emplace(TR_KEY_method, tr_variant::unmanaged_string(method)); req.try_emplace(TR_KEY_id, id); if (params != nullptr) { req.try_emplace(TR_KEY_params, params->clone()); } return { std::move(req), id }; } } // namespace RpcClient::RpcClient(QObject* parent) : QObject{ parent } { qRegisterMetaType("TrVariantPtr"); } void RpcClient::stop() { session_ = nullptr; session_id_.clear(); url_.clear(); request_.reset(); if (nam_ != nullptr) { nam_->deleteLater(); nam_ = nullptr; } } void RpcClient::start(tr_session* session) { session_ = session; } void RpcClient::start(QUrl const& url) { url_ = url; url_is_loopback_ = QHostAddress{ url_.host() }.isLoopback(); request_.reset(); } RpcResponseFuture RpcClient::exec(tr_quark const method, tr_variant* args) { auto [req, id] = buildRequest(method, args); req = api_compat::convert_outgoing_data(req); auto promise = QFutureInterface{}; promise.setExpectedResultCount(1); promise.setProgressRange(0, 1); promise.setProgressValue(0); promise.reportStarted(); if (session_ != nullptr) { sendLocalRequest(req, promise, id); } else if (!url_.isEmpty()) { auto const json = tr_variant_serde::json().compact().to_string(req); auto const body = QByteArray::fromStdString(json); sendNetworkRequest(body, promise); } return promise.future(); } void RpcClient::sendNetworkRequest(QByteArray const& body, QFutureInterface const& promise) { if (!request_) { QNetworkRequest request; request.setUrl(url_); request.setRawHeader( "User-Agent", (QApplication::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()); } request_ = request; } QNetworkReply* reply = networkAccessManager()->post(*request_, body); reply->setProperty(RequestBodyKey, body); reply->setProperty(RequestFutureinterfacePropertyKey, QVariant::fromValue(promise)); connect(reply, &QNetworkReply::downloadProgress, this, &RpcClient::dataReadProgress); connect(reply, &QNetworkReply::uploadProgress, this, &RpcClient::dataSendProgress); if (verbose_) { qInfo() << "sending POST " << qPrintable(url_.path()); for (QByteArray const& b : request_->rawHeaderList()) { qInfo() << b.constData() << ": " << request_->rawHeader(b).constData(); } qInfo() << "Body:"; qInfo() << body.constData(); } } void RpcClient::sendLocalRequest(tr_variant const& req, QFutureInterface const& promise, int64_t const id) { if (verbose_) { fmt::print("{:s}:{:d} sending req:\n{:s}\n", __FILE__, __LINE__, tr_variant_serde::json().to_string(req)); } local_requests_.try_emplace(id, promise); tr_rpc_request_exec( session_, req, [this](tr_session* /*sesson*/, tr_variant&& response) { auto converted = std::make_shared(api_compat::convert_incoming_data(response)); if (verbose_) { auto serde = tr_variant_serde::json(); serde.compact(); fmt::print("{:s}:{:d} got raw response:\n{:s}\n", __FILE__, __LINE__, serde.to_string(response)); fmt::print("{:s}:{:d} converted response:\n{:s}\n", __FILE__, __LINE__, serde.to_string(*converted)); } // 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(this, "localRequestFinished", Qt::QueuedConnection, Q_ARG(TrVariantPtr, converted)); }); } QNetworkAccessManager* RpcClient::networkAccessManager() { if (nam_ == nullptr) { nam_ = new QNetworkAccessManager{}; connect(nam_, &QNetworkAccessManager::finished, this, &RpcClient::networkRequestFinished); connect(nam_, &QNetworkAccessManager::authenticationRequired, this, &RpcClient::httpAuthenticationRequired); } return nam_; } void RpcClient::networkRequestFinished(QNetworkReply* reply) { reply->deleteLater(); auto promise = reply->property(RequestFutureinterfacePropertyKey).value>(); if (verbose_) { qInfo() << "http response header:"; for (QByteArray const& b : reply->rawHeaderList()) { qInfo() << b.constData() << ": " << reply->rawHeader(b).constData(); } } 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)); request_.reset(); sendNetworkRequest(reply->property(RequestBodyKey).toByteArray(), 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 { auto const json = reply->readAll().trimmed().toStdString(); if (verbose_) { fmt::print("{:s}:{:d} got raw response:\n{:s}\n", __FILE__, __LINE__, json); } auto response = RpcResponse{}; if (auto var = tr_variant_serde::json().parse(json)) { var = api_compat::convert_incoming_data(*var); if (verbose_) { auto serde = tr_variant_serde::json(); serde.compact(); fmt::print("{:s}:{:d} compat response:\n{:s}\n", __FILE__, __LINE__, serde.to_string(*var)); } response = parseResponseData(*var); } promise.setProgressValue(1); promise.reportFinished(&response); } } void RpcClient::localRequestFinished(TrVariantPtr response) { if (auto node = local_requests_.extract(parseResponseId(*response))) { auto const result = parseResponseData(*response); auto& promise = node.mapped(); promise.setProgressRange(0, 1); promise.setProgressValue(1); promise.reportFinished(&result); } } int64_t RpcClient::parseResponseId(tr_variant& response) const { return dictFind(&response, TR_KEY_id).value_or(-1); } RpcResponse RpcClient::parseResponseData(tr_variant& response) const { auto ret = RpcResponse{}; ret.success = false; ret.errmsg = QStringLiteral("unknown error"); if (auto* response_map = response.get_if()) { if (auto* result = response_map->find_if(TR_KEY_result)) { ret.success = true; ret.errmsg.clear(); ret.args = std::make_shared(std::move(*result)); } if (auto* error_map = response_map->find_if(TR_KEY_error)) { if (auto const errmsg = error_map->value_if(TR_KEY_message)) { ret.errmsg = QString::fromUtf8(std::data(*errmsg), std::size(*errmsg)); } } } return ret; }