diff options
Diffstat (limited to 'src/zenhttp')
| -rw-r--r-- | src/zenhttp/clients/httpclientcommon.h | 1 | ||||
| -rw-r--r-- | src/zenhttp/httpclient.cpp | 72 | ||||
| -rw-r--r-- | src/zenhttp/httpserver.cpp | 97 | ||||
| -rw-r--r-- | src/zenhttp/include/zenhttp/auth/authservice.h | 4 | ||||
| -rw-r--r-- | src/zenhttp/include/zenhttp/httpclient.h | 1 | ||||
| -rw-r--r-- | src/zenhttp/include/zenhttp/httpserver.h | 8 | ||||
| -rw-r--r-- | src/zenhttp/include/zenhttp/httpstats.h | 1 | ||||
| -rw-r--r-- | src/zenhttp/monitoring/httpstats.cpp | 154 |
8 files changed, 280 insertions, 58 deletions
diff --git a/src/zenhttp/clients/httpclientcommon.h b/src/zenhttp/clients/httpclientcommon.h index c30edab33..078d4a52f 100644 --- a/src/zenhttp/clients/httpclientcommon.h +++ b/src/zenhttp/clients/httpclientcommon.h @@ -62,6 +62,7 @@ public: LoggerRef Log() { return m_Log; } std::string_view GetBaseUri() const { return m_BaseUri; } + void SetBaseUri(std::string_view NewBaseUri) { m_BaseUri = NewBaseUri; } std::string_view GetSessionId() const { return m_SessionId; } bool Authenticate(); diff --git a/src/zenhttp/httpclient.cpp b/src/zenhttp/httpclient.cpp index 4000ea8a8..96107883e 100644 --- a/src/zenhttp/httpclient.cpp +++ b/src/zenhttp/httpclient.cpp @@ -402,6 +402,13 @@ HttpClient::~HttpClient() } void +HttpClient::SetBaseUri(std::string_view NewBaseUri) +{ + m_BaseUri = NewBaseUri; + m_Inner->SetBaseUri(NewBaseUri); +} + +void HttpClient::SetSessionId(const Oid& SessionId) { if (SessionId == Oid::Zero) @@ -980,6 +987,71 @@ TEST_CASE("httpclient.password") AsioServer->RequestExit(); } } +TEST_CASE("httpclient.setbaseuri") +{ + struct TestHttpService : public HttpService + { + explicit TestHttpService(std::string_view Identity) : m_Identity(Identity) {} + + virtual const char* BaseUri() const override { return "/test/"; } + virtual void HandleRequest(HttpServerRequest& Req) override + { + Req.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, m_Identity); + } + + std::string m_Identity; + }; + + ScopedTemporaryDirectory TmpDir1; + ScopedTemporaryDirectory TmpDir2; + TestHttpService Service1("server-one"); + TestHttpService Service2("server-two"); + + Ref<HttpServer> Server1 = CreateHttpAsioServer(AsioConfig{}); + Ref<HttpServer> Server2 = CreateHttpAsioServer(AsioConfig{}); + + int Port1 = Server1->Initialize(0, TmpDir1.Path()); + int Port2 = Server2->Initialize(0, TmpDir2.Path()); + REQUIRE(Port1 != -1); + REQUIRE(Port2 != -1); + + Server1->RegisterService(Service1); + Server2->RegisterService(Service2); + + std::thread Thread1([&]() { Server1->Run(false); }); + std::thread Thread2([&]() { Server2->Run(false); }); + + auto _ = MakeGuard([&]() { + if (Thread1.joinable()) + { + Thread1.join(); + } + if (Thread2.joinable()) + { + Thread2.join(); + } + Server1->Close(); + Server2->Close(); + }); + + HttpClient Client(fmt::format("127.0.0.1:{}", Port1), HttpClientSettings{}, {}); + CHECK_EQ(Client.GetBaseUri(), fmt::format("127.0.0.1:{}", Port1)); + + HttpClient::Response Resp1 = Client.Get("/test/hello"); + CHECK(Resp1.IsSuccess()); + CHECK_EQ(Resp1.AsText(), "server-one"); + + Client.SetBaseUri(fmt::format("127.0.0.1:{}", Port2)); + CHECK_EQ(Client.GetBaseUri(), fmt::format("127.0.0.1:{}", Port2)); + + HttpClient::Response Resp2 = Client.Get("/test/hello"); + CHECK(Resp2.IsSuccess()); + CHECK_EQ(Resp2.AsText(), "server-two"); + + Server1->RequestExit(); + Server2->RequestExit(); +} + TEST_SUITE_END(); void diff --git a/src/zenhttp/httpserver.cpp b/src/zenhttp/httpserver.cpp index a46c5b851..9fc42f18c 100644 --- a/src/zenhttp/httpserver.cpp +++ b/src/zenhttp/httpserver.cpp @@ -798,7 +798,18 @@ HttpRequestRouter::HandleRequest(zen::HttpServerRequest& Request) const HttpVerb Verb = Request.RequestVerb(); - std::string_view Uri = Request.RelativeUri(); + std::string_view Uri = Request.RelativeUri(); + + // Strip the separator slash left over after the service prefix is removed. + // When a service has BaseUri "/foo", the prefix length is set to len("/foo") = 4. + // Stripping 4 chars from "/foo/bar" yields "/bar" — the path separator becomes + // the first character of the relative URI. Remove it so patterns like "bar" or + // "{id}" match without needing to account for the leading slash. + if (!Uri.empty() && Uri.front() == '/') + { + Uri.remove_prefix(1); + } + HttpRouterRequest RouterRequest(Request); for (const MatcherEndpoint& Handler : m_MatcherEndpoints) @@ -974,6 +985,12 @@ HttpServer::SetHttpRequestFilter(IHttpRequestFilter* RequestFilter) OnSetHttpRequestFilter(RequestFilter); } +void +HttpServer::HandleStatsRequest(HttpServerRequest& Request) +{ + Request.WriteResponse(HttpResponseCode::OK, CollectStats()); +} + CbObject HttpServer::CollectStats() { @@ -1004,12 +1021,6 @@ HttpServer::CollectStats() return Cbo.Save(); } -void -HttpServer::HandleStatsRequest(HttpServerRequest& Request) -{ - Request.WriteResponse(HttpResponseCode::OK, CollectStats()); -} - ////////////////////////////////////////////////////////////////////////// HttpRpcHandler::HttpRpcHandler() @@ -1446,6 +1457,78 @@ TEST_CASE("http.common") } } + SUBCASE("router-leading-slash") + { + // Verify that HandleRequest strips the leading slash that server implementations + // leave in RelativeUri() when the service base URI has no trailing slash. + // e.g. BaseUri "/stats" + prefix-strip of "/stats/foo" yields "/foo", not "foo". + + bool HandledLiteral = false; + bool HandledPattern = false; + bool HandledTwoSeg = false; + std::vector<std::string> Captures; + auto Reset = [&] { + HandledLiteral = HandledPattern = HandledTwoSeg = false; + Captures.clear(); + }; + + TestHttpService Service; + HttpRequestRouter r; + + r.AddMatcher("seg", [](std::string_view In) -> bool { return !In.empty() && In.find('/') == std::string_view::npos; }); + + r.RegisterRoute( + "activity_counters", + [&](auto& /*Req*/) { HandledLiteral = true; }, + HttpVerb::kGet); + + r.RegisterRoute( + "{seg}", + [&](auto& Req) { + HandledPattern = true; + Captures = {std::string(Req.GetCapture(1))}; + }, + HttpVerb::kGet); + + r.RegisterRoute( + "prefix/{seg}", + [&](auto& Req) { + HandledTwoSeg = true; + Captures = {std::string(Req.GetCapture(1))}; + }, + HttpVerb::kGet); + + // Single-segment literal with leading slash — simulates real server RelativeUri + { + Reset(); + TestHttpServerRequest req{Service, "/activity_counters"sv}; + r.HandleRequest(req); + CHECK(HandledLiteral); + CHECK(!HandledPattern); + } + + // Single-segment pattern with leading slash + { + Reset(); + TestHttpServerRequest req{Service, "/hello"sv}; + r.HandleRequest(req); + CHECK(!HandledLiteral); + CHECK(HandledPattern); + REQUIRE_EQ(Captures.size(), 1); + CHECK_EQ(Captures[0], "hello"sv); + } + + // Two-segment route with leading slash — first literal segment + { + Reset(); + TestHttpServerRequest req{Service, "/prefix/world"sv}; + r.HandleRequest(req); + CHECK(HandledTwoSeg); + REQUIRE_EQ(Captures.size(), 1); + CHECK_EQ(Captures[0], "world"sv); + } + } + SUBCASE("content-type") { for (uint8_t i = 0; i < uint8_t(HttpContentType::kCOUNT); ++i) diff --git a/src/zenhttp/include/zenhttp/auth/authservice.h b/src/zenhttp/include/zenhttp/auth/authservice.h index 64b86e21f..ee67c0f5b 100644 --- a/src/zenhttp/include/zenhttp/auth/authservice.h +++ b/src/zenhttp/include/zenhttp/auth/authservice.h @@ -8,14 +8,14 @@ namespace zen { class AuthMgr; -class HttpAuthService final : public zen::HttpService +class HttpAuthService final : public HttpService { public: HttpAuthService(AuthMgr& AuthMgr); virtual ~HttpAuthService(); virtual const char* BaseUri() const override; - virtual void HandleRequest(zen::HttpServerRequest& Request) override; + virtual void HandleRequest(HttpServerRequest& Request) override; private: AuthMgr& m_AuthMgr; diff --git a/src/zenhttp/include/zenhttp/httpclient.h b/src/zenhttp/include/zenhttp/httpclient.h index b0d74951e..26d60b9ae 100644 --- a/src/zenhttp/include/zenhttp/httpclient.h +++ b/src/zenhttp/include/zenhttp/httpclient.h @@ -364,6 +364,7 @@ public: LoggerRef Log() { return m_Log; } std::string_view GetBaseUri() const { return m_BaseUri; } std::string_view GetSessionId() const { return m_SessionId; } + void SetBaseUri(std::string_view NewBaseUri); void SetSessionId(const Oid& SessionId); bool Authenticate(); diff --git a/src/zenhttp/include/zenhttp/httpserver.h b/src/zenhttp/include/zenhttp/httpserver.h index 633eb06be..5eaed6004 100644 --- a/src/zenhttp/include/zenhttp/httpserver.h +++ b/src/zenhttp/include/zenhttp/httpserver.h @@ -220,6 +220,12 @@ struct IHttpStatsProvider * not override this will be skipped in WebSocket broadcasts. */ virtual CbObject CollectStats() { return {}; } + + /** Return a number indicating activity. Increase the number + * when activity is detected. Example would be to return the + * number of received requests + */ + virtual uint64_t GetActivityCounter() { return 0; } }; struct IHttpStatsService @@ -302,8 +308,8 @@ public: } // IHttpStatsProvider - virtual CbObject CollectStats() override; virtual void HandleStatsRequest(HttpServerRequest& Request) override; + virtual CbObject CollectStats() override; private: std::vector<HttpService*> m_KnownServices; diff --git a/src/zenhttp/include/zenhttp/httpstats.h b/src/zenhttp/include/zenhttp/httpstats.h index 460315faf..bce771c75 100644 --- a/src/zenhttp/include/zenhttp/httpstats.h +++ b/src/zenhttp/include/zenhttp/httpstats.h @@ -62,6 +62,7 @@ private: std::atomic<bool> m_PushEnabled{false}; void BroadcastStats(); + void Initialize(); // Thread-based push (when no io_context is provided) std::thread m_PushThread; diff --git a/src/zenhttp/monitoring/httpstats.cpp b/src/zenhttp/monitoring/httpstats.cpp index 283cedca7..7e6207e56 100644 --- a/src/zenhttp/monitoring/httpstats.cpp +++ b/src/zenhttp/monitoring/httpstats.cpp @@ -16,6 +16,7 @@ HttpStatsService::HttpStatsService(bool EnableWebSockets) : m_Log(logging::Get(" m_PushEnabled.store(true); m_PushThread = std::thread([this] { PushThreadFunction(); }); } + Initialize(); } HttpStatsService::HttpStatsService(asio::io_context& IoContext, bool EnableWebSockets) : m_Log(logging::Get("stats")) @@ -26,6 +27,110 @@ HttpStatsService::HttpStatsService(asio::io_context& IoContext, bool EnableWebSo m_PushTimer = std::make_unique<asio::steady_timer>(IoContext); EnqueuePushTimer(); } + Initialize(); +} + +void +HttpStatsService::Initialize() +{ + m_Router.AddMatcher("handler_id", [](std::string_view Str) -> bool { + if (Str.empty()) + { + return false; + } + for (const auto C : Str) + { + if (std::isalnum(C) || C == '$') + { + // fine + } + else + { + // not fine + return false; + } + } + return true; + }); + + m_Router.RegisterRoute( + "activity_counters", + [this](HttpRouterRequest& Request) { + CbObjectWriter Obj; + + std::uint64_t SumActivity = 0; + + std::vector<std::pair<std::string, uint64_t>> Activities; + { + RwLock::SharedLockScope _(m_Lock); + Activities.reserve(m_Providers.size()); + for (const auto& It : m_Providers) + { + const std::string& HandlerName = It.first; + IHttpStatsProvider* Provider = It.second; + ZEN_ASSERT(Provider != nullptr); + uint64_t ProviderActivityCounter = Provider->GetActivityCounter(); + if (ProviderActivityCounter != 0) + { + Activities.push_back(std::make_pair(HandlerName, ProviderActivityCounter)); + } + SumActivity += ProviderActivityCounter; + } + } + + Obj.BeginArray("providers"); + for (const std::pair<std::string, uint64_t>& Activity : Activities) + { + const std::string& HandlerName = Activity.first; + uint64_t ProviderActivityCounter = Activity.second; + Obj.BeginObject(); + { + Obj.AddString("provider", HandlerName); + Obj.AddInteger("activity_counter", ProviderActivityCounter); + } + Obj.EndObject(); + } + Obj.EndArray(); + + Obj.AddInteger("sum", SumActivity); + + Request.ServerRequest().WriteResponse(HttpResponseCode::OK, Obj.Save()); + }, + HttpVerb::kGet); + + m_Router.RegisterRoute( + "{handler_id}", + [this](HttpRouterRequest& Request) { + std::string_view Handler = Request.GetCapture(1); + RwLock::SharedLockScope _(m_Lock); + if (auto It = m_Providers.find(std::string{Handler}); It != end(m_Providers)) + { + return It->second->HandleStatsRequest(Request.ServerRequest()); + } + Request.ServerRequest().WriteResponse(HttpResponseCode::NotFound); + }, + HttpVerb::kHead | HttpVerb::kGet); + + m_Router.RegisterRoute( + "", + [this](HttpRouterRequest& Request) { + CbObjectWriter Cbo; + + Cbo.BeginArray("providers"); + + { + RwLock::SharedLockScope _(m_Lock); + for (auto& Kv : m_Providers) + { + Cbo << Kv.first; + } + } + + Cbo.EndArray(); + + Request.ServerRequest().WriteResponse(HttpResponseCode::OK, Cbo.Save()); + }, + HttpVerb::kHead | HttpVerb::kGet); } HttpStatsService::~HttpStatsService() @@ -82,54 +187,7 @@ void HttpStatsService::HandleRequest(HttpServerRequest& Request) { ZEN_TRACE_CPU("HttpStatsService::HandleRequest"); - using namespace std::literals; - - std::string_view Key = Request.RelativeUri(); - - switch (Request.RequestVerb()) - { - case HttpVerb::kHead: - case HttpVerb::kGet: - { - if (Key.empty()) - { - CbObjectWriter Cbo; - - Cbo.BeginArray("providers"); - - { - RwLock::SharedLockScope _(m_Lock); - for (auto& Kv : m_Providers) - { - Cbo << Kv.first; - } - } - - Cbo.EndArray(); - - Request.WriteResponse(HttpResponseCode::OK, Cbo.Save()); - } - else if (Key[0] == '/') - { - Key.remove_prefix(1); - size_t SlashPos = Key.find_first_of("/?"); - if (SlashPos != std::string::npos) - { - Key = Key.substr(0, SlashPos); - } - - RwLock::SharedLockScope _(m_Lock); - if (auto It = m_Providers.find(std::string{Key}); It != end(m_Providers)) - { - return It->second->HandleStatsRequest(Request); - } - } - } - - [[fallthrough]]; - default: - return; - } + m_Router.HandleRequest(Request); } ////////////////////////////////////////////////////////////////////////// |