Index: llvm/include/llvm/Support/HTTPClient.h =================================================================== --- /dev/null +++ llvm/include/llvm/Support/HTTPClient.h @@ -0,0 +1,96 @@ +//===-- 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 the configuration of an outbound HTTP Request. +/// An HTTPRequest can be performed any number of times by HTTPClient::perform. +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 Twine &Url); +}; + +/// A handler for state updates during the lifecycle of a request performed +/// by HTTPClient::perform. +class HTTPResponseHandler { +public: + /// Processes one line of HTTP response headers. + virtual Expected handleHeaderLine(StringRef HeaderLine) = 0; + /// Processes an additional chunk of bytes of the HTTP response body. + /// Offset indicates the location of the chunk in the response body stream. + virtual Expected handleBodyChunk(StringRef BodyChunk, + size_t Offset) = 0; + /// Processes the HTTP response status code. + virtual void handleStatusCode(unsigned Code) = 0; + virtual ~HTTPResponseHandler() {} +}; + +/// An HTTP response status code bundled with a buffer which can store the +/// body. +struct HTTPResponseBuffer { + unsigned Code = 0; + std::unique_ptr Body; +}; + +/// A simple handler which writes returned data to an HTTPResponseBuffer. +/// This handler discards all headers except the Content-Length, which +/// it uses to allocate an appropriately-sized Body buffer. +class BufferedHTTPResponseHandler : public HTTPResponseHandler { +public: + HTTPResponseBuffer ResponseBuffer; + Expected handleHeaderLine(StringRef HeaderLine) override; + Expected handleBodyChunk(StringRef BodyChunk, size_t Offset) override; + void handleStatusCode(unsigned Code) override; +}; + +/// A reusable client for performing outbound HTTP requests. +class HTTPClient { +public: + /// Performs the Request and passes response data to the Handler. + /// Returns any errors which occur during request. + /// If any of the callbacks to Handler return an Error, a Client + /// should stop performing the request and return an Error. + virtual 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 Twine &Url); + + /// Children of HTTPClient are expected to clean up their resources + /// upon destruction. + virtual ~HTTPClient() {} +}; + +} // 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,80 @@ +//===-- 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; + +#define DEBUG_TYPE "HTTPClient" + +HTTPRequest::HTTPRequest(const Twine &Url) { this->Url = Url.str(); } + +Expected HTTPClient::perform(const HTTPRequest &Request) { + BufferedHTTPResponseHandler Handler; + if (Error Err = perform(Request, Handler)) + return Err; + return std::move(Handler.ResponseBuffer); +} + +Expected HTTPClient::get(const Twine &Url) { + HTTPRequest Request(Url); + return perform(Request); +} + +static bool parseContentLengthHeader(StringRef LineRef, + unsigned long long &ContentLength) { + // Content-Length is a mandatory header, and the only one we handle. + return LineRef.consume_front("Content-Length: ") && + !getAsUnsignedInteger(LineRef.trim(), 10, ContentLength); +} + +Expected +BufferedHTTPResponseHandler::handleHeaderLine(StringRef HeaderLine) { + if (ResponseBuffer.Body) + return HeaderLine.size(); + + unsigned long long ContentLength; + if (parseContentLengthHeader(HeaderLine, ContentLength)) + ResponseBuffer.Body = + WritableMemoryBuffer::getNewUninitMemBuffer(ContentLength); + + return HeaderLine.size(); +} + +Expected +BufferedHTTPResponseHandler::handleBodyChunk(StringRef BodyChunk, + size_t Offset) { + if (!ResponseBuffer.Body) + return createStringError(errc::io_error, + "Unallocated response buffer -- HTTP Body data " + "recieved before Content-Length Header."); + + if (Offset + BodyChunk.size() > ResponseBuffer.Body->getBufferSize()) + return createStringError(errc::io_error, + "Content is larger than response buffer."); + + memcpy(ResponseBuffer.Body->getBufferStart() + Offset, BodyChunk.data(), + BodyChunk.size()); + return BodyChunk.size(); +} + +void BufferedHTTPResponseHandler::handleStatusCode(unsigned Code) { + ResponseBuffer.Code = Code; +} 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,136 @@ +//===-- 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, bufferedHTTPResponseHandlerLifecycleTest) { + { + BufferedHTTPResponseHandler Handler; + // test initial state + EXPECT_EQ(Handler.ResponseBuffer.Code, 0u); + EXPECT_EQ(Handler.ResponseBuffer.Body, nullptr); + + // a body chunk passed before the content-length header should return Error + EXPECT_THAT_EXPECTED(Handler.handleBodyChunk("body", 0), + Failed()); + EXPECT_THAT_EXPECTED(Handler.handleHeaderLine("a header line"), + HasValue(13)); + EXPECT_THAT_EXPECTED(Handler.handleBodyChunk("body", 4), + Failed()); + } + + BufferedHTTPResponseHandler Handler; + EXPECT_THAT_EXPECTED(Handler.handleHeaderLine("Content-Length: 36\r\n"), + HasValue(20)); + EXPECT_THAT_EXPECTED(Handler.handleBodyChunk("body:", 0), HasValue(5)); + EXPECT_THAT_EXPECTED( + Handler.handleBodyChunk("this puts the total at 36 chars", 5), + HasValue(31)); + EXPECT_EQ(memcmp(Handler.ResponseBuffer.Body->getBufferStart(), + "body:this puts the total at 36 chars", + Handler.ResponseBuffer.Body->getBufferSize()), + 0); + + // additional content should be rejected by the handler + EXPECT_THAT_EXPECTED( + Handler.handleBodyChunk("extra content past the content-length", 36), + Failed()); + + // response code handling + Handler.handleStatusCode(200u); + EXPECT_EQ(Handler.ResponseBuffer.Code, 200u); +} + +TEST(HTTPClientTests, bufferedHTTPResponseHandlerZeroContentLengthTest) { + BufferedHTTPResponseHandler Handler; + EXPECT_THAT_EXPECTED(Handler.handleHeaderLine("Content-Length: 0"), + HasValue(17)); + EXPECT_NE(Handler.ResponseBuffer.Body, nullptr); + EXPECT_EQ(Handler.ResponseBuffer.Body->getBufferSize(), 0u); + + // all content should be rejected by the handler + EXPECT_THAT_EXPECTED(Handler.handleBodyChunk("non-empty body content", 0), + Failed()); +} + +TEST(HTTPClientTests, bufferedHTTPResponseHandlerMalformedContentLengthTest) { + // confirm getAsUnsignedInteger behaves as expected + // in particular, getAsUnsignedInteger returns false + // for valid inputs + unsigned long long ContentLength; + EXPECT_EQ(llvm::getAsUnsignedInteger("fff", 10, ContentLength), true); + EXPECT_EQ(llvm::getAsUnsignedInteger("", 10, ContentLength), true); + EXPECT_EQ(llvm::getAsUnsignedInteger("-1", 10, ContentLength), true); + EXPECT_EQ(llvm::getAsUnsignedInteger("0111", 10, ContentLength), false); + EXPECT_EQ(llvm::getAsUnsignedInteger("0", 10, ContentLength), false); + EXPECT_EQ(ContentLength, 0u); + + // check several invalid content lengths are ignored + BufferedHTTPResponseHandler Handler; + EXPECT_EQ(Handler.ResponseBuffer.Body, nullptr); + EXPECT_THAT_EXPECTED(Handler.handleHeaderLine("Content-Length: fff"), + HasValue(19)); + EXPECT_EQ(Handler.ResponseBuffer.Body, nullptr); + EXPECT_THAT_EXPECTED(Handler.handleHeaderLine("Content-Length: "), + HasValue(19)); + EXPECT_EQ(Handler.ResponseBuffer.Body, nullptr); + EXPECT_THAT_EXPECTED( + Handler.handleHeaderLine(StringRef("Content-Length: \0\0\0", 19)), + HasValue(19)); + EXPECT_EQ(Handler.ResponseBuffer.Body, nullptr); + EXPECT_THAT_EXPECTED(Handler.handleHeaderLine("Content-Length: -11"), + HasValue(19)); + EXPECT_EQ(Handler.ResponseBuffer.Body, nullptr); + + // all content should be rejected by the handler because + // no valid content length has been received + EXPECT_THAT_EXPECTED(Handler.handleBodyChunk("non-empty body content", 0), + Failed()); +} + +class HTTPResponseHandlerSimulator : public HTTPResponseHandler { +public: + struct { + size_t BodyBytesTotal = 0; + size_t HeaderLinesTotal = 0; + unsigned StatusCode = 0; + } State; + + Expected handleHeaderLine(StringRef HeaderLine) { + State.HeaderLinesTotal++; + return HeaderLine.size(); + }; + + Expected handleBodyChunk(StringRef BodyChunk, size_t Offset) { + State.BodyBytesTotal += BodyChunk.size(); + return BodyChunk.size(); + }; + + void handleStatusCode(unsigned Code) { State.StatusCode = Code; }; +}; + +TEST(HTTPClientTests, simulatedHTTPResponseHandlerTest) { + HTTPResponseHandlerSimulator Handler; + EXPECT_EQ(Handler.State.HeaderLinesTotal, 0u); + EXPECT_EQ(Handler.State.BodyBytesTotal, 0u); + EXPECT_EQ(Handler.State.StatusCode, 0u); + Handler.handleStatusCode(200); + EXPECT_THAT_EXPECTED(Handler.handleHeaderLine("Content-Length: 0"), + HasValue(17)); + EXPECT_THAT_EXPECTED(Handler.handleHeaderLine("Another-Header: foo"), + HasValue(19)); + EXPECT_THAT_EXPECTED(Handler.handleBodyChunk("1234567", 0), HasValue(7)); + EXPECT_EQ(Handler.State.HeaderLinesTotal, 2u); + EXPECT_EQ(Handler.State.BodyBytesTotal, 7u); + EXPECT_EQ(Handler.State.StatusCode, 200u); +}