964 lines
26 KiB
C++
964 lines
26 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#pragma once
|
|
|
|
#if !defined(NO_UE_INCLUDES)
|
|
#include <HAL/FileManager.h>
|
|
#include <Misc/Paths.h>
|
|
#endif
|
|
|
|
namespace UE::IoStore::HTTP
|
|
{
|
|
|
|
#if !(UE_BUILD_SHIPPING|UE_BUILD_TEST) && !defined(NO_UE_INCLUDES)
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static void MiscTest()
|
|
{
|
|
#define CRLF "\r\n"
|
|
struct {
|
|
FAnsiStringView Input;
|
|
int32 Output;
|
|
} FmtTestCases[] = {
|
|
{ "", -1 },
|
|
{ "abcd", -1 },
|
|
{ "abcd\r", -1 },
|
|
{ CRLF "\r\r", -1 },
|
|
{ CRLF CRLF, 4 },
|
|
{ "abc" CRLF CRLF, 7 },
|
|
};
|
|
for (const auto [Input, Output] : FmtTestCases)
|
|
{
|
|
check(FindMessageTerminal(Input.GetData(), Input.Len()) == Output);
|
|
}
|
|
|
|
FMessageOffsets MsgOut;
|
|
check(ParseMessage("", MsgOut) == -1);
|
|
check(ParseMessage("MR", MsgOut) == -1);
|
|
check(ParseMessage("HTTP/1.1", MsgOut) == -1);
|
|
check(ParseMessage("HTTP/1.1 ", MsgOut) == -1);
|
|
check(ParseMessage("HTTP/1.1 1" CRLF, MsgOut) > 0);
|
|
check(ParseMessage("HTTP/1.1 1" CRLF, MsgOut) > 0);
|
|
check(ParseMessage("HTTP/1.1 100 " CRLF, MsgOut) > 0);
|
|
check(ParseMessage("HTTP/1.1 100 Message of some sort " CRLF, MsgOut) > 0);
|
|
check(ParseMessage("HTTP/1.1 100 _Message with a \r in it" CRLF, MsgOut) == -1);
|
|
|
|
bool AllIsWell = true;
|
|
auto NotExpectedToBeCalled = [&AllIsWell] (auto, auto)
|
|
{
|
|
AllIsWell = false;
|
|
return false;
|
|
};
|
|
|
|
EnumerateHeaders("", NotExpectedToBeCalled); check(AllIsWell);
|
|
EnumerateHeaders(CRLF, NotExpectedToBeCalled); check(AllIsWell);
|
|
EnumerateHeaders("foo", NotExpectedToBeCalled); check(AllIsWell);
|
|
EnumerateHeaders(" foo", NotExpectedToBeCalled); check(AllIsWell);
|
|
EnumerateHeaders(" foo ", NotExpectedToBeCalled); check(AllIsWell);
|
|
EnumerateHeaders("foo:bar", NotExpectedToBeCalled); check(AllIsWell);
|
|
|
|
auto IsBar = [&] (auto, auto Value) { return AllIsWell = (Value == "bar"); };
|
|
EnumerateHeaders("foo: bar" CRLF, IsBar); check(AllIsWell);
|
|
EnumerateHeaders("foo: bar \t" CRLF, IsBar); check(AllIsWell);
|
|
EnumerateHeaders("foo:\tbar " CRLF, IsBar); check(AllIsWell);
|
|
EnumerateHeaders("foo:bar " CRLF, IsBar); check(AllIsWell);
|
|
EnumerateHeaders("foo:bar" CRLF "!", IsBar); check(AllIsWell);
|
|
EnumerateHeaders("foo:bar" CRLF " ", IsBar); check(AllIsWell);
|
|
EnumerateHeaders("foo:bar" CRLF "n:ej", IsBar); check(AllIsWell);
|
|
|
|
check(CrudeToInt("") < 0);
|
|
check(CrudeToInt("X") < 0);
|
|
check(CrudeToInt("/") < 0);
|
|
check(CrudeToInt(":") < 0);
|
|
check(CrudeToInt("-1") < -1);
|
|
check(CrudeToInt("0") == 0);
|
|
check(CrudeToInt("9") == 9);
|
|
check(CrudeToInt("493") == 493);
|
|
|
|
check(CrudeToInt<16>("56") == 0x56);
|
|
check(CrudeToInt<16>("1") == 0x01);
|
|
check(CrudeToInt<16>("9") == 0x09);
|
|
check(CrudeToInt<16>("a") == 0x0a); check(CrudeToInt<16>("A") == 0x0a);
|
|
check(CrudeToInt<16>("f") == 0x0f); check(CrudeToInt<16>("F") == 0x0f);
|
|
check(CrudeToInt<16>("g") < 0);
|
|
check(CrudeToInt<16>("49e") == 0x49e);
|
|
check(CrudeToInt<16>("aBcD") == 0xabcd);
|
|
check(CrudeToInt<16>("eEeE") == 0xeeee);
|
|
|
|
FUrlOffsets UrlOut;
|
|
|
|
check(ParseUrl("", UrlOut) == -1);
|
|
check(ParseUrl("abc://asd/", UrlOut) == -1);
|
|
check(ParseUrl("http://", UrlOut) == -1);
|
|
check(ParseUrl("http://:/", UrlOut) == -1);
|
|
check(ParseUrl("http://@:/", UrlOut) == -1);
|
|
check(ParseUrl("http://foo:ba:r/", UrlOut) == -1);
|
|
check(ParseUrl("http://foo@ba:r/", UrlOut) == -1);
|
|
check(ParseUrl("http://foo@ba:r", UrlOut) == -1);
|
|
check(ParseUrl("http://foo@ba:/", UrlOut) == -1);
|
|
check(ParseUrl("http://foo@ba@9/", UrlOut) == -1);
|
|
check(ParseUrl("http://@ba:9/", UrlOut) == -1);
|
|
check(ParseUrl(
|
|
"http://zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
|
|
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
|
|
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
|
|
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
|
|
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.com",
|
|
UrlOut) == -1);
|
|
|
|
check(ParseUrl("http://ab-c.com/", UrlOut) > 0);
|
|
check(ParseUrl("http://a@bc.com/", UrlOut) > 0);
|
|
check(ParseUrl("https://abc.com", UrlOut) > 0);
|
|
check(ParseUrl("https://abc.com:999", UrlOut) > 0);
|
|
check(ParseUrl("https://abc.com:999/", UrlOut) > 0);
|
|
check(ParseUrl("https://foo:bar@abc.com:999", UrlOut) > 0);
|
|
check(ParseUrl("https://foo:bar@abc.com:999/", UrlOut) > 0);
|
|
check(ParseUrl("https://foo_bar@abc.com:999", UrlOut) > 0);
|
|
check(ParseUrl("https://foo_bar@abc.com:999/", UrlOut) > 0);
|
|
|
|
for (int32 i : { 0x10, 0x20, 0x40, 0x7f, 0xff })
|
|
{
|
|
char Url[] = "http://stockholm.patchercache.epicgames.net:123";
|
|
char Buffer[512];
|
|
std::memset(Buffer, i, sizeof(Buffer));
|
|
std::memcpy(Buffer, Url, sizeof(Url) - 1);
|
|
check(ParseUrl(FAnsiStringView(Buffer, sizeof(Url) - 1), UrlOut) > 0);
|
|
check(UrlOut.Port.Get(Url) == "123");
|
|
}
|
|
|
|
FAnsiStringView Url = "http://abc:123@bc.com:999/";
|
|
check(ParseUrl(Url, UrlOut) > 0);
|
|
check(UrlOut.SchemeLength == 4);
|
|
check(UrlOut.UserInfo.Get(Url) == "abc:123");
|
|
check(UrlOut.HostName.Get(Url) == "bc.com");
|
|
check(UrlOut.Port.Get(Url) == "999");
|
|
check(UrlOut.Path == 25);
|
|
#undef CRLF
|
|
|
|
static const char* OutcomeMsg = "\x4d\x52";
|
|
check(FOutcome::Error(OutcomeMsg, -5).IsOk() == false);
|
|
check(FOutcome::Error(OutcomeMsg, -5).IsWaiting() == false);
|
|
check(FOutcome::Error(OutcomeMsg, -5).IsError());
|
|
check(FOutcome::Error(OutcomeMsg, -5).GetErrorCode() == -5);
|
|
check(FOutcome::Error(OutcomeMsg, 5).GetErrorCode() == 5);
|
|
check(FOutcome::Error(OutcomeMsg, -5).GetMessage() == OutcomeMsg);
|
|
|
|
check(FOutcome::Ok( 0).IsOk());
|
|
check(FOutcome::Ok(-13).IsWaiting() == false);
|
|
check(FOutcome::Ok( 13).IsError() == false);
|
|
|
|
check(FOutcome::Waiting().IsOk() == false);
|
|
check(FOutcome::Waiting().IsWaiting());
|
|
check(FOutcome::Waiting().IsError() == false);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static void ThrottleTest(FAnsiStringView TestUrl)
|
|
{
|
|
enum { TheMax = 0x7fff'fffful };
|
|
check(FThrottler().GetAllowance() >= TheMax);
|
|
|
|
FThrottler Throttler;
|
|
uint64 OneSecond = Throttler.CycleFreq;
|
|
|
|
// timing test
|
|
FIoBuffer RecvData;
|
|
for (uint32 SizeKiB : { 64, 128, 192 })
|
|
{
|
|
const uint32 ThrottleKiB = 64;
|
|
|
|
TAnsiStringBuilder<128> Url;
|
|
Url << TestUrl;
|
|
Url << (SizeKiB << 10);
|
|
|
|
FEventLoop Loop;
|
|
Loop.Throttle(ThrottleKiB);
|
|
|
|
FRequest Request = Loop.Request("GET", Url).Accept("*/*");
|
|
Loop.Send(MoveTemp(Request), [&] (const FTicketStatus& Status) {
|
|
check(Status.GetId() != FTicketStatus::EId::Error);
|
|
if (Status.GetId() == FTicketStatus::EId::Response)
|
|
{
|
|
Status.GetResponse().SetDestination(&RecvData);
|
|
}
|
|
});
|
|
|
|
int32 Timeout = -1;
|
|
if (SizeKiB < 128) Timeout = 123;
|
|
if (SizeKiB > 128) Timeout = 4567;
|
|
|
|
uint64 Time = FPlatformTime::Cycles64();
|
|
while (Loop.Tick(Timeout));
|
|
Time = FPlatformTime::Cycles64() - Time;
|
|
Time /= OneSecond;
|
|
|
|
// It's dangerous stuff testing elapsed time you know. The +1 is because
|
|
// throttling assumes one second has already passed when initialised.
|
|
#if PLATFORM_WINDOWS
|
|
check(Time + 1 == (SizeKiB / ThrottleKiB));
|
|
#endif
|
|
|
|
RecvData = FIoBuffer();
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static void TlsLoadRootCerts()
|
|
{
|
|
IFileManager& Ifm = IFileManager::Get();
|
|
FString PemPath = FPaths::EngineDir() / TEXT("Content/Certificates/ThirdParty/cacert.pem");
|
|
FArchive* Reader = Ifm.CreateFileReader(*PemPath);
|
|
|
|
uint32 Size = uint32(Reader->TotalSize());
|
|
FIoBuffer PemData(Size);
|
|
FMutableMemoryView PemView = PemData.GetMutableView();
|
|
Reader->Serialize(PemView.GetData(), Size);
|
|
|
|
FCertRoots CaRoots(PemData.GetView());
|
|
FCertRoots::SetDefault(MoveTemp(CaRoots));
|
|
|
|
delete Reader;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static void TlsTest()
|
|
{
|
|
FEventLoop Loop;
|
|
|
|
auto WaitForLoopIdle = [&] {
|
|
for (; Loop.Tick(-1); FPlatformProcess::SleepNoStats(0.02f));
|
|
};
|
|
|
|
auto OkSink = [Dest=FIoBuffer()] (const FTicketStatus& Status) mutable {
|
|
check(Status.GetId() != FTicketStatus::EId::Error);
|
|
if (Status.GetId() == FTicketStatus::EId::Response)
|
|
{
|
|
FResponse& Response = Status.GetResponse();
|
|
check(Response.GetStatusCode() == 200);
|
|
Response.SetDestination(&Dest);
|
|
return;
|
|
}
|
|
check(Status.GetId() == FTicketStatus::EId::Content);
|
|
};
|
|
|
|
auto NotOkSink = [Dest=FIoBuffer()] (const FTicketStatus& Status) mutable {
|
|
check(Status.GetId() == FTicketStatus::EId::Error);
|
|
};
|
|
|
|
static const ANSICHAR* Url = "https://httpbin.org/get";
|
|
|
|
{
|
|
FRequest Request = Loop.Get(Url);
|
|
Loop.Send(MoveTemp(Request), OkSink);
|
|
WaitForLoopIdle();
|
|
}
|
|
|
|
{
|
|
FCertRoots NotACert(FMemoryView("493", 3));
|
|
check(NotACert.IsValid() == false);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static void RedirectTest(const ANSICHAR* TestHost, FCertRootsRef VerifyCert)
|
|
{
|
|
FEventLoop Loop;
|
|
|
|
auto WaitForLoopIdle = [&] {
|
|
for (; Loop.Tick(-1); FPlatformProcess::SleepNoStats(0.02f));
|
|
};
|
|
|
|
FEventLoop::FRequestParams RequestParams = {
|
|
.bAutoRedirect = true,
|
|
};
|
|
|
|
enum ReTyp { ReAbs, ReAbsTls, ReRel, ReRelTls };
|
|
enum { RecvDataSize = 48 };
|
|
|
|
TAnsiStringBuilder<64> Builder;
|
|
auto BuildUrl = [&] (ReTyp Typ, uint32 Code) -> const auto&
|
|
{
|
|
bool bTls = (Typ & 1);
|
|
Builder.Reset();
|
|
Builder << ((bTls) ? "https://" : "http://");
|
|
Builder << TestHost;
|
|
Builder << ":" << (bTls ? 4939 : 9493);
|
|
Builder << "/redirect";
|
|
Builder << ((Typ <= ReAbsTls) ? "/abs/" : "/rel/");
|
|
Builder << Code;
|
|
Builder << "/data/" << uint32(RecvDataSize);
|
|
return Builder;
|
|
};
|
|
|
|
UPTRINT SinkParam = 0xaa'493'493'493'493'bbull;
|
|
|
|
uint32 RecvCount;
|
|
auto OkSink = [Dest=FIoBuffer(), SinkParam, &RecvCount] (const FTicketStatus& Status) mutable {
|
|
check(Status.GetParam() == SinkParam);
|
|
check(Status.GetId() != FTicketStatus::EId::Error);
|
|
if (Status.GetId() == FTicketStatus::EId::Response)
|
|
{
|
|
FResponse& Response = Status.GetResponse();
|
|
check(Response.GetStatusCode() == 200);
|
|
Response.SetDestination(&Dest);
|
|
return;
|
|
}
|
|
check(Status.GetId() == FTicketStatus::EId::Content);
|
|
RecvCount += uint32(Dest.GetSize());
|
|
};
|
|
|
|
uint32 TestCodes[] = { 301, 302, 307, 308 };
|
|
|
|
for (auto ReTest : { ReAbs, ReAbsTls, ReRel, ReRelTls })
|
|
{
|
|
RequestParams.VerifyCert = (ReTest & 1) ? VerifyCert : 0;
|
|
RecvCount = 0;
|
|
for (uint32 Code : TestCodes)
|
|
{
|
|
FRequest Request = Loop.Get(BuildUrl(ReTest, Code), &RequestParams);
|
|
if (Code > TestCodes[1])
|
|
{
|
|
Request.Header("TestCodeHeader", "Header-Of-Test-Codes");
|
|
}
|
|
Loop.Send(MoveTemp(Request), OkSink, SinkParam);
|
|
}
|
|
WaitForLoopIdle();
|
|
check(RecvCount == RecvDataSize * UE_ARRAY_COUNT(TestCodes));
|
|
}
|
|
|
|
RequestParams = FEventLoop::FRequestParams();
|
|
RequestParams.bAutoRedirect = true;
|
|
|
|
for (auto ReTest : { ReAbs, ReAbsTls, ReRel, ReRelTls })
|
|
{
|
|
FConnectionPool::FParams Params;
|
|
Params.SetHostFromUrl(BuildUrl(ReTest, 0));
|
|
Params.VerifyCert = (ReTest & 1) ? VerifyCert : 0;
|
|
Params.ConnectionCount = 4;
|
|
FConnectionPool Pool(Params);
|
|
|
|
RecvCount = 0;
|
|
uint32 ExpectCount = 0;
|
|
for (uint32 TestCount : { 4, 267, 55, 17, 1024, 13, 26, 39, 52, 493 })
|
|
{
|
|
ExpectCount += TestCount;
|
|
TAnsiStringBuilder<64> Path;
|
|
Path << "/redirect/abs/307/data/";
|
|
Path << TestCount;
|
|
Loop.Send(Loop.Get(Path.ToString(), Pool, &RequestParams), OkSink, SinkParam);
|
|
}
|
|
WaitForLoopIdle();
|
|
check(RecvCount == ExpectCount);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static void ChunkedTest(const ANSICHAR* TestHost)
|
|
{
|
|
FEventLoop Loop;
|
|
|
|
auto WaitForLoopIdle = [&] {
|
|
for (; Loop.Tick(-1); FPlatformProcess::SleepNoStats(0.02f));
|
|
};
|
|
|
|
TAnsiStringBuilder<64> Url;
|
|
|
|
// TestServer proxy doesn't support chunked transfer so find the actual httpd
|
|
int32 HttpdPort = -1;
|
|
Url << "http://" << TestHost << ":9493/port";
|
|
Loop.Send(Loop.Get(Url), [&HttpdPort, Dest=FIoBuffer()] (const FTicketStatus& Status) mutable
|
|
{
|
|
if (Status.GetId() == FTicketStatus::EId::Response)
|
|
{
|
|
Status.GetResponse().SetDestination(&Dest);
|
|
return;
|
|
}
|
|
|
|
check(Status.GetId() == FTicketStatus::EId::Content);
|
|
HttpdPort = int32(CrudeToInt({ (char*)Dest.GetView().GetData(), int32(Dest.GetSize()) }));
|
|
});
|
|
WaitForLoopIdle();
|
|
check(HttpdPort > -1);
|
|
|
|
auto BuildUrl = [&Url, TestHost, HttpdPort] (int32 PayloadSize, FAnsiStringView UrlSuffix) -> FAnsiStringView
|
|
{
|
|
Url.Reset();
|
|
Url << "http://" << TestHost << ":" << HttpdPort << "/chunked/" << PayloadSize << UrlSuffix;
|
|
return Url;
|
|
};
|
|
|
|
struct FTestState
|
|
{
|
|
uint32 Size = 0;
|
|
uint32 Hash = 0x493;
|
|
uint32 ExpectedHash = 0;
|
|
int32 ExpectedSize = -1;
|
|
};
|
|
auto ChunkedSink = [State=FTestState(), Dest=FIoBuffer()] (const FTicketStatus& Status) mutable
|
|
{
|
|
if (Status.GetId() == FTicketStatus::EId::Response)
|
|
{
|
|
FResponse& Response = Status.GetResponse();
|
|
check(Response.GetStatus() == EStatusCodeClass::Successful);
|
|
check(Response.GetStatusCode() == 200);
|
|
|
|
State.ExpectedHash = uint32(CrudeToInt(Response.GetHeader("X-TestServer-Hash")));
|
|
State.ExpectedSize = uint32(CrudeToInt(Response.GetHeader("X-TestServer-Size")));
|
|
|
|
uint32 DestSize = ((State.ExpectedHash & 0x3f) / 7) * 67;
|
|
Dest = FIoBuffer(DestSize);
|
|
|
|
Response.SetDestination(&Dest);
|
|
return;
|
|
}
|
|
|
|
check(Status.GetId() == FTicketStatus::EId::Content);
|
|
|
|
FMemoryView View = Dest.GetView();
|
|
State.Size += uint32(View.GetSize());
|
|
for (uint32 i = 0, n = uint32(View.GetSize()); i < n; ++i)
|
|
{
|
|
uint8 c = ((const uint8*)(View.GetData()))[i];
|
|
State.Hash = (State.Hash + c) * 0x493;
|
|
}
|
|
|
|
if (View.GetSize() == 0)
|
|
{
|
|
check(State.Hash == State.ExpectedHash);
|
|
check(State.Size == State.ExpectedSize);
|
|
}
|
|
};
|
|
|
|
// General soak test
|
|
for (FAnsiStringView UrlSuffix : { "", "/ext" })
|
|
{
|
|
for (uint32 Mixer : { 1, 2, 3, 17, 71, 4931, 0xa9e })
|
|
{
|
|
for (uint32 SizeToGet : { 4,8,32,64,1,2,3,5,7,11,13,17,19,41,43,47,59,67,71,83,89,103,109 })
|
|
{
|
|
BuildUrl(SizeToGet * Mixer, UrlSuffix);
|
|
FRequest Request = Loop.Get(Url);
|
|
Loop.Send(MoveTemp(Request), ChunkedSink);
|
|
}
|
|
WaitForLoopIdle();
|
|
}
|
|
}
|
|
|
|
// Rudimentary coverage for transfers with trailing headers.
|
|
uint32 ErrorMarks;
|
|
auto ExpectError = [&ErrorMarks, Dest=FIoBuffer()] (const FTicketStatus& Status) mutable
|
|
{
|
|
if (Status.GetId() == FTicketStatus::EId::Response)
|
|
{
|
|
FResponse& Response = Status.GetResponse();
|
|
Response.SetDestination(&Dest);
|
|
return;
|
|
}
|
|
|
|
if (Status.GetId() != FTicketStatus::EId::Error)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FAnsiStringView Reason = Status.GetError().Reason;
|
|
ErrorMarks |= Reason.Contains("ERRTRAIL", ESearchCase::CaseSensitive) ? 1 : 0;
|
|
ErrorMarks |= Reason.Contains("ERRNOCHUNK", ESearchCase::CaseSensitive) ? 2 : 0;
|
|
};
|
|
ErrorMarks = 0;
|
|
BuildUrl(16 << 10, "/trailer");
|
|
Loop.Send(Loop.Get(Url), ExpectError);
|
|
WaitForLoopIdle();
|
|
check(ErrorMarks == 1);
|
|
|
|
// Disabling of chunked transfers
|
|
{
|
|
ErrorMarks = 0;
|
|
FEventLoop::FRequestParams RequestParams = { .bAllowChunked = false };
|
|
BuildUrl(16 << 10, "");
|
|
Loop.Send(Loop.Get(Url, &RequestParams), ExpectError);
|
|
WaitForLoopIdle();
|
|
check(ErrorMarks == 2);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static void SeedHttp(const ANSICHAR* TestHost, uint32 Seed)
|
|
{
|
|
TAnsiStringBuilder<64> Url;
|
|
Url << "http://" << TestHost << ":9493/seed/" << Seed;
|
|
FEventLoop Loop;
|
|
FRequest Request = Loop.Request("GET", Url, nullptr);
|
|
Loop.Send(MoveTemp(Request), [] (const FTicketStatus&) {});
|
|
for (; Loop.Tick(-1); FPlatformProcess::SleepNoStats(0.02f));
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static void HttpTest(const ANSICHAR* TestHost, FCertRootsRef VerifyCert)
|
|
{
|
|
const uint32 DefaultPort = (VerifyCert != 0) ? 4939 : 9493;
|
|
|
|
TAnsiStringBuilder<64> Ret;
|
|
auto BuildUrl = [&] (const ANSICHAR* Suffix=nullptr, uint32 Port=0) -> const auto&
|
|
{
|
|
Port = Port ? Port : DefaultPort;
|
|
Ret.Reset();
|
|
Ret << ((Port == 4939) ? "https://" : "http://");
|
|
Ret << TestHost;
|
|
Ret << ":" << Port;
|
|
return (Suffix != nullptr) ? (Ret << Suffix) : Ret;
|
|
};
|
|
|
|
struct
|
|
{
|
|
FIoBuffer Dest;
|
|
uint64 Hash = 0;
|
|
} Content[64];
|
|
|
|
auto HashSink = [&] (const FTicketStatus& Status) -> FIoBuffer*
|
|
{
|
|
check(Status.GetId() != FTicketStatus::EId::Error);
|
|
|
|
uint32 Index = Status.GetIndex();
|
|
|
|
if (Status.GetId() == FTicketStatus::EId::Response)
|
|
{
|
|
FResponse& Response = Status.GetResponse();
|
|
check(Response.GetStatus() == EStatusCodeClass::Successful);
|
|
check(Response.GetStatusCode() == 200);
|
|
check(Response.GetContentLength() == Status.GetContentLength());
|
|
|
|
FAnsiStringView HashView = Response.GetHeader("X-TestServer-Hash");
|
|
Content[Index].Hash = CrudeToInt(HashView);
|
|
check(int64(Content[Index].Hash) > 0);
|
|
|
|
Content[Index].Dest = FIoBuffer();
|
|
Response.SetDestination(&(Content[Index].Dest));
|
|
return nullptr;
|
|
}
|
|
|
|
uint32 ReceivedHash = 0x493;
|
|
FMemoryView ContentView = Content[Index].Dest.GetView();
|
|
check(ContentView.GetSize() == Status.GetContentLength());
|
|
for (uint32 i = 0; i < Status.GetContentLength(); ++i)
|
|
{
|
|
uint8 c = ((const uint8*)(ContentView.GetData()))[i];
|
|
ReceivedHash = (ReceivedHash + c) * 0x493;
|
|
}
|
|
check(Content[Index].Hash == ReceivedHash);
|
|
Content[Index].Hash = 0;
|
|
|
|
return nullptr;
|
|
};
|
|
|
|
auto NullSink = [] (const FTicketStatus&) {};
|
|
|
|
auto NoErrorSink = [&] (const FTicketStatus& Status)
|
|
{
|
|
check(Status.GetId() != FTicketStatus::EId::Error);
|
|
if (Status.GetId() != FTicketStatus::EId::Response)
|
|
{
|
|
return;
|
|
}
|
|
|
|
uint32 Index = Status.GetIndex();
|
|
|
|
FResponse& Response = Status.GetResponse();
|
|
Content[Index].Dest = FIoBuffer();
|
|
Response.SetDestination(&(Content[Index].Dest));
|
|
};
|
|
|
|
FEventLoop Loop;
|
|
volatile bool LoopStop = false;
|
|
volatile bool LoopTickDelay = false;
|
|
auto LoopTask = UE::Tasks::Launch(TEXT("IasHttpTest.Loop"), [&] () {
|
|
uint32 DelaySeed = 493;
|
|
while (!LoopStop)
|
|
{
|
|
while (Loop.Tick())
|
|
{
|
|
if (!LoopTickDelay)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
float DelayFloat = float(DelaySeed % 75) / 1000.0f;
|
|
FPlatformProcess::SleepNoStats(DelayFloat);
|
|
DelaySeed *= 0xa93;
|
|
}
|
|
|
|
FPlatformProcess::SleepNoStats(0.005f);
|
|
}
|
|
});
|
|
|
|
auto WaitForLoopIdle = [&Loop] ()
|
|
{
|
|
FPlatformProcess::SleepNoStats(0.25f);
|
|
while (!Loop.IsIdle())
|
|
{
|
|
FPlatformProcess::SleepNoStats(0.03f);
|
|
}
|
|
};
|
|
|
|
FEventLoop::FRequestParams ReqParamObj = { .VerifyCert = VerifyCert };
|
|
const FEventLoop::FRequestParams* ReqParams = nullptr;
|
|
if (VerifyCert != 0)
|
|
{
|
|
ReqParams = &ReqParamObj;
|
|
}
|
|
|
|
// unused request
|
|
{
|
|
FRequest Request = Loop.Request("GET", BuildUrl("/data"));
|
|
}
|
|
|
|
// foundational
|
|
{
|
|
FRequest Request = Loop.Request("GET", BuildUrl("/data/67"), ReqParams);
|
|
Request.Accept(EMimeType::Json);
|
|
|
|
FTicket Ticket = Loop.Send(MoveTemp(Request), HashSink);
|
|
|
|
WaitForLoopIdle();
|
|
}
|
|
|
|
// convenience
|
|
{
|
|
FRequest Request = Loop.Get(BuildUrl("/data"), ReqParams).Accept(EMimeType::Json);
|
|
|
|
FTicket Tickets[] = {
|
|
Loop.Send(Loop.Get(BuildUrl("/data"), ReqParams).Accept(EMimeType::Json), HashSink),
|
|
Loop.Send(MoveTemp(Request), HashSink),
|
|
Loop.Send(Loop.Get("http://httpbin.org/get"), NoErrorSink),
|
|
};
|
|
WaitForLoopIdle();
|
|
}
|
|
|
|
// convenience
|
|
{
|
|
FRequest Request = Loop.Get(BuildUrl("/data"), ReqParams).Accept(EMimeType::Json);
|
|
FTicket Ticket = Loop.Send(MoveTemp(Request), HashSink);
|
|
WaitForLoopIdle();
|
|
}
|
|
|
|
// pool
|
|
for (uint16 i = 1; i < 64; ++i)
|
|
{
|
|
FConnectionPool::FParams Params;
|
|
Params.SetHostFromUrl(BuildUrl());
|
|
Params.VerifyCert = VerifyCert;
|
|
Params.ConnectionCount = (i % 2) + 1;
|
|
FConnectionPool Pool(Params);
|
|
for (int32 j = 0; j < i; ++j)
|
|
{
|
|
TAnsiStringBuilder<16> Path;
|
|
Path << "/data?pool=" << i << "x" << j;
|
|
FRequest Request = Loop.Get(Path, Pool);
|
|
Loop.Send(MoveTemp(Request), HashSink);
|
|
}
|
|
WaitForLoopIdle();
|
|
}
|
|
|
|
// head barage
|
|
{
|
|
FConnectionPool::FParams Params;
|
|
Params.SetHostFromUrl(BuildUrl());
|
|
Params.VerifyCert = VerifyCert;
|
|
Params.ConnectionCount = 1;
|
|
FConnectionPool Pool(Params);
|
|
for (int32 i = 0; i < 61; ++i)
|
|
{
|
|
TAnsiStringBuilder<16> Path;
|
|
Path << "/data?head";
|
|
FRequest Request = Loop.Request("HEAD", Path, Pool);
|
|
Loop.Send(MoveTemp(Request), NullSink);
|
|
}
|
|
WaitForLoopIdle();
|
|
}
|
|
|
|
// fatal timeout
|
|
for (int32 i = 0; i < 14; ++i)
|
|
{
|
|
bool bExpectFailTimeout = !!(i & 1);
|
|
auto Sink = [bExpectFailTimeout, Dest=FIoBuffer()] (const FTicketStatus& Status) mutable
|
|
{
|
|
if (Status.GetId() == FTicketStatus::EId::Response)
|
|
{
|
|
FResponse& Response = Status.GetResponse();
|
|
Response.SetDestination(&Dest);
|
|
return;
|
|
}
|
|
|
|
check(Status.GetId() == FTicketStatus::EId::Error);
|
|
|
|
const char* Reason = Status.GetError().Reason;
|
|
bool IsFailTimeout = (FCStringAnsi::Strstr(Reason, "FailTimeout") != nullptr);
|
|
check(IsFailTimeout == bExpectFailTimeout);
|
|
};
|
|
|
|
auto ErrorSink = [] (const FTicketStatus& Status)
|
|
{
|
|
check(Status.GetId() == FTicketStatus::EId::Error);
|
|
};
|
|
|
|
FConnectionPool::FParams Params;
|
|
Params.SetHostFromUrl(BuildUrl());
|
|
Params.VerifyCert = VerifyCert;
|
|
FConnectionPool Pool(Params);
|
|
|
|
FEventLoop Loop2;
|
|
Loop2.Send(Loop2.Get("/data?stall=1", Pool), Sink);
|
|
|
|
// Requests are pipelined. The second one will get sent during the stall so
|
|
// we expect it to fail. The subsequent ones are expected to succeed.
|
|
Loop2.Send(Loop2.Get("/data", Pool), ErrorSink);
|
|
Loop2.Send(Loop2.Get("/data", Pool), HashSink);
|
|
Loop2.Send(Loop2.Get("/data", Pool), HashSink);
|
|
|
|
int32 PollTimeoutMs = -1;
|
|
if (bExpectFailTimeout)
|
|
{
|
|
Loop2.SetFailTimeout(1000);
|
|
|
|
if ((i & 3) == 1)
|
|
{
|
|
PollTimeoutMs = 1000;
|
|
}
|
|
}
|
|
while (Loop2.Tick(PollTimeoutMs));
|
|
|
|
Loop2.Send(Loop2.Get("/data/23", Pool), NoErrorSink);
|
|
while (Loop2.Tick(PollTimeoutMs));
|
|
}
|
|
|
|
// no connect
|
|
{
|
|
FRequest Requests[] = {
|
|
Loop.Request("GET", BuildUrl(nullptr, 10930)),
|
|
Loop.Request("GET", "http://thisdoesnotexistihope/"),
|
|
};
|
|
Loop.Send(MoveTemp(Requests[0]), NullSink);
|
|
Loop.Send(MoveTemp(Requests[1]), NullSink);
|
|
WaitForLoopIdle();
|
|
}
|
|
|
|
// head and large requests
|
|
{
|
|
auto MixTh = [Th=uint32(0)] () mutable { return (Th = (Th * 75) + 74) & 255; };
|
|
|
|
char AsciiData[257];
|
|
for (char& c : AsciiData)
|
|
{
|
|
int32 i = int32(ptrdiff_t(&c - AsciiData));
|
|
c = 0x41 + (MixTh() % 26);
|
|
c += (MixTh() & 2) ? 0x20 : 0;
|
|
}
|
|
|
|
for (int32 i = 0; (i += 69493) < 2 << 20;)
|
|
{
|
|
FRequest Request = Loop.Request("HEAD", BuildUrl("/data"), ReqParams);
|
|
for (int32 j = i; j > 0;)
|
|
{
|
|
FAnsiStringView Name(AsciiData, MixTh() + 1);
|
|
FAnsiStringView Value(AsciiData, MixTh() + 1);
|
|
Request.Header(Name, Value);
|
|
j -= Name.Len() + Value.Len();
|
|
|
|
}
|
|
|
|
Loop.Send(MoveTemp(Request), [] (const FTicketStatus& Status) {
|
|
if (Status.GetId() == FTicketStatus::EId::Response)
|
|
{
|
|
FResponse& Response = Status.GetResponse();
|
|
check(Response.GetStatusCode() == 431); // "too many headers"
|
|
}
|
|
});
|
|
|
|
WaitForLoopIdle();
|
|
}
|
|
}
|
|
|
|
// stress 1
|
|
{
|
|
const uint32 StressLoad = 32;
|
|
|
|
struct {
|
|
const ANSICHAR* Uri;
|
|
bool Disconnect;
|
|
} StressUrls[] = {
|
|
{ "/data?slowly=1", false },
|
|
{ "/data?disconnect=1", true },
|
|
};
|
|
|
|
uint64 Errors = 0;
|
|
auto ErrorSink = [&] (const FTicketStatus& Status)
|
|
{
|
|
FTicket Ticket = Status.GetTicket();
|
|
uint32 Index = uint32(63 - FMath::CountLeadingZeros64(uint64(Ticket)));
|
|
|
|
if (Status.GetId() == FTicketStatus::EId::Error)
|
|
{
|
|
Errors |= 1ull << Index;
|
|
return;
|
|
}
|
|
|
|
else if (Status.GetId() == FTicketStatus::EId::Response)
|
|
{
|
|
FResponse& Response = Status.GetResponse();
|
|
Content[Index].Dest = FIoBuffer();
|
|
Response.SetDestination(&(Content[Index].Dest));
|
|
return;
|
|
}
|
|
|
|
check(false);
|
|
};
|
|
|
|
for (const auto& [StressUri, ExpectDisconnect] : StressUrls)
|
|
{
|
|
FTicketSink Sink = ExpectDisconnect ? FTicketSink(ErrorSink) : FTicketSink(HashSink);
|
|
|
|
const auto& StressUrl = BuildUrl(StressUri);
|
|
for (bool AddDelay : {false, true})
|
|
{
|
|
FTicket Tickets[StressLoad];
|
|
for (FTicket& Ticket : Tickets)
|
|
{
|
|
Ticket = Loop.Send(Loop.Get(StressUrl, ReqParams).Header("Accept", "*/*"), Sink);
|
|
}
|
|
|
|
LoopTickDelay = AddDelay;
|
|
|
|
WaitForLoopIdle();
|
|
}
|
|
|
|
LoopTickDelay = false;
|
|
}
|
|
}
|
|
|
|
// stress 2
|
|
{
|
|
const uint32 StressLoad = 3;
|
|
const uint32 StressTaskCount = 7;
|
|
static_assert(StressLoad * StressTaskCount <= 32);
|
|
|
|
FAnsiStringView Url = BuildUrl("/data");
|
|
|
|
auto StressTaskEntry = [&] {
|
|
for (uint32 i = 0; i < StressLoad; ++i)
|
|
{
|
|
FTicket Ticket = Loop.Send(Loop.Get(Url, ReqParams), HashSink);
|
|
if (!Ticket)
|
|
{
|
|
FPlatformProcess::SleepNoStats(0.01f);
|
|
--i;
|
|
}
|
|
}
|
|
};
|
|
|
|
UE::Tasks::FTask StressTasks[StressTaskCount];
|
|
for (auto& StressTask : StressTasks)
|
|
{
|
|
StressTask = UE::Tasks::Launch(TEXT("StressTask"), [&] { StressTaskEntry(); });
|
|
}
|
|
for (auto& StressTask : StressTasks)
|
|
{
|
|
StressTask.Wait();
|
|
}
|
|
|
|
WaitForLoopIdle();
|
|
}
|
|
|
|
// tamper
|
|
for (int32 i = 1; i <= 100; ++i)
|
|
{
|
|
TAnsiStringBuilder<32> TamperUrl;
|
|
TamperUrl << "/data?tamper=" << i;
|
|
FAnsiStringView Url = BuildUrl(TamperUrl.ToString());
|
|
|
|
for (int j = 0; j < 13; ++j)
|
|
{
|
|
FRequest Request = Loop.Request("GET", Url, ReqParams);
|
|
Loop.Send(MoveTemp(Request), NullSink);
|
|
}
|
|
|
|
WaitForLoopIdle();
|
|
}
|
|
|
|
LoopStop = true;
|
|
LoopTask.Wait();
|
|
|
|
check(Loop.IsIdle());
|
|
|
|
#if IS_PROGRAM
|
|
if (VerifyCert == 0)
|
|
{
|
|
ThrottleTest(BuildUrl("/data/"));
|
|
}
|
|
#endif
|
|
|
|
// pre-generated headers
|
|
// request-with-body
|
|
// proxy
|
|
// gzip / deflate
|
|
// loop multi-req.
|
|
// url auth credentials
|
|
// transfer-file / splice / sendfile
|
|
// (header field parser)
|
|
// (form-data)
|
|
// (cookies)
|
|
// (cache)
|
|
// (websocket)
|
|
// (ipv6)
|
|
// (utf-8 host names)
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
IOSTOREHTTPCLIENT_API void IasHttpTest(const ANSICHAR* TestHost="localhost", uint32 Seed=493)
|
|
{
|
|
#if PLATFORM_WINDOWS
|
|
WSADATA WsaData;
|
|
if (WSAStartup(MAKEWORD(2, 2), &WsaData) == 0x0a9e0493)
|
|
return;
|
|
ON_SCOPE_EXIT { WSACleanup(); };
|
|
#endif
|
|
|
|
MiscTest();
|
|
|
|
FCertRoots TestServerCaChain;
|
|
{
|
|
TAnsiStringBuilder<64> CaUrl;
|
|
CaUrl << "http://" << TestHost << ":9493/ca";
|
|
|
|
FIoBuffer CertBuffer;
|
|
|
|
FEventLoop Loop;
|
|
FRequest Request = Loop.Get(CaUrl);
|
|
Loop.Send(MoveTemp(Request), [&] (const FTicketStatus& Status) {
|
|
check(Status.GetId() != FTicketStatus::EId::Error);
|
|
|
|
if (Status.GetId() == FTicketStatus::EId::Response)
|
|
{
|
|
FResponse& Response = Status.GetResponse();
|
|
Response.SetDestination(&CertBuffer);
|
|
}
|
|
});
|
|
for (; Loop.Tick(-1); FPlatformProcess::SleepNoStats(0.02f));
|
|
|
|
TestServerCaChain = FCertRoots(CertBuffer.GetView());
|
|
}
|
|
FCertRootsRef TestServerCertRef = FCertRoots::Explicit(TestServerCaChain);
|
|
|
|
SeedHttp(TestHost, Seed);
|
|
HttpTest(TestHost, FCertRoots::NoTls());
|
|
HttpTest(TestHost, TestServerCertRef);
|
|
ChunkedTest(TestHost);
|
|
RedirectTest(TestHost, TestServerCertRef);
|
|
TlsLoadRootCerts();
|
|
TlsTest();
|
|
}
|
|
|
|
#endif // !SHIP|TEST
|
|
|
|
// }}}
|
|
|
|
} // namespace UE::IoStore::HTTP
|