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/httptrafficinspector.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/httptrafficinspector.cpp')
| -rw-r--r-- | src/zenserver/proxy/httptrafficinspector.cpp | 197 |
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 |