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 <lbecker@positivo.com.br>
This commit is contained in:
Leandro Becker 2024-08-27 12:17:10 -03:00 committed by mergify[bot]
parent 5262108822
commit 69139e39bc
6 changed files with 252 additions and 22 deletions

View File

@ -923,6 +923,9 @@ HttpBootGetBootFileCallback (
BufferSize has been updated with the size needed to complete BufferSize has been updated with the size needed to complete
the request. the request.
@retval EFI_ACCESS_DENIED The server needs to authenticate the client. @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. @retval Others Unexpected error happened.
**/ **/
@ -955,6 +958,10 @@ HttpBootGetBootFile (
CHAR8 BaseAuthValue[80]; CHAR8 BaseAuthValue[80];
EFI_HTTP_HEADER *HttpHeader; EFI_HTTP_HEADER *HttpHeader;
CHAR8 *Data; CHAR8 *Data;
UINTN HeadersCount;
BOOLEAN ResumingOperation;
CHAR8 *ContentRangeResponseValue;
CHAR8 RangeValue[64];
ASSERT (Private != NULL); ASSERT (Private != NULL);
ASSERT (Private->HttpCreated); 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. // Not found in cache, try to download it through HTTP.
// //
@ -1014,8 +1031,23 @@ HttpBootGetBootFile (
// Accept // Accept
// User-Agent // User-Agent
// [Authorization] // [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) { if (HttpIoHeader == NULL) {
Status = EFI_OUT_OF_RESOURCES; Status = EFI_OUT_OF_RESOURCES;
goto ERROR_2; 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. // 2.2 Build the rest of HTTP request info.
// //
@ -1245,6 +1333,62 @@ HttpBootGetBootFile (
Cache->ImageType = *ImageType; 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: <unit> <range-start>-<range-end>/<size>)
// 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. // 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, // In identity transfer-coding there is no need to parse the message body,
// just download the message body to the user provided buffer directly. // 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; ReceivedSize = 0;
while (ReceivedSize < ContentLength) { while (ReceivedSize < ContentLength) {
ResponseBody.Body = (CHAR8 *)Buffer + ReceivedSize; ResponseBody.Body = (CHAR8 *)Buffer + (ReceivedSize + Private->PartialTransferredSize);
ResponseBody.BodyLength = *BufferSize - ReceivedSize; ResponseBody.BodyLength = *BufferSize - (ReceivedSize + Private->PartialTransferredSize);
Status = HttpIoRecvResponse ( Status = HttpIoRecvResponse (
&Private->HttpIo, &Private->HttpIo,
FALSE, FALSE,
@ -1309,6 +1458,20 @@ HttpBootGetBootFile (
Status = ResponseBody.Status; 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; goto ERROR_6;
} }
@ -1326,6 +1489,9 @@ HttpBootGetBootFile (
} }
} }
} }
// download completed, there is no more partial data
Private->PartialTransferredSize = 0;
} else { } else {
// //
// In "chunked" transfer-coding mode, so we need to parse the received // 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. // 3.5 Message-body receive & parse is completed, we should be able to get the file size now.
// //
Status = HttpGetEntityLength (Parser, &ContentLength); if (!ResumingOperation) {
if (EFI_ERROR (Status)) { Status = HttpGetEntityLength (Parser, &ContentLength);
goto ERROR_6; if (EFI_ERROR (Status)) {
goto ERROR_6;
}
} else {
ContentLength = Private->BootFileSize;
} }
if (*BufferSize < ContentLength) { if (*BufferSize < ContentLength) {

View File

@ -108,6 +108,7 @@ HttpBootCreateHttpIo (
BufferSize has been updated with the size needed to complete BufferSize has been updated with the size needed to complete
the request. the request.
@retval EFI_ACCESS_DENIED The server needs to authenticate the client. @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. @retval Others Unexpected error happened.
**/ **/

View File

@ -214,6 +214,8 @@ struct _HTTP_BOOT_PRIVATE_DATA {
CHAR8 *BootFileUri; CHAR8 *BootFileUri;
VOID *BootFileUriParser; VOID *BootFileUriParser;
UINTN BootFileSize; UINTN BootFileSize;
UINTN PartialTransferredSize;
CHAR8 *LastModifiedOrEtag;
BOOLEAN NoGateway; BOOLEAN NoGateway;
HTTP_BOOT_IMAGE_TYPE ImageType; HTTP_BOOT_IMAGE_TYPE ImageType;

View File

@ -95,8 +95,10 @@
gEfiAdapterInfoUndiIpv6SupportGuid ## SOMETIMES_CONSUMES ## GUID gEfiAdapterInfoUndiIpv6SupportGuid ## SOMETIMES_CONSUMES ## GUID
[Pcd] [Pcd]
gEfiNetworkPkgTokenSpaceGuid.PcdAllowHttpConnections ## CONSUMES gEfiNetworkPkgTokenSpaceGuid.PcdAllowHttpConnections ## CONSUMES
gEfiNetworkPkgTokenSpaceGuid.PcdHttpIoTimeout ## CONSUMES gEfiNetworkPkgTokenSpaceGuid.PcdHttpIoTimeout ## CONSUMES
gEfiNetworkPkgTokenSpaceGuid.PcdMaxHttpResumeRetries ## CONSUMES
gEfiNetworkPkgTokenSpaceGuid.PcdHttpDelayBetweenResumeRetries ## CONSUMES
[UserExtensions.TianoCore."ExtraFiles"] [UserExtensions.TianoCore."ExtraFiles"]
HttpBootDxeExtra.uni HttpBootDxeExtra.uni

View File

@ -304,6 +304,7 @@ HttpBootGetBootFileCaller (
{ {
HTTP_GET_BOOT_FILE_STATE State; HTTP_GET_BOOT_FILE_STATE State;
EFI_STATUS Status; EFI_STATUS Status;
UINT32 Retries;
if (Private->BootFileSize == 0) { if (Private->BootFileSize == 0) {
State = GetBootFileHead; State = GetBootFileHead;
@ -370,13 +371,40 @@ HttpBootGetBootFileCaller (
// //
// Load the boot file into Buffer // Load the boot file into Buffer
// //
Status = HttpBootGetBootFile ( for (Retries = 1; Retries <= PcdGet32 (PcdMaxHttpResumeRetries); Retries++) {
Private, Status = HttpBootGetBootFile (
FALSE, Private,
BufferSize, FALSE,
Buffer, BufferSize,
ImageType 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; return Status;
case GetBootFileError: case GetBootFileError:
@ -522,12 +550,13 @@ HttpBootStop (
ZeroMem (&Private->StationIp, sizeof (EFI_IP_ADDRESS)); ZeroMem (&Private->StationIp, sizeof (EFI_IP_ADDRESS));
ZeroMem (&Private->SubnetMask, sizeof (EFI_IP_ADDRESS)); ZeroMem (&Private->SubnetMask, sizeof (EFI_IP_ADDRESS));
ZeroMem (&Private->GatewayIp, sizeof (EFI_IP_ADDRESS)); ZeroMem (&Private->GatewayIp, sizeof (EFI_IP_ADDRESS));
Private->Port = 0; Private->Port = 0;
Private->BootFileUri = NULL; Private->BootFileUri = NULL;
Private->BootFileUriParser = NULL; Private->BootFileUriParser = NULL;
Private->BootFileSize = 0; Private->BootFileSize = 0;
Private->SelectIndex = 0; Private->SelectIndex = 0;
Private->SelectProxyType = HttpOfferTypeMax; Private->SelectProxyType = HttpOfferTypeMax;
Private->PartialTransferredSize = 0;
if (!Private->UsingIpv6) { if (!Private->UsingIpv6) {
// //
@ -577,6 +606,11 @@ HttpBootStop (
Private->FilePathUriParser = NULL; Private->FilePathUriParser = NULL;
} }
if (Private->LastModifiedOrEtag != NULL) {
FreePool (Private->LastModifiedOrEtag);
Private->LastModifiedOrEtag = NULL;
}
ZeroMem (Private->OfferBuffer, sizeof (Private->OfferBuffer)); ZeroMem (Private->OfferBuffer, sizeof (Private->OfferBuffer));
Private->OfferNum = 0; Private->OfferNum = 0;
ZeroMem (Private->OfferCount, sizeof (Private->OfferCount)); ZeroMem (Private->OfferCount, sizeof (Private->OfferCount));
@ -765,7 +799,8 @@ HttpBootCallback (
if (Data != NULL) { if (Data != NULL) {
HttpMessage = (EFI_HTTP_MESSAGE *)Data; HttpMessage = (EFI_HTTP_MESSAGE *)Data;
if ((HttpMessage->Data.Request->Method == HttpMethodGet) && 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); 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 ( HttpHeader = HttpFindHeader (
HttpMessage->HeaderCount, HttpMessage->HeaderCount,
HttpMessage->Headers, HttpMessage->Headers,

View File

@ -104,6 +104,16 @@
# @Prompt Max size of total HTTP chunk transfer. the default value is 12MB. # @Prompt Max size of total HTTP chunk transfer. the default value is 12MB.
gEfiNetworkPkgTokenSpaceGuid.PcdMaxHttpChunkTransfer|0x0C00000|UINT32|0x0000000E 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] [PcdsFixedAtBuild, PcdsPatchableInModule]
## Indicates whether HTTP connections (i.e., unsecured) are permitted or not. ## 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. # TRUE - HTTP connections are allowed. Both the "https://" and "http://" URI schemes are permitted.