aboutsummaryrefslogtreecommitdiff
path: root/src/zenserver/proxy/httptrafficrecorder.cpp
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2026-03-12 15:03:03 +0100
committerGitHub Enterprise <[email protected]>2026-03-12 15:03:03 +0100
commit81bc43aa96f0059cecb28d1bd88338b7d84667f9 (patch)
treea3428cb7fddceae0b284d33562af5bf3e64a367e /src/zenserver/proxy/httptrafficrecorder.cpp
parentupdate fmt 12.0.0 -> 12.1.0 (#828) (diff)
downloadzen-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/httptrafficrecorder.cpp')
-rw-r--r--src/zenserver/proxy/httptrafficrecorder.cpp191
1 files changed, 191 insertions, 0 deletions
diff --git a/src/zenserver/proxy/httptrafficrecorder.cpp b/src/zenserver/proxy/httptrafficrecorder.cpp
new file mode 100644
index 000000000..0279555a0
--- /dev/null
+++ b/src/zenserver/proxy/httptrafficrecorder.cpp
@@ -0,0 +1,191 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "httptrafficrecorder.h"
+
+#include <zencore/compactbinarybuilder.h>
+#include <zencore/logging.h>
+
+#include <chrono>
+#include <filesystem>
+
+namespace zen {
+
+HttpTrafficRecorder::HttpTrafficRecorder(const std::filesystem::path& OutputDir, std::string_view ClientLabel, std::string_view TargetLabel)
+: m_Log(logging::Get("proxy.record"))
+, m_Dir(OutputDir)
+, m_ClientLabel(ClientLabel)
+, m_TargetLabel(TargetLabel)
+{
+ auto Now = std::chrono::system_clock::now();
+ m_StartTimeMs = uint64_t(std::chrono::duration_cast<std::chrono::milliseconds>(Now.time_since_epoch()).count());
+
+ std::error_code Ec;
+ std::filesystem::create_directories(m_Dir, Ec);
+ if (Ec)
+ {
+ ZEN_WARN("failed to create recording directory {} - {}", m_Dir.string(), Ec.message());
+ return;
+ }
+
+ std::error_code ReqEc;
+ m_RequestFile.Open(m_Dir / "request.bin", BasicFile::Mode::kTruncate, ReqEc);
+ if (ReqEc)
+ {
+ ZEN_WARN("failed to open request.bin in {} - {}", m_Dir.string(), ReqEc.message());
+ return;
+ }
+
+ std::error_code RespEc;
+ m_ResponseFile.Open(m_Dir / "response.bin", BasicFile::Mode::kTruncate, RespEc);
+ if (RespEc)
+ {
+ ZEN_WARN("failed to open response.bin in {} - {}", m_Dir.string(), RespEc.message());
+ m_RequestFile.Close();
+ return;
+ }
+
+ m_Valid = true;
+ ZEN_DEBUG("recording started in {}", m_Dir.string());
+}
+
+HttpTrafficRecorder::~HttpTrafficRecorder()
+{
+ if (m_Valid && !m_Finalized)
+ {
+ Finalize(false, Oid::Zero);
+ }
+}
+
+void
+HttpTrafficRecorder::WriteRequest(const char* Data, size_t Length)
+{
+ if (!m_Valid)
+ {
+ return;
+ }
+ m_RequestFile.Write(Data, Length, m_RequestOffset);
+ m_RequestOffset += Length;
+}
+
+void
+HttpTrafficRecorder::WriteResponse(const char* Data, size_t Length)
+{
+ if (!m_Valid)
+ {
+ return;
+ }
+ m_ResponseFile.Write(Data, Length, m_ResponseOffset);
+ m_ResponseOffset += Length;
+}
+
+void
+HttpTrafficRecorder::OnMessageComplete(HttpTrafficInspector::Direction Dir,
+ std::string_view Method,
+ std::string_view Url,
+ uint16_t StatusCode,
+ int64_t /*ContentLength*/)
+{
+ if (!m_Valid)
+ {
+ return;
+ }
+
+ if (Dir == HttpTrafficInspector::Direction::Request)
+ {
+ // Record the request boundary. The request spans from m_CurrentRequestStart to m_RequestOffset.
+ m_PendingReqOffset = m_CurrentRequestStart;
+ m_PendingReqSize = m_RequestOffset - m_CurrentRequestStart;
+ m_PendingMethod = Method;
+ m_PendingUrl = Url;
+ m_HasPendingRequest = true;
+
+ // Advance start to current offset for the next request.
+ m_CurrentRequestStart = m_RequestOffset;
+ }
+ else
+ {
+ // Response complete -- pair with pending request.
+ RecordedEntry Entry;
+ if (m_HasPendingRequest)
+ {
+ Entry.ReqOffset = m_PendingReqOffset;
+ Entry.ReqSize = m_PendingReqSize;
+ Entry.Method = std::move(m_PendingMethod);
+ Entry.Url = std::move(m_PendingUrl);
+ m_HasPendingRequest = false;
+ }
+
+ Entry.RespOffset = m_CurrentResponseStart;
+ Entry.RespSize = m_ResponseOffset - m_CurrentResponseStart;
+ Entry.Status = StatusCode;
+
+ m_Entries.push_back(std::move(Entry));
+
+ // Advance start to current offset for the next response.
+ m_CurrentResponseStart = m_ResponseOffset;
+ }
+}
+
+void
+HttpTrafficRecorder::Finalize(bool WebSocket, const Oid& SessionId)
+{
+ if (!m_Valid || m_Finalized)
+ {
+ return;
+ }
+ m_Finalized = true;
+
+ m_RequestFile.Close();
+ m_ResponseFile.Close();
+
+ // Write index.ucb as a CbObject.
+ CbObjectWriter Cbo;
+
+ Cbo << "client" << std::string_view(m_ClientLabel);
+ Cbo << "target" << std::string_view(m_TargetLabel);
+ Cbo << "startTime" << m_StartTimeMs;
+ Cbo << "websocket" << WebSocket;
+ if (SessionId != Oid::Zero)
+ {
+ std::string SessionIdStr = SessionId.ToString();
+ Cbo << "sessionId" << std::string_view(SessionIdStr);
+ }
+
+ Cbo.BeginArray("entries");
+ for (const RecordedEntry& Entry : m_Entries)
+ {
+ Cbo.BeginObject();
+ Cbo << "reqOffset" << Entry.ReqOffset;
+ Cbo << "reqSize" << Entry.ReqSize;
+ Cbo << "respOffset" << Entry.RespOffset;
+ Cbo << "respSize" << Entry.RespSize;
+ Cbo << "method" << std::string_view(Entry.Method);
+ Cbo << "url" << std::string_view(Entry.Url);
+ Cbo << "status" << Entry.Status;
+ Cbo.EndObject();
+ }
+ Cbo.EndArray();
+
+ CbObject IndexObj = Cbo.Save();
+
+ MemoryView View = IndexObj.GetView();
+ std::error_code Ec;
+ BasicFile IndexFile(m_Dir / "index.ucb", BasicFile::Mode::kTruncate, Ec);
+ if (!Ec)
+ {
+ IndexFile.Write(View, 0, Ec);
+ if (Ec)
+ {
+ ZEN_WARN("failed to write index.ucb in {} - {}", m_Dir.string(), Ec.message());
+ }
+ IndexFile.Close();
+ }
+ else
+ {
+ ZEN_WARN("failed to create index.ucb in {} - {}", m_Dir.string(), Ec.message());
+ }
+
+ ZEN_DEBUG("recording finalized in {} ({} entries, websocket: {})", m_Dir.string(), m_Entries.size(), WebSocket);
+}
+
+} // namespace zen