Author: Stefan Gränitz
Date: 2026-03-20T12:47:45+01:00
New Revision: 39d6bb21804d21abe2fa0ec019919d72104827ac

URL: 
https://github.com/llvm/llvm-project/commit/39d6bb21804d21abe2fa0ec019919d72104827ac
DIFF: 
https://github.com/llvm/llvm-project/commit/39d6bb21804d21abe2fa0ec019919d72104827ac.diff

LOG: [lldb] Add HTTP support in SymbolLocatorSymStore (#186986)

The initial version of SymbolLocatorSymStore supported servers only on
local paths. This patch extends it to HTTP/HTTPS end-points. For that to
work on Windows, we add a WinHTTP-based HTTP client backend in LLVM next
to the existing CURL-based implementation.

We don't add a HTTP server implementation, because there is no use right
now. Test coverage for the LLVM part is built on llvm-debuginfod-find
and works server-less, since it checks textual output of request
headers. The existing CURL-based implementation uses the same approach.
The LLDB API test for the specific SymbolLocatorSymStore feature spawns
a HTTP server from Python.

To keep the size of this patch within reasonable limits, the initial
implementation of the SymbolLocatorSymStore feature is dump: There is no
caching, no verification of downloaded files and no protection against
file corruptions. We use a local implementation of LLVM's
HTTPResponseHandler, but should think about extracting and reusing
Debuginfod's StreamedHTTPResponseHandler in the future.

The goal of this patch is a basic working implementation that is
testable in isolation. We will iterate on it to improve it further. This
should be fine since downloading from SymStores is not default-enabled
yet. Users have to set their server URLs explicitly.

---------

Co-authored-by: Alexandre Ganea <[email protected]>

Added: 
    lldb/test/API/symstore/TestSymStore.py
    llvm/test/tools/llvm-debuginfod-find/headers-curl.test
    llvm/test/tools/llvm-debuginfod-find/headers-winhttp.test

Modified: 
    lldb/source/Plugins/SymbolLocator/SymStore/CMakeLists.txt
    lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStore.cpp
    llvm/include/llvm/Support/HTTP/HTTPClient.h
    llvm/lib/Support/HTTP/CMakeLists.txt
    llvm/lib/Support/HTTP/HTTPClient.cpp

Removed: 
    lldb/test/API/symstore/TestSymStoreLocal.py
    llvm/test/tools/llvm-debuginfod-find/headers.test


################################################################################
diff  --git a/lldb/source/Plugins/SymbolLocator/SymStore/CMakeLists.txt 
b/lldb/source/Plugins/SymbolLocator/SymStore/CMakeLists.txt
index b0da27f26c6a8..775e0284cd8af 100644
--- a/lldb/source/Plugins/SymbolLocator/SymStore/CMakeLists.txt
+++ b/lldb/source/Plugins/SymbolLocator/SymStore/CMakeLists.txt
@@ -13,6 +13,7 @@ add_lldb_library(lldbPluginSymbolLocatorSymStore PLUGIN
     lldbCore
     lldbHost
     lldbSymbol
+    LLVMSupportHTTP
   )
 
 add_dependencies(lldbPluginSymbolLocatorSymStore

diff  --git 
a/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStore.cpp 
b/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStore.cpp
index 6d1119b998e4d..884519b54acfd 100644
--- a/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStore.cpp
+++ b/lldb/source/Plugins/SymbolLocator/SymStore/SymbolLocatorSymStore.cpp
@@ -20,6 +20,8 @@
 #include "llvm/ADT/StringExtras.h"
 #include "llvm/Support/Endian.h"
 #include "llvm/Support/FileSystem.h"
+#include "llvm/Support/FormatVariadic.h"
+#include "llvm/Support/HTTP/HTTPClient.h"
 #include "llvm/Support/Path.h"
 
 using namespace lldb;
@@ -65,11 +67,11 @@ static PluginProperties &GetGlobalPluginProperties() {
 SymbolLocatorSymStore::SymbolLocatorSymStore() : SymbolLocator() {}
 
 void SymbolLocatorSymStore::Initialize() {
-  // First version can only locate PDB in local SymStore (no download yet).
   PluginManager::RegisterPlugin(
       GetPluginNameStatic(), GetPluginDescriptionStatic(), CreateInstance,
       nullptr, LocateExecutableSymbolFile, nullptr, nullptr,
       SymbolLocatorSymStore::DebuggerInitialize);
+  llvm::HTTPClient::initialize();
 }
 
 void SymbolLocatorSymStore::DebuggerInitialize(Debugger &debugger) {
@@ -85,6 +87,7 @@ void SymbolLocatorSymStore::DebuggerInitialize(Debugger 
&debugger) {
 
 void SymbolLocatorSymStore::Terminate() {
   PluginManager::UnregisterPlugin(CreateInstance);
+  llvm::HTTPClient::cleanup();
 }
 
 llvm::StringRef SymbolLocatorSymStore::GetPluginDescriptionStatic() {
@@ -95,6 +98,8 @@ SymbolLocator *SymbolLocatorSymStore::CreateInstance() {
   return new SymbolLocatorSymStore();
 }
 
+namespace {
+
 // RSDS entries store identity as a 20-byte UUID composed of 16-byte GUID and
 // 4-byte age:
 //   12345678-1234-5678-9ABC-DEF012345678-00000001
@@ -102,13 +107,150 @@ SymbolLocator *SymbolLocatorSymStore::CreateInstance() {
 // SymStore key is a string with no separators and age as decimal:
 //   12345678123456789ABCDEF0123456781
 //
-static std::string formatSymStoreKey(const UUID &uuid) {
+std::string formatSymStoreKey(const UUID &uuid) {
   llvm::ArrayRef<uint8_t> bytes = uuid.GetBytes();
   uint32_t age = llvm::support::endian::read32be(bytes.data() + 16);
   constexpr bool LowerCase = false;
   return llvm::toHex(bytes.slice(0, 16), LowerCase) + std::to_string(age);
 }
 
+// This is a simple version of Debuginfod's StreamedHTTPResponseHandler. We
+// should consider reusing that once we introduce caching.
+class FileDownloadHandler : public llvm::HTTPResponseHandler {
+private:
+  std::error_code m_ec;
+  llvm::raw_fd_ostream m_stream;
+
+public:
+  FileDownloadHandler(llvm::StringRef file) : m_stream(file.str(), m_ec) {}
+  virtual ~FileDownloadHandler() = default;
+
+  llvm::Error handleBodyChunk(llvm::StringRef data) override {
+    // Propagate error from ctor.
+    if (m_ec)
+      return llvm::createStringError(m_ec, "Failed to open file for writing");
+    m_stream.write(data.data(), data.size());
+    if (std::error_code ec = m_stream.error())
+      return llvm::createStringError(ec, "Error writing to file");
+
+    return llvm::Error::success();
+  }
+};
+
+llvm::Error downloadFileHTTP(llvm::StringRef url, FileDownloadHandler dest) {
+  if (!llvm::HTTPClient::isAvailable())
+    return llvm::createStringError(
+        std::make_error_code(std::errc::not_supported),
+        "HTTP client is not available");
+  llvm::HTTPRequest Request(url);
+  Request.FollowRedirects = true;
+
+  llvm::HTTPClient Client;
+
+  // TODO: Since PDBs can be huge, we should distinguish between resolve,
+  // connect, send and receive.
+  Client.setTimeout(std::chrono::seconds(60));
+
+  if (llvm::Error Err = Client.perform(Request, dest))
+    return Err;
+
+  unsigned ResponseCode = Client.responseCode();
+  if (ResponseCode != 200) {
+    return llvm::createStringError(std::make_error_code(std::errc::io_error),
+                                   "HTTP request failed with status code " +
+                                       std::to_string(ResponseCode));
+  }
+
+  return llvm::Error::success();
+}
+
+bool has_unsafe_characters(llvm::StringRef s) {
+  for (unsigned char c : s) {
+    // RFC 3986 unreserved characters are safe for file names and URLs.
+    if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
+        (c >= '0' && c <= '9') || c == '-' || c == '.' || c == '_' ||
+        c == '~') {
+      continue;
+    }
+
+    return true;
+  }
+
+  // Avoid path semantics issues.
+  return s == "." || s == "..";
+}
+
+// TODO: This is a dump initial implementation: It always downloads the file, 
it
+// doesn't validate the result, it doesn't employ proper buffering for large
+// files.
+std::optional<FileSpec>
+requestFileFromSymStoreServerHTTP(llvm::StringRef base_url, llvm::StringRef 
key,
+                                  llvm::StringRef pdb_name) {
+  using namespace llvm::sys;
+  Log *log = GetLog(LLDBLog::Symbols);
+
+  // Make sure URL will be valid, portable, and compatible with symbol servers.
+  if (has_unsafe_characters(pdb_name)) {
+    Debugger::ReportWarning(llvm::formatv(
+        "Rejecting HTTP lookup for PDB file due to unsafe characters in "
+        "name: {0}",
+        pdb_name));
+    return {};
+  }
+
+  // Construct the path for local storage. Configurable cache coming soon.
+  llvm::SmallString<128> cache_file;
+  if (!path::cache_directory(cache_file)) {
+    Debugger::ReportWarning("Failed to determine cache directory for 
SymStore");
+    return {};
+  }
+  path::append(cache_file, "lldb", "SymStore", pdb_name, key);
+  if (std::error_code ec = fs::create_directories(cache_file)) {
+    Debugger::ReportWarning(
+        llvm::formatv("Failed to create cache directory '{0}': {1}", 
cache_file,
+                      ec.message()));
+    return {};
+  }
+  path::append(cache_file, pdb_name);
+
+  // Server has same directory structure with forward slashes as separators.
+  std::string source_url =
+      llvm::formatv("{0}/{1}/{2}/{1}", base_url, pdb_name, key);
+  if (llvm::Error err = downloadFileHTTP(source_url, cache_file.str())) {
+    LLDB_LOG_ERROR(log, std::move(err),
+                   "Failed to download from SymStore '{1}': {0}", source_url);
+    return {};
+  }
+
+  return FileSpec(cache_file.str());
+}
+
+std::optional<FileSpec> findFileInLocalSymStore(llvm::StringRef root_dir,
+                                                llvm::StringRef key,
+                                                llvm::StringRef pdb_name) {
+  llvm::SmallString<256> path;
+  llvm::sys::path::append(path, root_dir, pdb_name, key, pdb_name);
+  FileSpec spec(path);
+  if (!FileSystem::Instance().Exists(spec))
+    return {};
+
+  return spec;
+}
+
+std::optional<FileSpec> locateSymStoreEntry(llvm::StringRef base_url,
+                                            llvm::StringRef key,
+                                            llvm::StringRef pdb_name) {
+  if (base_url.starts_with("http://";) || base_url.starts_with("https://";))
+    return requestFileFromSymStoreServerHTTP(base_url, key, pdb_name);
+
+  if (base_url.starts_with("file://"))
+    base_url = base_url.drop_front(7);
+
+  return findFileInLocalSymStore(base_url, key, pdb_name);
+}
+
+} // namespace
+
 std::optional<FileSpec> SymbolLocatorSymStore::LocateExecutableSymbolFile(
     const ModuleSpec &module_spec, const FileSpecList &default_search_paths) {
   const UUID &uuid = module_spec.GetUUID();
@@ -135,12 +277,9 @@ std::optional<FileSpec> 
SymbolLocatorSymStore::LocateExecutableSymbolFile(
   std::string key = formatSymStoreKey(uuid);
   Args sym_store_urls = GetGlobalPluginProperties().GetURLs();
   for (const Args::ArgEntry &url : sym_store_urls) {
-    llvm::SmallString<256> path;
-    llvm::sys::path::append(path, url.ref(), pdb_name, key, pdb_name);
-    FileSpec spec(path);
-    if (FileSystem::Instance().Exists(spec)) {
+    if (auto spec = locateSymStoreEntry(url.ref(), key, pdb_name)) {
       LLDB_LOG_VERBOSE(log, "Found {0} in SymStore {1}", pdb_name, url.ref());
-      return spec;
+      return *spec;
     }
   }
 

diff  --git a/lldb/test/API/symstore/TestSymStoreLocal.py 
b/lldb/test/API/symstore/TestSymStore.py
similarity index 68%
rename from lldb/test/API/symstore/TestSymStoreLocal.py
rename to lldb/test/API/symstore/TestSymStore.py
index 98569d2b8c66f..13d0cc1666c84 100644
--- a/lldb/test/API/symstore/TestSymStoreLocal.py
+++ b/lldb/test/API/symstore/TestSymStore.py
@@ -1,5 +1,9 @@
+import http.server
 import os
 import shutil
+import socketserver
+import threading
+from functools import partial
 
 import lldb
 from lldbsuite.test.decorators import *
@@ -62,7 +66,31 @@ def __exit__(self, *exc_info):
         self._test.runCmd("settings clear plugin.symbol-locator.symstore")
 
 
-class SymStoreLocalTests(TestBase):
+class HTTPServer:
+    """
+    Context Manager to serve a local directory tree via HTTP.
+    """
+
+    def __init__(self, dir):
+        address = ("localhost", 0)  # auto-select free port
+        handler = partial(http.server.SimpleHTTPRequestHandler, directory=dir)
+        self._server = socketserver.ThreadingTCPServer(address, handler)
+        self._thread = threading.Thread(target=self._server.serve_forever, 
daemon=True)
+
+    def __enter__(self):
+        self._thread.start()
+        host, port = self._server.server_address
+        return f"http://{host}:{port}";
+
+    def __exit__(self, *exc_info):
+        if self._server:
+            self._server.shutdown()
+            self._server.server_close()
+        if self._thread:
+            self._thread.join()
+
+
+class SymStoreTests(TestBase):
     SHARED_BUILD_TESTCASE = False
     TEST_WITH_PDB_DEBUG_INFO = True
 
@@ -99,10 +127,8 @@ def test_external_lookup_off(self):
         Check that breakpoint doesn't resolve with external lookup disabled.
         """
         exe, sym = self.build_inferior()
-        with MockedSymStore(self, exe, sym) as symstore_dir:
-            self.runCmd(
-                f"settings set plugin.symbol-locator.symstore.urls 
{symstore_dir}"
-            )
+        with MockedSymStore(self, exe, sym) as dir:
+            self.runCmd(f"settings set plugin.symbol-locator.symstore.urls 
{dir}")
             self.try_breakpoint(exe, ext_lookup=False, should_have_loc=False)
 
     def test_local_dir(self):
@@ -110,8 +136,18 @@ def test_local_dir(self):
         Check that breakpoint resolves with local SymStore.
         """
         exe, sym = self.build_inferior()
-        with MockedSymStore(self, exe, sym) as symstore_dir:
-            self.runCmd(
-                f"settings set plugin.symbol-locator.symstore.urls 
{symstore_dir}"
-            )
+        with MockedSymStore(self, exe, sym) as dir:
+            self.runCmd(f"settings set plugin.symbol-locator.symstore.urls 
{dir}")
             self.try_breakpoint(exe, should_have_loc=True)
+
+    # TODO: Add test coverage for common HTTPS security scenarios, e.g. 
self-signed
+    # certs, non-HTTPS redirects, etc.
+    def test_http(self):
+        """
+        Check that breakpoint hits with remote SymStore.
+        """
+        exe, sym = self.build_inferior()
+        with MockedSymStore(self, exe, sym) as dir:
+            with HTTPServer(dir) as url:
+                self.runCmd(f"settings set plugin.symbol-locator.symstore.urls 
{url}")
+                self.try_breakpoint(exe, should_have_loc=True)

diff  --git a/llvm/include/llvm/Support/HTTP/HTTPClient.h 
b/llvm/include/llvm/Support/HTTP/HTTPClient.h
index aa4727ad33024..17b706ff3e083 100644
--- a/llvm/include/llvm/Support/HTTP/HTTPClient.h
+++ b/llvm/include/llvm/Support/HTTP/HTTPClient.h
@@ -51,8 +51,8 @@ class HTTPResponseHandler {
 
 /// A reusable client that can perform HTTPRequests through a network socket.
 class HTTPClient {
-#ifdef LLVM_ENABLE_CURL
-  void *Curl = nullptr;
+#if defined(LLVM_ENABLE_CURL) || defined(_WIN32)
+  void *Handle = nullptr;
 #endif
 
 public:

diff  --git a/llvm/lib/Support/HTTP/CMakeLists.txt 
b/llvm/lib/Support/HTTP/CMakeLists.txt
index 9bf1da8c60c88..e7a0e6fe34110 100644
--- a/llvm/lib/Support/HTTP/CMakeLists.txt
+++ b/llvm/lib/Support/HTTP/CMakeLists.txt
@@ -8,6 +8,11 @@ if (LLVM_ENABLE_HTTPLIB)
   set(imported_libs ${imported_libs} httplib::httplib)
 endif()
 
+# Use WinHTTP on Windows
+if (WIN32)
+  set(imported_libs ${imported_libs} winhttp.lib)
+endif()
+
 add_llvm_component_library(LLVMSupportHTTP
   HTTPClient.cpp
   HTTPServer.cpp

diff  --git a/llvm/lib/Support/HTTP/HTTPClient.cpp 
b/llvm/lib/Support/HTTP/HTTPClient.cpp
index 6301f86da4086..69780b32d1cf0 100644
--- a/llvm/lib/Support/HTTP/HTTPClient.cpp
+++ b/llvm/lib/Support/HTTP/HTTPClient.cpp
@@ -23,6 +23,9 @@
 #ifdef LLVM_ENABLE_CURL
 #include <curl/curl.h>
 #endif
+#ifdef _WIN32
+#include "llvm/Support/ConvertUTF.h"
+#endif
 
 using namespace llvm;
 
@@ -64,7 +67,7 @@ void HTTPClient::cleanup() {
 void HTTPClient::setTimeout(std::chrono::milliseconds Timeout) {
   if (Timeout < std::chrono::milliseconds(0))
     Timeout = std::chrono::milliseconds(0);
-  curl_easy_setopt(Curl, CURLOPT_TIMEOUT_MS, Timeout.count());
+  curl_easy_setopt(Handle, CURLOPT_TIMEOUT_MS, Timeout.count());
 }
 
 /// CurlHTTPRequest and the curl{Header,Write}Function are implementation
@@ -93,17 +96,17 @@ static size_t curlWriteFunction(char *Contents, size_t 
Size, size_t NMemb,
 HTTPClient::HTTPClient() {
   assert(IsInitialized &&
          "Must call HTTPClient::initialize() at the beginning of main().");
-  if (Curl)
+  if (Handle)
     return;
-  Curl = curl_easy_init();
-  assert(Curl && "Curl could not be initialized");
+  Handle = curl_easy_init();
+  assert(Handle && "Curl could not be initialized");
   // Set the callback hooks.
-  curl_easy_setopt(Curl, CURLOPT_WRITEFUNCTION, curlWriteFunction);
+  curl_easy_setopt(Handle, CURLOPT_WRITEFUNCTION, curlWriteFunction);
   // Detect supported compressed encodings and accept all.
-  curl_easy_setopt(Curl, CURLOPT_ACCEPT_ENCODING, "");
+  curl_easy_setopt(Handle, CURLOPT_ACCEPT_ENCODING, "");
 }
 
-HTTPClient::~HTTPClient() { curl_easy_cleanup(Curl); }
+HTTPClient::~HTTPClient() { curl_easy_cleanup(Handle); }
 
 Error HTTPClient::perform(const HTTPRequest &Request,
                           HTTPResponseHandler &Handler) {
@@ -112,17 +115,17 @@ Error HTTPClient::perform(const HTTPRequest &Request,
                              "Unsupported CURL request method.");
 
   SmallString<128> Url = Request.Url;
-  curl_easy_setopt(Curl, CURLOPT_URL, Url.c_str());
-  curl_easy_setopt(Curl, CURLOPT_FOLLOWLOCATION, Request.FollowRedirects);
+  curl_easy_setopt(Handle, CURLOPT_URL, Url.c_str());
+  curl_easy_setopt(Handle, CURLOPT_FOLLOWLOCATION, Request.FollowRedirects);
 
   curl_slist *Headers = nullptr;
   for (const std::string &Header : Request.Headers)
     Headers = curl_slist_append(Headers, Header.c_str());
-  curl_easy_setopt(Curl, CURLOPT_HTTPHEADER, Headers);
+  curl_easy_setopt(Handle, CURLOPT_HTTPHEADER, Headers);
 
   CurlHTTPRequest CurlRequest(Handler);
-  curl_easy_setopt(Curl, CURLOPT_WRITEDATA, &CurlRequest);
-  CURLcode CurlRes = curl_easy_perform(Curl);
+  curl_easy_setopt(Handle, CURLOPT_WRITEDATA, &CurlRequest);
+  CURLcode CurlRes = curl_easy_perform(Handle);
   curl_slist_free_all(Headers);
   if (CurlRes != CURLE_OK)
     return joinErrors(std::move(CurlRequest.ErrorState),
@@ -134,12 +137,240 @@ Error HTTPClient::perform(const HTTPRequest &Request,
 
 unsigned HTTPClient::responseCode() {
   long Code = 0;
-  curl_easy_getinfo(Curl, CURLINFO_RESPONSE_CODE, &Code);
+  curl_easy_getinfo(Handle, CURLINFO_RESPONSE_CODE, &Code);
   return Code;
 }
 
 #else
 
+#ifdef _WIN32
+#include <windows.h>
+#include <winhttp.h>
+#pragma comment(lib, "winhttp.lib")
+
+namespace {
+
+struct WinHTTPSession {
+  HINTERNET SessionHandle = nullptr;
+  HINTERNET ConnectHandle = nullptr;
+  HINTERNET RequestHandle = nullptr;
+  DWORD ResponseCode = 0;
+
+  ~WinHTTPSession() {
+    if (RequestHandle)
+      WinHttpCloseHandle(RequestHandle);
+    if (ConnectHandle)
+      WinHttpCloseHandle(ConnectHandle);
+    if (SessionHandle)
+      WinHttpCloseHandle(SessionHandle);
+  }
+};
+
+bool parseURL(StringRef Url, std::wstring &Host, std::wstring &Path,
+              INTERNET_PORT &Port, bool &Secure) {
+  // Parse URL: http://host:port/path
+  if (Url.starts_with("https://";)) {
+    Secure = true;
+    Url = Url.drop_front(8);
+  } else if (Url.starts_with("http://";)) {
+    Secure = false;
+    Url = Url.drop_front(7);
+  } else {
+    return false;
+  }
+
+  size_t SlashPos = Url.find('/');
+  StringRef HostPort =
+      (SlashPos != StringRef::npos) ? Url.substr(0, SlashPos) : Url;
+  StringRef PathPart =
+      (SlashPos != StringRef::npos) ? Url.substr(SlashPos) : StringRef("/");
+
+  size_t ColonPos = HostPort.find(':');
+  StringRef HostStr =
+      (ColonPos != StringRef::npos) ? HostPort.substr(0, ColonPos) : HostPort;
+
+  if (!llvm::ConvertUTF8toWide(HostStr, Host))
+    return false;
+  if (!llvm::ConvertUTF8toWide(PathPart, Path))
+    return false;
+
+  if (ColonPos != StringRef::npos) {
+    StringRef PortStr = HostPort.substr(ColonPos + 1);
+    Port = static_cast<INTERNET_PORT>(std::stoi(PortStr.str()));
+  } else {
+    Port = Secure ? INTERNET_DEFAULT_HTTPS_PORT : INTERNET_DEFAULT_HTTP_PORT;
+  }
+
+  return true;
+}
+
+} // namespace
+
+HTTPClient::HTTPClient() : Handle(new WinHTTPSession()) {}
+
+HTTPClient::~HTTPClient() { delete static_cast<WinHTTPSession *>(Handle); }
+
+bool HTTPClient::isAvailable() { return true; }
+
+void HTTPClient::initialize() {
+  if (!IsInitialized) {
+    IsInitialized = true;
+  }
+}
+
+void HTTPClient::cleanup() {
+  if (IsInitialized) {
+    IsInitialized = false;
+  }
+}
+
+void HTTPClient::setTimeout(std::chrono::milliseconds Timeout) {
+  WinHTTPSession *Session = static_cast<WinHTTPSession *>(Handle);
+  if (Session && Session->SessionHandle) {
+    DWORD TimeoutMs = static_cast<DWORD>(Timeout.count());
+    WinHttpSetOption(Session->SessionHandle, WINHTTP_OPTION_CONNECT_TIMEOUT,
+                     &TimeoutMs, sizeof(TimeoutMs));
+    WinHttpSetOption(Session->SessionHandle, WINHTTP_OPTION_RECEIVE_TIMEOUT,
+                     &TimeoutMs, sizeof(TimeoutMs));
+    WinHttpSetOption(Session->SessionHandle, WINHTTP_OPTION_SEND_TIMEOUT,
+                     &TimeoutMs, sizeof(TimeoutMs));
+  }
+}
+
+Error HTTPClient::perform(const HTTPRequest &Request,
+                          HTTPResponseHandler &Handler) {
+  if (Request.Method != HTTPMethod::GET)
+    return createStringError(errc::invalid_argument,
+                             "Only GET requests are supported.");
+  for (const std::string &Header : Request.Headers)
+    if (Header.find("\r") != std::string::npos ||
+        Header.find("\n") != std::string::npos) {
+      return createStringError(errc::invalid_argument,
+                               "Unsafe request can lead to header injection.");
+    }
+
+  WinHTTPSession *Session = static_cast<WinHTTPSession *>(Handle);
+
+  // Parse URL
+  std::wstring Host, Path;
+  INTERNET_PORT Port = 0;
+  bool Secure = false;
+  if (!parseURL(Request.Url, Host, Path, Port, Secure))
+    return createStringError(errc::invalid_argument,
+                             "Invalid URL: " + Request.Url);
+
+  // Create session
+  Session->SessionHandle =
+      WinHttpOpen(L"LLVM-HTTPClient/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
+                  WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
+  if (!Session->SessionHandle)
+    return createStringError(errc::io_error, "Failed to open WinHTTP session");
+
+  // Prevent fallback to TLS 1.0/1.1
+  DWORD SecureProtocols =
+      WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 | 
WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_3;
+  if (!WinHttpSetOption(Session->SessionHandle, 
WINHTTP_OPTION_SECURE_PROTOCOLS,
+                        &SecureProtocols, sizeof(SecureProtocols)))
+    return createStringError(errc::io_error, "Failed to set secure protocols");
+
+  // Use HTTP/2 if available
+  DWORD EnableHttp2 = WINHTTP_PROTOCOL_FLAG_HTTP2;
+  WinHttpSetOption(Session->SessionHandle, WINHTTP_OPTION_ENABLE_HTTP_PROTOCOL,
+                   &EnableHttp2, sizeof(EnableHttp2));
+
+  // Create connection
+  Session->ConnectHandle =
+      WinHttpConnect(Session->SessionHandle, Host.c_str(), Port, 0);
+  if (!Session->ConnectHandle) {
+    return createStringError(errc::io_error,
+                             "Failed to connect to host: " + Request.Url);
+  }
+
+  // Open request
+  DWORD Flags = WINHTTP_FLAG_REFRESH;
+  if (Secure)
+    Flags |= WINHTTP_FLAG_SECURE;
+
+  Session->RequestHandle = WinHttpOpenRequest(
+      Session->ConnectHandle, L"GET", Path.c_str(), nullptr, 
WINHTTP_NO_REFERER,
+      WINHTTP_DEFAULT_ACCEPT_TYPES, Flags);
+  if (!Session->RequestHandle)
+    return createStringError(errc::io_error, "Failed to open HTTP request");
+
+  // Enforce checks that certificate wasn't revoked.
+  DWORD EnableRevocationChecks = WINHTTP_ENABLE_SSL_REVOCATION;
+  if (!WinHttpSetOption(Session->RequestHandle, WINHTTP_OPTION_ENABLE_FEATURE,
+                        &EnableRevocationChecks,
+                        sizeof(EnableRevocationChecks)))
+    return createStringError(errc::io_error,
+                             "Failed to enable certificate revocation checks");
+
+  // Explicitly enforce default validation. This protects against insecure
+  // overrides like SECURITY_FLAG_IGNORE_UNKNOWN_CA.
+  DWORD SecurityFlags = 0;
+  if (!WinHttpSetOption(Session->RequestHandle, WINHTTP_OPTION_SECURITY_FLAGS,
+                        &SecurityFlags, sizeof(SecurityFlags)))
+    return createStringError(errc::io_error,
+                             "Failed to enforce security flags");
+
+  // Add headers
+  for (const std::string &Header : Request.Headers) {
+    std::wstring WideHeader;
+    if (!llvm::ConvertUTF8toWide(Header, WideHeader))
+      continue;
+    WinHttpAddRequestHeaders(Session->RequestHandle, WideHeader.c_str(),
+                             static_cast<DWORD>(WideHeader.length()),
+                             WINHTTP_ADDREQ_FLAG_ADD);
+  }
+
+  // Send request
+  if (!WinHttpSendRequest(Session->RequestHandle, 
WINHTTP_NO_ADDITIONAL_HEADERS,
+                          0, nullptr, 0, 0, 0))
+    return createStringError(errc::io_error, "Failed to send HTTP request");
+
+  // Receive response
+  if (!WinHttpReceiveResponse(Session->RequestHandle, nullptr))
+    return createStringError(errc::io_error, "Failed to receive HTTP 
response");
+
+  // Get response code
+  DWORD CodeSize = sizeof(Session->ResponseCode);
+  if (!WinHttpQueryHeaders(Session->RequestHandle,
+                           WINHTTP_QUERY_STATUS_CODE |
+                               WINHTTP_QUERY_FLAG_NUMBER,
+                           WINHTTP_HEADER_NAME_BY_INDEX, 
&Session->ResponseCode,
+                           &CodeSize, nullptr))
+    Session->ResponseCode = 0;
+
+  // Read response body
+  DWORD BytesAvailable = 0;
+  while (WinHttpQueryDataAvailable(Session->RequestHandle, &BytesAvailable)) {
+    if (BytesAvailable == 0)
+      break;
+
+    std::vector<char> Buffer(BytesAvailable);
+    DWORD BytesRead = 0;
+    if (!WinHttpReadData(Session->RequestHandle, Buffer.data(), BytesAvailable,
+                         &BytesRead))
+      return createStringError(errc::io_error, "Failed to read HTTP response");
+
+    if (BytesRead > 0) {
+      if (Error Err =
+              Handler.handleBodyChunk(StringRef(Buffer.data(), BytesRead)))
+        return Err;
+    }
+  }
+
+  return Error::success();
+}
+
+unsigned HTTPClient::responseCode() {
+  WinHTTPSession *Session = static_cast<WinHTTPSession *>(Handle);
+  return Session ? Session->ResponseCode : 0;
+}
+
+#else // _WIN32
+
+// Non-Windows, non-libcurl stub implementations
 HTTPClient::HTTPClient() = default;
 
 HTTPClient::~HTTPClient() = default;
@@ -161,4 +392,6 @@ unsigned HTTPClient::responseCode() {
   llvm_unreachable("No HTTP Client implementation available.");
 }
 
+#endif // _WIN32
+
 #endif

diff  --git a/llvm/test/tools/llvm-debuginfod-find/headers.test 
b/llvm/test/tools/llvm-debuginfod-find/headers-curl.test
similarity index 100%
rename from llvm/test/tools/llvm-debuginfod-find/headers.test
rename to llvm/test/tools/llvm-debuginfod-find/headers-curl.test

diff  --git a/llvm/test/tools/llvm-debuginfod-find/headers-winhttp.test 
b/llvm/test/tools/llvm-debuginfod-find/headers-winhttp.test
new file mode 100644
index 0000000000000..96a73c6bf373a
--- /dev/null
+++ b/llvm/test/tools/llvm-debuginfod-find/headers-winhttp.test
@@ -0,0 +1,31 @@
+REQUIRES: system-windows
+
+RUN: rm -rf %t
+RUN: mkdir -p %t/debuginfod-cache
+RUN: %python %S/Inputs/capture_req.py llvm-debuginfod-find --debuginfo 0 \
+RUN:   | FileCheck --check-prefix NO-HEADERS %s
+RUN: env DEBUGINFOD_CACHE=%t/debuginfod-cache DEBUGINFOD_HEADERS_FILE=bad 
%python %S/Inputs/capture_req.py \
+RUN:   llvm-debuginfod-find --debuginfo 0 \
+RUN:   | FileCheck --check-prefix NO-HEADERS %s
+RUN: rm -rf %t/debuginfod-cache/*
+RUN: env DEBUGINFOD_CACHE=%t/debuginfod-cache 
DEBUGINFOD_HEADERS_FILE=%S/Inputs/headers %python %S/Inputs/capture_req.py \
+RUN:   llvm-debuginfod-find --debuginfo 0 \
+RUN:   | FileCheck --check-prefix HEADERS %s
+RUN: rm -rf %t/debuginfod-cache/*
+RUN: env DEBUGINFOD_CACHE=%t/debuginfod-cache 
DEBUGINFOD_HEADERS_FILE=%S/Inputs/headers DEBUGINFOD_URLS=fake not 
llvm-debuginfod-find --debuginfo 0 2>&1 \
+RUN:   | FileCheck --check-prefix ERR -DHEADER_FILE=%S/Inputs/headers %s
+
+NO-HEADERS:      User-Agent: LLVM-HTTPClient/1.0
+NO-HEADERS-NEXT: Host: localhost:{{[0-9]+}}
+
+HEADERS:      User-Agent: LLVM-HTTPClient/1.0
+HEADERS-NEXT: A: B
+HEADERS-NEXT: C: D
+HEADERS-NEXT: E: F
+HEADERS-NEXT: hi!$:            j                       k
+HEADERS-NEXT: Host: localhost:{{[0-9]+}}
+
+ERR:      warning: could not parse debuginfod header: [[HEADER_FILE]]:3
+ERR-NEXT: warning: could not parse debuginfod header: [[HEADER_FILE]]:4
+ERR-NEXT: warning: could not parse debuginfod header: [[HEADER_FILE]]:5
+ERR-NEXT: warning: could not parse debuginfod header: [[HEADER_FILE]]:6


        
_______________________________________________
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits

Reply via email to