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.