[llvm] 39d6bb2 - [lldb] Add HTTP support in SymbolLocatorSymStore (#186986)
via llvm-commits
llvm-commits at lists.llvm.org
Fri Mar 20 04:47:51 PDT 2026
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 <aganea at havenstudios.com>
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
More information about the llvm-commits
mailing list