From 96b59f99266bd07840f00b248b837903d9404462 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Fri, 8 May 2026 10:49:15 +0200 Subject: [PATCH 1/6] [http] cleanup URL option before usage Remove any special symbols Add escape characters for quote and escape itself Discard all URL options longer than 1K Try to avoid manipulation of arguments for method execution --- net/http/src/TRootSniffer.cxx | 41 ++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/net/http/src/TRootSniffer.cxx b/net/http/src/TRootSniffer.cxx index b95b63ca9a55b..bdbb3b1da29fa 100644 --- a/net/http/src/TRootSniffer.cxx +++ b/net/http/src/TRootSniffer.cxx @@ -1289,11 +1289,15 @@ Bool_t TRootSniffer::ProduceXml(const std::string &/* path */, const std::string TString TRootSniffer::DecodeUrlOptionValue(const char *value, Bool_t remove_quotes) { - if (!value || (strlen(value) == 0)) - return TString(); + if (!value || !*value) + return ""; TString res = value; + // discard too large URL options, they should not appear at all + if (res.Length() > 1024) + return ""; + res.ReplaceAll("%27", "\'"); res.ReplaceAll("%22", "\""); res.ReplaceAll("%3E", ">"); @@ -1303,13 +1307,40 @@ TString TRootSniffer::DecodeUrlOptionValue(const char *value, Bool_t remove_quot res.ReplaceAll("%5D", "]"); res.ReplaceAll("%3D", "="); - if (remove_quotes && (res.Length() > 1) && ((res[0] == '\'') || (res[0] == '\"')) && - (res[0] == res[res.Length() - 1])) { + Char_t quote = 0; + + if ((res.Length() > 1) && ((res[0] == '\'') || (res[0] == '\"')) && (res[0] == res[res.Length() - 1])) + quote = res[0]; + + // first remove quotes + if (quote) { res.Remove(res.Length() - 1); res.Remove(0, 1); } - return res; + // we expect normal content here, no special symbols, no unescaped quotes + TString clean; + for (Ssiz_t i = 0; i < res.Length(); ++i) { + char c = res[i]; + if (c == '"' || c == '\\') { + // escape quotes and slahes + clean.Append('\\'); + clean.Append(c); + } else if (!std::iscntrl(c)) + // ignore all special symbols + clean.Append(c); + } + + if (quote && !remove_quotes) { + // return string with quotes - when desired + res = ""; + res.Append(quote); + res.Append(clean); + res.Append(quote); + return res; + } + + return clean; } //////////////////////////////////////////////////////////////////////////////// From 7ac37df9a66b80cbc1321d6b534d848d285fee58 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Fri, 8 May 2026 11:09:56 +0200 Subject: [PATCH 2/6] [http] cleanup draw option in image production Avoid special characters as draw arguments --- net/httpsniff/src/TRootSnifferFull.cxx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/net/httpsniff/src/TRootSnifferFull.cxx b/net/httpsniff/src/TRootSnifferFull.cxx index c9f265fb2f43e..b173e5c15226d 100644 --- a/net/httpsniff/src/TRootSnifferFull.cxx +++ b/net/httpsniff/src/TRootSnifferFull.cxx @@ -416,7 +416,7 @@ Bool_t TRootSnifferFull::ProduceImage(Int_t kind, const std::string &path, const if (gDebug > 1) Info("TRootSniffer", "Crate IMAGE from object %s", obj->GetName()); - Int_t width(300), height(200); + Int_t width = 300, height = 200; TString drawopt; if (!options.empty()) { @@ -429,9 +429,7 @@ Bool_t TRootSnifferFull::ProduceImage(Int_t kind, const std::string &path, const Int_t h = url.GetIntValueFromOptions("h"); if (h > 10) height = h; - const char *opt = url.GetValueFromOptions("opt"); - if (opt) - drawopt = opt; + drawopt = DecodeUrlOptionValue(url.GetValueFromOptions("opt"), kTRUE); } Bool_t isbatch = gROOT->IsBatch(); From 700c42309025d952a1735a3658ee5a7f9019f91e Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Fri, 8 May 2026 13:20:18 +0200 Subject: [PATCH 3/6] [http] strictly check arguments in ProduceExe Always use DecodeUrlOptionValue method when processing URL arguments or URL string. Internally method provides escape symbols for quotes and backslash. If expecting numeric value - remove all symbols keeping alphanumeric, '.', '+', '-' and ':' --- net/httpsniff/src/TRootSnifferFull.cxx | 101 +++++++++++++++---------- 1 file changed, 61 insertions(+), 40 deletions(-) diff --git a/net/httpsniff/src/TRootSnifferFull.cxx b/net/httpsniff/src/TRootSnifferFull.cxx index b173e5c15226d..894fc94281613 100644 --- a/net/httpsniff/src/TRootSnifferFull.cxx +++ b/net/httpsniff/src/TRootSnifferFull.cxx @@ -625,47 +625,46 @@ Bool_t TRootSnifferFull::ProduceExe(const std::string &path, const std::string & return debug != nullptr; const char *rest_url = pos + strlen(method_name) + 7; if (*rest_url == '&') ++rest_url; - call_args.Form("\"%s\"", rest_url); + call_args.Append("\""); + call_args.Append(DecodeUrlOptionValue(rest_url, kTRUE)); + call_args.Append("\""); break; } TString sval; const char *val = url.GetValueFromOptions(arg->GetName()); - if (val) { - sval = DecodeUrlOptionValue(val, kFALSE); - val = sval.Data(); - } + if (val) + sval = DecodeUrlOptionValue(val, kTRUE); + + Bool_t sanitize_numeric = kFALSE; - if ((val != nullptr) && (strcmp(val, "_this_") == 0)) { + if (sval == "_this_") { // special case - object itself is used as argument sval.Form("(%s*)0x%zx", obj_cl->GetName(), (size_t)obj_ptr); - val = sval.Data(); - } else if ((val != nullptr) && (fCurrentArg != nullptr) && (fCurrentArg->GetPostData() != nullptr)) { + } else if ((fCurrentArg != nullptr) && (fCurrentArg->GetPostData() != nullptr)) { // process several arguments which are specific for post requests - if (strcmp(val, "_post_object_xml_") == 0) { + if (sval == "_post_object_xml_") { // post data has extra 0 at the end and can be used as null-terminated string post_obj = TBufferXML::ConvertFromXML((const char *)fCurrentArg->GetPostData()); - if (!post_obj) { + if (!post_obj) sval = "0"; - } else { + else { sval.Form("(%s*)0x%zx", post_obj->ClassName(), (size_t)post_obj); if (url.HasOption("_destroy_post_")) garbage.Add(post_obj); } - val = sval.Data(); - } else if (strcmp(val, "_post_object_json_") == 0) { + } else if (sval == "_post_object_json_") { // post data has extra 0 at the end and can be used as null-terminated string post_obj = TBufferJSON::ConvertFromJSON((const char *)fCurrentArg->GetPostData()); - if (!post_obj) { + if (!post_obj) sval = "0"; - } else { + else { sval.Form("(%s*)0x%zx", post_obj->ClassName(), (size_t)post_obj); if (url.HasOption("_destroy_post_")) garbage.Add(post_obj); } - val = sval.Data(); - } else if ((strcmp(val, "_post_object_") == 0) && url.HasOption("_post_class_")) { - TString clname = url.GetValueFromOptions("_post_class_"); + } else if ((sval == "_post_object_") && url.HasOption("_post_class_")) { + TString clname = DecodeUrlOptionValue(url.GetValueFromOptions("_post_class_"), kTRUE); TClass *arg_cl = gROOT->GetClass(clname, kTRUE, kTRUE); if ((arg_cl != nullptr) && (arg_cl->GetBaseClassOffset(TObject::Class()) == 0) && (post_obj == nullptr)) { post_obj = (TObject *)arg_cl->New(); @@ -682,39 +681,61 @@ Bool_t TRootSnifferFull::ProduceExe(const std::string &path, const std::string & garbage.Add(post_obj); } } - sval.Form("(%s*)0x%zx", clname.Data(), (size_t)post_obj); - val = sval.Data(); - } else if (strcmp(val, "_post_data_") == 0) { + if (!post_obj) + sval = "0"; + else + sval.Form("(%s*)0x%zx", clname.Data(), (size_t)post_obj); + } else if (sval == "_post_data_") sval.Form("(void*)0x%zx", (size_t)fCurrentArg->GetPostData()); - val = sval.Data(); - } else if (strcmp(val, "_post_length_") == 0) { + else if (sval == "_post_length_") sval.Form("%ld", (long)fCurrentArg->GetPostDataLength()); - val = sval.Data(); - } - } + else + sanitize_numeric = kTRUE; + } else + sanitize_numeric = kTRUE; - if (!val) - val = arg->GetDefault(); + if (sval.IsNull() && arg->GetDefault()) + sval = arg->GetDefault(); if (debug) - debug->append(TString::Format(" Argument:%s Type:%s Value:%s \n", arg->GetName(), arg->GetFullTypeName(), - val ? val : "") - .Data()); - if (!val) - return debug != nullptr; + debug->append( + TString::Format(" Argument:%s Type:%s Value:%s \n", arg->GetName(), arg->GetFullTypeName(), sval.Data()) + .Data()); if (call_args.Length() > 0) call_args += ", "; - if ((strcmp(arg->GetFullTypeName(), "const char*") == 0) || (strcmp(arg->GetFullTypeName(), "Option_t*") == 0)) { - int len = strlen(val); - if ((strlen(val) < 2) || (*val != '\"') || (val[len - 1] != '\"')) - call_args.Append(TString::Format("\"%s\"", val)); - else - call_args.Append(val); + Bool_t isstr = (strcmp(arg->GetFullTypeName(), "const char*") == 0) || + (strcmp(arg->GetFullTypeName(), "Option_t*") == 0) || + (strcmp(arg->GetFullTypeName(), "string") == 0); + + if (isstr) { + // check that quotes provided for the string argument + // all special characters were escaped before + if (sval.IsNull()) + sval = "\"\""; + else { + if (sval[0] != '"') + sval.Prepend("\""); + if (sval[sval.Length() - 1] != '"') + sval.Append("\""); + } } else { - call_args.Append(val); + // for numeric types keep only numeric and alphabetic characters + // exclude others - especially remove all escape characters + if (sanitize_numeric) { + TString sanitized; + for(Size_t i = 0; i < sval.Length(); ++i) { + if (std::isalnum(sval[i]) || std::strchr(".:+-", sval[i])) + sanitized.Append(sval[i]); + } + sval = sanitized; + } + if (sval.IsNull()) + sval = "0"; } + + call_args.Append(sval); } TMethodCall *call = nullptr; From 29c2ed3b6edef6067d0f8baa50dc67deb67dc683 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Fri, 8 May 2026 14:27:16 +0200 Subject: [PATCH 4/6] [http] introduce fAllowPostObject flag It allows to deserialize post data as ROOT object when processing exe.json request. While this can leads to arbitrary code loading and injection, disable this feature by default. Can be enabled back with: ``` serv->SetAllowPostObject(kTRUE); ``` --- net/http/inc/THttpServer.h | 4 ++++ net/http/inc/TRootSniffer.h | 7 +++++++ net/http/src/THttpServer.cxx | 25 +++++++++++++++++++++++++ net/httpsniff/src/TRootSnifferFull.cxx | 6 +++--- 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/net/http/inc/THttpServer.h b/net/http/inc/THttpServer.h index c612773362be6..74f69239db425 100644 --- a/net/http/inc/THttpServer.h +++ b/net/http/inc/THttpServer.h @@ -94,6 +94,10 @@ class THttpServer : public TNamed { void SetReadOnly(Bool_t readonly = kTRUE); + Bool_t IsAllowPostObject() const; + + void SetAllowPostObject(Bool_t allow_post_obj); + Bool_t IsWSOnly() const; void SetWSOnly(Bool_t on = kTRUE); diff --git a/net/http/inc/TRootSniffer.h b/net/http/inc/TRootSniffer.h index 4351e66ffc9ed..558473ec4ce84 100644 --- a/net/http/inc/TRootSniffer.h +++ b/net/http/inc/TRootSniffer.h @@ -120,6 +120,7 @@ class TRootSniffer : public TNamed { protected: TString fObjectsPath; /// fTopFolder; ///SetReadOnly(readonly); } +//////////////////////////////////////////////////////////////////////////////// +/// Returns true if server accept object content in POST reequests + +Bool_t THttpServer::IsAllowPostObject() const +{ + return fSniffer ? fSniffer->IsAllowPostObject() : kFALSE; +} + +//////////////////////////////////////////////////////////////////////////////// +/// Set flag to allow receive and desereilize objects in POST requests +/// +/// When object methods are executed via exe.json request, +/// one can supply object as binary/json/xml in the body of POST request +/// To allow creation of such object, one need to enable this flag +/// Use of exe.json only possible in not-readonly mode +/// +/// CAUTION! This is sensitive functionality and therefore should be +/// used only when server not exposed to publicaly-accessed netowork. + +void THttpServer::SetAllowPostObject(Bool_t allow_post_obj) +{ + if (fSniffer) + fSniffer->SetAllowPostObject(allow_post_obj); +} + //////////////////////////////////////////////////////////////////////////////// /// returns true if only websockets are handled by the server /// diff --git a/net/httpsniff/src/TRootSnifferFull.cxx b/net/httpsniff/src/TRootSnifferFull.cxx index 894fc94281613..580ba3d854889 100644 --- a/net/httpsniff/src/TRootSnifferFull.cxx +++ b/net/httpsniff/src/TRootSnifferFull.cxx @@ -643,7 +643,7 @@ Bool_t TRootSnifferFull::ProduceExe(const std::string &path, const std::string & sval.Form("(%s*)0x%zx", obj_cl->GetName(), (size_t)obj_ptr); } else if ((fCurrentArg != nullptr) && (fCurrentArg->GetPostData() != nullptr)) { // process several arguments which are specific for post requests - if (sval == "_post_object_xml_") { + if (fAllowPostObject && (sval == "_post_object_xml_")) { // post data has extra 0 at the end and can be used as null-terminated string post_obj = TBufferXML::ConvertFromXML((const char *)fCurrentArg->GetPostData()); if (!post_obj) @@ -653,7 +653,7 @@ Bool_t TRootSnifferFull::ProduceExe(const std::string &path, const std::string & if (url.HasOption("_destroy_post_")) garbage.Add(post_obj); } - } else if (sval == "_post_object_json_") { + } else if (fAllowPostObject && (sval == "_post_object_json_")) { // post data has extra 0 at the end and can be used as null-terminated string post_obj = TBufferJSON::ConvertFromJSON((const char *)fCurrentArg->GetPostData()); if (!post_obj) @@ -663,7 +663,7 @@ Bool_t TRootSnifferFull::ProduceExe(const std::string &path, const std::string & if (url.HasOption("_destroy_post_")) garbage.Add(post_obj); } - } else if ((sval == "_post_object_") && url.HasOption("_post_class_")) { + } else if (fAllowPostObject && (sval == "_post_object_") && url.HasOption("_post_class_")) { TString clname = DecodeUrlOptionValue(url.GetValueFromOptions("_post_class_"), kTRUE); TClass *arg_cl = gROOT->GetClass(clname, kTRUE, kTRUE); if ((arg_cl != nullptr) && (arg_cl->GetBaseClassOffset(TObject::Class()) == 0) && (post_obj == nullptr)) { From 5213297cea0e4176639af38a70a13159c7d89cea Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 18 May 2026 16:57:44 +0200 Subject: [PATCH 5/6] [http] analyze arguments of cmd.json While here arbitrary string injected into ProcessLine, ensure that only numeric argument is not quoted. All other arguments kinds will be quoted and prevent execution of potentially dangerous code --- net/http/src/TRootSniffer.cxx | 38 +++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/net/http/src/TRootSniffer.cxx b/net/http/src/TRootSniffer.cxx index bdbb3b1da29fa..aa4368bba28d8 100644 --- a/net/http/src/TRootSniffer.cxx +++ b/net/http/src/TRootSniffer.cxx @@ -33,6 +33,8 @@ #include #include #include +#include + const char *item_prop_kind = "_kind"; const char *item_prop_more = "_more"; @@ -1213,9 +1215,41 @@ Bool_t TRootSniffer::ExecuteCmd(const std::string &path, const std::string &opti return kTRUE; } - TString svalue = DecodeUrlOptionValue(argvalue, kTRUE); argname = TString("%") + argname + TString("%"); - method.ReplaceAll(argname, svalue); + auto p = method.Index(argname); + if (p == kNPOS) + continue; + + method.Remove(p, argname.Length()); + + if ((p > 0) && (p < method.Length()) && (method.Length() > 1) && (method[p-1] == '"') && (method[p] == '"')) { + // command definition has quotes around argument + // one can insert value from URL removing quotes + method.Insert(p, DecodeUrlOptionValue(argvalue, kTRUE)); + continue; + } + + // extract argument without removing quotes + TString svalue = DecodeUrlOptionValue(argvalue, kFALSE); + + if ((svalue.Length() > 1) && (svalue[0] == '"') && (svalue[svalue.Length() - 1] == '"')) { + // if value itself has quotes, all special symbols already escaped and one can insert it as is + method.Insert(p, svalue); + continue; + } + + Bool_t is_numeric = kTRUE; + // expect decimal, hex or float values here, E/e also belong to hex + for(Size_t i = 0; is_numeric && (i < svalue.Length()); ++i) + is_numeric = std::isxdigit(svalue[i]) || std::strchr(".+-", svalue[i]); + + // always quote content which not numeric + if (!is_numeric) + svalue = "\"" + svalue + "\""; + else if (svalue.IsNull()) + svalue = "0"; + + method.Insert(p, svalue); } } From 8493420bd48888ca7ea24c9d00cac0f28d2a7494 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 18 May 2026 13:51:24 +0200 Subject: [PATCH 6/6] [test] add ROOT sniffer testing Verify execution of several supported requests which can be handled by http server. Testing: - root.json - root.xml - file.root - exe.json - exe.json with POST data - item.json - cmd.json - multi.json Also verify basic functionality of TRootSniffer::DecodeUrlOptionValue method --- net/httpsniff/CMakeLists.txt | 8 +- net/httpsniff/test/CMakeLists.txt | 12 + net/httpsniff/test/test_sniffer.cxx | 331 ++++++++++++++++++++++++++++ 3 files changed, 348 insertions(+), 3 deletions(-) create mode 100644 net/httpsniff/test/CMakeLists.txt create mode 100644 net/httpsniff/test/test_sniffer.cxx diff --git a/net/httpsniff/CMakeLists.txt b/net/httpsniff/CMakeLists.txt index 55b47fcd9e0c5..7541839a8b50e 100644 --- a/net/httpsniff/CMakeLists.txt +++ b/net/httpsniff/CMakeLists.txt @@ -1,12 +1,12 @@ -# Copyright (C) 1995-2019, Rene Brun and Fons Rademakers. +# Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. # All rights reserved. # # For the licensing terms see $ROOTSYS/LICENSE. # For the list of contributors see $ROOTSYS/README/CREDITS. ############################################################################ -# CMakeLists.txt file for building ROOT net/http package -# @author Pere Mato, CERN +# CMakeLists.txt file for building ROOT net/httpsniff package +# @author Sergey Linev, GSI ############################################################################ ROOT_STANDARD_LIBRARY_PACKAGE(RHTTPSniff @@ -24,3 +24,5 @@ ROOT_STANDARD_LIBRARY_PACKAGE(RHTTPSniff Tree XMLIO ) + +ROOT_ADD_TEST_SUBDIRECTORY(test) diff --git a/net/httpsniff/test/CMakeLists.txt b/net/httpsniff/test/CMakeLists.txt new file mode 100644 index 0000000000000..b23ad58177deb --- /dev/null +++ b/net/httpsniff/test/CMakeLists.txt @@ -0,0 +1,12 @@ +# Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. +# All rights reserved. +# +# For the licensing terms see $ROOTSYS/LICENSE. +# For the list of contributors see $ROOTSYS/README/CREDITS. + +############################################################################ +# CMakeLists.txt file for building ROOT net/http package +# @author Sergey Linev, GSI +############################################################################ + +ROOT_ADD_GTEST(testRootSniffer test_sniffer.cxx LIBRARIES RHTTPSniff) diff --git a/net/httpsniff/test/test_sniffer.cxx b/net/httpsniff/test/test_sniffer.cxx new file mode 100644 index 0000000000000..94a6a4f0b612c --- /dev/null +++ b/net/httpsniff/test/test_sniffer.cxx @@ -0,0 +1,331 @@ +#include "gtest/gtest.h" + +#include + +#include "TNamed.h" +#include "TH1.h" +#include "TBufferJSON.h" +#include "THttpCallArg.h" +#include "TROOT.h" +#include "TRootSnifferFull.h" + +#include "ROOT/TestSupport.hxx" + + +// simple class to access protected method + +class TDecodeTest : public TRootSniffer { + public: + std::string Decode(const char *value, Bool_t remove_quotes = kTRUE) + { + TString res = DecodeUrlOptionValue(value, remove_quotes); + return res.Data(); + } +}; + +// check basic URL parameters decoding +TEST(TRootSniffer, decode_url_options) +{ + TDecodeTest test; + + EXPECT_EQ(test.Decode(""), ""); + + // single quote has to be escaped + EXPECT_EQ(test.Decode("\""), "\\\""); + + // single backalsh has to be escaped + EXPECT_EQ(test.Decode("\\"), "\\\\"); + + // remove quotes + EXPECT_EQ(test.Decode("\"\""), ""); + + // remove quotes and escape quotes + EXPECT_EQ(test.Decode("\"\"\""), "\\\""); + + // remove quotes and escape backslah + EXPECT_EQ(test.Decode("\"\\\""), "\\\\"); + + // remove quotes and remove special charsescape backslah + EXPECT_EQ(test.Decode("\"abc\njkl\t\""), "abcjkl"); + + // escape quotes in the middle + EXPECT_EQ(test.Decode("someFunc(\"someArg\");someArray[3];"), "someFunc(\\\"someArg\\\");someArray[3];"); + + // keep quotes + EXPECT_EQ(test.Decode("\"\"", kFALSE), "\"\""); + + // keep quotes and escape inside quotes + EXPECT_EQ(test.Decode("\"\"\"", kFALSE), "\"\\\"\""); + + // keep quotes and keep german letters - remove new line + EXPECT_EQ(test.Decode("\"Gänse\nfüßchen\"", kFALSE), "\"Gänsefüßchen\""); +} + +// check JSON representation for the objects +TEST(TRootSniffer, root_json) +{ + TNamed obj("obj", "title"); + + TRootSnifferFull sniffer; + + sniffer.RegisterObject("/", &obj); + + std::string res; + sniffer.Produce("/obj", "root.json", "", res); + EXPECT_EQ(res, "{\n" + " \"_typename\" : \"TNamed\",\n" + " \"fUniqueID\" : 0,\n" + " \"fBits\" : 8,\n" + " \"fName\" : \"obj\",\n" + " \"fTitle\" : \"title\"\n" + "}"); +} + +// check XML representation for the objects +TEST(TRootSniffer, root_xml) +{ + TNamed obj("obj", "title"); + + TRootSnifferFull sniffer; + + sniffer.RegisterObject("/", &obj); + + std::string res; + sniffer.Produce("/obj", "root.xml", "", res); + EXPECT_EQ(res, "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n"); +} + +// check BINARY representation for the objects +TEST(TRootSniffer, root_bin) +{ + TNamed obj("obj", "title"); + + TRootSnifferFull sniffer; + + sniffer.RegisterObject("/", &obj); + + std::string res; + sniffer.Produce("/obj", "root.bin", "", res); + // keep minimal margin for binary format change + EXPECT_NEAR(res.length(), 26, 4); +} + +// check root file creation for the objects +TEST(TRootSniffer, file_root) +{ + TNamed obj("obj", "title"); + + TRootSnifferFull sniffer; + + sniffer.RegisterObject("/", &obj); + + std::string res; + sniffer.Produce("/obj", "file.root", "", res); + EXPECT_NEAR(res.length(), 2097152, 10000) << "size of file.root request"; +} + +// check hierarchy request +TEST(TRootSniffer, item_json) +{ + TNamed obj("obj", "title"); + + TRootSnifferFull sniffer; + + sniffer.RegisterObject("/", &obj); + + std::string res; + sniffer.Produce("/obj", "item.json", "", res); + + EXPECT_EQ(res, "{\n" + " \"_name\" : \"obj\",\n" + " \"_root_version\" : " + std::to_string(gROOT->GetVersionCode()) + ",\n" + " \"_kind\" : \"ROOT.TNamed\",\n" + " \"_title\" : \"title\"\n" + "}") << "return value of item.json"; +} + +// simple method execution +TEST(TRootSniffer, exe_json) +{ + TNamed obj("obj","title"); + + TRootSnifferFull sniffer; + + sniffer.RegisterObject("/", &obj); + + std::string res0; + // by default methods execution is not allowed + sniffer.Produce("/obj", "exe.json", "method=GetTitle", res0); + EXPECT_EQ(res0, "") << "return value of exe.json in readonly"; + + // disable readonly to get method executed + sniffer.SetReadOnly(kFALSE); + + // only now one can execute method + std::string res1; + sniffer.Produce("/obj", "exe.json", "method=GetTitle", res1); + EXPECT_EQ(res1, "\"title\"") << "return value of exe.json for GetTitle"; +} + + +// execute method with post data - lot of gymnastic around +TEST(TRootSniffer, exe_post_json) +{ + TH1I hist("hist", "title", 10, 0, 10); + hist.SetDirectory(nullptr); + hist.SetBinContent(5, 10); + + std::string json; + { + // only temporary to create json + TH1I hist2("hist", "title", 10, 0, 10); + hist.SetDirectory(nullptr); + hist2.SetBinContent(5, 20); + json = TBufferJSON::ToJSON(&hist2).Data(); + } + + TRootSnifferFull sniffer; + + sniffer.RegisterObject("/", &hist); + + // disable readonly to execute method + sniffer.SetReadOnly(kFALSE); + // allow use of POST data to decode object from JSON + sniffer.SetAllowPostObject(kTRUE); + + THttpCallArg arg; + arg.SetPostData(std::move(json)); + sniffer.SetCurrentCallArg(&arg); + + // before execution content is 10 + EXPECT_EQ(hist.GetBinContent(5), 10); + + std::string res; + sniffer.Produce("/hist", "exe.json", "method=Add&prototype='const TH1*,Double_t'&h1=_post_object_json_&_destroy_post_", res); + EXPECT_EQ(res, "1") << "return value of exe.json"; + + // and now most important - bin content has to change + EXPECT_EQ(hist.GetBinContent(5), 30) << "check of histogram content"; +} + +// changing object title +TEST(TRootSniffer, set_title) +{ + TNamed obj("obj", "title"); + + TRootSnifferFull sniffer; + // disable readonly to get method executed + sniffer.SetReadOnly(kFALSE); + + sniffer.RegisterObject("/", &obj); + + std::string res; + + sniffer.Produce("/obj", "exe.json", "method=SetTitle&title=NewTitle", res); + EXPECT_EQ(res, "null") << "return value of exe.json when methout return void"; + EXPECT_EQ(std::string("NewTitle"), obj.GetTitle()) << "compare object title with applied value"; + + res = ""; + sniffer.Produce("/obj", "exe.json", "method=SetTitle&title=\"QuotedTitle\"", res); + EXPECT_EQ(res, "null"); + EXPECT_EQ(std::string("QuotedTitle"), obj.GetTitle()) << "compare object title with applied value"; + + res = ""; + sniffer.Produce("/obj", "exe.json", "method=SetTitle&title=%22UrlStyleQuotedTitle%22", res); + EXPECT_EQ(res, "null"); + EXPECT_EQ(std::string("UrlStyleQuotedTitle"), obj.GetTitle()) << "compare object title with applied value"; + + res = ""; + sniffer.Produce("/obj", "exe.json", "method=SetTitle&title=Mail\"Formed\"Title", res); + EXPECT_EQ(res, "null"); + EXPECT_EQ(std::string("Mail\"Formed\"Title"), obj.GetTitle()) << "compare object title with applied value"; +} + +// testing command execution with different signatures +TEST(TRootSniffer, cmd_json) +{ + TNamed obj("obj", "title"); + + TRootSnifferFull sniffer; + sniffer.SetReadOnly(kFALSE); + + sniffer.RegisterObject("/", &obj); + sniffer.RegisterCommand("/Print1", "/obj/->Print(%arg1%)", ""); + sniffer.RegisterCommand("/Print2", "/obj/->Print(\"%arg1%\")", ""); + sniffer.RegisterCommand("/GetSize", "/obj/->Sizeof()", ""); + + std::string res; + // quotes are in URL + sniffer.Produce("/Print1", "cmd.json", "arg1=%22*%22", res); + EXPECT_EQ(res, "0") << "return value of cmd.json"; + + res = ""; + // skipping quotes from URL - when they are necessary + // sniffer should have add them automatically + sniffer.Produce("/Print1", "cmd.json", "arg1=*", res); + EXPECT_EQ(res, "0") << "return value of cmd.json"; + + res = ""; + // skipping quotes from URL - when they are necessary + // while value looks like number, sniffer will not quote it + // result of process line is not result is + sniffer.Produce("/Print1", "cmd.json", "arg1=0", res); + EXPECT_EQ(res, "0") << "return value of cmd.json"; + + res = ""; + // quotes are in command definition + sniffer.Produce("/Print2", "cmd.json", "arg1=*", res); + EXPECT_EQ(res, "0") << "return value of cmd.json"; + + res = ""; + // quotes are in command definition but we try to add our own + // sniffer will remove them + sniffer.Produce("/Print2", "cmd.json", "arg1=\"*\"", res); + EXPECT_EQ(res, "0") << "return value of cmd.json"; + + res = ""; + // Execute command which returns some value + sniffer.Produce("/GetSize", "cmd.json", "", res); + // returns only strings sizes + EXPECT_EQ(res, "10") << "return value of cmd.json with object size"; +} + +// check JSON representation for the objects +TEST(TRootSniffer, multi_json) +{ + TNamed obj1("obj1", "title1"); + TNamed obj2("obj2", "title2"); + + TRootSnifferFull sniffer; + + sniffer.RegisterObject("/", &obj1); + sniffer.RegisterObject("/", &obj2); + + std::string items = "/obj1/root.json\n/obj2/root.json\n"; + + THttpCallArg arg; + arg.SetPostData(std::move(items)); + sniffer.SetCurrentCallArg(&arg); + + std::string res; + sniffer.Produce("", "multi.json", "number=2", res); + EXPECT_EQ(res, "[{\n" + " \"_typename\" : \"TNamed\",\n" + " \"fUniqueID\" : 0,\n" + " \"fBits\" : 8,\n" + " \"fName\" : \"obj1\",\n" + " \"fTitle\" : \"title1\"\n" + "}, {\n" + " \"_typename\" : \"TNamed\",\n" + " \"fUniqueID\" : 0,\n" + " \"fBits\" : 8,\n" + " \"fName\" : \"obj2\",\n" + " \"fTitle\" : \"title2\"\n" + "}]") << "return value of multi.json"; +}