aboutsummaryrefslogtreecommitdiff
path: root/src/zenhttp/httpclient.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/zenhttp/httpclient.cpp')
-rw-r--r--src/zenhttp/httpclient.cpp451
1 files changed, 450 insertions, 1 deletions
diff --git a/src/zenhttp/httpclient.cpp b/src/zenhttp/httpclient.cpp
index 43e9fb468..281d512cf 100644
--- a/src/zenhttp/httpclient.cpp
+++ b/src/zenhttp/httpclient.cpp
@@ -21,9 +21,17 @@
#include "clients/httpclientcommon.h"
+#include <numeric>
+
#if ZEN_WITH_TESTS
+# include <zencore/scopeguard.h>
# include <zencore/testing.h>
# include <zencore/testutils.h>
+# include <zenhttp/security/passwordsecurityfilter.h>
+# include "servers/httpasio.h"
+# include "servers/httpsys.h"
+
+# include <thread>
#endif // ZEN_WITH_TESTS
namespace zen {
@@ -96,6 +104,44 @@ HttpClientBase::GetAccessToken()
//////////////////////////////////////////////////////////////////////////
+std::vector<std::pair<uint64_t, uint64_t>>
+HttpClient::Response::GetRanges(std::span<const std::pair<uint64_t, uint64_t>> OffsetAndLengthPairs) const
+{
+ if (Ranges.empty())
+ {
+ return {};
+ }
+
+ std::vector<std::pair<uint64_t, uint64_t>> Result;
+ Result.reserve(OffsetAndLengthPairs.size());
+
+ auto BoundaryIt = Ranges.begin();
+ auto OffsetAndLengthPairIt = OffsetAndLengthPairs.begin();
+ while (OffsetAndLengthPairIt != OffsetAndLengthPairs.end())
+ {
+ uint64_t Offset = OffsetAndLengthPairIt->first;
+ uint64_t Length = OffsetAndLengthPairIt->second;
+ while (Offset >= BoundaryIt->RangeOffset + BoundaryIt->RangeLength)
+ {
+ BoundaryIt++;
+ if (BoundaryIt == Ranges.end())
+ {
+ throw std::runtime_error("HttpClient::Response can not fulfill requested range");
+ }
+ }
+ if (Offset + Length > BoundaryIt->RangeOffset + BoundaryIt->RangeLength || Offset < BoundaryIt->RangeOffset)
+ {
+ throw std::runtime_error("HttpClient::Response can not fulfill requested range");
+ }
+ uint64_t OffsetIntoRange = Offset - BoundaryIt->RangeOffset;
+ uint64_t RangePayloadOffset = BoundaryIt->OffsetInPayload + OffsetIntoRange;
+ Result.emplace_back(std::make_pair(RangePayloadOffset, Length));
+
+ OffsetAndLengthPairIt++;
+ }
+ return Result;
+}
+
CbObject
HttpClient::Response::AsObject() const
{
@@ -334,10 +380,55 @@ HttpClient::Authenticate()
return m_Inner->Authenticate();
}
+LatencyTestResult
+MeasureLatency(HttpClient& Client, std::string_view Url)
+{
+ std::vector<double> MeasurementTimes;
+ std::string ErrorMessage;
+
+ for (uint32_t AttemptCount = 0; AttemptCount < 20 && MeasurementTimes.size() < 5; AttemptCount++)
+ {
+ HttpClient::Response MeasureResponse = Client.Get(Url);
+ if (MeasureResponse.IsSuccess())
+ {
+ MeasurementTimes.push_back(MeasureResponse.ElapsedSeconds);
+ Sleep(5);
+ }
+ else
+ {
+ ErrorMessage = MeasureResponse.ErrorMessage(fmt::format("Unable to measure latency using {}", Url));
+
+ // Connection-level failures (timeout, refused, DNS) mean the endpoint is unreachable.
+ // Bail out immediately — retrying will just burn the connect timeout each time.
+ if (MeasureResponse.Error && MeasureResponse.Error->IsConnectionError())
+ {
+ break;
+ }
+ }
+ }
+
+ if (MeasurementTimes.empty())
+ {
+ return {.Success = false, .FailureReason = ErrorMessage};
+ }
+
+ if (MeasurementTimes.size() > 2)
+ {
+ std::sort(MeasurementTimes.begin(), MeasurementTimes.end());
+ MeasurementTimes.pop_back(); // Remove the worst time
+ }
+
+ double AverageLatency = std::accumulate(MeasurementTimes.begin(), MeasurementTimes.end(), 0.0) / MeasurementTimes.size();
+
+ return {.Success = true, .LatencySeconds = AverageLatency};
+}
+
//////////////////////////////////////////////////////////////////////////
#if ZEN_WITH_TESTS
+TEST_SUITE_BEGIN("http.httpclient");
+
TEST_CASE("responseformat")
{
using namespace std::literals;
@@ -388,8 +479,366 @@ TEST_CASE("httpclient")
{
using namespace std::literals;
- SUBCASE("client") {}
+ struct TestHttpService : public HttpService
+ {
+ TestHttpService() = default;
+
+ virtual const char* BaseUri() const override { return "/test/"; }
+ virtual void HandleRequest(HttpServerRequest& HttpServiceRequest) override
+ {
+ if (HttpServiceRequest.RelativeUri() == "yo")
+ {
+ if (HttpServiceRequest.IsLocalMachineRequest())
+ {
+ return HttpServiceRequest.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, "hey family");
+ }
+ else
+ {
+ return HttpServiceRequest.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, "hey stranger");
+ }
+ }
+ return HttpServiceRequest.WriteResponse(HttpResponseCode::OK);
+ }
+ };
+
+ TestHttpService TestService;
+ ScopedTemporaryDirectory TmpDir;
+
+ SUBCASE("asio")
+ {
+ Ref<HttpServer> AsioServer = CreateHttpAsioServer(AsioConfig{});
+
+ int Port = AsioServer->Initialize(7575, TmpDir.Path());
+ REQUIRE(Port != -1);
+
+ AsioServer->RegisterService(TestService);
+
+ std::thread ServerThread([&]() { AsioServer->Run(false); });
+
+ {
+ auto _ = MakeGuard([&]() {
+ if (ServerThread.joinable())
+ {
+ ServerThread.join();
+ }
+ AsioServer->Close();
+ });
+
+ {
+ HttpClient Client(fmt::format("127.0.0.1:{}", Port),
+ HttpClientSettings{},
+ /*CheckIfAbortFunction*/ {});
+
+ ZEN_INFO("Request using {}", Client.GetBaseUri());
+
+ HttpClient::Response TestResponse = Client.Get("/test/yo");
+ CHECK(TestResponse.IsSuccess());
+ CHECK_EQ(TestResponse.AsText(), "hey family");
+ }
+
+ if (IsIPv6Capable())
+ {
+ HttpClient Client(fmt::format("[::1]:{}", Port),
+ HttpClientSettings{},
+ /*CheckIfAbortFunction*/ {});
+
+ ZEN_INFO("Request using {}", Client.GetBaseUri());
+
+ HttpClient::Response TestResponse = Client.Get("/test/yo");
+ CHECK(TestResponse.IsSuccess());
+ CHECK_EQ(TestResponse.AsText(), "hey family");
+ }
+
+ {
+ HttpClient Client(fmt::format("localhost:{}", Port),
+ HttpClientSettings{},
+ /*CheckIfAbortFunction*/ {});
+
+ ZEN_INFO("Request using {}", Client.GetBaseUri());
+
+ HttpClient::Response TestResponse = Client.Get("/test/yo");
+ CHECK(TestResponse.IsSuccess());
+ CHECK_EQ(TestResponse.AsText(), "hey family");
+ }
+# if 0
+ {
+ HttpClient Client(fmt::format("10.24.101.77:{}", Port),
+ HttpClientSettings{},
+ /*CheckIfAbortFunction*/ {});
+
+ ZEN_INFO("Request using {}", Client.GetBaseUri());
+
+ HttpClient::Response TestResponse = Client.Get("/test/yo");
+ CHECK(TestResponse.IsSuccess());
+ CHECK_EQ(TestResponse.AsText(), "hey family");
+ }
+ Sleep(20000);
+# endif // 0
+ AsioServer->RequestExit();
+ }
+ }
+
+# if ZEN_PLATFORM_WINDOWS
+ SUBCASE("httpsys")
+ {
+ Ref<HttpServer> HttpSysServer = CreateHttpSysServer(HttpSysConfig{.ForceLoopback = false});
+
+ int Port = HttpSysServer->Initialize(7575, TmpDir.Path());
+ REQUIRE(Port != -1);
+
+ HttpSysServer->RegisterService(TestService);
+
+ std::thread ServerThread([&]() { HttpSysServer->Run(false); });
+
+ {
+ auto _ = MakeGuard([&]() {
+ if (ServerThread.joinable())
+ {
+ ServerThread.join();
+ }
+ HttpSysServer->Close();
+ });
+
+ if (true)
+ {
+ HttpClient Client(fmt::format("127.0.0.1:{}", Port),
+ HttpClientSettings{},
+ /*CheckIfAbortFunction*/ {});
+
+ ZEN_INFO("Request using {}", Client.GetBaseUri());
+
+ HttpClient::Response TestResponse = Client.Get("/test/yo");
+ CHECK(TestResponse.IsSuccess());
+ CHECK_EQ(TestResponse.AsText(), "hey family");
+ }
+
+ if (IsIPv6Capable())
+ {
+ HttpClient Client(fmt::format("[::1]:{}", Port),
+ HttpClientSettings{},
+ /*CheckIfAbortFunction*/ {});
+
+ ZEN_INFO("Request using {}", Client.GetBaseUri());
+
+ HttpClient::Response TestResponse = Client.Get("/test/yo");
+ CHECK(TestResponse.IsSuccess());
+ CHECK_EQ(TestResponse.AsText(), "hey family");
+ }
+
+ {
+ HttpClient Client(fmt::format("localhost:{}", Port),
+ HttpClientSettings{},
+ /*CheckIfAbortFunction*/ {});
+
+ ZEN_INFO("Request using {}", Client.GetBaseUri());
+
+ HttpClient::Response TestResponse = Client.Get("/test/yo");
+ CHECK(TestResponse.IsSuccess());
+ CHECK_EQ(TestResponse.AsText(), "hey family");
+ }
+# if 0
+ {
+ HttpClient Client(fmt::format("10.24.101.77:{}", Port),
+ HttpClientSettings{},
+ /*CheckIfAbortFunction*/ {});
+
+ ZEN_INFO("Request using {}", Client.GetBaseUri());
+
+ HttpClient::Response TestResponse = Client.Get("/test/yo");
+ CHECK(TestResponse.IsSuccess());
+ CHECK_EQ(TestResponse.AsText(), "hey family");
+ }
+ Sleep(20000);
+# endif // 0
+ HttpSysServer->RequestExit();
+ }
+ }
+# endif // ZEN_PLATFORM_WINDOWS
+}
+
+TEST_CASE("httpclient.requestfilter")
+{
+ using namespace std::literals;
+
+ struct TestHttpService : public HttpService
+ {
+ TestHttpService() = default;
+
+ virtual const char* BaseUri() const override { return "/test/"; }
+ virtual void HandleRequest(HttpServerRequest& HttpServiceRequest) override
+ {
+ if (HttpServiceRequest.RelativeUri() == "yo")
+ {
+ return HttpServiceRequest.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, "hey family");
+ }
+
+ {
+ CHECK(HttpServiceRequest.RelativeUri() != "should_filter");
+ return HttpServiceRequest.WriteResponse(HttpResponseCode::InternalServerError);
+ }
+
+ {
+ CHECK(HttpServiceRequest.RelativeUri() != "should_forbid");
+ return HttpServiceRequest.WriteResponse(HttpResponseCode::InternalServerError);
+ }
+ }
+ };
+
+ TestHttpService TestService;
+ ScopedTemporaryDirectory TmpDir;
+
+ class MyFilterImpl : public IHttpRequestFilter
+ {
+ public:
+ virtual Result FilterRequest(HttpServerRequest& Request)
+ {
+ if (Request.RelativeUri() == "should_filter")
+ {
+ Request.WriteResponse(HttpResponseCode::MethodNotAllowed, HttpContentType::kText, "no thank you");
+ return Result::ResponseSent;
+ }
+ else if (Request.RelativeUri() == "should_forbid")
+ {
+ return Result::Forbidden;
+ }
+ return Result::Accepted;
+ }
+ };
+
+ MyFilterImpl MyFilter;
+
+ Ref<HttpServer> AsioServer = CreateHttpAsioServer(AsioConfig{});
+
+ AsioServer->SetHttpRequestFilter(&MyFilter);
+
+ int Port = AsioServer->Initialize(7575, TmpDir.Path());
+ REQUIRE(Port != -1);
+
+ AsioServer->RegisterService(TestService);
+
+ std::thread ServerThread([&]() { AsioServer->Run(false); });
+
+ {
+ auto _ = MakeGuard([&]() {
+ if (ServerThread.joinable())
+ {
+ ServerThread.join();
+ }
+ AsioServer->Close();
+ });
+
+ HttpClient Client(fmt::format("localhost:{}", Port),
+ HttpClientSettings{},
+ /*CheckIfAbortFunction*/ {});
+
+ ZEN_INFO("Request using {}", Client.GetBaseUri());
+
+ HttpClient::Response YoResponse = Client.Get("/test/yo");
+ CHECK(YoResponse.IsSuccess());
+ CHECK_EQ(YoResponse.AsText(), "hey family");
+
+ HttpClient::Response ShouldFilterResponse = Client.Get("/test/should_filter");
+ CHECK_EQ(ShouldFilterResponse.StatusCode, HttpResponseCode::MethodNotAllowed);
+ CHECK_EQ(ShouldFilterResponse.AsText(), "no thank you");
+
+ HttpClient::Response ShouldForbitResponse = Client.Get("/test/should_forbid");
+ CHECK_EQ(ShouldForbitResponse.StatusCode, HttpResponseCode::Forbidden);
+
+ AsioServer->RequestExit();
+ }
+}
+
+TEST_CASE("httpclient.password")
+{
+ using namespace std::literals;
+
+ struct TestHttpService : public HttpService
+ {
+ TestHttpService() = default;
+
+ virtual const char* BaseUri() const override { return "/test/"; }
+ virtual void HandleRequest(HttpServerRequest& HttpServiceRequest) override
+ {
+ if (HttpServiceRequest.RelativeUri() == "yo")
+ {
+ return HttpServiceRequest.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, "hey family");
+ }
+
+ {
+ CHECK(HttpServiceRequest.RelativeUri() != "should_filter");
+ return HttpServiceRequest.WriteResponse(HttpResponseCode::InternalServerError);
+ }
+
+ {
+ CHECK(HttpServiceRequest.RelativeUri() != "should_forbid");
+ return HttpServiceRequest.WriteResponse(HttpResponseCode::InternalServerError);
+ }
+ }
+ };
+
+ TestHttpService TestService;
+ ScopedTemporaryDirectory TmpDir;
+
+ Ref<HttpServer> AsioServer = CreateHttpAsioServer(AsioConfig{});
+
+ int Port = AsioServer->Initialize(7575, TmpDir.Path());
+ REQUIRE(Port != -1);
+
+ AsioServer->RegisterService(TestService);
+
+ std::thread ServerThread([&]() { AsioServer->Run(false); });
+
+ {
+ auto _ = MakeGuard([&]() {
+ if (ServerThread.joinable())
+ {
+ ServerThread.join();
+ }
+ AsioServer->Close();
+ });
+
+ SUBCASE("usernamepassword")
+ {
+ CbObjectWriter Writer;
+ {
+ Writer.BeginObject("basic");
+ {
+ Writer << "username"sv
+ << "me";
+ Writer << "password"sv
+ << "456123789";
+ }
+ Writer.EndObject();
+ Writer << "protect-machine-local-requests" << true;
+ }
+
+ PasswordHttpFilter::Configuration PasswordFilterOptions = PasswordHttpFilter::ReadConfiguration(Writer.Save());
+
+ PasswordHttpFilter MyFilter(PasswordFilterOptions);
+
+ AsioServer->SetHttpRequestFilter(&MyFilter);
+
+ HttpClient Client(fmt::format("localhost:{}", Port),
+ HttpClientSettings{},
+ /*CheckIfAbortFunction*/ {});
+
+ ZEN_INFO("Request using {}", Client.GetBaseUri());
+
+ HttpClient::Response ForbiddenResponse = Client.Get("/test/yo");
+ CHECK(!ForbiddenResponse.IsSuccess());
+ CHECK_EQ(ForbiddenResponse.StatusCode, HttpResponseCode::Forbidden);
+
+ HttpClient::Response WithBasicResponse =
+ Client.Get("/test/yo",
+ std::pair<std::string, std::string>("Authorization",
+ fmt::format("Basic {}", PasswordFilterOptions.PasswordConfig.Password)));
+ CHECK(WithBasicResponse.IsSuccess());
+ AsioServer->SetHttpRequestFilter(nullptr);
+ }
+ AsioServer->RequestExit();
+ }
}
+TEST_SUITE_END();
void
httpclient_forcelink()