From d4c1aa2b98f89b68a58463beb66485217a3ac589 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 Try to avoid manipulation of arguments for method execution --- net/http/src/TRootSniffer.cxx | 37 ++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/net/http/src/TRootSniffer.cxx b/net/http/src/TRootSniffer.cxx index b95b63ca9a55b..39d06b330fb6a 100644 --- a/net/http/src/TRootSniffer.cxx +++ b/net/http/src/TRootSniffer.cxx @@ -1289,8 +1289,8 @@ 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; @@ -1303,13 +1303,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 04331635008803383ef15d62db8277308669ffe3 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 5b99c6ecc789c0a5909263c52b2b86c8e6fb0728 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 fa53f879fbb523d45e31eb8100677d7524a48d0f 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 772aa0f30462ef710747bd10e4b88e7d8cc36012 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 39d06b330fb6a..374711482d83d 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 ab58efa45f3f0c220137a7b0dc0579c40bfab93a 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 - 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 | 336 ++++++++++++++++++++++++++++ 3 files changed, 353 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..240d064caef6a --- /dev/null +++ b/net/httpsniff/test/test_sniffer.cxx @@ -0,0 +1,336 @@ +#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); + // TODO: anlyze why so big size for small object + EXPECT_NEAR(res.length(), 2097152, 10000); +} + +// 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" + "}"); +} + +// 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, ""); + + // 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\""); +} + + +// 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"); + + // and now most important - bin content has to change + EXPECT_EQ(hist.GetBinContent(5), 30); +} + +// 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"); + EXPECT_EQ(std::string("NewTitle"), obj.GetTitle()); + + res = ""; + sniffer.Produce("/obj", "exe.json", "method=SetTitle&title=\"QuotedTitle\"", res); + EXPECT_EQ(res, "null"); + EXPECT_EQ(std::string("QuotedTitle"), obj.GetTitle()); + + res = ""; + sniffer.Produce("/obj", "exe.json", "method=SetTitle&title=%22UrlStyleQuotedTitle%22", res); + EXPECT_EQ(res, "null"); + EXPECT_EQ(std::string("UrlStyleQuotedTitle"), obj.GetTitle()); + + 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()); +} + +// 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"); + + 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"); + + 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"); + + res = ""; + // quotes are in command definition + sniffer.Produce("/Print2", "cmd.json", "arg1=*", res); + EXPECT_EQ(res, "0"); + + 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"); + + res = ""; + // Execute command which returns some value + sniffer.Produce("/GetSize", "cmd.json", "", res); + // returns only strings sizes + EXPECT_EQ(res, "10"); +} + + + +// 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" + "}]"); +}