// Copyright Epic Games, Inc. All Rights Reserved. #pragma once namespace UE::IoStore::HTTP { // {{{1 trace .................................................................. //////////////////////////////////////////////////////////////////////////////// enum class ETrace { LoopCreate, LoopTick, LoopDestroy, ActivityCreate, ActivityDestroy, SocketCreate, SocketDestroy, RequestBegin, StateChange, Wait, Unwait, Connect, Send, Recv, StartWork, }; //////////////////////////////////////////////////////////////////////////////// #if UE_TRACE_ENABLED static void Trace(const struct FActivity*, ETrace, uint32); static void Trace(UPTRINT, ETrace Action, const class FOutcome* =nullptr); static void Trace(ETrace Action) {} static void Trace(const void*, ETrace Action, ...) {} //////////////////////////////////////////////////////////////////////////////// IOSTOREHTTPCLIENT_API const void* GetIaxTraceChannel() { UE_TRACE_CHANNEL(Iax); return &Iax; } #else static void Trace(...) {} IOSTOREHTTPCLIENT_API const void* GetIaxTraceChannel() { return nullptr; } #endif // UE_TRACE_ENABLED // {{{1 misc ................................................................... //////////////////////////////////////////////////////////////////////////////// #define IAS_CVAR(Type, Name, Default, Desc, ...) \ Type G##Name = Default; \ static FAutoConsoleVariableRef CVar_Ias##Name( \ TEXT("ias.Http" #Name), \ G##Name, \ TEXT(Desc) \ __VA_ARGS__ \ ) //////////////////////////////////////////////////////////////////////////////// static IAS_CVAR(int32, RecvWorkThresholdKiB,80, "Threshold of data remaining at which next request is sent (in KiB)"); static IAS_CVAR(int32, IdleMs, 50'000, "Time in milliseconds to close idle connections or fail waits"); //////////////////////////////////////////////////////////////////////////////// class FOutcome { public: static FOutcome Ok(uint32 Result=0); static FOutcome Waiting(); static FOutcome Error(const char* Message, int32 Code=-1); static FOutcome None() { return Error(""); } bool IsError() const { return Message < 0x8000'0000'0000; } bool IsWaiting() const { return Tag == WaitTag; } bool IsOk() const { return Tag == OkTag; } FAnsiStringView GetMessage() const { check(IsError()); return (const char*)(Message); } int32 GetErrorCode() const{ check(IsError()); return int32(Code); } uint32 GetResult() const { check(IsOk()); return Result; } private: FOutcome() = default; static uint32 const OkTag = 0x0000'8000; static uint32 const WaitTag = 0x0001'8000; union { struct { UPTRINT Message : 48; PTRINT Code : 16; }; struct { uint32 Result; uint32 Tag; }; }; }; static_assert(sizeof(FOutcome) == sizeof(void*)); //////////////////////////////////////////////////////////////////////////////// FOutcome FOutcome::Ok(uint32 Result) { FOutcome Outcome; Outcome.Tag = OkTag; Outcome.Result = Result; return Outcome; } //////////////////////////////////////////////////////////////////////////////// FOutcome FOutcome::Waiting() { FOutcome Outcome; Outcome.Tag = WaitTag; return Outcome; } //////////////////////////////////////////////////////////////////////////////// FOutcome FOutcome::Error(const char* Message, int32 Code) { check(Message != nullptr); check(Code <= 0xffff && Code >= -0xffff); FOutcome Outcome; Outcome.Message = UPTRINT(Message); Outcome.Code = int16(Code); return Outcome; } //////////////////////////////////////////////////////////////////////////////// template static void EnumerateHeaders(FAnsiStringView Headers, LambdaType&& Lambda) { // NB. here we are assuming that we will be dealing with servers that will // not be returning headers with "obsolete line folding". auto IsOws = [] (int32 c) { return (c == ' ') | (c == '\t'); }; const char* Cursor = Headers.GetData(); const char* End = Cursor + Headers.Len(); do { int32 ColonIndex = 0; for (; Cursor + ColonIndex < End; ++ColonIndex) { if (Cursor[ColonIndex] == ':') { break; } } Cursor += ColonIndex; const char* Right = Cursor + 1; while (Right < End) { if (Right[0] != '\r' || Right[1] != '\n') { Right += 1 + (Right[1] != '\r'); continue; } FAnsiStringView Name(Cursor - ColonIndex, ColonIndex); const char* Left = Cursor + 1; for (; IsOws(Left[0]); ++Left); Cursor = Right; for (; Cursor > Left + 1 && IsOws(Cursor[-1]); --Cursor); FAnsiStringView Value (Left, int32(ptrdiff_t(Cursor - Left))); if (!Lambda(Name, Value)) { Right = End; } break; } Cursor = Right + 2; } while (Cursor < End); } //////////////////////////////////////////////////////////////////////////////// static int32 FindMessageTerminal(const char* Data, uint32 Length) { for (uint32 i = 4; i <= Length; ++i) { uint32 Candidate; ::memcpy(&Candidate, Data + i - 4, sizeof(Candidate)); if (Candidate == 0x0a0d0a0d) { return i; } i += (Data[i - 1] > 0x0d) ? 3 : 0; } return -1; } //////////////////////////////////////////////////////////////////////////////// template static int64 CrudeToInt(FAnsiStringView View) { static_assert(Base == 10 || Base == 16); // FCStringAnsi::* is not used to mitigate any locale hiccups. By // initialising 'Value' with MSB set we can detect cases where View did not // start with digits. This works as we won't be using this on huge numbers. int64 Value = 0x8000'0000'0000'0000ll; for (int32 c : View) { uint32 Digit = c - '0'; if (Digit > 9u) { if (Base != 16) { break; } Digit = (c | 0x20) - 'a'; if (Digit > uint32('f' - 'a')) { break; } Digit += 10; } Value *= Base; Value += Digit; } return Value; }; //////////////////////////////////////////////////////////////////////////////// struct FMessageOffsets { uint8 StatusCode; uint8 Message; uint16 Headers; }; static int32 ParseMessage(FAnsiStringView Message, FMessageOffsets& Out) { const FAnsiStringView Protocol("HTTP/1.1 "); // Check there's enough data if (Message.Len() < Protocol.Len() + 1) // "+1" accounts for at least one digit { return -1; } const char* Cursor = Message.GetData(); // Check for the expected protocol if (FAnsiStringView(Cursor, 9) != Protocol) { return -1; } int32 i = Protocol.Len(); // Trim left and tightly reject anything adventurous for (int n = 32; Cursor[i] == ' ' && i < n; ++i); Out.StatusCode = uint8(i); // At least one status line digit. (Note to self; expect exactly three) for (int n = 32; uint32(Cursor[i] - 0x30) <= 9 && i < n; ++i); if (uint32(i - Out.StatusCode - 1) > 32) { return -1; } // Trim left for (int n = 32; Cursor[i] == ' ' && i < n; ++i); Out.Message = uint8(i); // Extra conservative length allowance if (i > 32) { return -1; } // Find \r\n for (; Cursor[i] != '\r'; ++i) { if (i >= 2048) { return -1; } } if (Cursor[i + 1] != '\n') { return -1; } Out.Headers = uint16(i + 2); return 1; } //////////////////////////////////////////////////////////////////////////////// struct FUrlOffsets { struct Slice { Slice() = default; Slice(int32 l, int32 r) : Left(uint8(l)), Right(uint8(r)) {} FAnsiStringView Get(FAnsiStringView Url) const { return Url.Mid(Left, Right - Left); } operator bool () const { return Left > 0; } int32 Len() const { return Right - Left; } uint8 Left; uint8 Right; }; Slice UserInfo; Slice HostName; Slice Port; uint8 Path; uint8 SchemeLength; }; static int32 ParseUrl(FAnsiStringView Url, FUrlOffsets& Out) { if (Url.Len() < 5) { return -1; } Out = {}; const char* Start = Url.GetData(); const char* Cursor = Start; // Scheme int32 i = 0; for (; i < 5; ++i) { if (uint32(Cursor[i] - 'a') > uint32('z' - 'a')) { break; } } Out.SchemeLength = uint8(i); FAnsiStringView Scheme = Url.Left(i); if (Scheme != "http" && Scheme != "https") { return -1; } // Separator and authority if (Cursor[i] != ':' || Cursor[i + 1] != '/' || Cursor[i + 2] != '/') { return -1; } i += 3; struct { int32 c; int32 i; } Seps[2]; int32 SepCount = 0; for (; i < Url.Len(); ++i) { int32 c = Cursor[i]; if (c < '-') break; if (c != ':' && c != '@' && c != '/') continue; if (c == '/' || SepCount >= 2) break; if (c == '@' && SepCount) { SepCount -= (Seps[SepCount - 1].c == ':'); } Seps[SepCount++] = { c, i }; } if (i > 0xff || i <= Scheme.Len() + 3) { return -1; } if (i < Url.Len()) { Out.Path = uint8(i); } Out.HostName = { uint8(Scheme.Len() + 3), uint8(i) }; switch (SepCount) { case 0: break; case 1: if (Seps[0].c == ':') { Out.Port = { Seps[0].i + 1, i }; Out.HostName.Right = uint8(Seps[0].i); } else { Out.UserInfo = { Out.HostName.Left, Seps[0].i }; Out.HostName.Left += uint8(Seps[0].i + 1); Out.HostName.Right += uint8(Seps[0].i + 1); } break; case 2: if ((Seps[0].c != '@') | (Seps[1].c != ':')) { return -1; } Out.UserInfo = { Out.HostName.Left, Seps[0].i }; Out.Port.Left = uint8(Seps[1].i + 1); Out.Port.Right = Out.HostName.Right; Out.HostName.Left = Out.UserInfo.Right + 1; Out.HostName.Right = Out.Port.Left - 1; break; default: return -1; } bool Bad = false; Bad |= (Out.HostName.Len() == 0); Bad |= bool(Out.UserInfo) & (Out.UserInfo.Len() == 0); if (Out.Port.Left) { Bad |= (Out.Port.Len() == 0); for (int32 j = 0, n = Out.Port.Len(); j < n; ++j) { Bad |= (uint32(Start[Out.Port.Left + j] - '0') > 9); } } return Bad ? -1 : 1; } // {{{1 buffer ................................................................. //////////////////////////////////////////////////////////////////////////////// class alignas(16) FBuffer { public: struct FMutableSection { char* Data; uint32 Size; }; FBuffer() = default; FBuffer(char* InData, uint32 InMax); ~FBuffer(); FBuffer& operator = (FBuffer&& Rhs); void Fix(); void Resize(uint32 Size); const char* GetData() const; uint32 GetSize() const; uint32 GetCapacity() const; template T* Alloc(uint32 Count=1); FMutableSection GetMutableFree(uint32 MinSize, uint32 PageSize=256); void AdvanceUsed(uint32 Delta); private: char* GetDataPtr(); void Extend(uint32 AtLeast, uint32 PageSize); UPTRINT Data; uint32 Max = 0; union { struct { uint32 Used : 31; uint32 Inline : 1; }; uint32 UsedInline = 0; }; private: FBuffer(FBuffer&&) = delete; FBuffer(const FBuffer&) = delete; FBuffer& operator = (const FBuffer&) = delete; }; //////////////////////////////////////////////////////////////////////////////// FBuffer::FBuffer(char* InData, uint32 InMax) : Data(UPTRINT(InData)) , Max(InMax) { Inline = 1; } //////////////////////////////////////////////////////////////////////////////// FBuffer::~FBuffer() { if (Data && !Inline) { FMemory::Free(GetDataPtr()); } } //////////////////////////////////////////////////////////////////////////////// FBuffer& FBuffer::operator = (FBuffer&& Rhs) { Swap(Data, Rhs.Data); Swap(Max, Rhs.Max); Swap(UsedInline, Rhs.UsedInline); return *this; } //////////////////////////////////////////////////////////////////////////////// void FBuffer::Fix() { check(Inline); Data += Used; Max -= Used; Used = 0; } //////////////////////////////////////////////////////////////////////////////// void FBuffer::Resize(uint32 Size) { check(Size <= Max); Used = Size; } //////////////////////////////////////////////////////////////////////////////// char* FBuffer::GetDataPtr() { return (char*)Data; } //////////////////////////////////////////////////////////////////////////////// const char* FBuffer::GetData() const { return (char*)Data; } //////////////////////////////////////////////////////////////////////////////// uint32 FBuffer::GetSize() const { return Used; } //////////////////////////////////////////////////////////////////////////////// uint32 FBuffer::GetCapacity() const { return Max; } //////////////////////////////////////////////////////////////////////////////// template T* FBuffer::Alloc(uint32 Count) { uint32 AlignBias = uint32(Data) & (alignof(T) - 1); if (AlignBias) { AlignBias = alignof(T) - AlignBias; } uint32 PotentialUsed = Used + AlignBias + (sizeof(T) * Count); if (PotentialUsed > Max) { Extend(PotentialUsed, 256); } void* Ret = GetDataPtr() + Used + AlignBias; Used = PotentialUsed; return (T*)Ret; } //////////////////////////////////////////////////////////////////////////////// FBuffer::FMutableSection FBuffer::GetMutableFree(uint32 MinSize, uint32 PageSize) { MinSize = (MinSize == 0 && Used == Max) ? PageSize : MinSize; uint32 PotentialUsed = Used + MinSize; if (PotentialUsed > Max) { Extend(PotentialUsed, PageSize); } return FMutableSection{ GetDataPtr() + Used, Max - Used }; } //////////////////////////////////////////////////////////////////////////////// void FBuffer::AdvanceUsed(uint32 Delta) { Used += Delta; check(Used <= Max); } //////////////////////////////////////////////////////////////////////////////// void FBuffer::Extend(uint32 AtLeast, uint32 PageSize) { checkSlow((PageSize - 1 & PageSize) == 0); --PageSize; Max = (AtLeast + PageSize) & ~PageSize; if (!Inline) { Data = UPTRINT(FMemory::Realloc(GetDataPtr(), Max, alignof(FBuffer))); return; } const char* PrevData = GetDataPtr(); Data = UPTRINT(FMemory::Malloc(Max, alignof(FBuffer))); ::memcpy(GetDataPtr(), PrevData, Used); Inline = 0; } // {{{1 throttler .............................................................. //////////////////////////////////////////////////////////////////////////////// static void ThrottleTest(FAnsiStringView); //////////////////////////////////////////////////////////////////////////////// class FThrottler { public: FThrottler(); void SetLimit(uint32 KiBPerSec); int32 GetAllowance(); void ReturnUnused(int32 Unused); private: friend void ThrottleTest(FAnsiStringView); int32 GetAllowance(int64 CycleDelta); int64 CycleFreq; int64 CycleLast = 0; int64 CyclePeriod = 0; uint32 Limit = 0; enum : uint32 { LIMITLESS = MAX_int32, SLICES_POW2 = 5, }; }; //////////////////////////////////////////////////////////////////////////////// FThrottler::FThrottler() { CycleFreq = int64(1.0 / FPlatformTime::GetSecondsPerCycle64()); check(CycleFreq >> SLICES_POW2); } //////////////////////////////////////////////////////////////////////////////// void FThrottler::SetLimit(uint32 KiBPerSec) { // 512MiB/s might as well be limitless. KiBPerSec = (KiBPerSec < (512 << 10)) ? KiBPerSec : 0; if (KiBPerSec) { KiBPerSec = FMath::Max(KiBPerSec, 1u << SLICES_POW2); } Limit = KiBPerSec << 10; } //////////////////////////////////////////////////////////////////////////////// int32 FThrottler::GetAllowance() { int64 Cycle = FPlatformTime::Cycles64(); int64 CycleDelta = Cycle - CycleLast; CycleLast = Cycle; return GetAllowance(CycleDelta); } //////////////////////////////////////////////////////////////////////////////// int32 FThrottler::GetAllowance(int64 CycleDelta) { if (Limit == 0) { return LIMITLESS; } int64 CycleSlice = CycleFreq >> SLICES_POW2; CycleDelta = FMath::Min(CycleDelta, CycleSlice); CyclePeriod -= CycleDelta; if (CyclePeriod > 0) { return 0 - int32((CyclePeriod * 1000ll) / CycleFreq); } CyclePeriod += CycleSlice; int32 Released = Limit >> SLICES_POW2; return Released; } //////////////////////////////////////////////////////////////////////////////// void FThrottler::ReturnUnused(int32 Unused) { if (Limit == 0 || Unused == 0) { return; } int64 CycleReturn = (CycleFreq * Unused) / Limit; CycleLast -= CycleReturn; } // }}} } // namespace UE::IoStore::HTTP