[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@havenstudios.com>
This commit is contained in:
parent
22f5b8db12
commit
39d6bb2180
@ -13,6 +13,7 @@ add_lldb_library(lldbPluginSymbolLocatorSymStore PLUGIN
|
||||
lldbCore
|
||||
lldbHost
|
||||
lldbSymbol
|
||||
LLVMSupportHTTP
|
||||
)
|
||||
|
||||
add_dependencies(lldbPluginSymbolLocatorSymStore
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 @@ class MockedSymStore:
|
||||
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 @@ class SymStoreLocalTests(TestBase):
|
||||
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 @@ class SymStoreLocalTests(TestBase):
|
||||
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)
|
||||
@ -51,8 +51,8 @@ protected:
|
||||
|
||||
/// 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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
31
llvm/test/tools/llvm-debuginfod-find/headers-winhttp.test
Normal file
31
llvm/test/tools/llvm-debuginfod-find/headers-winhttp.test
Normal file
@ -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
|
||||
Loading…
x
Reference in New Issue
Block a user