From 69139e39bc9bd7410ee71a830b812fb74d21bdb4 Mon Sep 17 00:00:00 2001 From: Leandro Becker Date: Tue, 27 Aug 2024 12:17:10 -0300 Subject: 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 | 182 ++++++++++++++++++++++++++++++-- NetworkPkg/HttpBootDxe/HttpBootClient.h | 1 + NetworkPkg/HttpBootDxe/HttpBootDxe.h | 2 + NetworkPkg/HttpBootDxe/HttpBootDxe.inf | 6 +- NetworkPkg/HttpBootDxe/HttpBootImpl.c | 73 ++++++++++--- NetworkPkg/NetworkPkg.dec | 10 ++ 6 files changed, 252 insertions(+), 22 deletions(-) (limited to 'NetworkPkg') diff --git a/NetworkPkg/HttpBootDxe/HttpBootClient.c b/NetworkPkg/HttpBootDxe/HttpBootClient.c index 40f64fcb6b..858e7c2103 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,20 @@ HttpBootGetBootFile ( Status = ResponseBody.Status; } + if ((Status == EFI_TIMEOUT) || (Status == EFI_DEVICE_ERROR)) { + // For EFI_TIMEOUT and EFI_DEVICE_ERROR errors, we may resume the operation. + // 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. + Private->PartialTransferredSize = ReceivedSize; + DEBUG ( + ( + DEBUG_WARN | DEBUG_INFO, + "HttpBootGetBootFile: Transfer error. Bytes transferred so far: %lu.\n", + ReceivedSize + ) + ); + } + goto ERROR_6; } @@ -1326,6 +1489,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 +1551,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 86a28bc91a..406eefb542 100644 --- a/NetworkPkg/HttpBootDxe/HttpBootClient.h +++ b/NetworkPkg/HttpBootDxe/HttpBootClient.h @@ -108,6 +108,7 @@ 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_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 5ff8ad4698..193235dabb 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 cffa642a4b..3f87e58a14 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 fa27941f80..4f84e59a21 100644 --- a/NetworkPkg/HttpBootDxe/HttpBootImpl.c +++ b/NetworkPkg/HttpBootDxe/HttpBootImpl.c @@ -304,6 +304,7 @@ HttpBootGetBootFileCaller ( { HTTP_GET_BOOT_FILE_STATE State; EFI_STATUS Status; + UINT32 Retries; if (Private->BootFileSize == 0) { State = GetBootFileHead; @@ -370,13 +371,40 @@ HttpBootGetBootFileCaller ( // // Load the boot file into Buffer // - Status = HttpBootGetBootFile ( - Private, - FALSE, - BufferSize, - Buffer, - ImageType - ); + for (Retries = 1; Retries <= PcdGet32 (PcdMaxHttpResumeRetries); Retries++) { + Status = HttpBootGetBootFile ( + Private, + FALSE, + BufferSize, + Buffer, + ImageType + ); + if (!EFI_ERROR (Status) || + ((Status != EFI_TIMEOUT) && (Status != EFI_DEVICE_ERROR))) + { + break; + } + + // + // HttpBootGetBootFile returned EFI_TIMEOUT or EFI_DEVICE_ERROR. + // 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 (EFI_ERROR (Status) && (Retries >= PcdGet32 (PcdMaxHttpResumeRetries))) { + DEBUG ((DEBUG_ERROR, "HttpBootGetBootFileCaller: Error downloading NBP file, even after trying to resume %d times.\n", Retries)); + } + return Status; case GetBootFileError: @@ -522,12 +550,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 +606,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 +799,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 +832,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 7c4289b77b..29fc0c046c 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. -- cgit v1.2.3