diff --git a/docs/rpc-spec.md b/docs/rpc-spec.md index cb041d9a9..0b0b6f77f 100644 --- a/docs/rpc-spec.md +++ b/docs/rpc-spec.md @@ -1,8 +1,16 @@ +> [!IMPORTANT] +> Transmisson 4.1.0 (`rpc-version` 18) added support for the JSON-RPC 2.0 protocol. +> +> The old bespoke RPC protocol is still supported in Transmission 4 but is deprecated and will be removed in the future. People using the old protocol should update their code! +> +> For documentation of the old RPC protocol, please consult documentation from previous versions. +> https://github.com/transmission/transmission/blob/4.0.6/docs/rpc-spec.md + # Transmission's RPC specification This document describes a protocol for interacting with Transmission sessions remotely. ### 1.1 Terminology -The [JSON](https://www.json.org/) terminology in [RFC 4627](https://datatracker.ietf.org/doc/html/rfc4627) is used. +The [JSON](https://www.json.org/) terminology in [RFC 8259](https://datatracker.ietf.org/doc/html/rfc8259) is used. RPC requests and responses are formatted in JSON. ### 1.2 Tools @@ -24,48 +32,61 @@ Some people outside of the Transmission project have written libraries that wrap ## 2 Message format -Messages are formatted as objects. There are two types: requests (described in [section 2.1](#21-requests)) and responses (described in [section 2.2](#22-responses)). +Transmission follows the [JSON-RPC 2.0](https://www.jsonrpc.org/specification) specification and supports the entirety of it, +except that parameters by-position is not supported, meaning the request parameters must be an Object. -All text **must** be UTF-8 encoded. - -### 2.1 Requests -Requests support three keys: - -1. A required `method` string telling the name of the method to invoke -2. An optional `arguments` object of key/value pairs. The keys allowed are defined by the `method`. -3. An optional `tag` number used by clients to track responses. If provided by a request, the response MUST include the same tag. +Response parameters are returned in the `result` Object. +#### Example request ```json { - "arguments": { - "fields": [ - "version" - ] + "jsonrpc": "2.0", + "params": { + "fields": [ "version" ] }, "method": "session-get", - "tag": 912313 + "id": 912313 } ``` +#### Example response +```json +{ + "jsonrpc": "2.0", + "result": { + "version": "4.1.0-dev (ae226418eb)" + }, + "id": 912313 +} +``` -### 2.2 Responses -Responses to a request will include: +### 2.1 Error data -1. A required `result` string whose value MUST be `success` on success, or an error string on failure. -2. An optional `arguments` object of key/value pairs. Its keys contents are defined by the `method` and `arguments` of the original request. -3. An optional `tag` number as described in 2.1. +JSON-RPC 2.0 allows for additional information about an error be included in the `data` key of the Error object in an implementation-defined format. + +In Transmission, this key is an Object that includes: + +1. An optional `errorString` string that provides additional information that is not included in the `message` key of the Error object. +2. An optional `result` Object that contains additional keys defined by the method. ```json { - "arguments": { - "version": "2.93 (3c5870d4f5)" + "jsonrpc": "2.0", + "error": { + "code": 7, + "message": "HTTP error from backend service", + "data": { + "errorString": "Couldn't test port: No Response (0)", + "result": { + "ipProtocol": "ipv6" + } + } }, - "result": "success", - "tag": 912313 + "id": 912313 } ``` -### 2.3 Transport mechanism +### 2.2 Transport mechanism HTTP POSTing a JSON-encoded request is the preferred way of communicating with a Transmission RPC server. The current Transmission implementation has the default URL as `http://host:9091/transmission/rpc`. Clients @@ -73,7 +94,10 @@ may use this as a default, but should allow the URL to be reconfigured, since the port and path may be changed to allow mapping and/or multiple daemons to run on a single server. -#### 2.3.1 CSRF protection +The RPC server will normally return HTTP 200 regardless of whether the +request succeeded. For JSON-RPC 2.0 notifications, HTTP 204 will be returned. + +#### 2.2.1 CSRF protection Most Transmission RPC servers require a `X-Transmission-Session-Id` header to be sent with requests, to prevent CSRF attacks. @@ -85,7 +109,7 @@ right `X-Transmission-Session-Id` in its own headers. So, the correct way to handle a 409 response is to update your `X-Transmission-Session-Id` and to resend the previous request. -#### 2.3.2 DNS rebinding protection +#### 2.2.2 DNS rebinding protection Additional check is being made on each RPC request to make sure that the client sending the request does so using one of the allowed hostnames by which RPC server is meant to be available. @@ -99,7 +123,7 @@ addresses are always implicitly allowed. For more information on configuration, see settings.json documentation for `rpc-host-whitelist-enabled` and `rpc-host-whitelist` keys. -#### 2.3.3 Authentication +#### 2.2.3 Authentication Enabling authentication is an optional security feature that can be enabled on Transmission RPC servers. Authentication occurs by method of HTTP Basic Access Authentication. @@ -120,8 +144,8 @@ username and password (respectively), separated by a colon. | `torrent-verify` | tr_torrentVerify | verify torrent | `torrent-reannounce` | tr_torrentManualUpdate | re-announce to trackers now -Request arguments: `ids`, which specifies which torrents to use. -All torrents are used if the `ids` argument is omitted. +Request parameters: `ids`, which specifies which torrents to use. +All torrents are used if the `ids` parameter is omitted. `ids` should be one of the following: @@ -132,12 +156,12 @@ All torrents are used if the `ids` argument is omitted. Note that integer torrent ids are not stable across Transmission daemon restarts. Use torrent hashes if you need stable ids. -Response arguments: none +Response parameters: none ### 3.2 Torrent mutator: `torrent-set` Method name: `torrent-set` -Request arguments: +Request parameters: | Key | Value Type | Value Description |:--|:--|:-- @@ -173,20 +197,20 @@ Just as an empty `ids` value is shorthand for "all ids", using an empty array for `files-wanted`, `files-unwanted`, `priority-high`, `priority-low`, or `priority-normal` is shorthand for saying "all files". - Response arguments: none + Response parameters: none ### 3.3 Torrent accessor: `torrent-get` Method name: `torrent-get`. -Request arguments: +Request parameters: 1. An optional `ids` array as described in 3.1. 2. A required `fields` array of keys. (see list below) 3. An optional `format` string specifying how to format the `torrents` response field. Allowed values are `objects` - (default) and `table`. (see "Response arguments" below) + (default) and `table`. (see "Response parameters" below) -Response arguments: +Response parameters: 1. A `torrents` array. @@ -426,12 +450,13 @@ Request: ```json { - "arguments": { + "jsonrpc": "2.0", + "params": { "fields": [ "id", "name", "totalSize" ], "ids": [ 7, 10 ] }, "method": "torrent-get", - "tag": 39693 + "id": 39693 } ``` @@ -439,7 +464,8 @@ Response: ```json { - "arguments": { + "jsonrpc": "2.0", + "result": { "torrents": [ { "id": 10, @@ -453,15 +479,14 @@ Response: } ] }, - "result": "success", - "tag": 39693 + "id": 39693 } ``` ### 3.4 Adding a torrent Method name: `torrent-add` -Request arguments: +Request parameters: | Key | Value Type | Description |:--|:--|:-- @@ -481,11 +506,11 @@ Request arguments: | `sequential_download` | boolean | download torrent pieces sequentially | `sequential_download_from_piece` | number | download from a specific piece when sequential download is enabled -Either `filename` **or** `metainfo` **must** be included. All other arguments are optional. +Either `filename` **or** `metainfo` **must** be included. All other parameters are optional. The format of the `cookies` should be `NAME=CONTENTS`, where `NAME` is the cookie name and `CONTENTS` is what the cookie should contain. Set multiple cookies like this: `name1=content1; name2=content2;` etc. See [libcurl documentation](http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTCOOKIE) for more information. -Response arguments: +Response parameters: * On success, a `torrent-added` object in the form of one of 3.3's torrent objects with the fields for `id`, `name`, and `hashString`. @@ -499,12 +524,12 @@ Method name: `torrent-remove` | `ids` | array | torrent list, as described in 3.1 | `delete-local-data` | boolean | delete local data. (default: false) -Response arguments: none +Response parameters: none ### 3.6 Moving a torrent Method name: `torrent-set-location` -Request arguments: +Request parameters: | Key | Value Type | Description |:--|:--|:-- @@ -512,7 +537,7 @@ Request arguments: | `location` | string | the new torrent location | `move` | boolean | if true, move from previous location. otherwise, search "location" for files (default: false) -Response arguments: none +Response parameters: none ### 3.7 Renaming a torrent's path Method name: `torrent-rename-path` @@ -522,7 +547,7 @@ documentation of `tr_torrentRenamePath()`. In particular, note that if this call succeeds you'll want to update the torrent's `files` and `name` field with `torrent-get`. -Request arguments: +Request parameters: | Key | Value Type | Description |:--|:--|:-- @@ -530,10 +555,10 @@ Request arguments: | `path` | string | the path to the file or folder that will be renamed | `name` | string | the file or folder's new name -Response arguments: `path`, `name`, and `id`, holding the torrent ID integer +Response parameters: `path`, `name`, and `id`, holding the torrent ID integer ## 4 Session requests -### 4.1 Session arguments +### 4.1 Session parameters | Key | Value Type | Description |:--|:--|:-- | `alt-speed-down` | number | max global download speed (kB/s) @@ -619,7 +644,7 @@ to be common behavior. #### 4.1.1 Mutators Method name: `session-set` -Request arguments: the mutable properties from 4.1's arguments, i.e. all of them +Request parameters: the mutable properties from 4.1's parameters, i.e. all of them except: * `blocklist-size` @@ -631,22 +656,22 @@ except: * `units` * `version` -Response arguments: none +Response parameters: none #### 4.1.2 Accessors Method name: `session-get` -Request arguments: an optional `fields` array of keys (see 4.1) +Request parameters: an optional `fields` array of keys (see 4.1) -Response arguments: key/value pairs matching the request's `fields` -argument if present, or all supported fields (see 4.1) otherwise. +Response parameters: key/value pairs matching the request's `fields` +parameter if present, or all supported fields (see 4.1) otherwise. ### 4.2 Session statistics Method name: `session-stats` -Request arguments: none +Request parameters: none -Response arguments: +Response parameters: | Key | Value Type | Description |:--|:--|:-- @@ -671,9 +696,9 @@ A stats object contains: ### 4.3 Blocklist Method name: `blocklist-update` -Request arguments: none +Request parameters: none -Response arguments: a number `blocklist-size` +Response parameters: a number `blocklist-size` ### 4.4 Port checking This method tests to see if your incoming peer port is accessible @@ -681,14 +706,14 @@ from the outside world. Method name: `port-test` -Request arguments: an optional argument `ip_protocol`. +Request parameters: an optional parameter `ip_protocol`. `ip_protocol` is a string specifying the IP protocol version to be used for the port test. Set to `ipv4` to check IPv4, or set to `ipv6` to check IPv6. -For backwards compatibility, it is allowed to omit this argument to get the behaviour before Transmission `4.1.0`, +For backwards compatibility, it is allowed to omit this parameter to get the behaviour before Transmission `4.1.0`, which is to check whichever IP protocol the OS happened to use to connect to our port test service, frankly not very useful. -Response arguments: +Response parameters: | Key | Value Type | Description | :-- | :-- | :-- @@ -696,13 +721,13 @@ Response arguments: | `ip_protocol` | string | `ipv4` if the test was carried out on IPv4, `ipv6` if the test was carried out on IPv6, unset if it cannot be determined ### 4.5 Session shutdown -This method tells the transmission session to shut down. +This method tells the Transmission session to shut down. Method name: `session-close` -Request arguments: none +Request parameters: none -Response arguments: none +Response parameters: none ### 4.6 Queue movement requests | Method name | transmission.h source @@ -712,13 +737,13 @@ Response arguments: none | `queue-move-down` | tr_torrentQueueMoveDown() | `queue-move-bottom` | tr_torrentQueueMoveBottom() -Request arguments: +Request parameters: | Key | Value Type | Description |:--|:--|:-- | `ids` | array | torrent list, as described in 3.1. -Response arguments: none +Response parameters: none ### 4.7 Free space This method tests how much free space is available in a @@ -726,17 +751,17 @@ client-specified folder. Method name: `free-space` -Request arguments: +Request parameters: | Key | Value type | Description |:--|:--|:-- | `path` | string | the directory to query -Response arguments: +Response parameters: | Key | Value type | Description |:--|:--|:-- -| `path` | string | same as the Request argument +| `path` | string | same as the Request parameter | `size-bytes` | number | the size, in bytes, of the free space in that directory | `total_size` | number | the total capacity, in bytes, of that directory @@ -755,17 +780,17 @@ Request parameters: | `speed-limit-up-enabled` | boolean | true means enabled | `speed-limit-up` | number | max global upload speed (kB/s) -Response arguments: none +Response parameters: none #### 4.8.2 Bandwidth group accessor: `group-get` Method name: `group-get` -Request arguments: An optional argument `group`. +Request parameters: An optional parameter `group`. `group` is either a string naming the bandwidth group, or a list of such strings. If `group` is omitted, all bandwidth groups are used. -Response arguments: +Response parameters: | Key | Value type | Description |:--|:--|:-- @@ -1037,7 +1062,10 @@ Transmission 4.0.0 (`rpc-version-semver` 5.3.0, `rpc-version`: 17) | `group-get` | new method | `torrent-get` | :warning: old arg `wanted` was implemented as an array of `0` or `1` in Transmission 3.00 and older, despite being documented as an array of booleans. Transmission 4.0.0 and 4.0.1 "fixed" this by returning an array of booleans; but in practical terms, this change caused an unannounced breaking change for any 3rd party code that expected `0` or `1`. For this reason, 4.0.2 restored the 3.00 behavior and updated this spec to match the code. -Transmission 4.1.0 (`rpc-version-semver` 5.4.0, `rpc-version`: 18) +Transmission 4.1.0 (`rpc-version-semver` 6.0.0, `rpc-version`: 18) + +:bomb: switch to the JSON-RPC 2.0 protocol + | Method | Description |:---|:--- | `session-get` | new arg `sequential_download` diff --git a/libtransmission/quark.cc b/libtransmission/quark.cc index 92dd0c20f..77599b62d 100644 --- a/libtransmission/quark.cc +++ b/libtransmission/quark.cc @@ -65,6 +65,7 @@ auto constexpr MyStatic = std::array{ "clientIsChoked"sv, "clientIsInterested"sv, "clientName"sv, + "code"sv, "comment"sv, "compact-view"sv, "complete"sv, @@ -77,6 +78,7 @@ auto constexpr MyStatic = std::array{ "creator"sv, "cumulative-stats"sv, "current-stats"sv, + "data"sv, "date"sv, "dateCreated"sv, "default-trackers"sv, @@ -170,6 +172,7 @@ auto constexpr MyStatic = std::array{ "isStalled"sv, "isUTP"sv, "isUploadingTo"sv, + "jsonrpc"sv, "labels"sv, "lastAnnouncePeerCount"sv, "lastAnnounceResult"sv, @@ -200,6 +203,7 @@ auto constexpr MyStatic = std::array{ "maxConnectedPeers"sv, "memory-bytes"sv, "memory-units"sv, + "message"sv, "message-level"sv, "metadataPercentComplete"sv, "metadata_size"sv, @@ -215,6 +219,7 @@ auto constexpr MyStatic = std::array{ "nodes6"sv, "open-dialog-dir"sv, "p"sv, + "params"sv, "path"sv, "paused"sv, "pausedTorrentCount"sv, diff --git a/libtransmission/quark.h b/libtransmission/quark.h index 726d096f2..9f8da8928 100644 --- a/libtransmission/quark.h +++ b/libtransmission/quark.h @@ -67,6 +67,7 @@ enum // NOLINT(performance-enum-size) TR_KEY_clientIsChoked, TR_KEY_clientIsInterested, TR_KEY_clientName, + TR_KEY_code, TR_KEY_comment, TR_KEY_compact_view, TR_KEY_complete, @@ -79,6 +80,7 @@ enum // NOLINT(performance-enum-size) TR_KEY_creator, TR_KEY_cumulative_stats, TR_KEY_current_stats, + TR_KEY_data, TR_KEY_date, TR_KEY_dateCreated, TR_KEY_default_trackers, @@ -172,6 +174,7 @@ enum // NOLINT(performance-enum-size) TR_KEY_isStalled, TR_KEY_isUTP, TR_KEY_isUploadingTo, + TR_KEY_jsonrpc, TR_KEY_labels, TR_KEY_lastAnnouncePeerCount, TR_KEY_lastAnnounceResult, @@ -202,6 +205,7 @@ enum // NOLINT(performance-enum-size) TR_KEY_maxConnectedPeers, TR_KEY_memory_bytes, TR_KEY_memory_units, + TR_KEY_message, TR_KEY_message_level, TR_KEY_metadataPercentComplete, TR_KEY_metadata_size, @@ -217,6 +221,7 @@ enum // NOLINT(performance-enum-size) TR_KEY_nodes6, TR_KEY_open_dialog_dir, TR_KEY_p, + TR_KEY_params, TR_KEY_path, TR_KEY_paused, TR_KEY_pausedTorrentCount, diff --git a/libtransmission/rpc-server.cc b/libtransmission/rpc-server.cc index 253355b81..fa520a840 100644 --- a/libtransmission/rpc-server.cc +++ b/libtransmission/rpc-server.cc @@ -356,20 +356,23 @@ void handle_web_client(struct evhttp_request* req, tr_rpc_server const* server) void handle_rpc_from_json(struct evhttp_request* req, tr_rpc_server* server, std::string_view json) { - if (auto otop = tr_variant_serde::json().inplace().parse(json); otop) - { - tr_rpc_request_exec( - server->session, - *otop, - [req, server](tr_session* /*session*/, tr_variant&& content) + tr_rpc_request_exec( + server->session, + json, + [req, server](tr_session* /*session*/, tr_variant&& content) + { + if (!content.has_value()) { - auto* const output_headers = evhttp_request_get_output_headers(req); - auto* const response = make_response(req, server, tr_variant_serde::json().compact().to_string(content)); - evhttp_add_header(output_headers, "Content-Type", "application/json; charset=UTF-8"); - evhttp_send_reply(req, HTTP_OK, "OK", response); - evbuffer_free(response); - }); - } + evhttp_send_reply(req, HTTP_NOCONTENT, "OK", nullptr); + return; + } + + auto* const output_headers = evhttp_request_get_output_headers(req); + auto* const response = make_response(req, server, tr_variant_serde::json().compact().to_string(content)); + evhttp_add_header(output_headers, "Content-Type", "application/json; charset=UTF-8"); + evhttp_send_reply(req, HTTP_OK, "OK", response); + evbuffer_free(response); + }); } void handle_rpc(struct evhttp_request* req, tr_rpc_server* server) diff --git a/libtransmission/rpcimpl.cc b/libtransmission/rpcimpl.cc index 38b15e774..148eb327f 100644 --- a/libtransmission/rpcimpl.cc +++ b/libtransmission/rpcimpl.cc @@ -47,12 +47,119 @@ using namespace std::literals; using namespace libtransmission::Values; +namespace JsonRpc +{ +// https://www.jsonrpc.org/specification#error_object +namespace Error +{ +namespace +{ +[[nodiscard]] constexpr std::string_view get_message(Code code) +{ + switch (code) + { + case PARSE_ERROR: + return "Parse error"sv; + case INVALID_REQUEST: + return "Invalid Request"sv; + case METHOD_NOT_FOUND: + return "Method not found"sv; + case INVALID_PARAMS: + return "Invalid params"sv; + case INTERNAL_ERROR: + return "Internal error"sv; + case SUCCESS: + return "success"sv; + case SET_ANNOUNCE_LIST: + return "error setting announce list"sv; + case INVALID_TRACKER_LIST: + return "Invalid tracker list"sv; + case PATH_NOT_ABSOLUTE: + return "path is not absolute"sv; + case UNRECOGNIZED_INFO: + return "unrecognized info"sv; + case SYSTEM_ERROR: + return "system error"sv; + case FILE_IDX_OOR: + return "file index out of range"sv; + case PIECE_IDX_OOR: + return "piece index out of range"sv; + case HTTP_ERROR: + return "HTTP error from backend service"sv; + case CORRUPT_TORRENT: + return "invalid or corrupt torrent file"sv; + default: + return {}; + } +} + +[[nodiscard]] tr_variant::Map build_data(std::string_view error_string, tr_variant::Map&& result) +{ + auto ret = tr_variant::Map{ 2U }; + + if (!std::empty(error_string)) + { + ret.try_emplace(TR_KEY_errorString, error_string); + } + + if (!std::empty(result)) + { + ret.try_emplace(TR_KEY_result, std::move(result)); + } + + return ret; +} + +[[nodiscard]] tr_variant::Map build(Code code, tr_variant::Map&& data) +{ + auto ret = tr_variant::Map{ 3U }; + ret.try_emplace(TR_KEY_code, code); + ret.try_emplace(TR_KEY_message, tr_variant::unmanaged_string(get_message(code))); + if (!std::empty(data)) + { + ret.try_emplace(TR_KEY_data, std::move(data)); + } + + return ret; +} +} // namespace +} // namespace Error + +namespace +{ +// https://www.jsonrpc.org/specification#response_object +[[nodiscard]] tr_variant::Map build_response(Error::Code code, tr_variant id, tr_variant::Map&& body) +{ + if (id.index() != tr_variant::StringIndex && id.index() != tr_variant::IntIndex && id.index() != tr_variant::DoubleIndex && + id.index() != tr_variant::NullIndex) + { + TR_ASSERT(false); + id = nullptr; + } + + auto ret = tr_variant::Map{ 3U }; + ret.try_emplace(TR_KEY_jsonrpc, Version); + if (code == Error::SUCCESS) + { + ret.try_emplace(TR_KEY_result, std::move(body)); + } + else + { + ret.try_emplace(TR_KEY_error, Error::build(code, std::move(body))); + } + ret.try_emplace(TR_KEY_id, std::move(id)); + + return ret; +} +} // namespace +} // namespace JsonRpc + namespace { auto constexpr RecentlyActiveSeconds = time_t{ 60 }; auto constexpr RpcVersion = int64_t{ 18 }; auto constexpr RpcVersionMin = int64_t{ 14 }; -auto constexpr RpcVersionSemver = "5.4.0"sv; +auto constexpr RpcVersionSemver = "6.0.0"sv; enum class TrFormat : uint8_t { @@ -67,32 +174,49 @@ enum class TrFormat : uint8_t * when the task is complete */ struct tr_rpc_idle_data { - std::optional tag; + tr_variant id; tr_session* session = nullptr; tr_variant::Map args_out; tr_rpc_response_func callback; }; -auto constexpr SuccessResult = "success"sv; - -void tr_idle_function_done(struct tr_rpc_idle_data* data, std::string_view result) +void tr_rpc_idle_done_legacy(struct tr_rpc_idle_data* data, JsonRpc::Error::Code code, std::string_view result) { // build the response auto response_map = tr_variant::Map{ 3U }; response_map.try_emplace(TR_KEY_arguments, std::move(data->args_out)); - response_map.try_emplace(TR_KEY_result, result); - if (auto const& tag = data->tag; tag.has_value()) + response_map.try_emplace(TR_KEY_result, std::empty(result) ? JsonRpc::Error::get_message(code) : result); + if (auto& tag = data->id; tag.has_value()) { - response_map.try_emplace(TR_KEY_tag, *tag); + response_map.try_emplace(TR_KEY_tag, std::move(tag)); } // send the response back to the listener - (data->callback)(data->session, tr_variant{ std::move(response_map) }); - - // cleanup - delete data; + data->callback(data->session, tr_variant{ std::move(response_map) }); } +void tr_rpc_idle_done(struct tr_rpc_idle_data* data, JsonRpc::Error::Code code, std::string_view errmsg) +{ + using namespace JsonRpc; + + if (data->id.has_value()) + { + auto const is_success = code == Error::SUCCESS; + data->callback( + data->session, + build_response( + code, + std::move(data->id), + is_success ? std::move(data->args_out) : Error::build_data(errmsg, std::move(data->args_out)))); + } + else // notification + { + data->callback(data->session, {}); + } +} + +using DoneCb = std::function; + // --- [[nodiscard]] auto getTorrents(tr_session* session, tr_variant::Map const& args) @@ -161,39 +285,54 @@ void notifyBatchQueueChange(tr_session* session, std::vector const& session->rpcNotify(TR_RPC_SESSION_QUEUE_POSITIONS_CHANGED); } -char const* queueMoveTop(tr_session* session, tr_variant::Map const& args_in, tr_variant::Map& /*args_out*/) +[[nodiscard]] std::pair queueMoveTop( + tr_session* session, + tr_variant::Map const& args_in, + tr_variant::Map& /*args_out*/) { auto const torrents = getTorrents(session, args_in); tr_torrentsQueueMoveTop(std::data(torrents), std::size(torrents)); notifyBatchQueueChange(session, torrents); - return nullptr; + return { JsonRpc::Error::SUCCESS, {} }; } -char const* queueMoveUp(tr_session* session, tr_variant::Map const& args_in, tr_variant::Map& /*args_out*/) +[[nodiscard]] std::pair queueMoveUp( + tr_session* session, + tr_variant::Map const& args_in, + tr_variant::Map& /*args_out*/) { auto const torrents = getTorrents(session, args_in); tr_torrentsQueueMoveUp(std::data(torrents), std::size(torrents)); notifyBatchQueueChange(session, torrents); - return nullptr; + return { JsonRpc::Error::SUCCESS, {} }; } -char const* queueMoveDown(tr_session* session, tr_variant::Map const& args_in, tr_variant::Map& /*args_out*/) +[[nodiscard]] std::pair queueMoveDown( + tr_session* session, + tr_variant::Map const& args_in, + tr_variant::Map& /*args_out*/) { auto const torrents = getTorrents(session, args_in); tr_torrentsQueueMoveDown(std::data(torrents), std::size(torrents)); notifyBatchQueueChange(session, torrents); - return nullptr; + return { JsonRpc::Error::SUCCESS, {} }; } -char const* queueMoveBottom(tr_session* session, tr_variant::Map const& args_in, tr_variant::Map& /*args_out*/) +[[nodiscard]] std::pair queueMoveBottom( + tr_session* session, + tr_variant::Map const& args_in, + tr_variant::Map& /*args_out*/) { auto const torrents = getTorrents(session, args_in); tr_torrentsQueueMoveBottom(std::data(torrents), std::size(torrents)); notifyBatchQueueChange(session, torrents); - return nullptr; + return { JsonRpc::Error::SUCCESS, {} }; } -char const* torrentStart(tr_session* session, tr_variant::Map const& args_in, tr_variant::Map& /*args_out*/) +[[nodiscard]] std::pair torrentStart( + tr_session* session, + tr_variant::Map const& args_in, + tr_variant::Map& /*args_out*/) { auto torrents = getTorrents(session, args_in); std::sort(std::begin(torrents), std::end(torrents), tr_torrent::CompareQueuePosition); @@ -206,10 +345,13 @@ char const* torrentStart(tr_session* session, tr_variant::Map const& args_in, tr } } - return nullptr; + return { JsonRpc::Error::SUCCESS, {} }; } -char const* torrentStartNow(tr_session* session, tr_variant::Map const& args_in, tr_variant::Map& /*args_out*/) +[[nodiscard]] std::pair torrentStartNow( + tr_session* session, + tr_variant::Map const& args_in, + tr_variant::Map& /*args_out*/) { auto torrents = getTorrents(session, args_in); std::sort(std::begin(torrents), std::end(torrents), tr_torrent::CompareQueuePosition); @@ -222,10 +364,13 @@ char const* torrentStartNow(tr_session* session, tr_variant::Map const& args_in, } } - return nullptr; + return { JsonRpc::Error::SUCCESS, {} }; } -char const* torrentStop(tr_session* session, tr_variant::Map const& args_in, tr_variant::Map& /*args_out*/) +[[nodiscard]] std::pair torrentStop( + tr_session* session, + tr_variant::Map const& args_in, + tr_variant::Map& /*args_out*/) { for (auto* tor : getTorrents(session, args_in)) { @@ -236,10 +381,13 @@ char const* torrentStop(tr_session* session, tr_variant::Map const& args_in, tr_ } } - return nullptr; + return { JsonRpc::Error::SUCCESS, {} }; } -char const* torrentRemove(tr_session* session, tr_variant::Map const& args_in, tr_variant::Map& /*args_out*/) +[[nodiscard]] std::pair torrentRemove( + tr_session* session, + tr_variant::Map const& args_in, + tr_variant::Map& /*args_out*/) { auto const delete_flag = args_in.value_if(TR_KEY_delete_local_data).value_or(false); auto const type = delete_flag ? TR_RPC_TORRENT_TRASHING : TR_RPC_TORRENT_REMOVING; @@ -252,10 +400,13 @@ char const* torrentRemove(tr_session* session, tr_variant::Map const& args_in, t } } - return nullptr; + return { JsonRpc::Error::SUCCESS, {} }; } -char const* torrentReannounce(tr_session* session, tr_variant::Map const& args_in, tr_variant::Map& /*args_out*/) +[[nodiscard]] std::pair torrentReannounce( + tr_session* session, + tr_variant::Map const& args_in, + tr_variant::Map& /*args_out*/) { for (auto* tor : getTorrents(session, args_in)) { @@ -266,10 +417,13 @@ char const* torrentReannounce(tr_session* session, tr_variant::Map const& args_i } } - return nullptr; + return { JsonRpc::Error::SUCCESS, {} }; } -char const* torrentVerify(tr_session* session, tr_variant::Map const& args_in, tr_variant::Map& /*args_out*/) +[[nodiscard]] std::pair torrentVerify( + tr_session* session, + tr_variant::Map const& args_in, + tr_variant::Map& /*args_out*/) { for (auto* tor : getTorrents(session, args_in)) { @@ -277,7 +431,7 @@ char const* torrentVerify(tr_session* session, tr_variant::Map const& args_in, t session->rpcNotify(TR_RPC_TORRENT_CHANGED, tor); } - return nullptr; + return { JsonRpc::Error::SUCCESS, {} }; } // --- @@ -733,8 +887,13 @@ namespace make_torrent_field_helpers make_torrent_info_map(tor, fields, field_count); } -char const* torrentGet(tr_session* session, tr_variant::Map const& args_in, tr_variant::Map& args_out) +[[nodiscard]] std::pair torrentGet( + tr_session* session, + tr_variant::Map const& args_in, + tr_variant::Map& args_out) { + using namespace JsonRpc; + auto const torrents = getTorrents(session, args_in); auto torrents_vec = tr_variant::Vector{}; @@ -774,7 +933,7 @@ char const* torrentGet(tr_session* session, tr_variant::Map const& args_in, tr_v if (std::empty(keys)) { - return "no fields specified"; + return { Error::INVALID_PARAMS, "no fields specified"s }; } if (format == TrFormat::Table) @@ -796,13 +955,16 @@ char const* torrentGet(tr_session* session, tr_variant::Map const& args_in, tr_v } args_out.try_emplace(TR_KEY_torrents, std::move(torrents_vec)); - return nullptr; // no error message + return { Error::SUCCESS, {} }; // no error message } // --- -[[nodiscard]] std::pair make_labels(tr_variant::Vector const& labels_vec) +[[nodiscard]] std::tuple make_labels( + tr_variant::Vector const& labels_vec) { + using namespace JsonRpc; + auto const n_labels = std::size(labels_vec); auto labels = tr_torrent::labels_t{}; @@ -815,38 +977,37 @@ char const* torrentGet(tr_session* session, tr_variant::Map const& args_in, tr_v if (std::empty(label)) { - return { {}, "labels cannot be empty" }; + return { {}, Error::INVALID_PARAMS, "labels cannot be empty"s }; } if (tr_strv_contains(label, ',')) { - return { {}, "labels cannot contain comma (,) character" }; + return { {}, Error::INVALID_PARAMS, "labels cannot contain comma (,) character"s }; } labels.emplace_back(tr_quark_new(label)); } } - return { std::move(labels), nullptr }; + return { std::move(labels), Error::SUCCESS, {} }; } -char const* set_labels(tr_torrent* tor, tr_variant::Vector const& list) +[[nodiscard]] std::pair set_labels(tr_torrent* tor, tr_variant::Vector const& list) { - auto [labels, errmsg] = make_labels(list); - - if (errmsg != nullptr) + auto [labels, err, errmsg] = make_labels(list); + if (err == JsonRpc::Error::SUCCESS) { - return errmsg; + tor->set_labels(labels); } - - tor->set_labels(labels); - return nullptr; + return { err, std::move(errmsg) }; } -[[nodiscard]] std::pair, char const*> get_file_indices( +[[nodiscard]] std::tuple, JsonRpc::Error::Code, std::string> get_file_indices( tr_torrent const* tor, tr_variant::Vector const& files_vec) { + using namespace JsonRpc; + auto const n_files = tor->file_count(); auto files = std::vector{}; @@ -869,51 +1030,60 @@ char const* set_labels(tr_torrent* tor, tr_variant::Vector const& list) } else { - return { {}, "file index out of range" }; + return { {}, Error::FILE_IDX_OOR, std::string{} }; } } } } - return { std::move(files), nullptr }; + return { std::move(files), Error::SUCCESS, {} }; } -char const* set_file_priorities(tr_torrent* tor, tr_priority_t priority, tr_variant::Vector const& files_vec) +[[nodiscard]] std::pair set_file_priorities( + tr_torrent* tor, + tr_priority_t priority, + tr_variant::Vector const& files_vec) { - auto const [indices, errmsg] = get_file_indices(tor, files_vec); - if (errmsg != nullptr) + auto const [indices, err, errmsg] = get_file_indices(tor, files_vec); + if (err == JsonRpc::Error::SUCCESS) { - return errmsg; + tor->set_file_priorities(std::data(indices), std::size(indices), priority); } - - tor->set_file_priorities(std::data(indices), std::size(indices), priority); - return nullptr; // no error + return { err, std::move(errmsg) }; } -char const* set_sequential_download_from_piece(tr_torrent& tor, tr_piece_index_t piece) +[[nodiscard]] std::pair set_sequential_download_from_piece( + tr_torrent& tor, + tr_piece_index_t piece) { + using namespace JsonRpc; + if (piece >= tor.piece_count()) { - return "piece to sequentially download from is outside pieces range"; + return { Error::PIECE_IDX_OOR, "piece to sequentially download from is outside pieces range"s }; } tor.set_sequential_download_from_piece(piece); - return nullptr; // no error + return { Error::SUCCESS, {} }; // no error } -[[nodiscard]] char const* set_file_dls(tr_torrent* tor, bool wanted, tr_variant::Vector const& files_vec) +[[nodiscard]] std::pair set_file_dls( + tr_torrent* tor, + bool wanted, + tr_variant::Vector const& files_vec) { - auto const [indices, errmsg] = get_file_indices(tor, files_vec); - if (errmsg != nullptr) + auto const [indices, err, errmsg] = get_file_indices(tor, files_vec); + if (err == JsonRpc::Error::SUCCESS) { - return errmsg; + tor->set_files_wanted(std::data(indices), std::size(indices), wanted); } - tor->set_files_wanted(std::data(indices), std::size(indices), wanted); - return nullptr; // no error + return { err, std::move(errmsg) }; } -char const* add_tracker_urls(tr_torrent* tor, tr_variant::Vector const& urls_vec) +[[nodiscard]] std::pair add_tracker_urls(tr_torrent* tor, tr_variant::Vector const& urls_vec) { + using namespace JsonRpc; + auto ann = tor->announce_list(); auto const baseline = ann; @@ -927,15 +1097,17 @@ char const* add_tracker_urls(tr_torrent* tor, tr_variant::Vector const& urls_vec if (ann == baseline) // unchanged { - return "error setting announce list"; + return { Error::SET_ANNOUNCE_LIST, {} }; } tor->set_announce_list(std::move(ann)); - return nullptr; + return { Error::SUCCESS, {} }; } -char const* replace_trackers(tr_torrent* tor, tr_variant::Vector const& urls_vec) +[[nodiscard]] std::pair replace_trackers(tr_torrent* tor, tr_variant::Vector const& urls_vec) { + using namespace JsonRpc; + auto ann = tor->announce_list(); auto const baseline = ann; @@ -952,15 +1124,17 @@ char const* replace_trackers(tr_torrent* tor, tr_variant::Vector const& urls_vec if (ann == baseline) // unchanged { - return "error setting announce list"; + return { Error::SET_ANNOUNCE_LIST, {} }; } tor->set_announce_list(std::move(ann)); - return nullptr; + return { Error::SUCCESS, {} }; } -char const* remove_trackers(tr_torrent* tor, tr_variant::Vector const& ids_vec) +[[nodiscard]] std::pair remove_trackers(tr_torrent* tor, tr_variant::Vector const& ids_vec) { + using namespace JsonRpc; + auto ann = tor->announce_list(); auto const baseline = ann; @@ -974,16 +1148,22 @@ char const* remove_trackers(tr_torrent* tor, tr_variant::Vector const& ids_vec) if (ann == baseline) // unchanged { - return "error setting announce list"; + return { Error::SET_ANNOUNCE_LIST, {} }; } tor->set_announce_list(std::move(ann)); - return nullptr; + return { Error::SUCCESS, {} }; } -char const* torrentSet(tr_session* session, tr_variant::Map const& args_in, tr_variant::Map& /*args_out*/) +[[nodiscard]] std::pair torrentSet( + tr_session* session, + tr_variant::Map const& args_in, + tr_variant::Map& /*args_out*/) { - char const* errmsg = nullptr; + using namespace JsonRpc; + + auto err = Error::SUCCESS; + auto errmsg = std::string{}; for (auto* tor : getTorrents(session, args_in)) { @@ -1000,19 +1180,20 @@ char const* torrentSet(tr_session* session, tr_variant::Map const& args_in, tr_v tor->set_bandwidth_group(*val); } - if (auto const* val = args_in.find_if(TR_KEY_labels); val != nullptr && errmsg == nullptr) + if (auto const* val = args_in.find_if(TR_KEY_labels); val != nullptr && err == Error::SUCCESS) { - errmsg = set_labels(tor, *val); + std::tie(err, errmsg) = set_labels(tor, *val); } - if (auto const* val = args_in.find_if(TR_KEY_files_unwanted); val != nullptr && errmsg == nullptr) + if (auto const* val = args_in.find_if(TR_KEY_files_unwanted); + val != nullptr && err == Error::SUCCESS) { - errmsg = set_file_dls(tor, false, *val); + std::tie(err, errmsg) = set_file_dls(tor, false, *val); } - if (auto const* val = args_in.find_if(TR_KEY_files_wanted); val != nullptr && errmsg == nullptr) + if (auto const* val = args_in.find_if(TR_KEY_files_wanted); val != nullptr && err == Error::SUCCESS) { - errmsg = set_file_dls(tor, true, *val); + std::tie(err, errmsg) = set_file_dls(tor, true, *val); } if (auto const val = args_in.value_if(TR_KEY_peer_limit)) @@ -1020,19 +1201,21 @@ char const* torrentSet(tr_session* session, tr_variant::Map const& args_in, tr_v tr_torrentSetPeerLimit(tor, *val); } - if (auto const* val = args_in.find_if(TR_KEY_priority_high); val != nullptr && errmsg == nullptr) + if (auto const* val = args_in.find_if(TR_KEY_priority_high); + val != nullptr && err == Error::SUCCESS) { - errmsg = set_file_priorities(tor, TR_PRI_HIGH, *val); + std::tie(err, errmsg) = set_file_priorities(tor, TR_PRI_HIGH, *val); } - if (auto const* val = args_in.find_if(TR_KEY_priority_low); val != nullptr && errmsg == nullptr) + if (auto const* val = args_in.find_if(TR_KEY_priority_low); val != nullptr && err == Error::SUCCESS) { - errmsg = set_file_priorities(tor, TR_PRI_LOW, *val); + std::tie(err, errmsg) = set_file_priorities(tor, TR_PRI_LOW, *val); } - if (auto const* val = args_in.find_if(TR_KEY_priority_normal); val != nullptr && errmsg == nullptr) + if (auto const* val = args_in.find_if(TR_KEY_priority_normal); + val != nullptr && err == Error::SUCCESS) { - errmsg = set_file_priorities(tor, TR_PRI_NORMAL, *val); + std::tie(err, errmsg) = set_file_priorities(tor, TR_PRI_NORMAL, *val); } if (auto const val = args_in.value_if(TR_KEY_downloadLimit)) @@ -1045,9 +1228,9 @@ char const* torrentSet(tr_session* session, tr_variant::Map const& args_in, tr_v tor->set_sequential_download(*val); } - if (auto const val = args_in.value_if(TR_KEY_sequential_download_from_piece); val && errmsg == nullptr) + if (auto const val = args_in.value_if(TR_KEY_sequential_download_from_piece); val && err == Error::SUCCESS) { - errmsg = set_sequential_download_from_piece(*tor, *val); + std::tie(err, errmsg) = set_sequential_download_from_piece(*tor, *val); } if (auto const val = args_in.value_if(TR_KEY_downloadLimited)) @@ -1097,44 +1280,50 @@ char const* torrentSet(tr_session* session, tr_variant::Map const& args_in, tr_v if (auto const* val = args_in.find_if(TR_KEY_trackerAdd)) { - errmsg = add_tracker_urls(tor, *val); + std::tie(err, errmsg) = add_tracker_urls(tor, *val); } if (auto const* val = args_in.find_if(TR_KEY_trackerRemove)) { - errmsg = remove_trackers(tor, *val); + std::tie(err, errmsg) = remove_trackers(tor, *val); } if (auto const* val = args_in.find_if(TR_KEY_trackerReplace)) { - errmsg = replace_trackers(tor, *val); + std::tie(err, errmsg) = replace_trackers(tor, *val); } if (auto const val = args_in.value_if(TR_KEY_trackerList)) { if (!tor->set_announce_list(*val)) { - errmsg = "Invalid tracker list"; + err = Error::INVALID_TRACKER_LIST; + errmsg = {}; } } session->rpcNotify(TR_RPC_TORRENT_CHANGED, tor); } - return errmsg; + return { err, std::move(errmsg) }; } -char const* torrentSetLocation(tr_session* session, tr_variant::Map const& args_in, tr_variant::Map& /*args_out*/) +[[nodiscard]] std::pair torrentSetLocation( + tr_session* session, + tr_variant::Map const& args_in, + tr_variant::Map& /*args_out*/) { + using namespace JsonRpc; + auto const location = args_in.value_if(TR_KEY_location); if (!location) { - return "no location"; + return { Error::INVALID_PARAMS, "no location"s }; } if (tr_sys_path_is_relative(*location)) { - return "new location path is not absolute"; + return { Error::PATH_NOT_ABSOLUTE, "new location path is not absolute"s }; } auto const move_flag = args_in.value_if(TR_KEY_move).value_or(false); @@ -1144,40 +1333,62 @@ char const* torrentSetLocation(tr_session* session, tr_variant::Map const& args_ session->rpcNotify(TR_RPC_TORRENT_MOVED, tor); } - return nullptr; + return { Error::SUCCESS, {} }; } // --- -void torrentRenamePathDone(tr_torrent* tor, char const* oldpath, char const* newname, int error, void* user_data) +void torrentRenamePathDone( + tr_torrent* tor, + char const* oldpath, + char const* newname, + int error, + DoneCb const& done_cb, + void* user_data) { + using namespace JsonRpc; + auto* const data = static_cast(user_data); data->args_out.try_emplace(TR_KEY_id, tor->id()); data->args_out.try_emplace(TR_KEY_path, oldpath); data->args_out.try_emplace(TR_KEY_name, newname); - tr_idle_function_done(data, error != 0 ? tr_strerror(error) : SuccessResult); + auto const is_success = error == 0; + done_cb(data, is_success ? Error::SUCCESS : Error::SYSTEM_ERROR, is_success ? ""sv : tr_strerror(error)); } -char const* torrentRenamePath(tr_session* session, tr_variant::Map const& args_in, struct tr_rpc_idle_data* idle_data) +void torrentRenamePath( + tr_session* session, + tr_variant::Map const& args_in, + DoneCb&& done_cb, + struct tr_rpc_idle_data* idle_data) { + using namespace JsonRpc; + auto const torrents = getTorrents(session, args_in); if (std::size(torrents) != 1U) { - return "torrent-rename-path requires 1 torrent"; + done_cb(idle_data, Error::INVALID_PARAMS, "torrent-rename-path requires 1 torrent"sv); + return; } auto const oldpath = args_in.value_if(TR_KEY_path).value_or(""sv); auto const newname = args_in.value_if(TR_KEY_name).value_or(""sv); - torrents[0]->rename_path(oldpath, newname, torrentRenamePathDone, idle_data); - return nullptr; // no error + torrents[0]->rename_path( + oldpath, + newname, + [cb = std::move(done_cb)](tr_torrent* tor, char const* old_path, char const* new_name, int error, void* user_data) + { torrentRenamePathDone(tor, old_path, new_name, error, cb, user_data); }, + idle_data); } // --- -void onPortTested(tr_web::FetchResponse const& web_response) +void onPortTested(tr_web::FetchResponse const& web_response, DoneCb const& done_cb) { + using namespace JsonRpc; + auto const& [status, body, primary_ip, did_connect, did_timeout, user_data] = web_response; auto* data = static_cast(user_data); @@ -1189,8 +1400,9 @@ void onPortTested(tr_web::FetchResponse const& web_response) if (status != 200) { - tr_idle_function_done( + done_cb( data, + Error::HTTP_ERROR, fmt::format( fmt::runtime(_("Couldn't test port: {error} ({error_code})")), fmt::arg("error", tr_webGetResponseStr(status)), @@ -1199,44 +1411,57 @@ void onPortTested(tr_web::FetchResponse const& web_response) } data->args_out.try_emplace(TR_KEY_port_is_open, tr_strv_starts_with(body, '1')); - tr_idle_function_done(data, SuccessResult); + done_cb(data, Error::SUCCESS, {}); } -char const* portTest(tr_session* session, tr_variant::Map const& args_in, struct tr_rpc_idle_data* idle_data) +void portTest(tr_session* session, tr_variant::Map const& args_in, DoneCb&& done_cb, struct tr_rpc_idle_data* idle_data) { + using namespace JsonRpc; + static auto constexpr TimeoutSecs = 20s; auto const port = session->advertisedPeerPort(); auto const url = fmt::format("https://portcheck.transmissionbt.com/{:d}", port.host()); - auto options = tr_web::FetchOptions{ url, onPortTested, idle_data }; - options.timeout_secs = TimeoutSecs; + auto ip_proto = std::optional{}; if (auto const val = args_in.value_if(TR_KEY_ip_protocol)) { if (*val == "ipv4"sv) { - options.ip_proto = tr_web::FetchOptions::IPProtocol::V4; + ip_proto = tr_web::FetchOptions::IPProtocol::V4; idle_data->args_out.try_emplace(TR_KEY_ip_protocol, "ipv4"sv); } else if (*val == "ipv6"sv) { - options.ip_proto = tr_web::FetchOptions::IPProtocol::V6; + ip_proto = tr_web::FetchOptions::IPProtocol::V6; idle_data->args_out.try_emplace(TR_KEY_ip_protocol, "ipv6"sv); } else { - return "invalid ip protocol string"; + done_cb(idle_data, Error::INVALID_PARAMS, "invalid ip protocol string"sv); + return; } } + auto options = tr_web::FetchOptions{ + url, + [cb = std::move(done_cb)](tr_web::FetchResponse const& r) { onPortTested(r, cb); }, + idle_data, + }; + options.timeout_secs = TimeoutSecs; + if (ip_proto) + { + options.ip_proto = *ip_proto; + } session->fetch(std::move(options)); - return nullptr; } // --- -void onBlocklistFetched(tr_web::FetchResponse const& web_response) +void onBlocklistFetched(tr_web::FetchResponse const& web_response, DoneCb const& done_cb) { + using namespace JsonRpc; + auto const& [status, body, primary_ip, did_connect, did_timeout, user_data] = web_response; auto* data = static_cast(user_data); auto* const session = data->session; @@ -1244,8 +1469,9 @@ void onBlocklistFetched(tr_web::FetchResponse const& web_response) if (status != 200) { // we failed to download the blocklist... - tr_idle_function_done( + done_cb( data, + Error::HTTP_ERROR, fmt::format( fmt::runtime(_("Couldn't fetch blocklist: {error} ({error_code})")), fmt::arg("error", tr_webGetResponseStr(status)), @@ -1289,8 +1515,9 @@ void onBlocklistFetched(tr_web::FetchResponse const& web_response) auto const filename = tr_pathbuf{ session->configDir(), "/blocklist.tmp"sv }; if (auto error = tr_error{}; !tr_file_save(filename, content, &error)) { - tr_idle_function_done( + done_cb( data, + Error::SYSTEM_ERROR, fmt::format( fmt::runtime(_("Couldn't save '{path}': {error} ({error_code})")), fmt::arg("path", filename), @@ -1302,25 +1529,34 @@ void onBlocklistFetched(tr_web::FetchResponse const& web_response) // feed it to the session and give the client a response data->args_out.try_emplace(TR_KEY_blocklist_size, tr_blocklistSetContent(session, filename)); tr_sys_path_remove(filename); - tr_idle_function_done(data, SuccessResult); + done_cb(data, Error::SUCCESS, {}); } -char const* blocklistUpdate(tr_session* session, tr_variant::Map const& /*args_in*/, struct tr_rpc_idle_data* idle_data) +void blocklistUpdate( + tr_session* session, + tr_variant::Map const& /*args_in*/, + DoneCb&& done_cb, + struct tr_rpc_idle_data* idle_data) { - session->fetch({ session->blocklistUrl(), onBlocklistFetched, idle_data }); - return nullptr; + session->fetch({ + session->blocklistUrl(), + [cb = std::move(done_cb)](tr_web::FetchResponse const& r) { onBlocklistFetched(r, cb); }, + idle_data, + }); } // --- -void add_torrent_impl(struct tr_rpc_idle_data* data, tr_ctor& ctor) +void add_torrent_impl(struct tr_rpc_idle_data* data, DoneCb const& done_cb, tr_ctor& ctor) { + using namespace JsonRpc; + tr_torrent* duplicate_of = nullptr; tr_torrent* tor = tr_torrentNew(&ctor, &duplicate_of); if (tor == nullptr && duplicate_of == nullptr) { - tr_idle_function_done(data, "invalid or corrupt torrent file"sv); + done_cb(data, Error::CORRUPT_TORRENT, {}); return; } @@ -1330,7 +1566,7 @@ void add_torrent_impl(struct tr_rpc_idle_data* data, tr_ctor& ctor) data->args_out.try_emplace( TR_KEY_torrent_duplicate, make_torrent_info(duplicate_of, TrFormat::Object, std::data(Fields), std::size(Fields))); - tr_idle_function_done(data, SuccessResult); + done_cb(data, Error::SUCCESS, {}); return; } @@ -1338,7 +1574,7 @@ void add_torrent_impl(struct tr_rpc_idle_data* data, tr_ctor& ctor) data->args_out.try_emplace( TR_KEY_torrent_added, make_torrent_info(tor, TrFormat::Object, std::data(Fields), std::size(Fields))); - tr_idle_function_done(data, SuccessResult); + done_cb(data, Error::SUCCESS, {}); } struct add_torrent_idle_data @@ -1353,7 +1589,7 @@ struct add_torrent_idle_data tr_ctor ctor; }; -void onMetadataFetched(tr_web::FetchResponse const& web_response) +void onMetadataFetched(tr_web::FetchResponse const& web_response, DoneCb const& done_cb) { auto const& [status, body, primary_ip, did_connect, did_timeout, user_data] = web_response; auto* data = static_cast(user_data); @@ -1367,12 +1603,13 @@ void onMetadataFetched(tr_web::FetchResponse const& web_response) if (status == 200 || status == 221) /* http or ftp success.. */ { data->ctor.set_metainfo(body); - add_torrent_impl(data->data, data->ctor); + add_torrent_impl(data->data, done_cb, data->ctor); } else { - tr_idle_function_done( + done_cb( data->data, + JsonRpc::Error::HTTP_ERROR, fmt::format( fmt::runtime(_("Couldn't fetch torrent: {error} ({error_code})")), fmt::arg("error", tr_webGetResponseStr(status)), @@ -1404,26 +1641,30 @@ bool isCurlURL(std::string_view url) return files; } -char const* torrentAdd(tr_session* session, tr_variant::Map const& args_in, tr_rpc_idle_data* idle_data) +void torrentAdd(tr_session* session, tr_variant::Map const& args_in, DoneCb&& done_cb, tr_rpc_idle_data* idle_data) { + using namespace JsonRpc; + TR_ASSERT(idle_data != nullptr); auto const filename = args_in.value_if(TR_KEY_filename).value_or(""sv); auto const metainfo_base64 = args_in.value_if(TR_KEY_metainfo).value_or(""sv); if (std::empty(filename) && std::empty(metainfo_base64)) { - return "no filename or metainfo specified"; + done_cb(idle_data, Error::INVALID_PARAMS, "no filename or metainfo specified"sv); + return; } auto const download_dir = args_in.value_if(TR_KEY_download_dir); if (download_dir && tr_sys_path_is_relative(*download_dir)) { - return "download directory path is not absolute"; + done_cb(idle_data, Error::PATH_NOT_ABSOLUTE, "download directory path is not absolute"sv); + return; } auto ctor = tr_ctor{ session }; - // set the optional arguments + // set the optional parameters auto const cookies = args_in.value_if(TR_KEY_cookies).value_or(""sv); @@ -1479,11 +1720,12 @@ char const* torrentAdd(tr_session* session, tr_variant::Map const& args_in, tr_r if (auto const* val = args_in.find_if(TR_KEY_labels)) { - auto [labels, errmsg] = make_labels(*val); + auto [labels, err, errmsg] = make_labels(*val); - if (errmsg != nullptr) + if (err != Error::SUCCESS) { - return errmsg; + done_cb(idle_data, err, errmsg); + return; } ctor.set_labels(std::move(labels)); @@ -1504,36 +1746,38 @@ char const* torrentAdd(tr_session* session, tr_variant::Map const& args_in, tr_r if (isCurlURL(filename)) { auto* const d = new add_torrent_idle_data{ idle_data, std::move(ctor) }; - auto options = tr_web::FetchOptions{ filename, onMetadataFetched, d }; + auto options = tr_web::FetchOptions{ + filename, + [cb = std::move(done_cb)](tr_web::FetchResponse const& r) { onMetadataFetched(r, cb); }, + d, + }; options.cookies = cookies; session->fetch(std::move(options)); + return; + } + + auto ok = false; + + if (std::empty(filename)) + { + ok = ctor.set_metainfo(tr_base64_decode(metainfo_base64)); + } + else if (tr_sys_path_exists(tr_pathbuf{ filename })) + { + ok = ctor.set_metainfo_from_file(filename); } else { - auto ok = false; - - if (std::empty(filename)) - { - ok = ctor.set_metainfo(tr_base64_decode(metainfo_base64)); - } - else if (tr_sys_path_exists(tr_pathbuf{ filename })) - { - ok = ctor.set_metainfo_from_file(filename); - } - else - { - ok = ctor.set_metainfo_from_magnet_link(filename); - } - - if (!ok) - { - return "unrecognized info"; - } - - add_torrent_impl(idle_data, ctor); + ok = ctor.set_metainfo_from_magnet_link(filename); } - return nullptr; + if (!ok) + { + done_cb(idle_data, Error::UNRECOGNIZED_INFO, {}); + return; + } + + add_torrent_impl(idle_data, done_cb, ctor); } // --- @@ -1555,7 +1799,10 @@ void add_strings_from_var(std::set& strings, tr_variant const& } } -[[nodiscard]] char const* groupGet(tr_session* session, tr_variant::Map const& args_in, tr_variant::Map& args_out) +[[nodiscard]] std::pair groupGet( + tr_session* session, + tr_variant::Map const& args_in, + tr_variant::Map& args_out) { auto names = std::set{}; if (auto const iter = args_in.find(TR_KEY_name); iter != std::end(args_in)) @@ -1581,15 +1828,20 @@ void add_strings_from_var(std::set& strings, tr_variant const& } args_out.try_emplace(TR_KEY_group, std::move(groups_vec)); - return nullptr; + return { JsonRpc::Error::SUCCESS, {} }; } -char const* groupSet(tr_session* session, tr_variant::Map const& args_in, tr_variant::Map& /*args_out*/) +[[nodiscard]] std::pair groupSet( + tr_session* session, + tr_variant::Map const& args_in, + tr_variant::Map& /*args_out*/) { + using namespace JsonRpc; + auto const name = tr_strv_strip(args_in.value_if(TR_KEY_name).value_or(""sv)); if (std::empty(name)) { - return "No group name given"; + return { Error::INVALID_PARAMS, "No group name given"s }; } auto& group = session->getBandwidthGroup(name); @@ -1623,30 +1875,35 @@ char const* groupSet(tr_session* session, tr_variant::Map const& args_in, tr_var group.honor_parent_limits(TR_DOWN, *val); } - return nullptr; + return { Error::SUCCESS, {} }; } // --- -char const* sessionSet(tr_session* session, tr_variant::Map const& args_in, tr_variant::Map& /*args_out*/) +[[nodiscard]] std::pair sessionSet( + tr_session* session, + tr_variant::Map const& args_in, + tr_variant::Map& /*args_out*/) { + using namespace JsonRpc; + auto const download_dir = args_in.value_if(TR_KEY_download_dir); if (download_dir && tr_sys_path_is_relative(*download_dir)) { - return "download directory path is not absolute"; + return { Error::PATH_NOT_ABSOLUTE, "download directory path is not absolute"s }; } auto const incomplete_dir = args_in.value_if(TR_KEY_incomplete_dir); if (incomplete_dir && tr_sys_path_is_relative(*incomplete_dir)) { - return "incomplete torrents directory path is not absolute"; + return { Error::PATH_NOT_ABSOLUTE, "incomplete torrents directory path is not absolute"s }; } if (auto const iter = args_in.find(TR_KEY_preferred_transports); iter != std::end(args_in)) { if (!session->load_preferred_transports(iter->second)) { - return R"(the list must be unique with the values "utp" or "tcp")"; + return { Error::INVALID_PARAMS, R"(the list must be unique with the values "utp" or "tcp")" }; } } @@ -1901,10 +2158,13 @@ char const* sessionSet(tr_session* session, tr_variant::Map const& args_in, tr_v session->rpcNotify(TR_RPC_SESSION_CHANGED, nullptr); - return nullptr; + return { Error::SUCCESS, {} }; } -char const* sessionStats(tr_session* session, tr_variant::Map const& /*args_in*/, tr_variant::Map& args_out) +[[nodiscard]] std::pair sessionStats( + tr_session* session, + tr_variant::Map const& /*args_in*/, + tr_variant::Map& args_out) { auto const make_stats_map = [](auto const& stats) { @@ -1933,7 +2193,7 @@ char const* sessionStats(tr_session* session, tr_variant::Map const& /*args_in*/ args_out.try_emplace(TR_KEY_torrentCount, total); args_out.try_emplace(TR_KEY_uploadSpeed, session->piece_speed(TR_UP).base_quantity()); - return nullptr; + return { JsonRpc::Error::SUCCESS, {} }; } [[nodiscard]] constexpr std::string_view getEncryptionModeString(tr_encryption_mode mode) @@ -2084,7 +2344,10 @@ namespace session_get_helpers } } // namespace session_get_helpers -char const* sessionGet(tr_session* session, tr_variant::Map const& args_in, tr_variant::Map& args_out) +[[nodiscard]] std::pair sessionGet( + tr_session* session, + tr_variant::Map const& args_in, + tr_variant::Map& args_out) { using namespace session_get_helpers; @@ -2096,157 +2359,277 @@ char const* sessionGet(tr_session* session, tr_variant::Map const& args_in, tr_v } } - return nullptr; + return { JsonRpc::Error::SUCCESS, {} }; } -char const* freeSpace(tr_session* /*session*/, tr_variant::Map const& args_in, tr_variant::Map& args_out) +[[nodiscard]] std::pair freeSpace( + tr_session* /*session*/, + tr_variant::Map const& args_in, + tr_variant::Map& args_out) { + using namespace JsonRpc; + auto const path = args_in.value_if(TR_KEY_path); if (!path) { - return "directory path argument is missing"; + return { Error::INVALID_PARAMS, "directory path argument is missing"s }; } if (tr_sys_path_is_relative(*path)) { - return "directory path is not absolute"; + return { Error::PATH_NOT_ABSOLUTE, "directory path is not absolute"s }; } // get the free space auto const old_errno = errno; auto error = tr_error{}; auto const capacity = tr_sys_path_get_capacity(*path, &error); - char const* const err = error ? tr_strerror(error.code()) : nullptr; errno = old_errno; // response args_out.try_emplace(TR_KEY_path, *path); args_out.try_emplace(TR_KEY_size_bytes, capacity ? capacity->free : -1); args_out.try_emplace(TR_KEY_total_size, capacity ? capacity->total : -1); - return err; + + if (error) + { + return { Error::SYSTEM_ERROR, tr_strerror(error.code()) }; + } + return { Error::SUCCESS, {} }; } // --- -char const* sessionClose(tr_session* session, tr_variant::Map const& /*args_in*/, tr_variant::Map& /*args_out*/) +[[nodiscard]] std::pair sessionClose( + tr_session* session, + tr_variant::Map const& /*args_in*/, + tr_variant::Map& /*args_out*/) { session->rpcNotify(TR_RPC_SESSION_CLOSE, nullptr); - return nullptr; + return { JsonRpc::Error::SUCCESS, {} }; } // --- -using SyncHandler = char const* (*)(tr_session*, tr_variant::Map const&, tr_variant::Map&); +using SyncHandler = std::pair (*)(tr_session*, tr_variant::Map const&, tr_variant::Map&); -auto constexpr SyncHandlers = std::array, 20U>{ { - { "free-space"sv, freeSpace }, - { "group-get"sv, groupGet }, - { "group-set"sv, groupSet }, - { "queue-move-bottom"sv, queueMoveBottom }, - { "queue-move-down"sv, queueMoveDown }, - { "queue-move-top"sv, queueMoveTop }, - { "queue-move-up"sv, queueMoveUp }, - { "session-close"sv, sessionClose }, - { "session-get"sv, sessionGet }, - { "session-set"sv, sessionSet }, - { "session-stats"sv, sessionStats }, - { "torrent-get"sv, torrentGet }, - { "torrent-reannounce"sv, torrentReannounce }, - { "torrent-remove"sv, torrentRemove }, - { "torrent-set"sv, torrentSet }, - { "torrent-set-location"sv, torrentSetLocation }, - { "torrent-start"sv, torrentStart }, - { "torrent-start-now"sv, torrentStartNow }, - { "torrent-stop"sv, torrentStop }, - { "torrent-verify"sv, torrentVerify }, +auto constexpr SyncHandlers = std::array, 20U>{ { + { "free-space"sv, freeSpace, false }, + { "group-get"sv, groupGet, false }, + { "group-set"sv, groupSet, true }, + { "queue-move-bottom"sv, queueMoveBottom, true }, + { "queue-move-down"sv, queueMoveDown, true }, + { "queue-move-top"sv, queueMoveTop, true }, + { "queue-move-up"sv, queueMoveUp, true }, + { "session-close"sv, sessionClose, true }, + { "session-get"sv, sessionGet, false }, + { "session-set"sv, sessionSet, true }, + { "session-stats"sv, sessionStats, false }, + { "torrent-get"sv, torrentGet, false }, + { "torrent-reannounce"sv, torrentReannounce, true }, + { "torrent-remove"sv, torrentRemove, true }, + { "torrent-set"sv, torrentSet, true }, + { "torrent-set-location"sv, torrentSetLocation, true }, + { "torrent-start"sv, torrentStart, true }, + { "torrent-start-now"sv, torrentStartNow, true }, + { "torrent-stop"sv, torrentStop, true }, + { "torrent-verify"sv, torrentVerify, true }, } }; -using AsyncHandler = char const* (*)(tr_session*, tr_variant::Map const&, tr_rpc_idle_data*); +using AsyncHandler = void (*)(tr_session*, tr_variant::Map const&, DoneCb&&, tr_rpc_idle_data*); -auto constexpr AsyncHandlers = std::array, 4U>{ { - { "blocklist-update"sv, blocklistUpdate }, - { "port-test"sv, portTest }, - { "torrent-add"sv, torrentAdd }, - { "torrent-rename-path"sv, torrentRenamePath }, +auto constexpr AsyncHandlers = std::array, 4U>{ { + { "blocklist-update"sv, blocklistUpdate, true }, + { "port-test"sv, portTest, false }, + { "torrent-add"sv, torrentAdd, true }, + { "torrent-rename-path"sv, torrentRenamePath, true }, } }; void noop_response_callback(tr_session* /*session*/, tr_variant&& /*response*/) { } +void tr_rpc_request_exec_impl(tr_session* session, tr_variant const& request, tr_rpc_response_func&& callback, bool is_batch) +{ + using namespace JsonRpc; + + if (!callback) + { + callback = noop_response_callback; + } + + auto const* const map = request.get_if(); + if (map == nullptr) + { + callback( + session, + build_response( + Error::INVALID_REQUEST, + nullptr, + Error::build_data(is_batch ? "request must be an Object"sv : "request must be an Array or Object"sv, {}))); + return; + } + + auto is_jsonrpc = false; + if (auto jsonrpc = map->value_if(TR_KEY_jsonrpc); jsonrpc == Version) + { + is_jsonrpc = true; + } + else if (jsonrpc || is_batch) + { + callback( + session, + build_response(Error::INVALID_REQUEST, nullptr, Error::build_data("JSON-RPC version is not 2.0"sv, {}))); + return; + } + + auto const empty_params = tr_variant::Map{}; + auto const* params = map->find_if(is_jsonrpc ? TR_KEY_params : TR_KEY_arguments); + if (params == nullptr) + { + params = &empty_params; + } + + auto const method_name = map->value_if(TR_KEY_method).value_or(""sv); + + auto data = tr_rpc_idle_data{}; + data.session = session; + if (is_jsonrpc) + { + if (auto it_id = map->find(TR_KEY_id); it_id != std::end(*map)) + { + auto const& id = it_id->second; + switch (id.index()) + { + case tr_variant::StringIndex: + case tr_variant::IntIndex: + case tr_variant::DoubleIndex: + case tr_variant::NullIndex: + data.id.merge(id); // copy + break; + default: + callback( + session, + build_response( + Error::INVALID_REQUEST, + nullptr, + Error::build_data("id type must be String, Number, or Null"sv, {}))); + return; + } + } + } + else if (auto tag = map->value_if(TR_KEY_tag); tag) + { + data.id = *tag; + } + data.callback = std::move(callback); + + auto const is_notification = is_jsonrpc && !data.id.has_value(); + + auto done_cb = is_jsonrpc ? tr_rpc_idle_done : tr_rpc_idle_done_legacy; + + auto const test = [method_name](auto const& handler) + { + auto const& name = std::get<0>(handler); + return name == method_name; + }; + + if (auto const end = std::end(AsyncHandlers), handler = std::find_if(std::begin(AsyncHandlers), end, test); handler != end) + { + auto const& [name, func, has_side_effects] = *handler; + if (is_notification && !has_side_effects) + { + done_cb(&data, Error::SUCCESS, {}); + return; + } + + func( + session, + *params, + [cb = std::move(done_cb)](tr_rpc_idle_data* d, Error::Code code, std::string_view errmsg) + { + cb(d, code, errmsg); + delete d; + }, + new tr_rpc_idle_data{ std::move(data) }); + return; + } + + if (auto const end = std::end(SyncHandlers), handler = std::find_if(std::begin(SyncHandlers), end, test); handler != end) + { + auto const& [name, func, has_side_effects] = *handler; + if (is_notification && !has_side_effects) + { + done_cb(&data, Error::SUCCESS, {}); + return; + } + + auto const [err, errmsg] = func(session, *params, data.args_out); + done_cb(&data, err, errmsg); + return; + } + + // couldn't find a handler + done_cb(&data, Error::METHOD_NOT_FOUND, is_jsonrpc ? ""sv : "no method name"sv); +} + +void tr_rpc_request_exec_batch(tr_session* session, tr_variant::Vector const& requests, tr_rpc_response_func&& callback) +{ + auto const n_requests = std::size(requests); + auto responses = std::make_shared(n_requests); + auto n_responses = std::make_shared(); + auto cb = std::make_shared(std::move(callback)); + + for (size_t i = 0U; i < n_requests; ++i) + { + tr_rpc_request_exec_impl( + session, + requests[i], + [responses, n_requests, n_responses, i, cb](tr_session* s, tr_variant&& response) + { + (*responses)[i] = std::move(response); + + if (++(*n_responses) >= n_requests) + { + // Remove notifications from reply + auto const it_end = std::remove_if( + std::begin(*responses), + std::end(*responses), + [](auto const& r) { return !r.has_value(); }); + responses->erase(it_end, std::end(*responses)); + + (*cb)(s, !std::empty(*responses) ? std::move(*responses) : tr_variant{}); + } + }, + true); + } +} + } // namespace void tr_rpc_request_exec(tr_session* session, tr_variant const& request, tr_rpc_response_func&& callback) { auto const lock = session->unique_lock(); - auto const* const request_map = request.get_if(); - - if (!callback) + if (auto const* const vec = request.get_if(); vec != nullptr) { - callback = noop_response_callback; - } - - auto const empty_args = tr_variant::Map{}; - auto const* args_in = &empty_args; - auto method_name = std::string_view{}; - auto tag = std::optional{}; - if (request_map != nullptr) - { - // find the args - if (auto const* val = request_map->find_if(TR_KEY_arguments)) - { - args_in = val; - } - - // find the requested method - if (auto const val = request_map->value_if(TR_KEY_method)) - { - method_name = *val; - } - - tag = request_map->value_if(TR_KEY_tag); - } - - auto const test = [method_name](auto const& handler) - { - return handler.first == method_name; - }; - - if (auto const end = std::end(AsyncHandlers), handler = std::find_if(std::begin(AsyncHandlers), end, test); handler != end) - { - auto* const data = new tr_rpc_idle_data{}; - data->session = session; - data->tag = tag; - data->callback = std::move(callback); - if (char const* const errmsg = (*handler->second)(session, *args_in, data); errmsg != nullptr) - { - // Async operation failed prematurely? Invoke callback to ensure client gets a reply - tr_idle_function_done(data, errmsg); - } + tr_rpc_request_exec_batch(session, *vec, std::move(callback)); return; } - auto response = tr_variant::Map{ 3U }; - if (tag.has_value()) - { - response.try_emplace(TR_KEY_tag, *tag); - } - - if (auto const end = std::end(SyncHandlers), handler = std::find_if(std::begin(SyncHandlers), end, test); handler != end) - { - auto args_out = tr_variant::Map{}; - char const* const result = (handler->second)(session, *args_in, args_out); - - response.try_emplace(TR_KEY_arguments, std::move(args_out)); - response.try_emplace(TR_KEY_result, result != nullptr ? result : "success"); - } - else - { - // couldn't find a handler - response.try_emplace(TR_KEY_arguments, 0); - response.try_emplace(TR_KEY_result, "no method name"); - } - - callback(session, tr_variant{ std::move(response) }); + tr_rpc_request_exec_impl(session, request, std::move(callback), false); +} + +void tr_rpc_request_exec(tr_session* session, std::string_view request, tr_rpc_response_func&& callback) +{ + using namespace JsonRpc; + + auto serde = tr_variant_serde::json().inplace(); + if (auto otop = serde.parse(request); otop) + { + tr_rpc_request_exec(session, *otop, std::move(callback)); + return; + } + + callback(session, build_response(Error::PARSE_ERROR, nullptr, Error::build_data(serde.error_.message(), {}))); } diff --git a/libtransmission/rpcimpl.h b/libtransmission/rpcimpl.h index 700ab044f..9ee14c83e 100644 --- a/libtransmission/rpcimpl.h +++ b/libtransmission/rpcimpl.h @@ -10,6 +10,35 @@ struct tr_session; struct tr_variant; +namespace JsonRpc +{ +auto constexpr Version = std::string_view{ "2.0" }; + +namespace Error +{ +enum Code : int16_t +{ + PARSE_ERROR = -32700, + INVALID_REQUEST = -32600, + METHOD_NOT_FOUND = -32601, + INVALID_PARAMS = -32602, + INTERNAL_ERROR = -32603, + SUCCESS = 0, + SET_ANNOUNCE_LIST, + INVALID_TRACKER_LIST, + PATH_NOT_ABSOLUTE, + UNRECOGNIZED_INFO, + SYSTEM_ERROR, + FILE_IDX_OOR, + PIECE_IDX_OOR, + HTTP_ERROR, + CORRUPT_TORRENT +}; +} +} // namespace JsonRpc + using tr_rpc_response_func = std::function; void tr_rpc_request_exec(tr_session* session, tr_variant const& request, tr_rpc_response_func&& callback = {}); + +void tr_rpc_request_exec(tr_session* session, std::string_view request, tr_rpc_response_func&& callback = {}); diff --git a/libtransmission/torrent.cc b/libtransmission/torrent.cc index 772c0d137..64a05c09e 100644 --- a/libtransmission/torrent.cc +++ b/libtransmission/torrent.cc @@ -2438,7 +2438,7 @@ void renameTorrentFileString(tr_torrent* tor, std::string_view oldpath, std::str void tr_torrent::rename_path_in_session_thread( std::string_view const oldpath, std::string_view const newname, - tr_torrent_rename_done_func const callback, + tr_torrent_rename_done_func const& callback, void* const callback_user_data) { using namespace rename_helpers; @@ -2482,19 +2482,19 @@ void tr_torrent::rename_path_in_session_thread( { auto const szold = tr_pathbuf{ oldpath }; auto const sznew = tr_pathbuf{ newname }; - (*callback)(this, szold.c_str(), sznew.c_str(), error, callback_user_data); + callback(this, szold.c_str(), sznew.c_str(), error, callback_user_data); } } void tr_torrent::rename_path( std::string_view oldpath, std::string_view newname, - tr_torrent_rename_done_func callback, + tr_torrent_rename_done_func&& callback, void* callback_user_data) { this->session->run_in_session_thread( - [this, oldpath = std::string(oldpath), newname = std::string(newname), callback, callback_user_data]() - { rename_path_in_session_thread(oldpath, newname, callback, callback_user_data); }); + [this, oldpath = std::string(oldpath), newname = std::string(newname), cb = std::move(callback), callback_user_data]() + { rename_path_in_session_thread(oldpath, newname, std::move(cb), callback_user_data); }); } void tr_torrentRenamePath( @@ -2507,7 +2507,7 @@ void tr_torrentRenamePath( oldpath = oldpath != nullptr ? oldpath : ""; newname = newname != nullptr ? newname : ""; - tor->rename_path(oldpath, newname, callback, callback_user_data); + tor->rename_path(oldpath, newname, std::move(callback), callback_user_data); } // --- diff --git a/libtransmission/torrent.h b/libtransmission/torrent.h index a6ad3cff2..17740bc5e 100644 --- a/libtransmission/torrent.h +++ b/libtransmission/torrent.h @@ -183,7 +183,7 @@ struct tr_torrent void rename_path( std::string_view oldpath, std::string_view newname, - tr_torrent_rename_done_func callback, + tr_torrent_rename_done_func&& callback, void* callback_user_data); // these functions should become private when possible, @@ -1323,7 +1323,7 @@ private: void rename_path_in_session_thread( std::string_view oldpath, std::string_view newname, - tr_torrent_rename_done_func callback, + tr_torrent_rename_done_func const& callback, void* callback_user_data); void start_in_session_thread(); diff --git a/libtransmission/transmission.h b/libtransmission/transmission.h index 09da13c18..1d56d464e 100644 --- a/libtransmission/transmission.h +++ b/libtransmission/transmission.h @@ -15,6 +15,7 @@ #include // time_t #ifdef __cplusplus +#include #include #include #else @@ -853,12 +854,8 @@ void tr_torrentStart(tr_torrent* torrent); /** @brief Stop (pause) a torrent */ void tr_torrentStop(tr_torrent* torrent); -using tr_torrent_rename_done_func = void (*)( // - tr_torrent* torrent, - char const* oldpath, - char const* newname, - int error, - void* user_data); +using tr_torrent_rename_done_func = std::function< + void(tr_torrent* torrent, char const* oldpath, char const* newname, int error, void* user_data)>; /** * @brief Rename a file or directory in a torrent. diff --git a/tests/libtransmission/rpc-test.cc b/tests/libtransmission/rpc-test.cc index 03dafc623..0b0a315f3 100644 --- a/tests/libtransmission/rpc-test.cc +++ b/tests/libtransmission/rpc-test.cc @@ -30,7 +30,195 @@ namespace libtransmission::test using RpcTest = SessionTest; -TEST_F(RpcTest, tagSync) +TEST_F(RpcTest, EmptyRequest) +{ + static auto constexpr Request = ""sv; + + auto response = tr_variant{}; + tr_rpc_request_exec( + session_, + Request, + [&response](tr_session* /*session*/, tr_variant&& resp) { response = std::move(resp); }); + + auto const* const response_map = response.get_if(); + ASSERT_NE(response_map, nullptr); + auto const* const result = response_map->find_if(TR_KEY_result); + EXPECT_EQ(result, nullptr); + auto const* const error = response_map->find_if(TR_KEY_error); + ASSERT_NE(error, nullptr); + auto const error_code = error->value_if(TR_KEY_code); + ASSERT_TRUE(error_code); + EXPECT_EQ(*error_code, -32700); // don't use constants here in case they are wrong + auto const error_message = error->value_if(TR_KEY_message); + ASSERT_TRUE(error_message); + EXPECT_EQ(*error_message, "Parse error"sv); + auto const id = response_map->value_if(TR_KEY_id); + EXPECT_TRUE(id); +} + +TEST_F(RpcTest, NotArrayOrObject) +{ + auto requests = std::vector{}; + requests.emplace_back(12345); + requests.emplace_back(0.5); + requests.emplace_back("12345"sv); + requests.emplace_back(nullptr); + requests.emplace_back(true); + + for (auto const& req : requests) + { + auto response = tr_variant{}; + tr_rpc_request_exec( + session_, + req, + [&response](tr_session* /*session*/, tr_variant&& resp) { response = std::move(resp); }); + + auto const* const response_map = response.get_if(); + ASSERT_NE(response_map, nullptr); + auto const result = response_map->find(TR_KEY_result); + EXPECT_EQ(result, std::end(*response_map)); + auto const* const error = response_map->find_if(TR_KEY_error); + ASSERT_NE(error, nullptr); + auto const error_code = error->value_if(TR_KEY_code); + ASSERT_TRUE(error_code); + EXPECT_EQ(*error_code, -32600); // don't use constants here in case they are wrong + auto const error_message = error->value_if(TR_KEY_message); + ASSERT_TRUE(error_message); + EXPECT_EQ(*error_message, "Invalid Request"sv); + auto const error_data = error->find_if(TR_KEY_data); + ASSERT_NE(error_data, nullptr); + auto const error_string = error_data->value_if(TR_KEY_errorString); + ASSERT_TRUE(error_string); + EXPECT_EQ(*error_string, "request must be an Array or Object"sv); + auto const id = response_map->value_if(TR_KEY_id); + EXPECT_TRUE(id); + } +} + +TEST_F(RpcTest, JsonRpcWrongVersion) +{ + auto request_map = tr_variant::Map{ 3U }; + request_map.try_emplace(TR_KEY_jsonrpc, "1.0"); + request_map.try_emplace(TR_KEY_method, "session_stats"); + request_map.try_emplace(TR_KEY_id, 12345); + + auto response = tr_variant{}; + tr_rpc_request_exec( + session_, + std::move(request_map), + [&response](tr_session* /*session*/, tr_variant&& resp) { response = std::move(resp); }); + + auto const* const response_map = response.get_if(); + ASSERT_NE(response_map, nullptr); + auto const result = response_map->find(TR_KEY_result); + EXPECT_EQ(result, std::end(*response_map)); + auto const* const error = response_map->find_if(TR_KEY_error); + ASSERT_NE(error, nullptr); + auto const error_code = error->value_if(TR_KEY_code); + ASSERT_TRUE(error_code); + EXPECT_EQ(*error_code, -32600); // don't use constants here in case they are wrong + auto const error_message = error->value_if(TR_KEY_message); + ASSERT_TRUE(error_message); + EXPECT_EQ(*error_message, "Invalid Request"sv); + auto const error_data = error->find_if(TR_KEY_data); + ASSERT_NE(error_data, nullptr); + auto const error_string = error_data->value_if(TR_KEY_errorString); + ASSERT_TRUE(error_string); + EXPECT_EQ(*error_string, "JSON-RPC version is not 2.0"sv); + auto const id = response_map->value_if(TR_KEY_id); + EXPECT_TRUE(id); +} + +TEST_F(RpcTest, idSync) +{ + auto ids = std::vector{}; + ids.emplace_back(12345); + ids.emplace_back(0.5); + ids.emplace_back("12345"sv); + ids.emplace_back(nullptr); + + for (auto const& request_id : ids) + { + auto request_map = tr_variant::Map{ 3U }; + request_map.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version); + request_map.try_emplace(TR_KEY_method, "session-stats"); + request_map[TR_KEY_id].merge(request_id); // copy + + auto response = tr_variant{}; + tr_rpc_request_exec( + session_, + std::move(request_map), + [&response](tr_session* /*session*/, tr_variant&& resp) { response = std::move(resp); }); + + auto const* const response_map = response.get_if(); + ASSERT_NE(response_map, nullptr); + auto const* const result = response_map->find_if(TR_KEY_result); + EXPECT_NE(result, nullptr); + auto const error = response_map->find(TR_KEY_error); + EXPECT_EQ(error, std::end(*response_map)); + switch (request_id.index()) + { + case tr_variant::IntIndex: + EXPECT_EQ(request_id.value_if(), response_map->value_if(TR_KEY_id)); + break; + case tr_variant::DoubleIndex: + EXPECT_EQ(request_id.value_if(), response_map->value_if(TR_KEY_id)); + break; + case tr_variant::StringIndex: + EXPECT_EQ(request_id.value_if(), response_map->value_if(TR_KEY_id)); + break; + case tr_variant::NullIndex: + EXPECT_EQ(request_id.value_if(), response_map->value_if(TR_KEY_id)); + break; + default: + break; + } + } +} + +TEST_F(RpcTest, idWrongType) +{ + auto ids = std::vector{}; + ids.emplace_back(tr_variant::Map{}); + ids.emplace_back(tr_variant::Vector{}); + ids.emplace_back(true); + + for (auto const& request_id : ids) + { + auto request_map = tr_variant::Map{ 3U }; + request_map.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version); + request_map.try_emplace(TR_KEY_method, "session_stats"); + request_map[TR_KEY_id].merge(request_id); // copy + + auto response = tr_variant{}; + tr_rpc_request_exec( + session_, + std::move(request_map), + [&response](tr_session* /*session*/, tr_variant&& resp) { response = std::move(resp); }); + + auto const* const response_map = response.get_if(); + ASSERT_NE(response_map, nullptr); + auto const result = response_map->find(TR_KEY_result); + EXPECT_EQ(result, std::end(*response_map)); + auto const error = response_map->find_if(TR_KEY_error); + ASSERT_NE(error, nullptr); + auto const error_code = error->value_if(TR_KEY_code); + ASSERT_TRUE(error_code); + EXPECT_EQ(*error_code, -32600); // don't use constants here in case they are wrong + auto const error_message = error->value_if(TR_KEY_message); + ASSERT_TRUE(error_message); + EXPECT_EQ(*error_message, "Invalid Request"sv); + auto const error_data = error->find_if(TR_KEY_data); + ASSERT_NE(error_data, nullptr); + auto const error_string = error_data->value_if(TR_KEY_errorString); + ASSERT_TRUE(error_string); + EXPECT_EQ(*error_string, "id type must be String, Number, or Null"sv); + auto const id = response_map->value_if(TR_KEY_id); + EXPECT_TRUE(id); + } +} + +TEST_F(RpcTest, tagSyncLegacy) { auto request_map = tr_variant::Map{ 2U }; request_map.try_emplace(TR_KEY_method, "session-stats"); @@ -39,7 +227,7 @@ TEST_F(RpcTest, tagSync) auto response = tr_variant{}; tr_rpc_request_exec( session_, - tr_variant{ std::move(request_map) }, + std::move(request_map), [&response](tr_session* /*session*/, tr_variant&& resp) { response = std::move(resp); }); auto const* const response_map = response.get_if(); @@ -52,7 +240,67 @@ TEST_F(RpcTest, tagSync) EXPECT_EQ(*tag, 12345); } -TEST_F(RpcTest, tagAsync) +TEST_F(RpcTest, idAsync) +{ + auto ids = std::vector{}; + ids.emplace_back(12345); + ids.emplace_back(0.5); + ids.emplace_back("12345"sv); + ids.emplace_back(nullptr); + + for (auto const& request_id : ids) + { + auto* tor = zeroTorrentInit(ZeroTorrentState::Complete); + EXPECT_NE(nullptr, tor); + + auto request_map = tr_variant::Map{ 3U }; + request_map.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version); + request_map.try_emplace(TR_KEY_method, "torrent-rename-path"); + request_map[TR_KEY_id].merge(request_id); // copy + + auto params_map = tr_variant::Map{ 2U }; + params_map.try_emplace(TR_KEY_path, "files-filled-with-zeroes/512"); + params_map.try_emplace(TR_KEY_name, "512_test"); + request_map.try_emplace(TR_KEY_params, std::move(params_map)); + + auto promise = std::promise{}; + auto future = promise.get_future(); + tr_rpc_request_exec( + session_, + std::move(request_map), + [&promise](tr_session* /*session*/, tr_variant&& resp) { promise.set_value(std::move(resp)); }); + auto const response = future.get(); + + auto const* const response_map = response.get_if(); + ASSERT_NE(response_map, nullptr); + auto const result = response_map->find_if(TR_KEY_result); + EXPECT_NE(result, nullptr); + auto const error = response_map->find(TR_KEY_error); + EXPECT_EQ(error, std::end(*response_map)); + switch (request_id.index()) + { + case tr_variant::IntIndex: + EXPECT_EQ(request_id.value_if(), response_map->value_if(TR_KEY_id)); + break; + case tr_variant::DoubleIndex: + EXPECT_EQ(request_id.value_if(), response_map->value_if(TR_KEY_id)); + break; + case tr_variant::StringIndex: + EXPECT_EQ(request_id.value_if(), response_map->value_if(TR_KEY_id)); + break; + case tr_variant::NullIndex: + EXPECT_EQ(request_id.value_if(), response_map->value_if(TR_KEY_id)); + break; + default: + break; + } + + // cleanup + tr_torrentRemove(tor, false, nullptr, nullptr, nullptr, nullptr); + } +} + +TEST_F(RpcTest, tagAsyncLegacy) { auto* tor = zeroTorrentInit(ZeroTorrentState::Complete); EXPECT_NE(nullptr, tor); @@ -70,7 +318,7 @@ TEST_F(RpcTest, tagAsync) auto future = promise.get_future(); tr_rpc_request_exec( session_, - tr_variant{ std::move(request_map) }, + std::move(request_map), [&promise](tr_session* /*session*/, tr_variant&& resp) { promise.set_value(std::move(resp)); }); auto const response = future.get(); @@ -87,7 +335,83 @@ TEST_F(RpcTest, tagAsync) tr_torrentRemove(tor, false, nullptr, nullptr, nullptr, nullptr); } +TEST_F(RpcTest, NotificationSync) +{ + auto request_map = tr_variant::Map{ 2U }; + request_map.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version); + request_map.try_emplace(TR_KEY_method, "session_stats"); + + auto response = tr_variant{}; + tr_rpc_request_exec( + session_, + std::move(request_map), + [&response](tr_session* /*session*/, tr_variant&& resp) { response = std::move(resp); }); + + EXPECT_FALSE(response.has_value()); +} + +TEST_F(RpcTest, NotificationAsync) +{ + auto* tor = zeroTorrentInit(ZeroTorrentState::Complete); + EXPECT_NE(nullptr, tor); + + auto request_map = tr_variant::Map{ 2U }; + request_map.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version); + request_map.try_emplace(TR_KEY_method, "torrent_rename_path"); + + auto params_map = tr_variant::Map{ 2U }; + params_map.try_emplace(TR_KEY_path, "files-filled-with-zeroes/512"); + params_map.try_emplace(TR_KEY_name, "512_test"); + request_map.try_emplace(TR_KEY_params, std::move(params_map)); + + auto promise = std::promise{}; + auto future = promise.get_future(); + tr_rpc_request_exec( + session_, + std::move(request_map), + [&promise](tr_session* /*session*/, tr_variant&& resp) { promise.set_value(std::move(resp)); }); + auto const response = future.get(); + + EXPECT_FALSE(response.has_value()); + + // cleanup + tr_torrentRemove(tor, false, nullptr, nullptr, nullptr, nullptr); +} + TEST_F(RpcTest, tagNoHandler) +{ + auto request_map = tr_variant::Map{ 3U }; + request_map.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version); + request_map.try_emplace(TR_KEY_method, "sdgdhsgg"); + request_map.try_emplace(TR_KEY_id, 12345); + + auto response = tr_variant{}; + tr_rpc_request_exec( + session_, + std::move(request_map), + [&response](tr_session* /*session*/, tr_variant&& resp) { response = std::move(resp); }); + + auto const* const response_map = response.get_if(); + ASSERT_NE(response_map, nullptr); + auto const jsonrpc = response_map->value_if(TR_KEY_jsonrpc); + ASSERT_TRUE(jsonrpc); + EXPECT_EQ(*jsonrpc, JsonRpc::Version); + auto const result = response_map->find_if(TR_KEY_result); + EXPECT_EQ(result, nullptr); + auto const error = response_map->find_if(TR_KEY_error); + ASSERT_NE(error, nullptr); + auto const error_code = error->value_if(TR_KEY_code); + ASSERT_TRUE(error_code); + EXPECT_EQ(*error_code, JsonRpc::Error::METHOD_NOT_FOUND); + auto const error_message = error->value_if(TR_KEY_message); + ASSERT_TRUE(error_message); + EXPECT_EQ(*error_message, "Method not found"sv); + auto const id = response_map->value_if(TR_KEY_id); + ASSERT_TRUE(id); + EXPECT_EQ(*id, 12345); +} + +TEST_F(RpcTest, tagNoHandlerLegacy) { auto request_map = tr_variant::Map{ 2U }; request_map.try_emplace(TR_KEY_method, "sdgdhsgg"); @@ -96,7 +420,7 @@ TEST_F(RpcTest, tagNoHandler) auto response = tr_variant{}; tr_rpc_request_exec( session_, - tr_variant{ std::move(request_map) }, + std::move(request_map), [&response](tr_session* /*session*/, tr_variant&& resp) { response = std::move(resp); }); auto const* const response_map = response.get_if(); @@ -109,6 +433,154 @@ TEST_F(RpcTest, tagNoHandler) EXPECT_EQ(*tag, 12345); } +TEST_F(RpcTest, batch) +{ + auto request_vec = tr_variant::Vector{}; + request_vec.reserve(8U); + + auto request = tr_variant::Map{ 3U }; + request.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version); + request.try_emplace(TR_KEY_method, "session-stats"); + request.try_emplace(TR_KEY_id, 12345); + request_vec.emplace_back(std::move(request)); + + request = tr_variant::Map{ 2U }; + request.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version); + request.try_emplace(TR_KEY_method, "session-set"); + request_vec.emplace_back(std::move(request)); + + request = tr_variant::Map{ 3U }; + request.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version); + request.try_emplace(TR_KEY_method, "session-stats"); + request.try_emplace(TR_KEY_id, "12345"sv); + request_vec.emplace_back(std::move(request)); + + request = tr_variant::Map{ 1U }; + request.try_emplace(tr_quark_new("foo"sv), "boo"sv); + request_vec.emplace_back(std::move(request)); + + request_vec.emplace_back(1); + + request = tr_variant::Map{ 3U }; + request.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version); + request.try_emplace(TR_KEY_method, "dnfsojnsdkjf"); + request.try_emplace(TR_KEY_id, 12345); + request_vec.emplace_back(std::move(request)); + + request = tr_variant::Map{ 1U }; + request.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version); + request.try_emplace(TR_KEY_method, "dnfsojnsdkjf"); + request_vec.emplace_back(std::move(request)); + + request = tr_variant::Map{ 2U }; + request.try_emplace(TR_KEY_method, "session-stats"); + request.try_emplace(TR_KEY_tag, 12345); + request_vec.emplace_back(std::move(request)); + + auto response = tr_variant{}; + tr_rpc_request_exec( + session_, + std::move(request_vec), + [&response](tr_session* /*session*/, tr_variant&& resp) { response = std::move(resp); }); + + auto* const response_vec_ptr = response.get_if(); + ASSERT_NE(response_vec_ptr, nullptr); + auto const& response_vec = *response_vec_ptr; + + ASSERT_EQ(std::size(response_vec), 6U); + + auto const* response_map = response_vec[0].get_if(); + ASSERT_NE(response_map, nullptr); + auto const* result = response_map->find_if(TR_KEY_result); + EXPECT_NE(result, nullptr); + auto error_it = response_map->find(TR_KEY_error); + EXPECT_EQ(error_it, std::end(*response_map)); + auto id_int = response_map->value_if(TR_KEY_id); + ASSERT_TRUE(id_int); + EXPECT_EQ(*id_int, 12345); + + response_map = response_vec[1].get_if(); + ASSERT_NE(response_map, nullptr); + result = response_map->find_if(TR_KEY_result); + EXPECT_NE(result, nullptr); + error_it = response_map->find(TR_KEY_error); + EXPECT_EQ(error_it, std::end(*response_map)); + auto id_str = response_map->value_if(TR_KEY_id); + ASSERT_TRUE(id_str); + EXPECT_EQ(*id_str, "12345"sv); + + response_map = response_vec[2].get_if(); + ASSERT_NE(response_map, nullptr); + auto result_it = response_map->find(TR_KEY_result); + EXPECT_EQ(result_it, std::end(*response_map)); + auto error = response_map->find_if(TR_KEY_error); + ASSERT_NE(error, nullptr); + auto error_code = error->value_if(TR_KEY_code); + ASSERT_TRUE(error_code); + EXPECT_EQ(*error_code, -32600); // don't use constants here in case they are wrong + auto error_message = error->value_if(TR_KEY_message); + ASSERT_TRUE(error_message); + EXPECT_EQ(*error_message, "Invalid Request"sv); + auto id_null = response_map->value_if(TR_KEY_id); + EXPECT_TRUE(id_null); + + response_map = response_vec[3].get_if(); + ASSERT_NE(response_map, nullptr); + result_it = response_map->find(TR_KEY_result); + EXPECT_EQ(result_it, std::end(*response_map)); + error = response_map->find_if(TR_KEY_error); + ASSERT_NE(error, nullptr); + error_code = error->value_if(TR_KEY_code); + ASSERT_TRUE(error_code); + EXPECT_EQ(*error_code, -32600); // don't use constants here in case they are wrong + error_message = error->value_if(TR_KEY_message); + ASSERT_TRUE(error_message); + EXPECT_EQ(*error_message, "Invalid Request"sv); + auto error_data = error->find_if(TR_KEY_data); + ASSERT_NE(error_data, nullptr); + auto error_string = error_data->value_if(TR_KEY_errorString); + ASSERT_TRUE(error_string); + EXPECT_EQ(*error_string, "request must be an Object"sv); + id_null = response_map->value_if(TR_KEY_id); + EXPECT_TRUE(id_null); + + response_map = response_vec[4].get_if(); + ASSERT_NE(response_map, nullptr); + result_it = response_map->find(TR_KEY_result); + EXPECT_EQ(result_it, std::end(*response_map)); + error = response_map->find_if(TR_KEY_error); + ASSERT_NE(error, nullptr); + error_code = error->value_if(TR_KEY_code); + ASSERT_TRUE(error_code); + EXPECT_EQ(*error_code, -32601); // don't use constants here in case they are wrong + error_message = error->value_if(TR_KEY_message); + ASSERT_TRUE(error_message); + EXPECT_EQ(*error_message, "Method not found"sv); + id_int = response_map->value_if(TR_KEY_id); + ASSERT_TRUE(id_int); + EXPECT_EQ(*id_int, 12345); + + response_map = response_vec[5].get_if(); + ASSERT_NE(response_map, nullptr); + result_it = response_map->find(TR_KEY_result); + EXPECT_EQ(result_it, std::end(*response_map)); + error = response_map->find_if(TR_KEY_error); + ASSERT_NE(error, nullptr); + error_code = error->value_if(TR_KEY_code); + ASSERT_TRUE(error_code); + EXPECT_EQ(*error_code, -32600); // don't use constants here in case they are wrong + error_message = error->value_if(TR_KEY_message); + ASSERT_TRUE(error_message); + EXPECT_EQ(*error_message, "Invalid Request"sv); + error_data = error->find_if(TR_KEY_data); + ASSERT_NE(error_data, nullptr); + error_string = error_data->value_if(TR_KEY_errorString); + ASSERT_TRUE(error_string); + EXPECT_EQ(*error_string, "JSON-RPC version is not 2.0"sv); + id_null = response_map->value_if(TR_KEY_id); + EXPECT_TRUE(id_null); +} + /*** **** ***/ @@ -118,8 +590,10 @@ TEST_F(RpcTest, sessionGet) auto* tor = zeroTorrentInit(ZeroTorrentState::NoFiles); EXPECT_NE(nullptr, tor); - auto request_map = tr_variant::Map{ 1U }; + auto request_map = tr_variant::Map{ 3U }; + request_map.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version); request_map.try_emplace(TR_KEY_method, "session-get"sv); + request_map.try_emplace(TR_KEY_id, 12345); auto response = tr_variant{}; tr_rpc_request_exec( session_, @@ -128,7 +602,7 @@ TEST_F(RpcTest, sessionGet) auto* response_map = response.get_if(); ASSERT_NE(response_map, nullptr); - auto* args_map = response_map->find_if(TR_KEY_arguments); + auto* args_map = response_map->find_if(TR_KEY_result); ASSERT_NE(args_map, nullptr); // what we expected @@ -231,15 +705,17 @@ TEST_F(RpcTest, torrentGet) auto* tor = zeroTorrentInit(ZeroTorrentState::NoFiles); EXPECT_NE(nullptr, tor); - auto request = tr_variant::Map{ 1U }; + auto request = tr_variant::Map{ 3U }; - request.try_emplace(TR_KEY_method, "torrent-get"); + request.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version); + request.try_emplace(TR_KEY_method, "torrent-get"sv); + request.try_emplace(TR_KEY_id, 12345); - auto args_in = tr_variant::Map{ 1U }; + auto params = tr_variant::Map{ 1U }; auto fields = tr_variant::Vector{}; fields.emplace_back(tr_quark_get_string_view(TR_KEY_id)); - args_in.try_emplace(TR_KEY_fields, std::move(fields)); - request.try_emplace(TR_KEY_arguments, std::move(args_in)); + params.try_emplace(TR_KEY_fields, std::move(fields)); + request.try_emplace(TR_KEY_params, std::move(params)); auto response = tr_variant{}; tr_rpc_request_exec( @@ -249,10 +725,10 @@ TEST_F(RpcTest, torrentGet) auto* response_map = response.get_if(); ASSERT_NE(response_map, nullptr); - auto* args_out = response_map->find_if(TR_KEY_arguments); - ASSERT_NE(args_out, nullptr); + auto* result = response_map->find_if(TR_KEY_result); + ASSERT_NE(result, nullptr); - auto* torrents = args_out->find_if(TR_KEY_torrents); + auto* torrents = result->find_if(TR_KEY_torrents); ASSERT_NE(torrents, nullptr); EXPECT_EQ(1UL, std::size(*torrents)); diff --git a/web/src/open-dialog.js b/web/src/open-dialog.js index a65b8994e..7fc3e01d9 100644 --- a/web/src/open-dialog.js +++ b/web/src/open-dialog.js @@ -5,6 +5,7 @@ import { AlertDialog } from './alert-dialog.js'; import { Formatter } from './formatter.js'; +import { RPC } from './remote.js'; import { createDialogContainer, makeUUID } from './utils.js'; const is_ios = @@ -76,20 +77,24 @@ export class OpenDialog extends EventTarget { return; } const o = { - arguments: { + id: 'webui', + jsonrpc: RPC._JsonRpcVersion, + method: 'torrent-add', + params: { 'download-dir': destination, metainfo: contents.slice(Math.max(0, index + key.length)), paused, }, - method: 'torrent-add', }; remote.sendRequest(o, (response) => { - if (response.result !== 'success') { - alert(`Error adding "${file.name}": ${response.result}`); + if ('error' in response) { + const message = + response.error.data?.errorString ?? response.error.message; + alert(`Error adding "${file.name}": ${message}`); controller.setCurrentPopup( new AlertDialog({ heading: `Error adding "${file.name}"`, - message: response.result, + message, }), ); } @@ -104,19 +109,21 @@ export class OpenDialog extends EventTarget { url = `magnet:?xt=urn:btih:${url}`; } const o = { - arguments: { + id: 'webui', + jsonrpc: RPC._JsonRpcVersion, + method: 'torrent-add', + params: { 'download-dir': destination, filename: url, paused, }, - method: 'torrent-add', }; remote.sendRequest(o, (payload) => { - if (payload.result !== 'success') { + if ('error' in payload) { controller.setCurrentPopup( new AlertDialog({ heading: `Error adding "${url}"`, - message: payload.result, + message: payload.error.data?.errorString ?? payload.error.message, }), ); } diff --git a/web/src/prefs-dialog.js b/web/src/prefs-dialog.js index 7b758d013..90cdf7d89 100644 --- a/web/src/prefs-dialog.js +++ b/web/src/prefs-dialog.js @@ -61,10 +61,11 @@ export class PrefsDialog extends EventTarget { return; } + const args = response.result ?? response.error?.data ?? {}; const element = this.elements.network.port_status_label[ip_protocol]; - const is_open = response.arguments['port-is-open'] || false; + const is_open = args['port-is-open'] ?? false; element.dataset.open = is_open; - if ('port-is-open' in response.arguments) { + if ('port-is-open' in args) { setTextContent(element, is_open ? 'Open' : 'Closed'); } else { setTextContent(element, 'Error'); diff --git a/web/src/remote.js b/web/src/remote.js index ab26a2d12..f40984b10 100644 --- a/web/src/remote.js +++ b/web/src/remote.js @@ -8,6 +8,7 @@ export const RPC = { _DaemonVersion: 'version', _DownSpeedLimit: 'speed-limit-down', _DownSpeedLimited: 'speed-limit-down-enabled', + _JsonRpcVersion: '2.0', _QueueMoveBottom: 'queue-move-bottom', _QueueMoveDown: 'queue-move-down', _QueueMoveTop: 'queue-move-top', @@ -46,12 +47,17 @@ export class Remote { }) .then((response) => { response_argument = response; - if (response.status === 409) { - const error = new Error(Remote._SessionHeader); - error.header = response.headers.get(Remote._SessionHeader); - throw error; + switch (response.status) { + case 409: { + const error = new Error(Remote._SessionHeader); + error.header = response.headers.get(Remote._SessionHeader); + throw error; + } + case 204: + return null; + default: + return response.json(); } - return response.json(); }) .then((payload) => { if (callback) { @@ -85,6 +91,8 @@ export class Remote { // TODO: return a Promise loadDaemonPrefs(callback, context) { const o = { + id: 'webui', + jsonrpc: RPC._JsonRpcVersion, method: 'session-get', }; this.sendRequest(o, callback, context); @@ -92,36 +100,50 @@ export class Remote { checkPort(ip_protocol, callback, context) { const o = { - arguments: { + id: 'webui', + jsonrpc: RPC._JsonRpcVersion, + method: 'port-test', + params: { ip_protocol, }, - method: 'port-test', }; this.sendRequest(o, callback, context); } renameTorrent(torrentIds, oldpath, newname, callback, context) { const o = { - arguments: { + id: 'webui', + jsonrpc: RPC._JsonRpcVersion, + method: 'torrent-rename-path', + params: { ids: torrentIds, name: newname, path: oldpath, }, - method: 'torrent-rename-path', }; this.sendRequest(o, callback, context); } setLabels(torrentIds, labels, callback) { - const args = { + const params = { ids: torrentIds, labels, }; - this.sendRequest({ arguments: args, method: 'torrent-set' }, callback); + this.sendRequest( + { + id: 'webui', + jsonrpc: RPC._JsonRpcVersion, + method: 'torrent-set', + params, + }, + callback, + ); } loadDaemonStats(callback, context) { const o = { + id: 'webui', + jsonrpc: RPC._JsonRpcVersion, method: 'session-stats', }; this.sendRequest(o, callback, context); @@ -129,43 +151,46 @@ export class Remote { updateTorrents(torrentIds, fields, callback, context) { const o = { - arguments: { + id: 'webui', + jsonrpc: RPC._JsonRpcVersion, + method: 'torrent-get', + params: { fields, format: 'table', }, - method: 'torrent-get', }; if (torrentIds) { - o.arguments.ids = torrentIds; + o.params.ids = torrentIds; } this.sendRequest(o, (response) => { - const arguments_ = response['arguments']; - callback.call(context, arguments_.torrents, arguments_.removed); + const { torrents, removed } = response.result; + callback.call(context, torrents, removed); }); } getFreeSpace(dir, callback, context) { const o = { - arguments: { - path: dir, - }, + id: 'webui', + jsonrpc: RPC._JsonRpcVersion, method: 'free-space', + params: { path: dir }, }; this.sendRequest(o, (response) => { - const arguments_ = response['arguments']; - callback.call(context, arguments_.path, arguments_['size-bytes']); + const { path, 'size-bytes': size_bytes } = response.result; + callback.call(context, path, size_bytes); }); } changeFileCommand(torrentId, fileIndices, command) { - const arguments_ = { + const params = { ids: [torrentId], }; - arguments_[command] = fileIndices; + params[command] = fileIndices; this.sendRequest( { - arguments: arguments_, + jsonrpc: RPC._JsonRpcVersion, method: 'torrent-set', + params, }, () => { this._controller.refreshTorrents([torrentId]); @@ -173,14 +198,14 @@ export class Remote { ); } - sendTorrentSetRequests(method, torrent_ids, arguments_, callback, context) { - if (!arguments_) { - arguments_ = {}; - } - arguments_['ids'] = torrent_ids; + sendTorrentSetRequests(method, torrent_ids, params, callback, context) { + params ||= {}; + params.ids = torrent_ids; const o = { - arguments: arguments_, + id: 'webui', + jsonrpc: RPC._JsonRpcVersion, method, + params, }; this.sendRequest(o, callback, context); } @@ -217,16 +242,17 @@ export class Remote { removeTorrents(torrents, trash) { const o = { - arguments: { + jsonrpc: RPC._JsonRpcVersion, + method: 'torrent-remove', + params: { 'delete-local-data': trash, ids: [], }, - method: 'torrent-remove', }; if (torrents) { for (let index = 0, length_ = torrents.length; index < length_; ++index) { - o.arguments.ids.push(torrents[index].getId()); + o.params.ids.push(torrents[index].getId()); } } this.sendRequest(o, () => { @@ -254,20 +280,22 @@ export class Remote { url = `magnet:?xt=urn:btih:${url}`; } const o = { - arguments: { + jsonrpc: RPC._JsonRpcVersion, + method: 'torrent-add', + params: { filename: url, paused: options.paused, }, - method: 'torrent-add', }; this.sendRequest(o, () => { this._controller.refreshTorrents(); }); } - savePrefs(arguments_) { + savePrefs(params) { const o = { - arguments: arguments_, + jsonrpc: RPC._JsonRpcVersion, method: 'session-set', + params, }; this.sendRequest(o, () => { this._controller.loadDaemonPrefs(); @@ -275,6 +303,7 @@ export class Remote { } updateBlocklist() { const o = { + jsonrpc: RPC._JsonRpcVersion, method: 'blocklist-update', }; this.sendRequest(o, () => { diff --git a/web/src/rename-dialog.js b/web/src/rename-dialog.js index f985accf9..0adb26b8e 100644 --- a/web/src/rename-dialog.js +++ b/web/src/rename-dialog.js @@ -65,8 +65,8 @@ export class RenameDialog extends EventTarget { file_path, new_name, (response) => { - if (response.result === 'success') { - const args = response.arguments; + if ('result' in response) { + const args = response.result; if (handler) { handler.subtree.name = args.name; setTextContent(handler.name_container, args.name); @@ -85,11 +85,13 @@ export class RenameDialog extends EventTarget { } else { tor.refresh(args); } - } else if (response.result === 'Invalid argument') { + } else { + const error_obj = response.error; + const err_msg = + error_obj.data?.errorString ?? error_obj.message ?? ''; const connection_alert = new AlertDialog({ heading: `Error renaming "${file_path}"`, - message: - 'Could not rename a torrent or file name. The path to file may have changed/not reflected correctly or the argument is invalid.', + message: `${err_msg} (${error_obj.code}`, }); this.controller.setCurrentPopup(connection_alert); } diff --git a/web/src/statistics-dialog.js b/web/src/statistics-dialog.js index 0283d6546..1a592651d 100644 --- a/web/src/statistics-dialog.js +++ b/web/src/statistics-dialog.js @@ -18,7 +18,7 @@ export class StatisticsDialog extends EventTarget { this.remote = remote; const updateDaemon = () => - this.remote.loadDaemonStats((data) => this._update(data.arguments)); + this.remote.loadDaemonStats((data) => this._update(data.result)); const delay_msec = 5000; this.interval = setInterval(updateDaemon, delay_msec); updateDaemon(); diff --git a/web/src/transmission.js b/web/src/transmission.js index b4c9756e3..8efa34620 100644 --- a/web/src/transmission.js +++ b/web/src/transmission.js @@ -353,7 +353,7 @@ export class Transmission extends EventTarget { loadDaemonPrefs() { this.remote.loadDaemonPrefs((data) => { - this.session_properties = data.arguments; + this.session_properties = data.result; this._openTorrentFromUrl(); }); }