aboutsummaryrefslogtreecommitdiff
path: root/src/zenserver/proxy/httptrafficinspector.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/httptrafficinspector.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/httptrafficinspector.cpp')
-rw-r--r--src/zenserver/proxy/httptrafficinspector.cpp197
1 files changed, 197 insertions, 0 deletions
diff --git a/src/zenserver/proxy/httptrafficinspector.cpp b/src/zenserver/proxy/httptrafficinspector.cpp
new file mode 100644
index 000000000..74ecbfd48
--- /dev/null
+++ b/src/zenserver/proxy/httptrafficinspector.cpp
@@ -0,0 +1,197 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "httptrafficinspector.h"
+
+#include <zencore/logging.h>
+#include <zencore/string.h>
+
+#include <charconv>
+
+namespace zen {
+
+// clang-format off
+http_parser_settings HttpTrafficInspector::s_RequestSettings{
+ .on_message_begin = [](http_parser*) { return 0; },
+ .on_url = [](http_parser* p, const char* Data, size_t Len) { return GetThis(p)->OnUrl(Data, Len); },
+ .on_status = [](http_parser*, const char*, size_t) { return 0; },
+ .on_header_field = [](http_parser* p, const char* Data, size_t Len) { return GetThis(p)->OnHeaderField(Data, Len); },
+ .on_header_value = [](http_parser* p, const char* Data, size_t Len) { return GetThis(p)->OnHeaderValue(Data, Len); },
+ .on_headers_complete = [](http_parser* p) { return GetThis(p)->OnHeadersComplete(); },
+ .on_body = [](http_parser*, const char*, size_t) { return 0; },
+ .on_message_complete = [](http_parser* p) { return GetThis(p)->OnMessageComplete(); },
+ .on_chunk_header{},
+ .on_chunk_complete{}};
+
+http_parser_settings HttpTrafficInspector::s_ResponseSettings{
+ .on_message_begin = [](http_parser*) { return 0; },
+ .on_url = [](http_parser*, const char*, size_t) { return 0; },
+ .on_status = [](http_parser*, const char*, size_t) { return 0; },
+ .on_header_field = [](http_parser* p, const char* Data, size_t Len) { return GetThis(p)->OnHeaderField(Data, Len); },
+ .on_header_value = [](http_parser* p, const char* Data, size_t Len) { return GetThis(p)->OnHeaderValue(Data, Len); },
+ .on_headers_complete = [](http_parser* p) { return GetThis(p)->OnHeadersComplete(); },
+ .on_body = [](http_parser*, const char*, size_t) { return 0; },
+ .on_message_complete = [](http_parser* p) { return GetThis(p)->OnMessageComplete(); },
+ .on_chunk_header{},
+ .on_chunk_complete{}};
+// clang-format on
+
+HttpTrafficInspector::HttpTrafficInspector(Direction Dir, std::string_view SessionLabel)
+: m_Log(logging::Get("proxy.http"))
+, m_Direction(Dir)
+, m_SessionLabel(SessionLabel)
+{
+ http_parser_init(&m_Parser, Dir == Direction::Request ? HTTP_REQUEST : HTTP_RESPONSE);
+ m_Parser.data = this;
+}
+
+void
+HttpTrafficInspector::Inspect(const char* Data, size_t Length)
+{
+ if (m_Disabled)
+ {
+ return;
+ }
+
+ http_parser_settings* Settings = (m_Direction == Direction::Request) ? &s_RequestSettings : &s_ResponseSettings;
+
+ size_t Parsed = http_parser_execute(&m_Parser, Settings, Data, Length);
+
+ if (m_Parser.upgrade)
+ {
+ if (m_Direction == Direction::Request)
+ {
+ ZEN_DEBUG("[{}] >> {} {} (upgrade to WebSocket)", m_SessionLabel, m_Method, m_Url);
+ }
+ else
+ {
+ ZEN_DEBUG("[{}] << {} (upgrade to WebSocket)", m_SessionLabel, m_StatusCode);
+ }
+ ResetMessageState();
+ m_Upgraded.store(true, std::memory_order_relaxed);
+ m_Disabled = true;
+ return;
+ }
+
+ http_errno Error = HTTP_PARSER_ERRNO(&m_Parser);
+ if (Error != HPE_OK)
+ {
+ ZEN_DEBUG("[{}] non-HTTP traffic detected ({}), disabling inspection", m_SessionLabel, http_errno_name(Error));
+ m_Disabled = true;
+ }
+ else if (Parsed != Length)
+ {
+ ZEN_DEBUG("[{}] parser consumed {}/{} bytes, disabling inspection", m_SessionLabel, Parsed, Length);
+ m_Disabled = true;
+ }
+}
+
+int
+HttpTrafficInspector::OnUrl(const char* Data, size_t Length)
+{
+ m_Url.append(Data, Length);
+ return 0;
+}
+
+int
+HttpTrafficInspector::OnHeaderField(const char* Data, size_t Length)
+{
+ m_CurrentHeaderField.assign(Data, Length);
+ return 0;
+}
+
+int
+HttpTrafficInspector::OnHeaderValue(const char* Data, size_t Length)
+{
+ if (m_CurrentHeaderField.size() == 14 && StrCaseCompare(m_CurrentHeaderField.c_str(), "Content-Length", 14) == 0)
+ {
+ int64_t Value = 0;
+ std::from_chars(Data, Data + Length, Value);
+ m_ContentLength = Value;
+ }
+ else if (!m_SessionIdCaptured && m_CurrentHeaderField.size() == 10 &&
+ StrCaseCompare(m_CurrentHeaderField.c_str(), "UE-Session", 10) == 0)
+ {
+ Oid Parsed;
+ if (Oid::TryParse(std::string_view(Data, Length), Parsed))
+ {
+ m_SessionId = Parsed;
+ m_SessionIdCaptured = true;
+ }
+ }
+ m_CurrentHeaderField.clear();
+ return 0;
+}
+
+int
+HttpTrafficInspector::OnHeadersComplete()
+{
+ if (m_Direction == Direction::Request)
+ {
+ m_Method = http_method_str(static_cast<http_method>(m_Parser.method));
+ }
+ else
+ {
+ m_StatusCode = m_Parser.status_code;
+ }
+ return 0;
+}
+
+int
+HttpTrafficInspector::OnMessageComplete()
+{
+ if (m_Direction == Direction::Request)
+ {
+ if (m_ContentLength >= 0)
+ {
+ ZEN_DEBUG("[{}] >> {} {} (content-length: {})", m_SessionLabel, m_Method, m_Url, m_ContentLength);
+ }
+ else
+ {
+ ZEN_DEBUG("[{}] >> {} {}", m_SessionLabel, m_Method, m_Url);
+ }
+ }
+ else
+ {
+ if (m_ContentLength >= 0)
+ {
+ ZEN_DEBUG("[{}] << {} (content-length: {})", m_SessionLabel, m_StatusCode, m_ContentLength);
+ }
+ else
+ {
+ ZEN_DEBUG("[{}] << {}", m_SessionLabel, m_StatusCode);
+ }
+ }
+
+ if (m_Observer)
+ {
+ m_Observer->OnMessageComplete(m_Direction, m_Method, m_Url, m_StatusCode, m_ContentLength);
+ }
+
+ m_MessageCount.fetch_add(1, std::memory_order_relaxed);
+ ResetMessageState();
+ return 0;
+}
+
+Oid
+HttpTrafficInspector::GetSessionId() const
+{
+ return m_SessionId;
+}
+
+bool
+HttpTrafficInspector::HasSessionId() const
+{
+ return m_SessionIdCaptured;
+}
+
+void
+HttpTrafficInspector::ResetMessageState()
+{
+ m_Url.clear();
+ m_Method.clear();
+ m_StatusCode = 0;
+ m_ContentLength = -1;
+ m_CurrentHeaderField.clear();
+}
+
+} // namespace zen