BufferSize has been updated with the size needed to complete\r
the request.\r
@retval EFI_ACCESS_DENIED The server needs to authenticate the client.\r
+ @retval EFI_NOT_READY Data transfer has timed-out, call HttpBootGetBootFile again to resume\r
+ the download operation using HTTP Range headers.\r
+ @retval EFI_UNSUPPORTED Some HTTP response header is not supported.\r
@retval Others Unexpected error happened.\r
\r
**/\r
CHAR8 BaseAuthValue[80];\r
EFI_HTTP_HEADER *HttpHeader;\r
CHAR8 *Data;\r
+ UINTN HeadersCount;\r
+ BOOLEAN ResumingOperation;\r
+ CHAR8 *ContentRangeResponseValue;\r
+ CHAR8 RangeValue[64];\r
\r
ASSERT (Private != NULL);\r
ASSERT (Private->HttpCreated);\r
}\r
}\r
\r
+ // Check if this is a previous download that has failed and need to be resumed\r
+ if ((!HeaderOnly) &&\r
+ (Private->PartialTransferredSize > 0) &&\r
+ (Private->BootFileSize == *BufferSize))\r
+ {\r
+ ResumingOperation = TRUE;\r
+ } else {\r
+ ResumingOperation = FALSE;\r
+ }\r
+\r
//\r
// Not found in cache, try to download it through HTTP.\r
//\r
// Accept\r
// User-Agent\r
// [Authorization]\r
+ // [Range]\r
+ // [If-Match]|[If-Unmodified-Since]\r
//\r
- HttpIoHeader = HttpIoCreateHeader ((Private->AuthData != NULL) ? 4 : 3);\r
+ HeadersCount = 3;\r
+ if (Private->AuthData != NULL) {\r
+ HeadersCount++;\r
+ }\r
+\r
+ if (ResumingOperation) {\r
+ HeadersCount++;\r
+ if (Private->LastModifiedOrEtag) {\r
+ HeadersCount++;\r
+ }\r
+ }\r
+\r
+ HttpIoHeader = HttpIoCreateHeader (HeadersCount);\r
+\r
if (HttpIoHeader == NULL) {\r
Status = EFI_OUT_OF_RESOURCES;\r
goto ERROR_2;\r
}\r
}\r
\r
+ //\r
+ // Add HTTP header field 5 (optional): Range\r
+ //\r
+ if (ResumingOperation) {\r
+ // Resuming a failed download. Prepare the HTTP Range Header\r
+ Status = AsciiSPrint (\r
+ RangeValue,\r
+ sizeof (RangeValue),\r
+ "bytes=%lu-%lu",\r
+ Private->PartialTransferredSize,\r
+ Private->BootFileSize - 1\r
+ );\r
+ if (EFI_ERROR (Status)) {\r
+ goto ERROR_3;\r
+ }\r
+\r
+ Status = HttpIoSetHeader (HttpIoHeader, "Range", RangeValue);\r
+ if (EFI_ERROR (Status)) {\r
+ goto ERROR_3;\r
+ }\r
+\r
+ DEBUG (\r
+ (DEBUG_WARN | DEBUG_INFO,\r
+ "HttpBootGetBootFile: Resuming failed download. Range: %a\n",\r
+ RangeValue)\r
+ );\r
+\r
+ //\r
+ // Add HTTP header field 6 (optional): If-Match or If-Unmodified-Since\r
+ //\r
+ if (Private->LastModifiedOrEtag) {\r
+ if (Private->LastModifiedOrEtag[0] == '"') {\r
+ // An ETag value starts with "\r
+ DEBUG (\r
+ (DEBUG_WARN | DEBUG_INFO,\r
+ "HttpBootGetBootFile: If-Match=%a\n",\r
+ Private->LastModifiedOrEtag)\r
+ );\r
+ // Add If-Match header with the ETag value got from the first request.\r
+ Status = HttpIoSetHeader (HttpIoHeader, HTTP_HEADER_IF_MATCH, Private->LastModifiedOrEtag);\r
+ } else {\r
+ DEBUG (\r
+ (DEBUG_WARN | DEBUG_INFO,\r
+ "HttpBootGetBootFile: If-Unmodified-Since=%a\n",\r
+ Private->LastModifiedOrEtag)\r
+ );\r
+ // Add If-Unmodified-Since header with the timestamp value (Last-Modified) got from the first request.\r
+ Status = HttpIoSetHeader (HttpIoHeader, HTTP_HEADER_IF_UNMODIFIED_SINCE, Private->LastModifiedOrEtag);\r
+ }\r
+\r
+ if (EFI_ERROR (Status)) {\r
+ goto ERROR_3;\r
+ }\r
+ }\r
+ }\r
+\r
//\r
// 2.2 Build the rest of HTTP request info.\r
//\r
Cache->ImageType = *ImageType;\r
}\r
\r
+ // Cache ETag or Last-Modified response header value to\r
+ // be used when resuming an interrupted download.\r
+ HttpHeader = HttpFindHeader (\r
+ ResponseData->HeaderCount,\r
+ ResponseData->Headers,\r
+ HTTP_HEADER_ETAG\r
+ );\r
+ if (HttpHeader == NULL) {\r
+ HttpHeader = HttpFindHeader (\r
+ ResponseData->HeaderCount,\r
+ ResponseData->Headers,\r
+ HTTP_HEADER_LAST_MODIFIED\r
+ );\r
+ }\r
+\r
+ if (HttpHeader) {\r
+ if (Private->LastModifiedOrEtag) {\r
+ FreePool (Private->LastModifiedOrEtag);\r
+ }\r
+\r
+ Private->LastModifiedOrEtag = AllocateCopyPool (AsciiStrSize (HttpHeader->FieldValue), HttpHeader->FieldValue);\r
+ }\r
+\r
+ //\r
+ // 3.2.2 Validate the range response. If operation is being resumed,\r
+ // server must respond with Content-Range.\r
+ //\r
+ if (ResumingOperation) {\r
+ HttpHeader = HttpFindHeader (\r
+ ResponseData->HeaderCount,\r
+ ResponseData->Headers,\r
+ HTTP_HEADER_CONTENT_RANGE\r
+ );\r
+ if ((HttpHeader == NULL) ||\r
+ (AsciiStrnCmp (HttpHeader->FieldValue, "bytes", 5) != 0))\r
+ {\r
+ Status = EFI_UNSUPPORTED;\r
+ goto ERROR_5;\r
+ }\r
+\r
+ // Gets the total size of ranged data (Content-Range: <unit> <range-start>-<range-end>/<size>)\r
+ // and check if it remains the same\r
+ ContentRangeResponseValue = AsciiStrStr (HttpHeader->FieldValue, "/");\r
+ if (ContentRangeResponseValue == NULL) {\r
+ Status = EFI_INVALID_PARAMETER;\r
+ goto ERROR_5;\r
+ }\r
+\r
+ ContentRangeResponseValue++;\r
+ ContentLength = AsciiStrDecimalToUintn (ContentRangeResponseValue);\r
+ if (ContentLength != *BufferSize) {\r
+ Status = EFI_INVALID_PARAMETER;\r
+ goto ERROR_5;\r
+ }\r
+ }\r
+\r
//\r
// 3.3 Init a message-body parser from the header information.\r
//\r
// In identity transfer-coding there is no need to parse the message body,\r
// just download the message body to the user provided buffer directly.\r
//\r
+ if (ResumingOperation && ((ContentLength + Private->PartialTransferredSize) > *BufferSize)) {\r
+ Status = EFI_INVALID_PARAMETER;\r
+ goto ERROR_6;\r
+ }\r
+\r
ReceivedSize = 0;\r
while (ReceivedSize < ContentLength) {\r
- ResponseBody.Body = (CHAR8 *)Buffer + ReceivedSize;\r
- ResponseBody.BodyLength = *BufferSize - ReceivedSize;\r
+ ResponseBody.Body = (CHAR8 *)Buffer + (ReceivedSize + Private->PartialTransferredSize);\r
+ ResponseBody.BodyLength = *BufferSize - (ReceivedSize + Private->PartialTransferredSize);\r
Status = HttpIoRecvResponse (\r
&Private->HttpIo,\r
FALSE,\r
Status = ResponseBody.Status;\r
}\r
\r
+ if ((Status == EFI_TIMEOUT) || (Status == EFI_DEVICE_ERROR)) {\r
+ // For EFI_TIMEOUT and EFI_DEVICE_ERROR errors, we may resume the operation.\r
+ // We will not check if server sent Accept-Ranges header, because some back-ends\r
+ // do not report this header, even when supporting it. Know example: CloudFlare CDN Cache.\r
+ Private->PartialTransferredSize = ReceivedSize;\r
+ DEBUG (\r
+ (\r
+ DEBUG_WARN | DEBUG_INFO,\r
+ "HttpBootGetBootFile: Transfer error. Bytes transferred so far: %lu.\n",\r
+ ReceivedSize\r
+ )\r
+ );\r
+ }\r
+\r
goto ERROR_6;\r
}\r
\r
}\r
}\r
}\r
+\r
+ // download completed, there is no more partial data\r
+ Private->PartialTransferredSize = 0;\r
} else {\r
//\r
// In "chunked" transfer-coding mode, so we need to parse the received\r
//\r
// 3.5 Message-body receive & parse is completed, we should be able to get the file size now.\r
//\r
- Status = HttpGetEntityLength (Parser, &ContentLength);\r
- if (EFI_ERROR (Status)) {\r
- goto ERROR_6;\r
+ if (!ResumingOperation) {\r
+ Status = HttpGetEntityLength (Parser, &ContentLength);\r
+ if (EFI_ERROR (Status)) {\r
+ goto ERROR_6;\r
+ }\r
+ } else {\r
+ ContentLength = Private->BootFileSize;\r
}\r
\r
if (*BufferSize < ContentLength) {\r
{\r
HTTP_GET_BOOT_FILE_STATE State;\r
EFI_STATUS Status;\r
+ UINT32 Retries;\r
\r
if (Private->BootFileSize == 0) {\r
State = GetBootFileHead;\r
//\r
// Load the boot file into Buffer\r
//\r
- Status = HttpBootGetBootFile (\r
- Private,\r
- FALSE,\r
- BufferSize,\r
- Buffer,\r
- ImageType\r
- );\r
+ for (Retries = 1; Retries <= PcdGet32 (PcdMaxHttpResumeRetries); Retries++) {\r
+ Status = HttpBootGetBootFile (\r
+ Private,\r
+ FALSE,\r
+ BufferSize,\r
+ Buffer,\r
+ ImageType\r
+ );\r
+ if (!EFI_ERROR (Status) ||\r
+ ((Status != EFI_TIMEOUT) && (Status != EFI_DEVICE_ERROR)))\r
+ {\r
+ break;\r
+ }\r
+\r
+ //\r
+ // HttpBootGetBootFile returned EFI_TIMEOUT or EFI_DEVICE_ERROR.\r
+ // We may attempt to resume the interrupted download.\r
+ //\r
+\r
+ Private->HttpCreated = FALSE;\r
+ HttpIoDestroyIo (&Private->HttpIo);\r
+ Status = HttpBootCreateHttpIo (Private);\r
+ if (EFI_ERROR (Status)) {\r
+ break;\r
+ }\r
+\r
+ DEBUG ((DEBUG_WARN | DEBUG_INFO, "HttpBootGetBootFileCaller: NBP file download interrupted, will try to resume the operation.\n"));\r
+ gBS->Stall (1000 * 1000 * PcdGet32 (PcdHttpDelayBetweenResumeRetries));\r
+ }\r
+\r
+ if (EFI_ERROR (Status) && (Retries >= PcdGet32 (PcdMaxHttpResumeRetries))) {\r
+ DEBUG ((DEBUG_ERROR, "HttpBootGetBootFileCaller: Error downloading NBP file, even after trying to resume %d times.\n", Retries));\r
+ }\r
+\r
return Status;\r
\r
case GetBootFileError:\r
ZeroMem (&Private->StationIp, sizeof (EFI_IP_ADDRESS));\r
ZeroMem (&Private->SubnetMask, sizeof (EFI_IP_ADDRESS));\r
ZeroMem (&Private->GatewayIp, sizeof (EFI_IP_ADDRESS));\r
- Private->Port = 0;\r
- Private->BootFileUri = NULL;\r
- Private->BootFileUriParser = NULL;\r
- Private->BootFileSize = 0;\r
- Private->SelectIndex = 0;\r
- Private->SelectProxyType = HttpOfferTypeMax;\r
+ Private->Port = 0;\r
+ Private->BootFileUri = NULL;\r
+ Private->BootFileUriParser = NULL;\r
+ Private->BootFileSize = 0;\r
+ Private->SelectIndex = 0;\r
+ Private->SelectProxyType = HttpOfferTypeMax;\r
+ Private->PartialTransferredSize = 0;\r
\r
if (!Private->UsingIpv6) {\r
//\r
Private->FilePathUriParser = NULL;\r
}\r
\r
+ if (Private->LastModifiedOrEtag != NULL) {\r
+ FreePool (Private->LastModifiedOrEtag);\r
+ Private->LastModifiedOrEtag = NULL;\r
+ }\r
+\r
ZeroMem (Private->OfferBuffer, sizeof (Private->OfferBuffer));\r
Private->OfferNum = 0;\r
ZeroMem (Private->OfferCount, sizeof (Private->OfferCount));\r
if (Data != NULL) {\r
HttpMessage = (EFI_HTTP_MESSAGE *)Data;\r
if ((HttpMessage->Data.Request->Method == HttpMethodGet) &&\r
- (HttpMessage->Data.Request->Url != NULL))\r
+ (HttpMessage->Data.Request->Url != NULL) &&\r
+ (Private->PartialTransferredSize == 0))\r
{\r
Print (L"\n URI: %s\n", HttpMessage->Data.Request->Url);\r
}\r
}\r
}\r
\r
+ // If download was resumed, do not change progress variables\r
+ HttpHeader = HttpFindHeader (\r
+ HttpMessage->HeaderCount,\r
+ HttpMessage->Headers,\r
+ HTTP_HEADER_CONTENT_RANGE\r
+ );\r
+ if (HttpHeader) {\r
+ break;\r
+ }\r
+\r
HttpHeader = HttpFindHeader (\r
HttpMessage->HeaderCount,\r
HttpMessage->Headers,\r