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 <bit>
#include <cstddef> #include <cstddef>
#include <cstring>
#include <span> #include <span>
#include <vector> #include <vector>
#include "../debug/assert.hpp" #include "../debug/assert.hpp"
@ -50,6 +51,12 @@ public:
template<typename T> template<typename T>
[[nodiscard]] std::span<const T> makeSpan() const; [[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> template<typename T>
@ -84,6 +91,12 @@ public:
MIJIN_ASSERT(buffer_, "BufferView::resize(): cannot resize a view without a buffer."); MIJIN_ASSERT(buffer_, "BufferView::resize(): cannot resize a view without a buffer.");
buffer_->reserve(numElements * sizeof(T)); 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 iterator begin() { return buffer_ ? static_cast<T*>(buffer_->data()) : nullptr; }
[[nodiscard]] inline const_iterator begin() const { 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()) 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 } // namespace mijin
#endif // !defined(MIJIN_CONTAINER_TYPELESS_BUFFER_HPP_INCLUDED) #endif // !defined(MIJIN_CONTAINER_TYPELESS_BUFFER_HPP_INCLUDED)

View File

@ -131,6 +131,11 @@ public:
const std::size_t bytes = std::distance(range.begin(), range.end()) * sizeof(std::ranges::range_value_t<TRange>); const std::size_t bytes = std::distance(range.begin(), range.end()) * sizeof(std::ranges::range_value_t<TRange>);
return readRaw(&*range.begin(), bytes, {.partial = partial}, outBytesRead); 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> template<std::ranges::contiguous_range TRange>
mijin::Task<StreamError> c_readRaw(TRange& range, const ReadOptions& options = {}, std::size_t* outBytesRead = nullptr) mijin::Task<StreamError> c_readRaw(TRange& range, const ReadOptions& options = {}, std::size_t* outBytesRead = nullptr)
@ -138,6 +143,11 @@ public:
const std::size_t bytes = std::distance(range.begin(), range.end()) * sizeof(std::ranges::range_value_t<TRange>); const std::size_t bytes = std::distance(range.begin(), range.end()) * sizeof(std::ranges::range_value_t<TRange>);
return c_readRaw(&*range.begin(), bytes, options, outBytesRead); 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) StreamError writeRaw(const void* data, std::size_t bytes)
{ {

View File

@ -4,6 +4,8 @@
#if !defined(MIJIN_NET_CURL_WRAPPERS_HPP_INCLUDED) #if !defined(MIJIN_NET_CURL_WRAPPERS_HPP_INCLUDED)
#define MIJIN_NET_CURL_WRAPPERS_HPP_INCLUDED 1 #define MIJIN_NET_CURL_WRAPPERS_HPP_INCLUDED 1
#include <map>
#include <string>
#include <curl/curl.h> #include <curl/curl.h>
#include "./url.hpp" #include "./url.hpp"
@ -13,7 +15,6 @@
namespace curl namespace curl
{ {
#if !MIJIN_WITH_EXCEPTIONS
struct [[nodiscard]] Error struct [[nodiscard]] Error
{ {
CURLcode code = CURLE_OK; CURLcode code = CURLE_OK;
@ -21,6 +22,8 @@ struct [[nodiscard]] Error
[[nodiscard]] [[nodiscard]]
bool isSuccess() const MIJIN_NOEXCEPT { return code == CURLE_OK; } bool isSuccess() const MIJIN_NOEXCEPT { return code == CURLE_OK; }
}; };
template<typename TSuccess>
using Result = mijin::ResultBase<TSuccess, Error>;
#define MIJIN_CURL_VERIFY_RESULT(curlResult) \ #define MIJIN_CURL_VERIFY_RESULT(curlResult) \
do \ do \
@ -30,52 +33,60 @@ do \
return Error{curlResult}; \ return Error{curlResult}; \
} \ } \
} while (0) } while (0)
#define MIJIN_CURL_RETURN_SUCCESS() return Error()
#else
using Error = void;
[[nodiscard]] using curl_write_callback_t = size_t(*)(char*, size_t, size_t, void*);
std::string curlCodeMessage(CURLcode code) using curl_read_callback_t = curl_write_callback_t;
{
switch (code)
{
default:
return "Unknown CURL error.";
}
}
class CurlException : public std::runtime_error class SList
{ {
private: private:
CURLcode code_; curl_slist* next_ = nullptr;
public: 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]] SList& operator=(const SList&) = delete;
constexpr CURLcode getCode() const MIJIN_NOEXCEPT SList& operator=(SList&& other) MIJIN_NOEXCEPT
{ return code_; } {
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 class CurlEasy
{ {
private: private:
CURL* handle_ = nullptr; CURL* handle_ = nullptr;
SList headers_;
public: public:
CurlEasy() MIJIN_NOEXCEPT = default; CurlEasy() MIJIN_NOEXCEPT = default;
CurlEasy(const CurlEasy&) = delete; 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 ~CurlEasy() MIJIN_NOEXCEPT
{ {
reset(); reset();
@ -90,19 +101,21 @@ public:
} }
reset(); reset();
handle_ = std::exchange(other.handle_, nullptr); handle_ = std::exchange(other.handle_, nullptr);
headers_ = std::move(other.headers_);
return *this; return *this;
} }
MIJIN_ERROR_BOOL init() MIJIN_THROWS bool init() MIJIN_NOEXCEPT
{ {
reset(); reset();
handle_ = curl_easy_init(); 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 void reset() MIJIN_NOEXCEPT
{ {
if (handle_) if (handle_)
@ -115,8 +128,7 @@ public:
Error setURL(const char* url) MIJIN_THROWS Error setURL(const char* url) MIJIN_THROWS
{ {
const CURLcode result = curl_easy_setopt(handle_, CURLOPT_URL, url); const CURLcode result = curl_easy_setopt(handle_, CURLOPT_URL, url);
MIJIN_CURL_VERIFY_RESULT(result); return {result};
MIJIN_CURL_RETURN_SUCCESS();
} }
Error setURL(const std::string& url) MIJIN_NOEXCEPT Error setURL(const std::string& url) MIJIN_NOEXCEPT
@ -128,6 +140,123 @@ public:
{ {
return setURL(url.getBase().c_str()); 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 } // namespace curl

View File

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

View File

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

View File

@ -24,6 +24,12 @@ public:
} }
} gCURLGuard [[maybe_unused]]; } gCURLGuard [[maybe_unused]];
struct ReadData
{
mijin::TypelessBuffer* buffer;
std::size_t pos;
};
bool initCURL() MIJIN_NOEXCEPT bool initCURL() MIJIN_NOEXCEPT
{ {
if (gCurlInited) if (gCurlInited)
@ -38,23 +44,139 @@ bool initCURL() MIJIN_NOEXCEPT
gCurlInited = (curl_global_init(0) == CURLE_OK); gCurlInited = (curl_global_init(0) == CURLE_OK);
return gCurlInited; 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; ReadData& data = *static_cast<ReadData*>(userdata);
(void) request; 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()) if (!initCURL())
{ {
co_return StreamError::UNKNOWN_ERROR; co_return StreamError::UNKNOWN_ERROR;
} }
curl::CurlEasy easy; curl::CurlEasy easy;
if (!easy.init()) if (!easy.init())
{ {
co_return StreamError::UNKNOWN_ERROR; co_return StreamError::UNKNOWN_ERROR;
} }
easy.setURL(url);
curl::SList requestHeaders;
for (const auto& [name, value] : options.headers)
{
requestHeaders.append(std::format("{}: {}", name, value).c_str());
}
co_return StreamError::UNKNOWN_ERROR; #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 } // namespace mijin

View File

@ -9,7 +9,7 @@
namespace mijin 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 } // namespace mijin
#endif // !defined(MIJIN_NET_REQUST_HPP_INCLUDED) #endif // !defined(MIJIN_NET_REQUST_HPP_INCLUDED)