Compare commits

...

33 Commits

Author SHA1 Message Date
rakshasa 05cdd03cd2 Tagged release 0.16.24. 2026-09-22 15:24:24 +02:00
xirvik d1a6942ea7 Keep the object_storage key terminator inside the key buffer
find_raw_string now honours the raw_string size instead of scanning for a nul.
2026-09-21 09:22:26 +02:00
xirvik 3652329e2d Pass the typed download filter as an argument, not command text
A pattern containing '}' closed the match command early and ran the rest as commands.
2026-09-20 16:39:14 +02:00
xirvik 7916e6022f Stop the value command on the untrusted-safe allowlist crashing the daemon
glibc leaves endptr unset for a small invalid base; ERANGE throws instead of clamping.
2026-09-20 15:58:47 +02:00
xirvik c59da0848e Dump the ipv4 filter by reference instead of copying the whole map
A static table owns the map; the loop only reads it on the main thread. No test needed.
2026-09-20 15:33:09 +02:00
xirvik e2f66ff08c Drop the address range parser and its includes, dead since 29edda2f
External linkage kept -Wunused-function quiet; a deletion of dead code needs no test.
2026-09-20 15:01:44 +02:00
xirvik f0b6f2cb47 Fail p.multicall when a command changes the connection list
Disconnecting a peer frees it and reorders the list, so the iteration cannot continue.
2026-09-20 14:05:49 +02:00
xirvik d334b8d339 Stop printing a strerror of si_errno in the SIGBUS dump
Linux leaves si_errno zero there, so the line always read Error: Success.
2026-09-20 10:05:55 +02:00
xirvik 19b71a98b7 Fix data race reading the log buffer from the display thread
Copy entries under the buffer lock before rendering; other threads mutate the deque.
2026-09-20 09:42:59 +02:00
xirvik 8ee3b984dd Give network.xmlrpc.size_limit.set a floor
A limit below any valid request rejects the request that would raise it again.
2026-09-20 09:23:12 +02:00
xirvik 4e7129c71c Keep starting when the DHT cache file is rejected
The router could throw while loading it, stopping rtorrent from starting at all.
2026-09-20 08:53:34 +02:00
xirvik 80ae6a4590 Answer an oversized RPC response with a fault
Cap XML-RPC and JSON-RPC output at the SCGI response limit before handing it over.
2026-09-19 18:04:09 +02:00
xirvik 2086e8e780 Check the argument count before indexing config comment log args
A '# do:log.add_output=debug' line read args[1] past the end of the vector.
2026-09-19 12:13:43 +02:00
xirvik d9439d7a95 Dispatch view.event_added over the added half of the changed range
fb75510 pointed it at the removed half, so it fired on departing downloads instead.
2026-09-19 11:41:58 +02:00
noctuum 0d3404576a Reject a throttle rate that overflows
rate * 1024 wraps to zero at 2^54, which means no limit.
2026-09-19 11:11:00 +02:00
noctuum 84a0d86f2c Own the fault string in xmlrpc_error_c
xmlrpc_env_set_fault frees the buffer the pointer came from.
2026-09-19 10:34:46 +02:00
noctuum 1b15b7c518 Report a failed write in log.vmmap.dump
The dump is only flushed at fclose, whose result was discarded.
2026-09-19 10:11:23 +02:00
noctuum b1be898ba1 Reject an over-long or empty object_storage key
key_type stores a key of key_size or longer as the empty key.
2026-09-18 16:54:48 +02:00
Jari Sundell bd00b5f6fe Use explicit device name binding rather than a custom syntax. 2026-09-18 23:19:43 +09:00
noctuum b34eb1e895 Own the download factory until commit
A malformed data uri threw before commit, and nothing deleted it.
2026-09-18 11:24:30 +02:00
noctuum b857acd081 Increment d.state_counter on pause
The start path increments it; the pause path assigned it to itself.
2026-09-18 11:05:31 +02:00
noctuum 988aca1281 Release the scgi task buffer on close
clear() keeps the capacity, and the task is pooled until exit.
2026-09-18 10:53:58 +02:00
noctuum 378c7ee854 Reject a throttle rate that does not parse
A discarded parse result left the rate at zero, which is unlimited.
2026-09-18 10:37:14 +02:00
noctuum da9606bfae Take ownership of the magnet metadata object
Nothing freed the bencode map the meta download built.
2026-09-18 10:17:28 +02:00
noctuum 8423803b6e Check the flush on the dht cache and history
good() ran before close(), so a failed write replaced the file.
2026-09-17 16:13:55 +02:00
noctuum c1ce2febf0 Reject an over-long line instead of truncating
getline sets failbit, which both loops read as end of input.
2026-09-17 11:33:44 +02:00
noctuum 5ebb7bd151 Call the target deleter in the tinyxml2 backend
A tracker target allocates, and the deleter went out of scope unused.
2026-09-17 10:33:45 +02:00
noctuum 50f0cad0c6 Release the xmlrpc array item on a throw
xmlrpc_to_target throws for an invalid target, skipping the DECREF.
2026-09-17 10:14:21 +02:00
noctuum 3717649bb3 Guard the value iterator in method.insert.list
The list branch dereferenced end() when no value was given.
2026-09-17 09:54:14 +02:00
noctuum c32b56e9d8 Do not reschedule a deleted scheduler item
schedule.remove from the scheduled command deletes the item.
2026-09-16 11:37:19 +02:00
noctuum de54a4c2ad Clamp the negative width in wstring_width
A narrow terminal makes the file list width go below zero.
2026-09-16 11:18:54 +02:00
noctuum 26fdca8f4d Use empty_visible in the view focus movers
empty() counts filtered entries, so size() - 1 underflows.
2026-09-16 10:57:25 +02:00
noctuum 00bcdaee23 Check the info-hash target in p.call_target
Without it an unknown info-hash segfaults the process.
2026-09-16 10:29:52 +02:00
48 changed files with 772 additions and 145 deletions
+3 -3
View File
@@ -1,6 +1,6 @@
m4_pattern_allow([PKG_CHECK_EXISTS])
AC_INIT([rtorrent],[0.16.23],[sundell.software@gmail.com])
AC_INIT([rtorrent],[0.16.24],[sundell.software@gmail.com])
AC_CONFIG_HEADERS([config.h])
AC_CONFIG_MACRO_DIRS([scripts])
@@ -14,7 +14,7 @@ AX_CXX_COMPILE_STDCXX(20, noext, mandatory)
PKG_PROG_PKG_CONFIG
AC_DEFINE([API_VERSION], [27], [api version])
AC_DEFINE([API_VERSION], [28], [api version])
RAK_CHECK_CFLAGS
RAK_CHECK_CXXFLAGS
@@ -49,7 +49,7 @@ fi
PKG_CHECK_MODULES([CPPUNIT], [cppunit],, [no_cppunit="yes"])
PKG_CHECK_MODULES([ZLIB], [zlib])
PKG_CHECK_MODULES([DEPENDENCIES], [libtorrent >= 0.16.23])
PKG_CHECK_MODULES([DEPENDENCIES], [libtorrent >= 0.16.24])
AC_LANG_PUSH(C++)
TORRENT_WITH_XMLRPC_C
+12 -1
View File
@@ -402,13 +402,21 @@ p_multicall(core::Download* download, const torrent::Object::list_type& args) {
auto resultRaw = torrent::Object::create_list();
auto& result = resultRaw.as_list();
for (const auto& connection : *download->connection_list()) {
auto* connection_list = download->connection_list();
const auto change_counter = connection_list->change_counter();
for (const auto& connection : *connection_list) {
torrent::Object::list_type& row = result.insert(result.end(), torrent::Object::create_list())->as_list();
for (auto cItr = ++args.begin(); cItr != args.end(); cItr++) {
const std::string& cmd = cItr->as_string();
row.push_back(rpc::parse_command(rpc::make_target(connection), cmd.c_str(), cmd.c_str() + cmd.size()).first);
// Erasing a peer frees it and swaps the last element into its place, so
// neither this peer nor the iteration survives a change to the list.
if (connection_list->change_counter() != change_counter)
throw torrent::input_error("Command changed the connection list during p.multicall.");
}
}
@@ -431,6 +439,9 @@ p_call_target(const torrent::Object::list_type& args) {
const std::string& peer_id = itr++->as_string();
const std::string& command_key = itr++->as_string();
if (download == nullptr)
throw torrent::input_error("invalid parameters: info-hash not found");
torrent::HashString hash;
if (peer_id.size() != 40)
+1 -1
View File
@@ -157,7 +157,7 @@ system_method_insert_object(const torrent::Object::list_type& args, int flags) {
torrent::Object valueList = torrent::Object::create_list();
torrent::Object::list_type& valueListType = valueList.as_list();
if ((itrArgs)->is_list())
if (itrArgs != args.end() && (itrArgs)->is_list())
valueListType = (itrArgs)->as_list();
control->object_storage()->insert_str(raw_key, valueList, flags);
+7 -2
View File
@@ -319,6 +319,11 @@ apply_ipv4_filter_load(const torrent::Object::list_type& args) {
ipv4_filter_parse(buffer, value);
}
if (file.fail() && !file.eof()) {
lineNumber++;
throw torrent::input_error("Exceeded max line length.");
}
} catch (torrent::input_error& e) {
snprintf(buffer, 2048, "Error in ip filter file: %s:%u: %s", filename.c_str(), lineNumber, e.what());
@@ -339,8 +344,8 @@ apply_ipv4_filter_dump() {
torrent::Object raw_result = torrent::Object::create_list();
torrent::Object::list_type& result = raw_result.as_list();
torrent::ipv4_table::range_map_type range_map = torrent::PeerList::ipv4_filter()->range_map;
torrent::ipv4_table::range_map_type::iterator iter = range_map.begin();
const torrent::ipv4_table::range_map_type& range_map = torrent::PeerList::ipv4_filter()->range_map;
torrent::ipv4_table::range_map_type::const_iterator iter = range_map.begin();
while(iter != range_map.end()) {
char buffer[64];
+4 -1
View File
@@ -122,7 +122,10 @@ log_vmmap_dump(const std::string& str) {
fprintf(log_file, "%8p-%8p [%5llxk]\n", all_mapping.ptr, (char*)all_mapping.ptr + all_mapping.length, (long long unsigned int)(all_mapping.length / 1024));
}
fclose(log_file);
// The buffered output only reaches the kernel here.
if (fclose(log_file) != 0)
throw torrent::input_error("Could not write log file: " + str);
return torrent::Object();
}
+15 -9
View File
@@ -374,25 +374,31 @@ initialize_command_network() {
CMD_ANY_STRING ("network.tos.set", [](auto, auto& str) { return apply_tos(str); });
CMD_ANY ("network.bind_address", [nw_config](auto, auto) { return nw_config->bind_address_best_match_str(); });
CMD_ANY_STRING_V("network.bind_address.set", [nw_config](auto, auto& str) { return nw_config->set_bind_address_str(str); });
CMD_ANY_STRING_V("network.bind_address.set", [nw_config](auto, auto& str) { return nw_config->set_bind_address(str); });
CMD_ANY ("network.bind_address.ipv4", [nw_config](auto, auto) { return nw_config->bind_inet_address_str(); });
CMD_ANY_STRING_V("network.bind_address.ipv4.set", [nw_config](auto, auto& str) { return nw_config->set_bind_inet_address_str(str); });
CMD_ANY_STRING_V("network.bind_address.ipv4.set", [nw_config](auto, auto& str) { return nw_config->set_bind_inet_address(str); });
CMD_ANY ("network.bind_address.ipv6", [nw_config](auto, auto) { return nw_config->bind_inet6_address_str(); });
CMD_ANY_STRING_V("network.bind_address.ipv6.set", [nw_config](auto, auto& str) { return nw_config->set_bind_inet6_address_str(str); });
CMD_ANY_STRING_V("network.bind_address.ipv6.set", [nw_config](auto, auto& str) { return nw_config->set_bind_inet6_address(str); });
CMD_ANY_STRING_V("network.bind_device.set", [nw_config](auto, auto& str) { return nw_config->set_bind_device_name(str); });
CMD_ANY ("network.bind_device.ipv4", [nw_config](auto, auto) { return nw_config->bind_inet_device_name(); });
CMD_ANY_STRING_V("network.bind_device.ipv4.set", [nw_config](auto, auto& str) { return nw_config->set_bind_inet_device_name(str); });
CMD_ANY ("network.bind_device.ipv6", [nw_config](auto, auto) { return nw_config->bind_inet6_device_name(); });
CMD_ANY_STRING_V("network.bind_device.ipv6.set", [nw_config](auto, auto& str) { return nw_config->set_bind_inet6_device_name(str); });
CMD_ANY ("network.local_address", [nw_config](auto, auto) { return nw_config->local_address_best_match_str(); });
CMD_ANY_STRING_V("network.local_address.set", [nw_config](auto, auto& str) { return nw_config->set_local_address_str(str); });
CMD_ANY_STRING_V("network.local_address.set", [nw_config](auto, auto& str) { return nw_config->set_local_address(str); });
CMD_ANY ("network.local_address.ipv4", [nw_config](auto, auto) { return nw_config->local_inet_address_str(); });
CMD_ANY_STRING_V("network.local_address.ipv4.set", [nw_config](auto, auto& str) { return nw_config->set_local_inet_address_str(str); });
CMD_ANY_STRING_V("network.local_address.ipv4.set", [nw_config](auto, auto& str) { return nw_config->set_local_inet_address(str); });
CMD_ANY ("network.local_address.ipv6", [nw_config](auto, auto) { return nw_config->local_inet6_address_str(); });
CMD_ANY_STRING_V("network.local_address.ipv6.set", [nw_config](auto, auto& str) { return nw_config->set_local_inet6_address_str(str); });
CMD_ANY_STRING_V("network.local_address.ipv6.set", [nw_config](auto, auto& str) { return nw_config->set_local_inet6_address(str); });
CMD_ANY ("network.local_port", [nw_config](auto, auto) { return nw_config->local_port_best_match(); });
CMD_ANY_VALUE_V ("network.local_port.set", [nw_config](auto, auto& value) { return nw_config->set_local_port(checked_local_port_value(value, "local")); });
CMD_ANY_VALUE_V ("network.local_port.set", [nw_config](auto, auto& value) { return nw_config->set_local_port(checked_local_port_value(value, "local")); });
CMD_ANY ("network.local_port.ipv4", [nw_config](auto, auto) { return nw_config->local_inet_port(); });
CMD_ANY_VALUE_V ("network.local_port.ipv4.set", [nw_config](auto, auto& value) { return nw_config->set_local_inet_port(checked_local_port_value(value, "local ipv4")); });
CMD_ANY_VALUE_V ("network.local_port.ipv4.set", [nw_config](auto, auto& value) { return nw_config->set_local_inet_port(checked_local_port_value(value, "local ipv4")); });
CMD_ANY ("network.local_port.ipv6", [nw_config](auto, auto) { return nw_config->local_inet6_port(); });
CMD_ANY_VALUE_V ("network.local_port.ipv6.set", [nw_config](auto, auto& value) { return nw_config->set_local_inet6_port(checked_local_port_value(value, "local ipv6")); });
CMD_ANY_VALUE_V ("network.local_port.ipv6.set", [nw_config](auto, auto& value) { return nw_config->set_local_inet6_port(checked_local_port_value(value, "local ipv6")); });
CMD_ANY ("network.proxy.global", [](auto, auto) { return torrent::runtime::proxy_manager()->proxy_url(); });
CMD_ANY_STRING_V("network.proxy.global.set", [](auto, auto& str) { return torrent::runtime::proxy_manager()->set_proxy_url(str); });
+7 -56
View File
@@ -1,10 +1,9 @@
#include "config.h"
#include <cstdio>
#include <limits>
#include <torrent/throttle.h>
#include <torrent/rate.h>
#include <torrent/download/resource_manager.h>
#include <torrent/net/socket_address.h>
#include "core/manager.h"
#include "ui/root.h"
@@ -15,59 +14,6 @@
#include "control.h"
#include "command_helpers.h"
std::pair<uint32_t, uint32_t>
parse_address_range(const torrent::Object::list_type& args, torrent::Object::list_type::const_iterator itr) {
unsigned int prefixWidth, ret;
char dummy;
char host[1024];
torrent::sa_unique_ptr sa;
ret = std::sscanf(itr->as_string().c_str(), "%1023[^/]/%d%c", host, &prefixWidth, &dummy);
if (ret < 1)
throw torrent::input_error("Invalid address/prefix.");
try {
sa = torrent::sa_copy(torrent::sa_lookup_address(host, AF_INET).get());
} catch (torrent::input_error& e) {
throw torrent::input_error("Could not resolve host: " + std::string(e.what()));
}
uint32_t begin, end;
auto sa_addr = htonl(reinterpret_cast<sockaddr_in*>(sa.get())->sin_addr.s_addr);
begin = end = sa_addr;
if (ret == 2) {
if (++itr != args.end())
throw torrent::input_error("Cannot specify both network and range end.");
uint32_t netmask = std::numeric_limits<uint32_t>::max() << (32 - prefixWidth);
if (prefixWidth >= 32 || sa_addr & ~netmask)
throw torrent::input_error("Invalid address/prefix.");
end = sa_addr | ~netmask;
} else if (++itr != args.end()) {
try {
sa = torrent::sa_copy(torrent::sa_lookup_address(itr->as_string(), AF_INET).get());
} catch (torrent::input_error& e) {
throw torrent::input_error("Could not resolve host: " + std::string(e.what()));
}
sa_addr = htonl(reinterpret_cast<sockaddr_in*>(sa.get())->sin_addr.s_addr);
end = sa_addr;
}
// convert to [begin, end) making sure the end doesn't overflow
// (this precludes 255.255.255.255 from ever matching, but that's not a real IP anyway)
return std::make_pair((uint32_t)begin, (uint32_t)std::max(end, end + 1));
}
torrent::Object
apply_throttle(const torrent::Object::list_type& args, bool up) {
auto arg_itr = args.begin();
@@ -83,11 +29,16 @@ apply_throttle(const torrent::Object::list_type& args, bool up) {
throw torrent::input_error("Missing throttle rate for '" + name + "'.");
int64_t rate;
rpc::parse_whole_value_nothrow(arg_itr->as_string().c_str(), &rate);
if (!rpc::parse_whole_value_nothrow(arg_itr->as_string().c_str(), &rate))
throw torrent::input_error("Invalid throttle rate for '" + name + "'.");
if (rate < 0)
throw torrent::input_error("Throttle rate must be non-negative.");
if (rate > (std::numeric_limits<int64_t>::max() >> 10))
throw torrent::input_error("Throttle rate is too large.");
auto itr = control->core()->throttles().find(name);
if (itr == control->core()->throttles().end())
+12 -2
View File
@@ -2,6 +2,7 @@
#include <sys/types.h>
#include <cerrno>
#include <ctime>
#include <limits>
#include <regex>
@@ -126,11 +127,20 @@ apply_value([[maybe_unused]] rpc::target_type target, const torrent::Object::lis
if (args.front().is_value()) {
val = args.front().as_value();
} else {
int base = args.size() > 1 ? args.back().is_value() ?
args.back().as_value() : strtol(args.back().as_string().c_str(), NULL, 10) : 10;
int64_t base = args.size() > 1 ? args.back().is_value() ?
args.back().as_value() : strtoll(args.back().as_string().c_str(), NULL, 10) : 10;
if (base != 0 && (base < 2 || base > 36))
throw torrent::input_error("'value' base must be 0 or between 2 and 36!");
char* endptr = 0;
errno = 0;
val = strtoll(args.front().as_string().c_str(), &endptr, base);
if (errno == ERANGE)
throw torrent::input_error("Number out of range: " + args.front().as_string());
while (*endptr == ' ' || *endptr == '\n') ++endptr;
if (*endptr)
throw torrent::input_error("Junk at end of number: " + args.front().as_string());
+12 -3
View File
@@ -4,6 +4,7 @@
#include <fstream>
#include <sstream>
#include <torrent/exceptions.h>
#include <torrent/object.h>
#include <torrent/object_stream.h>
#include <torrent/rate.h>
@@ -62,7 +63,14 @@ DhtManager::load_dht_cache() {
LT_LOG("could not open cache file (path:%s)", cache_filename.c_str());
}
torrent::runtime::network_manager()->dht_controller()->initialize(cache);
try {
torrent::runtime::network_manager()->dht_controller()->initialize(cache);
} catch (const torrent::base_error& e) {
LT_LOG_ERROR("cache file rejected, discarding (path:%s errmsg:%s)", cache_filename.c_str(), e.what());
torrent::runtime::network_manager()->dht_controller()->initialize(torrent::Object::create_map());
}
if (m_start == dht_on)
start_dht();
@@ -132,11 +140,12 @@ DhtManager::save_dht_cache() {
torrent::Object cache = torrent::Object::create_map();
cache_file << *torrent::runtime::network_manager()->dht_controller()->store_cache(&cache);
// The data only reaches the kernel here, so this is where a full disk is seen.
cache_file.close();
if (!cache_file.good())
return;
cache_file.close();
::rename(filename_tmp.c_str(), filename.c_str());
}
+3 -4
View File
@@ -301,7 +301,7 @@ DownloadList::set_state_stopped(Download* download) {
void
DownloadList::update_paused_state(Download* download) {
rpc::call_command("d.state_changed.set", torrent::this_thread::cached_seconds().count(), rpc::make_target(download));
rpc::call_command("d.state_counter.set", rpc::call_command_value("d.state_counter", rpc::make_target(download)), rpc::make_target(download));
rpc::call_command("d.state_counter.set", rpc::call_command_value("d.state_counter", rpc::make_target(download)) + 1, rpc::make_target(download));
// If initial seeding is complete, don't try it again when restarting.
if (download->is_done() &&
@@ -726,11 +726,10 @@ DownloadList::process_meta_download(Download* download) {
return;
}
torrent::Object* bencode = new torrent::Object(torrent::Object::create_map());
auto bencode = std::make_unique<torrent::Object>(torrent::Object::create_map());
file >> bencode->insert_key("info", torrent::Object());
if (file.fail()) {
delete bencode;
lt_log_print(torrent::LOG_TORRENT_ERROR, "Could not create download, the input is not a valid torrent.");
return;
}
@@ -746,7 +745,7 @@ DownloadList::process_meta_download(Download* download) {
erase_ptr(download);
control->core()->try_create_download_from_meta_download(bencode, metafile);
control->core()->try_create_download_from_meta_download(std::move(bencode), metafile);
}
}
+6 -3
View File
@@ -195,14 +195,14 @@ Manager::try_create_download(const std::string& uri, int flags, const command_li
return;
// Adding download.
DownloadFactory* f = new DownloadFactory(this);
auto f = std::make_unique<DownloadFactory>(this);
f->variables()["tied_to_file"] = (int64_t)(bool)(flags & create_tied);
f->commands().insert(f->commands().end(), commands.begin(), commands.end());
f->set_start(flags & create_start);
f->set_print_log(!(flags & create_quiet));
f->slot_finished([f]() { delete f; });
f->slot_finished([factory = f.get()]() { delete factory; });
if (is_data_uri(uri)) {
// Allow the use of data URIs, primarily for JSON-RPC which
@@ -216,10 +216,13 @@ Manager::try_create_download(const std::string& uri, int flags, const command_li
}
f->commit();
// From here the finished slot deletes it.
f.release();
}
void
Manager::try_create_download_from_meta_download(torrent::Object* bencode, const std::string& metafile) {
Manager::try_create_download_from_meta_download(std::unique_ptr<torrent::Object> bencode, const std::string& metafile) {
DownloadFactory* f = new DownloadFactory(this);
f->variables()["tied_to_file"] = (int64_t)true;
+1 -1
View File
@@ -76,7 +76,7 @@ public:
// Temporary, find a better place for this.
void try_create_download(const std::string& uri, int flags, const command_list_type& commands);
void try_create_download_expand(const std::string& uri, int flags, command_list_type commands = command_list_type());
void try_create_download_from_meta_download(torrent::Object* bencode, const std::string& metafile);
void try_create_download_from_meta_download(std::unique_ptr<torrent::Object> bencode, const std::string& metafile);
private:
void create_http(const std::string& uri);
+3 -3
View File
@@ -216,7 +216,7 @@ View::set_not_visible(Download* download) {
void
View::next_focus(unsigned int i) {
if (empty())
if (empty_visible())
return;
// If at the boundary, roll over
@@ -239,7 +239,7 @@ View::next_focus(unsigned int i) {
void
View::prev_focus(unsigned int i) {
if (empty())
if (empty_visible())
return;
// If at the boundary, roll over
@@ -301,7 +301,7 @@ View::filter() {
std::for_each(changed.begin(), splitChanged, [this](const auto& d) { rpc::call_object_d_nothrow(m_event_removed, d.get()); });
if (!m_event_added.is_empty())
std::for_each(changed.begin(), splitChanged, [this](const auto& d) { rpc::call_object_d_nothrow(m_event_added, d.get()); });
std::for_each(splitChanged, changed.end(), [this](const auto& d) { rpc::call_object_d_nothrow(m_event_added, d.get()); });
emit_changed();
}
+4 -1
View File
@@ -25,7 +25,10 @@ WindowFileList::WindowFileList(const ui::ElementFileList* element) :
// Convert std::string to std::wstring of given width (in screen positions),
// taking into account that some characters may be occupying two screen positions.
std::wstring
wstring_width(const std::string& i_str, [[maybe_unused]] int width) {
wstring_width(const std::string& i_str, int width) {
if (width < 0)
width = 0;
std::wstring result(width + 1, L' ');
size_t length = std::mbstowcs(result.data(), i_str.c_str(), width);
+20 -6
View File
@@ -1,6 +1,8 @@
#include "config.h"
#include <ctime>
#include <mutex>
#include <vector>
#include <torrent/torrent.h>
#include <torrent/system/callbacks.h>
#include <torrent/system/thread.h>
@@ -41,13 +43,20 @@ WindowLog::redraw() {
int pos = m_canvas->height();
for (iterator itr = m_log->end(), last = find_older(); itr != last && pos > 0; --pos) {
itr--;
std::vector<torrent::log_entry> entries;
{
std::lock_guard<torrent::log_buffer> guard(*m_log);
for (iterator itr = m_log->end(), last = find_older(); itr != last && entries.size() < static_cast<size_t>(pos); )
entries.push_back(*--itr);
}
for (const auto& entry : entries) {
char buffer[16];
print_hhmmss_local(buffer, buffer + 16, static_cast<time_t>(itr->timestamp));
print_hhmmss_local(buffer, buffer + 16, static_cast<time_t>(entry.timestamp));
m_canvas->print(0, pos - 1, "(%s) %s", buffer, itr->message.c_str());
m_canvas->print(0, --pos, "(%s) %s", buffer, entry.message.c_str());
}
}
@@ -60,8 +69,13 @@ WindowLog::receive_update() {
if (!is_active())
return;
auto itr = find_older();
auto height = std::min(std::distance(itr, (iterator)m_log->end()), (std::iterator_traits<iterator>::difference_type)10);
std::iterator_traits<iterator>::difference_type height;
{
std::lock_guard<torrent::log_buffer> guard(*m_log);
height = std::min(std::distance(find_older(), (iterator)m_log->end()), (std::iterator_traits<iterator>::difference_type)10);
}
if (height != m_max_height) {
m_min_height = height != 0 ? 1 : 0;
+18 -6
View File
@@ -1,6 +1,8 @@
#include "config.h"
#include <ctime>
#include <mutex>
#include <vector>
#include "canvas.h"
#include "utils.h"
@@ -29,24 +31,34 @@ WindowLogComplete::redraw() {
int pos = m_canvas->height();
for (iterator itr = m_log->end(), last = m_log->begin(); itr != last && pos > 0; ) {
itr--;
std::vector<torrent::log_entry> entries;
{
std::lock_guard<torrent::log_buffer> guard(*m_log);
for (iterator itr = m_log->end(), last = m_log->begin(); itr != last && entries.size() < static_cast<size_t>(pos); )
entries.push_back(*--itr);
}
for (const auto& entry : entries) {
if (pos <= 0)
break;
char buffer[16];
// Use an arbitrary min width of 60 for allowing multiple
// lines. This should ensure we don't mess up the display when the
// screen is shrunk too much.
unsigned int timeWidth = 3 + print_hhmmss_local(buffer, buffer + 16, static_cast<time_t>(itr->timestamp)) - buffer;
unsigned int timeWidth = 3 + print_hhmmss_local(buffer, buffer + 16, static_cast<time_t>(entry.timestamp)) - buffer;
unsigned int logWidth = m_canvas->width() > 60 ? (m_canvas->width() - timeWidth) : (60 - timeWidth);
unsigned int logHeight = (itr->message.size() + logWidth - 1) / logWidth;
unsigned int logHeight = (entry.message.size() + logWidth - 1) / logWidth;
for (unsigned int j = logHeight; j > 0 && pos > 0; --j, --pos)
if (j == 1)
m_canvas->print(0, pos - 1, "(%s) %s", buffer, itr->message.substr(0, m_canvas->width() - timeWidth).c_str());
m_canvas->print(0, pos - 1, "(%s) %s", buffer, entry.message.substr(0, m_canvas->width() - timeWidth).c_str());
else
m_canvas->print(timeWidth, pos - 1, "%s", itr->message.substr(logWidth * (j - 1), m_canvas->width() - timeWidth).c_str());
m_canvas->print(timeWidth, pos - 1, "%s", entry.message.substr(logWidth * (j - 1), m_canvas->width() - timeWidth).c_str());
}
}
-2
View File
@@ -550,8 +550,6 @@ handle_sigbus(int signum, siginfo_t* sa, [[maybe_unused]] void* ptr) {
#else
output << "Stack dump not enabled." << std::endl;
#endif
output << std::endl << "Error: " << std::strerror(sa->si_errno) << std::endl;
const char* signal_reason;
switch (sa->si_code) {
+7 -1
View File
@@ -61,14 +61,20 @@ CommandScheduler::call_item(value_type item) {
// Remove the item before calling the command if it should be
// removed.
std::string key = item->key();
try {
rpc::call_object(item->command());
} catch (torrent::input_error& e) {
if (m_slotErrorMessage)
m_slotErrorMessage("Scheduled command failed: " + item->key() + ": " + e.what());
m_slotErrorMessage("Scheduled command failed: " + key + ": " + e.what());
}
// The command is allowed to erase or replace this item, which deletes it.
if (std::find(begin(), end(), item) == end())
return;
// Still schedule if we caught a torrrent::input_error?
auto next = item->next_time_scheduled();
+8
View File
@@ -13,6 +13,7 @@
#include "rpc/command_map.h"
#include "rpc/nlohmann/json.h"
#include "rpc/parse_commands.h"
#include "rpc/scgi_task.h"
#include "torrent/exceptions.h"
#include "torrent/object.h"
#include "utils/functional.h"
@@ -264,6 +265,13 @@ JsonRpc::process(const char* in_buffer, uint32_t length, slot_write callback) {
std::string response_str = response.dump();
if (response_str.size() > SCgiTask::max_response_size) {
const auto& id = response.is_object() && response.contains("id") ? response["id"] : json(nullptr);
auto err_str = json_error(JSONRPC_INTERNAL_ERROR, "response size exceeds maximum RPC limit", id).dump();
return callback(err_str.c_str(), err_str.size());
}
return callback(response_str.c_str(), response_str.size());
} catch (json::exception& e) {
+7 -6
View File
@@ -26,10 +26,7 @@ const size_t object_storage::key_size;
object_storage::iterator
object_storage::find_raw_string(const torrent::raw_string& key) {
fixed_key_type<64> k;
k.set_c_str(key.data());
return find(k);
return find(key_type::from_raw_string(key));
}
object_storage::iterator
@@ -64,8 +61,12 @@ object_storage::insert(const char* key_data, uint32_t key_size, const torrent::O
if (std::find(key_data, key_data + key_size, '\0') != key_data + key_size)
throw torrent::input_error("Found nul-char in string.");
// Check for size > key_size.
// Check for empty string.
// key_type turns a key this long into the empty key.
if (key_size >= object_storage::key_size)
throw torrent::input_error("Key is too long.");
if (key_size == 0)
throw torrent::input_error("Key is empty.");
bool use_raw = false;
torrent::Object object;
+1 -8
View File
@@ -158,14 +158,7 @@ fixed_key_type<MaxSize>::set_data(const value_type* src_data, size_type src_size
template <size_t MaxSize> inline void
fixed_key_type<MaxSize>::set_c_str(const value_type* src_data) {
value_type* itr = m_data;
const value_type* last = m_data + max_size;
while (itr != last && *src_data != '\0')
*itr++ = *src_data++;
*itr = '\0';
m_size = std::distance(m_data, itr);
set_c_str(src_data, std::strlen(src_data));
}
template <size_t MaxSize> inline void
+5
View File
@@ -181,6 +181,11 @@ parse_command_file(const std::string& path) {
getCount = 0;
}
if (file.fail() && !file.eof()) {
lineNumber++;
throw torrent::input_error("Exceeded max line length.");
}
} catch (torrent::input_error& e) {
snprintf(buffer, 2048, "Error in option file: %s:%u: %s", path.c_str(), lineNumber, e.what());
+3
View File
@@ -172,6 +172,9 @@ RpcManager::set_size_limit(uint64_t size) {
if (size > SCgiTask::max_content_size)
throw torrent::input_error("XMLRPC size limit cannot exceed the SCGI content size limit.");
if (size < min_size_limit)
throw torrent::input_error("XMLRPC size limit is too small to hold a request.");
m_xmlrpc.set_size_limit(size);
}
+4
View File
@@ -53,6 +53,10 @@ public:
enum RPCType { XML,
JSON };
// A limit below the shortest useful request rejects every request, the one
// that would raise it again included.
static constexpr uint64_t min_size_limit = 1024;
RpcManager() = default;
~RpcManager() = default;
+3 -2
View File
@@ -93,7 +93,8 @@ SCgiTask::close() {
// The callbacks are guaranteed to be finished/canceled at this point.
auto lock = std::lock_guard<std::mutex>(m_result_mutex);
m_buffer.clear();
// clear() would keep the capacity, and the task is pooled for the lifetime of the process.
std::vector<char>().swap(m_buffer);
}
void
@@ -404,7 +405,7 @@ void
SCgiTask::receive_write(const char* buffer, uint32_t length) {
assert(torrent::this_thread::thread() == torrent::main_thread::thread());
if (buffer == nullptr || length > (100 << 20))
if (buffer == nullptr || length > max_response_size)
throw torrent::internal_error("SCgiTask::receive_write(...) received bad input.");
// Main thread callback already locked this mutex.
+3
View File
@@ -2,6 +2,7 @@
#define RTORRENT_RPC_SCGI_TASK_H
#include <chrono>
#include <cstdint>
#include <memory>
#include <mutex>
#include <vector>
@@ -18,6 +19,8 @@ public:
static constexpr int max_header_size = 2000;
static constexpr int max_content_size = (1 << 26);
static constexpr uint32_t max_response_size = (100 << 20);
static constexpr auto timeout_request = std::chrono::seconds(60);
enum ContentType { XML, JSON };
+6 -7
View File
@@ -27,11 +27,11 @@ public:
virtual ~xmlrpc_error_c() throw() {}
virtual int type() const throw() { return m_type; }
virtual const char* what() const throw() { return m_msg; }
virtual const char* what() const throw() { return m_msg.c_str(); }
private:
int m_type;
const char* m_msg;
std::string m_msg;
};
torrent::Object xmlrpc_to_object(xmlrpc_env* env, xmlrpc_value* value, int call_type = 0, rpc::target_type* target = NULL, std::function<void()>* deleter = NULL);
@@ -44,10 +44,9 @@ xmlrpc_list_entry_to_object(xmlrpc_env* env, xmlrpc_value* src, int index) {
if (env->fault_occurred)
throw xmlrpc_error_c(env);
torrent::Object obj = xmlrpc_to_object(env, tmp);
xmlrpc_DECREF(tmp);
utils::scope_guard guard([tmp]() { xmlrpc_DECREF(tmp); });
return obj;
return xmlrpc_to_object(env, tmp);
}
int64_t
@@ -213,11 +212,11 @@ xmlrpc_to_object(xmlrpc_env* env, xmlrpc_value* value, int call_type, rpc::targe
if (env->fault_occurred)
throw xmlrpc_error_c(env);
utils::scope_guard guard([tmp]() { xmlrpc_DECREF(tmp); });
if (target != nullptr)
std::tie(*target, *deleter) = xmlrpc_to_target(env, tmp, call_type);
xmlrpc_DECREF(tmp);
if (env->fault_occurred)
throw xmlrpc_error_c(env);
+11 -4
View File
@@ -19,6 +19,7 @@
#include "rpc/tinyxml2/tinyxml2.h"
#include "rpc/rpc_manager.h"
#include "utils/base64.h"
#include "utils/functional.h"
#include "xmlrpc.h"
namespace rpc {
@@ -258,6 +259,8 @@ execute_command(std::string method_name, const tinyxml2::XMLElement* params_elem
torrent::Object params_raw = torrent::Object::create_list();
torrent::Object::list_type& params = params_raw.as_list();
rpc::target_type target = rpc::make_target();
std::function<void()> deleter = []() {};
utils::scope_guard guard([&deleter]() { deleter(); });
if (params_element != nullptr) {
if (std::strncmp(params_element->Name(), "params", sizeof("params")) == 0) {
@@ -265,8 +268,6 @@ execute_command(std::string method_name, const tinyxml2::XMLElement* params_elem
const auto* child = params_element->FirstChildElement("param");
if (child != nullptr) {
std::function<void()> deleter = []() {};
RpcManager::object_to_target(xml_value_to_object(child->FirstChildElement("value")), cmd_itr->second.m_flags, &target, &deleter);
child = child->NextSiblingElement("param");
@@ -282,8 +283,6 @@ execute_command(std::string method_name, const tinyxml2::XMLElement* params_elem
const auto* child = params_element->FirstChildElement("data")->FirstChildElement("value");
if (child != nullptr) {
std::function<void()> deleter = []() {};
RpcManager::object_to_target(xml_value_to_object(child), cmd_itr->second.m_flags, &target, &deleter);
child = child->NextSiblingElement("value");
@@ -426,6 +425,14 @@ XmlRpc::process(const char* inBuffer, uint32_t length, slot_write slotWrite) {
// remains.
tinyxml2::XMLPrinter printer(nullptr, true, 0);
process_document(&doc, &printer);
if (printer.CStrSize() - 1 > static_cast<int>(SCgiTask::max_response_size)) {
tinyxml2::XMLPrinter fault_printer(nullptr, true, 0);
print_xmlrpc_fault(XMLRPC_LIMIT_EXCEEDED_ERROR, "Response size exceeds maximum XML-RPC limit", &fault_printer);
return slotWrite(fault_printer.CStr(), fault_printer.CStrSize() - 1);
}
return slotWrite(printer.CStr(), printer.CStrSize() - 1);
} catch (rpc_error& e) {
tinyxml2::XMLPrinter printer(nullptr, true, 0);
+6 -2
View File
@@ -86,9 +86,13 @@ config_comment_log(const std::string& command, const std::string& raw_args) {
pos = next_pos + 1;
}
if (command == "log.add_output")
if (command == "log.add_output") {
if (args.size() != 2)
throw torrent::input_error("Invalid number of arguments.");
log_add_group_output_str(args[0], args[1]);
else if (command == "log.open_file")
} else if (command == "log.open_file")
apply_log_open_str(0, args);
else if (command == "log.open_file.flush")
apply_log_open_str(log_flag_flush, args);
+36 -8
View File
@@ -30,6 +30,38 @@
namespace ui {
std::string
create_filter_pattern(const std::string& input) {
std::string pattern = input;
if (pattern.empty())
return pattern;
if (pattern.back() != '$')
pattern = pattern + ".*";
if (pattern.front() != '^')
pattern = ".*" + pattern;
std::transform(pattern.begin(), pattern.end(), pattern.begin(), ::tolower);
return pattern;
}
torrent::Object
create_filter_command(const std::string& pattern) {
torrent::Object name = torrent::Object::create_dict_key();
name.as_dict_key() = "d.name";
torrent::Object args = torrent::Object::create_list();
args.as_list().push_back(name);
args.as_list().push_back(torrent::Object(pattern));
torrent::Object command = torrent::Object::create_dict_key();
command.as_dict_key() = "match";
command.as_dict_obj() = args;
return command;
}
DownloadList::DownloadList() {
m_uiArray[DISPLAY_DOWNLOAD] = NULL;
m_uiArray[DISPLAY_DOWNLOAD_LIST] = new ElementDownloadList();
@@ -319,16 +351,12 @@ DownloadList::receive_exit_input(Input type) {
current_view()->filter();
current_view()->sort();
} else {
std::string pattern = input->str();
if (pattern.back() != '$')
pattern = pattern + ".*";
if (pattern.front() != '^')
pattern = ".*" + pattern;
std::transform(pattern.begin(), pattern.end(), pattern.begin(), ::tolower);
std::string temp_filter = "match={d.name=," + pattern + "}";
std::string pattern = create_filter_pattern(input->str());
if (rpc::call_command_value("view.filter.temp.log"))
control->core()->push_log_std("Temporary filter on '" + current_view()->name() + "' view: " + pattern);
current_view()->set_filter_temp(temp_filter);
current_view()->set_filter_temp(create_filter_command(pattern));
current_view()->filter();
}
break;
+9
View File
@@ -1,6 +1,9 @@
#ifndef RTORRENT_UI_DOWNLOAD_LIST_H
#define RTORRENT_UI_DOWNLOAD_LIST_H
#include <string>
#include <torrent/object.h>
#include "element_base.h"
#include "globals.h"
#include "display/manager.h"
@@ -29,6 +32,12 @@ namespace ui {
class Download;
// Wrap a user-typed download name pattern in the anchors 'match' expects.
std::string create_filter_pattern(const std::string& input);
// Build the 'match' command the temporary view filter runs against d.name.
torrent::Object create_filter_command(const std::string& pattern);
class DownloadList : public ElementBase {
public:
typedef display::WindowDownloadList WList;
+3 -2
View File
@@ -467,6 +467,9 @@ Root::save_input_history() {
history_file << entry << "|" + category.at((pitr->second + i) % m_input_history_length) + "\n";
}
// The data only reaches the kernel here, so this is where a full disk is seen.
history_file.close();
if (!history_file.good()) {
lt_log_print(torrent::LOG_DEBUG, "input history file corrupted during writing, discarding (path:%s)", history_filename.c_str());
return;
@@ -474,8 +477,6 @@ Root::save_input_history() {
lt_log_print(torrent::LOG_DEBUG, "input history file written (path:%s)", history_filename.c_str());
}
history_file.close();
std::rename(history_filename_tmp.c_str(), history_filename.c_str());
}
+15 -1
View File
@@ -1,4 +1,5 @@
TESTS = \
rtorrent_Test_Core \
rtorrent_Test_Rpc \
rtorrent_Test_Src
@@ -7,6 +8,7 @@ check_PROGRAMS = $(TESTS)
rtorrent_Test_LDADD = \
../src/libsub_root.a
rtorrent_Test_Core_LDADD = $(rtorrent_Test_LDADD)
rtorrent_Test_Rpc_LDADD = $(rtorrent_Test_LDADD)
rtorrent_Test_Src_LDADD = $(rtorrent_Test_LDADD)
@@ -30,6 +32,10 @@ rtorrent_Test_Common = \
helpers/test_utils.h \
helpers/utils.h
rtorrent_Test_Core_SOURCES = $(rtorrent_Test_Common) \
core/test_view.cc \
core/test_view.h
rtorrent_Test_Rpc_SOURCES = $(rtorrent_Test_Common) \
rpc/test_command.cc \
rpc/test_command.h \
@@ -46,7 +52,9 @@ rtorrent_Test_Rpc_SOURCES = $(rtorrent_Test_Common) \
rpc/test_object_storage.cc \
rpc/test_object_storage.h \
rpc/test_parse_options.cc \
rpc/test_parse_options.h
rpc/test_parse_options.h \
rpc/test_rpc_manager.cc \
rpc/test_rpc_manager.h
rtorrent_Test_Src_SOURCES = $(rtorrent_Test_Common) \
src/test_command_dynamic.cc \
@@ -59,9 +67,15 @@ rtorrent_Test_Src_SOURCES = $(rtorrent_Test_Common) \
src/test_command_path.h \
src/test_command_string.cc \
src/test_command_string.h \
src/test_setup.cc \
src/test_setup.h \
src/test_ui_download_list.cc \
src/test_ui_download_list.h \
src/test_watch_ready_queue.cc \
src/test_watch_ready_queue.h
rtorrent_Test_Core_CXXFLAGS = $(CPPUNIT_CFLAGS)
rtorrent_Test_Core_LDFLAGS = $(CPPUNIT_LIBS) -ldl
rtorrent_Test_Rpc_CXXFLAGS = $(CPPUNIT_CFLAGS)
rtorrent_Test_Rpc_LDFLAGS = $(CPPUNIT_LIBS) -ldl
rtorrent_Test_Src_CXXFLAGS = $(CPPUNIT_CFLAGS)
+158
View File
@@ -0,0 +1,158 @@
#include "config.h"
#include "test/core/test_view.h"
#include <algorithm>
#include <cstddef>
#include <deque>
#include <memory>
#include <set>
#include <string>
#include <vector>
#include "command_helpers.h"
#include "control.h"
#include "core/download.h"
#include "core/view.h"
#include "globals.h"
#include "rpc/parse_commands.h"
CPPUNIT_TEST_SUITE_REGISTRATION(TestView);
namespace {
const char* filter_command = "test.view.filter";
const char* added_command = "test.view.event_added";
const char* removed_command = "test.view.event_removed";
// View only stores these and hands them to the command layer as RPC targets,
// which casts the pointer without reading through it.
struct download_stub {
alignas(core::Download) std::byte storage[sizeof(core::Download)];
};
std::deque<download_stub> stub_storage;
std::vector<core::Download*> stub_downloads;
std::set<core::Download*> matches_filter;
std::vector<core::Download*> added_events;
std::vector<core::Download*> removed_events;
std::shared_ptr<core::Download>
make_stub_download() {
auto download = reinterpret_cast<core::Download*>(&stub_storage.emplace_back());
stub_downloads.push_back(download);
return std::shared_ptr<core::Download>(download, [](core::Download*) {});
}
// Renders a dispatch list as "d0,d2" so a failure names the downloads the
// handler saw, not just how many there were.
std::string
describe(const std::vector<core::Download*>& downloads) {
std::string result;
for (const auto& download : downloads) {
auto itr = std::find(stub_downloads.begin(), stub_downloads.end(), download);
if (!result.empty())
result += ',';
result += 'd' + std::to_string(std::distance(stub_downloads.begin(), itr));
}
return result;
}
torrent::Object
cmd_filter(core::Download* download, const torrent::Object&) {
return torrent::Object(static_cast<int64_t>(matches_filter.count(download)));
}
torrent::Object
cmd_event_added(core::Download* download, const torrent::Object&) {
added_events.push_back(download);
return torrent::Object();
}
torrent::Object
cmd_event_removed(core::Download* download, const torrent::Object&) {
removed_events.push_back(download);
return torrent::Object();
}
torrent::Object
command_object(const char* command) {
return torrent::Object(std::string(command) + "=");
}
} // namespace
void
TestView::setUp() {
TestFixtureWithMainThread::setUp();
if (control == nullptr)
control = new Control;
if (!rpc::commands.has(filter_command)) {
CMD2_DL(filter_command, &cmd_filter);
CMD2_DL(added_command, &cmd_event_added);
CMD2_DL(removed_command, &cmd_event_removed);
}
stub_storage.clear();
stub_downloads.clear();
matches_filter.clear();
added_events.clear();
removed_events.clear();
}
void
TestView::tearDown() {
TestFixtureWithMainThread::tearDown();
}
// d0 and d1 start visible and stop matching, d2 and d3 start filtered out and
// start matching. The two halves are the same size on purpose: a test that
// only counted the dispatches would pass even if both events were sent over
// the same half of the changed range.
void
TestView::test_filter_dispatches_events_to_the_right_downloads() {
core::View view;
view.initialize("test_view");
auto d0 = make_stub_download();
auto d1 = make_stub_download();
auto d2 = make_stub_download();
auto d3 = make_stub_download();
view.insert(d0);
view.insert(d1);
view.insert(d2);
view.insert(d3);
view.set_visible(d0.get());
view.set_visible(d1.get());
CPPUNIT_ASSERT_EQUAL(core::View::size_type(2), view.size_visible());
matches_filter = {d2.get(), d3.get()};
view.set_filter(command_object(filter_command));
view.set_event_added(command_object(added_command));
view.set_event_removed(command_object(removed_command));
added_events.clear();
removed_events.clear();
view.filter();
CPPUNIT_ASSERT_EQUAL(std::string("d2,d3"), describe(added_events));
CPPUNIT_ASSERT_EQUAL(std::string("d0,d1"), describe(removed_events));
CPPUNIT_ASSERT_EQUAL(core::View::size_type(2), view.size_visible());
CPPUNIT_ASSERT(*view.begin_visible() == d2);
CPPUNIT_ASSERT(*(view.begin_visible() + 1) == d3);
}
+15
View File
@@ -0,0 +1,15 @@
#include "test/helpers/test_main_thread.h"
class TestView : public TestFixtureWithMainThread {
CPPUNIT_TEST_SUITE(TestView);
CPPUNIT_TEST(test_filter_dispatches_events_to_the_right_downloads);
CPPUNIT_TEST_SUITE_END();
public:
void setUp();
void tearDown();
void test_filter_dispatches_events_to_the_right_downloads();
};
+28
View File
@@ -8,12 +8,18 @@
#include "globals.h"
#include "command_helpers.h"
#include "rpc/command_map.h"
#include "rpc/scgi_task.h"
CPPUNIT_TEST_SUITE_REGISTRATION(TestJsonrpc);
torrent::Object
jsonrpc_cmd_test_reflect([[maybe_unused]] rpc::target_type t, const torrent::Object& obj) { return obj; }
torrent::Object
jsonrpc_cmd_test_oversized([[maybe_unused]] rpc::target_type t, [[maybe_unused]] const torrent::Object& obj) {
return torrent::Object(std::string(rpc::SCgiTask::max_response_size + (1 << 20), 'a'));
}
void initialize_command_dynamic();
// Name, Request, Expected response
@@ -130,6 +136,10 @@ TestJsonrpc::setUp() {
if (rpc::commands.find("jsonrpc_reflect") == rpc::commands.end()) {
CMD2_ANY("jsonrpc_reflect", &jsonrpc_cmd_test_reflect);
}
if (rpc::commands.find("jsonrpc_oversized") == rpc::commands.end()) {
CMD2_ANY("jsonrpc_oversized", &jsonrpc_cmd_test_oversized);
}
}
void
@@ -145,3 +155,21 @@ TestJsonrpc::test_basics() {
CPPUNIT_ASSERT_EQUAL_MESSAGE(std::get<0>(test), std::get<2>(test), output);
}
}
// A command whose result does not fit in the SCGI response buffer must be
// answered with a fault, not handed to the writer. SCgiTask::receive_write
// treats an oversized body as an internal_error, which is not caught by any
// RPC handler and terminates the process.
void
TestJsonrpc::test_response_size_limit() {
const std::string request = R"({"jsonrpc": "2.0", "method": "jsonrpc_oversized", "params": [""], "id": 1})";
const std::string expected = R"({"error":{"code":-32000,"message":"response size exceeds maximum RPC limit"},"id":1,"jsonrpc":"2.0"})";
std::string output;
m_jsonrpc.process(request.c_str(), request.size(), [&output](const char* c, uint32_t l) { output.append(c, l); return true; });
CPPUNIT_ASSERT_MESSAGE("response handed to the writer is " + std::to_string(output.size()) +
" bytes, over the " + std::to_string(rpc::SCgiTask::max_response_size) + " byte SCGI limit",
output.size() <= rpc::SCgiTask::max_response_size);
CPPUNIT_ASSERT_EQUAL(expected, output);
}
+2
View File
@@ -8,6 +8,7 @@ class TestJsonrpc : public test_fixture {
CPPUNIT_TEST_SUITE(TestJsonrpc);
CPPUNIT_TEST(test_basics);
CPPUNIT_TEST(test_response_size_limit);
CPPUNIT_TEST_SUITE_END();
@@ -16,6 +17,7 @@ public:
void tearDown();
void test_basics();
void test_response_size_limit();
private:
std::unique_ptr<TestMainThread> m_test_main_thread;
+28
View File
@@ -49,6 +49,34 @@ TestObjectStorage::test_validate_keys() {
torrent::raw_string raw_string_4("test_4\0foo", 10);
ASSERT_CATCH_INPUT_ERROR( { m_storage.insert(raw_string_4, torrent::Object("a"), rpc::object_storage::flag_string_type); } );
ASSERT_CATCH_INPUT_ERROR( { m_storage.insert_str("", torrent::Object("a"), rpc::object_storage::flag_string_type); } );
std::string key_max(rpc::object_storage::key_size - 1, 'k');
CPPUNIT_ASSERT(m_storage.insert_str(key_max, torrent::Object("a"), rpc::object_storage::flag_string_type)->first == key_max);
ASSERT_CATCH_INPUT_ERROR( { m_storage.insert_str(key_max + 'k', torrent::Object("a"), rpc::object_storage::flag_string_type); } );
// The over-long key must not have been stored as the empty key.
CPPUNIT_ASSERT(m_storage.find_raw_string(torrent::raw_string::from_c_str("")) == m_storage.end());
// An over-long key must not be truncated into a match either.
CPPUNIT_ASSERT(m_storage.find_raw_string(torrent::raw_string::from_string(key_max + 'k')) == m_storage.end());
// set_c_str puts the terminating nul at m_data[size()], so size() has to
// stay below max_size for that write to land inside the array.
rpc::object_storage::key_type key_long;
key_long.set_c_str(std::string(rpc::object_storage::key_size + 1, 'k').c_str());
CPPUNIT_ASSERT(key_long.size() < rpc::object_storage::key_size);
m_storage.clear();
m_storage.insert_str("test_5", torrent::Object("a"), rpc::object_storage::flag_string_type);
// insert rejects a key holding a nul-char, so a lookup must not match past one.
CPPUNIT_ASSERT(m_storage.find_raw_string(torrent::raw_string("test_5\0x", 8)) == m_storage.end());
CPPUNIT_ASSERT(m_storage.find_raw_string(torrent::raw_string::from_c_str("test_5")) != m_storage.end());
m_storage.clear();
}
// And test many other bad/good string combos.
+21
View File
@@ -0,0 +1,21 @@
#include "config.h"
#include "test/rpc/test_rpc_manager.h"
#include <torrent/exceptions.h>
#include "rpc/scgi_task.h"
CPPUNIT_TEST_SUITE_REGISTRATION(TestRpcManager);
// A size limit too small to hold any request rejects every request, including
// the one that would put it back, so it can only be undone by a restart.
void
TestRpcManager::test_size_limit_bounds() {
CPPUNIT_ASSERT_THROW(m_rpc_manager.set_size_limit(0), torrent::input_error);
CPPUNIT_ASSERT_THROW(m_rpc_manager.set_size_limit(rpc::RpcManager::min_size_limit - 1), torrent::input_error);
CPPUNIT_ASSERT_THROW(m_rpc_manager.set_size_limit(rpc::SCgiTask::max_content_size + 1), torrent::input_error);
CPPUNIT_ASSERT_NO_THROW(m_rpc_manager.set_size_limit(rpc::RpcManager::min_size_limit));
CPPUNIT_ASSERT_NO_THROW(m_rpc_manager.set_size_limit(rpc::SCgiTask::max_content_size));
}
+17
View File
@@ -0,0 +1,17 @@
#include "test/helpers/test_fixture.h"
#include "rpc/rpc_manager.h"
class TestRpcManager : public test_fixture {
CPPUNIT_TEST_SUITE(TestRpcManager);
CPPUNIT_TEST(test_size_limit_bounds);
CPPUNIT_TEST_SUITE_END();
public:
void test_size_limit_bounds();
private:
rpc::RpcManager m_rpc_manager;
};
+28
View File
@@ -8,6 +8,7 @@
#include "globals.h"
#include "command_helpers.h"
#include "rpc/command_map.h"
#include "rpc/scgi_task.h"
CPPUNIT_TEST_SUITE_REGISTRATION(TestXmlrpc);
@@ -21,6 +22,11 @@ xmlrpc_cmd_test_reflect_string([[maybe_unused]] rpc::target_type t, const std::s
return obj;
}
torrent::Object
xmlrpc_cmd_test_oversized([[maybe_unused]] rpc::target_type t, [[maybe_unused]] const torrent::Object& obj) {
return torrent::Object(std::string(rpc::SCgiTask::max_response_size + (1 << 20), 'a'));
}
void initialize_command_dynamic();
#if defined(HAVE_XMLRPC_TINYXML2) && !defined(HAVE_XMLRPC_C)
@@ -127,6 +133,9 @@ TestXmlrpc::setUp() {
if (rpc::commands.find("xmlrpc_reflect_string") == rpc::commands.end())
CMD2_ANY_STRING("xmlrpc_reflect_string", &xmlrpc_cmd_test_reflect_string);
if (rpc::commands.find("xmlrpc_oversized") == rpc::commands.end())
CMD2_ANY("xmlrpc_oversized", &xmlrpc_cmd_test_oversized);
}
void
@@ -165,11 +174,30 @@ TestXmlrpc::test_size_limit() {
CPPUNIT_ASSERT_EQUAL(expected, output);
}
// A command whose result does not fit in the SCGI response buffer must be
// answered with a fault, not handed to the writer. SCgiTask::receive_write
// treats an oversized body as an internal_error, which is not caught by any
// RPC handler and terminates the process.
void
TestXmlrpc::test_response_size_limit() {
std::string input = "<?xml version=\"1.0\"?><methodCall><methodName>xmlrpc_oversized</methodName><params><param><value><string></string></value></param></params></methodCall>";
std::string expected = "<?xml version=\"1.0\"?><methodResponse><fault><value><struct><member><name>faultCode</name><value><i8>-509</i8></value></member><member><name>faultString</name><value><string>Response size exceeds maximum XML-RPC limit</string></value></member></struct></value></fault></methodResponse>";
std::string output;
m_xmlrpc.process(input.c_str(), input.size(), [&output](const char* c, uint32_t l){ output.append(c, l); return true;});
CPPUNIT_ASSERT_MESSAGE("response handed to the writer is " + std::to_string(output.size()) +
" bytes, over the " + std::to_string(rpc::SCgiTask::max_response_size) + " byte SCGI limit",
output.size() <= rpc::SCgiTask::max_response_size);
CPPUNIT_ASSERT_EQUAL(expected, output);
}
#else
void TestXmlrpc::test_invalid_utf8() {}
void TestXmlrpc::test_basics() {}
void TestXmlrpc::test_size_limit() {}
void TestXmlrpc::test_response_size_limit() {}
void TestXmlrpc::setUp() {}
void TestXmlrpc::tearDown() {}
+2
View File
@@ -10,6 +10,7 @@ class TestXmlrpc : public test_fixture {
CPPUNIT_TEST(test_basics);
CPPUNIT_TEST(test_invalid_utf8);
CPPUNIT_TEST(test_size_limit);
CPPUNIT_TEST(test_response_size_limit);
CPPUNIT_TEST_SUITE_END();
@@ -22,6 +23,7 @@ public:
void test_basics();
void test_invalid_utf8();
void test_size_limit();
void test_response_size_limit();
private:
std::unique_ptr<TestMainThread> m_test_main_thread;
+50
View File
@@ -2,6 +2,8 @@
#include "test/src/test_command_dynamic.h"
#include "helpers/assert.h"
#include "control.h"
#include "globals.h"
#include "rpc/parse_commands.h"
@@ -61,3 +63,51 @@ TestCommandDynamic::test_old_style() {
rpc::commands.call_command("method.insert", rpc::create_object_list("test_old_style.4", "simple", "cat=test.3"));
CPPUNIT_ASSERT(rpc::commands.call_command("test_old_style.4", torrent::Object()).as_string() == "test.3");
}
void
TestCommandDynamic::test_insert_list() {
torrent::Object key_only = torrent::Object::create_list();
key_only.as_list().push_back("test_insert_list.1");
rpc::commands.call_command("method.insert.list", key_only);
torrent::Object result = rpc::commands.call_command("test_insert_list.1", torrent::Object());
CPPUNIT_ASSERT(result.is_list());
CPPUNIT_ASSERT(result.as_list().empty());
rpc::commands.call_command("method.insert.list",
rpc::create_object_list("test_insert_list.2", rpc::create_object_list("a", "b")));
torrent::Object filled = rpc::commands.call_command("test_insert_list.2", torrent::Object());
CPPUNIT_ASSERT(filled.is_list());
CPPUNIT_ASSERT_EQUAL((size_t)2, filled.as_list().size());
}
void
TestCommandDynamic::test_value_base() {
auto value = [](std::initializer_list<torrent::Object> objects) {
auto args = torrent::Object::create_list();
for (const auto& object : objects)
args.as_list().push_back(object);
return rpc::commands.call_command("value", args).as_value();
};
CPPUNIT_ASSERT_EQUAL(int64_t(10), value({"10"}));
CPPUNIT_ASSERT_EQUAL(int64_t(255), value({"ff", int64_t(16)}));
// strtoll only defines base 0 and base 2 through 36.
ASSERT_CATCH_INPUT_ERROR( { value({"10", int64_t(1)}); } );
ASSERT_CATCH_INPUT_ERROR( { value({"10", int64_t(37)}); } );
ASSERT_CATCH_INPUT_ERROR( { value({"10", int64_t(-1)}); } );
// An out-of-range base must not be narrowed into a valid one.
ASSERT_CATCH_INPUT_ERROR( { value({"10", int64_t(1) << 40}); } );
ASSERT_CATCH_INPUT_ERROR( { value({"ff", (int64_t(1) << 32) + 16}); } );
// A number too large for the result must be rejected, not clamped.
ASSERT_CATCH_INPUT_ERROR( { value({"99999999999999999999999"}); } );
}
+4
View File
@@ -7,6 +7,8 @@ class TestCommandDynamic : public test_fixture {
CPPUNIT_TEST(test_basics);
CPPUNIT_TEST(test_get_set);
CPPUNIT_TEST(test_old_style);
CPPUNIT_TEST(test_insert_list);
CPPUNIT_TEST(test_value_base);
CPPUNIT_TEST_SUITE_END();
@@ -18,6 +20,8 @@ public:
void test_get_set();
void test_old_style();
void test_insert_list();
void test_value_base();
private:
std::unique_ptr<TestMainThread> m_test_main_thread;
+78
View File
@@ -0,0 +1,78 @@
#include "config.h"
#include "test/src/test_setup.h"
#include <fstream>
#include <string>
#include <unistd.h>
#include <torrent/exceptions.h>
#include "setup.h"
CPPUNIT_TEST_SUITE_REGISTRATION(TestSetup);
// Linking setup.o pulls in the help printer, which lives in src/main.cc.
void
print_help() {}
namespace {
class temp_config_file {
public:
temp_config_file(const std::string& contents) {
char path[] = "/tmp/rtorrent_test_setup_XXXXXX";
CPPUNIT_ASSERT(::mkstemp(path) != -1);
m_path = path;
std::ofstream file(m_path);
file << contents << '\n';
}
~temp_config_file() { ::unlink(m_path.c_str()); }
const std::string& path() const { return m_path; }
private:
std::string m_path;
};
// The group name must be a valid one, else option_find_string throws before
// the output argument is ever touched.
void
assert_arg_count_error(const std::string& line) {
temp_config_file file(line);
try {
parse_config_file_comments(file.path());
} catch (torrent::input_error& e) {
CPPUNIT_ASSERT_EQUAL(std::string("Invalid number of arguments."), std::string(e.what()));
return;
}
CPPUNIT_FAIL("no torrent::input_error thrown for: " + line);
}
} // namespace
void
TestSetup::test_config_comment_log_add_output() {
temp_config_file file("# do:log.add_output=debug,test_output");
CPPUNIT_ASSERT_NO_THROW(parse_config_file_comments(file.path()));
}
void
TestSetup::test_config_comment_log_add_output_no_args() {
assert_arg_count_error("# do:log.add_output=");
}
void
TestSetup::test_config_comment_log_add_output_one_arg() {
assert_arg_count_error("# do:log.add_output=debug");
}
void
TestSetup::test_config_comment_log_add_output_too_many_args() {
assert_arg_count_error("# do:log.add_output=debug,test_output,extra");
}
+18
View File
@@ -0,0 +1,18 @@
#include "test/helpers/test_fixture.h"
class TestSetup : public test_fixture {
CPPUNIT_TEST_SUITE(TestSetup);
CPPUNIT_TEST(test_config_comment_log_add_output);
CPPUNIT_TEST(test_config_comment_log_add_output_no_args);
CPPUNIT_TEST(test_config_comment_log_add_output_one_arg);
CPPUNIT_TEST(test_config_comment_log_add_output_too_many_args);
CPPUNIT_TEST_SUITE_END();
public:
void test_config_comment_log_add_output();
void test_config_comment_log_add_output_no_args();
void test_config_comment_log_add_output_one_arg();
void test_config_comment_log_add_output_too_many_args();
};
+52
View File
@@ -0,0 +1,52 @@
#include "config.h"
#include "test/src/test_ui_download_list.h"
#include "ui/download_list.h"
CPPUNIT_TEST_SUITE_REGISTRATION(TestUiDownloadList);
void
TestUiDownloadList::test_filter_pattern() {
CPPUNIT_ASSERT_EQUAL(std::string(""), ui::create_filter_pattern(""));
CPPUNIT_ASSERT_EQUAL(std::string(".*linux.*"), ui::create_filter_pattern("linux"));
CPPUNIT_ASSERT_EQUAL(std::string("^linux.*"), ui::create_filter_pattern("^linux"));
CPPUNIT_ASSERT_EQUAL(std::string(".*linux$"), ui::create_filter_pattern("linux$"));
CPPUNIT_ASSERT_EQUAL(std::string(".*linux.*"), ui::create_filter_pattern("LiNuX"));
}
void
TestUiDownloadList::test_filter_command() {
torrent::Object command = ui::create_filter_command(".*linux.*");
CPPUNIT_ASSERT(command.is_dict_key());
CPPUNIT_ASSERT_EQUAL(std::string("match"), command.as_dict_key());
const torrent::Object::list_type& args = command.as_dict_obj().as_list();
CPPUNIT_ASSERT_EQUAL((size_t)2, args.size());
CPPUNIT_ASSERT(args.front().is_dict_key());
CPPUNIT_ASSERT_EQUAL(std::string("d.name"), args.front().as_dict_key());
CPPUNIT_ASSERT(args.back().is_string());
CPPUNIT_ASSERT_EQUAL(std::string(".*linux.*"), args.back().as_string());
}
// A pattern that closes the 'match' argument early and appends a second
// command must stay a single inert string argument.
void
TestUiDownloadList::test_filter_command_does_not_inject() {
const std::string pattern = ui::create_filter_pattern("zzz},$d.custom1.set={pwned");
CPPUNIT_ASSERT_EQUAL(std::string(".*zzz},$d.custom1.set={pwned.*"), pattern);
torrent::Object command = ui::create_filter_command(pattern);
CPPUNIT_ASSERT(command.is_dict_key());
CPPUNIT_ASSERT_EQUAL(std::string("match"), command.as_dict_key());
const torrent::Object::list_type& args = command.as_dict_obj().as_list();
CPPUNIT_ASSERT_EQUAL((size_t)2, args.size());
CPPUNIT_ASSERT(args.back().is_string());
CPPUNIT_ASSERT_EQUAL(pattern, args.back().as_string());
}
+16
View File
@@ -0,0 +1,16 @@
#include "test/helpers/test_fixture.h"
class TestUiDownloadList : public test_fixture {
CPPUNIT_TEST_SUITE(TestUiDownloadList);
CPPUNIT_TEST(test_filter_pattern);
CPPUNIT_TEST(test_filter_command);
CPPUNIT_TEST(test_filter_command_does_not_inject);
CPPUNIT_TEST_SUITE_END();
public:
void test_filter_pattern();
void test_filter_command();
void test_filter_command_does_not_inject();
};