Made the request stuff work.

This commit is contained in:
Patrick 2024-10-23 23:55:28 +02:00
parent 77d46d986c
commit 0d00dec8c7
7 changed files with 352 additions and 47 deletions

View File

@ -6,6 +6,7 @@
#include <bit>
#include <cstddef>
#include <cstring>
#include <span>
#include <vector>
#include "../debug/assert.hpp"
@ -50,6 +51,12 @@ public:
template<typename T>
[[nodiscard]] std::span<const T> makeSpan() const;
template<typename TChar = char, typename TTraits = std::char_traits<TChar>>
[[nodiscard]] std::basic_string_view<TChar, TTraits> makeStringView() const ;
template<typename T>
void append(std::span<const T> data);
};
template<typename T>
@ -84,6 +91,12 @@ public:
MIJIN_ASSERT(buffer_, "BufferView::resize(): cannot resize a view without a buffer.");
buffer_->reserve(numElements * sizeof(T));
}
void append(std::span<const T> data)
{
MIJIN_ASSERT(buffer_, "BufferView::resize(): cannot append to a view without a buffer.");
resize(size() + data.size());
std::copy(data.begin(), data.end(), begin() + size() - data.size());
}
[[nodiscard]] inline iterator begin() { return buffer_ ? static_cast<T*>(buffer_->data()) : nullptr; }
[[nodiscard]] inline const_iterator begin() const { return buffer_ ? static_cast<T*>(buffer_->data()) : nullptr; }
@ -127,6 +140,20 @@ std::span<const T> TypelessBuffer::makeSpan() const
std::bit_cast<const T*>(bytes_.data() + bytes_.size())
};
}
template<typename TChar, typename TTraits>
std::basic_string_view<TChar, TTraits> TypelessBuffer::makeStringView() const
{
MIJIN_ASSERT(bytes_.size() % sizeof(TChar) == 0, "Buffer cannot be divided into elements of this char type.");
return {std::bit_cast<const TChar*>(bytes_.data()), bytes_.size() / sizeof(TChar)};
}
template<typename T>
void TypelessBuffer::append(std::span<const T> data)
{
bytes_.resize(bytes_.size() + data.size_bytes());
std::memcpy(bytes_.data() + bytes_.size() - data.size_bytes(), data.data(), data.size_bytes());
}
} // namespace mijin
#endif // !defined(MIJIN_CONTAINER_TYPELESS_BUFFER_HPP_INCLUDED)

View File

