Skip to content

Commit

Permalink
NetworkPkg/HttpBootDxe: Resume an interrupted boot file download.
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
leandrobecker-pst committed Aug 9, 2024
1 parent b0f43dd commit 17021bb
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 20 deletions.
179 changes: 173 additions & 6 deletions NetworkPkg/HttpBootDxe/HttpBootClient.c
Original file line number Diff line number Diff line change
Expand Up @@ -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.
**/
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
//
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1097,6 +1129,58 @@ 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 If-Unmodified-Since header with the value got from the first request when resuming a download!
if (Private->LastModifiedOrEtag) {
if (Private->LastModifiedOrEtag[0] == '"') {
// ETag starts with "
DEBUG (
(DEBUG_WARN | DEBUG_INFO,
"HttpBootGetBootFile: If-Match=%a\n",
Private->LastModifiedOrEtag)
);
Status = HttpIoSetHeader (HttpIoHeader, HTTP_HEADER_IF_MATCH, Private->LastModifiedOrEtag);
} else {
DEBUG (
(DEBUG_WARN | DEBUG_INFO,
"HttpBootGetBootFile: If-Unmodified-Since=%a\n",
Private->LastModifiedOrEtag)
);
Status = HttpIoSetHeader (HttpIoHeader, "If-Unmodified-Since", Private->LastModifiedOrEtag);
}

if (EFI_ERROR (Status)) {
goto ERROR_3;
}
}
}

//
// 2.2 Build the rest of HTTP request info.
//
Expand Down Expand Up @@ -1245,6 +1329,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,
"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,
"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.
//
Expand Down Expand Up @@ -1295,10 +1435,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,
Expand All @@ -1309,6 +1454,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;
}

Expand All @@ -1326,6 +1486,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
Expand Down Expand Up @@ -1385,9 +1548,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) {
Expand Down
2 changes: 2 additions & 0 deletions NetworkPkg/HttpBootDxe/HttpBootDxe.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
1 change: 1 addition & 0 deletions NetworkPkg/HttpBootDxe/HttpBootDxe.inf
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
[Pcd]
gEfiNetworkPkgTokenSpaceGuid.PcdAllowHttpConnections ## CONSUMES
gEfiNetworkPkgTokenSpaceGuid.PcdHttpIoTimeout ## CONSUMES
gEfiNetworkPkgTokenSpaceGuid.PcdMaxHttpResumeRetries ## CONSUMES

[UserExtensions.TianoCore."ExtraFiles"]
HttpBootDxeExtra.uni
70 changes: 56 additions & 14 deletions NetworkPkg/HttpBootDxe/HttpBootImpl.c
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ HttpBootLoadFile (
)
{
EFI_STATUS Status;
UINT32 Retries;

if ((Private == NULL) || (ImageType == NULL) || (BufferSize == NULL)) {
return EFI_INVALID_PARAMETER;
Expand Down Expand Up @@ -400,13 +401,37 @@ HttpBootLoadFile (
//
// Load the boot file into Buffer
//
Status = HttpBootGetBootFile (
Private,
FALSE,
BufferSize,
Buffer,
ImageType
);
for (Retries = 0; Retries < PcdGet32 (PcdMaxHttpResumeRetries); Retries++) {
Status = HttpBootGetBootFile (
Private,
FALSE,
BufferSize,
Buffer,
ImageType
);
if (!EFI_ERROR (Status) || (Status != EFI_NOT_READY)) {
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, "HttpBootLoadFile: Download interrupted, will try to resume the operation.\n"));
gBS->Stall (1000 * 1000 * 3);
}

if (Status == EFI_NOT_READY) {
Status = EFI_TIMEOUT;
}

ON_EXIT:
HttpBootUninstallCallback (Private);
Expand Down Expand Up @@ -467,12 +492,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) {
//
Expand Down Expand Up @@ -522,6 +548,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));
Expand Down Expand Up @@ -710,7 +741,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);
}
Expand Down Expand Up @@ -742,6 +774,16 @@ HttpBootCallback (
}
}

// If download was resumed, do not change progress variables
HttpHeader = HttpFindHeader (
HttpMessage->HeaderCount,
HttpMessage->Headers,
"Content-Range"
);
if (HttpHeader) {
break;
}

HttpHeader = HttpFindHeader (
HttpMessage->HeaderCount,
HttpMessage->Headers,
Expand Down
5 changes: 5 additions & 0 deletions NetworkPkg/NetworkPkg.dec
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@
# @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

[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.
Expand Down

0 comments on commit 17021bb

Please sign in to comment.