1341 lines
39 KiB
C++
1341 lines
39 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "Http/HttpClient.h"
|
|
|
|
#include "Async/InheritedContext.h"
|
|
#include "Async/ManualResetEvent.h"
|
|
#include "Containers/AnsiString.h"
|
|
#include "Containers/ConsumeAllMpmcQueue.h"
|
|
#include "Containers/LockFreeList.h"
|
|
#include "Containers/StringView.h"
|
|
#include "HAL/PlatformProcess.h"
|
|
#include "HAL/Thread.h"
|
|
#include "Memory/CompositeBuffer.h"
|
|
#include "Memory/MemoryView.h"
|
|
#include "Misc/App.h"
|
|
#include "Misc/EngineVersion.h"
|
|
#include "Misc/StringBuilder.h"
|
|
#include "Serialization/CompactBinary.h"
|
|
#include "String/Find.h"
|
|
#include "Templates/RefCounting.h"
|
|
#include "Templates/UnrealTemplate.h"
|
|
|
|
#if WITH_SSL
|
|
#include "Ssl.h"
|
|
#include <openssl/ssl.h>
|
|
#endif
|
|
|
|
#ifndef CURL_NO_OLDIES
|
|
#define CURL_NO_OLDIES
|
|
#endif
|
|
|
|
#if PLATFORM_MICROSOFT
|
|
#include "Microsoft/AllowMicrosoftPlatformTypes.h"
|
|
#endif
|
|
#ifdef PLATFORM_CURL_INCLUDE
|
|
#include PLATFORM_CURL_INCLUDE
|
|
#else
|
|
#include "curl/curl.h"
|
|
#endif
|
|
#if PLATFORM_MICROSOFT
|
|
#include "Microsoft/HideMicrosoftPlatformTypes.h"
|
|
#endif
|
|
|
|
#include <atomic>
|
|
|
|
namespace UE { class FCurlHttpClient; }
|
|
namespace UE { class FCurlHttpRequest; }
|
|
namespace UE { class FCurlHttpResponse; }
|
|
|
|
namespace UE::CurlHttp::Private
|
|
{
|
|
|
|
template <typename T>
|
|
static bool TryIncrement(std::atomic<T>& Value, const T Max)
|
|
{
|
|
for (T Existing = Value.load(std::memory_order_relaxed);;)
|
|
{
|
|
if (Existing >= Max)
|
|
{
|
|
return false;
|
|
}
|
|
if (Value.compare_exchange_weak(Existing, Existing + 1, std::memory_order_relaxed))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
template <typename T>
|
|
static T AtomicFetchOr(std::atomic<T>& Atomic, const T Value, std::memory_order MemoryOrder = std::memory_order_seq_cst)
|
|
{
|
|
for (T Existing = Atomic.load(MemoryOrder);;)
|
|
{
|
|
if (Atomic.compare_exchange_weak(Existing, Existing | Value, MemoryOrder))
|
|
{
|
|
return Existing;
|
|
}
|
|
}
|
|
}
|
|
|
|
template <typename T, typename PredicateType>
|
|
static bool AtomicFetchOrIf(
|
|
std::atomic<T>& Atomic,
|
|
const T Value,
|
|
PredicateType Predicate,
|
|
std::memory_order MemoryOrder = std::memory_order_seq_cst)
|
|
{
|
|
for (T Existing = Atomic.load(MemoryOrder);;)
|
|
{
|
|
if (!Predicate(Existing))
|
|
{
|
|
return false;
|
|
}
|
|
if (Atomic.compare_exchange_weak(Existing, Existing | Value, MemoryOrder))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
} // UE::CurlHttp::Private
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
namespace UE
|
|
{
|
|
|
|
DEFINE_LOG_CATEGORY_STATIC(LogHttp, Display, All);
|
|
|
|
class FCurlHttpHeaders
|
|
{
|
|
public:
|
|
FCurlHttpHeaders() = default;
|
|
FCurlHttpHeaders(const FCurlHttpHeaders&) = delete;
|
|
FCurlHttpHeaders& operator=(const FCurlHttpHeaders&) = delete;
|
|
|
|
inline ~FCurlHttpHeaders()
|
|
{
|
|
curl_slist_free_all(List);
|
|
}
|
|
|
|
inline void Reset()
|
|
{
|
|
curl_slist_free_all(List);
|
|
List = nullptr;
|
|
}
|
|
|
|
inline void AddHeader(const ANSICHAR* Header)
|
|
{
|
|
List = curl_slist_append(List, Header);
|
|
}
|
|
|
|
inline curl_slist* GetList() const { return List; }
|
|
|
|
private:
|
|
curl_slist* List = nullptr;
|
|
};
|
|
|
|
class FCurlHttpManager final : public IHttpManager
|
|
{
|
|
public:
|
|
FCurlHttpManager();
|
|
|
|
THttpUniquePtr<IHttpConnectionPool> CreateConnectionPool(const FHttpConnectionPoolParams& Params) final;
|
|
|
|
void SetDefaultOptions(CURL* Curl, FCurlHttpHeaders& Headers);
|
|
|
|
private:
|
|
const FCbObjectId SessionId = FApp::GetSessionObjectId();
|
|
std::atomic<uint32> RequestId = 1;
|
|
FUtf8StringBuilderBase UserAgent;
|
|
};
|
|
|
|
class FCurlHttpConnectionPool final : public IHttpConnectionPool
|
|
{
|
|
public:
|
|
FCurlHttpConnectionPool(FCurlHttpManager& Manager, const FHttpConnectionPoolParams& Params);
|
|
|
|
THttpUniquePtr<IHttpClient> CreateClient(const FHttpClientParams& Params) final;
|
|
void DeleteClient(FCurlHttpClient* Client);
|
|
|
|
void SetDefaultOptions(CURL* Curl, FCurlHttpHeaders& Headers) const;
|
|
|
|
bool BeginAsyncRequest(FCurlHttpResponse* Response);
|
|
void CancelAsyncRequest(FCurlHttpResponse* Response);
|
|
|
|
private:
|
|
~FCurlHttpConnectionPool() final;
|
|
void Destroy() final { delete this; }
|
|
|
|
void ThreadLoop();
|
|
void CompleteRequest(CURL* Curl, CURLcode* OptionalReturnCode);
|
|
|
|
static void CurlLock(CURL* Curl, curl_lock_data Data, curl_lock_access Access, void* Param);
|
|
static void CurlUnlock(CURL* Curl, curl_lock_data Data, void* Param);
|
|
|
|
static void AssertShareCodeOk(CURLSHcode Code);
|
|
static void AssertMultiCodeOk(CURLMcode Code);
|
|
|
|
FCurlHttpManager& Manager;
|
|
CURLSH* CurlShare;
|
|
CURLM* CurlMulti;
|
|
|
|
FRWLock Locks[CURL_LOCK_DATA_LAST];
|
|
bool WriteLocked[CURL_LOCK_DATA_LAST]{};
|
|
|
|
std::atomic<uint32> ClientCount = 0;
|
|
|
|
enum class EThreadCommandType
|
|
{
|
|
Begin,
|
|
Cancel,
|
|
};
|
|
|
|
struct FThreadCommand
|
|
{
|
|
TRefCountPtr<FCurlHttpResponse> Response;
|
|
EThreadCommandType Type;
|
|
};
|
|
|
|
TConsumeAllMpmcQueue<FThreadCommand> ThreadCommands;
|
|
FThread Thread;
|
|
std::atomic<bool> bThreadStarting;
|
|
std::atomic<bool> bThreadStopping;
|
|
};
|
|
|
|
class FCurlHttpClient final : public IHttpClient
|
|
{
|
|
public:
|
|
FCurlHttpClient(FCurlHttpConnectionPool& ConnectionPool, const FHttpClientParams& Params);
|
|
|
|
THttpUniquePtr<IHttpRequest> TryCreateRequest(const FHttpRequestParams& Params) final;
|
|
void DeleteRequest(FCurlHttpRequest* Request);
|
|
|
|
void SetDefaultOptions(CURL* Curl, FCurlHttpHeaders& Headers) const;
|
|
|
|
bool BeginAsyncRequest(FCurlHttpResponse* Response);
|
|
void CancelAsyncRequest(FCurlHttpResponse* Response);
|
|
|
|
private:
|
|
~FCurlHttpClient() final;
|
|
void Destroy() final { delete this; }
|
|
|
|
static long ConvertVersion(EHttpVersion Version);
|
|
static long ConvertTlsLevel(EHttpTlsLevel Level);
|
|
|
|
FCurlHttpConnectionPool& ConnectionPool;
|
|
TLockFreePointerListLIFO<FCurlHttpRequest> RequestPool;
|
|
std::atomic<uint32> RequestPoolCount = 0;
|
|
std::atomic<uint32> RequestCount = 0;
|
|
FHttpClientParams Params;
|
|
};
|
|
|
|
class FCurlHttpRequest final : public IHttpRequest
|
|
{
|
|
public:
|
|
explicit FCurlHttpRequest(FCurlHttpClient& Client);
|
|
~FCurlHttpRequest() final;
|
|
|
|
void Reset() final;
|
|
void SetUri(FAnsiStringView Uri) final;
|
|
void SetUnixSocketPath(FAnsiStringView Uri) final;
|
|
void SetMethod(EHttpMethod Method) final;
|
|
void SetBody(const FCompositeBuffer& Body) final;
|
|
void AddHeader(FAnsiStringView Name, FAnsiStringView Value) final;
|
|
|
|
void Send(IHttpReceiver* Receiver, THttpUniquePtr<IHttpResponse>& OutResponse) final;
|
|
void SendAsync(IHttpReceiver* Receiver, THttpUniquePtr<IHttpResponse>& OutResponse) final;
|
|
|
|
void OnComplete(CURLcode Code);
|
|
|
|
private:
|
|
void Destroy() final { Client.DeleteRequest(this); }
|
|
|
|
void SetDefaultOptions();
|
|
void CheckIdle(const TCHAR* FunctionName);
|
|
|
|
FCurlHttpResponse* CreateResponse(IHttpReceiver* Receiver, THttpUniquePtr<IHttpResponse>& OutResponse);
|
|
|
|
static size_t CurlRead(void* Ptr, size_t SizeInBlocks, size_t BlockSizeInBytes, void* Param);
|
|
static size_t CurlSeek(void* Param, curl_off_t Offset, int Origin);
|
|
#if WITH_SSL
|
|
static CURLcode CurlSslContext(CURL* Curl, void* Context, void* Param);
|
|
static int SslCertVerify(int PreverifyOk, X509_STORE_CTX* Context);
|
|
#endif
|
|
|
|
EHttpMethod Method;
|
|
FAnsiStringBuilderBase Uri;
|
|
FAnsiString UnixSocketPath;
|
|
FCurlHttpClient& Client;
|
|
FCurlHttpHeaders Headers;
|
|
FCompositeBuffer Body;
|
|
uint64 BodyOffset = 0;
|
|
CURL* Curl = nullptr;
|
|
std::atomic<FCurlHttpResponse*> Response = nullptr;
|
|
};
|
|
|
|
enum class ECurlHttpResponseState : uint8
|
|
{
|
|
None = 0,
|
|
Complete = 1 << 0,
|
|
Canceled = 1 << 1,
|
|
};
|
|
|
|
ENUM_CLASS_FLAGS(ECurlHttpResponseState);
|
|
|
|
class FCurlHttpResponse final : public IHttpResponse, public IHttpResponseMonitor, private FInheritedContextBase
|
|
{
|
|
public:
|
|
FCurlHttpResponse(CURL* Curl, EHttpMethod Method, FAnsiStringView Uri, IHttpReceiver* Receiver);
|
|
|
|
bool Create();
|
|
|
|
CURL* GetCurl() { return Curl; }
|
|
void SetClient(FCurlHttpClient* InClient) { Client = InClient; }
|
|
void SetComplete(CURLcode Code);
|
|
|
|
TRefCountPtr<IHttpResponseMonitor> GetMonitor() final { return this; }
|
|
|
|
void Cancel() final;
|
|
void Wait() const final { CompleteEvent.Wait(); }
|
|
bool Poll() const final { return EnumHasAnyFlags(State.load(), ECurlHttpResponseState::Complete); }
|
|
bool IsCanceled() const { return EnumHasAnyFlags(State.load(std::memory_order_relaxed), ECurlHttpResponseState::Canceled); }
|
|
|
|
FAnsiStringView GetUri() const final { return Uri; }
|
|
EHttpMethod GetMethod() const final { return Method; }
|
|
int32 GetStatusCode() const final;
|
|
EHttpErrorCode GetErrorCode() const final { return ErrorCode; }
|
|
FAnsiStringView GetError() const final { return Error; }
|
|
const FHttpResponseStats& GetStats() const final;
|
|
TConstArrayView<FAnsiStringView> GetAllHeaders() const final { return HeaderViews; }
|
|
|
|
void AddRef() const final;
|
|
void Release() const final;
|
|
|
|
private:
|
|
~FCurlHttpResponse() final;
|
|
void Destroy() final;
|
|
|
|
void ConditionallySetResponseStatus();
|
|
bool WriteHeader(FAnsiStringView Header);
|
|
bool WriteBody(FMemoryView Body);
|
|
|
|
static size_t CurlDebug(CURL* Curl, curl_infotype DebugInfoType, char* DebugInfo, size_t DebugInfoSize, void* Param);
|
|
static size_t CurlHeader(void* Ptr, size_t SizeInBlocks, size_t BlockSizeInBytes, void* Param);
|
|
static size_t CurlWrite(void* Ptr, size_t SizeInBlocks, size_t BlockSizeInBytes, void* Param);
|
|
|
|
static EHttpErrorCode ConvertErrorCode(CURLcode Code);
|
|
|
|
FAnsiStringBuilderBase Uri;
|
|
EHttpMethod Method;
|
|
bool bHasBody = false;
|
|
EHttpErrorCode ErrorCode = EHttpErrorCode::Unknown;
|
|
std::atomic<ECurlHttpResponseState> State = ECurlHttpResponseState::None;
|
|
std::atomic<int32> StatusCode = -1;
|
|
mutable std::atomic<uint32> ReferenceCount = 0;
|
|
|
|
CURL* Curl; // null when complete
|
|
IHttpReceiver* Receiver;
|
|
FCurlHttpClient* Client = nullptr;
|
|
TArray<FAnsiStringView, TInlineAllocator<32>> HeaderViews;
|
|
TArray<int32, TInlineAllocator<32>> HeaderLengths;
|
|
TAnsiStringBuilder<4096> Headers;
|
|
FAnsiStringView Error;
|
|
ANSICHAR ErrorBuffer[CURL_ERROR_SIZE]{};
|
|
FHttpResponseStats Stats;
|
|
mutable FManualResetEvent CompleteEvent;
|
|
|
|
enum class EHttpReceiverFunction : uint8
|
|
{
|
|
None = 0,
|
|
OnCreate,
|
|
OnHeaders,
|
|
OnBody,
|
|
OnComplete,
|
|
};
|
|
inline static thread_local EHttpReceiverFunction ReceiverFunction;
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
FCurlHttpManager::FCurlHttpManager()
|
|
{
|
|
// User-Agent: UnrealEngine/X.Y.Z-<CL> (<Platform>; <Config> <TargetType>; <BranchName>) <AppName>[CommandletName] (<ProjectName>)
|
|
const FEngineVersion& Version = FEngineVersion::Current();
|
|
UserAgent << ANSITEXTVIEW("User-Agent: UnrealEngine/")
|
|
<< Version.GetMajor() << '.' << Version.GetMinor() << '.' << Version.GetPatch() << '-' << Version.GetChangelist()
|
|
<< ANSITEXTVIEW(" (") << FPlatformProperties::PlatformName()
|
|
<< ANSITEXTVIEW("; ") << LexToString(FApp::GetBuildConfiguration()) << ' ' << LexToString(FApp::GetBuildTargetType())
|
|
<< ANSITEXTVIEW("; ") << FApp::GetBranchName()
|
|
<< ANSITEXTVIEW(") ") << FApp::GetName();
|
|
|
|
if (IsRunningCommandlet())
|
|
{
|
|
FString CommandletName;
|
|
FParse::Value(FCommandLine::Get(), TEXT("Run="), CommandletName);
|
|
CommandletName.ToLowerInline();
|
|
UserAgent << '[' << CommandletName << ']';
|
|
}
|
|
|
|
if (FApp::HasProjectName() && FApp::GetName() != FApp::GetProjectName())
|
|
{
|
|
UserAgent << ANSITEXTVIEW(" (") << FApp::GetProjectName() << ')';
|
|
}
|
|
UserAgent.ToString();
|
|
}
|
|
|
|
THttpUniquePtr<IHttpConnectionPool> FCurlHttpManager::CreateConnectionPool(const FHttpConnectionPoolParams& Params)
|
|
{
|
|
return THttpUniquePtr<IHttpConnectionPool>(new FCurlHttpConnectionPool(*this, Params));
|
|
}
|
|
|
|
void FCurlHttpManager::SetDefaultOptions(CURL* Curl, FCurlHttpHeaders& Headers)
|
|
{
|
|
curl_easy_setopt(Curl, CURLOPT_NOSIGNAL, 1L);
|
|
curl_easy_setopt(Curl, CURLOPT_USERAGENT, UserAgent.GetData());
|
|
|
|
Headers.AddHeader(*WriteToAnsiString<64>(ANSITEXTVIEW("UE-IsBuildMachine: "), GIsBuildMachine));
|
|
Headers.AddHeader(*WriteToAnsiString<64>(ANSITEXTVIEW("UE-Session: "), SessionId));
|
|
Headers.AddHeader(*WriteToAnsiString<32>(ANSITEXTVIEW("UE-Request: "), RequestId.fetch_add(1, std::memory_order_relaxed)));
|
|
|
|
// Remove the Expect: 100-Continue header that curl adds by default because it adds latency.
|
|
Headers.AddHeader("Expect:");
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
FCurlHttpConnectionPool::FCurlHttpConnectionPool(FCurlHttpManager& InManager, const FHttpConnectionPoolParams& Params)
|
|
: Manager(InManager)
|
|
, CurlShare(curl_share_init())
|
|
, CurlMulti(curl_multi_init())
|
|
, bThreadStarting(false)
|
|
, bThreadStopping(!Params.bAllowAsync || !FPlatformProcess::SupportsMultithreading())
|
|
{
|
|
curl_share_setopt(CurlShare, CURLSHOPT_USERDATA, this);
|
|
curl_share_setopt(CurlShare, CURLSHOPT_LOCKFUNC, CurlLock);
|
|
curl_share_setopt(CurlShare, CURLSHOPT_UNLOCKFUNC, CurlUnlock);
|
|
curl_share_setopt(CurlShare, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
|
|
curl_share_setopt(CurlShare, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
|
|
|
|
checkf(!Params.MaxConnections || !Params.MinConnections || Params.MinConnections <= Params.MaxConnections,
|
|
TEXT("MinConnections (%u) exceeds MaxConnections (%u)"), Params.MinConnections, Params.MaxConnections);
|
|
constexpr uint32 DefaultConnectionCount = 8;
|
|
const uint32 MaxConnections = Params.MaxConnections ? Params.MaxConnections : FMath::Max(Params.MinConnections, DefaultConnectionCount);
|
|
const uint32 MinConnections = Params.MinConnections ? Params.MinConnections : FMath::Min(Params.MaxConnections, DefaultConnectionCount);
|
|
curl_multi_setopt(CurlMulti, CURLMOPT_MAXCONNECTS, MinConnections);
|
|
curl_multi_setopt(CurlMulti, CURLMOPT_MAX_TOTAL_CONNECTIONS, MaxConnections);
|
|
if (Params.MaxRequestsPerConnection)
|
|
{
|
|
curl_multi_setopt(CurlMulti, CURLMOPT_MAX_CONCURRENT_STREAMS, (long)FMath::Min(Params.MaxRequestsPerConnection, (uint32)MAX_int32));
|
|
}
|
|
curl_multi_setopt(CurlMulti, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
|
|
}
|
|
|
|
FCurlHttpConnectionPool::~FCurlHttpConnectionPool()
|
|
{
|
|
checkf(ClientCount.load(std::memory_order_relaxed) == 0,
|
|
TEXT("Connection pool has %u clients that were not deleted."), ClientCount.load());
|
|
|
|
bThreadStopping.store(true, std::memory_order_relaxed);
|
|
AssertMultiCodeOk(curl_multi_wakeup(CurlMulti));
|
|
if (Thread.IsJoinable())
|
|
{
|
|
Thread.Join();
|
|
}
|
|
|
|
check(ThreadCommands.IsEmpty());
|
|
|
|
AssertMultiCodeOk(curl_multi_cleanup(CurlMulti));
|
|
AssertShareCodeOk(curl_share_cleanup(CurlShare));
|
|
}
|
|
|
|
THttpUniquePtr<IHttpClient> FCurlHttpConnectionPool::CreateClient(const FHttpClientParams& Params)
|
|
{
|
|
ClientCount.fetch_add(1, std::memory_order_relaxed);
|
|
return THttpUniquePtr<IHttpClient>(new FCurlHttpClient(*this, Params));
|
|
}
|
|
|
|
void FCurlHttpConnectionPool::DeleteClient(FCurlHttpClient* Client)
|
|
{
|
|
ClientCount.fetch_sub(1, std::memory_order_relaxed);
|
|
}
|
|
|
|
void FCurlHttpConnectionPool::SetDefaultOptions(CURL* Curl, FCurlHttpHeaders& Headers) const
|
|
{
|
|
Manager.SetDefaultOptions(Curl, Headers);
|
|
|
|
curl_easy_setopt(Curl, CURLOPT_SHARE, CurlShare);
|
|
}
|
|
|
|
bool FCurlHttpConnectionPool::BeginAsyncRequest(FCurlHttpResponse* Response)
|
|
{
|
|
if (bThreadStopping.load(std::memory_order_relaxed))
|
|
{
|
|
return false;
|
|
}
|
|
if (ThreadCommands.ProduceItem(FThreadCommand{Response, EThreadCommandType::Begin}) == EConsumeAllMpmcQueueResult::WasEmpty)
|
|
{
|
|
AssertMultiCodeOk(curl_multi_wakeup(CurlMulti));
|
|
}
|
|
if (!bThreadStarting.load(std::memory_order_relaxed) && !bThreadStarting.exchange(true, std::memory_order_relaxed))
|
|
{
|
|
Thread = FThread(TEXT("HttpConnectionPool"), [this] { ThreadLoop(); }, 128 * 1024);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void FCurlHttpConnectionPool::CancelAsyncRequest(FCurlHttpResponse* Response)
|
|
{
|
|
if (ThreadCommands.ProduceItem(FThreadCommand{Response, EThreadCommandType::Cancel}) == EConsumeAllMpmcQueueResult::WasEmpty)
|
|
{
|
|
AssertMultiCodeOk(curl_multi_wakeup(CurlMulti));
|
|
}
|
|
}
|
|
|
|
void FCurlHttpConnectionPool::ThreadLoop()
|
|
{
|
|
while (!ThreadCommands.IsEmpty() || !bThreadStopping.load(std::memory_order_relaxed))
|
|
{
|
|
ThreadCommands.ConsumeAllFifo([this](FThreadCommand Command)
|
|
{
|
|
if (CURL* Curl = Command.Response->GetCurl())
|
|
{
|
|
switch (Command.Type)
|
|
{
|
|
case EThreadCommandType::Begin:
|
|
AssertMultiCodeOk(curl_multi_add_handle(CurlMulti, Curl));
|
|
break;
|
|
case EThreadCommandType::Cancel:
|
|
CompleteRequest(Curl, nullptr);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
int ActiveRequests = 0;
|
|
AssertMultiCodeOk(curl_multi_perform(CurlMulti, &ActiveRequests));
|
|
|
|
for (int MessagesInQueue = 0; CURLMsg* Message = curl_multi_info_read(CurlMulti, &MessagesInQueue);)
|
|
{
|
|
if (Message->msg == CURLMSG_DONE)
|
|
{
|
|
CompleteRequest(Message->easy_handle, &Message->data.result);
|
|
}
|
|
}
|
|
|
|
constexpr int WaitTimeMs = 60'000;
|
|
AssertMultiCodeOk(curl_multi_poll(CurlMulti, nullptr, 0, WaitTimeMs, nullptr));
|
|
}
|
|
}
|
|
|
|
void FCurlHttpConnectionPool::CompleteRequest(CURL* Curl, CURLcode* OptionalReturnCode)
|
|
{
|
|
AssertMultiCodeOk(curl_multi_remove_handle(CurlMulti, Curl));
|
|
void* Request = nullptr;
|
|
curl_easy_getinfo(Curl, CURLINFO_PRIVATE, &Request);
|
|
((FCurlHttpRequest*)Request)->OnComplete(OptionalReturnCode ? *OptionalReturnCode : CURLE_OK);
|
|
}
|
|
|
|
void FCurlHttpConnectionPool::CurlLock(CURL* Curl, curl_lock_data Data, curl_lock_access Access, void* Param)
|
|
{
|
|
FCurlHttpConnectionPool& ConnectionPool = *(FCurlHttpConnectionPool*)Param;
|
|
if (Access == CURL_LOCK_ACCESS_SHARED)
|
|
{
|
|
ConnectionPool.Locks[Data].ReadLock();
|
|
}
|
|
else
|
|
{
|
|
ConnectionPool.Locks[Data].WriteLock();
|
|
ConnectionPool.WriteLocked[Data] = true;
|
|
}
|
|
}
|
|
|
|
void FCurlHttpConnectionPool::CurlUnlock(CURL* Curl, curl_lock_data Data, void* Param)
|
|
{
|
|
FCurlHttpConnectionPool& ConnectionPool = *(FCurlHttpConnectionPool*)Param;
|
|
if (!ConnectionPool.WriteLocked[Data])
|
|
{
|
|
ConnectionPool.Locks[Data].ReadUnlock();
|
|
}
|
|
else
|
|
{
|
|
ConnectionPool.WriteLocked[Data] = false;
|
|
ConnectionPool.Locks[Data].WriteUnlock();
|
|
}
|
|
}
|
|
|
|
inline void FCurlHttpConnectionPool::AssertShareCodeOk(const CURLSHcode Code)
|
|
{
|
|
checkf(Code == CURLSHE_OK, TEXT("Error in curl_share operation: %hs"), curl_share_strerror(Code));
|
|
}
|
|
|
|
inline void FCurlHttpConnectionPool::AssertMultiCodeOk(const CURLMcode Code)
|
|
{
|
|
checkf(Code == CURLM_OK, TEXT("Error in curl_multi operation: %hs"), curl_multi_strerror(Code));
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
FCurlHttpClient::FCurlHttpClient(FCurlHttpConnectionPool& InConnectionPool, const FHttpClientParams& InParams)
|
|
: ConnectionPool(InConnectionPool)
|
|
, Params(InParams)
|
|
{
|
|
checkf(!Params.MaxRequests || !Params.MinRequests || Params.MinRequests <= Params.MaxRequests,
|
|
TEXT("MinRequests (%u) exceeds MaxRequests (%u)"), Params.MinRequests, Params.MaxRequests);
|
|
Params.MaxRequests = Params.MaxRequests ? Params.MaxRequests : FMath::Max(Params.MinRequests, 256u);
|
|
Params.MinRequests = Params.MinRequests ? Params.MinRequests : FMath::Min(Params.MaxRequests, 16u);
|
|
}
|
|
|
|
FCurlHttpClient::~FCurlHttpClient()
|
|
{
|
|
while (FCurlHttpRequest* Request = RequestPool.Pop())
|
|
{
|
|
delete Request;
|
|
RequestCount.fetch_sub(1, std::memory_order_relaxed);
|
|
RequestPoolCount.fetch_sub(1, std::memory_order_relaxed);
|
|
}
|
|
|
|
checkf(RequestCount.load(std::memory_order_relaxed) == 0,
|
|
TEXT("Client has %u requests that were not destroyed by IHttpRequest::Destroy()."), RequestCount.load());
|
|
|
|
ConnectionPool.DeleteClient(this);
|
|
}
|
|
|
|
THttpUniquePtr<IHttpRequest> FCurlHttpClient::TryCreateRequest(const FHttpRequestParams& RequestParams)
|
|
{
|
|
if (IHttpRequest* Request = RequestPool.Pop())
|
|
{
|
|
RequestPoolCount.fetch_sub(1, std::memory_order_relaxed);
|
|
return THttpUniquePtr<IHttpRequest>(Request);
|
|
}
|
|
|
|
if (RequestParams.bIgnoreMaxRequests)
|
|
{
|
|
RequestCount.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
else if (!CurlHttp::Private::TryIncrement(RequestCount, Params.MaxRequests))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
return THttpUniquePtr<IHttpRequest>(new FCurlHttpRequest(*this));
|
|
}
|
|
|
|
void FCurlHttpClient::DeleteRequest(FCurlHttpRequest* Request)
|
|
{
|
|
if (CurlHttp::Private::TryIncrement(RequestPoolCount, Params.MinRequests))
|
|
{
|
|
Request->Reset();
|
|
RequestPool.Push(Request);
|
|
}
|
|
else
|
|
{
|
|
delete Request;
|
|
RequestCount.fetch_sub(1, std::memory_order_relaxed);
|
|
}
|
|
|
|
if (Params.OnDestroyRequest)
|
|
{
|
|
Params.OnDestroyRequest();
|
|
}
|
|
}
|
|
|
|
void FCurlHttpClient::SetDefaultOptions(CURL* Curl, FCurlHttpHeaders& Headers) const
|
|
{
|
|
ConnectionPool.SetDefaultOptions(Curl, Headers);
|
|
|
|
curl_easy_setopt(Curl, CURLOPT_HTTP_VERSION, ConvertVersion(Params.Version));
|
|
|
|
curl_easy_setopt(Curl, CURLOPT_DNS_CACHE_TIMEOUT, (long)FMath::Min(Params.DnsCacheTimeout, (uint32)MAX_int32));
|
|
curl_easy_setopt(Curl, CURLOPT_CONNECTTIMEOUT_MS, long(Params.ConnectTimeout));
|
|
curl_easy_setopt(Curl, CURLOPT_LOW_SPEED_LIMIT, long(Params.LowSpeedLimit));
|
|
curl_easy_setopt(Curl, CURLOPT_LOW_SPEED_TIME, long(Params.LowSpeedTime));
|
|
curl_easy_setopt(Curl, CURLOPT_USE_SSL, ConvertTlsLevel(Params.TlsLevel));
|
|
curl_easy_setopt(Curl, CURLOPT_FOLLOWLOCATION, long(Params.bFollowRedirects));
|
|
curl_easy_setopt(Curl, CURLOPT_POSTREDIR, long(
|
|
(Params.bFollow301Post ? CURL_REDIR_POST_301 : 0) |
|
|
(Params.bFollow302Post ? CURL_REDIR_POST_302 : 0) |
|
|
(Params.bFollow303Post ? CURL_REDIR_POST_303 : 0)));
|
|
if (Params.bBypassProxy)
|
|
{
|
|
curl_easy_setopt(Curl, CURLOPT_NOPROXY, "*");
|
|
}
|
|
curl_easy_setopt(Curl, CURLOPT_VERBOSE, long(Params.bVerbose));
|
|
}
|
|
|
|
long FCurlHttpClient::ConvertVersion(const EHttpVersion Version)
|
|
{
|
|
switch (Version)
|
|
{
|
|
case EHttpVersion::None: return CURL_HTTP_VERSION_NONE;
|
|
case EHttpVersion::V1_0: return CURL_HTTP_VERSION_1_0;
|
|
case EHttpVersion::V1_1: return CURL_HTTP_VERSION_1_1;
|
|
case EHttpVersion::V2: return CURL_HTTP_VERSION_2_0;
|
|
case EHttpVersion::V2Only: return CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE;
|
|
default: checkNoEntry(); return CURL_HTTP_VERSION_NONE;
|
|
}
|
|
}
|
|
|
|
long FCurlHttpClient::ConvertTlsLevel(const EHttpTlsLevel Level)
|
|
{
|
|
switch (Level)
|
|
{
|
|
case EHttpTlsLevel::None: return CURLUSESSL_NONE;
|
|
case EHttpTlsLevel::Try: return CURLUSESSL_TRY;
|
|
case EHttpTlsLevel::All: return CURLUSESSL_ALL;
|
|
default: checkNoEntry(); return CURLUSESSL_NONE;
|
|
}
|
|
}
|
|
|
|
inline bool FCurlHttpClient::BeginAsyncRequest(FCurlHttpResponse* Response)
|
|
{
|
|
Response->SetClient(this);
|
|
return ConnectionPool.BeginAsyncRequest(Response);
|
|
}
|
|
|
|
inline void FCurlHttpClient::CancelAsyncRequest(FCurlHttpResponse* Response)
|
|
{
|
|
ConnectionPool.CancelAsyncRequest(Response);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
FCurlHttpRequest::FCurlHttpRequest(FCurlHttpClient& InClient)
|
|
: Client(InClient)
|
|
, Curl(curl_easy_init())
|
|
{
|
|
SetDefaultOptions();
|
|
}
|
|
|
|
FCurlHttpRequest::~FCurlHttpRequest()
|
|
{
|
|
curl_easy_cleanup(Curl);
|
|
}
|
|
|
|
void FCurlHttpRequest::Reset()
|
|
{
|
|
CheckIdle(TEXT("Reset"));
|
|
curl_easy_reset(Curl);
|
|
Headers.Reset();
|
|
Uri.Reset();
|
|
UnixSocketPath.Empty();
|
|
Body.Reset();
|
|
SetDefaultOptions();
|
|
}
|
|
|
|
void FCurlHttpRequest::SetDefaultOptions()
|
|
{
|
|
Method = EHttpMethod::Get;
|
|
|
|
Client.SetDefaultOptions(Curl, Headers);
|
|
|
|
curl_easy_setopt(Curl, CURLOPT_PRIVATE, this);
|
|
curl_easy_setopt(Curl, CURLOPT_READDATA, this);
|
|
curl_easy_setopt(Curl, CURLOPT_READFUNCTION, CurlRead);
|
|
curl_easy_setopt(Curl, CURLOPT_SEEKDATA, this);
|
|
curl_easy_setopt(Curl, CURLOPT_SEEKFUNCTION, CurlSeek);
|
|
#if WITH_SSL
|
|
curl_easy_setopt(Curl, CURLOPT_SSL_CTX_DATA, this);
|
|
curl_easy_setopt(Curl, CURLOPT_SSL_CTX_FUNCTION, CurlSslContext);
|
|
#endif
|
|
}
|
|
|
|
void FCurlHttpRequest::CheckIdle(const TCHAR* FunctionName)
|
|
{
|
|
checkf(!Response.load(std::memory_order_relaxed),
|
|
TEXT("%.*hs %.*hs: %s requires the request to be idle."),
|
|
LexToString(Method).Len(), LexToString(Method).GetData(), Uri.Len(), Uri.GetData(), FunctionName);
|
|
}
|
|
|
|
void FCurlHttpRequest::SetUri(const FAnsiStringView InUri)
|
|
{
|
|
CheckIdle(TEXT("SetUri"));
|
|
Uri.Reset();
|
|
Uri.Append(InUri);
|
|
curl_easy_setopt(Curl, CURLOPT_URL, *Uri);
|
|
}
|
|
|
|
void FCurlHttpRequest::SetUnixSocketPath(const FAnsiStringView InSocketPath)
|
|
{
|
|
CheckIdle(TEXT("SetUnixSocketPath"));
|
|
UnixSocketPath.Empty();
|
|
UnixSocketPath.Append(InSocketPath);
|
|
|
|
if (UnixSocketPath.Len() > 0)
|
|
{
|
|
curl_easy_setopt(Curl, CURLOPT_UNIX_SOCKET_PATH, *UnixSocketPath);
|
|
}
|
|
else
|
|
{
|
|
curl_easy_setopt(Curl, CURLOPT_UNIX_SOCKET_PATH, NULL);
|
|
}
|
|
}
|
|
|
|
void FCurlHttpRequest::SetMethod(const EHttpMethod InMethod)
|
|
{
|
|
CheckIdle(TEXT("SetMethod"));
|
|
Method = InMethod;
|
|
switch (Method)
|
|
{
|
|
case EHttpMethod::Get:
|
|
curl_easy_setopt(Curl, CURLOPT_HTTPGET, 1L);
|
|
break;
|
|
case EHttpMethod::Put:
|
|
curl_easy_setopt(Curl, CURLOPT_UPLOAD, 1L);
|
|
break;
|
|
case EHttpMethod::Post:
|
|
curl_easy_setopt(Curl, CURLOPT_POST, 1L);
|
|
break;
|
|
case EHttpMethod::Head:
|
|
curl_easy_setopt(Curl, CURLOPT_NOBODY, 1L);
|
|
break;
|
|
case EHttpMethod::Delete:
|
|
curl_easy_setopt(Curl, CURLOPT_CUSTOMREQUEST, "DELETE");
|
|
break;
|
|
default:
|
|
checkNoEntry();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void FCurlHttpRequest::SetBody(const FCompositeBuffer& InBody)
|
|
{
|
|
CheckIdle(TEXT("SetBody"));
|
|
Body = InBody;
|
|
}
|
|
|
|
void FCurlHttpRequest::AddHeader(const FAnsiStringView Name, const FAnsiStringView Value)
|
|
{
|
|
CheckIdle(TEXT("AddHeader"));
|
|
Headers.AddHeader(*WriteToAnsiString<256>(Name, ANSITEXTVIEW(": "), Value));
|
|
}
|
|
|
|
void FCurlHttpRequest::Send(IHttpReceiver* Receiver, THttpUniquePtr<IHttpResponse>& OutResponse)
|
|
{
|
|
CheckIdle(TEXT("Send"));
|
|
if (CreateResponse(Receiver, OutResponse))
|
|
{
|
|
OnComplete(curl_easy_perform(Curl));
|
|
}
|
|
}
|
|
|
|
void FCurlHttpRequest::SendAsync(IHttpReceiver* Receiver, THttpUniquePtr<IHttpResponse>& OutResponse)
|
|
{
|
|
CheckIdle(TEXT("SendAsync"));
|
|
if (FCurlHttpResponse* LocalResponse = CreateResponse(Receiver, OutResponse))
|
|
{
|
|
if (!Client.BeginAsyncRequest(LocalResponse))
|
|
{
|
|
OnComplete(curl_easy_perform(Curl));
|
|
}
|
|
}
|
|
}
|
|
|
|
FCurlHttpResponse* FCurlHttpRequest::CreateResponse(IHttpReceiver* Receiver, THttpUniquePtr<IHttpResponse>& OutResponse)
|
|
{
|
|
BodyOffset = 0;
|
|
|
|
if (const uint64 BodySize = Body.GetSize(); BodySize > 0)
|
|
{
|
|
switch (Method)
|
|
{
|
|
case EHttpMethod::Put:
|
|
curl_easy_setopt(Curl, CURLOPT_INFILESIZE_LARGE, curl_off_t(BodySize));
|
|
break;
|
|
case EHttpMethod::Post:
|
|
curl_easy_setopt(Curl, CURLOPT_POSTFIELDSIZE_LARGE, curl_off_t(BodySize));
|
|
break;
|
|
default:
|
|
checkf(false, TEXT("%s"), *WriteToString<256>(TEXTVIEW("Method "), LexToString(Method),
|
|
TEXTVIEW(" must not include a body but a body of "), BodySize, TEXTVIEW(" bytes was provided.")));
|
|
break;
|
|
}
|
|
}
|
|
|
|
curl_easy_setopt(Curl, CURLOPT_HTTPHEADER, Headers.GetList());
|
|
|
|
FCurlHttpResponse* LocalResponse = new FCurlHttpResponse(Curl, Method, Uri, Receiver);
|
|
OutResponse = THttpUniquePtr<IHttpResponse>(LocalResponse);
|
|
|
|
if (LocalResponse->Create())
|
|
{
|
|
verify(!Response.exchange(LocalResponse, std::memory_order_release));
|
|
return LocalResponse;
|
|
}
|
|
LocalResponse->SetComplete(CURLE_OK);
|
|
return nullptr;
|
|
}
|
|
|
|
void FCurlHttpRequest::OnComplete(CURLcode Code)
|
|
{
|
|
if (FCurlHttpResponse* LocalResponse = Response.exchange(nullptr, std::memory_order_acquire))
|
|
{
|
|
LocalResponse->SetComplete(Code);
|
|
}
|
|
}
|
|
|
|
size_t FCurlHttpRequest::CurlRead(void* Ptr, size_t SizeInBlocks, size_t BlockSizeInBytes, void* Param)
|
|
{
|
|
FCurlHttpRequest* Request = (FCurlHttpRequest*)Param;
|
|
const size_t TotalSize = Request->Body.GetSize();
|
|
const size_t Offset = Request->BodyOffset;
|
|
const size_t Size = FMath::Min(TotalSize - Offset, SizeInBlocks * BlockSizeInBytes);
|
|
Request->Body.CopyTo(MakeMemoryView(Ptr, Size), Offset);
|
|
Request->BodyOffset += Size;
|
|
return Size;
|
|
}
|
|
|
|
size_t FCurlHttpRequest::CurlSeek(void* Param, curl_off_t Offset, int Origin)
|
|
{
|
|
FCurlHttpRequest* Request = (FCurlHttpRequest*)Param;
|
|
|
|
const size_t TotalSize = Request->Body.GetSize();
|
|
size_t AbsoluteOffset = 0;
|
|
|
|
switch (Origin)
|
|
{
|
|
case SEEK_SET: AbsoluteOffset = Offset; break;
|
|
case SEEK_CUR: AbsoluteOffset = Request->BodyOffset + Offset; break;
|
|
case SEEK_END: AbsoluteOffset = TotalSize + Offset; break;
|
|
}
|
|
|
|
if (AbsoluteOffset > TotalSize)
|
|
{
|
|
return CURL_SEEKFUNC_FAIL;
|
|
}
|
|
|
|
Request->BodyOffset = AbsoluteOffset;
|
|
return CURL_SEEKFUNC_OK;
|
|
}
|
|
|
|
#if WITH_SSL
|
|
CURLcode FCurlHttpRequest::CurlSslContext(CURL* Curl, void* Context, void* Param)
|
|
{
|
|
SSL_CTX* SslContext = (SSL_CTX*)Context;
|
|
FSslModule::Get().GetCertificateManager().AddCertificatesToSslContext(SslContext);
|
|
SSL_CTX_set_verify(SslContext, SSL_CTX_get_verify_mode(SslContext), SslCertVerify);
|
|
SSL_CTX_set_app_data(SslContext, Param);
|
|
return CURLE_OK;
|
|
}
|
|
|
|
int FCurlHttpRequest::SslCertVerify(int PreverifyOk, X509_STORE_CTX* Context)
|
|
{
|
|
if (PreverifyOk == 1)
|
|
{
|
|
SSL* Ssl = (SSL*)X509_STORE_CTX_get_ex_data(Context, SSL_get_ex_data_X509_STORE_CTX_idx());
|
|
check(Ssl);
|
|
|
|
SSL_CTX* SslContext = SSL_get_SSL_CTX(Ssl);
|
|
check(SslContext);
|
|
|
|
FCurlHttpRequest* Request = (FCurlHttpRequest*)SSL_CTX_get_app_data(SslContext);
|
|
check(Request);
|
|
|
|
// Extract the domain from the URI.
|
|
FAnsiStringView Domain = Request->Uri;
|
|
if (const int32 SchemeIndex = String::FindFirst(Domain, ANSITEXTVIEW("://")); SchemeIndex != INDEX_NONE)
|
|
{
|
|
Domain.RightChopInline(SchemeIndex + ANSITEXTVIEW("://").Len());
|
|
}
|
|
if (const int32 SlashIndex = String::FindFirstChar(Domain, '/'); SlashIndex != INDEX_NONE)
|
|
{
|
|
Domain.LeftInline(SlashIndex);
|
|
}
|
|
if (const int32 AtIndex = String::FindFirstChar(Domain, '@'); AtIndex != INDEX_NONE)
|
|
{
|
|
Domain.RightChopInline(AtIndex + 1);
|
|
}
|
|
const auto RemovePort = [](FAnsiStringView& Authority)
|
|
{
|
|
if (const int32 ColonIndex = String::FindLastChar(Authority, ':'); ColonIndex != INDEX_NONE)
|
|
{
|
|
Authority.LeftInline(ColonIndex);
|
|
}
|
|
};
|
|
if (Domain.StartsWith('['))
|
|
{
|
|
if (const int32 LastBracketIndex = String::FindLastChar(Domain, ']'); LastBracketIndex != INDEX_NONE)
|
|
{
|
|
Domain.MidInline(1, LastBracketIndex - 1);
|
|
}
|
|
else
|
|
{
|
|
RemovePort(Domain);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
RemovePort(Domain);
|
|
}
|
|
|
|
if (!FSslModule::Get().GetCertificateManager().VerifySslCertificates(Context, FString(Domain)))
|
|
{
|
|
PreverifyOk = 0;
|
|
}
|
|
}
|
|
|
|
return PreverifyOk;
|
|
}
|
|
#endif
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
FCurlHttpResponse::FCurlHttpResponse(CURL* InCurl, EHttpMethod InMethod, FAnsiStringView InUri, IHttpReceiver* InReceiver)
|
|
: Method(InMethod)
|
|
, Curl(InCurl)
|
|
, Receiver(InReceiver)
|
|
{
|
|
CaptureInheritedContext();
|
|
|
|
// Release() is called by Destroy().
|
|
AddRef();
|
|
|
|
Uri.Append(InUri);
|
|
checkf(Receiver, TEXT("Receiver must not be null for %s"), *WriteToString<256>(*this));
|
|
|
|
curl_easy_setopt(Curl, CURLOPT_ERRORBUFFER, ErrorBuffer);
|
|
curl_easy_setopt(Curl, CURLOPT_DEBUGDATA, this);
|
|
curl_easy_setopt(Curl, CURLOPT_DEBUGFUNCTION, CurlDebug);
|
|
curl_easy_setopt(Curl, CURLOPT_HEADERDATA, this);
|
|
curl_easy_setopt(Curl, CURLOPT_HEADERFUNCTION, CurlHeader);
|
|
curl_easy_setopt(Curl, CURLOPT_WRITEDATA, this);
|
|
curl_easy_setopt(Curl, CURLOPT_WRITEFUNCTION, CurlWrite);
|
|
}
|
|
|
|
bool FCurlHttpResponse::Create()
|
|
{
|
|
for (TGuardValue GuardReceiverFunction(ReceiverFunction, EHttpReceiverFunction::OnCreate);;)
|
|
{
|
|
IHttpReceiver* NewReceiver = Receiver->OnCreate(*this);
|
|
if (Receiver == NewReceiver)
|
|
{
|
|
break;
|
|
}
|
|
checkf(NewReceiver, TEXT("Receiver must not be null for %s"), *WriteToString<256>(*this));
|
|
Receiver = NewReceiver;
|
|
}
|
|
return !IsCanceled();
|
|
}
|
|
|
|
FCurlHttpResponse::~FCurlHttpResponse()
|
|
{
|
|
checkf(!Curl, TEXT("Response must be completed before it is destroyed for %s"), *WriteToString<256>(*this));
|
|
}
|
|
|
|
void FCurlHttpResponse::Destroy()
|
|
{
|
|
if (Curl)
|
|
{
|
|
Cancel();
|
|
}
|
|
|
|
// AddRef() is called by the constructor.
|
|
Release();
|
|
}
|
|
|
|
void FCurlHttpResponse::ConditionallySetResponseStatus()
|
|
{
|
|
if (StatusCode < 0)
|
|
{
|
|
long ResponseCode;
|
|
curl_easy_getinfo(Curl, CURLINFO_RESPONSE_CODE, &ResponseCode);
|
|
if (ResponseCode > 0)
|
|
{
|
|
StatusCode = int32(ResponseCode);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool FCurlHttpResponse::WriteHeader(FAnsiStringView Header)
|
|
{
|
|
if (bHasBody)
|
|
{
|
|
return !EnumHasAnyFlags(State.load(std::memory_order_relaxed), ECurlHttpResponseState::Canceled);
|
|
}
|
|
|
|
FInheritedContextScope InheritedContextScope = RestoreInheritedContext();
|
|
|
|
// Reset headers between responses and keep only the last.
|
|
if (!HeaderViews.IsEmpty())
|
|
{
|
|
HeaderLengths.Reset();
|
|
HeaderViews.Reset();
|
|
Headers.Reset();
|
|
StatusCode = -1;
|
|
}
|
|
|
|
ConditionallySetResponseStatus();
|
|
|
|
if (Header == ANSITEXTVIEW("\r\n"))
|
|
{
|
|
const ANSICHAR* Data = Headers.GetData();
|
|
for (const int32 Len : HeaderLengths)
|
|
{
|
|
HeaderViews.Add(MakeStringView(Data, Len - 2));
|
|
Data += Len;
|
|
}
|
|
for (TGuardValue GuardReceiverFunction(ReceiverFunction, EHttpReceiverFunction::OnHeaders);;)
|
|
{
|
|
IHttpReceiver* NewReceiver = Receiver->OnHeaders(*this);
|
|
if (Receiver == NewReceiver)
|
|
{
|
|
break;
|
|
}
|
|
checkf(NewReceiver, TEXT("Receiver must not be null for %s"), *WriteToString<256>(*this));
|
|
Receiver = NewReceiver;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
HeaderLengths.Add(Header.Len());
|
|
Headers.Append(Header);
|
|
}
|
|
|
|
return !IsCanceled();
|
|
}
|
|
|
|
bool FCurlHttpResponse::WriteBody(FMemoryView Body)
|
|
{
|
|
FInheritedContextScope InheritedContextScope = RestoreInheritedContext();
|
|
|
|
ConditionallySetResponseStatus();
|
|
|
|
bHasBody = true;
|
|
|
|
while (!Body.IsEmpty() && !IsCanceled())
|
|
{
|
|
const FMemoryView InitialBody = Body;
|
|
IHttpReceiver* NewReceiver;
|
|
{
|
|
TGuardValue GuardReceiverFunction(ReceiverFunction, EHttpReceiverFunction::OnBody);
|
|
NewReceiver = Receiver->OnBody(*this, Body);
|
|
}
|
|
if (Receiver == NewReceiver)
|
|
{
|
|
checkf(Body.IsEmpty() || Body == InitialBody,
|
|
TEXT("Receiver must consume the entire body for %s"), *WriteToString<256>(*this));
|
|
break;
|
|
}
|
|
checkf(Body.IsEmpty() || Body.GetDataEnd() == InitialBody.GetDataEnd(),
|
|
TEXT("Receiver must consume the body sequentially for %s"), *WriteToString<256>(*this));
|
|
checkf(NewReceiver, TEXT("Receiver must not be null for %s"), *WriteToString<256>(*this));
|
|
Receiver = NewReceiver;
|
|
}
|
|
|
|
return !IsCanceled();
|
|
}
|
|
|
|
void FCurlHttpResponse::SetComplete(CURLcode Code)
|
|
{
|
|
Error = ErrorBuffer;
|
|
if (IsCanceled())
|
|
{
|
|
ErrorCode = EHttpErrorCode::Canceled;
|
|
Error = ANSITEXTVIEW("Canceled using IHttpResponse::Cancel()");
|
|
StatusCode = 0;
|
|
}
|
|
else if (Code == CURLE_OK)
|
|
{
|
|
ErrorCode = EHttpErrorCode::None;
|
|
ConditionallySetResponseStatus();
|
|
}
|
|
else
|
|
{
|
|
ErrorCode = ConvertErrorCode(Code);
|
|
if (Error.IsEmpty())
|
|
{
|
|
Error = curl_easy_strerror(Code);
|
|
}
|
|
StatusCode = 0;
|
|
}
|
|
|
|
const auto GetSizeInfo = [this](CURLINFO Info)
|
|
{
|
|
curl_off_t Value;
|
|
curl_easy_getinfo(Curl, Info, &Value);
|
|
return uint64(Value);
|
|
};
|
|
const auto GetDoubleInfo = [this](CURLINFO Info)
|
|
{
|
|
double Value;
|
|
curl_easy_getinfo(Curl, Info, &Value);
|
|
return Value;
|
|
};
|
|
Stats.SendSize = GetSizeInfo(CURLINFO_SIZE_UPLOAD_T);
|
|
Stats.RecvSize = GetSizeInfo(CURLINFO_SIZE_DOWNLOAD_T);
|
|
Stats.SendRate = GetSizeInfo(CURLINFO_SPEED_UPLOAD_T);
|
|
Stats.RecvRate = GetSizeInfo(CURLINFO_SPEED_DOWNLOAD_T);
|
|
Stats.NameResolveTime = GetDoubleInfo(CURLINFO_NAMELOOKUP_TIME);
|
|
Stats.ConnectTime = GetDoubleInfo(CURLINFO_CONNECT_TIME);
|
|
Stats.TlsConnectTime = GetDoubleInfo(CURLINFO_APPCONNECT_TIME);
|
|
Stats.PreTransferTime = GetDoubleInfo(CURLINFO_PRETRANSFER_TIME);
|
|
Stats.StartTransferTime = GetDoubleInfo(CURLINFO_STARTTRANSFER_TIME);
|
|
Stats.TotalTime = GetDoubleInfo(CURLINFO_TOTAL_TIME);
|
|
|
|
Curl = nullptr;
|
|
Client = nullptr;
|
|
|
|
// Hold a reference to safely access Receiver, State, CompleteEvent, and FInheritedContextBase.
|
|
TRefCountPtr<FCurlHttpResponse> Self(this);
|
|
FInheritedContextScope InheritedContextScope = RestoreInheritedContext();
|
|
|
|
for (TGuardValue GuardReceiverFunction(ReceiverFunction, EHttpReceiverFunction::OnComplete);;)
|
|
{
|
|
IHttpReceiver* NewReceiver = Receiver->OnComplete(*this);
|
|
if (Receiver == NewReceiver || !NewReceiver)
|
|
{
|
|
break;
|
|
}
|
|
Receiver = NewReceiver;
|
|
}
|
|
|
|
CurlHttp::Private::AtomicFetchOr(State, ECurlHttpResponseState::Complete);
|
|
CompleteEvent.Notify();
|
|
|
|
if (UE_LOG_ACTIVE(LogHttp, Verbose))
|
|
{
|
|
TStringBuilder<80> StatsText;
|
|
if (Stats.SendSize)
|
|
{
|
|
StatsText << TEXTVIEW("sent ") << Stats.SendSize << TEXTVIEW(" bytes, ");
|
|
}
|
|
if (Stats.RecvSize)
|
|
{
|
|
StatsText << TEXTVIEW("received ") << Stats.RecvSize << TEXTVIEW(" bytes, ");
|
|
}
|
|
StatsText.Appendf(TEXT("%.3f seconds"), Stats.TotalTime);
|
|
UE_LOG(LogHttp, Verbose, TEXT("%s (%s)"), *WriteToString<256>(*this), *StatsText);
|
|
}
|
|
|
|
// DO NOT ACCESS THIS AGAIN PAST THIS POINT!
|
|
}
|
|
|
|
void FCurlHttpResponse::Cancel()
|
|
{
|
|
const EHttpReceiverFunction LocalReceiverFunction = ReceiverFunction;
|
|
checkf(LocalReceiverFunction != EHttpReceiverFunction::OnComplete,
|
|
TEXT("Cancel() must not be called from within OnComplete for %s"), *WriteToString<256>(*this));
|
|
|
|
// Cancel only once and only if not completed.
|
|
const auto NotCompleteOrCanceled = [](ECurlHttpResponseState CheckState) -> bool
|
|
{
|
|
return !EnumHasAnyFlags(CheckState, ECurlHttpResponseState::Complete | ECurlHttpResponseState::Canceled);
|
|
};
|
|
if (CurlHttp::Private::AtomicFetchOrIf(State, ECurlHttpResponseState::Canceled, NotCompleteOrCanceled))
|
|
{
|
|
// Queue cancellation of an async request when called from outside of a receiver function.
|
|
// Cancellation within a receiver is handled by WriterHeader or WriteBody returning false,
|
|
// which causes an error in curl that causes the request to complete immediately.
|
|
if (Client && LocalReceiverFunction == EHttpReceiverFunction::None)
|
|
{
|
|
Client->CancelAsyncRequest(this);
|
|
}
|
|
}
|
|
|
|
// Wait for completion when called from outside of a receiver function.
|
|
// Waiting within a receiver function would prevent the response from completing.
|
|
if (LocalReceiverFunction == EHttpReceiverFunction::None)
|
|
{
|
|
CompleteEvent.Wait();
|
|
}
|
|
}
|
|
|
|
int32 FCurlHttpResponse::GetStatusCode() const
|
|
{
|
|
return ErrorCode == EHttpErrorCode::None ? StatusCode.load(std::memory_order_relaxed) : 0;
|
|
}
|
|
|
|
const FHttpResponseStats& FCurlHttpResponse::GetStats() const
|
|
{
|
|
checkf(!Curl, TEXT("Stats accessed before they are available for %s"), *WriteToString<256>(*this));
|
|
return Stats;
|
|
}
|
|
|
|
size_t FCurlHttpResponse::CurlDebug(CURL* Curl, curl_infotype DebugInfoType, char* DebugInfo, size_t DebugInfoSize, void* Param)
|
|
{
|
|
if (DebugInfoType != CURLINFO_TEXT && DebugInfoType != CURLINFO_HEADER_IN)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
FAnsiStringView DebugText(DebugInfo, DebugInfoSize);
|
|
DebugText.TrimStartAndEndInline();
|
|
if (DebugText.IsEmpty())
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
FCurlHttpResponse* Response = (FCurlHttpResponse*)Param;
|
|
const FAnsiStringView Method = LexToString(Response->GetMethod());
|
|
const FAnsiStringView Uri = Response->GetUri();
|
|
switch (DebugInfoType)
|
|
{
|
|
case CURLINFO_TEXT:
|
|
UE_LOG(LogHttp, VeryVerbose, TEXT("%.*hs %.*hs (%p): [TEXT] %.*hs"),
|
|
Method.Len(), Method.GetData(), Uri.Len(), Uri.GetData(), Response, DebugText.Len(), DebugText.GetData());
|
|
break;
|
|
case CURLINFO_HEADER_IN:
|
|
UE_LOG(LogHttp, VeryVerbose, TEXT("%.*hs %.*hs (%p): [RESPONSE] %.*hs"),
|
|
Method.Len(), Method.GetData(), Uri.Len(), Uri.GetData(), Response, DebugText.Len(), DebugText.GetData());
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
size_t FCurlHttpResponse::CurlHeader(void* Ptr, size_t SizeInBlocks, size_t BlockSizeInBytes, void* Param)
|
|
{
|
|
const int32 HeaderSize = IntCastChecked<int32>(SizeInBlocks * BlockSizeInBytes);
|
|
return ((FCurlHttpResponse*)Param)->WriteHeader(MakeStringView((const ANSICHAR*)Ptr, HeaderSize)) ? HeaderSize : 0;
|
|
}
|
|
|
|
size_t FCurlHttpResponse::CurlWrite(void* Ptr, size_t SizeInBlocks, size_t BlockSizeInBytes, void* Param)
|
|
{
|
|
const size_t Size = SizeInBlocks * BlockSizeInBytes;
|
|
return ((FCurlHttpResponse*)Param)->WriteBody(MakeMemoryView(Ptr, Size)) ? Size : 0;
|
|
}
|
|
|
|
EHttpErrorCode FCurlHttpResponse::ConvertErrorCode(CURLcode Code)
|
|
{
|
|
switch (Code)
|
|
{
|
|
case CURLE_OK:
|
|
return EHttpErrorCode::None;
|
|
case CURLE_COULDNT_RESOLVE_HOST:
|
|
return EHttpErrorCode::ResolveHost;
|
|
case CURLE_COULDNT_CONNECT:
|
|
return EHttpErrorCode::Connect;
|
|
case CURLE_SSL_CONNECT_ERROR:
|
|
return EHttpErrorCode::TlsConnect;
|
|
case CURLE_PEER_FAILED_VERIFICATION:
|
|
return EHttpErrorCode::TlsPeerVerification;
|
|
case CURLE_OPERATION_TIMEDOUT:
|
|
return EHttpErrorCode::TimedOut;
|
|
default:
|
|
return EHttpErrorCode::Unknown;
|
|
}
|
|
}
|
|
|
|
void FCurlHttpResponse::AddRef() const
|
|
{
|
|
ReferenceCount.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
|
|
void FCurlHttpResponse::Release() const
|
|
{
|
|
if (ReferenceCount.fetch_sub(1, std::memory_order_acq_rel) == 1)
|
|
{
|
|
delete this;
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
IHttpManager& IHttpManager::Get()
|
|
{
|
|
static FCurlHttpManager CurlHttpManager;
|
|
return CurlHttpManager;
|
|
}
|
|
|
|
} // UE
|