From 0d00dec8c72230e7a80ac38539b9bd7cfe4023d6 Mon Sep 17 00:00:00 2001 From: Patrick Wuttke Date: Wed, 23 Oct 2024 23:55:28 +0200 Subject: [PATCH] Made the request stuff work. --- source/mijin/container/typeless_buffer.hpp | 27 +++ source/mijin/io/stream.hpp | 10 + source/mijin/net/curl_wrappers.hpp | 205 +++++++++++++++++---- source/mijin/net/http.cpp | 4 +- source/mijin/net/http.hpp | 19 +- source/mijin/net/request.cpp | 132 ++++++++++++- source/mijin/net/request.hpp | 2 +- 7 files changed, 352 insertions(+), 47 deletions(-) diff --git a/source/mijin/container/typeless_buffer.hpp b/source/mijin/container/typeless_buffer.hpp index 0bc94d3..673c1cd 100644 --- a/source/mijin/container/typeless_buffer.hpp +++ b/source/mijin/container/typeless_buffer.hpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include "../debug/assert.hpp" @@ -50,6 +51,12 @@ public: template [[nodiscard]] std::span makeSpan() const; + + template> + [[nodiscard]] std::basic_string_view makeStringView() const ; + + template + void append(std::span data); }; template @@ -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 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(buffer_->data()) : nullptr; } [[nodiscard]] inline const_iterator begin() const { return buffer_ ? static_cast(buffer_->data()) : nullptr; } @@ -127,6 +140,20 @@ std::span TypelessBuffer::makeSpan() const std::bit_cast(bytes_.data() + bytes_.size()) }; } + +template +std::basic_string_view TypelessBuffer::makeStringView() const +{ + MIJIN_ASSERT(bytes_.size() % sizeof(TChar) == 0, "Buffer cannot be divided into elements of this char type."); + return {std::bit_cast(bytes_.data()), bytes_.size() / sizeof(TChar)}; +} + +template +void TypelessBuffer::append(std::span 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) diff --git a/source/mijin/io/stream.hpp b/source/mijin/io/stream.hpp index 8415f99..17e8515 100644 --- a/source/mijin/io/stream.hpp +++ b/source/mijin/io/stream.hpp @@ -131,6 +131,11 @@ public: const std::size_t bytes = std::distance(range.begin(), range.end()) * sizeof(std::ranges::range_value_t); return readRaw(&*range.begin(), bytes, {.partial = partial}, outBytesRead); } + + StreamError readRaw(TypelessBuffer& buffer, const ReadOptions& options = {}) + { + return readRaw(buffer.data(), buffer.byteSize(), options); + } template mijin::Task 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); return c_readRaw(&*range.begin(), bytes, options, outBytesRead); } + + mijin::Task c_readRaw(TypelessBuffer& buffer, const ReadOptions& options = {}) + { + return c_readRaw(buffer.data(), buffer.byteSize(), options); + } StreamError writeRaw(const void* data, std::size_t bytes) { diff --git a/source/mijin/net/curl_wrappers.hpp b/source/mijin/net/curl_wrappers.hpp index a600da9..8c9f2fc 100644 --- a/source/mijin/net/curl_wrappers.hpp +++ b/source/mijin/net/curl_wrappers.hpp @@ -4,6 +4,8 @@ #if !defined(MIJIN_NET_CURL_WRAPPERS_HPP_INCLUDED) #define MIJIN_NET_CURL_WRAPPERS_HPP_INCLUDED 1 +#include +#include #include #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 +using Result = mijin::ResultBase; #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 getHTTPVersion() MIJIN_NOEXCEPT + { + return getInfo(CURLINFO_HTTP_VERSION); + } + + Result getStatus() MIJIN_NOEXCEPT + { + return getInfo(CURLINFO_RESPONSE_CODE); + } + + Result> getResponseHeaders() MIJIN_NOEXCEPT + { + // TODO: how to detect errors? + std::multimap 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 + Result 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(value); + } }; } // namespace curl diff --git a/source/mijin/net/http.cpp b/source/mijin/net/http.cpp index 52eba0e..139db72 100644 --- a/source/mijin/net/http.cpp +++ b/source/mijin/net/http.cpp @@ -154,8 +154,8 @@ Task> 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; diff --git a/source/mijin/net/http.hpp b/source/mijin/net/http.hpp index 33502bc..89f2390 100644 --- a/source/mijin/net/http.hpp +++ b/source/mijin/net/http.hpp @@ -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 headers; + TypelessBuffer body; +}; + struct HTTPResponse { HTTPVersion version; unsigned status; std::string statusMessage; std::multimap headers; - std::string content; + TypelessBuffer body; }; class HTTPStream diff --git a/source/mijin/net/request.cpp b/source/mijin/net/request.cpp index 84f6168..d8e0bae 100644 --- a/source/mijin/net/request.cpp +++ b/source/mijin/net/request.cpp @@ -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(userdata); + body.append(std::span(ptr, nmemb)); + return nmemb; } -Task> 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(userdata); + const std::size_t bytesToRead = std::min(size * nmemb, data.buffer->byteSize() - data.pos); + std::memcpy(ptr, static_cast(data.buffer->data()) + data.pos, bytesToRead); + data.pos += bytesToRead; + + return bytesToRead; +} +} + +Task> 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); + + 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().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 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 status = easy.getStatus(); status.isSuccess()) + { + response.status = *status; + } + else + { + co_return StreamError::UNKNOWN_ERROR; + } + + if (curl::Result> headers = easy.getResponseHeaders(); headers.isSuccess()) + { + response.headers = std::move(*headers); + } + else + { + co_return StreamError::UNKNOWN_ERROR; + } + + co_return response; } } // namespace mijin diff --git a/source/mijin/net/request.hpp b/source/mijin/net/request.hpp index fd76b4a..f5e166a 100644 --- a/source/mijin/net/request.hpp +++ b/source/mijin/net/request.hpp @@ -9,7 +9,7 @@ namespace mijin { -Task> c_request(const URL& url, HTTPRequest request = {}) MIJIN_NOEXCEPT; +Task> c_request(const URL& url, HTTPRequestOptions options = {}) MIJIN_NOEXCEPT; } // namespace mijin #endif // !defined(MIJIN_NET_REQUST_HPP_INCLUDED)