// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #if !defined(NO_UE_INCLUDES) #include #include #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