//===--- HTTPClient.cpp - HTTP client library -----------------------------===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// /// /// \file /// This file defines the implementation of the HTTPClient library for issuing /// HTTP requests and handling the responses. /// //===----------------------------------------------------------------------===// #include "llvm/HTTP/HTTPClient.h" #include "llvm/ADT/APInt.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/Errc.h" #include "llvm/Support/Error.h" #include "llvm/Support/ManagedStatic.h" #include "llvm/Support/MemoryBuffer.h" #ifdef LLVM_ENABLE_CURL #include #endif #ifdef _WIN32 #include "llvm/Support/ConvertUTF.h" #endif using namespace llvm; HTTPRequest::HTTPRequest(StringRef Url) { this->Url = Url.str(); } bool operator==(const HTTPRequest &A, const HTTPRequest &B) { return A.Url == B.Url && A.Method == B.Method && A.FollowRedirects == B.FollowRedirects && A.PinnedCertFingerprint == B.PinnedCertFingerprint; } HTTPResponseHandler::~HTTPResponseHandler() = default; bool HTTPClient::IsInitialized = false; class HTTPClientCleanup { public: ~HTTPClientCleanup() { HTTPClient::cleanup(); } }; ManagedStatic Cleanup; #ifdef LLVM_ENABLE_CURL bool HTTPClient::isAvailable() { return true; } void HTTPClient::initialize() { if (!IsInitialized) { curl_global_init(CURL_GLOBAL_ALL); IsInitialized = true; } } void HTTPClient::cleanup() { if (IsInitialized) { curl_global_cleanup(); IsInitialized = false; } } void HTTPClient::setTimeout(std::chrono::milliseconds Timeout) { if (Timeout < std::chrono::milliseconds(0)) Timeout = std::chrono::milliseconds(0); curl_easy_setopt(Handle, CURLOPT_TIMEOUT_MS, Timeout.count()); } /// CurlHTTPRequest and the curl{Header,Write}Function are implementation /// details used to work with Curl. Curl makes callbacks with a single /// customizable pointer parameter. struct CurlHTTPRequest { CurlHTTPRequest(HTTPResponseHandler &Handler) : Handler(Handler) {} void storeError(Error Err) { ErrorState = joinErrors(std::move(Err), std::move(ErrorState)); } HTTPResponseHandler &Handler; llvm::Error ErrorState = Error::success(); }; static size_t curlWriteFunction(char *Contents, size_t Size, size_t NMemb, CurlHTTPRequest *CurlRequest) { Size *= NMemb; if (Error Err = CurlRequest->Handler.handleBodyChunk(StringRef(Contents, Size))) { CurlRequest->storeError(std::move(Err)); return 0; } return Size; } HTTPClient::HTTPClient() { assert(IsInitialized && "Must call HTTPClient::initialize() at the beginning of main()."); if (Handle) return; Handle = curl_easy_init(); assert(Handle && "Curl could not be initialized"); // Set the callback hooks. curl_easy_setopt(Handle, CURLOPT_WRITEFUNCTION, curlWriteFunction); // Detect supported compressed encodings and accept all. curl_easy_setopt(Handle, CURLOPT_ACCEPT_ENCODING, ""); } HTTPClient::~HTTPClient() { curl_easy_cleanup(Handle); } Error HTTPClient::perform(const HTTPRequest &Request, HTTPResponseHandler &Handler) { if (Request.Method != HTTPMethod::GET) return createStringError(errc::invalid_argument, "Unsupported CURL request method."); SmallString<128> Url = Request.Url; 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(Handle, CURLOPT_HTTPHEADER, Headers); CurlHTTPRequest CurlRequest(Handler); 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), createStringError(errc::io_error, "curl_easy_perform() failed: %s\n", curl_easy_strerror(CurlRes))); return std::move(CurlRequest.ErrorState); } unsigned HTTPClient::responseCode() { long Code = 0; curl_easy_getinfo(Handle, CURLINFO_RESPONSE_CODE, &Code); return Code; } #else #ifdef _WIN32 // We cannot sort these headers alphabetically. // clang-format off #include #include #include // clang-format on 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(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(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(Handle); if (Session && Session->SessionHandle) { DWORD TimeoutMs = static_cast(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)); } } static Error VerifyTLSCertWinHTTP(HINTERNET RequestHandle, const std::string &PinnedFingerprint) { // Decode the expected fingerprint from hex into binary. BYTE Expected[32]; DWORD ExpectedSize = sizeof(Expected); if (!CryptStringToBinaryA( PinnedFingerprint.c_str(), (DWORD)PinnedFingerprint.size(), CRYPT_STRING_HEXRAW, Expected, &ExpectedSize, nullptr, nullptr)) return createStringError(errc::invalid_argument, "Invalid certificate fingerprint format"); // Retrieve the server certificate and compute its SHA-256 hash. PCCERT_CONTEXT CertCtx = nullptr; DWORD CertCtxSize = sizeof(CertCtx); if (!WinHttpQueryOption(RequestHandle, WINHTTP_OPTION_SERVER_CERT_CONTEXT, &CertCtx, &CertCtxSize)) return createStringError(errc::io_error, "Failed to retrieve server certificate"); std::array Actual; DWORD ActualSize = Actual.size(); bool GotHash = CertGetCertificateContextProperty( CertCtx, CERT_SHA256_HASH_PROP_ID, Actual.data(), &ActualSize); CertFreeCertificateContext(CertCtx); if (!GotHash) return createStringError(errc::io_error, "Failed to compute certificate fingerprint"); if (memcmp(Actual.data(), Expected, Actual.size()) != 0) return createStringError(errc::permission_denied, "Certificate fingerprint mismatch"); return Error::success(); } 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(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"); // Disallow redirects in general or HTTPS to HTTP only. DWORD RedirectPolicy = WINHTTP_OPTION_REDIRECT_POLICY_DISALLOW_HTTPS_TO_HTTP; if (!Request.FollowRedirects) RedirectPolicy = WINHTTP_OPTION_REDIRECT_POLICY_NEVER; if (!WinHttpSetOption(Session->SessionHandle, WINHTTP_OPTION_REDIRECT_POLICY, &RedirectPolicy, sizeof(RedirectPolicy))) return createStringError(errc::io_error, "Failed to set redirect policy"); // 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"); DWORD SecurityFlags = 0; if (Secure) { // 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"); // Bypass certificate chain validation with pinned certificates so // that self-signed certificates are accepted at the WinHTTP level. Manual // verification happens right after receiving the response. if (Request.PinnedCertFingerprint) SecurityFlags = (SecurityFlags | SECURITY_FLAG_IGNORE_UNKNOWN_CA); 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(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"); // Verify the server certificate fingerprint if one was pinned. if ((SecurityFlags & SECURITY_FLAG_IGNORE_UNKNOWN_CA) != 0) if (Error Err = VerifyTLSCertWinHTTP(Session->RequestHandle, *Request.PinnedCertFingerprint)) return Err; // 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 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(Handle); return Session ? Session->ResponseCode : 0; } #else // _WIN32 // Non-Windows, non-libcurl stub implementations HTTPClient::HTTPClient() = default; HTTPClient::~HTTPClient() = default; bool HTTPClient::isAvailable() { return false; } void HTTPClient::initialize() {} void HTTPClient::cleanup() {} void HTTPClient::setTimeout(std::chrono::milliseconds Timeout) {} Error HTTPClient::perform(const HTTPRequest &Request, HTTPResponseHandler &Handler) { llvm_unreachable("No HTTP Client implementation available."); } unsigned HTTPClient::responseCode() { llvm_unreachable("No HTTP Client implementation available."); } #endif // _WIN32 #endif