diff options
| author | Stefan Boberg <[email protected]> | 2026-03-12 15:03:03 +0100 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2026-03-12 15:03:03 +0100 |
| commit | 81bc43aa96f0059cecb28d1bd88338b7d84667f9 (patch) | |
| tree | a3428cb7fddceae0b284d33562af5bf3e64a367e /src/zenserver/proxy/httpproxystats.cpp | |
| parent | update fmt 12.0.0 -> 12.1.0 (#828) (diff) | |
| download | zen-81bc43aa96f0059cecb28d1bd88338b7d84667f9.tar.xz zen-81bc43aa96f0059cecb28d1bd88338b7d84667f9.zip | |
Transparent proxy mode (#823)
Adds a **transparent TCP proxy mode** to zenserver (activated via `zenserver proxy`), allowing it to sit between clients and upstream Zen servers to inspect and monitor HTTP/1.x traffic in real time. Primarily useful during development, to be able to observe multi-server/client interactions in one place.
- **Dedicated proxy port** -- Proxy mode defaults to port 8118 with its own data directory to avoid collisions with a normal zenserver instance.
- **TCP proxy core** (`src/zenserver/proxy/`) -- A new transparent TCP proxy that forwards connections to upstream targets, with support for both TCP/IP and Unix socket listeners. Multi-threaded I/O for connection handling. Supports Unix domain sockets for both upstream/downstream.
- **HTTP traffic inspection** -- Parses HTTP/1.x request/response streams inline to extract method, path, status, content length, and WebSocket upgrades without breaking the proxied data.
- **Proxy dashboard** -- A web UI showing live connection stats, per-target request counts, active connections, bytes transferred, and client IP/session ID rollups.
- **Server mode display** -- Dashboard banner now shows the running server mode (Zen Proxy, Zen Compute, etc.).
Supporting changes included in this branch:
- **Wildcard log level matching** -- Log levels can now be set per-category using wildcard patterns (e.g. `proxy.*=debug`).
- **`zen down --all`** -- New flag to shut down all running zenserver instances; also used by the new `xmake kill` task.
- Minor test stability fixes (flaky hash collisions, per-thread RNG seeds).
- Support ZEN_MALLOC environment variable for default allocator selection and switch default to rpmalloc
- Fixed sentry-native build to allow LTO on Windows
Diffstat (limited to 'src/zenserver/proxy/httpproxystats.cpp')
| -rw-r--r-- | src/zenserver/proxy/httpproxystats.cpp | 234 |
1 files changed, 234 insertions, 0 deletions
diff --git a/src/zenserver/proxy/httpproxystats.cpp b/src/zenserver/proxy/httpproxystats.cpp new file mode 100644 index 000000000..6aa3e5c9b --- /dev/null +++ b/src/zenserver/proxy/httpproxystats.cpp @@ -0,0 +1,234 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "httpproxystats.h" + +#include "tcpproxy.h" + +#include <zencore/compactbinarybuilder.h> +#include <zencore/fmtutils.h> + +#include <chrono> +#include <filesystem> + +namespace zen { + +HttpProxyStatsService::HttpProxyStatsService(const std::vector<std::unique_ptr<TcpProxyService>>& ProxyServices, + IHttpStatsService& StatsService, + std::string DefaultRecordDir) +: m_ProxyServices(ProxyServices) +, m_StatsService(StatsService) +, m_DefaultRecordDir(std::move(DefaultRecordDir)) +{ + m_StatsService.RegisterHandler("proxy", *this); +} + +HttpProxyStatsService::~HttpProxyStatsService() +{ + m_StatsService.UnregisterHandler("proxy", *this); +} + +const char* +HttpProxyStatsService::BaseUri() const +{ + return "/proxy/"; +} + +void +HttpProxyStatsService::HandleRequest(HttpServerRequest& Request) +{ + std::string_view Uri = Request.RelativeUri(); + + if (Uri == "stats" || Uri == "stats/") + { + HandleStatsRequest(Request); + } + else if (Uri == "record/start" || Uri == "record/start/") + { + HandleRecordStart(Request); + } + else if (Uri == "record/stop" || Uri == "record/stop/") + { + HandleRecordStop(Request); + } + else if (Uri == "record" || Uri == "record/") + { + HandleRecordStatus(Request); + } + else + { + Request.WriteResponse(HttpResponseCode::NotFound); + } +} + +void +HttpProxyStatsService::HandleRecordStart(HttpServerRequest& Request) +{ + if (Request.RequestVerb() != HttpVerb::kPost) + { + Request.WriteResponse(HttpResponseCode::MethodNotAllowed); + return; + } + + auto Params = Request.GetQueryParams(); + std::string_view Dir = Params.GetValue("dir"); + + std::string RecordDir; + if (Dir.empty()) + { + RecordDir = m_DefaultRecordDir; + } + else + { + // Treat dir as a subdirectory name within the default record directory. + // Reject path separators and parent references to prevent path traversal. + if (Dir.find("..") != std::string_view::npos || Dir.find('/') != std::string_view::npos || Dir.find('\\') != std::string_view::npos) + { + Request.WriteResponse(HttpResponseCode::BadRequest); + return; + } + RecordDir = (std::filesystem::path(m_DefaultRecordDir) / std::string(Dir)).string(); + } + + for (const std::unique_ptr<TcpProxyService>& Service : m_ProxyServices) + { + Service->SetRecording(true, RecordDir); + } + + CbObjectWriter Cbo; + Cbo << "recording" << true; + Cbo << "dir" << std::string_view(RecordDir); + Request.WriteResponse(HttpResponseCode::OK, Cbo.Save()); +} + +void +HttpProxyStatsService::HandleRecordStop(HttpServerRequest& Request) +{ + if (Request.RequestVerb() != HttpVerb::kPost) + { + Request.WriteResponse(HttpResponseCode::MethodNotAllowed); + return; + } + + for (const std::unique_ptr<TcpProxyService>& Service : m_ProxyServices) + { + Service->SetRecording(false, Service->GetRecordDir()); + } + + CbObjectWriter Cbo; + Cbo << "recording" << false; + Request.WriteResponse(HttpResponseCode::OK, Cbo.Save()); +} + +void +HttpProxyStatsService::HandleRecordStatus(HttpServerRequest& Request) +{ + bool IsRecording = false; + std::string RecordDir; + for (const std::unique_ptr<TcpProxyService>& Service : m_ProxyServices) + { + if (Service->IsRecording()) + { + IsRecording = true; + RecordDir = Service->GetRecordDir(); + break; + } + } + + CbObjectWriter Cbo; + Cbo << "recording" << IsRecording; + Cbo << "dir" << std::string_view(RecordDir); + Request.WriteResponse(HttpResponseCode::OK, Cbo.Save()); +} + +CbObject +HttpProxyStatsService::CollectStats() +{ + CbObjectWriter Cbo; + + // Include recording status in stats output. + { + bool IsRecording = false; + std::string RecordDir; + for (const std::unique_ptr<TcpProxyService>& Service : m_ProxyServices) + { + if (Service->IsRecording()) + { + IsRecording = true; + RecordDir = Service->GetRecordDir(); + break; + } + } + Cbo << "recording" << IsRecording; + Cbo << "recordDir" << std::string_view(RecordDir); + } + + Cbo.BeginArray("mappings"); + for (const std::unique_ptr<TcpProxyService>& Service : m_ProxyServices) + { + const ProxyMapping& Mapping = Service->GetMapping(); + + Cbo.BeginObject(); + { + std::string ListenAddr = Mapping.ListenDescription(); + Cbo << "listen" << std::string_view(ListenAddr); + + std::string TargetAddr = Mapping.TargetDescription(); + Cbo << "target" << std::string_view(TargetAddr); + + Cbo << "activeConnections" << Service->GetActiveConnections(); + Cbo << "peakActiveConnections" << Service->GetPeakActiveConnections(); + Cbo << "totalConnections" << Service->GetTotalConnections(); + Cbo << "bytesFromClient" << Service->GetTotalBytesFromClient(); + Cbo << "bytesToClient" << Service->GetTotalBytesToClient(); + + Cbo << "requestRate1" << Service->GetRequestMeter().Rate1(); + Cbo << "byteRate1" << Service->GetBytesMeter().Rate1(); + Cbo << "byteRate5" << Service->GetBytesMeter().Rate5(); + + auto Now = std::chrono::steady_clock::now(); + auto Sessions = Service->GetActiveSessions(); + + Cbo.BeginArray("connections"); + for (const auto& Session : Sessions) + { + Cbo.BeginObject(); + { + std::string ClientLabel = Session->GetClientLabel(); + Cbo << "client" << std::string_view(ClientLabel); + + std::string TargetLabel = Mapping.TargetDescription(); + Cbo << "target" << std::string_view(TargetLabel); + + Cbo << "bytesFromClient" << Session->GetBytesFromClient(); + Cbo << "bytesToClient" << Session->GetBytesToClient(); + + Cbo << "requests" << Session->GetRequestCount(); + Cbo << "websocket" << Session->IsWebSocket(); + + if (Session->HasSessionId()) + { + std::string SessionId = Session->GetSessionId().ToString(); + Cbo << "sessionId" << std::string_view(SessionId); + } + + double DurationMs = std::chrono::duration<double, std::milli>(Now - Session->GetStartTime()).count(); + Cbo << "durationMs" << DurationMs; + } + Cbo.EndObject(); + } + Cbo.EndArray(); + } + Cbo.EndObject(); + } + Cbo.EndArray(); + + return Cbo.Save(); +} + +void +HttpProxyStatsService::HandleStatsRequest(HttpServerRequest& Request) +{ + Request.WriteResponse(HttpResponseCode::OK, CollectStats()); +} + +} // namespace zen |