Index: llvm/include/llvm/Support/HTTPClient.h =================================================================== --- /dev/null +++ llvm/include/llvm/Support/HTTPClient.h @@ -0,0 +1,107 @@ +//===-- llvm/Support/HTTPClient.h - HTTP client library ---*- C++ -------*-===// +// +// 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 contains the declarations of the HTTPClient, HTTPMethod, +/// HTTPResponseHandler, and BufferedHTTPResponseHandler classes, as well as +/// the HTTPResponseBuffer and HTTPRequest structs. +/// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_SUPPORT_HTTP_CLIENT_H +#define LLVM_SUPPORT_HTTP_CLIENT_H + +#include "llvm/Support/Error.h" +#include "llvm/Support/MemoryBuffer.h" + +namespace llvm { + +enum class HTTPMethod { GET }; + +/// A stateless description of an outbound HTTP request. +struct HTTPRequest { + SmallString<128> Url; + HTTPMethod Method = HTTPMethod::GET; + bool FollowRedirects = true; + bool operator==(const HTTPRequest &O) const { + return Url == O.Url && Method == O.Method && + FollowRedirects == O.FollowRedirects; + } + HTTPRequest(const StringRef Url); +}; + +/// A handler for state updates occurring while an HTTPRequest is performed. +/// Can trigger the client to abort the request by returning an Error from any +/// of its methods. +class HTTPResponseHandler { +public: + /// Processes one line of HTTP response headers. + virtual Error handleHeaderLine(StringRef HeaderLine) = 0; + + /// Processes an additional chunk of bytes of the HTTP response body. + virtual Error handleBodyChunk(StringRef BodyChunk) = 0; + + /// Processes the HTTP response status code. + virtual Error handleStatusCode(unsigned Code) = 0; + + virtual ~HTTPResponseHandler(); +}; + +/// An HTTP response status code bundled with a buffer to store the body. +struct HTTPResponseBuffer { + unsigned Code = 0; + std::unique_ptr Body; +}; + +/// A simple handler which writes returned data to an HTTPResponseBuffer. +/// Ignores all headers except the Content-Length, which it uses to +/// allocate an appropriately-sized Body buffer. +class BufferedHTTPResponseHandler : public HTTPResponseHandler { + size_t Offset = 0; + +public: + /// Stores the data received from the HTTP server. + HTTPResponseBuffer ResponseBuffer; + + /// These callbacks store the body and status code in an HTTPResponseBuffer + /// allocated based on Content-Length. The Content-Length header must be + /// handled by handleHeaderLine before any calls to handleBodyChunk. + Error handleHeaderLine(StringRef HeaderLine) override; + Error handleBodyChunk(StringRef BodyChunk) override; + Error handleStatusCode(unsigned Code) override; +}; + +/// A reusable client that can perform HTTPRequests through a network socket. +class HTTPClient { + HTTPClient(); + +public: + /// Returns true only if LLVM has been compiled with a working HTTPClient. + static bool isAvailable(); + + /// Creates and returns an HTTPClient, or an Error if the client cannot be + /// initialized. + static Expected create(); + + /// Performs the Request, passing response data to the Handler. Returns all + /// errors which occur during the request. Aborts if an error is returned by a + /// Handler method. + Error perform(const HTTPRequest &Request, HTTPResponseHandler &Handler); + + /// Performs the Request with the default BufferedHTTPResponseHandler, and + /// returns its HTTPResponseBuffer or an Error. + Expected perform(const HTTPRequest &Request); + + /// Performs an HTTPRequest with the default configuration to make a GET + /// request to the given Url. Returns an HTTPResponseBuffer or an Error. + Expected get(const StringRef Url); +}; + +} // end namespace llvm + +#endif // LLVM_SUPPORT_HTTP_CLIENT_H Index: llvm/lib/Support/CMakeLists.txt =================================================================== --- llvm/lib/Support/CMakeLists.txt +++ llvm/lib/Support/CMakeLists.txt @@ -155,6 +155,7 @@ GlobPattern.cpp GraphWriter.cpp Hashing.cpp + HTTPClient.cpp InitLLVM.cpp InstructionCost.cpp IntEqClasses.cpp Index: llvm/lib/Support/HTTPClient.cpp =================================================================== --- /dev/null +++ llvm/lib/Support/HTTPClient.cpp @@ -0,0 +1,177 @@ +//===-- llvm/Support/HTTPClient.cpp - HTTP client library -------*- C++ -*-===// +// +// 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 methods of the HTTPRequest, HTTPClient, and +/// BufferedHTTPResponseHandler classes. +/// +//===----------------------------------------------------------------------===// + +#include "llvm/Support/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/MemoryBuffer.h" +#ifdef LLVM_ENABLE_CURL +#include +#endif + +using namespace llvm; + +HTTPRequest::HTTPRequest(const StringRef Url) { this->Url = Url.str(); } + +HTTPResponseHandler::~HTTPResponseHandler() = default; + +Expected HTTPClient::perform(const HTTPRequest &Request) { + BufferedHTTPResponseHandler Handler; + if (Error Err = perform(Request, Handler)) + return std::move(Err); + return std::move(Handler.ResponseBuffer); +} + +Expected HTTPClient::get(const StringRef Url) { + HTTPRequest Request(Url); + return perform(Request); +} + +static inline bool parseContentLengthHeader(StringRef LineRef, + size_t &ContentLength) { + // Content-Length is a mandatory header, and the only one we handle. + return LineRef.consume_front("Content-Length: ") && + to_integer(LineRef.trim(), ContentLength, 10); +} + +Error BufferedHTTPResponseHandler::handleHeaderLine(StringRef HeaderLine) { + if (ResponseBuffer.Body) + return Error::success(); + + size_t ContentLength; + if (parseContentLengthHeader(HeaderLine, ContentLength)) + ResponseBuffer.Body = + WritableMemoryBuffer::getNewUninitMemBuffer(ContentLength); + + return Error::success(); +} + +Error BufferedHTTPResponseHandler::handleBodyChunk(StringRef BodyChunk) { + if (!ResponseBuffer.Body) + return createStringError(errc::io_error, + "Unallocated response buffer. HTTP Body data " + "received before Content-Length header."); + if (Offset + BodyChunk.size() > ResponseBuffer.Body->getBufferSize()) + return createStringError(errc::io_error, + "Content size exceeds buffer size."); + memcpy(ResponseBuffer.Body->getBufferStart() + Offset, BodyChunk.data(), + BodyChunk.size()); + Offset += BodyChunk.size(); + return Error::success(); +} + +Error BufferedHTTPResponseHandler::handleStatusCode(unsigned Code) { + ResponseBuffer.Code = Code; + return Error::success(); +} + +#ifdef LLVM_ENABLE_CURL + +bool HTTPClient::isAvailable() { return true; } + +/// CurlHTTPRequest and the curl{HeaderLine,BodyChunk}Hook functions 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 curlHeaderFunction(char *Contents, size_t Size, size_t NMemb, + CurlHTTPRequest *CurlRequest) { + assert(Size == 1 && "The Size passed by libCURL to CURLOPT_HEADERFUNCTION " + "should always be 1."); + if (Error Err = + CurlRequest->Handler.handleHeaderLine(StringRef(Contents, NMemb))) { + CurlRequest->storeError(Err); + return 0; + } + return NMemb; +} + +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(Err); + return 0; + } + return Size; +} + +HTTPClient:: + + Error + HTTPClient::curlInit() { + if (Curl) + return Error::success(); + if (!(Curl = curl_easy_init())) + return createStringError(errc::io_error, "Error initializing curl."); + + // Set the callback hooks on first initialization. + curl_easy_setopt(Curl, CURLOPT_WRITEFUNCTION, curlWriteFunction); + curl_easy_setopt(Curl, CURLOPT_HEADERFUNCTION, curlHeaderFunction); + return Error::success(); +} + +HTTPClient::~HTTPClient() { curl_easy_cleanup(Curl); } + +Error HTTPClient::perform(const HTTPRequest &Request, + HTTPResponseHandler &Handler) { + if (Request.Method != HTTPMethod::GET) + return createStringError(errc::invalid_argument, + "Unsupported CURL request method."); + if (Error Err = curlInit()) + return Err; + + SmallString<128> Url = Request.Url; + curl_easy_setopt(Curl, CURLOPT_URL, Url.c_str()); + curl_easy_setopt(Curl, CURLOPT_FOLLOWLOCATION, Request.FollowRedirects); + + CurlHTTPRequest CurlRequest(Handler); + curl_easy_setopt(Curl, CURLOPT_WRITEDATA, &CurlRequest); + curl_easy_setopt(Curl, CURLOPT_HEADERDATA, &CurlRequest); + CURLcode CurlRes = curl_easy_perform(Curl); + if (CurlRes != CURLE_OK) + return joinErrors(std::move(CurlRequest.ErrorState), + createStringError(errc::io_error, + "curl_easy_perform() failed: %s\n", + curl_easy_strerror(CurlRes))); + if (CurlRequest.ErrorState) + return std::move(CurlRequest.ErrorState); + + unsigned Code; + curl_easy_getinfo(Curl, CURLINFO_RESPONSE_CODE, &Code); + if (Error Err = Handler.handleStatusCode(Code)) + return Err; + return std::move(CurlRequest.ErrorState); +} + +#else + +bool HTTPClient::isAvailable() { return false; } +Error HTTPClient::perform(const HTTPRequest &Request, + HTTPResponseHandler &Handler) { + llvm_unreachable("No HTTP Client implementation available."); +} + +#endif Index: llvm/unittests/Support/CMakeLists.txt =================================================================== --- llvm/unittests/Support/CMakeLists.txt +++ llvm/unittests/Support/CMakeLists.txt @@ -41,6 +41,7 @@ GlobPatternTest.cpp HashBuilderTest.cpp Host.cpp + HTTPClient.cpp IndexedAccessorTest.cpp InstructionCostTest.cpp ItaniumManglingCanonicalizerTest.cpp Index: llvm/unittests/Support/HTTPClient.cpp =================================================================== --- /dev/null +++ llvm/unittests/Support/HTTPClient.cpp @@ -0,0 +1,87 @@ +//===-- llvm/unittest/Support/HTTPClient.cpp - unit tests -------*- C++ -*-===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#include "llvm/Support/HTTPClient.h" +#include "llvm/Support/Errc.h" +#include "llvm/Testing/Support/Error.h" +#include "gtest/gtest.h" + +using namespace llvm; + +TEST(HTTPClientTests, bufferedHTTPResponseHandlerNoContentLengthLifecycleTest) { + BufferedHTTPResponseHandler Handler; + EXPECT_EQ(Handler.ResponseBuffer.Code, 0u); + EXPECT_EQ(Handler.ResponseBuffer.Body, nullptr); + + // A body chunk passed before the content-length header is an error. + EXPECT_THAT_ERROR(Handler.handleBodyChunk("body"), + Failed()); + EXPECT_THAT_ERROR(Handler.handleHeaderLine("a header line"), Succeeded()); + EXPECT_THAT_ERROR(Handler.handleBodyChunk("body"), + Failed()); +} + +TEST(HTTPClientTests, bufferedHTTPResponseHandlerLifecycleTest) { + BufferedHTTPResponseHandler Handler; + EXPECT_THAT_ERROR(Handler.handleHeaderLine("Content-Length: 36\r\n"), + Succeeded()); + + EXPECT_THAT_ERROR(Handler.handleBodyChunk("body:"), Succeeded()); + EXPECT_THAT_ERROR(Handler.handleBodyChunk("this puts the total at 36 chars"), + Succeeded()); + EXPECT_EQ(Handler.ResponseBuffer.Body->MemoryBuffer::getBuffer(), + "body:this puts the total at 36 chars"); + + // Additional content should be rejected by the handler. + EXPECT_THAT_ERROR( + Handler.handleBodyChunk("extra content past the content-length"), + Failed()); + + // Test response code is set. + EXPECT_THAT_ERROR(Handler.handleStatusCode(200u), Succeeded()); + EXPECT_EQ(Handler.ResponseBuffer.Code, 200u); + EXPECT_THAT_ERROR(Handler.handleStatusCode(400u), Succeeded()); + EXPECT_EQ(Handler.ResponseBuffer.Code, 400u); +} + +TEST(HTTPClientTests, bufferedHTTPResponseHandlerZeroContentLengthTest) { + BufferedHTTPResponseHandler Handler; + EXPECT_THAT_ERROR(Handler.handleHeaderLine("Content-Length: 0"), Succeeded()); + EXPECT_NE(Handler.ResponseBuffer.Body, nullptr); + EXPECT_EQ(Handler.ResponseBuffer.Body->getBufferSize(), 0u); + + // All content should be rejected by the handler. + EXPECT_THAT_ERROR(Handler.handleBodyChunk("non-empty body content"), + Failed()); +} + +TEST(HTTPClientTests, bufferedHTTPResponseHandlerMalformedContentLengthTest) { + // Check that several invalid content lengths are ignored. + BufferedHTTPResponseHandler Handler; + EXPECT_EQ(Handler.ResponseBuffer.Body, nullptr); + EXPECT_THAT_ERROR(Handler.handleHeaderLine("Content-Length: fff"), + Succeeded()); + EXPECT_EQ(Handler.ResponseBuffer.Body, nullptr); + + EXPECT_THAT_ERROR(Handler.handleHeaderLine("Content-Length: "), + Succeeded()); + EXPECT_EQ(Handler.ResponseBuffer.Body, nullptr); + + EXPECT_THAT_ERROR(Handler.handleHeaderLine("Content-Length: \0\0\0"), + Succeeded()); + EXPECT_EQ(Handler.ResponseBuffer.Body, nullptr); + + EXPECT_THAT_ERROR(Handler.handleHeaderLine("Content-Length: -11"), + Succeeded()); + EXPECT_EQ(Handler.ResponseBuffer.Body, nullptr); + + // All content should be rejected by the handler because no valid + // Content-Length header has been received. + EXPECT_THAT_ERROR(Handler.handleBodyChunk("non-empty body content"), + Failed()); +}