]> xenbits.xensource.com Git - people/aperard/ovmf.git/commitdiff
NetworkPkg/HttpBootDxe: Resume an interrupted boot file download.
authorLeandro Becker <lbecker@positivo.com.br>
Tue, 27 Aug 2024 15:17:10 +0000 (12:17 -0300)
committermergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Fri, 13 Sep 2024 10:26:09 +0000 (10:26 +0000)
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>
NetworkPkg/HttpBootDxe/HttpBootClient.c
NetworkPkg/HttpBootDxe/HttpBootClient.h
NetworkPkg/HttpBootDxe/HttpBootDxe.h
NetworkPkg/HttpBootDxe/HttpBootDxe.inf
NetworkPkg/HttpBootDxe/HttpBootImpl.c
NetworkPkg/NetworkPkg.dec

index 40f64fcb6bf82ccec2de6d41ab22f75b4f890b7b..858e7c21034cab40911c145b58b58d267debec6f 100644 (file)
@@ -923,6 +923,9 @@ HttpBootGetBootFileCallback (
                                    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
@@ -955,6 +958,10 @@ HttpBootGetBootFile (
   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
@@ -985,6 +992,16 @@ HttpBootGetBootFile (
     }\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
@@ -1014,8 +1031,23 @@ HttpBootGetBootFile (
   //       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
@@ -1097,6 +1129,62 @@ HttpBootGetBootFile (
     }\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
@@ -1245,6 +1333,62 @@ HttpBootGetBootFile (
     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
@@ -1295,10 +1439,15 @@ HttpBootGetBootFile (
       // 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
@@ -1309,6 +1458,20 @@ HttpBootGetBootFile (
             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
@@ -1326,6 +1489,9 @@ HttpBootGetBootFile (
           }\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
@@ -1385,9 +1551,13 @@ HttpBootGetBootFile (
   //\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
index 86a28bc91aa23b2ec002203ee1ba8d957492c8c9..406eefb5429828e5c6badc67f28b2f350386a7db 100644 (file)
@@ -108,6 +108,7 @@ HttpBootCreateHttpIo (
                                    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_UNSUPPORTED          Some HTTP response header is not supported.\r
   @retval Others                   Unexpected error happened.\r
 \r
 **/\r
index 5ff8ad4698b2e74209b978aaa6cd664729e6dbe2..193235dabbfc87480afe33100ae58e1033cd9b75 100644 (file)
@@ -214,6 +214,8 @@ struct _HTTP_BOOT_PRIVATE_DATA {
   CHAR8                                        *BootFileUri;\r
   VOID                                         *BootFileUriParser;\r
   UINTN                                        BootFileSize;\r
+  UINTN                                        PartialTransferredSize;\r
+  CHAR8                                        *LastModifiedOrEtag;\r
   BOOLEAN                                      NoGateway;\r
   HTTP_BOOT_IMAGE_TYPE                         ImageType;\r
 \r
index cffa642a4bf79fc80ab5e1f37ea69bcd75e8ce50..3f87e58a14a91002b919cf2504410ed357ecc6f6 100644 (file)
   gEfiAdapterInfoUndiIpv6SupportGuid             ## SOMETIMES_CONSUMES ## GUID\r
 \r
 [Pcd]\r
-  gEfiNetworkPkgTokenSpaceGuid.PcdAllowHttpConnections       ## CONSUMES\r
-  gEfiNetworkPkgTokenSpaceGuid.PcdHttpIoTimeout              ## CONSUMES\r
+  gEfiNetworkPkgTokenSpaceGuid.PcdAllowHttpConnections           ## CONSUMES\r
+  gEfiNetworkPkgTokenSpaceGuid.PcdHttpIoTimeout                  ## CONSUMES\r
+  gEfiNetworkPkgTokenSpaceGuid.PcdMaxHttpResumeRetries           ## CONSUMES\r
+  gEfiNetworkPkgTokenSpaceGuid.PcdHttpDelayBetweenResumeRetries  ## CONSUMES\r
 \r
 [UserExtensions.TianoCore."ExtraFiles"]\r
   HttpBootDxeExtra.uni\r
index fa27941f80504f233248eb9ce6b7305925f9c9df..4f84e59a2183d8fba1f8600d66d0d3636c16d756 100644 (file)
@@ -304,6 +304,7 @@ HttpBootGetBootFileCaller (
 {\r
   HTTP_GET_BOOT_FILE_STATE  State;\r
   EFI_STATUS                Status;\r
+  UINT32                    Retries;\r
 \r
   if (Private->BootFileSize == 0) {\r
     State = GetBootFileHead;\r
@@ -370,13 +371,40 @@ HttpBootGetBootFileCaller (
         //\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
@@ -522,12 +550,13 @@ HttpBootStop (
   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
@@ -577,6 +606,11 @@ HttpBootStop (
     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
@@ -765,7 +799,8 @@ HttpBootCallback (
       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
@@ -797,6 +832,16 @@ HttpBootCallback (
           }\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
index 7c4289b77b216fba1186db0dcdbfec18bf4cf975..29fc0c046c5b12266241f0afa4dd4166eca31ec3 100644 (file)
   # @Prompt Max size of total HTTP chunk transfer. the default value is 12MB.\r
   gEfiNetworkPkgTokenSpaceGuid.PcdMaxHttpChunkTransfer|0x0C00000|UINT32|0x0000000E\r
 \r
+  ## The maximum number of retries while attempting to resume an\r
+  # interrupted HTTP download using a HTTP Range request header.\r
+  # @Prompt Max number of HTTP download resume retries. Default value is 5.\r
+  gEfiNetworkPkgTokenSpaceGuid.PcdMaxHttpResumeRetries|0x00000005|UINT32|0x00000012\r
+\r
+  ## Delay in seconds between each attempt to resume an\r
+  # interrupted HTTP download.\r
+  # @Prompt Delay in seconds between each HTTP resume retry. Default value is 2s.\r
+  gEfiNetworkPkgTokenSpaceGuid.PcdHttpDelayBetweenResumeRetries|0x00000002|UINT32|0x00000013\r
+\r
 [PcdsFixedAtBuild, PcdsPatchableInModule]\r
   ## Indicates whether HTTP connections (i.e., unsecured) are permitted or not.\r
   # TRUE  - HTTP connections are allowed. Both the "https://" and "http://" URI schemes are permitted.\r