aboutsummaryrefslogtreecommitdiff
path: root/src/zenhttp/include
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2026-04-23 18:16:57 +0200
committerStefan Boberg <[email protected]>2026-04-23 18:16:57 +0200
commit0232b991cd7d8e3a2114ea30e4591dd3e7b65c36 (patch)
tree94730e7594fd09ae1fa820391ce311f6daf13905 /src/zenhttp/include
parentFix forward declaration order for s_GotSigWinch and SigWinchHandler (diff)
parenttrace: declare Region event name fields as AnsiString (#1012) (diff)
downloadarchived-zen-sb/zen-help.tar.xz
archived-zen-sb/zen-help.zip
Merge branch 'main' into sb/zen-helpsb/zen-help
- Combine HelpCommand (this branch) with HistoryCommand (main) in zen CLI dispatcher - Keep filter-aware TuiPickOne rewrite; adopt main's ASCII arrow glyphs in doc comment
Diffstat (limited to 'src/zenhttp/include')
-rw-r--r--src/zenhttp/include/zenhttp/asynchttpclient.h123
-rw-r--r--src/zenhttp/include/zenhttp/httpclient.h7
-rw-r--r--src/zenhttp/include/zenhttp/httpclientauth.h3
-rw-r--r--src/zenhttp/include/zenhttp/httpcommon.h14
-rw-r--r--src/zenhttp/include/zenhttp/httpserver.h15
-rw-r--r--src/zenhttp/include/zenhttp/httpstats.h6
-rw-r--r--src/zenhttp/include/zenhttp/httpwsclient.h8
-rw-r--r--src/zenhttp/include/zenhttp/websocket.h2
-rw-r--r--src/zenhttp/include/zenhttp/zipfs.h35
9 files changed, 197 insertions, 16 deletions
diff --git a/src/zenhttp/include/zenhttp/asynchttpclient.h b/src/zenhttp/include/zenhttp/asynchttpclient.h
new file mode 100644
index 000000000..cb41626b9
--- /dev/null
+++ b/src/zenhttp/include/zenhttp/asynchttpclient.h
@@ -0,0 +1,123 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include "zenhttp.h"
+
+#include <zenhttp/httpclient.h>
+
+#include <functional>
+#include <future>
+#include <memory>
+
+namespace asio {
+class io_context;
+}
+
+namespace zen {
+
+/// Completion callback for async HTTP operations.
+using AsyncHttpCallback = std::function<void(HttpClient::Response)>;
+
+/** Asynchronous HTTP client backed by curl_multi and ASIO.
+ *
+ * Uses curl_multi_socket_action() driven by ASIO socket async_wait to process
+ * transfers without blocking the caller. All curl_multi operations are
+ * serialized on an internal strand; callers may issue requests from any
+ * thread, and the io_context may have multiple threads.
+ *
+ * Two construction modes:
+ * - Owned io_context: creates an internal thread (self-contained).
+ * - External io_context: caller runs the event loop.
+ *
+ * Completion callbacks are dispatched on the io_context (not the internal
+ * strand), so a slow callback will not block the curl poll loop. Future-
+ * based wrappers (Get, Post, ...) return a std::future<Response> for
+ * callers that prefer blocking on a result.
+ */
+class AsyncHttpClient
+{
+public:
+ using Response = HttpClient::Response;
+ using KeyValueMap = HttpClient::KeyValueMap;
+
+ /// Construct with an internally-owned io_context and thread.
+ explicit AsyncHttpClient(std::string_view BaseUri, const HttpClientSettings& Settings = {});
+
+ /// Construct with an externally-managed io_context. The io_context must
+ /// outlive this client and must be running (via run()) on at least one thread.
+ AsyncHttpClient(std::string_view BaseUri, asio::io_context& IoContext, const HttpClientSettings& Settings = {});
+
+ ~AsyncHttpClient();
+
+ AsyncHttpClient(const AsyncHttpClient&) = delete;
+ AsyncHttpClient& operator=(const AsyncHttpClient&) = delete;
+
+ // -- Callback-based API ----------------------------------------------
+
+ void AsyncGet(std::string_view Url,
+ AsyncHttpCallback Callback,
+ const KeyValueMap& AdditionalHeader = {},
+ const KeyValueMap& Parameters = {});
+
+ void AsyncHead(std::string_view Url, AsyncHttpCallback Callback, const KeyValueMap& AdditionalHeader = {});
+
+ void AsyncDelete(std::string_view Url, AsyncHttpCallback Callback, const KeyValueMap& AdditionalHeader = {});
+
+ void AsyncPost(std::string_view Url,
+ AsyncHttpCallback Callback,
+ const KeyValueMap& AdditionalHeader = {},
+ const KeyValueMap& Parameters = {});
+
+ void AsyncPost(std::string_view Url, const IoBuffer& Payload, AsyncHttpCallback Callback, const KeyValueMap& AdditionalHeader = {});
+
+ void AsyncPost(std::string_view Url,
+ const IoBuffer& Payload,
+ ZenContentType ContentType,
+ AsyncHttpCallback Callback,
+ const KeyValueMap& AdditionalHeader = {});
+
+ void AsyncPut(std::string_view Url,
+ const IoBuffer& Payload,
+ AsyncHttpCallback Callback,
+ const KeyValueMap& AdditionalHeader = {},
+ const KeyValueMap& Parameters = {});
+
+ void AsyncPut(std::string_view Url, AsyncHttpCallback Callback, const KeyValueMap& Parameters = {});
+
+ // -- Future-based API ------------------------------------------------
+
+ [[nodiscard]] std::future<Response> Get(std::string_view Url,
+ const KeyValueMap& AdditionalHeader = {},
+ const KeyValueMap& Parameters = {});
+
+ [[nodiscard]] std::future<Response> Head(std::string_view Url, const KeyValueMap& AdditionalHeader = {});
+
+ [[nodiscard]] std::future<Response> Delete(std::string_view Url, const KeyValueMap& AdditionalHeader = {});
+
+ [[nodiscard]] std::future<Response> Post(std::string_view Url,
+ const KeyValueMap& AdditionalHeader = {},
+ const KeyValueMap& Parameters = {});
+
+ [[nodiscard]] std::future<Response> Post(std::string_view Url, const IoBuffer& Payload, const KeyValueMap& AdditionalHeader = {});
+
+ [[nodiscard]] std::future<Response> Post(std::string_view Url,
+ const IoBuffer& Payload,
+ ZenContentType ContentType,
+ const KeyValueMap& AdditionalHeader = {});
+
+ [[nodiscard]] std::future<Response> Put(std::string_view Url,
+ const IoBuffer& Payload,
+ const KeyValueMap& AdditionalHeader = {},
+ const KeyValueMap& Parameters = {});
+
+ [[nodiscard]] std::future<Response> Put(std::string_view Url, const KeyValueMap& Parameters = {});
+
+private:
+ struct Impl;
+ std::unique_ptr<Impl> m_Impl;
+};
+
+void asynchttpclient_test_forcelink(); // internal
+
+} // namespace zen
diff --git a/src/zenhttp/include/zenhttp/httpclient.h b/src/zenhttp/include/zenhttp/httpclient.h
index e199b700f..8da94524e 100644
--- a/src/zenhttp/include/zenhttp/httpclient.h
+++ b/src/zenhttp/include/zenhttp/httpclient.h
@@ -120,6 +120,13 @@ struct HttpClientSettings
/// the system default CA store.
std::string CaBundlePath;
+ /// Automatically follow HTTP 3xx redirects. When true, curl handles
+ /// redirects internally (up to MaxRedirects hops). Default: false.
+ bool FollowRedirects = false;
+
+ /// Maximum number of redirects to follow when FollowRedirects is true.
+ int MaxRedirects = 5;
+
/// HTTP status codes that are expected and should not be logged as warnings.
/// 404 is always treated as expected regardless of this list.
std::vector<HttpResponseCode> ExpectedErrorCodes;
diff --git a/src/zenhttp/include/zenhttp/httpclientauth.h b/src/zenhttp/include/zenhttp/httpclientauth.h
index ce646ebd7..9220a50b6 100644
--- a/src/zenhttp/include/zenhttp/httpclientauth.h
+++ b/src/zenhttp/include/zenhttp/httpclientauth.h
@@ -33,7 +33,8 @@ namespace httpclientauth {
std::string_view CloudHost,
bool Quiet,
bool Unattended,
- bool Hidden);
+ bool Hidden,
+ bool IsHordeUrl = false);
} // namespace httpclientauth
} // namespace zen
diff --git a/src/zenhttp/include/zenhttp/httpcommon.h b/src/zenhttp/include/zenhttp/httpcommon.h
index f9a99f3cc..1d921600d 100644
--- a/src/zenhttp/include/zenhttp/httpcommon.h
+++ b/src/zenhttp/include/zenhttp/httpcommon.h
@@ -19,8 +19,8 @@ class StringBuilderBase;
struct HttpRange
{
- uint32_t Start = ~uint32_t(0);
- uint32_t End = ~uint32_t(0);
+ uint64_t Start = ~uint64_t(0);
+ uint64_t End = ~uint64_t(0);
};
using HttpRanges = std::vector<HttpRange>;
@@ -30,6 +30,16 @@ extern HttpContentType (*ParseContentType)(const std::string_view& ContentTypeSt
std::string_view ReasonStringForHttpResultCode(int HttpCode);
bool TryParseHttpRangeHeader(std::string_view RangeHeader, HttpRanges& Ranges);
+struct MultipartByteRangesResult
+{
+ std::vector<IoBuffer> Parts;
+ std::string ContentType;
+};
+
+// Build a multipart/byteranges response body from the given data and ranges.
+// Generates a unique boundary per call. Returns empty Parts if any range is out of bounds.
+MultipartByteRangesResult BuildMultipartByteRanges(const IoBuffer& Data, const HttpRanges& Ranges);
+
enum class HttpVerb : uint8_t
{
kGet = 1 << 0,
diff --git a/src/zenhttp/include/zenhttp/httpserver.h b/src/zenhttp/include/zenhttp/httpserver.h
index 76f219f04..955b8ed15 100644
--- a/src/zenhttp/include/zenhttp/httpserver.h
+++ b/src/zenhttp/include/zenhttp/httpserver.h
@@ -122,11 +122,13 @@ public:
virtual void WriteResponse(HttpResponseCode ResponseCode, HttpContentType ContentType, std::u8string_view ResponseString) = 0;
virtual void WriteResponse(HttpResponseCode ResponseCode, HttpContentType ContentType, CompositeBuffer& Payload);
+ void WriteResponse(HttpResponseCode ResponseCode, const std::string& CustomContentType, std::span<IoBuffer> Blobs);
void WriteResponse(HttpResponseCode ResponseCode, CbObject Data);
void WriteResponse(HttpResponseCode ResponseCode, CbArray Array);
void WriteResponse(HttpResponseCode ResponseCode, CbPackage Package);
void WriteResponse(HttpResponseCode ResponseCode, HttpContentType ContentType, std::string_view ResponseString);
void WriteResponse(HttpResponseCode ResponseCode, HttpContentType ContentType, IoBuffer Blob);
+ void WriteResponse(HttpContentType ContentType, const IoBuffer& Data, const HttpRanges& Ranges);
virtual void WriteResponseAsync(std::function<void(HttpServerRequest&)>&& ContinuationHandler) = 0;
@@ -152,6 +154,8 @@ protected:
std::string_view m_QueryString;
mutable uint32_t m_RequestId = ~uint32_t(0);
mutable Oid m_SessionId = Oid::Zero;
+ std::string m_ContentTypeOverride;
+ std::string m_ContentRangeHeader;
inline void SetIsHandled() { m_Flags |= kIsHandled; }
@@ -298,12 +302,12 @@ public:
std::string_view GetDefaultRedirect() const { return m_DefaultRedirect; }
- /** Track active WebSocket connections — called by server implementations on upgrade/close. */
+ /** Track active WebSocket connections - called by server implementations on upgrade/close. */
void OnWebSocketConnectionOpened() { m_ActiveWebSocketConnections.fetch_add(1, std::memory_order_relaxed); }
void OnWebSocketConnectionClosed() { m_ActiveWebSocketConnections.fetch_sub(1, std::memory_order_relaxed); }
uint64_t GetActiveWebSocketConnectionCount() const { return m_ActiveWebSocketConnections.load(std::memory_order_relaxed); }
- /** Track WebSocket frame and byte counters — called by WS connection implementations per frame. */
+ /** Track WebSocket frame and byte counters - called by WS connection implementations per frame. */
void OnWebSocketFrameReceived(uint64_t Bytes)
{
m_WsFramesReceived.fetch_add(1, std::memory_order_relaxed);
@@ -325,7 +329,7 @@ private:
int m_EffectiveHttpsPort = 0;
std::string m_ExternalHost;
metrics::Meter m_RequestMeter;
- metrics::HyperLogLog<12> m_ClientAddresses; // ~4 KiB, ~1.6% error — sufficient for client counting
+ metrics::HyperLogLog<12> m_ClientAddresses; // ~4 KiB, ~1.6% error - sufficient for client counting
metrics::HyperLogLog<12> m_ClientSessions;
std::string m_DefaultRedirect;
std::atomic<uint64_t> m_ActiveWebSocketConnections{0};
@@ -518,7 +522,8 @@ private:
bool HandlePackageOffers(HttpService& Service, HttpServerRequest& Request, Ref<IHttpPackageHandler>& PackageHandlerRef);
-void http_forcelink(); // internal
-void websocket_forcelink(); // internal
+void http_forcelink(); // internal
+void httpparser_forcelink(); // internal
+void websocket_forcelink(); // internal
} // namespace zen
diff --git a/src/zenhttp/include/zenhttp/httpstats.h b/src/zenhttp/include/zenhttp/httpstats.h
index bce771c75..51ab2e06e 100644
--- a/src/zenhttp/include/zenhttp/httpstats.h
+++ b/src/zenhttp/include/zenhttp/httpstats.h
@@ -23,11 +23,11 @@ namespace zen {
class HttpStatsService : public HttpService, public IHttpStatsService, public IWebSocketHandler
{
public:
- /// Construct without an io_context — optionally uses a dedicated push thread
+ /// Construct without an io_context - optionally uses a dedicated push thread
/// for WebSocket stats broadcasting.
explicit HttpStatsService(bool EnableWebSockets = false);
- /// Construct with an external io_context — uses an asio timer instead
+ /// Construct with an external io_context - uses an asio timer instead
/// of a dedicated thread for WebSocket stats broadcasting.
/// The caller must ensure the io_context outlives this service and that
/// its run loop is active.
@@ -43,7 +43,7 @@ public:
virtual void UnregisterHandler(std::string_view Id, IHttpStatsProvider& Provider) override;
// IWebSocketHandler
- void OnWebSocketOpen(Ref<WebSocketConnection> Connection) override;
+ void OnWebSocketOpen(Ref<WebSocketConnection> Connection, std::string_view RelativeUri) override;
void OnWebSocketMessage(WebSocketConnection& Conn, const WebSocketMessage& Msg) override;
void OnWebSocketClose(WebSocketConnection& Conn, uint16_t Code, std::string_view Reason) override;
diff --git a/src/zenhttp/include/zenhttp/httpwsclient.h b/src/zenhttp/include/zenhttp/httpwsclient.h
index 9c3b909a2..fd2f79171 100644
--- a/src/zenhttp/include/zenhttp/httpwsclient.h
+++ b/src/zenhttp/include/zenhttp/httpwsclient.h
@@ -26,7 +26,7 @@ namespace zen {
* Callback interface for WebSocket client events
*
* Separate from the server-side IWebSocketHandler because the caller
- * already owns the HttpWsClient — no Ref<WebSocketConnection> needed.
+ * already owns the HttpWsClient - no Ref<WebSocketConnection> needed.
*/
class IWsClientHandler
{
@@ -85,9 +85,9 @@ private:
/// it is treated as a plain host:port and gets the ws:// prefix.
///
/// Examples:
-/// HttpToWsUrl("http://host:8080", "/orch/ws") → "ws://host:8080/orch/ws"
-/// HttpToWsUrl("https://host", "/foo") → "wss://host/foo"
-/// HttpToWsUrl("host:8080", "/bar") → "ws://host:8080/bar"
+/// HttpToWsUrl("http://host:8080", "/orch/ws") -> "ws://host:8080/orch/ws"
+/// HttpToWsUrl("https://host", "/foo") -> "wss://host/foo"
+/// HttpToWsUrl("host:8080", "/bar") -> "ws://host:8080/bar"
std::string HttpToWsUrl(std::string_view Endpoint, std::string_view Path);
} // namespace zen
diff --git a/src/zenhttp/include/zenhttp/websocket.h b/src/zenhttp/include/zenhttp/websocket.h
index 710579faa..2d25515d3 100644
--- a/src/zenhttp/include/zenhttp/websocket.h
+++ b/src/zenhttp/include/zenhttp/websocket.h
@@ -59,7 +59,7 @@ class IWebSocketHandler
public:
virtual ~IWebSocketHandler() = default;
- virtual void OnWebSocketOpen(Ref<WebSocketConnection> Connection) = 0;
+ virtual void OnWebSocketOpen(Ref<WebSocketConnection> Connection, std::string_view RelativeUri) = 0;
virtual void OnWebSocketMessage(WebSocketConnection& Conn, const WebSocketMessage& Msg) = 0;
virtual void OnWebSocketClose(WebSocketConnection& Conn, uint16_t Code, std::string_view Reason) = 0;
};
diff --git a/src/zenhttp/include/zenhttp/zipfs.h b/src/zenhttp/include/zenhttp/zipfs.h
new file mode 100644
index 000000000..c6acf7334
--- /dev/null
+++ b/src/zenhttp/include/zenhttp/zipfs.h
@@ -0,0 +1,35 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include <zencore/iobuffer.h>
+#include <zencore/thread.h>
+
+#include <unordered_map>
+
+namespace zen {
+
+class ZipFs
+{
+public:
+ explicit ZipFs(IoBuffer&& Buffer);
+
+ IoBuffer GetFile(const std::string_view& FileName) const;
+
+private:
+ struct FileItem
+ {
+ MemoryView View; // Initially points to LFH (size=0); resolved to file data on first access
+ uint32_t CompressedSize = 0;
+ uint32_t UncompressedSize = 0;
+ uint16_t CompressionMethod = 0;
+ IoBuffer DecompressedData; // Owns decompressed buffer for deflate entries
+ };
+
+ using FileMap = std::unordered_map<std::string_view, FileItem>;
+ mutable RwLock m_FilesLock;
+ FileMap mutable m_Files;
+ IoBuffer m_Buffer;
+};
+
+} // namespace zen