1//===--- HTTPClient.cpp - HTTP client library -----------------------------===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8///
9/// \file
10/// This file defines the implementation of the HTTPClient library for issuing
11/// HTTP requests and handling the responses.
12///
13//===----------------------------------------------------------------------===//
14
15#include "llvm/HTTP/HTTPClient.h"
16
17#include "llvm/ADT/APInt.h"
18#include "llvm/ADT/StringRef.h"
19#include "llvm/Support/Errc.h"
20#include "llvm/Support/Error.h"
21#include "llvm/Support/ManagedStatic.h"
22#include "llvm/Support/MemoryBuffer.h"
23#ifdef LLVM_ENABLE_CURL
24#include <curl/curl.h>
25#endif
26#ifdef _WIN32
27#include "llvm/Support/ConvertUTF.h"
28#endif
29
30using namespace llvm;
31
32HTTPRequest::HTTPRequest(StringRef Url) { this->Url = Url.str(); }
33
34bool operator==(const HTTPRequest &A, const HTTPRequest &B) {
35 return A.Url == B.Url && A.Method == B.Method &&
36 A.FollowRedirects == B.FollowRedirects &&
37 A.PinnedCertFingerprint == B.PinnedCertFingerprint;
38}
39
40HTTPResponseHandler::~HTTPResponseHandler() = default;
41
42bool HTTPClient::IsInitialized = false;
43
44class HTTPClientCleanup {
45public:
46 ~HTTPClientCleanup() { HTTPClient::cleanup(); }
47};
48ManagedStatic<HTTPClientCleanup> Cleanup;
49
50#ifdef LLVM_ENABLE_CURL
51
52bool HTTPClient::isAvailable() { return true; }
53
54void HTTPClient::initialize() {
55 if (!IsInitialized) {
56 curl_global_init(CURL_GLOBAL_ALL);
57 IsInitialized = true;
58 }
59}
60
61void HTTPClient::cleanup() {
62 if (IsInitialized) {
63 curl_global_cleanup();
64 IsInitialized = false;
65 }
66}
67
68void HTTPClient::setTimeout(std::chrono::milliseconds Timeout) {
69 if (Timeout < std::chrono::milliseconds(0))
70 Timeout = std::chrono::milliseconds(0);
71 curl_easy_setopt(Handle, CURLOPT_TIMEOUT_MS, Timeout.count());
72}
73
74/// CurlHTTPRequest and the curl{Header,Write}Function are implementation
75/// details used to work with Curl. Curl makes callbacks with a single
76/// customizable pointer parameter.
77struct CurlHTTPRequest {
78 CurlHTTPRequest(HTTPResponseHandler &Handler) : Handler(Handler) {}
79 void storeError(Error Err) {
80 ErrorState = joinErrors(std::move(Err), std::move(ErrorState));
81 }
82 HTTPResponseHandler &Handler;
83 llvm::Error ErrorState = Error::success();
84};
85
86static size_t curlWriteFunction(char *Contents, size_t Size, size_t NMemb,
87 CurlHTTPRequest *CurlRequest) {
88 Size *= NMemb;
89 if (Error Err =
90 CurlRequest->Handler.handleBodyChunk(StringRef(Contents, Size))) {
91 CurlRequest->storeError(std::move(Err));
92 return 0;
93 }
94 return Size;
95}
96
97HTTPClient::HTTPClient() {
98 assert(IsInitialized &&
99 "Must call HTTPClient::initialize() at the beginning of main().");
100 if (Handle)
101 return;
102 Handle = curl_easy_init();
103 assert(Handle && "Curl could not be initialized");
104 // Set the callback hooks.
105 curl_easy_setopt(Handle, CURLOPT_WRITEFUNCTION, curlWriteFunction);
106 // Detect supported compressed encodings and accept all.
107 curl_easy_setopt(Handle, CURLOPT_ACCEPT_ENCODING, "");
108}
109
110HTTPClient::~HTTPClient() { curl_easy_cleanup(Handle); }
111
112Error HTTPClient::perform(const HTTPRequest &Request,
113 HTTPResponseHandler &Handler) {
114 if (Request.Method != HTTPMethod::GET)
115 return createStringError(errc::invalid_argument,
116 "Unsupported CURL request method.");
117
118 SmallString<128> Url = Request.Url;
119 curl_easy_setopt(Handle, CURLOPT_URL, Url.c_str());
120 curl_easy_setopt(Handle, CURLOPT_FOLLOWLOCATION, Request.FollowRedirects);
121
122 curl_slist *Headers = nullptr;
123 for (const std::string &Header : Request.Headers)
124 Headers = curl_slist_append(Headers, Header.c_str());
125 curl_easy_setopt(Handle, CURLOPT_HTTPHEADER, Headers);
126
127 CurlHTTPRequest CurlRequest(Handler);
128 curl_easy_setopt(Handle, CURLOPT_WRITEDATA, &CurlRequest);
129 CURLcode CurlRes = curl_easy_perform(Handle);
130 curl_slist_free_all(Headers);
131 if (CurlRes != CURLE_OK)
132 return joinErrors(std::move(CurlRequest.ErrorState),
133 createStringError(errc::io_error,
134 "curl_easy_perform() failed: %s\n",
135 curl_easy_strerror(CurlRes)));
136 return std::move(CurlRequest.ErrorState);
137}
138
139unsigned HTTPClient::responseCode() {
140 long Code = 0;
141 curl_easy_getinfo(Handle, CURLINFO_RESPONSE_CODE, &Code);
142 return Code;
143}
144
145#else
146
147#ifdef _WIN32
148
149// We cannot sort these headers alphabetically.
150// clang-format off
151#include <windows.h>
152#include <wincrypt.h>
153#include <winhttp.h>
154// clang-format on
155
156namespace {
157
158struct WinHTTPSession {
159 HINTERNET SessionHandle = nullptr;
160 HINTERNET ConnectHandle = nullptr;
161 HINTERNET RequestHandle = nullptr;
162 DWORD ResponseCode = 0;
163
164 ~WinHTTPSession() {
165 if (RequestHandle)
166 WinHttpCloseHandle(RequestHandle);
167 if (ConnectHandle)
168 WinHttpCloseHandle(ConnectHandle);
169 if (SessionHandle)
170 WinHttpCloseHandle(SessionHandle);
171 }
172};
173
174bool parseURL(StringRef Url, std::wstring &Host, std::wstring &Path,
175 INTERNET_PORT &Port, bool &Secure) {
176 // Parse URL: http://host:port/path
177 if (Url.starts_with("https://")) {
178 Secure = true;
179 Url = Url.drop_front(8);
180 } else if (Url.starts_with("http://")) {
181 Secure = false;
182 Url = Url.drop_front(7);
183 } else {
184 return false;
185 }
186
187 size_t SlashPos = Url.find('/');
188 StringRef HostPort =
189 (SlashPos != StringRef::npos) ? Url.substr(0, SlashPos) : Url;
190 StringRef PathPart =
191 (SlashPos != StringRef::npos) ? Url.substr(SlashPos) : StringRef("/");
192
193 size_t ColonPos = HostPort.find(':');
194 StringRef HostStr =
195 (ColonPos != StringRef::npos) ? HostPort.substr(0, ColonPos) : HostPort;
196
197 if (!llvm::ConvertUTF8toWide(HostStr, Host))
198 return false;
199 if (!llvm::ConvertUTF8toWide(PathPart, Path))
200 return false;
201
202 if (ColonPos != StringRef::npos) {
203 StringRef PortStr = HostPort.substr(ColonPos + 1);
204 Port = static_cast<INTERNET_PORT>(std::stoi(PortStr.str()));
205 } else {
206 Port = Secure ? INTERNET_DEFAULT_HTTPS_PORT : INTERNET_DEFAULT_HTTP_PORT;
207 }
208
209 return true;
210}
211
212} // namespace
213
214HTTPClient::HTTPClient() : Handle(new WinHTTPSession()) {}
215
216HTTPClient::~HTTPClient() { delete static_cast<WinHTTPSession *>(Handle); }
217
218bool HTTPClient::isAvailable() { return true; }
219
220void HTTPClient::initialize() {
221 if (!IsInitialized) {
222 IsInitialized = true;
223 }
224}
225
226void HTTPClient::cleanup() {
227 if (IsInitialized) {
228 IsInitialized = false;
229 }
230}
231
232void HTTPClient::setTimeout(std::chrono::milliseconds Timeout) {
233 WinHTTPSession *Session = static_cast<WinHTTPSession *>(Handle);
234 if (Session && Session->SessionHandle) {
235 DWORD TimeoutMs = static_cast<DWORD>(Timeout.count());
236 WinHttpSetOption(Session->SessionHandle, WINHTTP_OPTION_CONNECT_TIMEOUT,
237 &TimeoutMs, sizeof(TimeoutMs));
238 WinHttpSetOption(Session->SessionHandle, WINHTTP_OPTION_RECEIVE_TIMEOUT,
239 &TimeoutMs, sizeof(TimeoutMs));
240 WinHttpSetOption(Session->SessionHandle, WINHTTP_OPTION_SEND_TIMEOUT,
241 &TimeoutMs, sizeof(TimeoutMs));
242 }
243}
244
245static Error VerifyTLSCertWinHTTP(HINTERNET RequestHandle,
246 const std::string &PinnedFingerprint) {
247 // Decode the expected fingerprint from hex into binary.
248 BYTE Expected[32];
249 DWORD ExpectedSize = sizeof(Expected);
250 if (!CryptStringToBinaryA(
251 PinnedFingerprint.c_str(), (DWORD)PinnedFingerprint.size(),
252 CRYPT_STRING_HEXRAW, Expected, &ExpectedSize, nullptr, nullptr))
253 return createStringError(errc::invalid_argument,
254 "Invalid certificate fingerprint format");
255
256 // Retrieve the server certificate and compute its SHA-256 hash.
257 PCCERT_CONTEXT CertCtx = nullptr;
258 DWORD CertCtxSize = sizeof(CertCtx);
259 if (!WinHttpQueryOption(RequestHandle, WINHTTP_OPTION_SERVER_CERT_CONTEXT,
260 &CertCtx, &CertCtxSize))
261 return createStringError(errc::io_error,
262 "Failed to retrieve server certificate");
263
264 std::array<BYTE, 32> Actual;
265 DWORD ActualSize = Actual.size();
266 bool GotHash = CertGetCertificateContextProperty(
267 CertCtx, CERT_SHA256_HASH_PROP_ID, Actual.data(), &ActualSize);
268 CertFreeCertificateContext(CertCtx);
269 if (!GotHash)
270 return createStringError(errc::io_error,
271 "Failed to compute certificate fingerprint");
272
273 if (memcmp(Actual.data(), Expected, Actual.size()) != 0)
274 return createStringError(errc::permission_denied,
275 "Certificate fingerprint mismatch");
276
277 return Error::success();
278}
279
280Error HTTPClient::perform(const HTTPRequest &Request,
281 HTTPResponseHandler &Handler) {
282 if (Request.Method != HTTPMethod::GET)
283 return createStringError(errc::invalid_argument,
284 "Only GET requests are supported.");
285 for (const std::string &Header : Request.Headers)
286 if (Header.find("\r") != std::string::npos ||
287 Header.find("\n") != std::string::npos) {
288 return createStringError(errc::invalid_argument,
289 "Unsafe request can lead to header injection.");
290 }
291
292 WinHTTPSession *Session = static_cast<WinHTTPSession *>(Handle);
293
294 // Parse URL
295 std::wstring Host, Path;
296 INTERNET_PORT Port = 0;
297 bool Secure = false;
298 if (!parseURL(Request.Url, Host, Path, Port, Secure))
299 return createStringError(errc::invalid_argument,
300 "Invalid URL: " + Request.Url);
301
302 // Create session
303 Session->SessionHandle =
304 WinHttpOpen(L"LLVM-HTTPClient/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
305 WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
306 if (!Session->SessionHandle)
307 return createStringError(errc::io_error, "Failed to open WinHTTP session");
308
309 // Prevent fallback to TLS 1.0/1.1
310 DWORD SecureProtocols =
311 WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 | WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_3;
312 if (!WinHttpSetOption(Session->SessionHandle, WINHTTP_OPTION_SECURE_PROTOCOLS,
313 &SecureProtocols, sizeof(SecureProtocols))) {
314 // Fallback to TLS 1.2 if Windows does not support 1.3.
315 SecureProtocols = WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2;
316 if (!WinHttpSetOption(Session->SessionHandle,
317 WINHTTP_OPTION_SECURE_PROTOCOLS, &SecureProtocols,
318 sizeof(SecureProtocols)))
319 return createStringError(errc::io_error,
320 "Failed to set secure protocols");
321 }
322
323 // Disallow redirects in general or HTTPS to HTTP only.
324 DWORD RedirectPolicy = WINHTTP_OPTION_REDIRECT_POLICY_DISALLOW_HTTPS_TO_HTTP;
325 if (!Request.FollowRedirects)
326 RedirectPolicy = WINHTTP_OPTION_REDIRECT_POLICY_NEVER;
327 if (!WinHttpSetOption(Session->SessionHandle, WINHTTP_OPTION_REDIRECT_POLICY,
328 &RedirectPolicy, sizeof(RedirectPolicy)))
329 return createStringError(errc::io_error, "Failed to set redirect policy");
330
331 // Use HTTP/2 if available
332 DWORD EnableHttp2 = WINHTTP_PROTOCOL_FLAG_HTTP2;
333 WinHttpSetOption(Session->SessionHandle, WINHTTP_OPTION_ENABLE_HTTP_PROTOCOL,
334 &EnableHttp2, sizeof(EnableHttp2));
335
336 // Create connection
337 Session->ConnectHandle =
338 WinHttpConnect(Session->SessionHandle, Host.c_str(), Port, 0);
339 if (!Session->ConnectHandle) {
340 return createStringError(errc::io_error,
341 "Failed to connect to host: " + Request.Url);
342 }
343
344 // Open request
345 DWORD Flags = WINHTTP_FLAG_REFRESH;
346 if (Secure)
347 Flags |= WINHTTP_FLAG_SECURE;
348
349 Session->RequestHandle = WinHttpOpenRequest(
350 Session->ConnectHandle, L"GET", Path.c_str(), nullptr, WINHTTP_NO_REFERER,
351 WINHTTP_DEFAULT_ACCEPT_TYPES, Flags);
352 if (!Session->RequestHandle)
353 return createStringError(errc::io_error, "Failed to open HTTP request");
354
355 DWORD SecurityFlags = 0;
356 if (Secure) {
357 // Enforce checks that certificate wasn't revoked.
358 DWORD EnableRevocationChecks = WINHTTP_ENABLE_SSL_REVOCATION;
359 if (!WinHttpSetOption(Session->RequestHandle, WINHTTP_OPTION_ENABLE_FEATURE,
360 &EnableRevocationChecks,
361 sizeof(EnableRevocationChecks)))
362 return createStringError(
363 errc::io_error, "Failed to enable certificate revocation checks");
364
365 // Bypass certificate chain validation with pinned certificates so
366 // that self-signed certificates are accepted at the WinHTTP level. Manual
367 // verification happens right after receiving the response.
368 if (Request.PinnedCertFingerprint)
369 SecurityFlags = (SecurityFlags | SECURITY_FLAG_IGNORE_UNKNOWN_CA);
370 if (!WinHttpSetOption(Session->RequestHandle, WINHTTP_OPTION_SECURITY_FLAGS,
371 &SecurityFlags, sizeof(SecurityFlags)))
372 return createStringError(errc::io_error,
373 "Failed to enforce security flags");
374 }
375
376 // Add headers
377 for (const std::string &Header : Request.Headers) {
378 std::wstring WideHeader;
379 if (!llvm::ConvertUTF8toWide(Header, WideHeader))
380 continue;
381 WinHttpAddRequestHeaders(Session->RequestHandle, WideHeader.c_str(),
382 static_cast<DWORD>(WideHeader.length()),
383 WINHTTP_ADDREQ_FLAG_ADD);
384 }
385
386 // Send request
387 if (!WinHttpSendRequest(Session->RequestHandle, WINHTTP_NO_ADDITIONAL_HEADERS,
388 0, nullptr, 0, 0, 0))
389 return createStringError(errc::io_error, "Failed to send HTTP request");
390
391 // Receive response
392 if (!WinHttpReceiveResponse(Session->RequestHandle, nullptr))
393 return createStringError(errc::io_error, "Failed to receive HTTP response");
394
395 // Verify the server certificate fingerprint if one was pinned.
396 if ((SecurityFlags & SECURITY_FLAG_IGNORE_UNKNOWN_CA) != 0)
397 if (Error Err = VerifyTLSCertWinHTTP(Session->RequestHandle,
398 *Request.PinnedCertFingerprint))
399 return Err;
400
401 // Get response code
402 DWORD CodeSize = sizeof(Session->ResponseCode);
403 if (!WinHttpQueryHeaders(Session->RequestHandle,
404 WINHTTP_QUERY_STATUS_CODE |
405 WINHTTP_QUERY_FLAG_NUMBER,
406 WINHTTP_HEADER_NAME_BY_INDEX, &Session->ResponseCode,
407 &CodeSize, nullptr))
408 Session->ResponseCode = 0;
409
410 // Read response body
411 DWORD BytesAvailable = 0;
412 while (WinHttpQueryDataAvailable(Session->RequestHandle, &BytesAvailable)) {
413 if (BytesAvailable == 0)
414 break;
415
416 std::vector<char> Buffer(BytesAvailable);
417 DWORD BytesRead = 0;
418 if (!WinHttpReadData(Session->RequestHandle, Buffer.data(), BytesAvailable,
419 &BytesRead))
420 return createStringError(errc::io_error, "Failed to read HTTP response");
421
422 if (BytesRead > 0) {
423 if (Error Err =
424 Handler.handleBodyChunk(StringRef(Buffer.data(), BytesRead)))
425 return Err;
426 }
427 }
428
429 return Error::success();
430}
431
432unsigned HTTPClient::responseCode() {
433 WinHTTPSession *Session = static_cast<WinHTTPSession *>(Handle);
434 return Session ? Session->ResponseCode : 0;
435}
436
437#else // _WIN32
438
439// Non-Windows, non-libcurl stub implementations
440HTTPClient::HTTPClient() = default;
441
442HTTPClient::~HTTPClient() = default;
443
444bool HTTPClient::isAvailable() { return false; }
445
446void HTTPClient::initialize() {}
447
448void HTTPClient::cleanup() {}
449
450void HTTPClient::setTimeout(std::chrono::milliseconds Timeout) {}
451
452Error HTTPClient::perform(const HTTPRequest &Request,
453 HTTPResponseHandler &Handler) {
454 llvm_unreachable("No HTTP Client implementation available.");
455}
456
457unsigned HTTPClient::responseCode() {
458 llvm_unreachable("No HTTP Client implementation available.");
459}
460
461#endif // _WIN32
462
463#endif
464