Add JSON-RPC capability

Inline nlohmann/json for the JSON parsing itself, and handle requests
with the same SCGI interface as XML-RPC.

Based off the work in https://github.com/jesec/rtorrent
This commit is contained in:
kannibalox
2025-01-04 20:50:55 -05:00
committed by Jari Sundell
parent 78915746ee
commit 9f48226663
30 changed files with 26375 additions and 307 deletions
+6
View File
@@ -109,6 +109,10 @@ libsub_root_a_SOURCES = \
rpc/ip_table_list.h \
rpc/lua.h \
rpc/lua.cc \
rpc/jsonrpc.cc \
rpc/jsonrpc.h \
rpc/rpc_manager.cc \
rpc/rpc_manager.h \
rpc/object_storage.cc \
rpc/object_storage.h \
rpc/parse.cc \
@@ -127,6 +131,7 @@ libsub_root_a_SOURCES = \
rpc/xmlrpc_tinyxml2.cc \
rpc/tinyxml2/tinyxml2.h \
rpc/tinyxml2/tinyxml2.cc \
rpc/nlohmann/json.h \
\
ui/download.cc \
ui/download.h \
@@ -157,6 +162,7 @@ libsub_root_a_SOURCES = \
ui/root.cc \
ui/root.h \
\
utils/base64.cc \
utils/base64.h \
utils/directory.cc \
utils/directory.h \
+2 -2
View File
@@ -187,7 +187,7 @@ system_method_insert_object(const torrent::Object::list_type& args, int flags) {
if (!(flags & rpc::object_storage::flag_static))
cmd_flags |= rpc::CommandMap::flag_modifiable;
if (!(flags & rpc::object_storage::flag_private))
cmd_flags |= rpc::CommandMap::flag_public_xmlrpc;
cmd_flags |= rpc::CommandMap::flag_public_rpc;
if ((flags & rpc::object_storage::mask_type) == rpc::object_storage::flag_list_type) {
torrent::Object valueList = torrent::Object::create_list();
@@ -360,7 +360,7 @@ system_method_redirect(const torrent::Object::list_type& args) {
std::string new_key = torrent::object_create_string(args.front());
std::string dest_key = torrent::object_create_string(args.back());
rpc::commands.create_redirect(new_key, dest_key, rpc::CommandMap::flag_public_xmlrpc | rpc::CommandMap::flag_modifiable);
rpc::commands.create_redirect(new_key, dest_key, rpc::CommandMap::flag_public_rpc | rpc::CommandMap::flag_modifiable);
return torrent::Object();
}
+6 -6
View File
@@ -49,7 +49,7 @@ void initialize_commands();
#define CMD2_A_FUNCTION(key, function, slot, parm, doc) \
rpc::commands.insert_slot<rpc::command_base_is_type<rpc::function>::type>(key, slot, &rpc::function, \
rpc::CommandMap::flag_dont_delete | rpc::CommandMap::flag_public_xmlrpc, NULL, NULL);
rpc::CommandMap::flag_dont_delete | rpc::CommandMap::flag_public_rpc, NULL, NULL);
#define CMD2_A_FUNCTION_PRIVATE(key, function, slot, parm, doc) \
rpc::commands.insert_slot<rpc::command_base_is_type<rpc::function>::type>(key, slot, &rpc::function, \
@@ -137,19 +137,19 @@ void initialize_commands();
std::placeholders::_1, std::placeholders::_2));
#define CMD2_REDIRECT(from_key, to_key) \
rpc::commands.create_redirect(from_key, to_key, rpc::CommandMap::flag_public_xmlrpc | rpc::CommandMap::flag_dont_delete);
rpc::commands.create_redirect(from_key, to_key, rpc::CommandMap::flag_public_rpc | rpc::CommandMap::flag_dont_delete);
#define CMD2_REDIRECT_GENERIC(from_key, to_key) \
rpc::commands.create_redirect(from_key, to_key, rpc::CommandMap::flag_public_xmlrpc | rpc::CommandMap::flag_no_target | rpc::CommandMap::flag_dont_delete);
rpc::commands.create_redirect(from_key, to_key, rpc::CommandMap::flag_public_rpc | rpc::CommandMap::flag_no_target | rpc::CommandMap::flag_dont_delete);
#define CMD2_REDIRECT_GENERIC_NO_EXPORT(from_key, to_key) \
rpc::commands.create_redirect(from_key, to_key, rpc::CommandMap::flag_no_target | rpc::CommandMap::flag_dont_delete);
#define CMD2_REDIRECT_FILE(from_key, to_key) \
rpc::commands.create_redirect(from_key, to_key, rpc::CommandMap::flag_public_xmlrpc | rpc::CommandMap::flag_file_target | rpc::CommandMap::flag_dont_delete);
rpc::commands.create_redirect(from_key, to_key, rpc::CommandMap::flag_public_rpc | rpc::CommandMap::flag_file_target | rpc::CommandMap::flag_dont_delete);
#define CMD2_REDIRECT_TRACKER(from_key, to_key) \
rpc::commands.create_redirect(from_key, to_key, rpc::CommandMap::flag_public_xmlrpc | rpc::CommandMap::flag_tracker_target | rpc::CommandMap::flag_dont_delete);
rpc::commands.create_redirect(from_key, to_key, rpc::CommandMap::flag_public_rpc | rpc::CommandMap::flag_tracker_target | rpc::CommandMap::flag_dont_delete);
#define CMD2_REDIRECT_GENERIC_STR(from_key, to_key) \
rpc::commands.create_redirect(from_key, to_key, \
rpc::CommandMap::flag_public_xmlrpc | rpc::CommandMap::flag_no_target);
rpc::CommandMap::flag_public_rpc | rpc::CommandMap::flag_no_target);
#define CMD2_REDIRECT_GENERIC_STR_NO_EXPORT(from_key, to_key) \
rpc::commands.create_redirect(from_key, to_key, \
+2 -1
View File
@@ -142,5 +142,6 @@ initialize_command_logging() {
CMD2_ANY_STRING ("log.execute", std::bind(&apply_log, std::placeholders::_2, 0));
CMD2_ANY_STRING ("log.vmmap.dump", std::bind(&log_vmmap_dump, std::placeholders::_2));
CMD2_ANY_STRING_V("log.xmlrpc", std::bind(&ThreadWorker::set_xmlrpc_log, worker_thread, std::placeholders::_2));
CMD2_ANY_STRING_V("log.rpc", std::bind(&ThreadWorker::set_rpc_log, worker_thread, std::placeholders::_2));
CMD2_REDIRECT ("log.xmlrpc", "log.rpc"); // For backwards compatibility
}
+20 -18
View File
@@ -95,7 +95,7 @@ apply_tos(const torrent::Object::string_type& arg) {
torrent::Object apply_encoding_list(const std::string& arg) { torrent::encoding_list()->push_back(arg); return torrent::Object(); }
torrent::File*
xmlrpc_find_file(core::Download* download, uint32_t index) {
rpc_find_file(core::Download* download, uint32_t index) {
if (index >= download->file_list()->size_files())
return NULL;
@@ -104,7 +104,7 @@ xmlrpc_find_file(core::Download* download, uint32_t index) {
// Ergh... time to update the Tracker API to allow proper ptrs.
torrent::Tracker*
xmlrpc_find_tracker(core::Download* download, uint32_t index) {
rpc_find_tracker(core::Download* download, uint32_t index) {
if (index >= download->tracker_list()->size())
return NULL;
@@ -112,7 +112,7 @@ xmlrpc_find_tracker(core::Download* download, uint32_t index) {
}
torrent::Peer*
xmlrpc_find_peer(core::Download* download, const torrent::HashString& hash) {
rpc_find_peer(core::Download* download, const torrent::HashString& hash) {
torrent::ConnectionList::iterator itr = download->connection_list()->find(hash.c_str());
if (itr == download->connection_list()->end())
@@ -122,23 +122,23 @@ xmlrpc_find_peer(core::Download* download, const torrent::HashString& hash) {
}
void
initialize_xmlrpc() {
rpc::xmlrpc.initialize();
rpc::xmlrpc.slot_find_download() = std::bind(&core::DownloadList::find_hex_ptr, control->core()->download_list(), std::placeholders::_1);
rpc::xmlrpc.slot_find_file() = std::bind(&xmlrpc_find_file, std::placeholders::_1, std::placeholders::_2);
rpc::xmlrpc.slot_find_tracker() = std::bind(&xmlrpc_find_tracker, std::placeholders::_1, std::placeholders::_2);
rpc::xmlrpc.slot_find_peer() = std::bind(&xmlrpc_find_peer, std::placeholders::_1, std::placeholders::_2);
initialize_rpc() {
rpc::rpc.initialize();
rpc::rpc.slot_find_download() = [](const char* hash) { return control->core()->download_list()->find_hex_ptr(hash); };
rpc::rpc.slot_find_file() = [](core::Download* d, uint32_t index) { return rpc_find_file(d, index); };
rpc::rpc.slot_find_tracker() = [](core::Download* d, uint32_t index) { return rpc_find_tracker(d, index); };
rpc::rpc.slot_find_peer() = [](core::Download* d, const torrent::HashString& hash) { return rpc_find_peer(d, hash); };
unsigned int count = 0;
for (rpc::CommandMap::const_iterator itr = rpc::commands.begin(), last = rpc::commands.end(); itr != last; itr++, count++) {
if (!(itr->second.m_flags & rpc::CommandMap::flag_public_xmlrpc))
if (!(itr->second.m_flags & rpc::CommandMap::flag_public_rpc))
continue;
rpc::xmlrpc.insert_command(itr->first.c_str(), itr->second.m_parm, itr->second.m_doc);
rpc::rpc.insert_command(itr->first.c_str(), itr->second.m_parm, itr->second.m_doc);
}
lt_log_print(torrent::LOG_RPC_EVENTS, "XMLRPC initialized with %u functions.", count);
lt_log_print(torrent::LOG_RPC_EVENTS, "RPC initialized with %u functions.", count);
}
torrent::Object
@@ -146,8 +146,7 @@ apply_scgi(const std::string& arg, int type) {
if (worker_thread->scgi() != NULL)
throw torrent::input_error("SCGI already enabled.");
if (!rpc::xmlrpc.is_valid())
initialize_xmlrpc();
initialize_rpc();
rpc::SCgi* scgi = new rpc::SCgi;
@@ -225,7 +224,7 @@ apply_xmlrpc_dialect(const std::string& arg) {
else
value = -1;
rpc::xmlrpc.set_dialect(value);
rpc::rpc.set_dialect(value);
return torrent::Object();
}
@@ -298,9 +297,12 @@ initialize_command_network() {
CMD2_ANY_STRING ("network.scgi.open_local", std::bind(&apply_scgi, std::placeholders::_2, 2));
CMD2_VAR_BOOL ("network.scgi.dont_route", false);
CMD2_ANY_STRING ("network.xmlrpc.dialect.set", std::bind(&apply_xmlrpc_dialect, std::placeholders::_2));
CMD2_ANY ("network.xmlrpc.size_limit", std::bind(&rpc::XmlRpc::size_limit, rpc::xmlrpc));
CMD2_ANY_VALUE_V ("network.xmlrpc.size_limit.set", std::bind(&rpc::XmlRpc::set_size_limit, rpc::xmlrpc, std::placeholders::_2));
CMD2_ANY_STRING ("network.xmlrpc.dialect.set", [](const auto&, const auto& arg) { return apply_xmlrpc_dialect(arg); })
CMD2_ANY ("network.xmlrpc.size_limit", [](const auto&, const auto&){ return rpc::rpc.size_limit(); });
CMD2_ANY_VALUE_V ("network.xmlrpc.size_limit.set", [](const auto&, const auto& arg){ return rpc::rpc.set_size_limit(arg); });
CMD2_VAR_BOOL ("network.rpc.use_xmlrpc", true);
CMD2_VAR_BOOL ("network.rpc.use_jsonrpc", true);
CMD2_ANY ("network.block.ipv4", std::bind(&torrent::ConnectionManager::is_block_ipv4, cm));
CMD2_ANY_VALUE_V ("network.block.ipv4.set", std::bind(&torrent::ConnectionManager::set_block_ipv4, cm, std::placeholders::_2));
+1 -1
View File
@@ -93,7 +93,7 @@ Control::initialize() {
void
Control::cleanup() {
// delete m_scgi; m_scgi = NULL;
rpc::xmlrpc.cleanup();
rpc::rpc.cleanup();
priority_queue_erase(&taskScheduler, &m_taskShutdown);
+25 -2
View File
@@ -23,6 +23,7 @@
#include "rpc/parse_commands.h"
#include "utils/directory.h"
#include "utils/base64.h"
#include "utils/file_status_cache.h"
#include "globals.h"
@@ -313,6 +314,21 @@ Manager::receive_http_failed(std::string msg) {
push_log_std("Http download error: \"" + msg + "\"");
}
bool
is_data_uri(const std::string& uri) {
return std::strncmp(uri.c_str(), "data:", 5) == 0;
}
std::string
decode_data_uri(const std::string& uri) {
const auto start = uri.find("base64,", 5) + 7;
if (start == std::string::npos)
throw torrent::input_error("Invalid data uri: not base64 encoded.");
if (start >= uri.size())
throw torrent::input_error("Empty base64.");
return utils::decode_base64(uri.substr(start));
}
void
Manager::try_create_download(const std::string& uri, int flags, const command_list_type& commands) {
// If the path was attempted loaded before, skip it.
@@ -320,6 +336,7 @@ Manager::try_create_download(const std::string& uri, int flags, const command_li
!(flags & create_raw_data) &&
!is_network_uri(uri) &&
!is_magnet_uri(uri) &&
!is_data_uri(uri) &&
!file_status_cache()->insert(uri, 0))
return;
@@ -333,10 +350,16 @@ Manager::try_create_download(const std::string& uri, int flags, const command_li
f->set_print_log(!(flags & create_quiet));
f->slot_finished([f]() { delete f; });
if (flags & create_raw_data)
if (is_data_uri(uri)) {
// Allow the use of data URIs, primarily for JSON-RPC which
// doesn't have a defined mechanism for binary data
f->load_raw_data(decode_data_uri(uri));
f->variables()["tied_to_file"] = (int64_t)false;
} else if (flags & create_raw_data) {
f->load_raw_data(uri);
else
} else {
f->load(uri);
}
f->commit();
}
+6 -7
View File
@@ -48,8 +48,7 @@
#include "command.h"
#include "command_map.h"
// For XMLRPC stuff, clean up.
#include "xmlrpc.h"
#include "rpc_manager.h"
#include "parse_commands.h"
namespace rpc {
@@ -64,8 +63,8 @@ CommandMap::insert(const key_type& key, int flags, const char* parm, const char*
throw torrent::internal_error("CommandMap::insert(...) tried to insert an already existing key.");
// TODO: This is not honoring the public_xmlrpc flags!!!
if (rpc::xmlrpc.is_valid() && (flags & flag_public_xmlrpc))
rpc::xmlrpc.insert_command(key.c_str(), parm, doc);
if (rpc::rpc.is_initialized() && (flags & flag_public_rpc))
rpc::rpc.insert_command(key.c_str(), parm, doc);
return base_type::insert(itr, value_type(key, command_map_data_type(flags, parm, doc)));
}
@@ -102,11 +101,11 @@ CommandMap::create_redirect(const key_type& key_new, const key_type& key_dest, i
dest_itr->second.m_flags |= flag_has_redirects;
flags |= dest_itr->second.m_flags & ~(flag_has_redirects | flag_public_xmlrpc);
flags |= dest_itr->second.m_flags & ~(flag_has_redirects | flag_public_rpc);
// TODO: This is not honoring the public_xmlrpc flags!!!
if (rpc::xmlrpc.is_valid() && (flags & flag_public_xmlrpc))
rpc::xmlrpc.insert_command(key_new.c_str(), dest_itr->second.m_parm, dest_itr->second.m_doc);
if (rpc::rpc.is_initialized() && (flags & flag_public_rpc))
rpc::rpc.insert_command(key_new.c_str(), dest_itr->second.m_parm, dest_itr->second.m_doc);
iterator itr = base_type::insert(base_type::end(),
value_type(key_new, command_map_data_type(flags,
+1 -1
View File
@@ -86,7 +86,7 @@ public:
using base_type::find;
static const int flag_dont_delete = 0x1;
static const int flag_public_xmlrpc = 0x4;
static const int flag_public_rpc = 0x4;
static const int flag_modifiable = 0x10;
static const int flag_is_redirect = 0x20;
static const int flag_has_redirects = 0x40;
+241
View File
@@ -0,0 +1,241 @@
#include "config.h"
#include "rpc/jsonrpc.h"
#include "rpc/rpc_manager.h"
#include "torrent/exceptions.h"
#include "torrent/object.h"
#include <cstdint>
#include <string>
#include <torrent/common.h>
#include <torrent/torrent.h>
#include "rpc/command.h"
#include "rpc/command_map.h"
#include "rpc/parse_commands.h"
#include "thread_base.h"
#include "nlohmann/json.h"
namespace rpc {
// Taken from https://www.jsonrpc.org/specification
constexpr int JSONRPC_PARSE_ERROR = -32700;
constexpr int JSONRPC_INVALID_REQUEST_ERROR = -32600;
constexpr int JSONRPC_METHOD_NOT_FOUND_ERROR = -32601;
constexpr int JSONRPC_INVALID_PARAMS_ERROR = -32602;
constexpr int JSONRPC_INTERNAL_ERROR = -32000;
using json = nlohmann::json;
torrent::Object
json_to_object(const json& value) {
switch (value.type()) {
case json::value_t::number_unsigned:
case json::value_t::number_integer:
return torrent::Object(value.get<int64_t>());
case json::value_t::boolean:
return value.get<bool>() ? torrent::Object(int64_t(1)) : torrent::Object(int64_t(0));
case json::value_t::string:
return torrent::Object(value.get<std::string>());
case json::value_t::array: {
auto array_raw = torrent::Object::create_list();
auto& array = array_raw.as_list();
for (const auto& entry : value) {
array.push_back(json_to_object(entry));
}
return array_raw;
}
case json::value_t::object: {
auto map_raw = torrent::Object::create_map();
auto& map = map_raw.as_map();
for (const auto& entry : value.items()) {
map[entry.key()] = json_to_object(entry.value());
}
return map_raw;
}
case json::value_t::number_float:
// type_name() for floats returns 'number', which is accurate for JSON but misleading
throw torrent::input_error("invalid parameters: unexpected data type float");
default:
throw torrent::input_error("invalid parameters: unexpected data type " + std::string(value.type_name()));
}
}
json
object_to_json(const torrent::Object& object) noexcept {
switch (object.type()) {
case torrent::Object::TYPE_VALUE:
return object.as_value();
case torrent::Object::TYPE_STRING:
return object.as_string();
case torrent::Object::TYPE_LIST: {
json result = json::array();
std::transform(object.as_list().cbegin(), object.as_list().cend(), std::back_inserter(result), [](const torrent::Object& element) {
return object_to_json(element);
});
return result;
}
case torrent::Object::TYPE_MAP: {
json result = json::object();
for (const auto& entry : object.as_map()) {
result.emplace(entry.first, object_to_json(entry.second));
}
return result;
}
case torrent::Object::TYPE_DICT_KEY: {
json result = json::array();
result.push_back(object_to_json(object.as_dict_key()));
const auto& dict_obj = object.as_dict_obj();
if (dict_obj.is_list()) {
std::transform(dict_obj.as_list().cbegin(), dict_obj.as_list().cend(), std::back_inserter(result), [](const torrent::Object& element) {
return object_to_json(element);
});
} else {
result.push_back(object_to_json(dict_obj));
}
return result;
}
default:
return 0;
}
}
json
jsonrpc_call_command(const std::string& method, const json& params) {
if (params.type() == json::value_t::object) {
// Named parameters is valid JSON-RPC, rtorrent just doesn't support it
throw rpc_error(JSONRPC_INVALID_PARAMS_ERROR, "invalid parameter: procedure named parameter not supported");
} else if (params.type() != json::value_t::array) {
throw rpc_error(JSONRPC_INVALID_REQUEST_ERROR, "invalid request: params field must be an array");
}
CommandMap::iterator itr = commands.find(method.c_str());
if (itr == commands.end()) {
throw rpc_error(JSONRPC_METHOD_NOT_FOUND_ERROR, "method not found: " + method);
}
torrent::Object params_object = json_to_object(params);
auto& params_object_list = params_object.as_list();
rpc::target_type target = rpc::make_target();
if (!(itr->second.m_flags & CommandMap::flag_no_target)) {
// Provide a blank target if none was provided
if (params_object_list.empty())
params_object_list.push_back("");
if (!params_object_list.begin()->is_string())
throw torrent::input_error("invalid parameters: target must be a string");
RpcManager::object_to_target(params_object_list.begin()->as_string(), itr->second.m_flags, &target);
params_object_list.erase(params_object_list.begin());
}
const auto& result = rpc::commands.call_command(itr, params_object, target);
return object_to_json(result);
}
json
json_error(int code, const std::string& msg, json id) {
return json{{"jsonrpc", "2.0"}, {"id", id}, {"error", {{"code", code}, {"message", msg}}}};
}
json
handle_request(const json& request) {
json response = {{"jsonrpc", "2.0"}, {"id", nullptr}};
try {
if (!request["id"].is_number() && !request["id"].is_string() && !request["id"].is_null())
return json_error(JSONRPC_INVALID_REQUEST_ERROR, "request id is invalid type " + std::string(request["id"].type_name()), response["id"]);
response["id"] = request["id"];
if (!request.contains("method") || !request["method"].is_string())
return json_error(JSONRPC_INVALID_REQUEST_ERROR, "method string not present", response["id"]);
if (request.contains("params"))
response["result"] = jsonrpc_call_command(request["method"], request["params"]);
else
response["result"] = jsonrpc_call_command(request["method"], json::array({""}));
return response;
} catch (rpc_error& e) {
return json_error(e.type(), e.what(), response["id"]);
} catch (torrent::input_error& e) {
return json_error(JSONRPC_INVALID_PARAMS_ERROR, e.what(), response["id"]);
} catch (torrent::local_error& e) {
return json_error(JSONRPC_INTERNAL_ERROR, e.what(), response["id"]);
}
}
// Notifications are basically the same as requests, except we can
// just drop the message on the floor if there are any errors
void
handle_notification(const json& request) noexcept {
if (!request.contains("method") || !request["method"].is_string())
return;
try {
if (request.contains("params"))
jsonrpc_call_command(request["method"], request["params"]);
else
jsonrpc_call_command(request["method"], json::array({""}));
} catch (std::exception& e) {
}
}
bool
JsonRpc::process(const char* in_buffer, uint32_t length, slot_write callback) {
json response;
json body;
try {
body = json::parse(in_buffer, in_buffer + length);
switch (body.type()) {
case json::value_t::object: {
if (!body.contains("id")) {
handle_notification(body);
return callback("", 0);
} else {
response = handle_request(body);
}
break;
}
case json::value_t::array: {
// Empty batch requests are invalid as per the spec
if (body.empty()) {
response = json_error(JSONRPC_INVALID_REQUEST_ERROR, "invalid request: empty batch", nullptr);
break;
}
response = json::array();
for (const auto& sub_body : body) {
if (!sub_body.contains("id"))
handle_notification(sub_body);
else
response.push_back(handle_request(sub_body));
}
// This indicates the batch was composed entirely of
// notifications, in which case nothing is returned
if (response.empty())
return callback("", 0);
break;
}
default:
response = json_error(JSONRPC_PARSE_ERROR, "message type " + std::string(body.type_name()) + " unsupported", nullptr);
}
std::string response_str = response.dump();
return callback(response_str.c_str(), response_str.size());
} catch (json::parse_error& e) {
auto err_str = json_error(JSONRPC_PARSE_ERROR, e.what(), nullptr).dump(-1, ' ', false, json::error_handler_t::replace);
return callback(err_str.c_str(), err_str.size());
} catch (json::type_error& e) {
// Type errors may be caused by invalid UTF-8 strings in exception strings, hence the ::replace
auto err_str = json_error(JSONRPC_PARSE_ERROR, e.what(), nullptr).dump(-1, ' ', false, json::error_handler_t::replace);
return callback(err_str.c_str(), err_str.size());
}
}
} // namespace rpc
+24
View File
@@ -0,0 +1,24 @@
#ifndef RTORRENT_RPC_JSONRPC_H
#define RTORRENT_RPC_JSONRPC_H
#include <functional>
#include <cstdint>
namespace rpc {
class JsonRpc {
public:
using slot_write = std::function<bool(const char*, uint32_t)>;
void initialize() {};
void cleanup() {};
bool process(const char* in_buffer, uint32_t length, slot_write callback);
void insert_command(const char* name, const char* parm, const char* doc) {};
};
} // namespace rpc
#endif
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -36,6 +36,7 @@
#include "config.h"
#include "rpc/rpc_manager.h"
#include <algorithm>
#include <fstream>
#include <string>
@@ -49,7 +50,7 @@
namespace rpc {
CommandMap commands;
XmlRpc xmlrpc;
RpcManager rpc;
ExecFile execFile;
inline bool command_map_is_space(char c) {
+2 -1
View File
@@ -43,6 +43,7 @@
#include "command_map.h"
#include "exec_file.h"
#include "xmlrpc.h"
#include "rpc_manager.h"
namespace core {
class Download;
@@ -52,7 +53,7 @@ namespace rpc {
// Move to another file?
extern CommandMap commands;
extern XmlRpc xmlrpc;
extern RpcManager rpc;
extern ExecFile execFile;
typedef std::pair<torrent::Object, const char*> parse_command_type;
+152
View File
@@ -0,0 +1,152 @@
#include "config.h"
#include <cstring>
#include <torrent/exceptions.h>
#include "parse_commands.h"
#include "rpc/rpc_manager.h"
namespace rpc {
void
RpcManager::object_to_target(const torrent::Object& obj, int call_flags, rpc::target_type* target) {
if (call_flags & CommandMap::flag_no_target)
return;
if (!obj.is_string()) {
throw torrent::input_error("invalid parameters: target must be a string");
}
std::string target_string = obj.as_string();
bool require_index = (call_flags & (CommandMap::flag_tracker_target | CommandMap::flag_file_target));
if (target_string.size() == 0 && !require_index) {
return;
}
// Length of SHA1 hash is 40
if (target_string.size() < 40) {
throw torrent::input_error("invalid parameters: invalid target");
}
char type = 'd';
std::string hash;
std::string index;
const auto& delim_pos = target_string.find_first_of(':', 40);
if (delim_pos == target_string.npos ||
delim_pos + 2 >= target_string.size()) {
if (require_index) {
throw torrent::input_error("invalid parameters: no index");
}
hash = target_string;
} else {
hash = target_string.substr(0, delim_pos);
type = target_string[delim_pos + 1];
index = target_string.substr(delim_pos + 2);
}
core::Download* download = rpc.slot_find_download()(hash.c_str());
if (download == nullptr)
throw torrent::input_error("invalid parameters: info-hash not found");
try {
switch (type) {
case 'd':
*target = rpc::make_target(download);
break;
case 'f':
*target = rpc::make_target(
command_base::target_file,
rpc.slot_find_file()(download, std::stoi(std::string(index))));
break;
case 't':
*target = rpc::make_target(
command_base::target_tracker,
rpc.slot_find_tracker()(download, std::stoi(std::string(index))));
break;
case 'p': {
if (index.size() < 40) {
throw torrent::input_error("invalid parameters: not a hash string.");
}
torrent::HashString hash;
torrent::hash_string_from_hex_c_str(index.c_str(), hash);
*target = rpc::make_target(
command_base::target_peer,
rpc.slot_find_peer()(download, hash));
break;
}
default:
throw torrent::input_error("invalid parameters: unexpected target type");
}
} catch (const std::logic_error&) {
throw torrent::input_error("invalid parameters: invalid index");
}
}
bool
RpcManager::process(RPCType type, const char* in_buffer, uint32_t length, slot_response_callback callback) {
switch (type) {
case RPCType::XML: {
if (m_xmlrpc.is_valid() && rpc::call_command_value("network.rpc.use_xmlrpc")) {
return m_xmlrpc.process(in_buffer, length, callback);
} else {
const std::string response = "<?xml version=\"1.0\"?><methodResponse><fault><struct><member><name>faultCode</name><value><i8>-501</i8></value></member><member><name>faultString</name><value><string>XML-RPC not supported</string></value></member></struct></fault></methodResponse>";
return callback(response.c_str(), response.size());
}
}
case RPCType::JSON: {
if (rpc::call_command_value("network.rpc.use_jsonrpc")) {
return m_jsonrpc.process(in_buffer, length, callback);
} else {
const std::string response = "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32601,\"message\":\"JSON-RPC not supported\"},\"id\":null}";
return callback(response.c_str(), response.size());
}
}
}
}
void
RpcManager::initialize() {
m_xmlrpc.initialize();
m_jsonrpc.initialize();
m_initialized = true;
}
void
RpcManager::cleanup() {
m_xmlrpc.cleanup();
m_jsonrpc.cleanup();
}
bool
RpcManager::is_type_enabled(RPCType type) const {
switch (type) {
case RPCType::XML:
return m_is_xmlrpc_enabled;
case RPCType::JSON:
return m_is_jsonrpc_enabled;
}
}
void
RpcManager::set_type_enabled(RPCType type, bool enabled) {
switch (type) {
case RPCType::XML:
m_is_xmlrpc_enabled = enabled;
case RPCType::JSON:
m_is_jsonrpc_enabled = enabled;
}
}
bool
RpcManager::is_initialized() const {
return m_initialized;
}
void
RpcManager::insert_command(const char* name, const char* parm, const char* doc) {
m_xmlrpc.insert_command(name, parm, doc);
m_jsonrpc.insert_command(name, parm, doc);
}
} // namespace rpc
+88
View File
@@ -0,0 +1,88 @@
#ifndef RTORRENT_RPC_MANAGER_H
#define RTORRENT_RPC_MANAGER_H
#include <cstdint>
#include <functional>
#include "rpc/command.h"
#include "rpc/jsonrpc.h"
#include "rpc/xmlrpc.h"
namespace core {
class Download;
}
namespace torrent {
class File;
class Object;
class Tracker;
} // namespace torrent
namespace rpc {
class rpc_error : public torrent::base_error {
public:
rpc_error(int type, std::string msg) :
m_type(type), m_msg(msg) {}
virtual ~rpc_error() throw() {}
virtual int type() const throw() { return m_type; }
virtual const char* what() const throw() { return m_msg.c_str(); }
private:
int m_type;
std::string m_msg;
};
class RpcManager {
public:
using slot_download = std::function<core::Download*(const char*)>;
using slot_file = std::function<torrent::File*(core::Download*, uint32_t)>;
using slot_tracker = std::function<torrent::Tracker*(core::Download*, uint32_t)>;
using slot_peer = std::function<torrent::Peer*(core::Download*, const torrent::HashString&)>;
using slot_response_callback = std::function<bool(const char*, uint32_t)>;
enum RPCType { XML,
JSON };
RpcManager() = default;
~RpcManager() = default;
void initialize();
void cleanup();
bool is_initialized() const;
int64_t size_limit() { return m_xmlrpc.size_limit(); };
void set_size_limit(uint64_t size) { m_xmlrpc.set_size_limit(size); };
int dialect() { return m_xmlrpc.dialect(); }
void set_dialect(int dialect) { m_xmlrpc.set_dialect(dialect); }
bool is_type_enabled(RPCType type) const;
void set_type_enabled(RPCType type, bool enabled);
bool process(RPCType type, const char* in_buffer, uint32_t length, slot_response_callback callback);
void insert_command(const char* name, const char* parm, const char* doc);
static void object_to_target(const torrent::Object& obj, int callFlags, rpc::target_type* target);
slot_download& slot_find_download() { return m_slot_find_download; }
slot_file& slot_find_file() { return m_slot_find_file; }
slot_tracker& slot_find_tracker() { return m_slot_find_tracker; }
slot_peer& slot_find_peer() { return m_slot_find_peer; }
private:
XmlRpc m_xmlrpc;
JsonRpc m_jsonrpc;
bool m_initialized = false;
bool m_is_jsonrpc_enabled = true;
bool m_is_xmlrpc_enabled = true;
slot_download m_slot_find_download;
slot_file m_slot_find_file;
slot_tracker m_slot_find_tracker;
slot_peer m_slot_find_peer;
};
} // namespace rpc
#endif
+10 -2
View File
@@ -36,6 +36,7 @@
#include "config.h"
#include "rpc/scgi_task.h"
#include <rak/error_number.h>
#include <rak/socket_address.h>
#include <sys/un.h>
@@ -166,11 +167,18 @@ SCgi::event_error() {
bool
SCgi::receive_call(SCgiTask* task, const char* buffer, uint32_t length) {
bool result = false;
torrent::thread_base::acquire_global_lock();
torrent::main_thread()->interrupt();
bool result = xmlrpc.process(buffer, length, [task](const char* b, uint32_t l) { return task->receive_write(b, l); });
switch (task->content_type()) {
case rpc::SCgiTask::ContentType::JSON:
result = rpc.process(RpcManager::RPCType::JSON, buffer, length, [task](const char* b, uint32_t l) { return task->receive_write(b, l); });
break;
case rpc::SCgiTask::ContentType::XML:
result = rpc.process(RpcManager::RPCType::XML, buffer, length, [task](const char* b, uint32_t l) { return task->receive_write(b, l); });
break;
}
torrent::thread_base::release_global_lock();
return result;
+76 -60
View File
@@ -1,44 +1,9 @@
// rTorrent - BitTorrent client
// Copyright (C) 2005-2011, Jari Sundell
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// In addition, as a special exception, the copyright holders give
// permission to link the code of portions of this program with the
// OpenSSL library under certain conditions as described in each
// individual source file, and distribute linked combinations
// including the two.
//
// You must obey the GNU General Public License in all respects for
// all of the code used other than OpenSSL. If you modify file(s)
// with this exception, you may extend this exception to your version
// of the file(s), but you are not obligated to do so. If you do not
// wish to do so, delete this exception statement from your version.
// If you delete this exception statement from all source files in the
// program, then also delete it here.
//
// Contact: Jari Sundell <jaris@ifi.uio.no>
//
// Skomakerveien 33
// 3185 Skoppum, NORWAY
#include "config.h"
#include <rak/allocators.h>
#include <rak/error_number.h>
#include <cstdio>
#include <vector>
#include <sys/types.h>
#include <sys/socket.h>
#include <torrent/exceptions.h>
@@ -126,47 +91,94 @@ SCgiTask::event_read() {
// receive all the data we need the first time.
char* current;
int contentSize;
int headerSize = strtol(m_buffer, &current, 0);
int header_size = strtol(m_buffer, &current, 0);
if (current == m_position)
return;
// If the request doesn't start with an integer or if it didn't
// end in ':', then close the connection.
if (current == m_buffer || *current != ':' || headerSize < 17 || headerSize > max_header_size)
if (current == m_buffer || *current != ':' || header_size < 17 || header_size > max_header_size)
goto event_read_failed;
if (std::distance(++current, m_position) < headerSize + 1)
if (std::distance(++current, m_position) < header_size + 1)
return;
// We'll parse this fully below, but the SCGI spec requires it to
// be the first header.
if (std::memcmp(current, "CONTENT_LENGTH", 15) != 0)
goto event_read_failed;
char* contentPos;
contentSize = strtol(current + 15, &contentPos, 0);
std::string content_type = "";
size_t content_length = 0;
const char* header_end = current + header_size;
if (*contentPos != '\0' || contentSize <= 0 || contentSize > max_content_size)
// Parse out the null-terminated header keys and values, with
// checks to ensure it doesn't scan beyond the limits of the
// header
while (current < header_end) {
char* key = current;
char* key_end = static_cast<char*>(std::memchr(current, '\0', header_end - current));
if (!key_end)
goto event_read_failed;
current = key_end + 1;
if (current >= header_end)
goto event_read_failed;
char* value = current;
char* value_end = static_cast<char*>(std::memchr(current, '\0', header_end - current));
if (!value_end)
goto event_read_failed;
current = value_end + 1;
if (strcmp(key, "CONTENT_LENGTH") == 0) {
char* content_pos;
content_length = strtol(value, &content_pos, 10);
if (*content_pos != '\0' || content_length <= 0 || content_length > max_content_size)
goto event_read_failed;
} else if (strcmp(key, "CONTENT_TYPE") == 0) {
content_type = value;
}
}
if (current != header_end)
goto event_read_failed;
m_body = current + headerSize + 1;
headerSize = std::distance(m_buffer, m_body);
if (content_length <= 0)
goto event_read_failed;
if ((unsigned int)(contentSize + headerSize) < m_bufferSize) {
m_bufferSize = contentSize + headerSize;
m_body = current + 1;
header_size = std::distance(m_buffer, m_body);
} else if ((unsigned int)contentSize <= default_buffer_size) {
m_bufferSize = contentSize;
if (content_type == "") {
// If no CONTENT_TYPE was supplied, peek at the body to check if it's JSON
// { is a single request object, while [ is a batch array
if (*m_body == '{' || *m_body == '[')
m_content_type = ContentType::JSON;
} else if (content_type == "application/json") {
m_content_type = ContentType::JSON;
} else if (content_type == "text/xml") {
m_content_type = ContentType::XML;
} else {
goto event_read_failed;
}
if ((unsigned int)(content_length + header_size) < m_bufferSize) {
m_bufferSize = content_length + header_size;
} else if ((unsigned int)content_length <= default_buffer_size) {
m_bufferSize = content_length;
std::memmove(m_buffer, m_body, std::distance(m_body, m_position));
m_position = m_buffer + std::distance(m_body, m_position);
m_body = m_buffer;
m_body = m_buffer;
} else {
realloc_buffer((m_bufferSize = contentSize) + 1, m_body, std::distance(m_body, m_position));
realloc_buffer((m_bufferSize = content_length) + 1, m_body, std::distance(m_body, m_position));
m_position = m_buffer + std::distance(m_body, m_position);
m_body = m_buffer;
m_body = m_buffer;
}
}
@@ -193,14 +205,14 @@ SCgiTask::event_read() {
return;
event_read_failed:
// throw torrent::internal_error("SCgiTask::event_read() fault not handled.");
event_read_failed:
// throw torrent::internal_error("SCgiTask::event_read() fault not handled.");
close();
}
void
SCgiTask::event_write() {
// Apple and Solaris do not support MSG_NOSIGNAL,
// Apple and Solaris do not support MSG_NOSIGNAL,
// so disable this fix until we find a better solution
#if defined(__APPLE__) || defined(__sun__)
int bytes = ::send(m_fileDesc, m_position, m_bufferSize, 0);
@@ -236,12 +248,16 @@ SCgiTask::receive_write(const char* buffer, uint32_t length) {
if (length + 256 > std::max(m_bufferSize, (unsigned int)default_buffer_size))
realloc_buffer(length + 256, NULL, 0);
// Who ever bothers to check the return value?
int headerSize = sprintf(m_buffer, "Status: 200 OK\r\nContent-Type: text/xml\r\nContent-Length: %i\r\n\r\n", length);
const auto header = m_content_type == ContentType::JSON
? "Status: 200 OK\r\nContent-Type: application/json\r\nContent-Length: %i\r\n\r\n"
: "Status: 200 OK\r\nContent-Type: text/xml\r\nContent-Length: %i\r\n\r\n";
m_position = m_buffer;
// Who ever bothers to check the return value?
int headerSize = sprintf(m_buffer, header, length);
m_position = m_buffer;
m_bufferSize = length + headerSize;
std::memcpy(m_buffer + headerSize, buffer, length);
if (m_parent->log_fd() >= 0) {
@@ -258,4 +274,4 @@ SCgiTask::receive_write(const char* buffer, uint32_t length) {
return true;
}
}
} // namespace rpc
+6
View File
@@ -53,6 +53,8 @@ public:
static const int max_header_size = 2000;
static const int max_content_size = (2 << 23);
enum ContentType { XML, JSON };
SCgiTask() { m_fileDesc = -1; }
bool is_open() const { return m_fileDesc != -1; }
@@ -61,6 +63,8 @@ public:
void open(SCgi* parent, int fd);
void close();
ContentType content_type() const { return m_content_type; }
virtual void event_read();
virtual void event_write();
virtual void event_error();
@@ -79,6 +83,8 @@ private:
char* m_body;
unsigned int m_bufferSize;
ContentType m_content_type{ XML };
};
}
+1 -89
View File
@@ -44,98 +44,10 @@
namespace rpc {
class xmlrpc_error : public torrent::base_error {
public:
xmlrpc_error(int type, std::string msg) : m_type(type), m_msg(msg) {}
virtual ~xmlrpc_error() throw() {}
virtual int type() const throw() { return m_type; }
virtual const char* what() const throw() { return m_msg.c_str(); }
private:
int m_type;
std::string m_msg;
};
void
XmlRpc::object_to_target(const torrent::Object& obj, int callFlags, rpc::target_type* target) {
if (callFlags & CommandMap::flag_no_target)
return;
if (!obj.is_string()) {
throw torrent::input_error("invalid parameters: target must be a string");
}
std::string target_string = obj.as_string();
bool require_index = (callFlags & (CommandMap::flag_tracker_target | CommandMap::flag_file_target));
if (target_string.size() == 0 && !require_index) {
return;
}
// Length of SHA1 hash is 40
if (target_string.size() < 40) {
throw torrent::input_error("invalid parameters: invalid target");
}
char type = 'd';
std::string hash;
std::string index;
const auto& delim_pos = target_string.find_first_of(':', 40);
if (delim_pos == target_string.npos ||
delim_pos + 2 >= target_string.size()) {
if (require_index) {
throw torrent::input_error("invalid parameters: no index");
}
hash = target_string;
} else {
hash = target_string.substr(0, delim_pos);
type = target_string[delim_pos + 1];
index = target_string.substr(delim_pos + 2);
}
core::Download* download = xmlrpc.slot_find_download()(hash.c_str());
if (download == nullptr)
throw torrent::input_error("invalid parameters: info-hash not found");
try {
switch (type) {
case 'd':
*target = rpc::make_target(download);
break;
case 'f':
*target = rpc::make_target(
command_base::target_file,
xmlrpc.slot_find_file()(download, std::stoi(std::string(index))));
break;
case 't':
*target = rpc::make_target(
command_base::target_tracker,
xmlrpc.slot_find_tracker()(download, std::stoi(std::string(index))));
break;
case 'p': {
if (index.size() < 40) {
// -501 == XMLRPC_TYPE_ERROR, used here directly to avoid
// conflicts between tinyxml2 and xmlrpc-c
throw xmlrpc_error(-501, "Not a hash string.");
}
torrent::HashString hash;
torrent::hash_string_from_hex_c_str(index.c_str(), hash);
*target = rpc::make_target(
command_base::target_peer,
xmlrpc.slot_find_peer()(download, hash));
break;
}
default:
throw torrent::input_error("invalid parameters: unexpected target type");
}
} catch (const std::logic_error&) {
throw torrent::input_error("invalid parameters: invalid index");
}
}
#ifndef HAVE_XMLRPC_C
#ifndef HAVE_XMLRPC_TINYXML2
void XmlRpc::initialize() { throw torrent::resource_error("XMLRPC not supported."); }
void XmlRpc::initialize() {}
void XmlRpc::cleanup() {}
void XmlRpc::insert_command(__UNUSED const char* name, __UNUSED const char* parm, __UNUSED const char* doc) {}
+2 -1
View File
@@ -51,6 +51,7 @@
#include <torrent/object.h>
#include <torrent/exceptions.h>
#include "rpc_manager.h"
#include "xmlrpc.h"
#include "parse_commands.h"
@@ -138,7 +139,7 @@ rpc::target_type
xmlrpc_to_target(xmlrpc_env* env, xmlrpc_value* value, int callType) {
rpc::target_type target;
XmlRpc::object_to_target(xmlrpc_to_object(env, value, -1, nullptr), callType, &target);
Rpc::object_to_target(xmlrpc_to_object(env, value, -1, nullptr), callType, &target);
return target;
}
+19 -32
View File
@@ -17,6 +17,7 @@
#include "parse_commands.h"
#include "rpc/tinyxml2/tinyxml2.h"
#include "rpc/rpc_manager.h"
#include "utils/base64.h"
#include "xmlrpc.h"
@@ -35,20 +36,6 @@ const int XMLRPC_INTROSPECTION_DISABLED_ERROR = -508;
const int XMLRPC_LIMIT_EXCEEDED_ERROR = -509;
const int XMLRPC_INVALID_UTF8_ERROR = -510;
class xmlrpc_error : public torrent::base_error {
public:
xmlrpc_error(int type, std::string msg) :
m_type(type), m_msg(msg) {}
virtual ~xmlrpc_error() throw() {}
virtual int type() const throw() { return m_type; }
virtual const char* what() const throw() { return m_msg.c_str(); }
private:
int m_type;
std::string m_msg;
};
const tinyxml2::XMLElement*
element_access(const tinyxml2::XMLElement* elem, std::initializer_list<std::string> names) {
// Helper function to check each step of a element access, in lieu of XPath
@@ -56,7 +43,7 @@ element_access(const tinyxml2::XMLElement* elem, std::initializer_list<std::stri
for (auto itr : names) {
result = result->FirstChildElement(itr.c_str());
if (result == nullptr) {
throw xmlrpc_error(XMLRPC_PARSE_ERROR, "could not find expected element " + itr);
throw rpc_error(XMLRPC_PARSE_ERROR, "could not find expected element " + itr);
}
}
return result;
@@ -66,22 +53,22 @@ long long
element_to_int(const tinyxml2::XMLNode* elem) {
char* pos;
if (elem->FirstChild() == nullptr) {
throw xmlrpc_error(XMLRPC_TYPE_ERROR, "unable to parse empty integer");
throw rpc_error(XMLRPC_TYPE_ERROR, "unable to parse empty integer");
}
auto str = elem->FirstChild()->ToText()->Value();
auto result = std::strtoll(str, &pos, 10);
if (pos == str || *pos != '\0')
throw xmlrpc_error(XMLRPC_TYPE_ERROR, "unable to parse integer value");
throw rpc_error(XMLRPC_TYPE_ERROR, "unable to parse integer value");
return result;
}
torrent::Object
xml_value_to_object(const tinyxml2::XMLNode* elem) {
if (elem == nullptr) {
throw xmlrpc_error(XMLRPC_INTERNAL_ERROR, "received null element to convert");
throw rpc_error(XMLRPC_INTERNAL_ERROR, "received null element to convert");
}
if (std::strncmp(elem->Value(), "value", sizeof("value")) != 0) {
throw xmlrpc_error(XMLRPC_INTERNAL_ERROR, "received non-value element to convert");
throw rpc_error(XMLRPC_INTERNAL_ERROR, "received non-value element to convert");
}
auto root_element = elem->FirstChild();
auto root_type = root_element->Value();
@@ -102,13 +89,13 @@ xml_value_to_object(const tinyxml2::XMLNode* elem) {
} else if (boolean_text == "0") {
return torrent::Object((int64_t)0);
}
throw xmlrpc_error(XMLRPC_TYPE_ERROR, "unknown boolean value: " + boolean_text);
throw rpc_error(XMLRPC_TYPE_ERROR, "unknown boolean value: " + boolean_text);
} else if (std::strncmp(root_type, "array", sizeof("array")) == 0) {
auto array_raw = torrent::Object::create_list();
auto& array = array_raw.as_list();
auto data_element = root_element->ToElement()->FirstChildElement("data");
if (data_element == nullptr)
throw xmlrpc_error(XMLRPC_PARSE_ERROR, "could not find expected data element in array");
throw rpc_error(XMLRPC_PARSE_ERROR, "could not find expected data element in array");
for (auto child = data_element->FirstChildElement("value"); child; child = child->NextSiblingElement("value")) {
array.push_back(xml_value_to_object(child));
}
@@ -128,7 +115,7 @@ xml_value_to_object(const tinyxml2::XMLNode* elem) {
}
return torrent::Object(utils::decode_base64(utils::remove_newlines(child_element->ToText()->Value())));
} else {
throw xmlrpc_error(XMLRPC_INTERNAL_ERROR, "received unsupported value type: " + std::string(root_type));
throw rpc_error(XMLRPC_INTERNAL_ERROR, "received unsupported value type: " + std::string(root_type));
}
return torrent::Object();
}
@@ -202,8 +189,8 @@ print_object_xml(const torrent::Object& obj, tinyxml2::XMLPrinter* printer) {
torrent::Object
execute_command(std::string method_name, const tinyxml2::XMLElement* params_element) {
CommandMap::iterator cmd_itr = commands.find(method_name.c_str());
if (cmd_itr == commands.end() || !(cmd_itr->second.m_flags & CommandMap::flag_public_xmlrpc)) {
throw xmlrpc_error(XMLRPC_NO_SUCH_METHOD_ERROR, "method '" + method_name + "' not defined");
if (cmd_itr == commands.end() || !(cmd_itr->second.m_flags & CommandMap::flag_public_rpc)) {
throw rpc_error(XMLRPC_NO_SUCH_METHOD_ERROR, "method '" + method_name + "' not defined");
}
torrent::Object params_raw = torrent::Object::create_list();
torrent::Object::list_type& params = params_raw.as_list();
@@ -214,7 +201,7 @@ execute_command(std::string method_name, const tinyxml2::XMLElement* params_elem
const auto* child = params_element->FirstChildElement("param");
if (child != nullptr) {
if (!(cmd_itr->second.m_flags & CommandMap::flag_no_target)) {
XmlRpc::object_to_target(xml_value_to_object(child->FirstChildElement("value")), cmd_itr->second.m_flags, &target);
RpcManager::object_to_target(xml_value_to_object(child->FirstChildElement("value")), cmd_itr->second.m_flags, &target);
child = child->NextSiblingElement("param");
}
// Parse out any other params
@@ -228,7 +215,7 @@ execute_command(std::string method_name, const tinyxml2::XMLElement* params_elem
const auto* child = params_element->FirstChildElement("data")->FirstChildElement("value");
if (child != nullptr) {
if (!(cmd_itr->second.m_flags & CommandMap::flag_no_target)) {
XmlRpc::object_to_target(xml_value_to_object(child), cmd_itr->second.m_flags, &target);
RpcManager::object_to_target(xml_value_to_object(child), cmd_itr->second.m_flags, &target);
child = child->NextSiblingElement("value");
}
while (child != nullptr) {
@@ -239,7 +226,7 @@ execute_command(std::string method_name, const tinyxml2::XMLElement* params_elem
}
}
if (params.empty() && (cmd_itr->second.m_flags & (CommandMap::flag_file_target | CommandMap::flag_tracker_target))) {
throw xmlrpc_error(XMLRPC_TYPE_ERROR, "invalid parameters: too few");
throw rpc_error(XMLRPC_TYPE_ERROR, "invalid parameters: too few");
}
return rpc::commands.call_command(cmd_itr, params_raw, target);
}
@@ -247,11 +234,11 @@ execute_command(std::string method_name, const tinyxml2::XMLElement* params_elem
void
process_document(const tinyxml2::XMLDocument* doc, tinyxml2::XMLPrinter* printer) {
if (doc->Error())
throw xmlrpc_error(XMLRPC_PARSE_ERROR, doc->ErrorStr());
throw rpc_error(XMLRPC_PARSE_ERROR, doc->ErrorStr());
if (doc->FirstChildElement("methodCall") == nullptr)
throw xmlrpc_error(XMLRPC_PARSE_ERROR, "methodCall element not found");
throw rpc_error(XMLRPC_PARSE_ERROR, "methodCall element not found");
if (doc->FirstChildElement("methodCall")->FirstChildElement("methodName") == nullptr)
throw xmlrpc_error(XMLRPC_PARSE_ERROR, "methodName element not found");
throw rpc_error(XMLRPC_PARSE_ERROR, "methodName element not found");
auto method_name = doc->FirstChildElement("methodCall")->FirstChildElement("methodName")->GetText();
torrent::Object result;
@@ -276,7 +263,7 @@ process_document(const tinyxml2::XMLDocument* doc, tinyxml2::XMLPrinter* printer
auto sub_result = torrent::Object::create_list();
sub_result.as_list().push_back(execute_command(sub_method_name, sub_params));
result_list.push_back(sub_result);
} catch (xmlrpc_error& e) {
} catch (rpc_error& e) {
auto fault = torrent::Object::create_map();
fault.as_map()["faultString"] = e.what();
fault.as_map()["faultCode"] = e.type();
@@ -357,7 +344,7 @@ XmlRpc::process(const char* inBuffer, uint32_t length, slot_write slotWrite) {
tinyxml2::XMLPrinter printer(nullptr, true, 0);
process_document(&doc, &printer);
return slotWrite(printer.CStr(), printer.CStrSize() - 1);
} catch (xmlrpc_error& e) {
} catch (rpc_error& e) {
tinyxml2::XMLPrinter printer(nullptr, true, 0);
print_xmlrpc_fault(e.type(), e.what(), &printer);
return slotWrite(printer.CStr(), printer.CStrSize() - 1);
+12 -13
View File
@@ -48,7 +48,6 @@
#include "core/manager.h"
#include "rpc/scgi.h"
#include "rpc/xmlrpc.h"
#include "rpc/parse_commands.h"
ThreadWorker::ThreadWorker() {
@@ -71,17 +70,17 @@ ThreadWorker::set_scgi(rpc::SCgi* scgi) {
if (!m_scgi.compare_exchange_strong(expected, scgi))
return false;
change_xmlrpc_log();
change_rpc_log();
queue_item((thread_base_func)&start_scgi);
return true;
}
void
ThreadWorker::set_xmlrpc_log(const std::string& filename) {
m_xmlrpcLog = filename;
ThreadWorker::set_rpc_log(const std::string& filename) {
m_rpcLog = filename;
queue_item((thread_base_func)&msg_change_xmlrpc_log);
queue_item((thread_base_func)&msg_change_rpc_log);
}
void
@@ -95,34 +94,34 @@ ThreadWorker::start_scgi(ThreadBase* baseThread) {
}
void
ThreadWorker::msg_change_xmlrpc_log(ThreadBase* baseThread) {
ThreadWorker::msg_change_rpc_log(ThreadBase* baseThread) {
ThreadWorker* thread = (ThreadWorker*)baseThread;
acquire_global_lock();
thread->change_xmlrpc_log();
thread->change_rpc_log();
release_global_lock();
}
void
ThreadWorker::change_xmlrpc_log() {
ThreadWorker::change_rpc_log() {
if (scgi() == NULL)
return;
if (scgi()->log_fd() != -1) {
::close(scgi()->log_fd());
scgi()->set_log_fd(-1);
control->core()->push_log("Closed XMLRPC log.");
control->core()->push_log("Closed RPC log.");
}
if (m_xmlrpcLog.empty())
if (m_rpcLog.empty())
return;
scgi()->set_log_fd(open(rak::path_expand(m_xmlrpcLog).c_str(), O_WRONLY | O_APPEND | O_CREAT, 0644));
scgi()->set_log_fd(open(rak::path_expand(m_rpcLog).c_str(), O_WRONLY | O_APPEND | O_CREAT, 0644));
if (scgi()->log_fd() == -1) {
control->core()->push_log_std("Could not open XMLRPC log file '" + m_xmlrpcLog + "'.");
control->core()->push_log_std("Could not open RPC log file '" + m_rpcLog + "'.");
return;
}
control->core()->push_log_std("Logging XMLRPC events to '" + m_xmlrpcLog + "'.");
control->core()->push_log_std("Logging RPC events to '" + m_rpcLog + "'.");
}
+4 -4
View File
@@ -61,21 +61,21 @@ public:
rpc::SCgi* scgi() { return m_scgi; }
bool set_scgi(rpc::SCgi* scgi);
void set_xmlrpc_log(const std::string& filename);
void set_rpc_log(const std::string& filename);
static void start_scgi(ThreadBase* thread);
static void msg_change_xmlrpc_log(ThreadBase* thread);
static void msg_change_rpc_log(ThreadBase* thread);
private:
void task_touch_log();
void change_xmlrpc_log();
void change_rpc_log();
std::atomic<rpc::SCgi*> m_scgi{ nullptr };
// The following types shall only be modified while holding the
// global lock.
std::string m_xmlrpcLog;
std::string m_rpcLog;
};
#endif
+73
View File
@@ -0,0 +1,73 @@
#include "base64.h"
#include <string>
namespace utils {
std::string
remove_newlines(const std::string& str) {
std::string result;
for (auto& itr : str) {
if (itr != '\n' || itr != '\n')
result.push_back(itr);
}
return result;
}
// Modified from the public domain code in
// https://en.wikibooks.org/wiki/Algorithm_Implementation/Miscellaneous/Base64#C++_2
constexpr static char base64_pad_character = '=';
std::string
decode_base64(const std::string& input) {
if (input.length() % 4) // Sanity check
throw torrent::input_error("Invalid base64.");
size_t padding = 0;
if (input.length()) {
if (input[input.length() - 1] == base64_pad_character)
padding++;
if (input[input.length() - 2] == base64_pad_character)
padding++;
}
// Setup a vector to hold the result
std::string decodedBytes;
decodedBytes.reserve(((input.length() / 4) * 3) - padding);
long temp = 0; // Holds decoded quanta
std::string::const_iterator cursor = input.begin();
while (cursor < input.end()) {
for (size_t quantumPosition = 0; quantumPosition < 4; quantumPosition++) {
temp <<= 6;
if (*cursor >= 0x41 && *cursor <= 0x5A) // This area will need tweaking if
temp |= *cursor - 0x41; // you are using an alternate alphabet
else if (*cursor >= 0x61 && *cursor <= 0x7A)
temp |= *cursor - 0x47;
else if (*cursor >= 0x30 && *cursor <= 0x39)
temp |= *cursor + 0x04;
else if (*cursor == 0x2B)
temp |= 0x3E; // change to 0x2D for URL alphabet
else if (*cursor == 0x2F)
temp |= 0x3F; // change to 0x5F for URL alphabet
else if (*cursor == base64_pad_character) // pad
{
switch (input.end() - cursor) {
case 1: // One pad character
decodedBytes.push_back((temp >> 16) & 0x000000FF);
decodedBytes.push_back((temp >> 8) & 0x000000FF);
return decodedBytes;
case 2: // Two pad characters
decodedBytes.push_back((temp >> 10) & 0x000000FF);
return decodedBytes;
default:
throw torrent::input_error("Invalid padding in base64.");
}
} else
throw torrent::input_error("Invalid character in base64.");
cursor++;
}
decodedBytes.push_back((temp >> 16) & 0x000000FF);
decodedBytes.push_back((temp >> 8) & 0x000000FF);
decodedBytes.push_back((temp) & 0x000000FF);
}
return decodedBytes;
}
} // namespace utils
+7 -65
View File
@@ -1,73 +1,15 @@
#ifndef RTORRENT_UTILS_BASE64_H
#define RTORRENT_UTILS_BASE64_H
#include <string>
#include <torrent/exceptions.h>
namespace utils {
std::string
remove_newlines(const std::string& str) {
std::string result;
for (auto &itr : str) {
if (itr != '\n' || itr != '\n')
result.push_back(itr);
}
return result;
}
std::string remove_newlines(const std::string& str);
std::string decode_base64(const std::string& input);
// Modified from the public domain code in
// https://en.wikibooks.org/wiki/Algorithm_Implementation/Miscellaneous/Base64#C++_2
constexpr static char base64_pad_character = '=';
} // namespace utils
std::string
decode_base64(const std::string& input) {
if (input.length() % 4) // Sanity check
throw torrent::input_error("Invalid base64.");
size_t padding = 0;
if (input.length()) {
if (input[input.length() - 1] == base64_pad_character)
padding++;
if (input[input.length() - 2] == base64_pad_character)
padding++;
}
// Setup a vector to hold the result
std::string decodedBytes;
decodedBytes.reserve(((input.length() / 4) * 3) - padding);
long temp = 0; // Holds decoded quanta
std::string::const_iterator cursor = input.begin();
while (cursor < input.end()) {
for (size_t quantumPosition = 0; quantumPosition < 4; quantumPosition++) {
temp <<= 6;
if (*cursor >= 0x41 && *cursor <= 0x5A) // This area will need tweaking if
temp |= *cursor - 0x41; // you are using an alternate alphabet
else if (*cursor >= 0x61 && *cursor <= 0x7A)
temp |= *cursor - 0x47;
else if (*cursor >= 0x30 && *cursor <= 0x39)
temp |= *cursor + 0x04;
else if (*cursor == 0x2B)
temp |= 0x3E; // change to 0x2D for URL alphabet
else if (*cursor == 0x2F)
temp |= 0x3F; // change to 0x5F for URL alphabet
else if (*cursor == base64_pad_character) // pad
{
switch (input.end() - cursor) {
case 1: // One pad character
decodedBytes.push_back((temp >> 16) & 0x000000FF);
decodedBytes.push_back((temp >> 8) & 0x000000FF);
return decodedBytes;
case 2: // Two pad characters
decodedBytes.push_back((temp >> 10) & 0x000000FF);
return decodedBytes;
default:
throw torrent::input_error("Invalid padding in base64.");
}
} else
throw torrent::input_error("Invalid character in base64.");
cursor++;
}
decodedBytes.push_back((temp >> 16) & 0x000000FF);
decodedBytes.push_back((temp >> 8) & 0x000000FF);
decodedBytes.push_back((temp)&0x000000FF);
}
return decodedBytes;
}
}
#endif
+2
View File
@@ -9,6 +9,8 @@ rtorrentTest_SOURCES = \
rpc/command_test.h \
rpc/command_map_test.cc \
rpc/command_map_test.h \
rpc/jsonrpc_test.cc \
rpc/jsonrpc_test.h \
rpc/xmlrpc_test.cc \
rpc/xmlrpc_test.h \
rpc/command_slot_test.cc \
+1 -1
View File
@@ -11,7 +11,7 @@ CPPUNIT_TEST_SUITE_REGISTRATION(CommandMapTest);
#define CMD2_A_FUNCTION(key, function, slot, parm, doc) \
m_map.insert_slot<rpc::command_base_is_type<rpc::function>::type>(key, slot, &rpc::function, \
rpc::CommandMap::flag_dont_delete | rpc::CommandMap::flag_public_xmlrpc, NULL, NULL);
rpc::CommandMap::flag_dont_delete | rpc::CommandMap::flag_public_rpc, NULL, NULL);
torrent::Object cmd_test_map_a(rpc::target_type t, const torrent::Object& obj) { return obj; }
torrent::Object cmd_test_map_b(rpc::target_type t, const torrent::Object& obj, uint64_t c) { return torrent::Object(c); }
+138
View File
@@ -0,0 +1,138 @@
#include "config.h"
#include <string>
#include "command_helpers.h"
#include "rpc/command_map.h"
#include "control.h"
#include "globals.h"
#include "jsonrpc_test.h"
CPPUNIT_TEST_SUITE_REGISTRATION(JsonrpcTest);
torrent::Object
jsonrpc_cmd_test_reflect(rpc::target_type t, const torrent::Object& obj) { return obj; }
void initialize_command_dynamic();
// Name, Request, Expected response
std::vector<std::tuple<std::string, std::string, std::string>> basic_jsonrpc_requests = {
std::make_tuple("Basic call",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": [""], "id": 1})",
R"({"id":1,"jsonrpc":"2.0","result":[]})"),
std::make_tuple("Basic call with empty params",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": [], "id": 1})",
R"({"id":1,"jsonrpc":"2.0","result":[]})"),
std::make_tuple("Basic call without params",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "id": 1})",
R"({"id":1,"jsonrpc":"2.0","result":[]})"),
std::make_tuple("Basic call with null id",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "id": null})",
R"({"id":null,"jsonrpc":"2.0","result":[]})"),
std::make_tuple("Basic call with string id",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "id": "1"})",
R"({"id":"1","jsonrpc":"2.0","result":[]})"),
std::make_tuple("UTF-8 string",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", "чао"], "id": 1})",
R"({"id":1,"jsonrpc":"2.0","result":["чао"]})"),
std::make_tuple("Emoji string",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", "😊"], "id": 1})",
R"({"id":1,"jsonrpc":"2.0","result":["😊"]})"),
std::make_tuple("Small int",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", 41], "id": 1})",
R"({"id":1,"jsonrpc":"2.0","result":[41]})"),
std::make_tuple("Large int",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", 2247483647], "id": 1})",
R"({"id":1,"jsonrpc":"2.0","result":[2247483647]})"),
std::make_tuple("Boolean",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", true], "id": 1})",
R"({"id":1,"jsonrpc":"2.0","result":[1]})"),
std::make_tuple("Negative large ints",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", -2247483647], "id": 1})",
R"({"id":1,"jsonrpc":"2.0","result":[-2247483647]})"),
std::make_tuple("Simple array",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", [2247483647]], "id": 1})",
R"({"id":1,"jsonrpc":"2.0","result":[[2247483647]]})"),
std::make_tuple("Empty array",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", []], "id": 1})",
R"({"id":1,"jsonrpc":"2.0","result":[[]]})"),
std::make_tuple("Empty struct",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", {}], "id": 1})",
R"({"id":1,"jsonrpc":"2.0","result":[{}]})"),
std::make_tuple("Simple struct",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", {"lowerBound": 18}], "id": 1})",
R"({"id":1,"jsonrpc":"2.0","result":[{"lowerBound":18}]})"),
std::make_tuple("Notification",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": [""]})",
""),
std::make_tuple("Batch",
R"([{"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": [""], "id": 1},{"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": [""], "id": 2}])",
R"([{"id":1,"jsonrpc":"2.0","result":[]},{"id":2,"jsonrpc":"2.0","result":[]}])"),
std::make_tuple("Batch with notification",
R"([{"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": [""], "id": 1},{"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": [""]}])",
R"([{"id":1,"jsonrpc":"2.0","result":[]}])"),
std::make_tuple("Invalid - empty batch",
"[]",
R"({"error":{"code":-32600,"message":"invalid request: empty batch"},"id":null,"jsonrpc":"2.0"})"),
std::make_tuple("Invalid - missing method",
R"({"jsonrpc": "2.0", "method": "no_such_method", "id": 1})",
R"({"error":{"code":-32601,"message":"method not found: no_such_method"},"id":1,"jsonrpc":"2.0"})"),
std::make_tuple("Invalid - i8 target",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": [41], "id": 1})",
R"({"error":{"code":-32602,"message":"invalid parameters: target must be a string"},"id":1,"jsonrpc":"2.0"})"),
std::make_tuple("Invalid - broken JSON",
R"(nrpc": "2.0", "method": "jsonrpc_reflect", "params": [41], ")",
R"({"error":{"code":-32700,"message":"[json.exception.parse_error.101] parse error at line 1, column 2: syntax error while parsing value - invalid literal; last read: 'nr'"},"id":null,"jsonrpc":"2.0"})"),
std::make_tuple("Invalid - float",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", 3.14], "id": 1})",
R"({"error":{"code":-32602,"message":"invalid parameters: unexpected data type float"},"id":1,"jsonrpc":"2.0"})"),
std::make_tuple("Invalid - invalid UTF-8",
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", ")"
"\xc3\x28"
R"("], "id": 1})",
R"({"error":{"code":-32700,"message":"[json.exception.parse_error.101] parse error at line 1, column 66: syntax error while parsing value - invalid string: ill-formed UTF-8 byte; last read: '\"('"},"id":null,"jsonrpc":"2.0"})"),
};
void
JsonrpcTest::setUp() {
m_jsonrpc = rpc::JsonRpc();
m_jsonrpc.initialize();
setlocale(LC_ALL, "");
cachedTime = rak::timer::current();
control = new Control;
if (rpc::commands.find("jsonrpc_reflect") == rpc::commands.end()) {
CMD2_ANY("jsonrpc_reflect", &jsonrpc_cmd_test_reflect);
}
}
void
JsonrpcTest::test_basics() {
for (auto& test : basic_jsonrpc_requests) {
std::string output;
m_jsonrpc.process(std::get<1>(test).c_str(), std::get<1>(test).size(), [&output](const char* c, uint32_t l) { output.append(c, l); return true; });
CPPUNIT_ASSERT_EQUAL_MESSAGE(std::get<0>(test), std::get<2>(test), output);
}
}
+19
View File
@@ -0,0 +1,19 @@
#include <cppunit/extensions/HelperMacros.h>
#include "rpc/command_map.h"
#include "rpc/jsonrpc.h"
class JsonrpcTest : public CppUnit::TestFixture {
CPPUNIT_TEST_SUITE(JsonrpcTest);
CPPUNIT_TEST(test_basics);
CPPUNIT_TEST_SUITE_END();
public:
void setUp();
void tearDown() {}
void test_basics();
private:
rpc::JsonRpc m_jsonrpc;
};