blob: 69780b32d1cf0d25b509c81c341d77a62383d86a [file] [log] [blame] [edit]
//===--- 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/Support/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;
}
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
#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;
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