diff --git a/src/command_helpers.h b/src/command_helpers.h index 07d06eda..faecda15 100644 --- a/src/command_helpers.h +++ b/src/command_helpers.h @@ -19,6 +19,11 @@ void initialize_commands(); rpc::commands.insert_slot::type>(key, slot, &rpc::function, \ rpc::CommandMap::flag_dont_delete, NULL, NULL); +#define CMD2_A_FUNCTION_U(key, function, slot, parm, doc) \ + rpc::commands.insert_slot::type>(key, slot, &rpc::function, \ + rpc::CommandMap::flag_dont_delete | rpc::CommandMap::flag_public_rpc | \ + rpc::CommandMap::flag_untrusted_safe, NULL, NULL); + #define CMD2_ANY(key, slot) CMD2_A_FUNCTION(key, command_base_call, slot, "i:", "") #define CMD2_ANY_P(key, slot) CMD2_A_FUNCTION_PRIVATE(key, command_base_call, slot, "i:", "") @@ -35,6 +40,42 @@ void initialize_commands(); #define CMD2_ANY_LIST(key, slot) CMD2_A_FUNCTION(key, command_base_call_list, slot, "i:", "") +#define CMD2_ANY_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call, slot, "i:", "") +#define CMD2_ANY_VOID_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call, object_convert_void(slot), "i:", "") +#define CMD2_ANY_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_list, object_convert_void(slot), "i:", "") +#define CMD2_ANY_L_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_list, slot, "A:", "") + +#define CMD2_ANY_VALUE_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_value, slot, "i:i", "") +#define CMD2_ANY_VALUE_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_value, object_convert_void(slot), "i:i", "") +#define CMD2_ANY_VALUE_KB_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_value_kb, object_convert_void(slot), "i:i", "") + +#define CMD2_ANY_STRING_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_string, slot, "i:s", "") +#define CMD2_ANY_STRING_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_string, object_convert_void(slot), "i:s", "") + +#define CMD2_ANY_LIST_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_list, slot, "i:", "") + +#define CMD2_DL_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call, slot, "i:", "") +#define CMD2_DL_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call, object_convert_void(slot), "i:", "") +#define CMD2_DL_VALUE_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_value, slot, "i:", "") +#define CMD2_DL_VALUE_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_value, object_convert_void(slot), "i:", "") +#define CMD2_DL_STRING_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_string, slot, "i:", "") +#define CMD2_DL_STRING_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_string, object_convert_void(slot), "i:", "") +#define CMD2_DL_LIST_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_list, slot, "i:", "") + +#define CMD2_FILE_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call, slot, "i:", "") +#define CMD2_FILE_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call, object_convert_void(slot), "i:", "") +#define CMD2_FILE_VALUE_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_value, object_convert_void(slot), "i:i", "") + +#define CMD2_FILEITR_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call, slot, "i:", "") + +#define CMD2_PEER_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call, slot, "i:", "") +#define CMD2_PEER_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call, object_convert_void(slot), "i:", "") +#define CMD2_PEER_VALUE_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_value, object_convert_void(slot), "i:i", "") + +#define CMD2_TRACKER_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call, slot, "i:", "") +#define CMD2_TRACKER_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call, object_convert_void(slot), "i:", "") +#define CMD2_TRACKER_VALUE_V_U(key, slot) CMD2_A_FUNCTION_U(key, command_base_call_value, object_convert_void(slot), "i:i", "") + #define CMD2_DL(key, slot) CMD2_A_FUNCTION(key, command_base_call, slot, "i:", "") #define CMD2_DL_V(key, slot) CMD2_A_FUNCTION(key, command_base_call, object_convert_void(slot), "i:", "") #define CMD2_DL_VALUE(key, slot) CMD2_A_FUNCTION(key, command_base_call_value, slot, "i:", "") @@ -96,6 +137,71 @@ void initialize_commands(); CMD2_ANY_VOID(key ".push_back", std::bind(&rpc::object_storage::list_push_back, control->object_storage(), \ torrent::raw_string::from_c_str(key), std::placeholders::_2)); +#define CMD2_VAR_BOOL_U(key, value) \ + control->object_storage()->insert_c_str(key, int64_t(value), rpc::object_storage::flag_bool_type); \ + CMD2_ANY_U(key, std::bind(&rpc::object_storage::get, control->object_storage(), \ + torrent::raw_string::from_c_str(key))); \ + CMD2_ANY_VALUE_U(key ".set", std::bind(&rpc::object_storage::set_bool, control->object_storage(), \ + torrent::raw_string::from_c_str(key), std::placeholders::_2)); + +#define CMD2_VAR_BOOL_U_GET(key, value) \ + control->object_storage()->insert_c_str(key, int64_t(value), rpc::object_storage::flag_bool_type); \ + CMD2_ANY_U(key, std::bind(&rpc::object_storage::get, control->object_storage(), \ + torrent::raw_string::from_c_str(key))); \ + CMD2_ANY_VALUE(key ".set", std::bind(&rpc::object_storage::set_bool, control->object_storage(), \ + torrent::raw_string::from_c_str(key), std::placeholders::_2)); + +#define CMD2_VAR_VALUE_U(key, value) \ + control->object_storage()->insert_c_str(key, int64_t(value), rpc::object_storage::flag_value_type); \ + CMD2_ANY_U(key, std::bind(&rpc::object_storage::get, control->object_storage(), \ + torrent::raw_string::from_c_str(key))); \ + CMD2_ANY_VALUE_U(key ".set", std::bind(&rpc::object_storage::set_value, control->object_storage(), \ + torrent::raw_string::from_c_str(key), std::placeholders::_2)); + +#define CMD2_VAR_VALUE_U_GET(key, value) \ + control->object_storage()->insert_c_str(key, int64_t(value), rpc::object_storage::flag_value_type); \ + CMD2_ANY_U(key, std::bind(&rpc::object_storage::get, control->object_storage(), \ + torrent::raw_string::from_c_str(key))); \ + CMD2_ANY_VALUE(key ".set", std::bind(&rpc::object_storage::set_value, control->object_storage(), \ + torrent::raw_string::from_c_str(key), std::placeholders::_2)); + +#define CMD2_VAR_STRING_U(key, value) \ + control->object_storage()->insert_c_str(key, value, rpc::object_storage::flag_string_type); \ + CMD2_ANY_U(key, std::bind(&rpc::object_storage::get, control->object_storage(), \ + torrent::raw_string::from_c_str(key))); \ + CMD2_ANY_STRING_U(key ".set", std::bind(&rpc::object_storage::set_string, control->object_storage(), \ + torrent::raw_string::from_c_str(key), std::placeholders::_2)); + +#define CMD2_VAR_STRING_U_GET(key, value) \ + control->object_storage()->insert_c_str(key, value, rpc::object_storage::flag_string_type); \ + CMD2_ANY_U(key, std::bind(&rpc::object_storage::get, control->object_storage(), \ + torrent::raw_string::from_c_str(key))); \ + CMD2_ANY_STRING(key ".set", std::bind(&rpc::object_storage::set_string, control->object_storage(), \ + torrent::raw_string::from_c_str(key), std::placeholders::_2)); + +#define CMD2_VAR_C_STRING_U(key, value) \ + control->object_storage()->insert_c_str(key, value, rpc::object_storage::flag_string_type); \ + CMD2_ANY_U(key, std::bind(&rpc::object_storage::get, control->object_storage(), \ + torrent::raw_string::from_c_str(key))); + +#define CMD2_VAR_LIST_U(key) \ + control->object_storage()->insert_c_str(key, torrent::Object::create_list(), rpc::object_storage::flag_list_type); \ + CMD2_ANY_U(key, std::bind(&rpc::object_storage::get, control->object_storage(), \ + torrent::raw_string::from_c_str(key))); \ + CMD2_ANY_LIST_U(key ".set", std::bind(&rpc::object_storage::set_list, control->object_storage(), \ + torrent::raw_string::from_c_str(key), std::placeholders::_2)); \ + CMD2_ANY_VOID_U(key ".push_back", std::bind(&rpc::object_storage::list_push_back, control->object_storage(), \ + torrent::raw_string::from_c_str(key), std::placeholders::_2)); + +#define CMD2_VAR_LIST_U_GET(key) \ + control->object_storage()->insert_c_str(key, torrent::Object::create_list(), rpc::object_storage::flag_list_type); \ + CMD2_ANY_U(key, std::bind(&rpc::object_storage::get, control->object_storage(), \ + torrent::raw_string::from_c_str(key))); \ + CMD2_ANY_LIST(key ".set", std::bind(&rpc::object_storage::set_list, control->object_storage(), \ + torrent::raw_string::from_c_str(key), std::placeholders::_2)); \ + CMD2_ANY_VOID(key ".push_back", std::bind(&rpc::object_storage::list_push_back, control->object_storage(), \ + torrent::raw_string::from_c_str(key), std::placeholders::_2)); + #define CMD2_FUNC_SINGLE(key, cmds) \ CMD2_ANY(key, std::bind(&rpc::command_function_call_object, torrent::Object(torrent::raw_string::from_c_str(cmds)), \ std::placeholders::_1, std::placeholders::_2)); diff --git a/src/rpc/command_map.cc b/src/rpc/command_map.cc index ff4d6e3d..6816be47 100644 --- a/src/rpc/command_map.cc +++ b/src/rpc/command_map.cc @@ -98,11 +98,17 @@ CommandMap::call_command(const key_type& key, const mapped_type& arg, const targ if (itr == base_type::end()) throw torrent::input_error("Command \"" + std::string(key) + "\" does not exist."); + if (!RpcManager::is_trusted() && !(itr->second.m_flags & flag_untrusted_safe)) + throw untrusted_error("Command \"" + std::string(key) + "\" is not allowed for untrusted connections."); + return itr->second.m_anySlot(&itr->second.m_variable, target, arg); } const CommandMap::mapped_type CommandMap::call_command(iterator itr, const mapped_type& arg, const target_type& target) { + if (!RpcManager::is_trusted() && !(itr->second.m_flags & flag_untrusted_safe)) + throw untrusted_error("Command \"" + itr->first + "\" is not allowed for untrusted connections."); + return itr->second.m_anySlot(&itr->second.m_variable, target, arg); } diff --git a/src/rpc/command_map.h b/src/rpc/command_map.h index 8ff816a3..c67cb1fd 100644 --- a/src/rpc/command_map.h +++ b/src/rpc/command_map.h @@ -56,6 +56,8 @@ public: static const int flag_file_target = 0x100; static const int flag_tracker_target = 0x200; + static const int flag_untrusted_safe = 0x400; + CommandMap() = default; bool has(const std::string& key) const { return base_type::find(key) != base_type::end(); } diff --git a/src/rpc/jsonrpc.cc b/src/rpc/jsonrpc.cc index 83a625a8..56c01855 100644 --- a/src/rpc/jsonrpc.cc +++ b/src/rpc/jsonrpc.cc @@ -133,9 +133,12 @@ jsonrpc_call_command(const std::string& method, const json& params) { params_object_list.erase(params_object_list.begin()); - const auto& result = rpc::commands.call_command(itr, params_object, target); - - return object_to_json(result); + try { + const auto& result = rpc::commands.call_command(itr, params_object, target); + return object_to_json(result); + } catch (untrusted_error& e) { + throw rpc_error(JSONRPC_METHOD_NOT_FOUND_ERROR, e.what()); + } } json diff --git a/src/rpc/rpc_manager.cc b/src/rpc/rpc_manager.cc index 16eceb16..04876929 100644 --- a/src/rpc/rpc_manager.cc +++ b/src/rpc/rpc_manager.cc @@ -13,6 +13,28 @@ CommandMap commands; RpcManager rpc; ExecFile execFile; +// Trusted/untrusted XMLRPC connection model. +// +// The trust state is set per-request by the SCGI layer based on the +// UNTRUSTED_CONNECTION header. Commands without flag_untrusted_safe +// are blocked for untrusted connections. The check is in +// CommandMap::call_command(), which catches all command execution +// including nested calls through argument expansion. + +thread_local bool RpcManager::m_trusted = true; + +bool +RpcManager::set_trusted(bool trusted) { + bool prev = m_trusted; + m_trusted = trusted; + return prev; +} + +bool +RpcManager::is_trusted() { + return m_trusted; +} + void RpcManager::object_to_target(const torrent::Object& obj, int call_flags, rpc::target_type* target, std::function* deleter) { if (!obj.is_string()) diff --git a/src/rpc/rpc_manager.h b/src/rpc/rpc_manager.h index 17bccafb..bf870a96 100644 --- a/src/rpc/rpc_manager.h +++ b/src/rpc/rpc_manager.h @@ -34,6 +34,17 @@ private: std::string m_msg; }; +class untrusted_error : public torrent::base_error { +public: + untrusted_error(std::string msg) : m_msg(std::move(msg)) {} + virtual ~untrusted_error() throw() = default; + + virtual const char* what() const throw() { return m_msg.c_str(); } + +private: + std::string m_msg; +}; + class RpcManager { public: using slot_download = std::function; @@ -71,9 +82,17 @@ public: slot_tracker& slot_find_tracker() { return m_slot_find_tracker; } slot_peer& slot_find_peer() { return m_slot_find_peer; } + // Trusted/untrusted XMLRPC connection model. + // When an SCGI request includes the UNTRUSTED_CONNECTION header, + // commands without flag_untrusted_safe are blocked. + static bool set_trusted(bool trusted); + static bool is_trusted(); + static void object_to_target(const torrent::Object& obj, int callFlags, rpc::target_type* target, std::function* deleter); private: + static thread_local bool m_trusted; + XmlRpc m_xmlrpc; JsonRpc m_jsonrpc; diff --git a/src/rpc/scgi_task.cc b/src/rpc/scgi_task.cc index 089c760c..3046e9d5 100644 --- a/src/rpc/scgi_task.cc +++ b/src/rpc/scgi_task.cc @@ -111,6 +111,9 @@ SCgiTask::event_read() { size_t content_length = 0; const char* header_end = current + header_size; + // Assume trusted until we find the UNTRUSTED_CONNECTION header. + m_trusted = true; + // Parse out the null-terminated header keys and values, with // checks to ensure it doesn't scan beyond the limits of the // header @@ -141,6 +144,8 @@ SCgiTask::event_read() { goto event_read_failed; } else if (strcmp(key, "CONTENT_TYPE") == 0) { content_type = value; + } else if (strcmp(key, "UNTRUSTED_CONNECTION") == 0 && strcmp(value, "1") == 0) { + m_trusted = false; } } @@ -270,6 +275,7 @@ SCgiTask::receive_call(const char* buffer, uint32_t length) { // TODO: Rewrite RpcManager.process to pass the result buffer instead of having to copy it. auto scgi_thread = torrent::utils::Thread::self(); + bool trusted = m_trusted; auto result_callback = [this, scgi_thread](const char* b, uint32_t l) { receive_write(b, l); @@ -285,30 +291,35 @@ SCgiTask::receive_call(const char* buffer, uint32_t length) { auto lock = std::lock_guard(m_result_mutex); + RpcManager::RPCType rpc_type; + switch (content_type()) { case rpc::SCgiTask::ContentType::JSON: - torrent::main_thread::thread()->callback_interrupt_polling(this, [buffer, length, result_callback]() { - rpc.process(RpcManager::RPCType::JSON, buffer, length, - [result_callback](const char* b, uint32_t l) { - result_callback(b, l); - return true; - }); - }); + rpc_type = RpcManager::RPCType::JSON; break; - case rpc::SCgiTask::ContentType::XML: - torrent::main_thread::thread()->callback_interrupt_polling(this, [buffer, length, result_callback]() { - rpc.process(RpcManager::RPCType::XML, buffer, length, - [result_callback](const char* b, uint32_t l) { - result_callback(b, l); - return true; - }); - }); + rpc_type = RpcManager::RPCType::XML; break; - default: throw torrent::internal_error("SCgiTask::receive_call(...) received bad input."); } + + torrent::main_thread::thread()->callback_interrupt_polling(this, [buffer, length, result_callback, trusted, rpc_type]() { + rpc::RpcManager::set_trusted(trusted); + + try { + rpc.process(rpc_type, buffer, length, + [result_callback](const char* b, uint32_t l) { + result_callback(b, l); + return true; + }); + } catch (...) { + rpc::RpcManager::set_trusted(true); + throw; + } + + rpc::RpcManager::set_trusted(true); + }); } void diff --git a/src/rpc/scgi_task.h b/src/rpc/scgi_task.h index db21732c..717752a3 100644 --- a/src/rpc/scgi_task.h +++ b/src/rpc/scgi_task.h @@ -53,6 +53,7 @@ private: unsigned int m_buffer_size{0}; ContentType m_content_type{ XML }; + bool m_trusted{true}; }; } diff --git a/src/rpc/xmlrpc_c.cc b/src/rpc/xmlrpc_c.cc index 3177d577..cf80b83a 100644 --- a/src/rpc/xmlrpc_c.cc +++ b/src/rpc/xmlrpc_c.cc @@ -385,6 +385,10 @@ xmlrpc_call_command(xmlrpc_env* env, xmlrpc_value* args, void* voidServerInfo) { return object_to_xmlrpc(env, rpc::commands.call_command(itr, object, target)); + } catch (untrusted_error& e) { + xmlrpc_env_set_fault(env, XMLRPC_REQUEST_REFUSED_ERROR, e.what()); + return NULL; + } catch (xmlrpc_error_c& e) { xmlrpc_env_set_fault(env, e.type(), e.what()); return NULL; diff --git a/src/rpc/xmlrpc_tinyxml2.cc b/src/rpc/xmlrpc_tinyxml2.cc index 86445b27..204b96b0 100644 --- a/src/rpc/xmlrpc_tinyxml2.cc +++ b/src/rpc/xmlrpc_tinyxml2.cc @@ -31,7 +31,7 @@ const int XMLRPC_PARSE_ERROR = -503; // const int XMLRPC_NETWORK_ERROR = -504; // const int XMLRPC_TIMEOUT_ERROR = -505; const int XMLRPC_NO_SUCH_METHOD_ERROR = -506; -// const int XMLRPC_REQUEST_REFUSED_ERROR = -507; +const int XMLRPC_REQUEST_REFUSED_ERROR = -507; // const int XMLRPC_INTROSPECTION_DISABLED_ERROR = -508; const int XMLRPC_LIMIT_EXCEEDED_ERROR = -509; // const int XMLRPC_INVALID_UTF8_ERROR = -510; @@ -238,7 +238,11 @@ execute_command(std::string method_name, const tinyxml2::XMLElement* params_elem throw rpc_error(XMLRPC_TYPE_ERROR, "invalid parameters: too few"); } - return rpc::commands.call_command(cmd_itr, params_raw, target); + try { + return rpc::commands.call_command(cmd_itr, params_raw, target); + } catch (untrusted_error& e) { + throw rpc_error(XMLRPC_REQUEST_REFUSED_ERROR, e.what()); + } } void