public inbox for devel@edk2.groups.io
 help / color / mirror / Atom feed
From: "Saloni Kasbekar" <saloni.kasbekar@intel.com>
To: devel@edk2.groups.io
Cc: Saloni Kasbekar <saloni.kasbekar@intel.com>,
	Maciej Rabeda <maciej.rabeda@linux.intel.com>,
	Wu Jiaxin <jiaxin.wu@intel.com>, Siyuan Fu <siyuan.fu@intel.com>
Subject: [edk2-staging/HttpProxy PATCH v3 5/7] NetworkPkg: Add support for HTTP CONNECT Method
Date: Fri,  2 Dec 2022 11:12:24 -0800	[thread overview]
Message-ID: <99951a1bb8751c1e9b001a047575be45413ef616.1670008048.git.saloni.kasbekar@intel.com> (raw)
In-Reply-To: <cover.1670008048.git.saloni.kasbekar@intel.com>

REF: https://bugzilla.tianocore.org/show_bug.cgi?id=3951

Add support for the HTTP CONNECT method to request the Proxy Server
to open a tunnel to the EndPoint Server

Cc: Maciej Rabeda <maciej.rabeda@linux.intel.com>
Cc: Wu Jiaxin <jiaxin.wu@intel.com>
Cc: Siyuan Fu <siyuan.fu@intel.com>
Signed-off-by: Saloni Kasbekar <saloni.kasbekar@intel.com>
---
 NetworkPkg/HttpBootDxe/HttpBootClient.c    | 176 +++++++++++++++++++++
 NetworkPkg/HttpBootDxe/HttpBootClient.h    |  15 ++
 NetworkPkg/HttpBootDxe/HttpBootImpl.c      |  16 +-
 NetworkPkg/HttpBootDxe/HttpBootImpl.h      |   1 +
 NetworkPkg/HttpDxe/HttpDriver.h            |   2 +
 NetworkPkg/HttpDxe/HttpDxe.inf             |   1 +
 NetworkPkg/HttpDxe/HttpImpl.c              | 151 ++++++++++++++----
 NetworkPkg/Library/DxeHttpLib/DxeHttpLib.c |   5 +
 8 files changed, 335 insertions(+), 32 deletions(-)

diff --git a/NetworkPkg/HttpBootDxe/HttpBootClient.c b/NetworkPkg/HttpBootDxe/HttpBootClient.c
index b13155b576..b4d02eaff2 100644
--- a/NetworkPkg/HttpBootDxe/HttpBootClient.c
+++ b/NetworkPkg/HttpBootDxe/HttpBootClient.c
@@ -905,6 +905,182 @@ HttpBootGetBootFileCallback (
   return EFI_SUCCESS;
 }
 