@ -132,6 +132,11 @@ public:
return readRaw(&*range.begin(), bytes, {.partial = partial}, outBytesRead);
}
StreamError readRaw(TypelessBuffer& buffer, const ReadOptions& options = {})
{
return readRaw(buffer.data(), buffer.byteSize(), options);
}
template<std::ranges::contiguous_range TRange>
mijin::Task<StreamError> c_readRaw(TRange& range, const ReadOptions& options = {}, std::size_t* outBytesRead = nullptr)
{
@ -139,6 +144,11 @@ public:
return c_readRaw(&*range.begin(), bytes, options, outBytesRead);
}
mijin::Task<StreamError> c_readRaw(TypelessBuffer& buffer, const ReadOptions& options = {})
{
return c_readRaw(buffer.data(), buffer.byteSize(), options);
}
StreamError writeRaw(const void* data, std::size_t bytes)
{
const std::uint8_t* ptr = static_cast<const std::uint8_t*>(data);

View File

@ -4,6 +4,8 @@
#if !defined(MIJIN_NET_CURL_WRAPPERS_HPP_INCLUDED)
#define MIJIN_NET_CURL_WRAPPERS_HPP_INCLUDED 1
#include <map>
#include <string>
#include <curl/curl.h>
#include "./url.hpp"
@ -13,7 +15,6 @@
namespace curl
{
#if !MIJIN_WITH_EXCEPTIONS
struct [[nodiscard]] Error
{
CURLcode code = CURLE_OK;
@ -21,6 +22,8 @@ struct [[nodiscard]] Error
[[nodiscard]]
bool isSuccess() const MIJIN_NOEXCEPT { return code == CURLE_OK; }
};
template<typename TSuccess>
using Result = mijin::ResultBase<TSuccess, Error>;
#define MIJIN_CURL_VERIFY_RESULT(curlResult) \
do \
@ -30,52 +33,60 @@ do \
return Error{curlResult}; \
} \
} while (0)
#define MIJIN_CURL_RETURN_SUCCESS() return Error()
#else
using Error = void;
[[nodiscard]]
std::string curlCodeMessage(CURLcode code)
{
switch (code)
{
default:
return "Unknown CURL error.";
}
}
using curl_write_callback_t = size_t(*)(char*, size_t, size_t, void*);
using curl_read_callback_t = curl_write_callback_t;
class CurlException : public std::runtime_error
class SList
{
private:
CURLcode code_;
curl_slist* next_ = nullptr;
public:
explicit CurlException(CURLcode code) MIJIN_NOEXCEPT: std::runtime_error(curlCodeMessage(code)), code_(code)
{}
SList() MIJIN_NOEXCEPT = default;
SList(const SList&) = delete;
SList(SList&& other) noexcept : next_(std::exchange(other.next_, nullptr)) {}
~SList() MIJIN_NOEXCEPT
{
if (next_ != nullptr)
{
curl_slist_free_all(next_);
}
}
[[nodiscard]]
constexpr CURLcode getCode() const MIJIN_NOEXCEPT
{ return code_; }
SList& operator=(const SList&) = delete;
SList& operator=(SList&& other) MIJIN_NOEXCEPT
{
if (&other == this)
{
return *this;
}
if (next_ != nullptr)
{
curl_slist_free_all(next_);
}
next_ = std::exchange(other.next_, nullptr);
return *this;
}
auto operator<=>(const SList&) const noexcept = default;
void append(const char* str) MIJIN_NOEXCEPT
{
next_ = curl_slist_append(next_, str);
}
friend class CurlEasy;
};
#define MIJIN_CURL_VERIFY_RESULT(curlResult) \
do \
{ \
if ((curlResult) != CURLE_OK) \
{ \
throw CurlException((curlResult)); \
} \
} while (0)
#define MIJIN_CURL_RETURN_SUCCESS() return
#endif
class CurlEasy
{
private:
CURL* handle_ = nullptr;
SList headers_;
public:
CurlEasy() MIJIN_NOEXCEPT = default;
CurlEasy(const CurlEasy&) = delete;
CurlEasy(CurlEasy&& other) MIJIN_NOEXCEPT : handle_(std::exchange(other.handle_, nullptr)) {}
CurlEasy(CurlEasy&& other) MIJIN_NOEXCEPT : handle_(std::exchange(other.handle_, nullptr)), headers_(std::move(other.headers_)) {}
~CurlEasy() MIJIN_NOEXCEPT
{
reset();
@ -90,19 +101,21 @@ public:
}
reset();
handle_ = std::exchange(other.handle_, nullptr);
headers_ = std::move(other.headers_);
return *this;
}
MIJIN_ERROR_BOOL init() MIJIN_THROWS
bool init() MIJIN_NOEXCEPT
{
reset();
handle_ = curl_easy_init();
if (handle_ != nullptr)
if (handle_ == nullptr)
{
MIJIN_THROW_OR_RETURN_STD(false, "Error initializing CURL easy.");
return false;
}
MIJIN_RETURN_SUCCESS(true);
return true;
}
void reset() MIJIN_NOEXCEPT
{
if (handle_)
@ -115,8 +128,7 @@ public:
Error setURL(const char* url) MIJIN_THROWS
{
const CURLcode result = curl_easy_setopt(handle_, CURLOPT_URL, url);
MIJIN_CURL_VERIFY_RESULT(result);
MIJIN_CURL_RETURN_SUCCESS();
return {result};
}
Error setURL(const std::string& url) MIJIN_NOEXCEPT
@ -128,6 +140,123 @@ public:
{
return setURL(url.getBase().c_str());
}
Error setSSLVerifyPeer(bool verify) MIJIN_NOEXCEPT
{
const CURLcode result = curl_easy_setopt(handle_, CURLOPT_SSL_VERIFYPEER, verify);
return {result};
}
Error setSSLVerifyHost(bool verify) MIJIN_NOEXCEPT
{
const CURLcode result = curl_easy_setopt(handle_, CURLOPT_SSL_VERIFYHOST, verify);
return {result};
}
Error setWriteFunction(curl_write_callback_t callback) MIJIN_NOEXCEPT
{
const CURLcode result = curl_easy_setopt(handle_, CURLOPT_WRITEFUNCTION, callback);
return {result};
}
Error setWriteData(void* data) MIJIN_NOEXCEPT
{
const CURLcode result = curl_easy_setopt(handle_, CURLOPT_WRITEDATA, data);
return {result};
}
Error setPostFields(const char* data, bool copy = false) MIJIN_NOEXCEPT
{
const CURLcode result = curl_easy_setopt(handle_, copy ? CURLOPT_COPYPOSTFIELDS : CURLOPT_POSTFIELDS, data);
return {result};
}
Error setUpload(bool upload) MIJIN_NOEXCEPT
{
const CURLcode result = curl_easy_setopt(handle_, CURLOPT_UPLOAD, upload ? 1 : 0);
return {result};
}
Error setInFileSize(curl_off_t size) MIJIN_NOEXCEPT
{
const CURLcode result = curl_easy_setopt(handle_, CURLOPT_INFILESIZE_LARGE, size);
return {result};
}
Error setReadFunction(curl_read_callback_t callback) MIJIN_NOEXCEPT
{
const CURLcode result = curl_easy_setopt(handle_, CURLOPT_READFUNCTION, callback);
return {result};
}
Error setReadData(const void* data) MIJIN_NOEXCEPT
{
const CURLcode result = curl_easy_setopt(handle_, CURLOPT_READDATA, data);
return {result};
}
Error setNoBody(bool nobody) MIJIN_NOEXCEPT
{
const CURLcode result = curl_easy_setopt(handle_, CURLOPT_NOBODY, nobody ? 1 : 0);
return {result};
}
Error setCustomRequest(bool customRequest) MIJIN_NOEXCEPT
{
const CURLcode result = curl_easy_setopt(handle_, CURLOPT_CUSTOMREQUEST, customRequest ? 1 : 0);
return {result};
}
Error setHeaders(SList headers) MIJIN_NOEXCEPT
{
headers_ = std::move(headers);
const CURLcode result = curl_easy_setopt(handle_, CURLOPT_HTTPHEADER, headers_.next_);
return {result};
}
Error perform() MIJIN_NOEXCEPT
{
const CURLcode result = curl_easy_perform(handle_);
return {result};
}
Result<int> getHTTPVersion() MIJIN_NOEXCEPT
{
return getInfo<int, long>(CURLINFO_HTTP_VERSION);
}
Result<int> getStatus() MIJIN_NOEXCEPT
{
return getInfo<int, long>(CURLINFO_RESPONSE_CODE);
}
Result<std::multimap<std::string, std::string>> getResponseHeaders() MIJIN_NOEXCEPT
{
// TODO: how to detect errors?
std::multimap<std::string, std::string> result;
curl_header* header = nullptr;
curl_header* prevHeader = nullptr;
while ((header = curl_easy_nextheader(handle_, CURLH_HEADER, 0, prevHeader)) != nullptr)
{
result.emplace(header->name, header->value);
prevHeader = header;
}
return result;
}
private:
template<typename TResult, typename TInfo = TResult>
Result<TResult> getInfo(CURLINFO info) MIJIN_NOEXCEPT
{
TInfo value = 0;
const CURLcode result = curl_easy_getinfo(handle_, info, &value);
if (result != CURLE_OK)
{
return Error{result};
}
return static_cast<TResult>(value);
}
};
} // namespace curl

