remove old web utils

This commit is contained in:
dankmeme01 2024-06-07 11:20:39 +02:00
parent 8ea17f3d38
commit b129808723
7 changed files with 445 additions and 1552 deletions
loader
include/Geode/utils
src

View file

@ -1,390 +1,134 @@
#pragma once
#include "../DefaultInclude.hpp"
#include "MiniFunction.hpp"
#include <Geode/loader/Loader.hpp> // another great circular dependency fix
#include <matjson.hpp>
#include "Result.hpp"
#include "general.hpp"
#include <filesystem>
#include "Task.hpp"
#include <chrono>
#include <optional>
namespace geode::utils::web {
GEODE_DLL void openLinkInBrowser(std::string const& url);
// https://curl.se/libcurl/c/CURLOPT_HTTPAUTH.html
namespace http_auth {
constexpr static long BASIC = 0x0001;
constexpr static long DIGEST = 0x0002;
constexpr static long DIGEST_IE = 0x0004;
constexpr static long BEARER = 0x0008;
constexpr static long NEGOTIATE = 0x0010;
constexpr static long NTLM = 0x0020;
constexpr static long NTLM_WB = 0x0040;
constexpr static long ANY = 0x0080;
constexpr static long ANYSAFE = 0x0100;
constexpr static long ONLY = 0x0200;
constexpr static long AWS_SIGV4 = 0x0400;
}
using FileProgressCallback = utils::MiniFunction<bool(double, double)>;
// https://curl.se/libcurl/c/CURLOPT_PROXYTYPE.html
enum class ProxyType {
HTTP, // HTTP
HTTPS, // HTTPS
HTTPS2, // HTTPS (attempt to use HTTP/2)
SOCKS4, // Socks4
SOCKS4A, // Socks4 with hostname resolution
SOCKS5, // Socks5
SOCKS5H, // Socks5 with hostname resolution
};
/**
* Synchronously fetch data from the internet
* @param url URL to fetch
* @returns Returned data as bytes, or error on error
*/
[[deprecated("Use the WebRequest class from the web2.hpp header instead")]]
GEODE_DLL Result<ByteVector> fetchBytes(std::string const& url);
struct ProxyOpts {
std::string address; // Proxy address/FQDN
std::optional<std::uint16_t> port; // Proxy port
ProxyType type = ProxyType::HTTP; // Proxy type
long auth = http_auth::BASIC; // HTTP proxy auth method
std::string username; // Proxy username
std::string password; // Proxy password
bool tunneling = false; // Enable HTTP tunneling
bool certVerification = true; // Enable HTTPS certificate verification
};
/**
* Synchronously fetch data from the internet
* @param url URL to fetch
* @returns Returned data as string, or error on error
*/
[[deprecated("Use the WebRequest class from the web2.hpp header instead")]]
GEODE_DLL Result<std::string> fetch(std::string const& url);
class WebRequest;
/**
* Syncronously download a file from the internet
* @param url URL to fetch
* @param into Path to download file into
* @param prog Progress function; first parameter is bytes downloaded so
* far, and second is total bytes to download. Return true to continue
* downloading, and false to interrupt. Note that interrupting does not
* automatically remove the file that was being downloaded
* @returns Returned data as JSON, or error on error
*/
[[deprecated("Use the WebRequest class from the web2.hpp header instead")]]
GEODE_DLL Result<> fetchFile(
std::string const& url, std::filesystem::path const& into, FileProgressCallback prog = nullptr
);
/**
* Synchronously fetch data from the internet and parse it as JSON
* @param url URL to fetch
* @returns Returned data as JSON, or error on error
*/
[[deprecated("Use the WebRequest class from the web2.hpp header instead")]]
GEODE_DLL Result<matjson::Value> fetchJSON(std::string const& url);
class SentAsyncWebRequest;
template <class T>
class AsyncWebResult;
class AsyncWebResponse;
class AsyncWebRequest;
using AsyncProgress = utils::MiniFunction<void(SentAsyncWebRequest&, double, double)>;
using AsyncExpect = utils::MiniFunction<void(std::string const&)>;
using AsyncExpectCode = utils::MiniFunction<void(std::string const&, int)>;
using AsyncThen = utils::MiniFunction<void(SentAsyncWebRequest&, ByteVector const&)>;
using AsyncCancelled = utils::MiniFunction<void(SentAsyncWebRequest&)>;
/**
* A handle to an in-progress sent asynchronous web request. Use this to
* cancel the request / query information about it
*/
class GEODE_DLL [[deprecated("Use the WebRequest class from the web2.hpp header instead")]] SentAsyncWebRequest {
class GEODE_DLL WebResponse final {
private:
class Impl;
std::shared_ptr<Impl> m_impl;
template <class T>
friend class AsyncWebResult;
friend class AsyncWebRequest;
void pause();
void resume();
void error(std::string const& error, int code);
void doCancel();
friend class WebRequest;
public:
/**
* Do not call these manually.
*/
SentAsyncWebRequest();
~SentAsyncWebRequest();
static std::shared_ptr<SentAsyncWebRequest> create(AsyncWebRequest const&, std::string const& id);
// Must be default-constructible for use in Promise
WebResponse();
/**
* Cancel the request. Cleans up any downloaded files, but if you run
* extra code in `then`, you will have to clean it up manually in
* `cancelled`
*/
void cancel();
/**
* Check if the request is finished
*/
bool finished() const;
bool ok() const;
int code() const;
/**
* Get the response header of the request. Only valid after the request
*/
std::string getResponseHeader(std::string_view header) const;
Result<std::string> string() const;
Result<matjson::Value> json() const;
ByteVector data() const;
std::vector<std::string> headers() const;
std::optional<std::string> header(std::string_view name) const;
};
using SentAsyncWebRequestHandle = std::shared_ptr<SentAsyncWebRequest>;
template <class T>
using DataConverter = Result<T> (*)(ByteVector const&);
/**
* An asynchronous, thread-safe web request. Downloads data from the
* internet without slowing the main thread. All callbacks are run in the
* GD thread, so interacting with the Cocos2d UI is perfectly safe
*/
class GEODE_DLL [[deprecated("Use the WebRequest class from the web2.hpp header instead")]] AsyncWebRequest {
class GEODE_DLL WebProgress final {
private:
class Impl;
std::unique_ptr<Impl> m_impl;
template <class T>
friend class AsyncWebResult;
friend class SentAsyncWebRequest;
friend class AsyncWebResponse;
friend class SentAsyncWebRequest::Impl;
std::shared_ptr<Impl> m_impl;
AsyncWebRequest& setThen(AsyncThen);
friend class WebRequest;
public:
/**
* An asynchronous, thread-safe web request. Downloads data from the
* internet without slowing the main thread. All callbacks are run in the
* GD thread, so interacting with the Cocos2d UI is perfectly safe
*/
AsyncWebRequest();
~AsyncWebRequest();
// Must be default-constructible for use in Promise
WebProgress();
/**
* URL to fetch from the internet asynchronously
* @param url URL of the data to download. Redirects will be
* automatically followed
* @returns An AsyncWebResponse object
*/
AsyncWebResponse fetch(std::string_view const url);
/**
* Begin the web request. It's not always necessary to call this as the
* destructor calls it automatically, but if you need access to the
* handle of the sent request, use this
* @returns Handle to the sent web request
*/
SentAsyncWebRequestHandle send();
size_t downloaded() const;
size_t downloadTotal() const;
std::optional<float> downloadProgress() const;
/**
* Shorthand for a GET request to the given url.
* Same return value as fetch.
*/
AsyncWebResponse get(std::string_view const url);
/**
* Shorthand for a POST request to the given url.
* Same return value as fetch.
*/
AsyncWebResponse post(std::string_view const url);
/**
* Shorthand for a PUT request to the given url.
* Same return value as fetch.
*/
AsyncWebResponse put(std::string_view const url);
/**
* Shorthand for a PATCH request to the given url.
* Same return value as fetch.
*/
AsyncWebResponse patch(std::string_view const url);
/**
* If you only want one instance of this web request to run (for example,
* you're downloading some global data for a manager), then use this
* to specify a Join ID. If another request with the same ID is
* already running, this request's callbacks will be appended to the
* existing one instead of creating a new request
* @param requestID The Join ID of the request. Can be anything,
* recommended to be something unique
* @returns Same AsyncWebRequest
*/
AsyncWebRequest& join(std::string_view const requestID);
// Request parameters
/**
* In order to specify a http header to the request, give it here.
* Can be called more than once.
*/
AsyncWebRequest& header(std::string_view const header);
/**
* In order to specify a http header to the request, give it here.
* Can be called more than once.
*/
AsyncWebRequest& header(std::string_view const headerName, std::string_view const headerValue);
/**
* In order to specify an user agent to the request, give it here.
*/
AsyncWebRequest& userAgent(std::string_view const userAgent);
/**
* Sets the Content-Type header to the specified value.
*/
AsyncWebRequest& contentType(std::string_view const contentType);
/**
* Specify that the request is a POST request.
*/
AsyncWebRequest& postRequest();
/**
* Specify the HTTP method for the request, like PUT and DELETE.
*/
AsyncWebRequest& method(std::string_view const method);
/**
* Specify the raw request body to send with the request.
* Content type is unchanged.
*/
AsyncWebRequest& bodyRaw(std::string_view const content);
/**
* Specify a json request body. Additionally sets the content type to
* application/json.
*/
AsyncWebRequest& body(matjson::Value const& value);
/**
* Specify a timeout, in seconds, in which the request will fail.
*/
AsyncWebRequest& timeout(std::chrono::seconds seconds);
// Callbacks
/**
* Specify a callback to run if the download fails. The callback is
* always ran in the GD thread, so interacting with UI is safe
* @param handler Callback to run if the download fails
* @returns Same AsyncWebRequest
*/
AsyncWebRequest& expect(AsyncExpect handler);
/**
* Specify a callback to run if the download fails. The callback is
* always ran in the GD thread, so interacting with UI is safe
* @param handler Callback to run if the download fails
* @returns Same AsyncWebRequest
*/
AsyncWebRequest& expect(AsyncExpectCode handler);
/**
* Specify a callback to run when the download progresses. The callback is
* always ran in the GD thread, so interacting with UI is safe
* @param handler Callback to run when the download progresses
* @returns Same AsyncWebRequest
*/
AsyncWebRequest& progress(AsyncProgress handler);
/**
* Specify a callback to run if the download is cancelled. The callback is
* always ran in the GD thread, so interacting with UI is safe. Web
* requests may be cancelled after they are finished (for example, if
* downloading files in bulk and one fails). In that case, handle
* freeing up the results of `then` in this handler
* @param handler Callback to run if the download is cancelled
* @returns Same AsyncWebRequest
*/
AsyncWebRequest& cancelled(AsyncCancelled handler);
size_t uploaded() const;
size_t uploadTotal() const;
std::optional<float> uploadProgress() const;
};
template <class T>
class [[deprecated("Use the WebRequest class from the web2.hpp header instead")]] AsyncWebResult {
using WebTask = Task<WebResponse, WebProgress>;
class GEODE_DLL WebRequest final {
private:
AsyncWebRequest& m_request;
DataConverter<T> m_converter;
class Impl;
AsyncWebResult(AsyncWebRequest& request, DataConverter<T> converter) :
m_request(request), m_converter(converter) {}
friend class AsyncWebResponse;
std::shared_ptr<Impl> m_impl;
public:
/**
* Specify a callback to run after a download is finished. Runs in the
* GD thread, so interacting with UI is safe
* @param handle Callback to run
* @returns The original AsyncWebRequest, where you can specify more
* aspects about the request like failure and progress callbacks
*/
AsyncWebRequest& then(utils::MiniFunction<void(T)> handle);
/**
* Specify a callback to run after a download is finished. Runs in the
* GD thread, so interacting with UI is safe
* @param handle Callback to run
* @returns The original AsyncWebRequest, where you can specify more
* aspects about the request like failure and progress callbacks
*/
AsyncWebRequest& then(utils::MiniFunction<void(SentAsyncWebRequest&, T)> handle);
};
WebRequest();
~WebRequest();
class GEODE_DLL [[deprecated("Use the WebRequest class from the web2.hpp header instead")]] AsyncWebResponse {
private:
AsyncWebRequest& m_request;
WebTask send(std::string_view method, std::string_view url);
WebTask post(std::string_view url);
WebTask get(std::string_view url);
WebTask put(std::string_view url);
WebTask patch(std::string_view url);
inline AsyncWebResponse(AsyncWebRequest& request) : m_request(request) {}
WebRequest& header(std::string_view name, std::string_view value);
WebRequest& param(std::string_view name, std::string_view value);
friend class AsyncWebRequest;
public:
/**
* Download into a stream. Make sure the stream lives for the entire
* duration of the request. If you want to download a file, use the
* `std::filesystem::path` overload of `into` instead
* @param stream Stream to download into. Make sure it lives long
* enough, otherwise the web request will crash
* @returns AsyncWebResult, where you can specify the `then` action for
* after the download is finished. The result has a `std::monostate`
* template parameter, as it can be assumed you know what you passed
* into `into`
*/
AsyncWebResult<std::monostate> into(std::ostream& stream);
/**
* Download into a file
* @param path File to download into. If it already exists, it will
* be overwritten.
* @returns AsyncWebResult, where you can specify the `then` action for
* after the download is finished. The result has a `std::monostate`
* template parameter, as it can be assumed you know what you passed
* into `into`
*/
AsyncWebResult<std::monostate> into(std::filesystem::path const& path);
/**
* Download into memory as a string
* @returns AsyncWebResult, where you can specify the `then` action for
* after the download is finished
*/
AsyncWebResult<std::string> text();
/**
* Download into memory as a byte array
* @returns AsyncWebResult, where you can specify the `then` action for
* after the download is finished
*/
AsyncWebResult<ByteVector> bytes();
/**
* Download into memory as JSON
* @returns AsyncWebResult, where you can specify the `then` action for
* after the download is finished
*/
AsyncWebResult<matjson::Value> json();
/**
* Download into memory as a custom type. The data will first be
* downloaded into memory as a byte array, and then converted using
* the specified converter function
* @param converter Function that converts the data from a byte array
* to the desired type
* @returns AsyncWebResult, where you can specify the `then` action for
* after the download is finished
*/
template <class T>
AsyncWebResult<T> as(DataConverter<T> converter) {
return AsyncWebResult(m_request, converter);
template <std::integral T>
WebRequest& param(std::string_view name, T value) {
return this->param(name, std::to_string(value));
}
WebRequest& userAgent(std::string_view name);
WebRequest& timeout(std::chrono::seconds time);
WebRequest& certVerification(bool enabled);
WebRequest& CABundleContent(std::string_view content);
WebRequest& proxyOpts(ProxyOpts const& proxyOpts);
WebRequest& body(ByteVector raw);
WebRequest& bodyString(std::string_view str);
WebRequest& bodyJSON(matjson::Value const& json);
};
template <class T>
AsyncWebRequest& AsyncWebResult<T>::then(utils::MiniFunction<void(T)> handle) {
return m_request.setThen([converter = m_converter,
handle](SentAsyncWebRequest& req, ByteVector const& arr) {
auto conv = converter(arr);
if (conv) {
handle(conv.unwrap());
}
else {
req.error("Unable to convert value: " + conv.unwrapErr(), -1);
}
});
}
template <class T>
AsyncWebRequest& AsyncWebResult<T>::then(utils::MiniFunction<void(SentAsyncWebRequest&, T)> handle) {
return m_request.setThen([converter = m_converter,
handle](SentAsyncWebRequest& req, ByteVector const& arr) {
auto conv = converter(arr);
if (conv) {
handle(req, conv.value());
}
else {
req.error("Unable to convert value: " + conv.error(), -1);
}
});
}
}

View file

@ -1,132 +0,0 @@
#pragma once
#include <Geode/loader/Loader.hpp> // another great circular dependency fix
#include <matjson.hpp>
#include "Result.hpp"
#include "Task.hpp"
#include <chrono>
#include <optional>
namespace geode::utils::web {
// https://curl.se/libcurl/c/CURLOPT_HTTPAUTH.html
namespace http_auth {
constexpr static long BASIC = 0x0001;
constexpr static long DIGEST = 0x0002;
constexpr static long DIGEST_IE = 0x0004;
constexpr static long BEARER = 0x0008;
constexpr static long NEGOTIATE = 0x0010;
constexpr static long NTLM = 0x0020;
constexpr static long NTLM_WB = 0x0040;
constexpr static long ANY = 0x0080;
constexpr static long ANYSAFE = 0x0100;
constexpr static long ONLY = 0x0200;
constexpr static long AWS_SIGV4 = 0x0400;
}
// https://curl.se/libcurl/c/CURLOPT_PROXYTYPE.html
enum class ProxyType {
HTTP, // HTTP
HTTPS, // HTTPS
HTTPS2, // HTTPS (attempt to use HTTP/2)
SOCKS4, // Socks4
SOCKS4A, // Socks4 with hostname resolution
SOCKS5, // Socks5
SOCKS5H, // Socks5 with hostname resolution
};
struct ProxyOpts {
std::string address; // Proxy address/FQDN
std::optional<std::uint16_t> port; // Proxy port
ProxyType type = ProxyType::HTTP; // Proxy type
long auth = http_auth::BASIC; // HTTP proxy auth method
std::string username; // Proxy username
std::string password; // Proxy password
bool tunneling = false; // Enable HTTP tunneling
bool certVerification = true; // Enable HTTPS certificate verification
};
class WebRequest;
class GEODE_DLL WebResponse final {
private:
class Impl;
std::shared_ptr<Impl> m_impl;
friend class WebRequest;
public:
// Must be default-constructible for use in Promise
WebResponse();
bool ok() const;
int code() const;
Result<std::string> string() const;
Result<matjson::Value> json() const;
ByteVector data() const;
std::vector<std::string> headers() const;
std::optional<std::string> header(std::string_view name) const;
};
class GEODE_DLL WebProgress final {
private:
class Impl;
std::shared_ptr<Impl> m_impl;
friend class WebRequest;
public:
// Must be default-constructible for use in Promise
WebProgress();
size_t downloaded() const;
size_t downloadTotal() const;
std::optional<float> downloadProgress() const;
size_t uploaded() const;
size_t uploadTotal() const;
std::optional<float> uploadProgress() const;
};
using WebTask = Task<WebResponse, WebProgress>;
class GEODE_DLL WebRequest final {
private:
class Impl;
std::shared_ptr<Impl> m_impl;
public:
WebRequest();
~WebRequest();
WebTask send(std::string_view method, std::string_view url);
WebTask post(std::string_view url);
WebTask get(std::string_view url);
WebTask put(std::string_view url);
WebTask patch(std::string_view url);
WebRequest& header(std::string_view name, std::string_view value);
WebRequest& param(std::string_view name, std::string_view value);
template <std::integral T>
WebRequest& param(std::string_view name, T value) {
return this->param(name, std::to_string(value));
}
WebRequest& userAgent(std::string_view name);
WebRequest& timeout(std::chrono::seconds time);
WebRequest& certVerification(bool enabled);
WebRequest& CABundleContent(std::string_view content);
WebRequest& proxyOpts(ProxyOpts const& proxyOpts);
WebRequest& body(ByteVector raw);
WebRequest& bodyString(std::string_view str);
WebRequest& bodyJSON(matjson::Value const& json);
};
}

View file

@ -1,5 +1,5 @@
#include "updater.hpp"
#include <Geode/utils/web2.hpp>
#include <Geode/utils/web.hpp>
#include <resources.hpp>
#include <hash.hpp>
#include <utility>

View file

@ -2,7 +2,7 @@
#include "Geode/utils/VersionInfo.hpp"
#include <Geode/DefaultInclude.hpp>
#include <Geode/utils/web2.hpp>
#include <Geode/utils/web.hpp>
#include <Geode/loader/SettingEvent.hpp>
#include <matjson.hpp>
#include <vector>

View file

@ -4,7 +4,7 @@
#include <Geode/modify/MenuLayer.hpp>
#include <Geode/ui/Popup.hpp>
#include <Geode/ui/BasedButtonSprite.hpp>
#include <Geode/utils/web2.hpp>
#include <Geode/utils/web.hpp>
#include <server/Server.hpp>
#include "../sources/ModListSource.hpp"

File diff suppressed because it is too large Load diff

View file

@ -1,442 +0,0 @@
#include "Geode/utils/general.hpp"
#include <matjson.hpp>
#define CURL_STATICLIB
#include <curl/curl.h>
#include <ca_bundle.h>
#include <Geode/utils/web2.hpp>
#include <Geode/utils/map.hpp>
#include <Geode/utils/terminate.hpp>
#include <sstream>
using namespace geode::prelude;
using namespace geode::utils::web;
static long unwrapProxyType(ProxyType type) {
switch (type) {
using enum ProxyType;
case HTTP:
return CURLPROXY_HTTP;
case HTTPS:
return CURLPROXY_HTTPS;
case HTTPS2:
return CURLPROXY_HTTPS2;
case SOCKS4:
return CURLPROXY_SOCKS4;
case SOCKS4A:
return CURLPROXY_SOCKS4A;
case SOCKS5:
return CURLPROXY_SOCKS5;
case SOCKS5H:
return CURLPROXY_SOCKS5_HOSTNAME;
}
// Shouldn't happen.
unreachable("Unexpected proxy type!");
}
static long unwrapHttpAuth(long auth) {
long unwrapped = 0;
#define SET_AUTH(name) \
if (auth & http_auth::name) \
unwrapped |= CURLAUTH_##name;
SET_AUTH(BASIC);
SET_AUTH(DIGEST);
SET_AUTH(DIGEST_IE);
SET_AUTH(BEARER);
SET_AUTH(NEGOTIATE);
SET_AUTH(NTLM);
SET_AUTH(NTLM_WB);
SET_AUTH(ANY);
SET_AUTH(ANYSAFE);
SET_AUTH(ONLY);
SET_AUTH(AWS_SIGV4);
#undef SET_AUTH
return unwrapped;
}
class WebResponse::Impl {
public:
int m_code;
ByteVector m_data;
std::unordered_map<std::string, std::string> m_headers;
};
WebResponse::WebResponse() : m_impl(std::make_shared<Impl>()) {}
bool WebResponse::ok() const {
return 200 <= m_impl->m_code && m_impl->m_code < 300;
}
int WebResponse::code() const {
return m_impl->m_code;
}
Result<std::string> WebResponse::string() const {
return Ok(std::string(m_impl->m_data.begin(), m_impl->m_data.end()));
}
Result<matjson::Value> WebResponse::json() const {
GEODE_UNWRAP_INTO(auto value, this->string());
std::string error;
auto res = matjson::parse(value, error);
if (error.size() > 0) {
return Err("Error parsing JSON: " + error);
}
return Ok(res.value());
}
ByteVector WebResponse::data() const {
return m_impl->m_data;
}
std::vector<std::string> WebResponse::headers() const {
return map::keys(m_impl->m_headers);
}
std::optional<std::string> WebResponse::header(std::string_view name) const {
auto str = std::string(name);
if (m_impl->m_headers.contains(str)) {
return m_impl->m_headers.at(str);
}
return std::nullopt;
}
class WebProgress::Impl {
public:
size_t m_downloadCurrent;
size_t m_downloadTotal;
size_t m_uploadCurrent;
size_t m_uploadTotal;
};
WebProgress::WebProgress() : m_impl(std::make_shared<Impl>()) {}
size_t WebProgress::downloaded() const {
return m_impl->m_downloadCurrent;
}
size_t WebProgress::downloadTotal() const {
return m_impl->m_downloadTotal;
}
std::optional<float> WebProgress::downloadProgress() const {
return downloadTotal() > 0 ? std::optional(downloaded() * 100.f / downloadTotal()) : std::nullopt;
}
size_t WebProgress::uploaded() const {
return m_impl->m_uploadCurrent;
}
size_t WebProgress::uploadTotal() const {
return m_impl->m_uploadTotal;
}
std::optional<float> WebProgress::uploadProgress() const {
return uploadTotal() > 0 ? std::optional(uploaded() * 100.f / uploadTotal()) : std::nullopt;
}
class WebRequest::Impl {
public:
std::string m_method;
std::string m_url;
std::unordered_map<std::string, std::string> m_headers;
std::unordered_map<std::string, std::string> m_urlParameters;
std::optional<std::string> m_userAgent;
std::optional<ByteVector> m_body;
std::optional<std::chrono::seconds> m_timeout;
bool m_certVerification = true;
std::string m_CABundleContent;
ProxyOpts m_proxyOpts = {};
WebResponse makeError(int code, std::string const& msg) {
auto res = WebResponse();
res.m_impl->m_code = code;
res.m_impl->m_data = ByteVector(msg.begin(), msg.end());
return res;
}
};
WebRequest::WebRequest() : m_impl(std::make_shared<Impl>()) {}
WebRequest::~WebRequest() {}
// Encodes a url param
std::string urlParamEncode(std::string_view const input) {
std::ostringstream ss;
ss << std::hex << std::uppercase;
for (char c : input) {
if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
ss << c;
} else {
ss << '%' << static_cast<int>(c);
}
}
return ss.str();
}
WebTask WebRequest::send(std::string_view method, std::string_view url) {
m_impl->m_method = method;
m_impl->m_url = url;
return WebTask::run([impl = m_impl](auto progress, auto hasBeenCancelled) -> WebTask::Result {
// Init Curl
auto curl = curl_easy_init();
if (!curl) {
return impl->makeError(-1, "Curl not initialized");
}
// todo: in the future, we might want to support downloading directly into
// files / in-memory streams like the old AsyncWebRequest class
// Struct that holds values for the curl callbacks
struct ResponseData {
WebResponse response;
Impl* impl;
WebTask::PostProgress progress;
WebTask::HasBeenCancelled hasBeenCancelled;
} responseData = {
.response = WebResponse(),
.impl = impl.get(),
.progress = progress,
.hasBeenCancelled = hasBeenCancelled,
};
// Store downloaded response data into a byte vector
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseData);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, +[](char* data, size_t size, size_t nmemb, void* ptr) {
auto& target = static_cast<ResponseData*>(ptr)->response.m_impl->m_data;
target.insert(target.end(), data, data + size * nmemb);
return size * nmemb;
});
// Set headers
curl_slist* headers = nullptr;
for (auto& [name, value] : impl->m_headers) {
// Sanitize header name
auto header = name;
header.erase(std::remove_if(header.begin(), header.end(), [](char c) {
return c == '\r' || c == '\n';
}), header.end());
// Append value
header += ": " + value;
headers = curl_slist_append(headers, header.c_str());
}
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
// Add parameters to the URL and pass it to curl
auto url = impl->m_url;
bool first = true;
for (auto param : impl->m_urlParameters) {
url += (first ? "?" : "&") + urlParamEncode(param.first) + "=" + urlParamEncode(param.second);
first = false;
}
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
// Set request method
if (impl->m_method != "GET") {
if (impl->m_method == "POST") {
curl_easy_setopt(curl, CURLOPT_POST, 1L);
}
else {
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, impl->m_method.c_str());
}
}
// Set body if provided
if (impl->m_body) {
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, impl->m_body->data());
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, impl->m_body->size());
} else if (impl->m_method == "POST") {
// curl_easy_perform would freeze on a POST request with no fields, so set it to an empty string
// why? god knows
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "");
}
// Cert verification
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, impl->m_certVerification ? 1 : 0);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2);
if (impl->m_certVerification) {
curl_blob caBundleBlob = {};
if (impl->m_CABundleContent.empty()) {
impl->m_CABundleContent = CA_BUNDLE_CONTENT;
}
caBundleBlob.data = reinterpret_cast<void*>(impl->m_CABundleContent.data());
caBundleBlob.len = impl->m_CABundleContent.size();
caBundleBlob.flags = CURL_BLOB_COPY;
curl_easy_setopt(curl, CURLOPT_CAINFO_BLOB, &caBundleBlob);
}
// Set user agent if provided
if (impl->m_userAgent) {
curl_easy_setopt(curl, CURLOPT_USERAGENT, impl->m_userAgent->c_str());
}
// Set timeout
if (impl->m_timeout) {
curl_easy_setopt(curl, CURLOPT_TIMEOUT, impl->m_timeout->count());
}
// Set proxy options
auto const& proxyOpts = impl->m_proxyOpts;
if (!proxyOpts.address.empty()) {
curl_easy_setopt(curl, CURLOPT_PROXY, proxyOpts.address.c_str());
if (proxyOpts.port.has_value()) {
curl_easy_setopt(curl, CURLOPT_PROXYPORT, proxyOpts.port.value());
}
curl_easy_setopt(curl, CURLOPT_PROXYTYPE, unwrapProxyType(proxyOpts.type));
if (!proxyOpts.username.empty() || !proxyOpts.username.empty()) {
curl_easy_setopt(curl, CURLOPT_PROXYAUTH, unwrapHttpAuth(proxyOpts.auth));
curl_easy_setopt(curl, CURLOPT_PROXYUSERPWD,
fmt::format("{}:{}", proxyOpts.username, proxyOpts.password).c_str());
}
curl_easy_setopt(curl, CURLOPT_HTTPPROXYTUNNEL, proxyOpts.tunneling ? 1 : 0);
curl_easy_setopt(curl, CURLOPT_PROXY_SSL_VERIFYPEER, proxyOpts.certVerification ? 1 : 0);
curl_easy_setopt(curl, CURLOPT_PROXY_SSL_VERIFYHOST, 2);
}
// Track progress
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0);
// Follow redirects
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
// Do not fail if response code is 4XX or 5XX
curl_easy_setopt(curl, CURLOPT_FAILONERROR, 0L);
// Get headers from the response
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &responseData);
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, (+[](char* buffer, size_t size, size_t nitems, void* ptr) {
auto& headers = static_cast<ResponseData*>(ptr)->response.m_impl->m_headers;
std::string line;
std::stringstream ss(std::string(buffer, size * nitems));
while (std::getline(ss, line)) {
auto colon = line.find(':');
if (colon == std::string::npos) continue;
auto key = line.substr(0, colon);
auto value = line.substr(colon + 2);
if (value.ends_with('\r')) {
value = value.substr(0, value.size() - 1);
}
headers.insert_or_assign(key, value);
}
return size * nitems;
}));
// Track & post progress on the Promise
curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, &responseData);
curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, +[](void* ptr, double dtotal, double dnow, double utotal, double unow) -> int {
auto data = static_cast<ResponseData*>(ptr);
// Check for cancellation and abort if so
if (data->hasBeenCancelled()) {
return 1;
}
// Post progress to Promise listener
auto progress = WebProgress();
progress.m_impl->m_downloadTotal = dtotal;
progress.m_impl->m_downloadCurrent = dnow;
progress.m_impl->m_uploadTotal = utotal;
progress.m_impl->m_uploadCurrent = unow;
data->progress(std::move(progress));
// Continue as normal
return 0;
});
// Make the actual web request
auto curlResponse = curl_easy_perform(curl);
// Get the response code; note that this will be invalid if the
// curlResponse is not CURLE_OK
long code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
responseData.response.m_impl->m_code = static_cast<int>(code);
// Free up curl memory
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
// Check if the request failed on curl's side or because of cancellation
if (curlResponse != CURLE_OK) {
if (hasBeenCancelled()) {
return WebTask::Cancel();
}
else {
return impl->makeError(-1, "Curl failed: " + std::string(curl_easy_strerror(curlResponse)));
}
}
// Check if the response was an error code
if (code >= 400 && code <= 600) {
return std::move(responseData.response);
}
// Otherwise resolve with success :-)
return std::move(responseData.response);
}, fmt::format("{} request to {}", method, url));
}
WebTask WebRequest::post(std::string_view url) {
return this->send("POST", url);
}
WebTask WebRequest::get(std::string_view url) {
return this->send("GET", url);
}
WebTask WebRequest::put(std::string_view url) {
return this->send("PUT", url);
}
WebTask WebRequest::patch(std::string_view url) {
return this->send("PATCH", url);
}
WebRequest& WebRequest::header(std::string_view name, std::string_view value) {
m_impl->m_headers.insert_or_assign(std::string(name), std::string(value));
return *this;
}
WebRequest& WebRequest::param(std::string_view name, std::string_view value) {
m_impl->m_urlParameters.insert_or_assign(std::string(name), std::string(value));
return *this;
}
WebRequest& WebRequest::userAgent(std::string_view name) {
m_impl->m_userAgent = name;
return *this;
}
WebRequest& WebRequest::timeout(std::chrono::seconds time) {
m_impl->m_timeout = time;
return *this;
}
WebRequest& WebRequest::certVerification(bool enabled) {
m_impl->m_certVerification = enabled;
return *this;
}
WebRequest& WebRequest::CABundleContent(std::string_view content) {
m_impl->m_CABundleContent = content;
return *this;
}
WebRequest& WebRequest::proxyOpts(ProxyOpts const& proxyOpts) {
m_impl->m_proxyOpts = proxyOpts;
return *this;
}
WebRequest& WebRequest::body(ByteVector raw) {
m_impl->m_body = raw;
return *this;
}
WebRequest& WebRequest::bodyString(std::string_view str) {
m_impl->m_body = ByteVector { str.begin(), str.end() };
return *this;
}
WebRequest& WebRequest::bodyJSON(matjson::Value const& json) {
this->header("Content-Type", "application/json");
std::string str = json.dump(matjson::NO_INDENTATION);
m_impl->m_body = ByteVector { str.begin(), str.end() };
return *this;
}