#include "config.h" #include "test/rpc/test_xmlrpc.h" #include #include "control.h" #include "globals.h" #include "command_helpers.h" #include "rpc/command_map.h" #include "rpc/scgi_task.h" CPPUNIT_TEST_SUITE_REGISTRATION(TestXmlrpc); torrent::Object xmlrpc_cmd_test_reflect([[maybe_unused]] rpc::target_type t, const torrent::Object& obj) { return obj; } torrent::Object xmlrpc_cmd_test_reflect_string([[maybe_unused]] rpc::target_type t, const std::string& obj) { 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) std::vector> basic_requests = { std::make_tuple("Basic call", "xmlrpc_reflect", ""), std::make_tuple("Basic call without params", "xmlrpc_reflect", ""), std::make_tuple("UTF-8 string", "xmlrpc_reflectчао", "чао"), std::make_tuple("emoji string", "xmlrpc_reflect😊", "😊"), std::make_tuple("base64 data (which gets returned as a string)", "xmlrpc_reflectZm9vYmFy", "foobar"), std::make_tuple("i8 ints", "xmlrpc_reflect41", "41"), std::make_tuple("i8 ints", "xmlrpc_reflect2247483647", "2247483647"), std::make_tuple("negative i8 ints", "xmlrpc_reflect-2347483647", "-2347483647"), std::make_tuple("Simple array", "xmlrpc_reflect2247483647", "2247483647"), std::make_tuple("Empty array", "xmlrpc_reflect", ""), std::make_tuple("Empty struct", "xmlrpc_reflect", ""), std::make_tuple("Simple struct", "xmlrpc_reflectlowerBound18upperBound139", "lowerBound18upperBound139"), std::make_tuple("Invalid - missing method", "no_such_method41", "faultCode-506faultStringmethod 'no_such_method' not defined"), std::make_tuple("Invalid - i8 target", "xmlrpc_reflect41", "faultCode-500faultStringinvalid parameters: target must be a string"), std::make_tuple("Invalid - empty int tag", "xmlrpc_reflect", "faultCode-501faultStringunable to parse empty integer"), std::make_tuple("Invalid - empty int text", "xmlrpc_reflect", "faultCode-501faultStringunable to parse empty integer"), std::make_tuple("Invalid - broken XML", "thodCall>test_a41faultCode-503faultStringError=XML_ERROR_PARSING_ELEMENT ErrorID=6 (0x6) Line number=1: XMLElement name=method"), std::make_tuple("Invalid - non-integer i8", "xmlrpc_reflectstring value", "faultCode-501faultStringunable to parse integer value"), std::make_tuple("Invalid - float i8", "xmlrpc_reflect3.14", "faultCode-501faultStringunable to parse integer value"), std::make_tuple("Invalid - non-boolean boolean", "xmlrpc_reflectstring value", "faultCode-501faultStringunknown boolean value: string value"), std::make_tuple("CMD2_ANY_STRING", "xmlrpc_reflect_stringtest", "test") }; void TestXmlrpc::setUp() { m_test_main_thread = TestMainThread::create(); m_test_main_thread->init_thread(); m_xmlrpc = rpc::XmlRpc(); m_xmlrpc.initialize(); setlocale(LC_ALL, ""); control = new Control; if (rpc::commands.find("xmlrpc_reflect") == rpc::commands.end()) CMD2_ANY("xmlrpc_reflect", &xmlrpc_cmd_test_reflect); 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 TestXmlrpc::tearDown() { m_test_main_thread.reset(); } void TestXmlrpc::test_basics() { for (auto& test : basic_requests) { std::string output; m_xmlrpc.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); } } void TestXmlrpc::test_invalid_utf8() { // Surprisingly, this call doesn't fail. TinyXML-2 technically expects // valid UTF-8, but doesn't check strings, and Object strings are // just a series of bytes so it reflects just fine. std::string input = "xmlrpc_reflect\xc3\x28"; std::string expected = "\xc3\x28"; 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_EQUAL(expected, output); } void TestXmlrpc::test_size_limit() { std::string input = "xmlrpc_reflect\xc3\x28"; std::string expected = "faultCode-509faultStringContent size exceeds maximum XML-RPC limit"; std::string output; m_xmlrpc.set_size_limit(1); m_xmlrpc.process(input.c_str(), input.size(), [&output](const char* c, uint32_t l){ output.append(c, l); return true;}); 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 = "xmlrpc_oversized"; std::string expected = "faultCode-509faultStringResponse size exceeds maximum XML-RPC limit"; 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); } namespace { const std::string multicall_method_name = "methodNamexmlrpc_reflect"; const std::string multicall_params = "params" "a" ""; std::string multicall_request(const std::string& members) { return "system.multicall" "" + members + ""; } } 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 = "" "faultCode-503" "faultStringmulticall struct's first member must be methodName" ""; 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() {} #endif