aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDan Engelbrecht <[email protected]>2026-02-13 15:19:51 +0100
committerGitHub Enterprise <[email protected]>2026-02-13 15:19:51 +0100
commitdf97b6b2abcc8ce13b1d63e3d2cf27c3bd841768 (patch)
treecd7b89d4a68520ef01a7fb23bc2fb2386013588b
parentspelling fixes (#755) (diff)
downloadzen-df97b6b2abcc8ce13b1d63e3d2cf27c3bd841768.tar.xz
zen-df97b6b2abcc8ce13b1d63e3d2cf27c3bd841768.zip
add foundation for http password protection (#756)
-rw-r--r--src/zenhttp/include/zenhttp/security/passwordsecurity.h52
-rw-r--r--src/zenhttp/security/passwordsecurity.cpp221
-rw-r--r--src/zenhttp/zenhttp.cpp2
3 files changed, 275 insertions, 0 deletions
diff --git a/src/zenhttp/include/zenhttp/security/passwordsecurity.h b/src/zenhttp/include/zenhttp/security/passwordsecurity.h
new file mode 100644
index 000000000..026c2865b
--- /dev/null
+++ b/src/zenhttp/include/zenhttp/security/passwordsecurity.h
@@ -0,0 +1,52 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include <zencore/compactbinary.h>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <tsl/robin_map.h>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+namespace zen {
+
+struct PasswordSecurityConfiguration
+{
+ std::string Password; // "password"
+ bool ProtectMachineLocalRequests = false; // "protect-machine-local-requests"
+ std::vector<std::string> UnprotectedUris; // "unprotected-urls"
+};
+
+class PasswordSecurity
+{
+public:
+ PasswordSecurity(const PasswordSecurityConfiguration& Config);
+
+ [[nodiscard]] inline std::string_view Password() const { return m_Config.Password; }
+ [[nodiscard]] inline bool ProtectMachineLocalRequests() const { return m_Config.ProtectMachineLocalRequests; }
+ [[nodiscard]] bool IsUnprotectedUri(std::string_view Uri) const;
+
+ bool IsAllowed(std::string_view Password, std::string_view Uri, bool IsMachineLocalRequest);
+
+private:
+ const PasswordSecurityConfiguration m_Config;
+ tsl::robin_map<uint32_t, uint32_t> m_UnprotectedUrlHashes;
+};
+
+/**
+ * Expected format (Json)
+ * {
+ * "password\": \"1234\",
+ * "protect-machine-local-requests\": false,
+ * "unprotected-urls\": [
+ * "/health\",
+ * "/health/info\",
+ * "/health/version\"
+ * ]
+ * }
+ */
+PasswordSecurityConfiguration ReadPasswordSecurityConfiguration(CbObjectView ConfigObject);
+
+void passwordsecurity_forcelink(); // internal
+
+} // namespace zen
diff --git a/src/zenhttp/security/passwordsecurity.cpp b/src/zenhttp/security/passwordsecurity.cpp
new file mode 100644
index 000000000..37be9a018
--- /dev/null
+++ b/src/zenhttp/security/passwordsecurity.cpp
@@ -0,0 +1,221 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "zenhttp/security/passwordsecurity.h"
+#include <zencore/compactbinaryutil.h>
+#include <zencore/fmtutils.h>
+#include <zencore/string.h>
+
+#if ZEN_WITH_TESTS
+# include <zencore/compactbinarybuilder.h>
+# include <zencore/testing.h>
+#endif // ZEN_WITH_TESTS
+
+namespace zen {
+using namespace std::literals;
+
+PasswordSecurity::PasswordSecurity(const PasswordSecurityConfiguration& Config) : m_Config(Config)
+{
+ m_UnprotectedUrlHashes.reserve(m_Config.UnprotectedUris.size());
+ for (uint32_t Index = 0; Index < m_Config.UnprotectedUris.size(); Index++)
+ {
+ const std::string& UnprotectedUri = m_Config.UnprotectedUris[Index];
+ if (auto Result = m_UnprotectedUrlHashes.insert({HashStringDjb2(UnprotectedUri), Index}); !Result.second)
+ {
+ throw std::runtime_error(fmt::format(
+ "password security unprotected uris does not generate unique hashes. Uri #{} ('{}') collides with uri #{} ('{}')",
+ Index + 1,
+ UnprotectedUri,
+ Result.first->second + 1,
+ m_Config.UnprotectedUris[Result.first->second]));
+ }
+ }
+}
+
+bool
+PasswordSecurity::IsUnprotectedUri(std::string_view Uri) const
+{
+ if (!m_Config.UnprotectedUris.empty())
+ {
+ uint32_t UriHash = HashStringDjb2(Uri);
+ if (auto It = m_UnprotectedUrlHashes.find(UriHash); It != m_UnprotectedUrlHashes.end())
+ {
+ if (m_Config.UnprotectedUris[It->second] == Uri)
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+PasswordSecurityConfiguration
+ReadPasswordSecurityConfiguration(CbObjectView ConfigObject)
+{
+ return PasswordSecurityConfiguration{
+ .Password = std::string(ConfigObject["password"sv].AsString()),
+ .ProtectMachineLocalRequests = ConfigObject["protect-machine-local-requests"sv].AsBool(),
+ .UnprotectedUris = compactbinary_helpers::ReadArray<std::string>("unprotected-urls"sv, ConfigObject)};
+}
+
+bool
+PasswordSecurity::IsAllowed(std::string_view InPassword, std::string_view Uri, bool IsMachineLocalRequest)
+{
+ if (IsUnprotectedUri(Uri))
+ {
+ return true;
+ }
+ if (!ProtectMachineLocalRequests() && IsMachineLocalRequest)
+ {
+ return true;
+ }
+ if (Password().empty())
+ {
+ return true;
+ }
+ if (Password() == InPassword)
+ {
+ return true;
+ }
+ return false;
+}
+
+#if ZEN_WITH_TESTS
+
+TEST_CASE("passwordsecurity.readconfig")
+{
+ auto ReadConfigJson = [](std::string_view Json) {
+ std::string JsonError;
+ CbObject Config = LoadCompactBinaryFromJson(Json, JsonError).AsObject();
+ REQUIRE(JsonError.empty());
+ return Config;
+ };
+
+ {
+ PasswordSecurityConfiguration EmptyConfig = ReadPasswordSecurityConfiguration(CbObject());
+ CHECK(EmptyConfig.Password.empty());
+ CHECK(!EmptyConfig.ProtectMachineLocalRequests);
+ CHECK(EmptyConfig.UnprotectedUris.empty());
+ }
+
+ {
+ const std::string_view SimpleConfigJson =
+ "{\n"
+ " \"password\": \"1234\"\n"
+ "}";
+ PasswordSecurityConfiguration SimpleConfig = ReadPasswordSecurityConfiguration(ReadConfigJson(SimpleConfigJson));
+ CHECK(SimpleConfig.Password == "1234");
+ CHECK(!SimpleConfig.ProtectMachineLocalRequests);
+ CHECK(SimpleConfig.UnprotectedUris.empty());
+ }
+
+ {
+ const std::string_view ComplexConfigJson =
+ "{\n"
+ " \"password\": \"1234\",\n"
+ " \"protect-machine-local-requests\": true,\n"
+ " \"unprotected-urls\": [\n"
+ " \"/health\",\n"
+ " \"/health/info\",\n"
+ " \"/health/version\"\n"
+ " ]\n"
+ "}";
+ PasswordSecurityConfiguration ComplexConfig = ReadPasswordSecurityConfiguration(ReadConfigJson(ComplexConfigJson));
+ CHECK(ComplexConfig.Password == "1234");
+ CHECK(ComplexConfig.ProtectMachineLocalRequests);
+ CHECK(ComplexConfig.UnprotectedUris == std::vector<std::string>({"/health", "/health/info", "/health/version"}));
+ }
+}
+
+TEST_CASE("passwordsecurity.allowanything")
+{
+ PasswordSecurity Anything({});
+ CHECK(Anything.IsAllowed(""sv, "/supersecret/uri", /*IsMachineLocalRequest*/ false));
+ CHECK(Anything.IsAllowed(""sv, "/supersecret/uri", /*IsMachineLocalRequest*/ true));
+ CHECK(Anything.IsAllowed("thewrongpassword"sv, "/supersecret/uri", /*IsMachineLocalRequest*/ false));
+ CHECK(Anything.IsAllowed("thewrongpassword"sv, "/supersecret/uri", /*IsMachineLocalRequest*/ true));
+}
+
+TEST_CASE("passwordsecurity.allowalllocal")
+{
+ PasswordSecurity AllLocal({.Password = "123456"});
+ CHECK(AllLocal.IsAllowed(""sv, "/supersecret/uri", /*IsMachineLocalRequest*/ true));
+ CHECK(!AllLocal.IsAllowed(""sv, "/supersecret/uri", /*IsMachineLocalRequest*/ false));
+ CHECK(AllLocal.IsAllowed("thewrongpassword"sv, "/supersecret/uri", /*IsMachineLocalRequest*/ true));
+ CHECK(!AllLocal.IsAllowed("thewrongpassword"sv, "/supersecret/uri", /*IsMachineLocalRequest*/ false));
+ CHECK(AllLocal.IsAllowed("123456"sv, "/supersecret/uri", /*IsMachineLocalRequest*/ true));
+}
+
+TEST_CASE("passwordsecurity.allowonlypassword")
+{
+ PasswordSecurity AllLocal({.Password = "123456", .ProtectMachineLocalRequests = true});
+ CHECK(!AllLocal.IsAllowed(""sv, "/supersecret/uri", /*IsMachineLocalRequest*/ true));
+ CHECK(AllLocal.IsAllowed("123456"sv, "/supersecret/uri", /*IsMachineLocalRequest*/ true));
+ CHECK(!AllLocal.IsAllowed(""sv, "/supersecret/uri", /*IsMachineLocalRequest*/ false));
+ CHECK(!AllLocal.IsAllowed("thewrongpassword"sv, "/supersecret/uri", /*IsMachineLocalRequest*/ true));
+ CHECK(!AllLocal.IsAllowed("thewrongpassword"sv, "/supersecret/uri", /*IsMachineLocalRequest*/ false));
+ CHECK(AllLocal.IsAllowed("123456"sv, "/supersecret/uri", /*IsMachineLocalRequest*/ false));
+}
+
+TEST_CASE("passwordsecurity.allowsomeexternaluris")
+{
+ PasswordSecurity AllLocal(
+ {.Password = "123456", .ProtectMachineLocalRequests = false, .UnprotectedUris = std::vector<std::string>({"/free/access", "/ok"})});
+ CHECK(AllLocal.IsAllowed(""sv, "/supersecret/uri", /*IsMachineLocalRequest*/ true));
+ CHECK(AllLocal.IsAllowed("123456"sv, "/supersecret/uri", /*IsMachineLocalRequest*/ true));
+ CHECK(!AllLocal.IsAllowed(""sv, "/supersecret/uri", /*IsMachineLocalRequest*/ false));
+ CHECK(AllLocal.IsAllowed("thewrongpassword"sv, "/supersecret/uri", /*IsMachineLocalRequest*/ true));
+ CHECK(!AllLocal.IsAllowed("thewrongpassword"sv, "/supersecret/uri", /*IsMachineLocalRequest*/ false));
+ CHECK(AllLocal.IsAllowed(""sv, "/free/access", /*IsMachineLocalRequest*/ true));
+ CHECK(AllLocal.IsAllowed(""sv, "/ok", /*IsMachineLocalRequest*/ true));
+ CHECK(AllLocal.IsAllowed("thewrongpassword"sv, "/free/access", /*IsMachineLocalRequest*/ true));
+ CHECK(AllLocal.IsAllowed("thewrongpassword"sv, "/ok", /*IsMachineLocalRequest*/ true));
+ CHECK(AllLocal.IsAllowed(""sv, "/free/access", /*IsMachineLocalRequest*/ false));
+ CHECK(AllLocal.IsAllowed(""sv, "/ok", /*IsMachineLocalRequest*/ false));
+ CHECK(AllLocal.IsAllowed("thewrongpassword"sv, "/free/access", /*IsMachineLocalRequest*/ false));
+ CHECK(AllLocal.IsAllowed("thewrongpassword"sv, "/ok", /*IsMachineLocalRequest*/ false));
+ CHECK(AllLocal.IsAllowed("123456"sv, "/supersecret/uri", /*IsMachineLocalRequest*/ false));
+}
+
+TEST_CASE("passwordsecurity.allowsomelocaluris")
+{
+ PasswordSecurity AllLocal(
+ {.Password = "123456", .ProtectMachineLocalRequests = true, .UnprotectedUris = std::vector<std::string>({"/free/access", "/ok"})});
+ CHECK(!AllLocal.IsAllowed(""sv, "/supersecret/uri", /*IsMachineLocalRequest*/ true));
+ CHECK(AllLocal.IsAllowed("123456"sv, "/supersecret/uri", /*IsMachineLocalRequest*/ true));
+ CHECK(!AllLocal.IsAllowed(""sv, "/supersecret/uri", /*IsMachineLocalRequest*/ false));
+ CHECK(!AllLocal.IsAllowed("thewrongpassword"sv, "/supersecret/uri", /*IsMachineLocalRequest*/ true));
+ CHECK(!AllLocal.IsAllowed("thewrongpassword"sv, "/supersecret/uri", /*IsMachineLocalRequest*/ false));
+ CHECK(AllLocal.IsAllowed(""sv, "/free/access", /*IsMachineLocalRequest*/ true));
+ CHECK(AllLocal.IsAllowed(""sv, "/ok", /*IsMachineLocalRequest*/ true));
+ CHECK(AllLocal.IsAllowed("thewrongpassword"sv, "/free/access", /*IsMachineLocalRequest*/ true));
+ CHECK(AllLocal.IsAllowed("thewrongpassword"sv, "/ok", /*IsMachineLocalRequest*/ true));
+ CHECK(AllLocal.IsAllowed(""sv, "/free/access", /*IsMachineLocalRequest*/ false));
+ CHECK(AllLocal.IsAllowed(""sv, "/ok", /*IsMachineLocalRequest*/ false));
+ CHECK(AllLocal.IsAllowed("thewrongpassword"sv, "/free/access", /*IsMachineLocalRequest*/ false));
+ CHECK(AllLocal.IsAllowed("thewrongpassword"sv, "/ok", /*IsMachineLocalRequest*/ false));
+ CHECK(AllLocal.IsAllowed("123456"sv, "/supersecret/uri", /*IsMachineLocalRequest*/ false));
+}
+
+TEST_CASE("passwordsecurity.conflictingunprotecteduris")
+{
+ try
+ {
+ PasswordSecurity AllLocal({.Password = "123456",
+ .ProtectMachineLocalRequests = true,
+ .UnprotectedUris = std::vector<std::string>({"/free/access", "/free/access"})});
+ CHECK(false);
+ }
+ catch (const std::runtime_error& Ex)
+ {
+ CHECK_EQ(Ex.what(),
+ std::string("password security unprotected uris does not generate unique hashes. Uri #2 ('/free/access') collides with "
+ "uri #1 ('/free/access')"));
+ }
+}
+void
+passwordsecurity_forcelink()
+{
+}
+#endif // ZEN_WITH_TESTS
+
+} // namespace zen
diff --git a/src/zenhttp/zenhttp.cpp b/src/zenhttp/zenhttp.cpp
index a2679f92e..0b5408453 100644
--- a/src/zenhttp/zenhttp.cpp
+++ b/src/zenhttp/zenhttp.cpp
@@ -7,6 +7,7 @@
# include <zenhttp/httpclient.h>
# include <zenhttp/httpserver.h>
# include <zenhttp/packageformat.h>
+# include <zenhttp/security/passwordsecurity.h>
namespace zen {
@@ -16,6 +17,7 @@ zenhttp_forcelinktests()
http_forcelink();
httpclient_forcelink();
forcelink_packageformat();
+ passwordsecurity_forcelink();
}
} // namespace zen