+/**
+  This function establishes a connection through a proxy server
+
+  @param[in]       Private         The pointer to the driver's private data.
+
+  @retval EFI_SUCCESS              Connection successful.
+  @retval EFI_OUT_OF_RESOURCES     Could not allocate needed resources
+  @retval Others                   Unexpected error happened.
+
+**/
+EFI_STATUS
+HttpBootConnectProxy (
+  IN     HTTP_BOOT_PRIVATE_DATA  *Private
+  )
+{
+  EFI_STATUS             Status;
+  EFI_HTTP_STATUS_CODE   StatusCode;
+  CHAR8                  *HostName;
+  EFI_HTTP_REQUEST_DATA  *RequestData;
+  HTTP_IO_RESPONSE_DATA  *ResponseData;
+  HTTP_IO                *HttpIo;
+  HTTP_IO_HEADER         *HttpIoHeader;
+  CHAR16                 *Url;
+  CHAR16                 *ProxyUrl;
+  UINTN                  UrlSize;
+
+  Url          = NULL;
+  ProxyUrl     = NULL;
+  RequestData  = NULL;
+  ResponseData = NULL;
+  HttpIoHeader = NULL;
+
+  UrlSize = AsciiStrSize (Private->BootFileUri);
+  Url     = AllocatePool (UrlSize * sizeof (CHAR16));
+  if (Url == NULL) {
+    return EFI_OUT_OF_RESOURCES;
+  }
+
+  AsciiStrToUnicodeStrS (Private->BootFileUri, Url, UrlSize);
+
+  UrlSize  = AsciiStrSize (Private->ProxyUri);
+  ProxyUrl = AllocatePool (UrlSize * (sizeof (CHAR16)));
+  if (ProxyUrl == NULL) {
+    Status = EFI_OUT_OF_RESOURCES;
+    goto EXIT;
+  }
+
+  AsciiStrToUnicodeStrS (Private->ProxyUri, ProxyUrl, UrlSize);
+
+  //
+  // Send HTTP request message.
+  //
+
+  //
+  // Build HTTP header for the request, 2 headers are needed to send a CONNECT method:
+  //   Host
+  //   User
+  //
+  HttpIoHeader = HttpIoCreateHeader (2);
+  if (HttpIoHeader == NULL) {
+    Status = EFI_OUT_OF_RESOURCES;
+    goto EXIT;
+  }
+
+  //
+  // Add HTTP header field 1: Host (EndPoint URI)
+  //
+  HostName = NULL;
+  Status   = HttpUrlGetHostName (
+               Private->BootFileUri,
+               Private->BootFileUriParser,
+               &HostName
+               );
+  if (EFI_ERROR (Status)) {
+    goto EXIT;
+  }
+
+  Status = HttpIoSetHeader (
+             HttpIoHeader,
+             HTTP_HEADER_HOST,
+             HostName
+             );
+  if (EFI_ERROR (Status)) {
+    goto EXIT;
+  }
+
+  //
+  // Add HTTP header field 2: User-Agent
+  //
+  Status = HttpIoSetHeader (
+             HttpIoHeader,
+             HTTP_HEADER_USER_AGENT,
+             HTTP_USER_AGENT_EFI_HTTP_BOOT
+             );
+  if (EFI_ERROR (Status)) {
+    goto EXIT;
+  }
+
+  //
+  // Build the rest of HTTP request info.
+  //
+  RequestData = AllocatePool (sizeof (EFI_HTTP_REQUEST_DATA));
+  if (RequestData == NULL) {
+    Status = EFI_OUT_OF_RESOURCES;
+    goto EXIT;
+  }
+
+  RequestData->Method   = HttpMethodConnect;
+  RequestData->ProxyUrl = ProxyUrl;
+  RequestData->Url      = Url;
+
+  //
+  // Send out the request to HTTP server.
+  //
+  HttpIo = &Private->HttpIo;
+  Status = HttpIoSendRequest (
+             HttpIo,
+             RequestData,
+             HttpIoHeader->HeaderCount,
+             HttpIoHeader->Headers,
+             0,
+             NULL
+             );
+  if (EFI_ERROR (Status)) {
+    goto EXIT;
+  }
+
+  //
+  // Receive HTTP response message.
+  //
+
+  //
+  // Use zero BodyLength to only receive the response headers.
+  //
+  ResponseData = AllocateZeroPool (sizeof (HTTP_IO_RESPONSE_DATA));
+  if (ResponseData == NULL) {
+    Status = EFI_OUT_OF_RESOURCES;
+    goto EXIT;
+  }
+
+  Status = HttpIoRecvResponse (
+             &Private->HttpIo,
+             TRUE,
+             ResponseData
+             );
+
+  if (EFI_ERROR (Status) || EFI_ERROR (ResponseData->Status)) {
+    if (EFI_ERROR (ResponseData->Status)) {
+      StatusCode = HttpIo->RspToken.Message->Data.Response->StatusCode;
+      HttpBootPrintErrorMessage (StatusCode);
+      Status = ResponseData->Status;
+    }
+  }
+
+EXIT:
+  if (ResponseData != NULL) {
+    FreePool (ResponseData);
+  }
+
+  if (RequestData != NULL) {
+    FreePool (RequestData);
+  }
+
+  HttpIoFreeHeader (HttpIoHeader);
+
+  if (ProxyUrl != NULL) {
+    FreePool (ProxyUrl);
+  }
+
+  if (Url != NULL) {
+    FreePool (Url);
+  }
+
+  return Status;
+}
+
 /**
   This function download the boot file by using UEFI HTTP protocol.
 
diff --git a/NetworkPkg/HttpBootDxe/HttpBootClient.h b/NetworkPkg/HttpBootDxe/HttpBootClient.h
index 2fba713679..fcd624f536 100644
--- a/NetworkPkg/HttpBootDxe/HttpBootClient.h
+++ b/NetworkPkg/HttpBootDxe/HttpBootClient.h
@@ -86,6 +86,21 @@ HttpBootCreateHttpIo (
   IN     HTTP_BOOT_PRIVATE_DATA  *Private
   );
 
+/**
+  This function establishes a connection through a proxy server
+
+  @param[in]       Private         The pointer to the driver's private data.
+
+  @retval EFI_SUCCESS              Connection successful.
+  @retval EFI_OUT_OF_RESOURCES     Could not allocate needed resources
+  @retval Others                   Unexpected error happened.
+
+**/
+EFI_STATUS
+HttpBootConnectProxy (
+  IN     HTTP_BOOT_PRIVATE_DATA  *Private
+  );
+
 /**
   This function download the boot file by using UEFI HTTP protocol.
 
diff --git a/NetworkPkg/HttpBootDxe/HttpBootImpl.c b/NetworkPkg/HttpBootDxe/HttpBootImpl.c
index 4748de0603..d4a7c8385a 100644
--- a/NetworkPkg/HttpBootDxe/HttpBootImpl.c
+++ b/NetworkPkg/HttpBootDxe/HttpBootImpl.c
@@ -313,7 +313,11 @@ HttpBootGetBootFileCaller (
   EFI_STATUS                Status;
 
   if (Private->BootFileSize == 0) {
-    State = GetBootFileHead;
+    if (Private->ProxyUri != NULL) {
+      State = ConnectToProxy;
+    } else {
+      State = GetBootFileHead;
+    }
   } else {
     State = LoadBootFile;
   }
@@ -366,6 +370,16 @@ HttpBootGetBootFileCaller (
 
         break;
 
+      case ConnectToProxy:
+        Status = HttpBootConnectProxy (Private);
+        if (Status == EFI_SUCCESS) {
+          State = GetBootFileHead;
+        } else {
+          State = GetBootFileError;
+        }
+
+        break;
+
       case LoadBootFile:
         if (*BufferSize < Private->BootFileSize) {
           *BufferSize = Private->BootFileSize;
diff --git a/NetworkPkg/HttpBootDxe/HttpBootImpl.h b/NetworkPkg/HttpBootDxe/HttpBootImpl.h
index 33da4fec51..e4ffc3ed48 100644
--- a/NetworkPkg/HttpBootDxe/HttpBootImpl.h
+++ b/NetworkPkg/HttpBootDxe/HttpBootImpl.h
@@ -14,6 +14,7 @@ SPDX-License-Identifier: BSD-2-Clause-Patent
 typedef enum {
   GetBootFileHead,
   GetBootFileGet,
+  ConnectToProxy,
   LoadBootFile,
   GetBootFileError
 } HTTP_GET_BOOT_FILE_STATE;
diff --git a/NetworkPkg/HttpDxe/HttpDriver.h b/NetworkPkg/HttpDxe/HttpDriver.h
index 01a6bb7f4b..e0917f431e 100644
--- a/NetworkPkg/HttpDxe/HttpDriver.h
+++ b/NetworkPkg/HttpDxe/HttpDriver.h
@@ -26,6 +26,7 @@
 #include <Library/NetLib.h>
 #include <Library/HttpLib.h>
 #include <Library/DpcLib.h>
+#include <Library/PrintLib.h>
 
 //
 // UEFI Driver Model Protocols
@@ -64,6 +65,7 @@
 // Driver Version
 //
 #define HTTP_DRIVER_VERSION  0xa
+#define URI_STR_MAX_SIZE     255
 
 //
 // Protocol instances
diff --git a/NetworkPkg/HttpDxe/HttpDxe.inf b/NetworkPkg/HttpDxe/HttpDxe.inf
index c9502d0bb6..30b7de1951 100644
--- a/NetworkPkg/HttpDxe/HttpDxe.inf
+++ b/NetworkPkg/HttpDxe/HttpDxe.inf
@@ -47,6 +47,7 @@
   NetLib
   HttpLib
   DpcLib
+  PrintLib
 
 [Protocols]
   gEfiHttpServiceBindingProtocolGuid               ## BY_START
diff --git a/NetworkPkg/HttpDxe/HttpImpl.c b/NetworkPkg/HttpDxe/HttpImpl.c
index a761ce3d5d..2a305e0864 100644
--- a/NetworkPkg/HttpDxe/HttpImpl.c
+++ b/NetworkPkg/HttpDxe/HttpImpl.c
@@ -234,6 +234,7 @@ EfiHttpRequest (
   EFI_HTTP_MESSAGE       *HttpMsg;
   EFI_HTTP_REQUEST_DATA  *Request;
   VOID                   *UrlParser;
+  VOID                   *EndPointUrlParser;
   EFI_STATUS             Status;
   CHAR8                  *HostName;
   UINTN                  HostNameSize;
@@ -247,25 +248,31 @@ EfiHttpRequest (
   UINTN                  UrlLen;
   CHAR8                  *ProxyUrl;
   UINTN                  ProxyUrlLen;
+  CHAR8                  *ParseUrl;
   CHAR16                 *HostNameStr;
   HTTP_TOKEN_WRAP        *Wrap;
   CHAR8                  *FileUrl;
   UINTN                  RequestMsgSize;
   EFI_HANDLE             ImageHandle;
+  UINT16                 EndPointRemotePort;
+  CHAR8                  *EndPointUrlMsg;
 
   //
   // Initializations
   //
-  Url          = NULL;
-  ProxyUrl     = NULL;
-  UrlParser    = NULL;
-  RemotePort   = 0;
-  HostName     = NULL;
-  RequestMsg   = NULL;
-  HostNameStr  = NULL;
-  Wrap         = NULL;
-  FileUrl      = NULL;
-  TlsConfigure = FALSE;
+  Url                = NULL;
+  ProxyUrl           = NULL;
+  UrlParser          = NULL;
+  EndPointUrlParser  = NULL;
+  RemotePort         = 0;
+  HostName           = NULL;
+  RequestMsg         = NULL;
+  HostNameStr        = NULL;
+  Wrap               = NULL;
+  FileUrl            = NULL;
+  TlsConfigure       = FALSE;
+  EndPointUrlMsg     = NULL;
+  EndPointRemotePort = 0;
 
   if ((This == NULL) || (Token == NULL)) {
     return EFI_INVALID_PARAMETER;
@@ -279,7 +286,7 @@ EfiHttpRequest (
   Request = HttpMsg->Data.Request;
 
   //
-  // Only support GET, HEAD, DELETE, PATCH, PUT and POST method in current implementation.
+  // Only support GET, HEAD, DELETE, CONNECT, PATCH, PUT and POST method in current implementation.
   //
   if (Request != NULL) {
     switch (Request->Method) {
@@ -289,6 +296,12 @@ EfiHttpRequest (
       case HttpMethodPut:
       case HttpMethodPost:
       case HttpMethodPatch:
+        break;
+      case HttpMethodConnect:
+        if (Request->ProxyUrl == NULL) {
+          return EFI_INVALID_PARAMETER;
+        }
+
         break;
       default:
         return EFI_UNSUPPORTED;
@@ -391,10 +404,14 @@ EfiHttpRequest (
     }
 
     //
-    // From the information in Url, the HTTP instance will
+    // From the information in the Urls, the HTTP instance will
     // be able to determine whether to use http or https.
     //
-    HttpInstance->UseHttps = IsHttpsUrl (Url);
+    if (Request->Method == HttpMethodConnect) {
+      HttpInstance->UseHttps = IsHttpsUrl (ProxyUrl);
+    } else {
+      HttpInstance->UseHttps = IsHttpsUrl (Url);
+    }
 
     //
     // HTTP is disabled, return directly if the URI is not HTTPS.
@@ -431,13 +448,26 @@ EfiHttpRequest (
       TlsConfigure = TRUE;
     }
 
-    UrlParser = NULL;
-    Status = HttpParseUrl (Url, (UINT32)AsciiStrLen (Url), FALSE, &UrlParser);
+    //
+    // Setup RemoteAddress and RemotePort of HttpInstance.
+    //
+    if (Request->Method == HttpMethodConnect) {
+      // Case 1: HTTP Connect request
+      ParseUrl = ProxyUrl;
+    } else if (HttpInstance->ProxyConnected == TRUE) {
+      // Case 2: Other HTTP request (proxy connected)
+      ParseUrl = HttpInstance->ProxyUrl;
+    } else {
+      // Case 3: Other HTTP request (proxy not connected)
+      ParseUrl = Url;
+    }
+
+    Status = HttpParseUrl (ParseUrl, (UINT32)AsciiStrLen (ParseUrl), FALSE, &UrlParser);
     if (EFI_ERROR (Status)) {
       goto Error1;
     }
 
-    Status = HttpUrlGetHostName (Url, UrlParser, &HostName);
+    Status = HttpUrlGetHostName (ParseUrl, UrlParser, &HostName);
     if (EFI_ERROR (Status)) {
       goto Error1;
     }
@@ -455,7 +485,7 @@ EfiHttpRequest (
       }
     }
 
-    Status = HttpUrlGetPort (Url, UrlParser, &RemotePort);
+    Status = HttpUrlGetPort (ParseUrl, UrlParser, &RemotePort);
     if (EFI_ERROR (Status)) {
       if (HttpInstance->UseHttps) {
         RemotePort = HTTPS_DEFAULT_PORT;
@@ -551,7 +581,7 @@ EfiHttpRequest (
     if (!HttpInstance->LocalAddressIsIPv6) {
       Status = NetLibAsciiStrToIp4 (HostName, &HttpInstance->RemoteAddr);
     } else {
-      Status = HttpUrlGetIp6 (Url, UrlParser, &HttpInstance->RemoteIpv6Addr);
+      Status = HttpUrlGetIp6 (ParseUrl, UrlParser, &HttpInstance->RemoteIpv6Addr);
     }
 
     if (EFI_ERROR (Status)) {
@@ -649,27 +679,74 @@ EfiHttpRequest (
   //
   // Create request message.
   //
-  FileUrl = Url;
-  if ((Url != NULL) && (*FileUrl != '/')) {
+  if (Request->Method == HttpMethodConnect) {
     //
-    // Convert the absolute-URI to the absolute-path
+    // HTTP Connect shall contain EndPoint host name in URI
     //
-    while (*FileUrl != ':') {
-      FileUrl++;
+    Status = HttpParseUrl (Url, (UINT32)AsciiStrLen (Url), FALSE, &EndPointUrlParser);
+    if (EFI_ERROR (Status)) {
+      goto Error3;
     }
 
-    if ((*(FileUrl+1) == '/') && (*(FileUrl+2) == '/')) {
-      FileUrl += 3;
-      while (*FileUrl != '/') {
-        FileUrl++;
+    Status = HttpUrlGetHostName (
+               Url,
+               EndPointUrlParser,
+               &HttpInstance->EndPointHostName
+               );
+    if (EFI_ERROR (Status)) {
+      goto Error3;
+    }
+
+    Status = HttpUrlGetPort (Url, EndPointUrlParser, &EndPointRemotePort);
+    if (EFI_ERROR (Status)) {
+      if (IsHttpsUrl (Url)) {
+        EndPointRemotePort = HTTPS_DEFAULT_PORT;
+      } else {
+        EndPointRemotePort = HTTP_DEFAULT_PORT;
       }
-    } else {
-      Status = EFI_INVALID_PARAMETER;
+    }
+
+    EndPointUrlMsg = AllocateZeroPool (URI_STR_MAX_SIZE);
+    if (EndPointUrlMsg == NULL) {
+      Status = EFI_OUT_OF_RESOURCES;
       goto Error3;
     }
-  }
 
-  Status = HttpGenRequestMessage (HttpMsg, FileUrl, &RequestMsg, &RequestMsgSize);
+    AsciiSPrint (
+      EndPointUrlMsg,
+      URI_STR_MAX_SIZE,
+      "%a:%d",
+      HttpInstance->EndPointHostName,
+      EndPointRemotePort
+      );
+
+    Status = HttpGenRequestMessage (HttpMsg, EndPointUrlMsg, &RequestMsg, &RequestMsgSize);
+
+    FreePool (EndPointUrlMsg);
+    HttpUrlFreeParser (EndPointUrlParser);
+  } else {
+    FileUrl = Url;
+    if ((Url != NULL) && (*FileUrl != '/')) {
+      //
+      // Convert the absolute-URI to the absolute-path
+      //
+      while (*FileUrl != ':') {
+        FileUrl++;
+      }
+
+      if ((*(FileUrl+1) == '/') && (*(FileUrl+2) == '/')) {
+        FileUrl += 3;
+        while (*FileUrl != '/') {
+          FileUrl++;
+        }
+      } else {
+        Status = EFI_INVALID_PARAMETER;
+        goto Error3;
+      }
+    }
+
+    Status = HttpGenRequestMessage (HttpMsg, FileUrl, &RequestMsg, &RequestMsgSize);
+  }
 
   if (EFI_ERROR (Status) || (NULL == RequestMsg)) {
     goto Error3;
@@ -705,6 +782,10 @@ EfiHttpRequest (
 
   DispatchDpc ();
 
+  if (HttpInstance->Method == HttpMethodConnect) {
+    HttpInstance->ProxyConnected = TRUE;
+  }
+
   if (HostName != NULL) {
     FreePool (HostName);
   }
@@ -750,6 +831,14 @@ Error2:
   }
 
 Error1:
+  if (EndPointUrlMsg != NULL) {
+    FreePool (EndPointUrlMsg);
+  }
+
+  if (EndPointUrlParser != NULL) {
+    HttpUrlFreeParser (EndPointUrlParser);
+  }
+
   if (HostName != NULL) {
     FreePool (HostName);
   }
diff --git a/NetworkPkg/Library/DxeHttpLib/DxeHttpLib.c b/NetworkPkg/Library/DxeHttpLib/DxeHttpLib.c
index 6a5d78629b..45087a1935 100644
--- a/NetworkPkg/Library/DxeHttpLib/DxeHttpLib.c
+++ b/NetworkPkg/Library/DxeHttpLib/DxeHttpLib.c
@@ -1927,6 +1927,11 @@ HttpGenRequestMessage (
         CopyMem (RequestPtr, HTTP_METHOD_DELETE, StrLength);
         RequestPtr += StrLength;
         break;
+      case HttpMethodConnect:
+        StrLength = sizeof (HTTP_METHOD_CONNECT) - 1;
+        CopyMem (RequestPtr, HTTP_METHOD_CONNECT, StrLength);
+        RequestPtr += StrLength;
+        break;
       default:
         ASSERT (FALSE);
         Status = EFI_INVALID_PARAMETER;
-- 
2.36.1.windows.1


  parent reply	other threads:[~2022-12-02 19:12 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-12-02 19:12 [edk2-staging/HttpProxy PATCH v3 0/7] Support HTTPS Proxy Server for HTTP Boot Saloni Kasbekar
2022-12-02 19:12 ` [edk2-staging/HttpProxy PATCH v3 1/7] MdeModulePkg/Library: Support multi-URI HTTP Boot device path Saloni Kasbekar
2022-12-02 19:12 ` [edk2-staging/HttpProxy PATCH v3 2/7] MdePkg/Include: Add Proxy Server URL in EFI_HTTP_REQUEST_DATA Saloni Kasbekar
2022-12-02 19:12 ` [edk2-staging/HttpProxy PATCH v3 3/7] NetworkPkg/HttpBootDxe: Update HTTP Boot Driver with parsed Proxy URL Saloni Kasbekar
2022-12-02 19:12 ` [edk2-staging/HttpProxy PATCH v3 4/7] NetworkPkg: Add Proxy Support to HTTP_PROTOCOL Saloni Kasbekar
2022-12-02 19:12 ` Saloni Kasbekar [this message]
2022-12-02 19:12 ` [edk2-staging/HttpProxy PATCH v3 6/7] NetworkPkg/HttpDxe: Support HTTPS EndPoint server with Proxy Saloni Kasbekar
2022-12-02 19:12 ` [edk2-staging/HttpProxy PATCH v3 7/7] NetworkPkg/HttpBootDxe: Add Proxy URI input in setup menu Saloni Kasbekar

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-list from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=99951a1bb8751c1e9b001a047575be45413ef616.1670008048.git.saloni.kasbekar@intel.com \
    --to=devel@edk2.groups.io \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox