From cdedcae441a0b07d2fef5494d713d3b86ee2ebe7 Mon Sep 17 00:00:00 2001 From: Leandro Becker Date: Tue, 27 Aug 2024 12:17:10 -0300 Subject: [PATCH] NetworkPkg/HttpBootDxe: Resume an interrupted boot file download. When the boot file download operation is interrupted for some reason, HttpBootDxe will use HTTP Range header to try resume the download operation reusing the bytes downloaded so far. Signed-off-by: Leandro Gustavo Biss Becker --- NetworkPkg/HttpBootDxe/HttpBootClient.c | 183 +++++++++++++++++++++++- NetworkPkg/HttpBootDxe/HttpBootClient.h | 3 + NetworkPkg/HttpBootDxe/HttpBootDxe.h | 2 + NetworkPkg/HttpBootDxe/HttpBootDxe.inf | 6 +- NetworkPkg/HttpBootDxe/HttpBootImpl.c | 81 +++++++++-- NetworkPkg/NetworkPkg.dec | 10 ++ 6 files changed, 263 insertions(+), 22 deletions(-) diff --git a/NetworkPkg/HttpBootDxe/HttpBootClient.c b/NetworkPkg/HttpBootDxe/HttpBootClient.c index 40f64fcb6bf82..f9c46166512f2 100644 --- a/NetworkPkg/HttpBootDxe/HttpBootClient.c +++ b/NetworkPkg/HttpBootDxe/HttpBootClient.c @@ -923,6 +923,9 @@ HttpBootGetBootFileCallback ( BufferSize has been updated with the size needed to complete the request. @retval EFI_ACCESS_DENIED The server needs to authenticate the client. + @retval EFI_NOT_READY Data transfer has timed-out, call HttpBootGetBootFile again to resume + the download operation using HTTP Range headers. + @retval EFI_UNSUPPORTED Some HTTP response header is not supported. @retval Others Unexpected error happened. **/ @@ -955,6 +958,10 @@ HttpBootGetBootFile ( CHAR8 BaseAuthValue[80]; EFI_HTTP_HEADER *HttpHeader; CHAR8 *Data; + UINTN HeadersCount; + BOOLEAN ResumingOperation; + CHAR8 *ContentRangeResponseValue; + CHAR8 RangeValue[64]; ASSERT (Private != NULL); ASSERT (Private->HttpCreated); @@ -985,6 +992,16 @@ HttpBootGetBootFile ( } } + // Check if this is a previous download that has failed and need to be resumed + if ((!HeaderOnly) && + (Private->PartialTransferredSize > 0) && + (Private->BootFileSize == *BufferSize)) + { + ResumingOperation = TRUE; + } else { + ResumingOperation = FALSE; + } + // // Not found in cache, try to download it through HTTP. // @@ -1014,8 +1031,23 @@ HttpBootGetBootFile ( // Accept // User-Agent // [Authorization] + // [Range] + // [If-Match]|[If-Unmodified-Since] // - HttpIoHeader = HttpIoCreateHeader ((Private->AuthData != NULL) ? 4 : 3); + HeadersCount = 3; + if (Private->AuthData != NULL) { + HeadersCount++; + } + + if (ResumingOperation) { + HeadersCount++; + if (Private->LastModifiedOrEtag) { + HeadersCount++; + } + } + + HttpIoHeader = HttpIoCreateHeader (HeadersCount); + if (HttpIoHeader == NULL) { Status = EFI_OUT_OF_RESOURCES; goto ERROR_2; @@ -1097,6 +1129,62 @@ HttpBootGetBootFile ( } } + // + // Add HTTP header field 5 (optional): Range + // + if (ResumingOperation) { + // Resuming a failed download. Prepare the HTTP Range Header + Status = AsciiSPrint ( + RangeValue, + sizeof (RangeValue), + "bytes=%lu-%lu", + Private->PartialTransferredSize, + Private->BootFileSize - 1 + ); + if (EFI_ERROR (Status)) { + goto ERROR_3; + } + + Status = HttpIoSetHeader (HttpIoHeader, "Range", RangeValue); + if (EFI_ERROR (Status)) { + goto ERROR_3; + } + + DEBUG ( + (DEBUG_WARN | DEBUG_INFO, + "HttpBootGetBootFile: Resuming failed download. Range: %a\n", + RangeValue) + ); + + // + // Add HTTP header field 6 (optional): If-Match or If-Unmodified-Since + // + if (Private->LastModifiedOrEtag) { + if (Private->LastModifiedOrEtag[0] == '"') { + // An ETag value starts with " + DEBUG ( + (DEBUG_WARN | DEBUG_INFO, + "HttpBootGetBootFile: If-Match=%a\n", + Private->LastModifiedOrEtag) + ); + // Add If-Match header with the ETag value got from the first request. + Status = HttpIoSetHeader (HttpIoHeader, HTTP_HEADER_IF_MATCH, Private->LastModifiedOrEtag); + } else { + DEBUG ( + (DEBUG_WARN | DEBUG_INFO, + "HttpBootGetBootFile: If-Unmodified-Since=%a\n", + Private->LastModifiedOrEtag) + ); + // Add If-Unmodified-Since header with the timestamp value (Last-Modified) got from the first request. + Status = HttpIoSetHeader (HttpIoHeader, HTTP_HEADER_IF_UNMODIFIED_SINCE, Private->LastModifiedOrEtag); + } + + if (EFI_ERROR (Status)) { + goto ERROR_3; + } + } + } + // // 2.2 Build the rest of HTTP request info. // @@ -1245,6 +1333,62 @@ HttpBootGetBootFile ( Cache->ImageType = *ImageType; } + // Cache ETag or Last-Modified response header value to + // be used when resuming an interrupted download. + HttpHeader = HttpFindHeader ( + ResponseData->HeaderCount, + ResponseData->Headers, + HTTP_HEADER_ETAG + ); + if (HttpHeader == NULL) { + HttpHeader = HttpFindHeader ( + ResponseData->HeaderCount, + ResponseData->Headers, + HTTP_HEADER_LAST_MODIFIED + ); + } + + if (HttpHeader) { + if (Private->LastModifiedOrEtag) { + FreePool (Private->LastModifiedOrEtag); + } + + Private->LastModifiedOrEtag = AllocateCopyPool (AsciiStrSize (HttpHeader->FieldValue), HttpHeader->FieldValue); + } + + // + // 3.2.2 Validate the range response. If operation is being resumed, + // server must respond with Content-Range. + // + if (ResumingOperation) { + HttpHeader = HttpFindHeader ( + ResponseData->HeaderCount, + ResponseData->Headers, + HTTP_HEADER_CONTENT_RANGE + ); + if ((HttpHeader == NULL) || + (AsciiStrnCmp (HttpHeader->FieldValue, "bytes", 5) != 0)) + { + Status = EFI_UNSUPPORTED; + goto ERROR_5; + } + + // Gets the total size of ranged data (Content-Range: -/) + // and check if it remains the same + ContentRangeResponseValue = AsciiStrStr (HttpHeader->FieldValue, "/"); + if (ContentRangeResponseValue == NULL) { + Status = EFI_INVALID_PARAMETER; + goto ERROR_5; + } + + ContentRangeResponseValue++; + ContentLength = AsciiStrDecimalToUintn (ContentRangeResponseValue); + if (ContentLength != *BufferSize) { + Status = EFI_INVALID_PARAMETER; + goto ERROR_5; + } + } + // // 3.3 Init a message-body parser from the header information. // @@ -1295,10 +1439,15 @@ HttpBootGetBootFile ( // In identity transfer-coding there is no need to parse the message body, // just download the message body to the user provided buffer directly. // + if (ResumingOperation && ((ContentLength + Private->PartialTransferredSize) > *BufferSize)) { + Status = EFI_INVALID_PARAMETER; + goto ERROR_6; + } + ReceivedSize = 0; while (ReceivedSize < ContentLength) { - ResponseBody.Body = (CHAR8 *)Buffer + ReceivedSize; - ResponseBody.BodyLength = *BufferSize - ReceivedSize; + ResponseBody.Body = (CHAR8 *)Buffer + (ReceivedSize + Private->PartialTransferredSize); + ResponseBody.BodyLength = *BufferSize - (ReceivedSize + Private->PartialTransferredSize); Status = HttpIoRecvResponse ( &Private->HttpIo, FALSE, @@ -1309,6 +1458,21 @@ HttpBootGetBootFile ( Status = ResponseBody.Status; } + if ((Status == EFI_TIMEOUT) || (Status == EFI_DEVICE_ERROR)) { + // Indicate to the caller that operation may be retried to resume the download. + // We will not check if server sent Accept-Ranges header, because some back-ends + // do not report this header, even when supporting it. Know example: CloudFlare CDN Cache. + Status = EFI_NOT_READY; + Private->PartialTransferredSize = ReceivedSize; + DEBUG ( + ( + DEBUG_WARN | DEBUG_INFO, + "HttpBootGetBootFile: Transfer error. Bytes transferred so far: %lu.\n", + ReceivedSize + ) + ); + } + goto ERROR_6; } @@ -1326,6 +1490,9 @@ HttpBootGetBootFile ( } } } + + // download completed, there is no more partial data + Private->PartialTransferredSize = 0; } else { // // In "chunked" transfer-coding mode, so we need to parse the received @@ -1385,9 +1552,13 @@ HttpBootGetBootFile ( // // 3.5 Message-body receive & parse is completed, we should be able to get the file size now. // - Status = HttpGetEntityLength (Parser, &ContentLength); - if (EFI_ERROR (Status)) { - goto ERROR_6; + if (!ResumingOperation) { + Status = HttpGetEntityLength (Parser, &ContentLength); + if (EFI_ERROR (Status)) { + goto ERROR_6; + } + } else { + ContentLength = Private->BootFileSize; } if (*BufferSize < ContentLength) { diff --git a/NetworkPkg/HttpBootDxe/HttpBootClient.h b/NetworkPkg/HttpBootDxe/HttpBootClient.h index 86a28bc91aa23..cfe3fb662a908 100644 --- a/NetworkPkg/HttpBootDxe/HttpBootClient.h +++ b/NetworkPkg/HttpBootDxe/HttpBootClient.h @@ -108,6 +108,9 @@ HttpBootCreateHttpIo ( BufferSize has been updated with the size needed to complete the request. @retval EFI_ACCESS_DENIED The server needs to authenticate the client. + @retval EFI_NOT_READY Data transfer has timed-out, call HttpBootGetBootFile again to resume + the download operation using HTTP Range headers. + @retval EFI_UNSUPPORTED Some HTTP response header is not supported. @retval Others Unexpected error happened. **/ diff --git a/NetworkPkg/HttpBootDxe/HttpBootDxe.h b/NetworkPkg/HttpBootDxe/HttpBootDxe.h index 5ff8ad4698b2e..193235dabbfc8 100644 --- a/NetworkPkg/HttpBootDxe/HttpBootDxe.h +++ b/NetworkPkg/HttpBootDxe/HttpBootDxe.h @@ -214,6 +214,8 @@ struct _HTTP_BOOT_PRIVATE_DATA { CHAR8 *BootFileUri; VOID *BootFileUriParser; UINTN BootFileSize; + UINTN PartialTransferredSize; + CHAR8 *LastModifiedOrEtag; BOOLEAN NoGateway; HTTP_BOOT_IMAGE_TYPE ImageType; diff --git a/NetworkPkg/HttpBootDxe/HttpBootDxe.inf b/NetworkPkg/HttpBootDxe/HttpBootDxe.inf index cffa642a4bf79..3f87e58a14a91 100644 --- a/NetworkPkg/HttpBootDxe/HttpBootDxe.inf +++ b/NetworkPkg/HttpBootDxe/HttpBootDxe.inf @@ -95,8 +95,10 @@ gEfiAdapterInfoUndiIpv6SupportGuid ## SOMETIMES_CONSUMES ## GUID [Pcd] - gEfiNetworkPkgTokenSpaceGuid.PcdAllowHttpConnections ## CONSUMES - gEfiNetworkPkgTokenSpaceGuid.PcdHttpIoTimeout ## CONSUMES + gEfiNetworkPkgTokenSpaceGuid.PcdAllowHttpConnections ## CONSUMES + gEfiNetworkPkgTokenSpaceGuid.PcdHttpIoTimeout ## CONSUMES + gEfiNetworkPkgTokenSpaceGuid.PcdMaxHttpResumeRetries ## CONSUMES + gEfiNetworkPkgTokenSpaceGuid.PcdHttpDelayBetweenResumeRetries ## CONSUMES [UserExtensions.TianoCore."ExtraFiles"] HttpBootDxeExtra.uni diff --git a/NetworkPkg/HttpBootDxe/HttpBootImpl.c b/NetworkPkg/HttpBootDxe/HttpBootImpl.c index fa27941f80504..15d5e4ea5070d 100644 --- a/NetworkPkg/HttpBootDxe/HttpBootImpl.c +++ b/NetworkPkg/HttpBootDxe/HttpBootImpl.c @@ -292,6 +292,9 @@ HttpBootDhcp ( BufferSize has been updated with the size needed to complete the request. @retval EFI_ACCESS_DENIED Server authentication failed. + @retval EFI_TIMEOUT Server response timeout or the number of retries to resume + an interrupted download operation has reached the limit + defined by PcdMaxHttpResumeRetries. @retval Others Unexpected error happened. **/ EFI_STATUS @@ -304,6 +307,7 @@ HttpBootGetBootFileCaller ( { HTTP_GET_BOOT_FILE_STATE State; EFI_STATUS Status; + UINT32 Retries; if (Private->BootFileSize == 0) { State = GetBootFileHead; @@ -370,13 +374,42 @@ HttpBootGetBootFileCaller ( // // Load the boot file into Buffer // - Status = HttpBootGetBootFile ( - Private, - FALSE, - BufferSize, - Buffer, - ImageType - ); + for (Retries = 1; ; Retries++) { + Status = HttpBootGetBootFile ( + Private, + FALSE, + BufferSize, + Buffer, + ImageType + ); + if (!EFI_ERROR (Status) || + (Status != EFI_NOT_READY) || + (Retries >= PcdGet32 (PcdMaxHttpResumeRetries))) + { + break; + } + + // + // HttpBootGetBootFile returned EFI_NOT_READY, we may attempt to resume + // the interrupted download. + // + + Private->HttpCreated = FALSE; + HttpIoDestroyIo (&Private->HttpIo); + Status = HttpBootCreateHttpIo (Private); + if (EFI_ERROR (Status)) { + break; + } + + DEBUG ((DEBUG_WARN | DEBUG_INFO, "HttpBootGetBootFileCaller: NBP file download interrupted, will try to resume the operation.\n")); + gBS->Stall (1000 * 1000 * PcdGet32 (PcdHttpDelayBetweenResumeRetries)); + } + + if (Status == EFI_NOT_READY) { + DEBUG ((DEBUG_ERROR, "HttpBootGetBootFileCaller: Error downloading NBP file, even after trying to resume %d times.\n", Retries)); + Status = EFI_TIMEOUT; + } + return Status; case GetBootFileError: @@ -407,6 +440,9 @@ HttpBootGetBootFileCaller ( @retval EFI_BUFFER_TOO_SMALL The BufferSize is too small to read the boot file. BufferSize has been updated with the size needed to complete the request. @retval EFI_DEVICE_ERROR An unexpected network error occurred. + @retval EFI_TIMEOUT Server response timeout or the number of retries to resume + an interrupted download operation has reached the limit + defined by PcdMaxHttpResumeRetries. @retval Others Other errors as indicated. **/ @@ -522,12 +558,13 @@ HttpBootStop ( ZeroMem (&Private->StationIp, sizeof (EFI_IP_ADDRESS)); ZeroMem (&Private->SubnetMask, sizeof (EFI_IP_ADDRESS)); ZeroMem (&Private->GatewayIp, sizeof (EFI_IP_ADDRESS)); - Private->Port = 0; - Private->BootFileUri = NULL; - Private->BootFileUriParser = NULL; - Private->BootFileSize = 0; - Private->SelectIndex = 0; - Private->SelectProxyType = HttpOfferTypeMax; + Private->Port = 0; + Private->BootFileUri = NULL; + Private->BootFileUriParser = NULL; + Private->BootFileSize = 0; + Private->SelectIndex = 0; + Private->SelectProxyType = HttpOfferTypeMax; + Private->PartialTransferredSize = 0; if (!Private->UsingIpv6) { // @@ -577,6 +614,11 @@ HttpBootStop ( Private->FilePathUriParser = NULL; } + if (Private->LastModifiedOrEtag != NULL) { + FreePool (Private->LastModifiedOrEtag); + Private->LastModifiedOrEtag = NULL; + } + ZeroMem (Private->OfferBuffer, sizeof (Private->OfferBuffer)); Private->OfferNum = 0; ZeroMem (Private->OfferCount, sizeof (Private->OfferCount)); @@ -765,7 +807,8 @@ HttpBootCallback ( if (Data != NULL) { HttpMessage = (EFI_HTTP_MESSAGE *)Data; if ((HttpMessage->Data.Request->Method == HttpMethodGet) && - (HttpMessage->Data.Request->Url != NULL)) + (HttpMessage->Data.Request->Url != NULL) && + (Private->PartialTransferredSize == 0)) { Print (L"\n URI: %s\n", HttpMessage->Data.Request->Url); } @@ -797,6 +840,16 @@ HttpBootCallback ( } } + // If download was resumed, do not change progress variables + HttpHeader = HttpFindHeader ( + HttpMessage->HeaderCount, + HttpMessage->Headers, + HTTP_HEADER_CONTENT_RANGE + ); + if (HttpHeader) { + break; + } + HttpHeader = HttpFindHeader ( HttpMessage->HeaderCount, HttpMessage->Headers, diff --git a/NetworkPkg/NetworkPkg.dec b/NetworkPkg/NetworkPkg.dec index 7c4289b77b216..29fc0c046c5b1 100644 --- a/NetworkPkg/NetworkPkg.dec +++ b/NetworkPkg/NetworkPkg.dec @@ -104,6 +104,16 @@ # @Prompt Max size of total HTTP chunk transfer. the default value is 12MB. gEfiNetworkPkgTokenSpaceGuid.PcdMaxHttpChunkTransfer|0x0C00000|UINT32|0x0000000E + ## The maximum number of retries while attempting to resume an + # interrupted HTTP download using a HTTP Range request header. + # @Prompt Max number of HTTP download resume retries. Default value is 5. + gEfiNetworkPkgTokenSpaceGuid.PcdMaxHttpResumeRetries|0x00000005|UINT32|0x00000012 + + ## Delay in seconds between each attempt to resume an + # interrupted HTTP download. + # @Prompt Delay in seconds between each HTTP resume retry. Default value is 2s. + gEfiNetworkPkgTokenSpaceGuid.PcdHttpDelayBetweenResumeRetries|0x00000002|UINT32|0x00000013 + [PcdsFixedAtBuild, PcdsPatchableInModule] ## Indicates whether HTTP connections (i.e., unsecured) are permitted or not. # TRUE - HTTP connections are allowed. Both the "https://" and "http://" URI schemes are permitted.