mirror of
https://github.com/rakshasa/rtorrent.git
synced 2026-10-05 05:39:22 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6471dc181e | |||
| a5c39566c8 | |||
| 019c16a077 | |||
| 439d23ce62 | |||
| 83b03c2c1e |
+22
-4
@@ -89,6 +89,24 @@ throttle_update(const char* variable, int64_t value) {
|
||||
return torrent::Object();
|
||||
}
|
||||
|
||||
static unsigned int
|
||||
throttle_rate_to_kb(int64_t rate) {
|
||||
if (rate < 0 || rate > std::numeric_limits<unsigned int>::max() - 1)
|
||||
throw torrent::input_error("Throttle rate must be between 0 and 4294967294.");
|
||||
|
||||
return static_cast<unsigned int>(rate >> 10);
|
||||
}
|
||||
|
||||
static void
|
||||
set_up_throttle_i64(ui::Root* root, int64_t rate) {
|
||||
root->set_up_throttle(throttle_rate_to_kb(rate));
|
||||
}
|
||||
|
||||
static void
|
||||
set_down_throttle_i64(ui::Root* root, int64_t rate) {
|
||||
root->set_down_throttle(throttle_rate_to_kb(rate));
|
||||
}
|
||||
|
||||
void
|
||||
initialize_command_throttle() {
|
||||
CMD2_ANY ("throttle.unchoked_uploads", std::bind(&torrent::ResourceManager::currently_upload_unchoked, torrent::resource_manager()));
|
||||
@@ -125,13 +143,13 @@ initialize_command_throttle() {
|
||||
CMD2_ANY ("throttle.global_up.rate", std::bind(&torrent::Rate::rate, torrent::up_rate()));
|
||||
CMD2_ANY ("throttle.global_up.total", std::bind(&torrent::Rate::total, torrent::up_rate()));
|
||||
CMD2_ANY ("throttle.global_up.max_rate", std::bind(&torrent::Throttle::max_rate, torrent::up_throttle_global()));
|
||||
CMD2_ANY_VALUE_V ("throttle.global_up.max_rate.set", std::bind(&ui::Root::set_up_throttle_i64, control->ui(), std::placeholders::_2));
|
||||
CMD2_ANY_VALUE_KB("throttle.global_up.max_rate.set_kb", std::bind(&ui::Root::set_up_throttle_i64, control->ui(), std::placeholders::_2));
|
||||
CMD2_ANY_VALUE_V ("throttle.global_up.max_rate.set", std::bind(&set_up_throttle_i64, control->ui(), std::placeholders::_2));
|
||||
CMD2_ANY_VALUE_KB("throttle.global_up.max_rate.set_kb", std::bind(&set_up_throttle_i64, control->ui(), std::placeholders::_2));
|
||||
CMD2_ANY ("throttle.global_down.rate", std::bind(&torrent::Rate::rate, torrent::down_rate()));
|
||||
CMD2_ANY ("throttle.global_down.total", std::bind(&torrent::Rate::total, torrent::down_rate()));
|
||||
CMD2_ANY ("throttle.global_down.max_rate", std::bind(&torrent::Throttle::max_rate, torrent::down_throttle_global()));
|
||||
CMD2_ANY_VALUE_V ("throttle.global_down.max_rate.set", std::bind(&ui::Root::set_down_throttle_i64, control->ui(), std::placeholders::_2));
|
||||
CMD2_ANY_VALUE_KB("throttle.global_down.max_rate.set_kb", std::bind(&ui::Root::set_down_throttle_i64, control->ui(), std::placeholders::_2));
|
||||
CMD2_ANY_VALUE_V ("throttle.global_down.max_rate.set", std::bind(&set_down_throttle_i64, control->ui(), std::placeholders::_2));
|
||||
CMD2_ANY_VALUE_KB("throttle.global_down.max_rate.set_kb", std::bind(&set_down_throttle_i64, control->ui(), std::placeholders::_2));
|
||||
|
||||
// Temporary names, need to change this to accept real rates rather
|
||||
// than kB.
|
||||
|
||||
@@ -51,6 +51,14 @@ public:
|
||||
bool is_hash_checking() const { return m_download.is_hash_checking(); }
|
||||
|
||||
bool is_hash_failed() const { return m_hashFailed; }
|
||||
|
||||
// Expires once the download is erased, even if other owners keep the
|
||||
// object alive. Take it before triggering events that may erase.
|
||||
std::weak_ptr<void> lifetime() const { return m_lifetime; }
|
||||
void release_lifetime() { m_lifetime.reset(); }
|
||||
|
||||
bool is_erasing() const { return m_erasing; }
|
||||
void set_erasing() { m_erasing = true; }
|
||||
void set_hash_failed(bool v) { m_hashFailed = v; }
|
||||
|
||||
download_type* download() { return &m_download; }
|
||||
@@ -103,6 +111,8 @@ private:
|
||||
// Store the FileList instance so we can use slots etc on it.
|
||||
download_type m_download;
|
||||
bool m_hashFailed{};
|
||||
bool m_erasing{};
|
||||
std::shared_ptr<void> m_lifetime{std::make_shared<char>()};
|
||||
std::string m_message;
|
||||
uint32_t m_resumeFlags{default_resume_flags};
|
||||
unsigned int m_group{};
|
||||
|
||||
@@ -194,6 +194,12 @@ DownloadList::erase(iterator itr) {
|
||||
if (itr == end())
|
||||
throw torrent::internal_error("DownloadList::erase(...) could not find download.");
|
||||
|
||||
// An event handler below may erase the same download again.
|
||||
if ((*itr)->is_erasing())
|
||||
return std::next(itr);
|
||||
|
||||
(*itr)->set_erasing();
|
||||
|
||||
lt_log_print_info(torrent::LOG_TORRENT_INFO, (*itr)->info(), "download_list", "Erasing download.");
|
||||
|
||||
// Makes sure close doesn't restart hashing of this download.
|
||||
@@ -207,6 +213,7 @@ DownloadList::erase(iterator itr) {
|
||||
for (auto v : *control->view_manager())
|
||||
v->erase(itr->get());
|
||||
|
||||
(*itr)->release_lifetime();
|
||||
torrent::download_remove(*(*itr)->download());
|
||||
|
||||
return base_type::erase(itr);
|
||||
@@ -275,6 +282,7 @@ void
|
||||
DownloadList::close_directly(Download* download) {
|
||||
lt_log_print_info(torrent::LOG_TORRENT_INFO, download->info(), "download_list", "Closing download directly.");
|
||||
|
||||
auto lifetime = download->lifetime();
|
||||
bool was_active = download->download()->info()->is_active();
|
||||
bool was_open = download->download()->info()->is_open();
|
||||
|
||||
@@ -283,11 +291,19 @@ DownloadList::close_directly(Download* download) {
|
||||
|
||||
if (was_active) {
|
||||
DL_TRIGGER_EVENT(download, "event.download.paused");
|
||||
|
||||
if (lifetime.expired())
|
||||
return;
|
||||
|
||||
update_paused_state(download);
|
||||
}
|
||||
|
||||
if (was_open) {
|
||||
DL_TRIGGER_EVENT(download, "event.download.hash_removed");
|
||||
|
||||
if (lifetime.expired())
|
||||
return;
|
||||
|
||||
DL_TRIGGER_EVENT(download, "event.download.closed");
|
||||
}
|
||||
}
|
||||
@@ -330,8 +346,13 @@ DownloadList::close_throw(Download* download) {
|
||||
// When pause gets called it will clear the initial hash check state
|
||||
// and set hash failed. This should ensure hashing doesn't restart
|
||||
// until resume gets called.
|
||||
auto lifetime = download->lifetime();
|
||||
|
||||
pause(download);
|
||||
|
||||
if (lifetime.expired())
|
||||
return;
|
||||
|
||||
// Check for is_open after pause due to hashing.
|
||||
if (!download->is_open())
|
||||
return;
|
||||
@@ -351,6 +372,10 @@ DownloadList::close_throw(Download* download) {
|
||||
throw torrent::internal_error("DownloadList::close_throw(...) called but we're going into a hashing loop.");
|
||||
|
||||
DL_TRIGGER_EVENT(download, "event.download.hash_removed");
|
||||
|
||||
if (lifetime.expired())
|
||||
return;
|
||||
|
||||
DL_TRIGGER_EVENT(download, "event.download.closed");
|
||||
}
|
||||
|
||||
@@ -457,6 +482,8 @@ DownloadList::pause(Download* download, int flags) {
|
||||
|
||||
lt_log_print_info(torrent::LOG_TORRENT_INFO, download->info(), "download_list", "Pausing download: flags:%0x.", flags);
|
||||
|
||||
auto lifetime = download->lifetime();
|
||||
|
||||
try {
|
||||
|
||||
download->set_resume_flags(Download::default_resume_flags);
|
||||
@@ -470,6 +497,9 @@ DownloadList::pause(Download* download, int flags) {
|
||||
rpc::call_command_set_value("d.hashing.set", Download::variable_hashing_stopped, rpc::make_target(download));
|
||||
|
||||
DL_TRIGGER_EVENT(download, "event.download.hash_removed");
|
||||
|
||||
if (lifetime.expired())
|
||||
return;
|
||||
}
|
||||
|
||||
if (!download->download()->info()->is_active())
|
||||
@@ -483,6 +513,9 @@ DownloadList::pause(Download* download, int flags) {
|
||||
// view.
|
||||
DL_TRIGGER_EVENT(download, "event.download.paused");
|
||||
|
||||
if (lifetime.expired())
|
||||
return;
|
||||
|
||||
update_paused_state(download);
|
||||
|
||||
// Save the state after all the slots, etc have been called so we
|
||||
@@ -563,9 +596,15 @@ DownloadList::hash_done(Download* download) {
|
||||
rpc::call_command("d.complete.set", (int64_t)download->is_done(), rpc::make_target(download));
|
||||
torrent::resume_save_progress(*download->download(), download->download()->bencode()->get_key("libtorrent_resume"));
|
||||
|
||||
if (rpc::call_command_value("d.state", rpc::make_target(download)) == 1)
|
||||
if (rpc::call_command_value("d.state", rpc::make_target(download)) == 1) {
|
||||
auto lifetime = download->lifetime();
|
||||
|
||||
resume(download, download->resume_flags());
|
||||
|
||||
if (lifetime.expired())
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case Download::variable_hashing_last:
|
||||
@@ -602,11 +641,24 @@ DownloadList::hash_queue(Download* download, int type) {
|
||||
|
||||
// HACK
|
||||
if (download->is_open()) {
|
||||
auto lifetime = download->lifetime();
|
||||
|
||||
pause(download, torrent::Download::stop_skip_tracker);
|
||||
|
||||
if (lifetime.expired())
|
||||
return;
|
||||
|
||||
download->download()->close();
|
||||
|
||||
DL_TRIGGER_EVENT(download, "event.download.hash_removed");
|
||||
|
||||
if (lifetime.expired())
|
||||
return;
|
||||
|
||||
DL_TRIGGER_EVENT(download, "event.download.closed");
|
||||
|
||||
if (lifetime.expired())
|
||||
return;
|
||||
}
|
||||
|
||||
torrent::resume_clear_progress(*download->download(), download->download()->bencode()->get_key("libtorrent_resume"));
|
||||
@@ -682,12 +734,12 @@ DownloadList::confirm_finished(Download* download) {
|
||||
// up/downloaded baseline.
|
||||
download->download()->send_completed();
|
||||
|
||||
// Save the hash in case the finished event erases it.
|
||||
torrent::HashString infohash = download->info()->hash();
|
||||
// The finished event may erase the download.
|
||||
auto lifetime = download->lifetime();
|
||||
|
||||
DL_TRIGGER_EVENT(download, "event.download.finished");
|
||||
|
||||
if (find(infohash) == end())
|
||||
if (lifetime.expired())
|
||||
return;
|
||||
|
||||
// if (download->resume_flags() != Download::default_resume_flags)
|
||||
|
||||
+39
-1
@@ -2,8 +2,10 @@
|
||||
|
||||
#include "rpc/jsonrpc.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <torrent/common.h>
|
||||
#include <torrent/torrent.h>
|
||||
#include <torrent/utils/string_manip.h>
|
||||
@@ -223,13 +225,49 @@ handle_notification(const json& request) noexcept {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using json_input_adapter = decltype(nlohmann::detail::input_adapter(std::declval<const char*>(), std::declval<const char*>()));
|
||||
using json_dom_parser = nlohmann::detail::json_sax_dom_parser<json, json_input_adapter>;
|
||||
|
||||
class json_depth_limited_parser : public json_dom_parser {
|
||||
public:
|
||||
explicit json_depth_limited_parser(json& root) : json_dom_parser(root) {}
|
||||
|
||||
bool start_object(std::size_t length) { return enter() && json_dom_parser::start_object(length); }
|
||||
bool start_array(std::size_t length) { return enter() && json_dom_parser::start_array(length); }
|
||||
|
||||
bool end_object() { m_depth--; return json_dom_parser::end_object(); }
|
||||
bool end_array() { m_depth--; return json_dom_parser::end_array(); }
|
||||
|
||||
private:
|
||||
bool enter() { return ++m_depth <= max_json_depth; }
|
||||
|
||||
uint32_t m_depth{0};
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool
|
||||
JsonRpc::process(const char* in_buffer, uint32_t length, slot_write callback) {
|
||||
json response;
|
||||
json body;
|
||||
|
||||
if (length > m_size_limit) {
|
||||
auto err_str = json_error(JSONRPC_INVALID_REQUEST_ERROR, "content size exceeds maximum RPC limit", nullptr).dump();
|
||||
|
||||
return callback(err_str.c_str(), err_str.size());
|
||||
}
|
||||
|
||||
try {
|
||||
body = json::parse(in_buffer, in_buffer + length);
|
||||
json_depth_limited_parser handler(body);
|
||||
|
||||
if (!json::sax_parse(in_buffer, in_buffer + length, &handler)) {
|
||||
auto err_str = json_error(JSONRPC_INVALID_REQUEST_ERROR, "maximum nesting depth exceeded", nullptr).dump();
|
||||
|
||||
return callback(err_str.c_str(), err_str.size());
|
||||
}
|
||||
|
||||
switch (body.type()) {
|
||||
case json::value_t::object: {
|
||||
if (!body.contains("id")) {
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "rpc/scgi_task.h"
|
||||
|
||||
namespace rpc {
|
||||
|
||||
class JsonRpc {
|
||||
@@ -17,6 +19,11 @@ public:
|
||||
bool process(const char* in_buffer, uint32_t length, slot_write callback);
|
||||
|
||||
void insert_command(const char* name, const char* parm, const char* doc) {};
|
||||
|
||||
void set_size_limit(uint64_t size) { m_size_limit = size; }
|
||||
|
||||
private:
|
||||
uint64_t m_size_limit{SCgiTask::max_content_size};
|
||||
};
|
||||
|
||||
} // namespace rpc
|
||||
|
||||
@@ -176,6 +176,7 @@ RpcManager::set_size_limit(uint64_t size) {
|
||||
throw torrent::input_error("XMLRPC size limit is too small to hold a request.");
|
||||
|
||||
m_xmlrpc.set_size_limit(size);
|
||||
m_jsonrpc.set_size_limit(size);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -326,15 +326,20 @@ process_document(const tinyxml2::XMLDocument* doc, tinyxml2::XMLPrinter* printer
|
||||
auto& result_list = result.as_list();
|
||||
auto parent_elements = element_access(doc->RootElement(), {"params", "param", "value", "array", "data"});
|
||||
for (auto child = parent_elements->FirstChildElement("value"); child; child = child->NextSiblingElement("value")) {
|
||||
auto sub_method_name = element_access(child, {"struct", "member", "value", "string"})->GetText();
|
||||
auto method_name_member = element_access(child, {"struct", "member"});
|
||||
auto member_name = method_name_member->FirstChildElement("name");
|
||||
|
||||
if (member_name == nullptr || member_name->GetText() == nullptr ||
|
||||
std::strncmp(member_name->GetText(), "methodName", sizeof("methodName")) != 0)
|
||||
throw rpc_error(XMLRPC_PARSE_ERROR, "multicall struct's first member must be methodName");
|
||||
|
||||
auto sub_method_name = element_access(method_name_member, {"value", "string"})->GetText();
|
||||
|
||||
if (sub_method_name == nullptr)
|
||||
throw rpc_error(XMLRPC_PARSE_ERROR, "multicall methodName element is empty");
|
||||
// If sub_params ends up a nullptr at the end of this if-chian,
|
||||
// execute_command will turn it into an empty list
|
||||
auto sub_params = element_access(child, {"struct", "member"});
|
||||
if (sub_params != nullptr)
|
||||
sub_params = sub_params->NextSiblingElement("member");
|
||||
auto sub_params = method_name_member->NextSiblingElement("member");
|
||||
if (sub_params != nullptr)
|
||||
sub_params = sub_params->FirstChildElement("value");
|
||||
if (sub_params != nullptr)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "download_storer.h"
|
||||
|
||||
#include <cerrno>
|
||||
#include <fcntl.h>
|
||||
#include <fstream>
|
||||
#include <unistd.h>
|
||||
@@ -116,28 +117,36 @@ is_correct_format(const std::string& f) {
|
||||
|
||||
void
|
||||
save_stream(const std::string& path, bool use_fsyncdisk, const std::stringstream& stream) {
|
||||
std::fstream output(path.c_str(), std::ios::out | std::ios::trunc);
|
||||
// Remove any leftover temporary file first so that O_EXCL only ever fails on
|
||||
// an entry that appeared after the unlink, and O_NOFOLLOW keeps a symlink
|
||||
// planted in the session directory from redirecting the write.
|
||||
if (::unlink(path.c_str()) == -1 && errno != ENOENT)
|
||||
throw torrent::storage_error("failed to remove stale file : " + path);
|
||||
|
||||
// TODO: If we cannot open more files, wait for some to finish and try again.
|
||||
if (!output.is_open())
|
||||
throw torrent::storage_error("failed to open file for writing : " + path);
|
||||
|
||||
output << stream.rdbuf();
|
||||
|
||||
if (!output.good())
|
||||
throw torrent::storage_error("failed to write stream to file : " + path);
|
||||
|
||||
// The data only reaches the kernel here, so this is where a full disk is seen.
|
||||
output.close();
|
||||
|
||||
if (!output.good())
|
||||
throw torrent::storage_error("failed to flush stream to file : " + path);
|
||||
|
||||
// Ensure that the new file is actually written to the disk
|
||||
int fd = ::open(path.c_str(), O_WRONLY);
|
||||
int fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0600);
|
||||
|
||||
if (fd < 0)
|
||||
throw torrent::storage_error("failed to open file descriptor for fsync : " + path);
|
||||
throw torrent::storage_error("failed to open file for writing : " + path);
|
||||
|
||||
const auto data = stream.view();
|
||||
std::size_t remaining = data.size();
|
||||
const char* cursor = data.data();
|
||||
|
||||
while (remaining != 0) {
|
||||
ssize_t result = ::write(fd, cursor, remaining);
|
||||
|
||||
if (result == -1) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
|
||||
::close(fd);
|
||||
throw torrent::storage_error("failed to write stream to file : " + path);
|
||||
}
|
||||
|
||||
cursor += result;
|
||||
remaining -= result;
|
||||
}
|
||||
|
||||
if (use_fsyncdisk) {
|
||||
#ifdef __APPLE__
|
||||
@@ -152,6 +161,7 @@ save_stream(const std::string& path, bool use_fsyncdisk, const std::stringstream
|
||||
}
|
||||
}
|
||||
|
||||
// A full disk may only be seen when the descriptor is closed.
|
||||
if (::close(fd) == -1)
|
||||
throw torrent::storage_error("failed to close file descriptor : " + path);
|
||||
}
|
||||
|
||||
@@ -70,10 +70,6 @@ public:
|
||||
void set_down_throttle(unsigned int throttle);
|
||||
void set_up_throttle(unsigned int throttle);
|
||||
|
||||
// Rename to raw or something, make base function.
|
||||
void set_down_throttle_i64(int64_t throttle) { set_down_throttle(throttle >> 10); }
|
||||
void set_up_throttle_i64(int64_t throttle) { set_up_throttle(throttle >> 10); }
|
||||
|
||||
void adjust_down_throttle(int throttle);
|
||||
void adjust_up_throttle(int throttle);
|
||||
|
||||
|
||||
+27
-5
@@ -5,6 +5,7 @@
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <functional>
|
||||
#include <sys/stat.h>
|
||||
#include <torrent/exceptions.h>
|
||||
@@ -13,6 +14,24 @@
|
||||
|
||||
namespace utils {
|
||||
|
||||
namespace {
|
||||
|
||||
uint8_t
|
||||
entry_type_from_mode(mode_t mode) {
|
||||
if (S_ISREG(mode))
|
||||
return DT_REG;
|
||||
|
||||
if (S_ISDIR(mode))
|
||||
return DT_DIR;
|
||||
|
||||
if (S_ISLNK(mode))
|
||||
return DT_LNK;
|
||||
|
||||
return DT_UNKNOWN;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Keep this?
|
||||
bool
|
||||
Directory::is_valid() const {
|
||||
@@ -38,9 +57,6 @@ Directory::update(int flags) {
|
||||
return false;
|
||||
|
||||
struct dirent* entry;
|
||||
#ifdef __sun__
|
||||
struct stat s;
|
||||
#endif
|
||||
|
||||
while ((entry = readdir(d)) != NULL) {
|
||||
if ((flags & update_hide_dot) && entry->d_name[0] == '.')
|
||||
@@ -49,16 +65,22 @@ Directory::update(int flags) {
|
||||
iterator itr = base_type::insert(end(), value_type());
|
||||
|
||||
#ifdef __sun__
|
||||
stat(entry->d_name, &s);
|
||||
itr->s_fileno = entry->d_ino;
|
||||
itr->s_reclen = 0;
|
||||
itr->s_type = s.st_mode;
|
||||
itr->s_type = DT_UNKNOWN;
|
||||
#else
|
||||
itr->s_fileno = entry->d_fileno;
|
||||
itr->s_reclen = entry->d_reclen;
|
||||
itr->s_type = entry->d_type;
|
||||
#endif
|
||||
|
||||
if (itr->s_type == DT_UNKNOWN) {
|
||||
struct stat st;
|
||||
|
||||
if (fstatat(dirfd(d), entry->d_name, &st, AT_SYMLINK_NOFOLLOW) == 0)
|
||||
itr->s_type = entry_type_from_mode(st.st_mode);
|
||||
}
|
||||
|
||||
#ifdef DIRENT_NAMLEN_EXISTS_FOOBAR
|
||||
itr->s_name = std::string(entry->d_name, entry->d_name + entry->d_namlen);
|
||||
#else
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
#define RTORRENT_UTILS_DIRECTORY_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <dirent.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace utils {
|
||||
|
||||
struct directory_entry {
|
||||
// Fix.
|
||||
bool is_file() const { return true; }
|
||||
bool is_file() const { return s_type == DT_REG; }
|
||||
|
||||
// The name and types should match POSIX.
|
||||
uint32_t s_fileno;
|
||||
|
||||
@@ -67,6 +67,10 @@ rtorrent_Test_Src_SOURCES = $(rtorrent_Test_Common) \
|
||||
src/test_command_path.h \
|
||||
src/test_command_string.cc \
|
||||
src/test_command_string.h \
|
||||
src/test_command_throttle.cc \
|
||||
src/test_command_throttle.h \
|
||||
src/test_session_storer.cc \
|
||||
src/test_session_storer.h \
|
||||
src/test_setup.cc \
|
||||
src/test_setup.h \
|
||||
src/test_ui_download_list.cc \
|
||||
|
||||
@@ -173,3 +173,82 @@ TestJsonrpc::test_response_size_limit() {
|
||||
output.size() <= rpc::SCgiTask::max_response_size);
|
||||
CPPUNIT_ASSERT_EQUAL(expected, output);
|
||||
}
|
||||
|
||||
// The bound is applied while the document is parsed: json_to_object only ever
|
||||
// walks "params", and by the time it runs the whole tree already exists.
|
||||
void
|
||||
TestJsonrpc::test_depth_limit() {
|
||||
// A JSON string holding a quote and brackets that must not be counted.
|
||||
const std::string tricky = R"("\"[[[")";
|
||||
|
||||
std::vector<std::tuple<std::string, std::string, std::string>> requests = {
|
||||
std::make_tuple("Nesting under the limit is accepted",
|
||||
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", )" +
|
||||
std::string(1000, '[') + std::string(1000, ']') + R"(], "id": 1})",
|
||||
R"({"id":1,"jsonrpc":"2.0","result":[)" +
|
||||
std::string(1000, '[') + std::string(1000, ']') + R"(]})"),
|
||||
|
||||
// Nesting outside "params" is never converted, so json_to_object's own
|
||||
// bound never sees it.
|
||||
std::make_tuple("Nesting outside params is rejected",
|
||||
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": [""], "id": 1, "x": )" +
|
||||
std::string(2000, '[') + std::string(2000, ']') + R"(})",
|
||||
R"({"error":{"code":-32600,"message":"maximum nesting depth exceeded"},"id":null,"jsonrpc":"2.0"})"),
|
||||
|
||||
std::make_tuple("Nesting over the limit is rejected",
|
||||
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", )" +
|
||||
std::string(2000, '[') + std::string(2000, ']') + R"(], "id": 1})",
|
||||
R"({"error":{"code":-32600,"message":"maximum nesting depth exceeded"},"id":null,"jsonrpc":"2.0"})"),
|
||||
|
||||
std::make_tuple("Brackets inside a string are not nesting",
|
||||
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", ")" +
|
||||
std::string(2000, '[') + R"("], "id": 1})",
|
||||
R"({"id":1,"jsonrpc":"2.0","result":[")" +
|
||||
std::string(2000, '[') + R"("]})"),
|
||||
|
||||
std::make_tuple("An escaped quote does not end a string",
|
||||
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", )" + tricky + R"(, ")" +
|
||||
std::string(2000, '[') + R"("], "id": 1})",
|
||||
R"({"id":1,"jsonrpc":"2.0","result":[)" + tricky + R"(,")" +
|
||||
std::string(2000, '[') + R"("]})"),
|
||||
|
||||
// The bound is on the whole document, so the outer object and the params
|
||||
// array are two of the 1024 containers and 1022 are left for the payload.
|
||||
std::make_tuple("Nesting one below the limit is accepted",
|
||||
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", )" +
|
||||
std::string(1021, '[') + std::string(1021, ']') + R"(], "id": 1})",
|
||||
R"({"id":1,"jsonrpc":"2.0","result":[)" +
|
||||
std::string(1021, '[') + std::string(1021, ']') + R"(]})"),
|
||||
|
||||
std::make_tuple("Nesting at the limit is accepted",
|
||||
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", )" +
|
||||
std::string(1022, '[') + std::string(1022, ']') + R"(], "id": 1})",
|
||||
R"({"id":1,"jsonrpc":"2.0","result":[)" +
|
||||
std::string(1022, '[') + std::string(1022, ']') + R"(]})"),
|
||||
|
||||
std::make_tuple("Nesting one over the limit is rejected",
|
||||
R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": ["", )" +
|
||||
std::string(1023, '[') + std::string(1023, ']') + R"(], "id": 1})",
|
||||
R"({"error":{"code":-32600,"message":"maximum nesting depth exceeded"},"id":null,"jsonrpc":"2.0"})"),
|
||||
};
|
||||
|
||||
for (auto& test : 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);
|
||||
}
|
||||
}
|
||||
|
||||
// network.xmlrpc.size_limit is the only knob bounding how much input a single
|
||||
// request may spend memory on, and it has to bound the JSON path too.
|
||||
void
|
||||
TestJsonrpc::test_size_limit() {
|
||||
const std::string request = R"({"jsonrpc": "2.0", "method": "jsonrpc_reflect", "params": [""], "id": 1})";
|
||||
const std::string expected = R"({"error":{"code":-32600,"message":"content size exceeds maximum RPC limit"},"id":null,"jsonrpc":"2.0"})";
|
||||
|
||||
std::string output;
|
||||
m_jsonrpc.set_size_limit(1);
|
||||
m_jsonrpc.process(request.c_str(), request.size(), [&output](const char* c, uint32_t l) { output.append(c, l); return true; });
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(expected, output);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ class TestJsonrpc : public test_fixture {
|
||||
|
||||
CPPUNIT_TEST(test_basics);
|
||||
CPPUNIT_TEST(test_response_size_limit);
|
||||
CPPUNIT_TEST(test_depth_limit);
|
||||
CPPUNIT_TEST(test_size_limit);
|
||||
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
@@ -18,6 +20,8 @@ public:
|
||||
|
||||
void test_basics();
|
||||
void test_response_size_limit();
|
||||
void test_depth_limit();
|
||||
void test_size_limit();
|
||||
|
||||
private:
|
||||
std::unique_ptr<TestMainThread> m_test_main_thread;
|
||||
|
||||
@@ -192,12 +192,54 @@ TestXmlrpc::test_response_size_limit() {
|
||||
CPPUNIT_ASSERT_EQUAL(expected, output);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
const std::string multicall_method_name =
|
||||
"<member><name>methodName</name><value><string>xmlrpc_reflect</string></value></member>";
|
||||
const std::string multicall_params =
|
||||
"<member><name>params</name><value><array><data>"
|
||||
"<value><string></string></value><value><string>a</string></value>"
|
||||
"</data></array></value></member>";
|
||||
|
||||
std::string
|
||||
multicall_request(const std::string& members) {
|
||||
return "<?xml version=\"1.0\"?><methodCall><methodName>system.multicall</methodName>"
|
||||
"<params><param><value><array><data><value><struct>" + members +
|
||||
"</struct></value></data></array></value></param></params></methodCall>";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void
|
||||
TestXmlrpc::test_multicall_member_order() {
|
||||
auto call = [this](const std::string& input) {
|
||||
std::string output;
|
||||
m_xmlrpc.process(input.c_str(), input.size(), [&output](const char* c, uint32_t l){ output.append(c, l); return true;});
|
||||
return output;
|
||||
};
|
||||
|
||||
// methodName first is the only order accepted; the positive control proves
|
||||
// the harness drives the real code path rather than a stub.
|
||||
std::string ordered = call(multicall_request(multicall_method_name + multicall_params));
|
||||
CPPUNIT_ASSERT(ordered.find("faultCode") == std::string::npos);
|
||||
|
||||
// params before methodName is rejected with a clear top-level fault,
|
||||
// instead of the "could not find expected element string" of a positional read.
|
||||
std::string expected_fault =
|
||||
"<?xml version=\"1.0\"?><methodResponse><fault><value><struct>"
|
||||
"<member><name>faultCode</name><value><i8>-503</i8></value></member>"
|
||||
"<member><name>faultString</name><value><string>multicall struct's first member must be methodName</string></value></member>"
|
||||
"</struct></value></fault></methodResponse>";
|
||||
CPPUNIT_ASSERT_EQUAL(expected_fault, call(multicall_request(multicall_params + multicall_method_name)));
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
void TestXmlrpc::test_invalid_utf8() {}
|
||||
void TestXmlrpc::test_basics() {}
|
||||
void TestXmlrpc::test_size_limit() {}
|
||||
void TestXmlrpc::test_response_size_limit() {}
|
||||
void TestXmlrpc::test_multicall_member_order() {}
|
||||
void TestXmlrpc::setUp() {}
|
||||
void TestXmlrpc::tearDown() {}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ class TestXmlrpc : public test_fixture {
|
||||
CPPUNIT_TEST(test_invalid_utf8);
|
||||
CPPUNIT_TEST(test_size_limit);
|
||||
CPPUNIT_TEST(test_response_size_limit);
|
||||
CPPUNIT_TEST(test_multicall_member_order);
|
||||
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
@@ -24,6 +25,7 @@ public:
|
||||
void test_invalid_utf8();
|
||||
void test_size_limit();
|
||||
void test_response_size_limit();
|
||||
void test_multicall_member_order();
|
||||
|
||||
private:
|
||||
std::unique_ptr<TestMainThread> m_test_main_thread;
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
#include "config.h"
|
||||
|
||||
#include "test/src/test_command_throttle.h"
|
||||
|
||||
#include <torrent/throttle.h>
|
||||
#include <torrent/torrent.h>
|
||||
|
||||
#include "core/manager.h"
|
||||
#include "control.h"
|
||||
#include "globals.h"
|
||||
#include "rpc/parse_commands.h"
|
||||
|
||||
CPPUNIT_TEST_SUITE_REGISTRATION(TestCommandThrottle);
|
||||
|
||||
void initialize_command_throttle();
|
||||
|
||||
static void
|
||||
call_set(const char* key, const char* value) {
|
||||
rpc::commands.call_command(key, torrent::Object(std::string(value)));
|
||||
}
|
||||
|
||||
static void
|
||||
call_named(const char* key, const char* name, const char* value) {
|
||||
torrent::Object::list_type args;
|
||||
args.push_back(torrent::Object(std::string(name)));
|
||||
args.push_back(torrent::Object(std::string(value)));
|
||||
|
||||
rpc::commands.call_command(key, torrent::Object::create_list_range(args.begin(), args.end()));
|
||||
}
|
||||
|
||||
static uint64_t
|
||||
down_rate() {
|
||||
return torrent::down_throttle_global()->max_rate();
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandThrottle::setUp() {
|
||||
torrent::initialize_main_thread();
|
||||
torrent::initialize();
|
||||
|
||||
if (control == nullptr)
|
||||
control = new Control;
|
||||
|
||||
if (!rpc::commands.has("throttle.global_down.max_rate.set_kb"))
|
||||
initialize_command_throttle();
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandThrottle::tearDown() {
|
||||
torrent::cleanup();
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandThrottle::test_global_rate_in_range() {
|
||||
call_set("throttle.global_down.max_rate.set_kb", "1024");
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t{1048576}, down_rate());
|
||||
|
||||
call_set("throttle.global_down.max_rate.set_kb", "4194303");
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t{4294966272}, down_rate());
|
||||
|
||||
call_set("throttle.global_down.max_rate.set", "4294966272");
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t{4294966272}, down_rate());
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandThrottle::test_global_rate_kb_out_of_range() {
|
||||
call_set("throttle.global_down.max_rate.set_kb", "1024");
|
||||
|
||||
CPPUNIT_ASSERT_THROW(call_set("throttle.global_down.max_rate.set_kb", "4194304"), torrent::input_error);
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t{1048576}, down_rate());
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandThrottle::test_global_rate_bytes_out_of_range() {
|
||||
call_set("throttle.global_down.max_rate.set", "1048576");
|
||||
|
||||
CPPUNIT_ASSERT_THROW(call_set("throttle.global_down.max_rate.set", "4294967296"), torrent::input_error);
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t{1048576}, down_rate());
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandThrottle::test_global_rate_negative() {
|
||||
call_set("throttle.global_down.max_rate.set", "1048576");
|
||||
|
||||
CPPUNIT_ASSERT_THROW(call_set("throttle.global_down.max_rate.set", "-1"), torrent::input_error);
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t{1048576}, down_rate());
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandThrottle::test_named_rate_in_range() {
|
||||
call_named("throttle.down", "test_named_in_range", "1024");
|
||||
|
||||
auto itr = control->core()->throttles().find("test_named_in_range");
|
||||
|
||||
CPPUNIT_ASSERT(itr != control->core()->throttles().end());
|
||||
CPPUNIT_ASSERT_EQUAL(uint64_t{1048576}, itr->second.second->max_rate());
|
||||
}
|
||||
|
||||
void
|
||||
TestCommandThrottle::test_named_rate_out_of_range() {
|
||||
CPPUNIT_ASSERT_THROW(call_named("throttle.down", "test_named_out_of_range", "18014398509481984"), torrent::input_error);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#include "test/helpers/test_fixture.h"
|
||||
|
||||
class TestCommandThrottle : public test_fixture {
|
||||
CPPUNIT_TEST_SUITE(TestCommandThrottle);
|
||||
|
||||
CPPUNIT_TEST(test_global_rate_in_range);
|
||||
CPPUNIT_TEST(test_global_rate_kb_out_of_range);
|
||||
CPPUNIT_TEST(test_global_rate_bytes_out_of_range);
|
||||
CPPUNIT_TEST(test_global_rate_negative);
|
||||
CPPUNIT_TEST(test_named_rate_in_range);
|
||||
CPPUNIT_TEST(test_named_rate_out_of_range);
|
||||
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
public:
|
||||
void setUp();
|
||||
void tearDown();
|
||||
|
||||
void test_global_rate_in_range();
|
||||
void test_global_rate_kb_out_of_range();
|
||||
void test_global_rate_bytes_out_of_range();
|
||||
void test_global_rate_negative();
|
||||
void test_named_rate_in_range();
|
||||
void test_named_rate_out_of_range();
|
||||
};
|
||||
@@ -0,0 +1,152 @@
|
||||
#include "config.h"
|
||||
|
||||
#include "test/src/test_session_storer.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <dirent.h>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "session/download_storer.h"
|
||||
#include "utils/directory.h"
|
||||
|
||||
CPPUNIT_TEST_SUITE_REGISTRATION(TestSessionStorer);
|
||||
|
||||
namespace {
|
||||
|
||||
const char* entry_name = "0123456789ABCDEF0123456789ABCDEF01234567.torrent";
|
||||
const char* link_name = "FEDCBA9876543210FEDCBA9876543210FEDCBA98.torrent";
|
||||
|
||||
void
|
||||
write_file(const std::string& path, const std::string& content) {
|
||||
std::ofstream file(path.c_str());
|
||||
|
||||
file << content;
|
||||
file.close();
|
||||
|
||||
CPPUNIT_ASSERT(file.good());
|
||||
}
|
||||
|
||||
std::string
|
||||
read_file(const std::string& path) {
|
||||
std::ifstream file(path.c_str());
|
||||
std::stringstream buffer;
|
||||
|
||||
buffer << file.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
void
|
||||
save_session_files(const std::string& path) {
|
||||
std::stringstream torrent_stream("torrent-data");
|
||||
std::stringstream rtorrent_stream("rtorrent-data");
|
||||
std::stringstream libtorrent_stream("libtorrent-data");
|
||||
|
||||
session::DownloadStorer::save_and_move_streams(path, false, &torrent_stream, &rtorrent_stream, &libtorrent_stream);
|
||||
}
|
||||
|
||||
unsigned int
|
||||
permissions_of(const std::string& path) {
|
||||
struct stat st;
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(0, ::stat(path.c_str(), &st));
|
||||
return st.st_mode & 07777;
|
||||
}
|
||||
|
||||
void
|
||||
remove_directory(const std::string& path) {
|
||||
DIR* d = ::opendir(path.c_str());
|
||||
|
||||
if (d == NULL)
|
||||
return;
|
||||
|
||||
struct dirent* entry;
|
||||
|
||||
while ((entry = ::readdir(d)) != NULL) {
|
||||
if (entry->d_name[0] == '.' && (entry->d_name[1] == '\0' || (entry->d_name[1] == '.' && entry->d_name[2] == '\0')))
|
||||
continue;
|
||||
|
||||
::unlink((path + "/" + entry->d_name).c_str());
|
||||
}
|
||||
|
||||
::closedir(d);
|
||||
::rmdir(path.c_str());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void
|
||||
TestSessionStorer::setUp() {
|
||||
test_fixture::setUp();
|
||||
|
||||
char temp_dir[] = "/tmp/rtorrent_test_session_XXXXXX";
|
||||
|
||||
CPPUNIT_ASSERT(mkdtemp(temp_dir) != nullptr);
|
||||
|
||||
m_temp_dir = temp_dir;
|
||||
m_session_dir = m_temp_dir + "/session";
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(0, ::mkdir(m_session_dir.c_str(), 0755));
|
||||
}
|
||||
|
||||
void
|
||||
TestSessionStorer::tearDown() {
|
||||
remove_directory(m_session_dir);
|
||||
remove_directory(m_temp_dir);
|
||||
|
||||
test_fixture::tearDown();
|
||||
}
|
||||
|
||||
// A symlink planted where the next temporary session file will be written must
|
||||
// not redirect the write to the file it points at.
|
||||
void
|
||||
TestSessionStorer::test_temp_file_symlink_is_not_followed() {
|
||||
auto outside = m_temp_dir + "/outside.txt";
|
||||
auto path = m_session_dir + "/" + entry_name;
|
||||
|
||||
write_file(outside, "original");
|
||||
CPPUNIT_ASSERT_EQUAL(0, ::symlink(outside.c_str(), (path + ".new").c_str()));
|
||||
|
||||
save_session_files(path);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("original"), read_file(outside));
|
||||
CPPUNIT_ASSERT_EQUAL(std::string("torrent-data"), read_file(path));
|
||||
}
|
||||
|
||||
// Session files carry tracker announce urls, so they must not be readable by
|
||||
// other users regardless of the umask rtorrent was started with.
|
||||
void
|
||||
TestSessionStorer::test_saved_files_are_owner_only() {
|
||||
auto path = m_session_dir + "/" + entry_name;
|
||||
auto prev_umask = ::umask(0);
|
||||
|
||||
save_session_files(path);
|
||||
::umask(prev_umask);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(0600u, permissions_of(path));
|
||||
CPPUNIT_ASSERT_EQUAL(0600u, permissions_of(path + ".rtorrent"));
|
||||
CPPUNIT_ASSERT_EQUAL(0600u, permissions_of(path + ".libtorrent_resume"));
|
||||
}
|
||||
|
||||
// Entries listed for loading must report their real type so that a symlink in
|
||||
// the session directory is skipped instead of loaded.
|
||||
void
|
||||
TestSessionStorer::test_symlinked_entry_is_not_a_file() {
|
||||
write_file(m_temp_dir + "/outside.txt", "d0:e");
|
||||
write_file(m_session_dir + "/" + entry_name, "d0:e");
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(0, ::symlink((m_temp_dir + "/outside.txt").c_str(), (m_session_dir + "/" + link_name).c_str()));
|
||||
|
||||
auto entries = session::DownloadStorer::get_formated_entries(m_session_dir + "/");
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(size_t{2}, size_t{entries.size()});
|
||||
|
||||
for (const auto& entry : entries) {
|
||||
if (entry.s_name == entry_name)
|
||||
CPPUNIT_ASSERT(entry.is_file());
|
||||
else
|
||||
CPPUNIT_ASSERT(!entry.is_file());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#include "test/helpers/test_fixture.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
class TestSessionStorer : public test_fixture {
|
||||
CPPUNIT_TEST_SUITE(TestSessionStorer);
|
||||
|
||||
CPPUNIT_TEST(test_temp_file_symlink_is_not_followed);
|
||||
CPPUNIT_TEST(test_saved_files_are_owner_only);
|
||||
CPPUNIT_TEST(test_symlinked_entry_is_not_a_file);
|
||||
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
public:
|
||||
void setUp();
|
||||
void tearDown();
|
||||
|
||||
void test_temp_file_symlink_is_not_followed();
|
||||
void test_saved_files_are_owner_only();
|
||||
void test_symlinked_entry_is_not_a_file();
|
||||
|
||||
private:
|
||||
std::string m_temp_dir;
|
||||
std::string m_session_dir;
|
||||
};
|
||||
Reference in New Issue
Block a user