View File

@ -154,8 +154,8 @@ Task<StreamResult<HTTPResponse>> HTTPStream::c_readResponse() MIJIN_NOEXCEPT
{
co_return StreamError::PROTOCOL_ERROR;
}
response.content.resize(contentLength);
MIJIN_HTTP_CHECKREAD(base_->c_readRaw(response.content));
response.body.resize(contentLength);
MIJIN_HTTP_CHECKREAD(base_->c_readRaw(response.body));
}
co_return response;

View File

@ -10,11 +10,21 @@
#include "./socket.hpp"
#include "./url.hpp"
#include "../container/boxed_object.hpp"
#include "../container/typeless_buffer.hpp"
#include "../internal/common.hpp"
#include "../io/stream.hpp"
namespace mijin
{
namespace http_method
{
inline constexpr std::string GET = "GET";
inline constexpr std::string POST = "POST";
inline constexpr std::string HEAD = "HEAD";
inline constexpr std::string PUT = "PUT";
inline constexpr std::string DELETE = "DELETE";
}
struct HTTPVersion
{
unsigned major;
@ -30,13 +40,20 @@ struct HTTPRequest
std::string body;
};
struct HTTPRequestOptions
{
std::string method = "GET";
std::multimap<std::string, std::string> headers;
TypelessBuffer body;
};
struct HTTPResponse
{
HTTPVersion version;
unsigned status;
std::string statusMessage;
std::multimap<std::string, std::string> headers;
std::string content;
TypelessBuffer body;
};
class HTTPStream

View File

@ -24,6 +24,12 @@ public:
}
} gCURLGuard [[maybe_unused]];
struct ReadData
{
mijin::TypelessBuffer* buffer;
std::size_t pos;
};
bool initCURL() MIJIN_NOEXCEPT
{
if (gCurlInited)
@ -38,23 +44,139 @@ bool initCURL() MIJIN_NOEXCEPT
gCurlInited = (curl_global_init(0) == CURLE_OK);
return gCurlInited;
}
std::size_t writeCallback(char* ptr, std::size_t /* size */, std::size_t nmemb, void* userdata) noexcept
{
TypelessBuffer& body = *static_cast<TypelessBuffer*>(userdata);
body.append(std::span<const char>(ptr, nmemb));
return nmemb;
}
Task<StreamResult<HTTPResponse>> c_request(const URL& url, HTTPRequest request) MIJIN_NOEXCEPT
std::size_t readCallback(char* ptr, std::size_t size, std::size_t nmemb, void* userdata) noexcept
{
(void) url;
(void) request;
ReadData& data = *static_cast<ReadData*>(userdata);
const std::size_t bytesToRead = std::min(size * nmemb, data.buffer->byteSize() - data.pos);
std::memcpy(ptr, static_cast<std::byte*>(data.buffer->data()) + data.pos, bytesToRead);
data.pos += bytesToRead;
return bytesToRead;
}
}
Task<StreamResult<HTTPResponse>> c_request(const URL& url, HTTPRequestOptions options) MIJIN_NOEXCEPT
{
ReadData readData;
if (!initCURL())
{
co_return StreamError::UNKNOWN_ERROR;
}
curl::CurlEasy easy;
if (!easy.init())
{
co_return StreamError::UNKNOWN_ERROR;
}
easy.setURL(url);
co_return StreamError::UNKNOWN_ERROR;
curl::SList requestHeaders;
for (const auto& [name, value] : options.headers)
{
requestHeaders.append(std::format("{}: {}", name, value).c_str());
}
#define HANDLE_CURL_RESULT(result) \
if (const curl::Error error = (result); !error.isSuccess()) \
{ \
co_return StreamError::UNKNOWN_ERROR; \
}
HANDLE_CURL_RESULT(easy.setHeaders(std::move(requestHeaders)))
if (options.method == http_method::POST)
{
HANDLE_CURL_RESULT(easy.setPostFields(options.body.makeSpan<const char>().data()))
}
else if (options.method == http_method::PUT)
{
HANDLE_CURL_RESULT(easy.setUpload(true))
HANDLE_CURL_RESULT(easy.setInFileSize(options.body.byteSize()));
HANDLE_CURL_RESULT(easy.setReadFunction(&readCallback));
readData.buffer = &options.body;
readData.pos = 0;
HANDLE_CURL_RESULT(easy.setReadData(&readData));
}
else if (options.method == http_method::HEAD)
{
HANDLE_CURL_RESULT(easy.setNoBody(true))
}
else if (options.method != http_method::GET)
{
// different option, let's do our best
HANDLE_CURL_RESULT(easy.setCustomRequest(true))
if (!options.body.empty())
{
HANDLE_CURL_RESULT(easy.setUpload(true))
HANDLE_CURL_RESULT(easy.setInFileSize(options.body.byteSize()));
HANDLE_CURL_RESULT(easy.setReadFunction(&readCallback));
readData.buffer = &options.body;
readData.pos = 0;
HANDLE_CURL_RESULT(easy.setReadData(&readData));
}
}
HANDLE_CURL_RESULT(easy.setURL(url))
HANDLE_CURL_RESULT(easy.setWriteFunction(&writeCallback))
TypelessBuffer body;
HANDLE_CURL_RESULT(easy.setWriteData(&body))
HANDLE_CURL_RESULT(easy.perform())
#undef HANDLE_CURL_RESULT
HTTPResponse response = {
.body = std::move(body)
};
if (const curl::Result<int> httpVersion = easy.getHTTPVersion(); httpVersion.isSuccess())
{
switch (*httpVersion)
{
case CURL_HTTP_VERSION_1_0:
response.version = {1, 0};
break;
case CURL_HTTP_VERSION_1_1:
response.version = {1, 1};
break;
case CURL_HTTP_VERSION_2_0:
response.version = {2, 0};
break;
case CURL_HTTP_VERSION_3:
response.version = {3, 0};
break;
default:
MIJIN_ERROR("Unknown CURL http version returned.");
response.version = {1, 0};
break;
}
}
else
{
co_return StreamError::UNKNOWN_ERROR;
}
if (const curl::Result<int> status = easy.getStatus(); status.isSuccess())
{
response.status = *status;
}
else
{
co_return StreamError::UNKNOWN_ERROR;
}
if (curl::Result<std::multimap<std::string, std::string>> headers = easy.getResponseHeaders(); headers.isSuccess())
{
response.headers = std::move(*headers);
}
else
{
co_return StreamError::UNKNOWN_ERROR;
}
co_return response;
}
} // namespace mijin

View File

@ -9,7 +9,7 @@
namespace mijin
{
Task<StreamResult<HTTPResponse>> c_request(const URL& url, HTTPRequest request = {}) MIJIN_NOEXCEPT;
Task<StreamResult<HTTPResponse>> c_request(const URL& url, HTTPRequestOptions options = {}) MIJIN_NOEXCEPT;
} // namespace mijin
#endif // !defined(MIJIN_NET_REQUST_HPP_INCLUDED)