Using self-signed certificates is the only way forward for testing security features on the HTTPS path. As we don't want to allow any arbitrary certificate, we add a new property that pins a fingerprint and any self-signed certificate is only accepted if it matches this fingerprint.
457 lines
15 KiB
C++
457 lines
15 KiB
C++
//===--- 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 <curl/curl.h>
|
|
#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<HTTPClientCleanup> 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 <windows.h>
|
|
#include <wincrypt.h>
|
|
#include <winhttp.h>
|
|
// 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<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));
|
|
}
|
|
}
|
|
|
|
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<BYTE, 32> 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<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");
|
|
|
|
// 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<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");
|
|
|
|
// 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<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;
|
|
|
|
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
|