[llvm] [Support] Add HTTP Client Support library.

This patch implements a small HTTP client library consisting primarily of the `HTTPRequest`, `HTTPResponseHandler`, and `BufferedHTTPResponseHandler` classes. Unit tests of the `HTTPResponseHandler` and `BufferedHTTPResponseHandler` are included.

Reviewed By: dblaikie

Differential Revision: https://reviews.llvm.org/D112751
This commit is contained in:
Noah Shutty 2021-12-01 23:46:57 +00:00
parent 6703fe25b7
commit 170783f991
5 changed files with 300 additions and 0 deletions

View File

@ -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<WritableMemoryBuffer> 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<HTTPResponseBuffer> 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<HTTPResponseBuffer> get(StringRef Url);
};
} // end namespace llvm
#endif // LLVM_SUPPORT_HTTP_CLIENT_H

View File

@ -155,6 +155,7 @@ add_llvm_component_library(LLVMSupport
GlobPattern.cpp
GraphWriter.cpp
Hashing.cpp
HTTPClient.cpp
InitLLVM.cpp
InstructionCost.cpp
IntEqClasses.cpp

View File

@ -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<HTTPResponseBuffer> HTTPClient::perform(const HTTPRequest &Request) {
BufferedHTTPResponseHandler Handler;
if (Error Err = perform(Request, Handler))
return std::move(Err);
return std::move(Handler.ResponseBuffer);
}
Expected<HTTPResponseBuffer> 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.");
}

View File

@ -41,6 +41,7 @@ add_llvm_unittest(SupportTests
GlobPatternTest.cpp
HashBuilderTest.cpp
Host.cpp
HTTPClient.cpp
IndexedAccessorTest.cpp
InstructionCostTest.cpp
ItaniumManglingCanonicalizerTest.cpp

View File

@ -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<llvm::StringError>());
// 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<llvm::StringError>());
EXPECT_THAT_ERROR(Handler.handleHeaderLine("a header line"), Succeeded());
EXPECT_THAT_ERROR(Handler.handleBodyChunk("body"),
Failed<llvm::StringError>());
}
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<llvm::StringError>());
}
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<llvm::StringError>());
}