// Copyright Epic Games, Inc. All Rights Reserved. #if WITH_CURL #include "Curl/CurlHttp.h" #include "Stats/Stats.h" #include "Misc/App.h" #include "HttpConstants.h" #include "HttpModule.h" #include "Http.h" #include "Misc/EngineVersion.h" #include "Misc/Paths.h" #include "Curl/CurlHttpManager.h" #include "Misc/ScopeLock.h" #include "HAL/IConsoleManager.h" #include "Internationalization/Regex.h" #if WITH_SSL #include "Ssl.h" #include #endif TAutoConsoleVariable CVarHttpCurlAllowHTTP2( TEXT("http.CurlAllowHTTP2"), false, TEXT("Whether to allow HTTP/2 for curl requests"), ECVF_Default ); #if WITH_SSL static int SslCertVerify(int PreverifyOk, X509_STORE_CTX* Context) { if (PreverifyOk == 1) { SSL* Handle = static_cast(X509_STORE_CTX_get_ex_data(Context, SSL_get_ex_data_X509_STORE_CTX_idx())); check(Handle); SSL_CTX* SslContext = SSL_get_SSL_CTX(Handle); check(SslContext); FCurlHttpRequest* Request = static_cast(SSL_CTX_get_app_data(SslContext)); check(Request); const FString Domain = FPlatformHttp::GetUrlDomain(Request->GetURL()); if (!FSslModule::Get().GetCertificateManager().VerifySslCertificates(Context, Domain)) { PreverifyOk = 0; } } return PreverifyOk; } static CURLcode sslctx_function(CURL * curl, void * sslctx, void * parm) { SSL_CTX* Context = static_cast(sslctx); const ISslCertificateManager& CertificateManager = FSslModule::Get().GetCertificateManager(); CertificateManager.AddCertificatesToSslContext(Context); if (FCurlHttpManager::CurlRequestOptions.bVerifyPeer) { FCurlHttpRequest* Request = static_cast(parm); SSL_CTX_set_verify(Context, SSL_CTX_get_verify_mode(Context), SslCertVerify); SSL_CTX_set_app_data(Context, Request); } /* all set to go */ return CURLE_OK; } #endif //#if WITH_SSL FCurlHttpRequest::FCurlHttpRequest() : EasyHandle(nullptr) , HeaderList(nullptr) , Verb(TEXT("GET")) , bCurlRequestCompleted(false) , bRedirected(false) , CurlAddToMultiResult(CURLM_OK) , CurlCompletionResult(CURLE_OK) , bAnyHttpActivity(false) , BytesSent(0) , TotalBytesSent(0) , TotalBytesRead(0) , LastReportedBytesRead(0) , LastReportedBytesSent(0) , LeastRecentlyCachedInfoMessageIndex(0) { checkf(FCurlHttpManager::IsInit(), TEXT("Curl request was created while the library is shutdown")); EasyHandle = curl_easy_init(); // Always setup the debug function to allow for activity to be tracked curl_easy_setopt(EasyHandle, CURLOPT_DEBUGDATA, this); curl_easy_setopt(EasyHandle, CURLOPT_DEBUGFUNCTION, StaticDebugCallback); curl_easy_setopt(EasyHandle, CURLOPT_VERBOSE, 1L); curl_easy_setopt(EasyHandle, CURLOPT_BUFFERSIZE, FCurlHttpManager::CurlRequestOptions.BufferSize); curl_easy_setopt(EasyHandle, CURLOPT_USE_SSL, CURLUSESSL_ALL); // HTTP2 is linked in for newer libcurl builds and the library will use it by default. // There have been issues found with it use in production on long lived servers with heavy HTTP usage, for // that reason we're disabling its use by default in the general purpose curl request wrapper and only // allowing use of HTTP2 from other curl wrappers like the DerivedDataCache one. // Note that CURL_HTTP_VERSION_1_1 was the default for libcurl version before 7.62.0 if (CVarHttpCurlAllowHTTP2.GetValueOnAnyThread()) { curl_easy_setopt(EasyHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS); } else { curl_easy_setopt(EasyHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); } // set certificate verification (disable to allow self-signed certificates) if (FCurlHttpManager::CurlRequestOptions.bVerifyPeer) { curl_easy_setopt(EasyHandle, CURLOPT_SSL_VERIFYPEER, 1L); } else { curl_easy_setopt(EasyHandle, CURLOPT_SSL_VERIFYPEER, 0L); } // allow http redirects to be followed curl_easy_setopt(EasyHandle, CURLOPT_FOLLOWLOCATION, 1L); // required for all multi-threaded handles curl_easy_setopt(EasyHandle, CURLOPT_NOSIGNAL, 1L); // associate with this just in case curl_easy_setopt(EasyHandle, CURLOPT_PRIVATE, this); const FString& ProxyAddress = FHttpModule::Get().GetProxyAddress(); if (!ProxyAddress.IsEmpty()) { // guaranteed to be valid at this point curl_easy_setopt(EasyHandle, CURLOPT_PROXY, TCHAR_TO_ANSI(*ProxyAddress)); } const FString& HttpNoProxy = FHttpModule::Get().GetHttpNoProxy(); if (!HttpNoProxy.IsEmpty()) { curl_easy_setopt(EasyHandle, CURLOPT_NOPROXY, TCHAR_TO_ANSI(*HttpNoProxy)); } if (FCurlHttpManager::CurlRequestOptions.bDontReuseConnections) { curl_easy_setopt(EasyHandle, CURLOPT_FORBID_REUSE, 1L); } #if PLATFORM_LINUX && !WITH_SSL static const char* const CertBundlePath = []() -> const char* { static const char * KnownBundlePaths[] = { "/etc/pki/tls/certs/ca-bundle.crt", "/etc/ssl/certs/ca-certificates.crt", "/etc/ssl/ca-bundle.pem" }; for (const char* BundlePath : KnownBundlePaths) { FString FileName(BundlePath); UE_LOG(LogHttp, Log, TEXT(" Libcurl: checking if '%s' exists"), *FileName); if (FPaths::FileExists(FileName)) { return BundlePath; } } return nullptr; }(); // set CURLOPT_CAINFO to a bundle we know exists as the default may not be present curl_easy_setopt(EasyHandle, CURLOPT_CAINFO, CertBundlePath); #endif curl_easy_setopt(EasyHandle, CURLOPT_SSLCERTTYPE, "PEM"); #if WITH_SSL // unset CURLOPT_CAINFO as certs will be added via sslctx_function curl_easy_setopt(EasyHandle, CURLOPT_CAINFO, nullptr); curl_easy_setopt(EasyHandle, CURLOPT_SSL_CTX_FUNCTION, *sslctx_function); curl_easy_setopt(EasyHandle, CURLOPT_SSL_CTX_DATA, this); #endif // #if WITH_SSL InfoMessageCache.AddDefaulted(NumberOfInfoMessagesToCache); // Add default headers const TMap& DefaultHeaders = FHttpModule::Get().GetDefaultHeaders(); for (TMap::TConstIterator It(DefaultHeaders); It; ++It) { SetHeader(It.Key(), It.Value()); } bUsePlatformActivityTimeout = false; } FCurlHttpRequest::~FCurlHttpRequest() { checkf(FCurlHttpManager::IsInit(), TEXT("Curl request was held after the library was shutdown.")); if (EasyHandle) { // cleanup the handle first (that order is used in howtos) curl_easy_cleanup(EasyHandle); EasyHandle = nullptr; } // destroy headers list if (HeaderList) { curl_slist_free_all(HeaderList); HeaderList = nullptr; } } FString FCurlHttpRequest::GetHeader(const FString& HeaderName) const { FString Result; const FString* Header = Headers.Find(HeaderName); if (Header != NULL) { Result = *Header; } return Result; } FString FCurlHttpRequest::CombineHeaderKeyValue(const FString& HeaderKey, const FString& HeaderValue) { FString Combined; const TCHAR Separator[] = TEXT(": "); constexpr const int32 SeparatorLength = UE_ARRAY_COUNT(Separator) - 1; Combined.Reserve(HeaderKey.Len() + SeparatorLength + HeaderValue.Len()); Combined.Append(HeaderKey); Combined.AppendChars(Separator, SeparatorLength); Combined.Append(HeaderValue); return Combined; } TArray FCurlHttpRequest::GetAllHeaders() const { TArray Result; Result.Reserve(Headers.Num()); for (const TPair& It : Headers) { Result.Emplace(CombineHeaderKeyValue(It.Key, It.Value)); } return Result; } FString FCurlHttpRequest::GetContentType() const { return GetHeader(TEXT( "Content-Type" )); } uint64 FCurlHttpRequest::GetContentLength() const { return RequestPayload.IsValid() ? RequestPayload->GetContentLength() : 0; } const TArray& FCurlHttpRequest::GetContent() const { static const TArray EmptyContent; return RequestPayload.IsValid() ? RequestPayload->GetContent() : EmptyContent; } void FCurlHttpRequest::SetVerb(const FString& InVerb) { if (CompletionStatus == EHttpRequestStatus::Processing) { UE_LOG(LogHttp, Warning, TEXT("FCurlHttpRequest::SetVerb() - attempted to set verb on a request that is inflight")); return; } check(EasyHandle); Verb = InVerb.ToUpper(); } void FCurlHttpRequest::SetOption(const FName Option, const FString& OptionValue) { if (CompletionStatus == EHttpRequestStatus::Processing) { UE_LOG(LogHttp, Warning, TEXT("FCurlHttpRequest::SetOption() - attempted to set option on a request that is inflight")); return; } check(EasyHandle); FHttpRequestCommon::SetOption(Option, OptionValue); } void FCurlHttpRequest::SetContent(const TArray& ContentPayload) { SetContent(CopyTemp(ContentPayload)); } void FCurlHttpRequest::SetContent(TArray&& ContentPayload) { if (CompletionStatus == EHttpRequestStatus::Processing) { UE_LOG(LogHttp, Warning, TEXT("FCurlHttpRequest::SetContent() - attempted to set content on a request that is inflight")); return; } RequestPayload = MakeUnique(MoveTemp(ContentPayload)); bIsRequestPayloadSeekable = true; } void FCurlHttpRequest::SetContentAsString(const FString& ContentString) { if (CompletionStatus == EHttpRequestStatus::Processing) { UE_LOG(LogHttp, Warning, TEXT("FCurlHttpRequest::SetContentAsString() - attempted to set content on a request that is inflight")); return; } uint64 Utf8Length = FPlatformString::ConvertedLength(*ContentString, ContentString.Len()); TArray Buffer; Buffer.SetNumUninitialized(Utf8Length); FPlatformString::Convert((UTF8CHAR*)Buffer.GetData(), Buffer.Num(), *ContentString, ContentString.Len()); RequestPayload = MakeUnique(MoveTemp(Buffer)); bIsRequestPayloadSeekable = true; } bool FCurlHttpRequest::SetContentAsStreamedFile(const FString& Filename) { if (!SetContentAsStreamedFileDefaultImpl(Filename)) { return false; } bIsRequestPayloadSeekable = false; return true; } bool FCurlHttpRequest::SetContentFromStream(TSharedRef Stream) { UE_LOG(LogHttp, Verbose, TEXT("FCurlHttpRequest::SetContentFromStream() - %s"), *Stream->GetArchiveName()); if (CompletionStatus == EHttpRequestStatus::Processing) { UE_LOG(LogHttp, Warning, TEXT("FCurlHttpRequest::SetContentFromStream() - attempted to set content on a request that is inflight")); return false; } RequestPayload = MakeUnique(Stream); bIsRequestPayloadSeekable = false; return true; } void FCurlHttpRequest::SetHeader(const FString& HeaderName, const FString& HeaderValue) { if (CompletionStatus == EHttpRequestStatus::Processing) { UE_LOG(LogHttp, Warning, TEXT("FCurlHttpRequest::SetHeader() - attempted to set header on a request that is inflight")); return; } Headers.Add(HeaderName, HeaderValue); } void FCurlHttpRequest::AppendToHeader(const FString& HeaderName, const FString& AdditionalHeaderValue) { if (CompletionStatus == EHttpRequestStatus::Processing) { UE_LOG(LogHttp, Warning, TEXT("FCurlHttpRequest::AppendToHeader() - attempted to append to header on a request that is inflight")); return; } if (!HeaderName.IsEmpty() && !AdditionalHeaderValue.IsEmpty()) { FString* PreviousValue = Headers.Find(HeaderName); FString NewValue; if (PreviousValue != nullptr && !PreviousValue->IsEmpty()) { NewValue = (*PreviousValue) + TEXT(", "); } NewValue += AdditionalHeaderValue; SetHeader(HeaderName, NewValue); } } FString FCurlHttpRequest::GetVerb() const { return Verb; } size_t FCurlHttpRequest::StaticUploadCallback(void* Ptr, size_t SizeInBlocks, size_t BlockSizeInBytes, void* UserData) { QUICK_SCOPE_CYCLE_COUNTER(STAT_FCurlHttpRequest_StaticUploadCallback); check(Ptr); check(UserData); // dispatch FCurlHttpRequest* Request = reinterpret_cast(UserData); return Request->UploadCallback(Ptr, SizeInBlocks, BlockSizeInBytes); } int FCurlHttpRequest::StaticSeekCallback(void* UserData, curl_off_t Offset, int Origin) { QUICK_SCOPE_CYCLE_COUNTER(STAT_FCurlHttpRequest_StaticSeekCallback); check(UserData); // dispatch FCurlHttpRequest* Request = reinterpret_cast(UserData); return Request->SeekCallback(Offset, Origin); } size_t FCurlHttpRequest::StaticReceiveResponseHeaderCallback(void* Ptr, size_t SizeInBlocks, size_t BlockSizeInBytes, void* UserData) { QUICK_SCOPE_CYCLE_COUNTER(STAT_FCurlHttpRequest_StaticReceiveResponseHeaderCallback); check(Ptr); check(UserData); // dispatch FCurlHttpRequest* Request = reinterpret_cast(UserData); return Request->ReceiveResponseHeaderCallback(Ptr, SizeInBlocks, BlockSizeInBytes); } size_t FCurlHttpRequest::StaticReceiveResponseBodyCallback(void* Ptr, size_t SizeInBlocks, size_t BlockSizeInBytes, void* UserData) { QUICK_SCOPE_CYCLE_COUNTER(STAT_FCurlHttpRequest_StaticReceiveResponseBodyCallback); check(Ptr); check(UserData); // dispatch FCurlHttpRequest* Request = reinterpret_cast(UserData); size_t Result = Request->ReceiveResponseBodyCallback(Ptr, SizeInBlocks, BlockSizeInBytes); if (Request->DelegateThreadPolicy == EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread) { Request->CheckProgressDelegate(); } return Result; } size_t FCurlHttpRequest::StaticDebugCallback(CURL * Handle, curl_infotype DebugInfoType, char * DebugInfo, size_t DebugInfoSize, void* UserData) { QUICK_SCOPE_CYCLE_COUNTER(STAT_FCurlHttpRequest_StaticDebugCallback); check(Handle); check(UserData); // dispatch FCurlHttpRequest* Request = reinterpret_cast(UserData); return Request->DebugCallback(Handle, DebugInfoType, DebugInfo, DebugInfoSize); } size_t FCurlHttpRequest::ReceiveResponseHeaderCallback(void* Ptr, size_t SizeInBlocks, size_t BlockSizeInBytes) { QUICK_SCOPE_CYCLE_COUNTER(STAT_FCurlHttpRequest_ReceiveResponseHeaderCallback); InitResponse(); OnAnyActivityOccur(TEXTVIEW("Received header")); uint32 HeaderSize = SizeInBlocks * BlockSizeInBytes; if (HeaderSize > 0 && HeaderSize <= CURL_MAX_HTTP_HEADER) { TArray AnsiHeader; AnsiHeader.AddUninitialized(HeaderSize + 1); FMemory::Memcpy(AnsiHeader.GetData(), Ptr, HeaderSize); AnsiHeader[HeaderSize] = 0; FString Header(ANSI_TO_TCHAR(AnsiHeader.GetData())); // kill \n\r Header = Header.Replace(TEXT("\n"), TEXT("")); Header = Header.Replace(TEXT("\r"), TEXT("")); UE_LOG(LogHttp, Verbose, TEXT("%p: Received response header '%s'."), this, *Header); FString HeaderKey, HeaderValue; if (Header.Split(TEXT(":"), &HeaderKey, &HeaderValue)) { HeaderValue.TrimStartInline(); if (!HeaderKey.IsEmpty() && !HeaderValue.IsEmpty() && !bRedirected) { TSharedPtr Response = StaticCastSharedPtr(ResponseCommon); //Store the content length so OnRequestProgress64() delegates have something to work with if (HeaderKey == TEXT("Content-Length")) { Response->ContentLength = FCString::Atoi64(*HeaderValue); } const constexpr FStringView Seperator(TEXTVIEW(", ")); FString NewValue; FString* PreviousValue = Response->Headers.Find(HeaderKey); if (PreviousValue != nullptr && !PreviousValue->IsEmpty()) { NewValue.Reserve(PreviousValue->Len() + Seperator.Len() + HeaderValue.Len()); NewValue = *PreviousValue; NewValue += Seperator; } NewValue += HeaderValue; Response->Headers.Add(HeaderKey, MoveTemp(NewValue)); if (DelegateThreadPolicy == EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread) { OnHeaderReceived().ExecuteIfBound(SharedThis(this), HeaderKey, HeaderValue); } else if (OnHeaderReceived().IsBound()) { NewlyReceivedHeaders.Enqueue(TPair(MoveTemp(HeaderKey), MoveTemp(HeaderValue))); } } } else { if (Header.IsEmpty()) { char* EffectiveUrlPtr = nullptr; if (curl_easy_getinfo(EasyHandle, CURLINFO_EFFECTIVE_URL, &EffectiveUrlPtr) == CURLE_OK) { if (EffectiveUrlPtr) { SetEffectiveURL(FString(EffectiveUrlPtr)); } } } else { long HttpCode = 0; if (CURLE_OK == curl_easy_getinfo(EasyHandle, CURLINFO_RESPONSE_CODE, &HttpCode)) { bRedirected = (HttpCode >= 300 && HttpCode < 400); if (!bRedirected) { TriggerStatusCodeReceivedDelegate(HttpCode); } } } } return HeaderSize; } else { UE_LOG(LogHttp, Warning, TEXT("%p: Could not process response header for request - header size (%d) is invalid."), this, HeaderSize); } return 0; } size_t FCurlHttpRequest::ReceiveResponseBodyCallback(void* Ptr, size_t SizeInBlocks, size_t BlockSizeInBytes) { QUICK_SCOPE_CYCLE_COUNTER(STAT_FCurlHttpRequest_ReceiveResponseBodyCallback); LLM_SCOPE(ELLMTag::Networking); InitResponse(); OnAnyActivityOccur(TEXTVIEW("Received body")); // Number of bytes actually taken care of. If that amount differs from the amount passed to your // callback function, it will signal an error condition to the library. This will cause the transfer // to get aborted and the libcurl function used will return CURLE_WRITE_ERROR. size_t NumberOfBytesProcessed = 0; uint64 SizeToDownload = SizeInBlocks * BlockSizeInBytes; TSharedPtr Response = StaticCastSharedPtr(ResponseCommon); UE_LOG(LogHttp, Verbose, TEXT("%p: ReceiveResponseBodyCallback: %llu bytes out of %llu received. (SizeInBlocks=%llu, BlockSizeInBytes=%llu, TotalBytesRead=%llu, Response->GetContentLength()=%llu, SizeToDownload=%llu (<-this will get returned from the callback))"), this, TotalBytesRead.load() + SizeToDownload, Response->GetContentLength(), SizeInBlocks, BlockSizeInBytes, TotalBytesRead.load(), Response->GetContentLength(), SizeToDownload); // note that we can be passed 0 bytes if file transmitted has 0 length if (SizeToDownload == 0) { return NumberOfBytesProcessed; } if (bInitializedWithValidStream) { if (PassReceivedDataToStream(Ptr, SizeToDownload)) { NumberOfBytesProcessed = SizeToDownload; } else if (bCanceled) { // If it's because of cancellation, set processed size as well so curl don't raise a warning caused by CURLE_WRITE_ERROR // The transfer will be stopped by cancel flow anyway NumberOfBytesProcessed = SizeToDownload; } } else { Response->Payload.AddUninitialized(SizeToDownload); check(TotalBytesRead.load() + SizeToDownload <= Response->Payload.Num()); FMemory::Memcpy(static_cast(Response->Payload.GetData()) + TotalBytesRead.load(), Ptr, SizeToDownload); NumberOfBytesProcessed = SizeToDownload; } TotalBytesRead += NumberOfBytesProcessed; return NumberOfBytesProcessed; } size_t FCurlHttpRequest::UploadCallback(void* Ptr, size_t SizeInBlocks, size_t BlockSizeInBytes) { OnAnyActivityOccur(TEXTVIEW("Upload callback")); size_t MaxBufferSize = SizeInBlocks * BlockSizeInBytes; size_t SizeAlreadySent = BytesSent.load(); size_t SizeSentThisTime = RequestPayload->FillOutputBuffer(Ptr, MaxBufferSize, SizeAlreadySent); BytesSent += SizeSentThisTime; TotalBytesSent += SizeSentThisTime; UE_LOG(LogHttp, Verbose, TEXT("%p: UploadCallback: %llu bytes out of %llu sent (%llu bytes total sent). (SizeInBlocks=%llu, BlockSizeInBytes=%llu, SizeToSendThisTime=%llu (<-this will get returned from the callback))"), this, BytesSent.load(), RequestPayload->GetContentLength(), TotalBytesSent.load(), SizeInBlocks, BlockSizeInBytes, SizeSentThisTime); return SizeSentThisTime; } int FCurlHttpRequest::SeekCallback(curl_off_t Offset, int Origin) { // Only support seeking to the very beginning if (bIsRequestPayloadSeekable && Origin == SEEK_SET && Offset == 0) { UE_LOG(LogHttp, Log, TEXT("%p: SeekCallback: Resetting to the beginning. We had uploaded %llu bytes"), this, BytesSent.load()); BytesSent.store(0); bIsRequestPayloadSeekable = false; // Do not attempt to re-seek return CURL_SEEKFUNC_OK; } UE_LOG(LogHttp, Warning, TEXT("%p: SeekCallback: Failed to seek to Offset=%lld, Origin=%d %s"), this, (int64)(Offset), Origin, bIsRequestPayloadSeekable ? TEXT("not implemented") : TEXT("seek disabled")); return CURL_SEEKFUNC_CANTSEEK; } size_t FCurlHttpRequest::DebugCallback(CURL * Handle, curl_infotype DebugInfoType, char * DebugInfo, size_t DebugInfoSize) { check(Handle == EasyHandle); // make sure it's us #if CURL_ENABLE_DEBUG_CALLBACK switch(DebugInfoType) { case CURLINFO_TEXT: { // in this case DebugInfo is a C string (see http://curl.haxx.se/libcurl/c/debug.html) // C string is not null terminated: https://curl.haxx.se/libcurl/c/CURLOPT_DEBUGFUNCTION.html // Truncate at 1023 characters. This is just an arbitrary number based on a buffer size seen in // the libcurl code. DebugInfoSize = FMath::Min(DebugInfoSize, (size_t)1023); // Calculate the actual length of the string due to incorrect use of snprintf() in lib/vtls/openssl.c. char* FoundNulPtr = (char*)memchr(DebugInfo, 0, DebugInfoSize); int CalculatedSize = FoundNulPtr != nullptr ? FoundNulPtr - DebugInfo : DebugInfoSize; FString DebugText(CalculatedSize, static_cast(DebugInfo)); DebugText.ReplaceInline(TEXT("\n"), TEXT(""), ESearchCase::CaseSensitive); DebugText.ReplaceInline(TEXT("\r"), TEXT(""), ESearchCase::CaseSensitive); UE_LOG(LogHttp, VeryVerbose, TEXT("%p: '%s'"), this, *DebugText); const FScopeLock CacheLock(&InfoMessageCacheCriticalSection); if (InfoMessageCache.Num() > 0) { InfoMessageCache[LeastRecentlyCachedInfoMessageIndex] = MoveTemp(DebugText); LeastRecentlyCachedInfoMessageIndex = (LeastRecentlyCachedInfoMessageIndex + 1) % InfoMessageCache.Num(); } } break; case CURLINFO_HEADER_IN: UE_LOG(LogHttp, VeryVerbose, TEXT("%p: Received header (%" SIZE_T_FMT " bytes)"), this, DebugInfoSize); break; case CURLINFO_HEADER_OUT: { if (UE_LOG_ACTIVE(LogHttp, VeryVerbose)) { // C string is not null terminated: https://curl.haxx.se/libcurl/c/CURLOPT_DEBUGFUNCTION.html // Scan for \r\n\r\n. According to some code in tool_cb_dbg.c, special processing is needed for // CURLINFO_HEADER_OUT blocks when containing both headers and data (which may be binary). // // Truncate at 1023 characters. This is just an arbitrary number based on a buffer size seen in // the libcurl code. int RecalculatedSize = FMath::Min(DebugInfoSize, (size_t)1023); for (int Index = 0; Index <= RecalculatedSize - 4; ++Index) { if (DebugInfo[Index] == '\r' && DebugInfo[Index + 1] == '\n' && DebugInfo[Index + 2] == '\r' && DebugInfo[Index + 3] == '\n') { RecalculatedSize = Index; break; } } // As lib/http.c states that CURLINFO_HEADER_OUT may contain binary data, only print it if // the header data is readable. bool bIsPrintable = true; for (int Index = 0; Index < RecalculatedSize; ++Index) { unsigned char Ch = DebugInfo[Index]; if (!isprint(Ch) && !isspace(Ch)) { bIsPrintable = false; break; } } if (bIsPrintable) { FString DebugText(RecalculatedSize, static_cast(DebugInfo)); DebugText.ReplaceInline(TEXT("\n"), TEXT(""), ESearchCase::CaseSensitive); DebugText.ReplaceInline(TEXT("\r"), TEXT(""), ESearchCase::CaseSensitive); UE_LOG(LogHttp, VeryVerbose, TEXT("%p: Sent header (%d bytes) - %s"), this, RecalculatedSize, *DebugText); } else { UE_LOG(LogHttp, VeryVerbose, TEXT("%p: Sent header (%d bytes) - contains binary data"), this, RecalculatedSize); } } } break; case CURLINFO_DATA_IN: UE_LOG(LogHttp, VeryVerbose, TEXT("%p: Received data (%" SIZE_T_FMT " bytes)"), this, DebugInfoSize); break; case CURLINFO_DATA_OUT: UE_LOG(LogHttp, VeryVerbose, TEXT("%p: Sent data (%" SIZE_T_FMT " bytes)"), this, DebugInfoSize); break; case CURLINFO_SSL_DATA_IN: UE_LOG(LogHttp, VeryVerbose, TEXT("%p: Received SSL data (%" SIZE_T_FMT " bytes)"), this, DebugInfoSize); break; case CURLINFO_SSL_DATA_OUT: UE_LOG(LogHttp, VeryVerbose, TEXT("%p: Sent SSL data (%" SIZE_T_FMT " bytes)"), this, DebugInfoSize); break; default: UE_LOG(LogHttp, VeryVerbose, TEXT("%p: DebugCallback: Unknown DebugInfoType=%d (DebugInfoSize: %" SIZE_T_FMT " bytes)"), this, (int32)DebugInfoType, DebugInfoSize); break; } #endif // CURL_ENABLE_DEBUG_CALLBACK switch (DebugInfoType) { case CURLINFO_HEADER_IN: OnAnyActivityOccur(TEXTVIEW("Header in")); break; case CURLINFO_HEADER_OUT: // Unlike libCurl, currently there is an issue in xCurl that it triggers CURLINFO_HEADER_OUT even if can't // connect. Had to disable this code, make sure not to treat that event as connected/activity happened #if !WITH_CURL_XCURL OnAnyActivityOccur(TEXTVIEW("Header out")); #endif break; case CURLINFO_DATA_IN: OnAnyActivityOccur(TEXTVIEW("Data in")); break; case CURLINFO_DATA_OUT: OnAnyActivityOccur(TEXTVIEW("Data out")); break; case CURLINFO_SSL_DATA_IN: OnAnyActivityOccur(TEXTVIEW("Ssl data in")); break; case CURLINFO_SSL_DATA_OUT: OnAnyActivityOccur(TEXTVIEW("Ssl data out")); break; default: break; } return 0; } void FCurlHttpRequest::OnAnyActivityOccur(FStringView Reason) { if (!bAnyHttpActivity) { bAnyHttpActivity = true; #if WITH_CURL_XCURL ConnectTime = FPlatformTime::Seconds() - StartProcessTime; #else curl_easy_getinfo(EasyHandle, CURLINFO_CONNECT_TIME, &ConnectTime); #endif StartActivityTimeoutTimer(); } ResetActivityTimeoutTimer(Reason); } bool FCurlHttpRequest::SetupRequest() { QUICK_SCOPE_CYCLE_COUNTER(STAT_FCurlHttpRequest_SetupRequest); check(EasyHandle); if (!RequestPayload.IsValid()) { RequestPayload = MakeUnique(TArray()); bIsRequestPayloadSeekable = true; } if (!OpenRequestPayloadDefaultImpl()) { return false; } bCurlRequestCompleted = false; CurlAddToMultiResult = CURLM_OK; LastReportedBytesSent = 0; UE_LOG(LogHttp, Verbose, TEXT("%p: Custom headers are %s"), this, Headers.Num() ? TEXT("present") : TEXT("NOT present")); UE_LOG(LogHttp, Verbose, TEXT("%p: Payload size=%llu"), this, RequestPayload->GetContentLength()); // content-length should be present http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4 if (GetHeader(TEXT("Content-Length")).IsEmpty()) { SetHeader(TEXT("Content-Length"), FString::Printf(TEXT("%llu"), RequestPayload->GetContentLength())); } // Remove "Expect: 100-continue" since this is supposed to cause problematic behavior on Amazon ELB (and WinInet doesn't send that either) // (also see http://www.iandennismiller.com/posts/curl-http1-1-100-continue-and-multipartform-data-post.html , http://the-stickman.com/web-development/php-and-curl-disabling-100-continue-header/ ) if (GetHeader(TEXT("Expect")).IsEmpty()) { SetHeader(TEXT("Expect"), TEXT("")); } return true; } bool FCurlHttpRequest::SetupRequestHttpThread() { QUICK_SCOPE_CYCLE_COUNTER(STAT_FCurlHttpRequest_SetupRequestHttpThread); { QUICK_SCOPE_CYCLE_COUNTER(STAT_FCurlHttpRequest_SetupRequest_SLIST_FREE_HEADERS); curl_slist_free_all(HeaderList); HeaderList = nullptr; } { QUICK_SCOPE_CYCLE_COUNTER(STAT_FCurlHttpRequest_SetupRequest_EASY_SETOPT); curl_easy_setopt(EasyHandle, CURLOPT_URL, TCHAR_TO_ANSI(*GetURL())); SetupOptionUnixSocketPath(); SetupOptionHttpVersion(); if (!FCurlHttpManager::CurlRequestOptions.LocalHostAddr.IsEmpty()) { // Set the local address to use for making these requests CURLcode ErrCode = curl_easy_setopt(EasyHandle, CURLOPT_INTERFACE, TCHAR_TO_ANSI(*FCurlHttpManager::CurlRequestOptions.LocalHostAddr)); } bool bUseReadFunction = false; // set up verb (note that Verb is expected to be uppercase only) if (Verb == TEXT("POST")) { // If we don't pass any other Content-Type, RequestPayload is assumed to be URL-encoded by this time // In the case of using a streamed file, you must explicitly set the Content-Type, because RequestPayload->IsURLEncoded returns false. check(!GetHeader(TEXT("Content-Type")).IsEmpty() || RequestPayload->IsURLEncoded()); curl_easy_setopt(EasyHandle, CURLOPT_POST, 1L); curl_easy_setopt(EasyHandle, CURLOPT_POSTFIELDS, NULL); #if WITH_CURL_XCURL checkf(RequestPayload->GetContentLength() <= TNumericLimits::Max(), TEXT("xCurl 2206.4.0.0 doesn't support uploading file with length more than 32 bits")); curl_easy_setopt(EasyHandle, CURLOPT_INFILESIZE, RequestPayload->GetContentLength()); #else curl_easy_setopt(EasyHandle, CURLOPT_POSTFIELDSIZE, RequestPayload->GetContentLength()); #endif bUseReadFunction = true; } else if (Verb == TEXT("PUT") || Verb == TEXT("PATCH")) { curl_easy_setopt(EasyHandle, CURLOPT_UPLOAD, 1L); #if WITH_CURL_XCURL checkf(RequestPayload->GetContentLength() <= TNumericLimits::Max(), TEXT("xCurl 2206.4.0.0 doesn't support uploading file with length more than 32 Bits")); #endif curl_easy_setopt(EasyHandle, CURLOPT_INFILESIZE, RequestPayload->GetContentLength()); if (Verb != TEXT("PUT")) { curl_easy_setopt(EasyHandle, CURLOPT_CUSTOMREQUEST, TCHAR_TO_UTF8(*Verb)); } bUseReadFunction = true; } else if (Verb == TEXT("GET")) { // technically might not be needed unless we reuse the handles curl_easy_setopt(EasyHandle, CURLOPT_HTTPGET, 1L); } else if (Verb == TEXT("HEAD")) { curl_easy_setopt(EasyHandle, CURLOPT_NOBODY, 1L); } else if (Verb == TEXT("DELETE")) { // If we don't pass any other Content-Type, RequestPayload is assumed to be URL-encoded by this time // (if we pass, don't check here and trust the request) check(!GetHeader(TEXT("Content-Type")).IsEmpty() || RequestPayload->IsURLEncoded()); curl_easy_setopt(EasyHandle, CURLOPT_POST, 1L); curl_easy_setopt(EasyHandle, CURLOPT_CUSTOMREQUEST, "DELETE"); curl_easy_setopt(EasyHandle, CURLOPT_POSTFIELDSIZE, RequestPayload->GetContentLength()); bUseReadFunction = true; } else { curl_easy_setopt(EasyHandle, CURLOPT_CUSTOMREQUEST, TCHAR_TO_ANSI(*Verb)); } if (bUseReadFunction) { BytesSent.store(0); TotalBytesSent.store(0); curl_easy_setopt(EasyHandle, CURLOPT_READDATA, this); curl_easy_setopt(EasyHandle, CURLOPT_READFUNCTION, StaticUploadCallback); } // set up header function to receive response headers curl_easy_setopt(EasyHandle, CURLOPT_HEADERDATA, this); curl_easy_setopt(EasyHandle, CURLOPT_HEADERFUNCTION, StaticReceiveResponseHeaderCallback); // set up write function to receive response payload curl_easy_setopt(EasyHandle, CURLOPT_WRITEDATA, this); curl_easy_setopt(EasyHandle, CURLOPT_WRITEFUNCTION, StaticReceiveResponseBodyCallback); // set up headers // Empty string here tells Curl to list all supported encodings, allowing servers to send compressed content. if (FCurlHttpManager::CurlRequestOptions.bAcceptCompressedContent) { #if WITH_CURL_XCURL // xCurl doesn't seem to support passing empty string to enable all supported built-in compressions curl_easy_setopt(EasyHandle, CURLOPT_ACCEPT_ENCODING, "gzip"); #else curl_easy_setopt(EasyHandle, CURLOPT_ACCEPT_ENCODING, ""); #endif } { QUICK_SCOPE_CYCLE_COUNTER(STAT_FCurlHttpRequest_SetupRequest_SLIST_APPEND_HEADERS); TArray AllHeaders = GetAllHeaders(); const int32 NumAllHeaders = AllHeaders.Num(); for (int32 Idx = 0; Idx < NumAllHeaders; ++Idx) { const bool bCanLogHeaderValue = !AllHeaders[Idx].Contains(TEXT("Authorization")); if (bCanLogHeaderValue) { UE_LOG(LogHttp, Verbose, TEXT("%p: Adding header '%s'"), this, *AllHeaders[Idx]); } curl_slist* NewHeaderList = curl_slist_append(HeaderList, TCHAR_TO_UTF8(*AllHeaders[Idx])); if (!NewHeaderList) { if (bCanLogHeaderValue) { UE_LOG(LogHttp, Warning, TEXT("Failed to append header '%s'"), *AllHeaders[Idx]); } else { UE_LOG(LogHttp, Warning, TEXT("Failed to append header 'Authorization'")); } } else { HeaderList = NewHeaderList; } } } if (HeaderList) { curl_easy_setopt(EasyHandle, CURLOPT_HTTPHEADER, HeaderList); } // Set connection timeout in seconds int32 HttpConnectionTimeout = FHttpModule::Get().GetHttpConnectionTimeout(); check(HttpConnectionTimeout > 0); curl_easy_setopt(EasyHandle, CURLOPT_CONNECTTIMEOUT, HttpConnectionTimeout); if (FCurlHttpManager::CurlRequestOptions.bAllowSeekFunction && bIsRequestPayloadSeekable) { curl_easy_setopt(EasyHandle, CURLOPT_SEEKDATA, this); curl_easy_setopt(EasyHandle, CURLOPT_SEEKFUNCTION, StaticSeekCallback); } } #if !WITH_CURL_XCURL { //Tracking the locking in the CURLOPT_SHARE branch of the curl_easy_setopt implementation QUICK_SCOPE_CYCLE_COUNTER(STAT_FCurlHttpRequest_SetupRequest_EASY_CURLOPT_SHARE); curl_easy_setopt(EasyHandle, CURLOPT_SHARE, FCurlHttpManager::GShareHandle); } #endif #if WITH_CURL_QUICKEXIT // Avoid hanging and waiting for threaded resolver curl_easy_setopt(EasyHandle, CURLOPT_QUICK_EXIT, 1L); #endif UE_LOG(LogHttp, Log, TEXT("%p: Starting %s request to URL='%s'"), this, *Verb, *GetURL()); return true; } void FCurlHttpRequest::SetupOptionUnixSocketPath() { #if UE_HTTP_SUPPORT_UNIX_SOCKET FString UnixSocketPath = GetOption(HttpRequestOptions::UnixSocketPath); if (UnixSocketPath.Len() > 0) { curl_easy_setopt(EasyHandle, CURLOPT_UNIX_SOCKET_PATH, TCHAR_TO_ANSI(*UnixSocketPath)); } #endif //UE_HTTP_SUPPORT_UNIX_SOCKET } void FCurlHttpRequest::SetupOptionHttpVersion() { const FString HttpVersion = GetOption(HttpRequestOptions::HttpVersion); if (!HttpVersion.IsEmpty()) { if (HttpVersion == FHttpConstants::VERSION_2TLS) { curl_easy_setopt(EasyHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS); } else if (HttpVersion == FHttpConstants::VERSION_1_1) { curl_easy_setopt(EasyHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); } else { UE_LOG(LogHttp, Warning, TEXT("Unsupported http version %s"), *HttpVersion); } } } void FCurlHttpRequest::CleanupRequestHttpThread() { curl_easy_setopt(EasyHandle, CURLOPT_SHARE, nullptr); } bool FCurlHttpRequest::ProcessRequest() { QUICK_SCOPE_CYCLE_COUNTER(STAT_FCurlHttpRequest_ProcessRequest); check(EasyHandle); // Clear out response. If this is a re-used request, Response could point to a stale response until SetupRequestHttpThread is called ResponseCommon = nullptr; LastReportedBytesRead = 0; if (!PreProcess()) { return false; } QUICK_SCOPE_CYCLE_COUNTER(STAT_CurlHttpAddThreadedRequest); // Mark as in-flight to prevent overlapped requests using the same object SetStatus(EHttpRequestStatus::Processing); // Add to global list while being processed so that the ref counted request does not get deleted FHttpModule::Get().GetHttpManager().AddThreadedRequest(SharedThis(this)); #if WITH_CURL_XCURL StartProcessTime = FPlatformTime::Seconds(); #endif UE_LOG(LogHttp, Verbose, TEXT("%p: request (easy handle:%p) has been added to threaded queue for processing"), this, EasyHandle); return true; } void FCurlHttpRequest::ClearInCaseOfRetry() { FHttpRequestCommon::ClearInCaseOfRetry(); // Clear out response. If this is a re-used request, Response could point to a stale response until SetupRequestHttpThread is called LastReportedBytesRead = 0; TotalBytesRead = 0; bAnyHttpActivity = false; // Clear the info cache log so we don't output messages from previous requests when reusing/retrying a request { const FScopeLock CacheLock(&InfoMessageCacheCriticalSection); for (FString& Line : InfoMessageCache) { Line.Reset(); } } } bool FCurlHttpRequest::StartThreadedRequest() { // reset timeout ElapsedTime = 0.0f; UE_LOG(LogHttp, Verbose, TEXT("%p: request (easy handle:%p) has started threaded processing"), this, EasyHandle); return true; } bool FCurlHttpRequest::IsThreadedRequestComplete() { if (bCurlRequestCompleted && ElapsedTime >= FHttpModule::Get().GetHttpDelayTime()) { return true; } if (CurlAddToMultiResult != CURLM_OK) { return true; } return false; } void FCurlHttpRequest::TickThreadedRequest(float DeltaSeconds) { ElapsedTime += DeltaSeconds; } void FCurlHttpRequest::AbortRequest() { if (bCurlRequestCompleted) { return; } FHttpManager& HttpManager = FHttpModule::Get().GetHttpManager(); if (HttpManager.IsValidRequest(this)) { HttpManager.CancelThreadedRequest(SharedThis(this)); } else { FinishRequestNotInHttpManager(); } } void FCurlHttpRequest::Tick(float DeltaSeconds) { if (DelegateThreadPolicy == EHttpRequestDelegateThreadPolicy::CompleteOnGameThread) { CheckProgressDelegate(); BroadcastNewlyReceivedHeaders(); } } void FCurlHttpRequest::CheckProgressDelegate() { QUICK_SCOPE_CYCLE_COUNTER(STAT_FCurlHttpRequest_CheckProgressDelegate); const uint64 CurrentBytesRead = TotalBytesRead.load(); const uint64 CurrentBytesSent = BytesSent.load(); const bool bProcessing = CompletionStatus == EHttpRequestStatus::Processing; const bool bBytesSentChanged = (CurrentBytesSent != LastReportedBytesSent); const bool bBytesReceivedChanged = CurrentBytesRead != LastReportedBytesRead; const bool bProgressChanged = bBytesSentChanged || bBytesReceivedChanged; if (bProcessing && bProgressChanged) { LastReportedBytesSent = CurrentBytesSent; LastReportedBytesRead = CurrentBytesRead; // Update response progress OnRequestProgress64().ExecuteIfBound(SharedThis(this), LastReportedBytesSent, LastReportedBytesRead); } } void FCurlHttpRequest::BroadcastNewlyReceivedHeaders() { // Process the headers received on the HTTP thread and merge them into the response's list of headers and then broadcast the new headers TPair NewHeader; while (NewlyReceivedHeaders.Dequeue(NewHeader)) { OnHeaderReceived().ExecuteIfBound(SharedThis(this), NewHeader.Key, NewHeader.Value); } } void FCurlHttpRequest::MarkAsCompleted(CURLcode InCurlCompletionResult) { CurlCompletionResult = InCurlCompletionResult; bCurlRequestCompleted = true; if (TSharedPtr Response = StaticCastSharedPtr(ResponseCommon)) { // get the information long HttpCode = 0; if (CURLE_OK == curl_easy_getinfo(EasyHandle, CURLINFO_RESPONSE_CODE, &HttpCode)) { Response->SetResponseCode(HttpCode); } } StopActivityTimeoutTimer(); } void FCurlHttpRequest::FinishRequest() { QUICK_SCOPE_CYCLE_COUNTER(STAT_FCurlHttpRequest_FinishRequest); PostProcess(); CheckProgressDelegate(); TSharedPtr Response = StaticCastSharedPtr(ResponseCommon); // if completed, get more info if (bCurlRequestCompleted) { if (Response.IsValid()) { Response->bSucceeded = (CURLE_OK == CurlCompletionResult); // If content length wasn't received through response header if (Response->ContentLength == 0) { Response->ContentLength = TotalBytesRead; } if (Response->GetResponseCode() <= 0 && GetURL().StartsWith(TEXT("Http"), ESearchCase::IgnoreCase)) { UE_LOG(LogHttp, Warning, TEXT("%p: invalid HTTP response code received. URL: %s, HTTP code: %d, content length: %llu, actual payload size: %llu"), this, *GetURL(), Response->GetResponseCode(), Response->ContentLength, TotalBytesRead.load()); Response->bSucceeded = false; } } } // if just finished, mark as stopped async processing if (Response.IsValid()) { // Broadcast any headers we haven't broadcast yet // If using EHttpRequestDelegateThreadPolicy::CompleteOnHttpThread, we don't add to NewlyReceivedHeaders and will have already broadcast if (DelegateThreadPolicy == EHttpRequestDelegateThreadPolicy::CompleteOnGameThread) { BroadcastNewlyReceivedHeaders(); } Response->bIsReady = true; } bool bSucceeded = Response.IsValid() && Response->bSucceeded; if (!bSucceeded) { if (CurlAddToMultiResult != CURLM_OK) { UE_LOG(LogHttp, Warning, TEXT("%p: request failed, libcurl multi error: %d (%s)"), this, (int32)CurlAddToMultiResult, ANSI_TO_TCHAR(curl_multi_strerror(CurlAddToMultiResult))); } else if (CurlCompletionResult != CURLE_OK) { UE_LOG(LogHttp, Warning, TEXT("%p: request failed, libcurl error: %d (%s)"), this, (int32)CurlCompletionResult, ANSI_TO_TCHAR(curl_easy_strerror(CurlCompletionResult))); } const bool bAborted = (bCanceled || bTimedOut || bActivityTimedOut); if (!bAborted) { const FScopeLock CacheLock(&InfoMessageCacheCriticalSection); for (int32 i = 0; i < InfoMessageCache.Num(); ++i) { if (InfoMessageCache[(LeastRecentlyCachedInfoMessageIndex + i) % InfoMessageCache.Num()].Len() > 0) { UE_LOG(LogHttp, Warning, TEXT("%p: libcurl info message cache %d (%s)"), this, (LeastRecentlyCachedInfoMessageIndex + i) % InfoMessageCache.Num(), *(InfoMessageCache[(LeastRecentlyCachedInfoMessageIndex + i) % NumberOfInfoMessagesToCache])); } } } if (bCurlRequestCompleted) { switch (CurlCompletionResult) { case CURLE_COULDNT_CONNECT: case CURLE_OPERATION_TIMEDOUT: case CURLE_COULDNT_RESOLVE_PROXY: case CURLE_COULDNT_RESOLVE_HOST: case CURLE_SSL_CONNECT_ERROR: #if WITH_CURL_XCURL case CURLE_SEND_ERROR: #endif // report these as connection errors (safe to retry) SetFailureReason(EHttpFailureReason::ConnectionError); break; default: break; } } } OnFinishRequest(bSucceeded); } void FCurlHttpRequest::CleanupRequest() { CloseRequestPayloadDefaultImpl(); } FHttpResponsePtr FCurlHttpRequest::CreateResponse() { return MakeShared(*this); } void FCurlHttpRequest::MockResponseData() { CurlCompletionResult = CURLE_OK; bCurlRequestCompleted = true; TSharedPtr Response = StaticCastSharedPtr(ResponseCommon); Response->bSucceeded = true; } // FCurlHttpRequest FCurlHttpResponse::FCurlHttpResponse(const FCurlHttpRequest& InRequest) : FHttpResponseCommon(InRequest) , ContentLength(0) , bIsReady(0) , bSucceeded(0) { } FString FCurlHttpResponse::GetHeader(const FString& HeaderName) const { FString Result; if (!bIsReady) { UE_LOG(LogHttp, Warning, TEXT("Can't get cached header [%s]. Response still processing. %s"), *HeaderName, *GetURL()); } else { const FString* Header = Headers.Find(HeaderName); if (Header != NULL) { Result = *Header; } } return Result; } TArray FCurlHttpResponse::GetAllHeaders() const { TArray Result; if (!bIsReady) { UE_LOG(LogHttp, Warning, TEXT("Can't get cached headers. Response still processing. %s"), *GetURL()); } else { Result.Reserve(Headers.Num()); for (const TPair& It : Headers) { Result.Emplace(FCurlHttpRequest::CombineHeaderKeyValue(It.Key, It.Value)); } } return Result; } FString FCurlHttpResponse::GetContentType() const { return GetHeader(TEXT("Content-Type")); } uint64 FCurlHttpResponse::GetContentLength() const { return ContentLength; } const TArray& FCurlHttpResponse::GetContent() const { if (!bIsReady) { UE_LOG(LogHttp, Warning, TEXT("Payload is incomplete. Response still processing. %s"), *GetURL()); } return Payload; } FString FCurlHttpResponse::GetContentAsString() const { // Content is NOT null-terminated; we need to specify lengths here FUTF8ToTCHAR TCHARData(reinterpret_cast(Payload.GetData()), Payload.Num()); return FString(TCHARData.Length(), TCHARData.Get()); } #endif //WITH_CURL