// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. #include "HttpPrivatePCH.h" #include "HttpWinInet.h" bool FWinInetConnection::bStaticConnectionInitialized = false; /** * Translates an error returned from GetLastError returned from a WinInet API call. * * @param GetLastErrorResult - error code to translate * @return string for the error code */ FString InternetTranslateError(::DWORD GetLastErrorResult) { FString ErrorStr = FString::Printf(TEXT("ErrorCode: %08X. "), (uint32)GetLastErrorResult); HANDLE ProcessHeap = GetProcessHeap(); if (ProcessHeap == NULL) { ErrorStr += TEXT("Call to GetProcessHeap() failed, cannot translate error... "); return ErrorStr; } TCHAR FormatBuffer[1024]; uint32 BaseLength = FormatMessageW( FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_FROM_HMODULE, GetModuleHandle(TEXT("wininet.dll")), GetLastErrorResult, 0, FormatBuffer, ARRAYSIZE(FormatBuffer), NULL); if (!BaseLength) { ErrorStr += FString::Printf(TEXT("Call to FormatMessage() failed: %08X. "), (uint32)GetLastError()); return ErrorStr; } ErrorStr += FString::Printf(TEXT("Desc: %s. "), FormatBuffer); if (GetLastErrorResult == ERROR_INTERNET_EXTENDED_ERROR) { ::DWORD InetError; ::DWORD ExtLength = 0; InternetGetLastResponseInfo(&InetError, NULL, &ExtLength); ExtLength = ExtLength+1; TArray ExtErrMsg; ExtErrMsg.AddUninitialized(ExtLength); if (!InternetGetLastResponseInfo(&InetError, ExtErrMsg.GetData(), &ExtLength)) { ErrorStr += FString::Printf(TEXT("Call to InternetGetLastResponseInfo() failed: %08X. "), (uint32) GetLastError()); return ErrorStr; } } return ErrorStr; } // override for debug logging #define DEBUG_LOG_HTTP(bIsDebug, Verbosity, Format, ...) \ if (bIsDebug) \ { \ UE_LOG(LogHttp, Log, Format, ##__VA_ARGS__); \ } \ else \ { \ UE_LOG(LogHttp, Verbosity, Format, ##__VA_ARGS__); \ } /** * Global callback for WinInet API. Will use the dwInternetStatus and dwContext * fields to route the results back to the appropriate class instance if necessary. * * @param hInternet - See WinInet API * @param dwContext - See WinInet API * @param dwInternetStatus - See WinInet API * @param lpvStatusInformation - See WinInet API * @param dwStatusInformationLength - See WinInet API */ void CALLBACK InternetStatusCallbackWinInet( HINTERNET hInternet, DWORD_PTR dwContext, ::DWORD dwInternetStatus, LPVOID lpvStatusInformation, ::DWORD dwStatusInformationLength) { FScopeLock ScopeLock(&FHttpManager::RequestLock); // Ignore callbacks on module shutdown if ( !FWinInetConnection::bStaticConnectionInitialized ) { return; } check(FWinInetConnection::Get().GetHttpManager() != NULL); uint32 Error = GetLastError(); if (Error != 0 && Error != ERROR_HTTP_HEADER_NOT_FOUND && Error != ERROR_IO_PENDING) { // Connection has been closed an any further processing is using an invalid handle if (Error == ERROR_INVALID_HANDLE) { return; } } // Original request/response that kicked off the connection attempt FHttpResponseWinInet* Response = NULL; FHttpRequestWinInet* Request = (FHttpRequestWinInet*)dwContext; // Verify request is still valid if (Request != NULL) { if (!FWinInetConnection::Get().GetHttpManager()->IsValidRequest(Request)) { // Invalid since not in the active request list Request = NULL; UE_LOG(LogHttp, Warning, TEXT("InternetStatusCallbackWinInet: on invalid request %p. "), dwContext); } } // Response is always valid while there is a request in flight if (Request != NULL) { Response = Request->Response.Get(); } bool bDebugLog = Request != NULL && Request->bDebugVerbose; switch (dwInternetStatus) { case INTERNET_STATUS_PREFETCH: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("STATUS_PREFETCH: %p"), dwContext); break; case INTERNET_STATUS_USER_INPUT_REQUIRED: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("USER_INPUT_REQUIRED: %p"), dwContext); break; case INTERNET_STATUS_DETECTING_PROXY: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("DETECTING_PROXY: %p"), dwContext); break; case INTERNET_STATUS_CLOSING_CONNECTION: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("CLOSING_CONNECTION: %p"), dwContext); break; case INTERNET_STATUS_CONNECTED_TO_SERVER: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("CONNECTED_TO_SERVER: %p"), dwContext); break; case INTERNET_STATUS_CONNECTING_TO_SERVER: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("CONNECTING_TO_SERVER: %p"), dwContext); break; case INTERNET_STATUS_CONNECTION_CLOSED: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("CONNECTION_CLOSED: %p"), dwContext); break; case INTERNET_STATUS_HANDLE_CLOSING: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("HANDLE_CLOSING: %p"), dwContext); break; case INTERNET_STATUS_HANDLE_CREATED: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("HANDLE_CREATED: %p"), dwContext); break; case INTERNET_STATUS_INTERMEDIATE_RESPONSE: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("INTERMEDIATE_RESPONSE: %p"), dwContext); break; case INTERNET_STATUS_NAME_RESOLVED: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("NAME_RESOLVED: %p"), dwContext); break; case INTERNET_STATUS_RECEIVING_RESPONSE: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("RECEIVING_RESPONSE: %p"), dwContext); break; case INTERNET_STATUS_RESPONSE_RECEIVED: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("RESPONSE_RECEIVED (%d bytes): %p"), *(uint32*)lpvStatusInformation, dwContext); break; case INTERNET_STATUS_REDIRECT: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("STATUS_REDIRECT: %p"), dwContext); break; case INTERNET_STATUS_REQUEST_COMPLETE: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("REQUEST_COMPLETE: %p"), dwContext); if (lpvStatusInformation != NULL) { const INTERNET_ASYNC_RESULT* AsyncResult = (const INTERNET_ASYNC_RESULT*)lpvStatusInformation; // callback failed so can't process response if (AsyncResult->dwResult == 0) { DEBUG_LOG_HTTP(bDebugLog, Log, TEXT("InternetStatusCallbackWinInet request=%p AsyncResult.dwError: %08X. "), dwContext, AsyncResult->dwError, *InternetTranslateError(AsyncResult->dwError)); if (Response != NULL) { // Done processing response due to error FPlatformAtomics::InterlockedExchange(&Response->bIsReady, 1); } Response = NULL; } } if (Request != NULL && Request->GetStatus() == EHttpRequestStatus::Processing && Response != NULL && !Response->bIsReady) { Response->ProcessResponse(); } break; case INTERNET_STATUS_REQUEST_SENT: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("REQUEST_SENT: %p"), dwContext); break; case INTERNET_STATUS_RESOLVING_NAME: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("RESOLVING_NAME: %p"), dwContext); break; case INTERNET_STATUS_SENDING_REQUEST: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("SENDING_REQUEST: %p"), dwContext); break; case INTERNET_STATUS_STATE_CHANGE: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("STATE_CHANGE: %p"), dwContext); break; case INTERNET_STATUS_COOKIE_SENT: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("COOKIE_SENT: %p"), dwContext); break; case INTERNET_STATUS_COOKIE_RECEIVED: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("COOKIE_RECEIVED: %p"), dwContext); break; case INTERNET_STATUS_PRIVACY_IMPACTED: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("PRIVACY_IMPACTED: %p"), dwContext); break; case INTERNET_STATUS_P3P_HEADER: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("P3P_HEADER: %p"), dwContext); break; case INTERNET_STATUS_P3P_POLICYREF: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("P3P_POLICYREF: %p"), dwContext); break; case INTERNET_STATUS_COOKIE_HISTORY: { const InternetCookieHistory* CookieHistory = (const InternetCookieHistory*)lpvStatusInformation; DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("COOKIE_HISTORY: %p. Accepted: %u. Leashed: %u. Downgraded: %u. Rejected: %u.") , dwContext , CookieHistory->fAccepted , CookieHistory->fLeashed , CookieHistory->fDowngraded , CookieHistory->fRejected); } break; default: DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("Unknown Status: %u. %p"), (uint32)dwInternetStatus, dwContext); break; } } bool FWinInetConnection::InitConnection() { // Make sure previous connection is closed ShutdownConnection(); UE_LOG(LogHttp, Log, TEXT("Initializing WinInet connection")); // Check and log the connected state so we can report early errors. ::DWORD ConnectedFlags; BOOL bConnected = InternetGetConnectedState(&ConnectedFlags, 0); FString ConnectionType; ConnectionType += (ConnectedFlags & INTERNET_CONNECTION_CONFIGURED) ? TEXT("Configured ") : TEXT(""); ConnectionType += (ConnectedFlags & INTERNET_CONNECTION_LAN) ? TEXT("LAN ") : TEXT(""); ConnectionType += (ConnectedFlags & INTERNET_CONNECTION_MODEM) ? TEXT("Modem ") : TEXT(""); ConnectionType += (ConnectedFlags & INTERNET_CONNECTION_MODEM_BUSY) ? TEXT("Modem Busy ") : TEXT(""); ConnectionType += (ConnectedFlags & INTERNET_CONNECTION_OFFLINE) ? TEXT("Offline ") : TEXT(""); ConnectionType += (ConnectedFlags & INTERNET_CONNECTION_PROXY) ? TEXT("Proxy Server ") : TEXT(""); ConnectionType += (ConnectedFlags & INTERNET_RAS_INSTALLED) ? TEXT("RAS Installed ") : TEXT(""); UE_LOG(LogHttp, Log, TEXT("Connected State: %s. Flags: (%s)"), bConnected ? TEXT("Good") : TEXT("Bad"), *ConnectionType); // max simultaneous connections allowed by wininet ::DWORD MaxServerConnections = FHttpModule::Get().GetHttpMaxConnectionsPerServer(); InternetSetOption(NULL, INTERNET_OPTION_MAX_CONNS_PER_SERVER, (LPVOID)&MaxServerConnections, sizeof(::DWORD)); InternetSetOption(NULL, INTERNET_OPTION_MAX_CONNS_PER_1_0_SERVER, (LPVOID)&MaxServerConnections, sizeof(::DWORD)); if (InternetAttemptConnect(0) != ERROR_SUCCESS) { UE_LOG(LogHttp, Warning, TEXT("InternetAttemptConnect failed: %s\n"), *InternetTranslateError(GetLastError())); return false; } // setup net connection InternetHandle = InternetOpen( *FString::Printf(TEXT("game=%s, engine=UE4, version=%d"), FApp::GetGameName(), GEngineNetVersion), INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, INTERNET_FLAG_ASYNC); if (InternetHandle == NULL) { UE_LOG(LogHttp, Warning, TEXT("Failed WinHttpOpen: %s"), *InternetTranslateError(GetLastError())); return false; } { FScopeLock ScopeLock(&FHttpManager::RequestLock); bStaticConnectionInitialized = true; } // Register callback to update based on WinInet connection state InternetSetStatusCallback(InternetHandle, &InternetStatusCallbackWinInet); return true; } bool FWinInetConnection::ShutdownConnection() { UE_LOG(LogHttp, Log, TEXT("Closing internet connection")); { FScopeLock ScopeLock(&FHttpManager::RequestLock); bStaticConnectionInitialized = false; if (InternetHandle != NULL) { // Clear the callback if still set InternetSetStatusCallback(InternetHandle, NULL); // shut down WinINet if (!InternetCloseHandle(InternetHandle)) { UE_LOG(LogHttp, Warning, TEXT("InternetCloseHandle failed on the FHttpRequestWinInet: %s"), *InternetTranslateError(GetLastError())); return false; } InternetHandle = NULL; } } return true; } // FHttpRequestWinInet FHttpRequestWinInet::FHttpRequestWinInet() : CompletionStatus(EHttpRequestStatus::NotStarted) , ConnectionHandle(NULL) , RequestHandle(NULL) , ElapsedTimeSinceLastServerResponse(0) , ProgressBytesSent(0) , StartRequestTime(0) , ElapsedTime(0.0f) , bDebugVerbose(false) { } FHttpRequestWinInet::~FHttpRequestWinInet() { UE_LOG(LogHttp, Verbose, TEXT("Destroying FHttpRequestWinInet %p %p"), this, RequestHandle); CleanupRequest(); } FString FHttpRequestWinInet::GetURL() { return RequestURL.GetURL(); } FString FHttpRequestWinInet::GetURLParameter(const FString& ParameterName) { const FString* Result = RequestURL.GetParameter(ParameterName); return Result != NULL ? *Result : FString(); } FString FHttpRequestWinInet::GetHeader(const FString& HeaderName) { FString* Header = RequestHeaders.Find(HeaderName); return Header != NULL ? *Header : TEXT(""); } TArray FHttpRequestWinInet::GetAllHeaders() { TArray Result; for (TMap::TConstIterator It(RequestHeaders); It; ++It) { Result.Add(It.Key() + TEXT(": ") + It.Value()); } return Result; } FString FHttpRequestWinInet::GetContentType() { return GetHeader(TEXT("Content-Type")); } int32 FHttpRequestWinInet::GetContentLength() { return RequestPayload.Num(); } const TArray& FHttpRequestWinInet::GetContent() { return RequestPayload; } FString FHttpRequestWinInet::GetVerb() { return RequestVerb; } void FHttpRequestWinInet::SetVerb(const FString& Verb) { RequestVerb = Verb; } void FHttpRequestWinInet::SetURL(const FString& URL) { RequestURL = FURLWinInet(URL); } void FHttpRequestWinInet::SetContent(const TArray& ContentPayload) { RequestPayload = ContentPayload; } void FHttpRequestWinInet::SetContentAsString(const FString& ContentString) { FTCHARToUTF8 Converter(*ContentString); RequestPayload.SetNumUninitialized(Converter.Length()); FMemory::Memcpy(RequestPayload.GetData(), (const uint8*)Converter.Get(), RequestPayload.Num()); } void FHttpRequestWinInet::SetHeader(const FString& HeaderName, const FString& HeaderValue) { RequestHeaders.Add(HeaderName, HeaderValue); } bool FHttpRequestWinInet::ProcessRequest() { bool bStarted = false; // Disabled http request processing if (!FHttpModule::Get().IsHttpEnabled()) { UE_LOG(LogHttp, Verbose, TEXT("Http disabled. Skipping request. url=%s"), *GetURL()); } // Prevent overlapped requests using the same instance else if (CompletionStatus == EHttpRequestStatus::Processing) { UE_LOG(LogHttp, Warning, TEXT("ProcessRequest failed. Still processing last request.")); } // Make sure Internet connection has been setup else if (!FWinInetConnection::Get().IsConnectionValid() && !FWinInetConnection::Get().InitConnection()) { UE_LOG(LogHttp, Warning, TEXT("ProcessRequest failed. Could not initialize Internet connection.")); } // Nothing to do without a valid URL else if (RequestURL.GetURL().IsEmpty()) { UE_LOG(LogHttp, Warning, TEXT("ProcessRequest failed. No URL was specified.")); } // Make sure the URL is parsed correctly with a valid HTTP scheme else if (RequestURL.GetURLComponents().nScheme != INTERNET_SCHEME_HTTP && RequestURL.GetURLComponents().nScheme != INTERNET_SCHEME_HTTPS) { UE_LOG(LogHttp, Warning, TEXT("ProcessRequest failed. URL '%s' is not a valid HTTP request. %p"), *RequestURL.GetURL(), this); } else { // Mark as in-flight to prevent overlapped requests using the same object CompletionStatus = EHttpRequestStatus::Processing; // Response object to handle data that comes back after starting this request Response = MakeShareable(new FHttpResponseWinInet(*this)); // Add to global list while being processed so that the ref counted request does not get deleted FHttpModule::Get().GetHttpManager().AddRequest(SharedThis(this)); // keep track of time when request was started StartRequestTime = FPlatformTime::Seconds(); // reset elapsed time. ElapsedTime = 0.0f; // Try to start the connection and send the Http request bStarted = StartRequest(); } if (!bStarted) { // No response since connection failed Response = NULL; // Cleanup and call delegate FinishedRequest(); } // Successfully started the request return bStarted; } bool FHttpRequestWinInet::StartRequest() { // Make sure old handles are not being reused CleanupRequest(); UE_LOG(LogHttp, Log, TEXT("Start request. %p %s url=%s"), this, *GetVerb(), *GetURL()); if (UE_LOG_ACTIVE(LogHttp, Verbose)) { for (TMap::TConstIterator It(RequestHeaders); It; ++It) { if (!It.Key().Contains(TEXT("Authorization"))) { UE_LOG(LogHttp, Verbose, TEXT("%p Header %s : %s"), this, *It.Key(), *It.Value()); } } } if (FWinInetConnection::Get().IsConnectionValid()) { // Open an internet connection to the URL endpoint ConnectionHandle = InternetConnect( FWinInetConnection::Get().InternetHandle, *RequestURL.GetHost(), RequestURL.GetPort(), NULL, NULL, INTERNET_SERVICE_HTTP, 0, (DWORD_PTR)this); } if (ConnectionHandle == NULL) { UE_LOG(LogHttp, Warning, TEXT("InternetConnect failed: %s"), *InternetTranslateError(GetLastError())); return false; } // Disable IE offline mode ::BOOL bEnabled = true; InternetSetOption(ConnectionHandle, INTERNET_OPTION_IGNORE_OFFLINE, &bEnabled, sizeof(::BOOL)); // Set connection timeout in ms if (FHttpModule::Get().GetHttpConnectionTimeout() >= 0) { uint32 HttpConnectionTimeout = FHttpModule::Get().GetHttpConnectionTimeout() == 0 ? 0xFFFFFFFF : FHttpModule::Get().GetHttpConnectionTimeout() * 1000; InternetSetOption(ConnectionHandle, INTERNET_OPTION_CONNECT_TIMEOUT, (::LPVOID)&HttpConnectionTimeout, sizeof(::DWORD)); } // Set receive timeout in ms if (FHttpModule::Get().GetHttpReceiveTimeout() >= 0) { uint32 HttpReceiveTimeout = FHttpModule::Get().GetHttpReceiveTimeout() * 1000; InternetSetOption(ConnectionHandle, INTERNET_OPTION_RECEIVE_TIMEOUT, (::LPVOID)&HttpReceiveTimeout, sizeof(::DWORD)); } // Set send timeout in ms if (FHttpModule::Get().GetHttpSendTimeout() >= 0) { uint32 HttpSendTimeout = FHttpModule::Get().GetHttpSendTimeout() * 1000; InternetSetOption(ConnectionHandle, INTERNET_OPTION_SEND_TIMEOUT, (::LPVOID)&HttpSendTimeout, sizeof(::DWORD)); } // Query these options to verify { ::DWORD OptionSize = sizeof(::DWORD); ::DWORD OptionData = 0; InternetQueryOption(ConnectionHandle, INTERNET_OPTION_CONNECT_TIMEOUT, (::LPVOID)&OptionData, (::LPDWORD)&OptionSize); UE_LOG(LogHttp, VeryVerbose, TEXT("INTERNET_OPTION_CONNECT_TIMEOUT: %d"), OptionData); InternetQueryOption(ConnectionHandle, INTERNET_OPTION_RECEIVE_TIMEOUT, (::LPVOID)&OptionData, (::LPDWORD)&OptionSize); UE_LOG(LogHttp, VeryVerbose, TEXT("INTERNET_OPTION_RECEIVE_TIMEOUT: %d"), OptionData); InternetQueryOption(ConnectionHandle, INTERNET_OPTION_SEND_TIMEOUT, (::LPVOID)&OptionData, (::LPDWORD)&OptionSize); UE_LOG(LogHttp, VeryVerbose, TEXT("INTERNET_OPTION_SEND_TIMEOUT: %d"), OptionData); } // Only custom request flag is for SSL/HTTPS requests uint32 RequestFlags = RequestURL.GetURLComponents().nScheme == INTERNET_SCHEME_HTTPS ? INTERNET_FLAG_SECURE : 0; // Always download from server instead of cache // Forces the request to be resolved by the origin server, even if a cached copy exists on the proxy. RequestFlags |= INTERNET_FLAG_RELOAD | INTERNET_FLAG_NO_CACHE_WRITE | INTERNET_FLAG_PRAGMA_NOCACHE; // Keep the connection open RequestFlags |= INTERNET_FLAG_KEEP_CONNECTION; // Build full path FString PathAndExtra(RequestURL.GetPath() + RequestURL.GetExtraInfo()); // Create the request RequestHandle = HttpOpenRequest( ConnectionHandle, RequestVerb.IsEmpty() ? NULL : *RequestVerb, *PathAndExtra, NULL, NULL, NULL, RequestFlags, (DWORD_PTR)this); if (RequestHandle == NULL) { UE_LOG(LogHttp, Warning, TEXT("HttpOpenRequest failed: %s"), *InternetTranslateError(GetLastError())); return false; } // Disable certificate checks ::DWORD SecurityFlags = SECURITY_FLAG_IGNORE_REVOCATION; if (!InternetSetOption(RequestHandle, INTERNET_OPTION_SECURITY_FLAGS, (LPVOID)&SecurityFlags, sizeof(::DWORD))) { UE_LOG(LogHttp, Warning, TEXT("InternetSetOption failed: %s"), *InternetTranslateError(GetLastError())); } // Send the request with the payload if any FString Headers = GenerateHeaderBuffer(RequestPayload.Num()); BOOL bSentRequest = HttpSendRequest( RequestHandle, *Headers, Headers.Len(), RequestPayload.Num() > 0 ? RequestPayload.GetData() : NULL, RequestPayload.Num()); if (!bSentRequest && GetLastError() != ERROR_IO_PENDING) { UE_LOG(LogHttp, Warning, TEXT("HttpSendRequest failed: %s"), *InternetTranslateError(GetLastError())); return false; } // Successfully started the request return true; } void FHttpRequestWinInet::FinishedRequest() { // Clean up session/request handles that may have been created CleanupRequest(); TSharedRef Request = SharedThis(this); // Remove from global list since processing is now complete FHttpModule::Get().GetHttpManager().RemoveRequest(Request); ElapsedTime = (float)(FPlatformTime::Seconds() - StartRequestTime); if (Response.IsValid() && Response->bResponseSucceeded) { UE_LOG(LogHttp, Log, TEXT("Finished request %p. response=%d %s url=%s elapsed=%.3f DownloadSize=%d"), this, Response->GetResponseCode(), *GetVerb(), *GetURL(), ElapsedTime, Response->GetContentLength()); const bool bDebugServerResponse = Response->GetResponseCode() >= 500 && Response->GetResponseCode() <= 505; // log info about cloud front to identify failed downloads if (UE_LOG_ACTIVE(LogHttp, Verbose) || bDebugServerResponse) { TArray AllHeaders = Response->GetAllHeaders(); for (TArray::TConstIterator It(AllHeaders); It; ++It) { const FString& HeaderStr = *It; if (!HeaderStr.Contains(TEXT("Authorization"))) { if (bDebugServerResponse) { UE_LOG(LogHttp, Warning, TEXT("%p Response Header %s"), this, *HeaderStr); } else { UE_LOG(LogHttp, Verbose, TEXT("%p Response Header %s"), this, *HeaderStr); } } } } // Mark last request attempt as completed successfully CompletionStatus = EHttpRequestStatus::Succeeded; // Call delegate with valid request/response objects OnProcessRequestComplete().ExecuteIfBound(Request,Response,true); } else { UE_LOG(LogHttp, Log, TEXT("Finished request %p. no response %s url=%s elapsed=%.3f"), this, *GetVerb(), *GetURL(), ElapsedTime); // Mark last request attempt as completed but failed CompletionStatus = EHttpRequestStatus::Failed; // No response since connection failed Response = NULL; // Call delegate with failure OnProcessRequestComplete().ExecuteIfBound(Request,NULL,false); } } void FHttpRequestWinInet::CleanupRequest() { if (RequestHandle != NULL) { InternetCloseHandle(RequestHandle); RequestHandle = NULL; } if (ConnectionHandle != NULL) { InternetCloseHandle(ConnectionHandle); ConnectionHandle = NULL; } } FString FHttpRequestWinInet::GenerateHeaderBuffer(uint32 ContentLength) { FString Result; for (TMap::TConstIterator It(RequestHeaders); It; ++It) { Result += It.Key() + TEXT(": ") + It.Value() + TEXT("\r\n"); } if (ContentLength > 0) { Result += FString(TEXT("Content-Length: ")) + FString::FromInt(ContentLength) + TEXT("\r\n"); } return Result; } void FHttpRequestWinInet::ResetRequestTimeout() { FPlatformAtomics::InterlockedExchange(&ElapsedTimeSinceLastServerResponse, 0); } FHttpRequestCompleteDelegate& FHttpRequestWinInet::OnProcessRequestComplete() { return RequestCompleteDelegate; } FHttpRequestProgressDelegate& FHttpRequestWinInet::OnRequestProgress() { return RequestProgressDelegate; } void FHttpRequestWinInet::CancelRequest() { UE_LOG(LogHttp, Log, TEXT("Canceling Http request. %p url=%s"), this, *GetURL()); // force finish/cleanup of request // note: will still call completion delegates FinishedRequest(); } EHttpRequestStatus::Type FHttpRequestWinInet::GetStatus() { return CompletionStatus; } const FHttpResponsePtr FHttpRequestWinInet::GetResponse() const { return Response; } void FHttpRequestWinInet::Tick(float DeltaSeconds) { // keep track of elapsed milliseconds const int32 ElapsedMillisecondsThisFrame = DeltaSeconds * 1000; FPlatformAtomics::InterlockedAdd(&ElapsedTimeSinceLastServerResponse, ElapsedMillisecondsThisFrame); // Update response progress if(Response.IsValid()) { int32 ResponseBytes = Response->ProgressBytesRead.GetValue(); if(ResponseBytes > ProgressBytesSent) { ProgressBytesSent = ResponseBytes; OnRequestProgress().ExecuteIfBound(SharedThis(this), RequestPayload.Num(), ResponseBytes); } } // Convert to seconds for comparison to the timeout value const float HttpTimeout = FHttpModule::Get().GetHttpTimeout(); // Log verbose once total elapsed time surpasses HttpTimeout const double TotalElapsed = FPlatformTime::Seconds() - StartRequestTime; if (HttpTimeout > 0 && TotalElapsed > HttpTimeout && !bDebugVerbose) { UE_LOG(LogHttp, Warning, TEXT("Http request taking too long! Elapsed %.3f. Enabling verbose logs %p url=%s"), TotalElapsed, this, *GetURL()); bDebugVerbose = true; } // Timeout if no server response for HttpTimeout const float SecondsSinceLastResponse = ElapsedTimeSinceLastServerResponse / 1000.f; if (HttpTimeout > 0 && SecondsSinceLastResponse >= HttpTimeout) { UE_LOG(LogHttp, Warning, TEXT("Timeout processing Http request. %p url=%s"), this, *GetURL()); // finish it off since it is timeout FinishedRequest(); } // No longer waiting for a response and done processing it else if (CompletionStatus == EHttpRequestStatus::Processing && Response.IsValid() && Response->bIsReady && TotalElapsed >= FHttpModule::Get().GetHttpDelayTime()) { FinishedRequest(); } } float FHttpRequestWinInet::GetElapsedTime() { return ElapsedTime; } // FHttpResponseWinInet FHttpResponseWinInet::FHttpResponseWinInet(FHttpRequestWinInet& InRequest) : Request(InRequest) , RequestURL(InRequest.RequestURL) , AsyncBytesRead(0) , TotalBytesRead(0) , ResponseCode(EHttpResponseCodes::Unknown) , ContentLength(0) , bIsReady(0) , bResponseSucceeded(0) , MaxReadBufferSize(FHttpModule::Get().GetMaxReadBufferSize()) { } FHttpResponseWinInet::~FHttpResponseWinInet() { } FString FHttpResponseWinInet::GetURL() { return RequestURL.GetURL(); } FString FHttpResponseWinInet::GetURLParameter(const FString& ParameterName) { const FString* Result = RequestURL.GetParameter(ParameterName); return Result != NULL ? *Result : FString(); } FString FHttpResponseWinInet::GetHeader(const FString& HeaderName) { FString Result(TEXT("")); if (!bIsReady) { UE_LOG(LogHttp, Warning, TEXT("Can't get cached header [%s]. Response still processing. %p"), *HeaderName, &Request); } else { FString* Header = ResponseHeaders.Find(HeaderName); if (Header != NULL) { return *Header; } } return Result; } TArray FHttpResponseWinInet::GetAllHeaders() { TArray Result; if (!bIsReady) { UE_LOG(LogHttp, Warning, TEXT("Can't get cached headers. Response still processing. %p"),&Request); } else { for (TMap::TConstIterator It(ResponseHeaders); It; ++It) { Result.Add(It.Key() + TEXT(": ") + It.Value()); } } return Result; } FString FHttpResponseWinInet::GetContentType() { return GetHeader(TEXT("Content-Type")); } int32 FHttpResponseWinInet::GetContentLength() { return ContentLength; } const TArray& FHttpResponseWinInet::GetContent() { if (!bIsReady) { UE_LOG(LogHttp, Warning, TEXT("Payload is incomplete. Response still processing. %p"),&Request); } return ResponsePayload; } int32 FHttpResponseWinInet::GetResponseCode() { return ResponseCode; } FString FHttpResponseWinInet::GetContentAsString() { TArray ZeroTerminatedPayload(GetContent()); ZeroTerminatedPayload.Add(0); return UTF8_TO_TCHAR(ZeroTerminatedPayload.GetData()); } void FHttpResponseWinInet::ProcessResponse() { // Keep track of total read from last async callback TotalBytesRead += AsyncBytesRead; // We might be calling back into this from another asynchronous read, so continue where we left off. // if there is no content length, we're probably receiving chunked data. ContentLength = QueryContentLength(); // Set buffer size based on content length or hard code if chunked response int32 BufferSize = 0; // For non-chunked responses, allocate one extra uint8 to check if we are sent extra content if (ContentLength > 0) { if (TotalBytesRead == 0) { BufferSize = ContentLength + 1; // Size read buffer ResponsePayload.SetNum(BufferSize); } } else { // For chunked responses, add data using a fixed size buffer at a time. BufferSize = TotalBytesRead + MaxReadBufferSize; // Size read buffer ResponsePayload.SetNum(BufferSize); } bool bDebugLog = Request.bDebugVerbose; bool bFailed = false; int32 LoopCount = 0; do { const int32 NumBytesToRead = FMath::Min(ResponsePayload.Num() - TotalBytesRead, MaxReadBufferSize); // Read directly into the response payload const BOOL bReadFile = InternetReadFile( Request.RequestHandle, &ResponsePayload[TotalBytesRead], NumBytesToRead, (LPDWORD)&AsyncBytesRead); const int32 ErrorCode = GetLastError(); DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("InternetReadFile result=%d error=%d (%u bytes read) (%u bytes total read) (%u bytes to read). LoopCount=%d %p"), bReadFile, ErrorCode, AsyncBytesRead, TotalBytesRead, NumBytesToRead, LoopCount, &Request); if (!bReadFile) { if (ErrorCode == ERROR_IO_PENDING) { // Chunked responses could cause InternetReadFile to return IO_PENDING, in which // case the largely undocumented behavior is to return and wait for the callback function // to be called again later when you receive another REQUEST_COMPLETE signal. // You are supposed to keep doing this until InternetReadFile returns 0 with a successful return value. DEBUG_LOG_HTTP(bDebugLog, VeryVerbose, TEXT("InternetReadFile is completing asynchronously, so waiting for callback again. %p"), &Request); } else if (ErrorCode != ERROR_SUCCESS) { DEBUG_LOG_HTTP(bDebugLog, Log, TEXT("InternetReadFile failed (%u bytes read). Returning what we've read so far: error=%d %s. %p"), AsyncBytesRead, ErrorCode, *InternetTranslateError(ErrorCode), &Request); } // Allow processing to continue return; } else { // Keep track of total read so far TotalBytesRead += AsyncBytesRead; // resize the buffer if we don't know our content length, otherwise don't let the buffer grow larger than content length. if (TotalBytesRead >= ResponsePayload.Num()) { if (ContentLength > 0) { DEBUG_LOG_HTTP(bDebugLog, Log, TEXT("Response payload (%d bytes read so far) is larger than the content-length (%d). Resizing buffer to accommodate. %p"), TotalBytesRead, ContentLength, &Request); } ResponsePayload.AddZeroed(MaxReadBufferSize); } } LoopCount++; // Update progress bytes ProgressBytesRead.Set( TotalBytesRead ); Request.ResetRequestTimeout(); } while (AsyncBytesRead > 0); if (ContentLength != 0 && TotalBytesRead != ContentLength) { DEBUG_LOG_HTTP(bDebugLog, Warning, TEXT("Response payload was %d bytes, content-length indicated (%d) bytes. %p"), TotalBytesRead, ContentLength, &Request); } DEBUG_LOG_HTTP(bDebugLog, Verbose, TEXT("TotalBytesRead = %d. %p"), TotalBytesRead, &Request); // Shrink array to only the valid data ResponsePayload.SetNum(TotalBytesRead); // Query for header data and cache it ProcessResponseHeaders(); // Query for response code and cache it ProcessResponseCode(); // Cache content length now that response is done ContentLength = QueryContentLength(); // Mark as valid processed response FPlatformAtomics::InterlockedExchange(&bResponseSucceeded, 1); // Done processing FPlatformAtomics::InterlockedExchange(&bIsReady, 1); // Update progress bytes ProgressBytesRead.Set( TotalBytesRead ); } void FHttpResponseWinInet::ProcessResponseHeaders() { ::DWORD HeaderSize = 0; TArray Result; if (!HttpQueryInfo(Request.RequestHandle, HTTP_QUERY_RAW_HEADERS_CRLF, NULL, &HeaderSize, NULL)) { uint32 ErrorCode = GetLastError(); if (ErrorCode != ERROR_INSUFFICIENT_BUFFER) { UE_LOG(LogHttp, Warning, TEXT("HttpQueryInfo to get header length for all headers failed: %s. %p"), *InternetTranslateError(GetLastError()), &Request); } if (HeaderSize == 0) { UE_LOG(LogHttp, Warning, TEXT("HttpQueryInfo for all headers returned zero header size. %p"), this); } TArray HeaderBuffer; HeaderBuffer.AddUninitialized(HeaderSize/sizeof(TCHAR)); if (!HttpQueryInfo(Request.RequestHandle, HTTP_QUERY_RAW_HEADERS_CRLF, HeaderBuffer.GetData(), &HeaderSize, NULL)) { UE_LOG(LogHttp, Warning, TEXT("HttpQueryInfo for all headers failed: %s. %p"), *InternetTranslateError(GetLastError()), &Request); } // parse all the key/value pairs const TCHAR* HeaderPtr = HeaderBuffer.GetData(); // don't count the terminating NULL character as one to search. const TCHAR* EndPtr = HeaderPtr + HeaderBuffer.Num()-1; while (HeaderPtr < EndPtr) { const TCHAR* DelimiterPtr = FCString::Strstr(HeaderPtr, TEXT("\r\n")); if (DelimiterPtr == NULL) { DelimiterPtr = EndPtr; } FString HeaderLine(DelimiterPtr-HeaderPtr, HeaderPtr); FString HeaderKey,HeaderValue; if (HeaderLine.Split(TEXT(":"), &HeaderKey, &HeaderValue, ESearchCase::CaseSensitive)) { if (!HeaderKey.IsEmpty()) { ResponseHeaders.Add(HeaderKey, HeaderValue.Trim()); } } HeaderPtr = DelimiterPtr + 2; } } else { UE_LOG(LogHttp, Warning, TEXT("HttpQueryInfo for all headers failed when trying to determine the size for the header buffer. %p"), &Request); } } void FHttpResponseWinInet::ProcessResponseCode() { // get the response code ResponseCode = EHttpResponseCodes::Unknown; ::DWORD CodeSize = sizeof(ResponseCode); if (!HttpQueryInfo(Request.RequestHandle, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &ResponseCode, &CodeSize, NULL)) { UE_LOG(LogHttp, Warning, TEXT("HttpQueryInfo for response code failed: %s. %p"), *InternetTranslateError(GetLastError()), &Request); } } FString FHttpResponseWinInet::QueryHeaderString(uint32 HttpQueryInfoLevel, const FString& HeaderName) const { // try to use stack allocation where possible. ::DWORD HeaderSize = 0; TCHAR HeaderValue[128]; TArray HeaderValueLong; TCHAR* HeaderValueReal = HeaderValue; if (!HttpQueryInfo(Request.RequestHandle, HttpQueryInfoLevel, const_cast(*HeaderName), &HeaderSize, NULL)) { uint32 ErrorCode = GetLastError(); if (ErrorCode == ERROR_HTTP_HEADER_NOT_FOUND) { return FString(); } else if (ErrorCode == ERROR_INSUFFICIENT_BUFFER) { // make sure we have enough room to supply the HeaderName and Value. If not, dynamically allocate. uint32 HeaderSizeChars = HeaderSize / sizeof(TCHAR) + 1; uint32 HeaderNameChars = HeaderName.Len() == 0 ? 0 : HeaderName.Len() + 1; if (HeaderSizeChars > ARRAYSIZE(HeaderValue) || HeaderNameChars > ARRAYSIZE(HeaderValue)) { // we have to create a dynamic allocation to hold the result. UE_LOG(LogHttp, Verbose, TEXT("Having to resize default buffer for retrieving header %s. Name length: %u. Value length: %u. %p"), *HeaderName, HeaderNameChars, HeaderSizeChars, &Request); uint32 NewBufferSizeChars = FMath::Max(HeaderSizeChars, HeaderNameChars); // Have to copy the HeaderName into the buffer as well for the API to work. if (HeaderName.Len() > 0) { HeaderValueLong = HeaderName.GetCharArray(); } // Set the size of the array to hold the entire value. HeaderValueLong.SetNum(NewBufferSizeChars); HeaderSize = NewBufferSizeChars * sizeof(TCHAR); HeaderValueReal = HeaderValueLong.GetData(); } else { // Use the stack allocated space if we have the room. FMemory::Memcpy(HeaderValue, *HeaderName, HeaderName.Len()*sizeof(TCHAR)); HeaderValue[HeaderName.Len()] = 0; } if (!HttpQueryInfo(Request.RequestHandle, HttpQueryInfoLevel, HeaderValueReal, &HeaderSize, NULL)) { UE_LOG(LogHttp, Warning, TEXT("HttpQueryInfo failed trying to get Header Value for Name %s: %s. %p"), *HeaderName, *InternetTranslateError(GetLastError()), &Request); return FString(); } } } return FString(HeaderValueReal); } int32 FHttpResponseWinInet::QueryContentLength() const { int32 Result = 0; FString ContentLengthStr(QueryHeaderString(HTTP_QUERY_CONTENT_LENGTH, FString())); if (ContentLengthStr.Len() > 0) { Result = FCString::Strtoi(*ContentLengthStr, NULL, 10); } return Result; } // FURLWinInet FURLWinInet::FURLWinInet() { ClearCachedData(); } FURLWinInet::FURLWinInet( const FString& InURL ) :RequestURL(InURL) { ClearCachedData(); } FURLWinInet::FURLWinInet( const FURLWinInet& InURL ) :RequestURL(InURL.RequestURL) { ClearCachedData(); } FURLWinInet& FURLWinInet::operator=( const FURLWinInet& InURL ) { if (this != &InURL) { RequestURL = InURL.RequestURL; ClearCachedData(); } return *this; } const FString& FURLWinInet::GetURL() const { return RequestURL; } FString FURLWinInet::GetHost() const { if (URLPtr == NULL) CrackUrlParameters(); if (URLPtr != NULL) { return FString(URLParts.dwHostNameLength, URLParts.lpszHostName); } return FString(); } WORD FURLWinInet::GetPort() const { if (URLPtr == NULL) CrackUrlParameters(); if (URLPtr != NULL) { return URLParts.nPort; } return 0; } FString FURLWinInet::GetPath() const { if (URLPtr == NULL) CrackUrlParameters(); if (URLPtr != NULL) { return FString(URLParts.dwUrlPathLength, URLParts.lpszUrlPath); } return FString(); } FString FURLWinInet::GetExtraInfo() const { if (URLPtr == NULL) CrackUrlParameters(); if (URLPtr != NULL) { return FString(URLParts.dwExtraInfoLength, URLParts.lpszExtraInfo); } return FString(); } const URL_COMPONENTS& FURLWinInet::GetURLComponents() const { if (URLPtr == NULL) CrackUrlParameters(); return URLParts; } const FString* FURLWinInet::GetParameter( const FString& ParameterName ) const { if (URLPtr == NULL) CrackUrlParameters(); return URLParameters.Find(ParameterName); } void FURLWinInet::ClearCachedData() const { URLPtr = NULL; URLParameters.Reset(); FMemory::Memzero(URLParts); } void FURLWinInet::CrackUrlParameters() const { // used to make sure we can't early exit from this function without cleaning up. struct FClearCachedDataGuard { FClearCachedDataGuard(const FURLWinInet& InURL) : bClearCachedData(true), CachedURL(InURL) {} ~FClearCachedDataGuard() { if (bClearCachedData) { CachedURL.ClearCachedData(); } } bool bClearCachedData; const FURLWinInet& CachedURL; }; FClearCachedDataGuard CachedDataGuard(*this); // don't crack anything if the request is empty if (RequestURL.IsEmpty()) { return; } URLPtr = *RequestURL; // crack open the URL into its component parts URLParts.dwStructSize = sizeof(URLParts); URLParts.dwHostNameLength = 1; URLParts.dwUrlPathLength = 1; URLParts.dwExtraInfoLength = 1; if (!InternetCrackUrl(URLPtr, 0, 0, &URLParts)) { UE_LOG(LogHttp, Warning, TEXT("Failed to crack URL parameters for URL:%s"), *RequestURL); return; } // make sure we didn't fail. FString Result; if (URLParts.dwExtraInfoLength > 1) { if (URLParts.lpszExtraInfo[0] == TEXT('?')) { const TCHAR* ParamPtr = URLParts.lpszExtraInfo+1; const TCHAR* ParamEnd = URLParts.lpszExtraInfo+URLParts.dwExtraInfoLength; while (ParamPtr < ParamEnd) { const TCHAR* DelimiterPtr = FCString::Strchr(ParamPtr, TEXT('&')); if (DelimiterPtr == NULL) { DelimiterPtr = ParamEnd; } const TCHAR* EqualPtr = FCString::Strchr(ParamPtr, TEXT('=')); if (EqualPtr != NULL && EqualPtr < DelimiterPtr) { // This will handle the case of Key=&Key2=Value... by allocating a zero-length string with the math below URLParameters.Add(FString(EqualPtr-ParamPtr, ParamPtr), FString(DelimiterPtr-EqualPtr-1, EqualPtr+1)); } else { URLParameters.Add(FString(DelimiterPtr-ParamPtr, ParamPtr), FString()); } ParamPtr = DelimiterPtr + 1; } } else { UE_LOG(LogHttp, Warning, TEXT("URL '%s' extra info did not start with a '?', so can't parse headers."), *RequestURL); } } CachedDataGuard.bClearCachedData = false; }