aboutsummaryrefslogtreecommitdiff
path: root/src/zenhttp
diff options
context:
space:
mode:
Diffstat (limited to 'src/zenhttp')
-rw-r--r--src/zenhttp/clients/httpclientcommon.h1
-rw-r--r--src/zenhttp/httpclient.cpp72
-rw-r--r--src/zenhttp/httpserver.cpp97
-rw-r--r--src/zenhttp/include/zenhttp/auth/authservice.h4
-rw-r--r--src/zenhttp/include/zenhttp/httpclient.h1
-rw-r--r--src/zenhttp/include/zenhttp/httpserver.h8
-rw-r--r--src/zenhttp/include/zenhttp/httpstats.h1
-rw-r--r--src/zenhttp/monitoring/httpstats.cpp154
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);
}
//////////////////////////////////////////////////////////////////////////