diff --git a/llvm/include/llvm/Support/HTTPClient.h b/llvm/include/llvm/Support/HTTPClient.h new file mode 100644 --- /dev/null +++ b/llvm/include/llvm/Support/HTTPClient.h @@ -0,0 +1,113 @@ +//===-- 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; + HTTPRequest(StringRef Url); +}; + +bool operator==(const HTTPRequest &A, const HTTPRequest &B); + +/// 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; + +protected: + ~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 final : 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 { +public: + HTTPClient(); + ~HTTPClient(); + + /// Returns true only if LLVM has been compiled with a working HTTPClient. + static bool isAvailable(); + + /// Must be called at the beginning of a program, while it is a single thread. + static void initialize(); + + /// Must be called at the end of a program, while it is a single thread. + static void cleanup(); + + /// Sets the timeout for the entire request, in milliseconds. A zero or + /// negative value means the request never times out. + void setTimeout(std::chrono::milliseconds Timeout); + + /// 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(StringRef Url); +}; + +} // end namespace llvm + +#endif // LLVM_SUPPORT_HTTP_CLIENT_H diff --git a/llvm/lib/Support/CMakeLists.txt b/llvm/lib/Support/CMakeLists.txt --- a/llvm/lib/Support/CMakeLists.txt +++ b/llvm/lib/Support/CMakeLists.txt @@ -155,6 +155,7 @@ GlobPattern.cpp GraphWriter.cpp Hashing.cpp + HTTPClient.cpp InitLLVM.cpp InstructionCost.cpp IntEqClasses.cpp diff --git a/llvm/lib/Support/HTTPClient.cpp b/llvm/lib/Support/HTTPClient.cpp new file mode 100644 --- /dev/null +++ b/llvm/lib/Support/HTTPClient.cpp @@ -0,0 +1,97 @@ +//===-- 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" + +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; + +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(); +} + +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(StringRef Url) { + HTTPRequest Request(Url); + return perform(Request); +} + +HTTPClient::HTTPClient() = default; + +HTTPClient::~HTTPClient() = default; + +bool HTTPClient::isAvailable() { return false; } + +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."); +} diff --git a/llvm/unittests/Support/CMakeLists.txt b/llvm/unittests/Support/CMakeLists.txt --- a/llvm/unittests/Support/CMakeLists.txt +++ b/llvm/unittests/Support/CMakeLists.txt @@ -41,6 +41,7 @@ GlobPatternTest.cpp HashBuilderTest.cpp Host.cpp + HTTPClient.cpp IndexedAccessorTest.cpp InstructionCostTest.cpp ItaniumManglingCanonicalizerTest.cpp diff --git a/llvm/unittests/Support/HTTPClient.cpp b/llvm/unittests/Support/HTTPClient.cpp new file mode 100644 --- /dev/null +++ b/llvm/unittests/Support/HTTPClient.cpp @@ -0,0 +1,88 @@ +//===-- 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(BufferedHTTPResponseHandler, Lifecycle) { + 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(BufferedHTTPResponseHandler, NoContentLengthLifecycle) { + 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(BufferedHTTPResponseHandler, ZeroContentLength) { + 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(BufferedHTTPResponseHandler, MalformedContentLength) { + // 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); + + using namespace std::string_literals; + EXPECT_THAT_ERROR(Handler.handleHeaderLine("Content-Length: \0\0\0"s), + 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()); +}