mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-09-04 01:07:56 -04:00
Mods: HttpService (#2370)
* Update Android toolchain to platform 37 * Mods: HttpService * Update modding.md * Update modding.md * Set minor version back to 0
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
#define HTTP_SERVICE_ID "dev.twilitrealm.dusklight.http"
|
||||
#define HTTP_SERVICE_MAJOR 1u
|
||||
#define HTTP_SERVICE_MINOR 0u
|
||||
|
||||
/* Handle for an in-flight request. 0 is never a valid handle. */
|
||||
typedef uint64_t HttpRequestHandle;
|
||||
|
||||
typedef enum HttpMethod {
|
||||
HTTP_METHOD_GET = 0,
|
||||
HTTP_METHOD_POST = 1,
|
||||
HTTP_METHOD_HEAD = 2,
|
||||
} HttpMethod;
|
||||
|
||||
/* Transport-level outcome. HTTP status errors are reported through status_code. */
|
||||
typedef enum HttpError {
|
||||
HTTP_ERROR_NONE = 0,
|
||||
HTTP_ERROR_INVALID_URL = 1,
|
||||
HTTP_ERROR_UNSUPPORTED_SCHEME = 2,
|
||||
HTTP_ERROR_TIMEOUT = 3,
|
||||
HTTP_ERROR_TOO_LARGE = 4,
|
||||
HTTP_ERROR_CANCELED = 5,
|
||||
HTTP_ERROR_IO = 6,
|
||||
HTTP_ERROR_NETWORK = 7,
|
||||
} HttpError;
|
||||
|
||||
typedef struct HttpHeader {
|
||||
const char* name;
|
||||
const char* value;
|
||||
} HttpHeader;
|
||||
|
||||
typedef struct HttpRequestDesc {
|
||||
uint32_t struct_size;
|
||||
HttpMethod method;
|
||||
const char* url;
|
||||
const HttpHeader* headers;
|
||||
uint32_t header_count;
|
||||
/* Request body; POST only. */
|
||||
const void* body;
|
||||
size_t body_size;
|
||||
/* Absolute destination under this mod's data_dir or mod_dir, or NULL for an in-memory body.
|
||||
* GET and POST only. */
|
||||
const char* download_path;
|
||||
uint32_t connect_timeout_ms; /* 0 = 10 seconds */
|
||||
uint32_t idle_timeout_ms; /* 0 = 10 seconds without network progress */
|
||||
uint32_t total_timeout_ms; /* 0 = no total timeout */
|
||||
size_t max_body_bytes; /* 0 = 1 MiB; ignored for downloads */
|
||||
} HttpRequestDesc;
|
||||
|
||||
#define HTTP_REQUEST_DESC_INIT \
|
||||
{sizeof(HttpRequestDesc), HTTP_METHOD_GET, NULL, NULL, 0u, NULL, 0u, NULL, 0u, 0u, 0u, 0u}
|
||||
|
||||
/* Snapshot valid only for the duration of the completion callback. */
|
||||
typedef struct HttpResult {
|
||||
uint32_t struct_size;
|
||||
HttpError error;
|
||||
const char* error_message;
|
||||
int32_t status_code;
|
||||
const HttpHeader* headers;
|
||||
uint32_t header_count;
|
||||
const void* body;
|
||||
size_t body_size;
|
||||
/* Published absolute destination, or NULL unless a download succeeded. */
|
||||
const char* download_path;
|
||||
} HttpResult;
|
||||
|
||||
/* Runs on the game thread exactly once, unless the calling mod begins deactivation first. */
|
||||
typedef void (*HttpCompleteFn)(
|
||||
ModContext* ctx, HttpRequestHandle request, const HttpResult* result, void* user_data);
|
||||
|
||||
typedef struct HttpProgress {
|
||||
uint32_t struct_size;
|
||||
uint64_t completed_bytes;
|
||||
uint64_t total_bytes;
|
||||
bool total_known;
|
||||
} HttpProgress;
|
||||
|
||||
#define HTTP_PROGRESS_INIT {sizeof(HttpProgress), 0u, 0u, false}
|
||||
|
||||
typedef struct HttpService {
|
||||
ServiceHeader header;
|
||||
|
||||
/* Starts an asynchronous HTTPS request. */
|
||||
ModResult (*request)(ModContext* ctx, const HttpRequestDesc* desc, HttpCompleteFn fn,
|
||||
void* user_data, HttpRequestHandle* out_handle);
|
||||
ModResult (*progress)(ModContext* ctx, HttpRequestHandle request, HttpProgress* out_progress);
|
||||
/* Requests cancellation. The completion callback still runs if the mod remains active. */
|
||||
ModResult (*cancel)(ModContext* ctx, HttpRequestHandle request);
|
||||
} HttpService;
|
||||
|
||||
MOD_DECLARE_SERVICE(HttpService, svc_http, HTTP_SERVICE_ID, HTTP_SERVICE_MAJOR, HTTP_SERVICE_MINOR);
|
||||
@@ -0,0 +1,204 @@
|
||||
#pragma once
|
||||
|
||||
#include <mods/svc/http.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace mods::http {
|
||||
|
||||
struct Header {
|
||||
std::string name;
|
||||
std::string value;
|
||||
};
|
||||
|
||||
struct Request {
|
||||
HttpMethod method = HTTP_METHOD_GET;
|
||||
std::string url;
|
||||
std::vector<Header> headers;
|
||||
std::string body;
|
||||
std::string downloadPath;
|
||||
uint32_t connectTimeoutMs = 0;
|
||||
uint32_t idleTimeoutMs = 0;
|
||||
uint32_t totalTimeoutMs = 0;
|
||||
size_t maxBodyBytes = 0;
|
||||
};
|
||||
|
||||
struct Response {
|
||||
HttpError error = HTTP_ERROR_NETWORK;
|
||||
std::string errorMessage;
|
||||
int statusCode = 0;
|
||||
std::vector<Header> headers;
|
||||
std::vector<uint8_t> body;
|
||||
std::string downloadPath;
|
||||
|
||||
bool ok() const { return error == HTTP_ERROR_NONE && statusCode >= 200 && statusCode < 300; }
|
||||
|
||||
const std::string* header(std::string_view name) const {
|
||||
const auto equal = [](std::string_view left, std::string_view right) {
|
||||
return left.size() == right.size() &&
|
||||
std::equal(left.begin(), left.end(), right.begin(), [](char a, char b) {
|
||||
return std::tolower(static_cast<unsigned char>(a)) ==
|
||||
std::tolower(static_cast<unsigned char>(b));
|
||||
});
|
||||
};
|
||||
const auto iter = std::find_if(headers.begin(), headers.end(),
|
||||
[&](const Header& value) { return equal(value.name, name); });
|
||||
return iter != headers.end() ? &iter->value : nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
|
||||
struct Completion {
|
||||
HttpRequestHandle handle = 0;
|
||||
std::function<void(Response)> callback;
|
||||
};
|
||||
|
||||
inline std::unordered_map<HttpRequestHandle, std::unique_ptr<Completion>> completions;
|
||||
|
||||
inline void complete(ModContext*, HttpRequestHandle handle, const HttpResult* raw, void* userData) {
|
||||
const auto iter = completions.find(handle);
|
||||
if (iter == completions.end() || iter->second.get() != userData) {
|
||||
return;
|
||||
}
|
||||
auto completion = std::move(iter->second);
|
||||
completions.erase(iter);
|
||||
|
||||
Response response;
|
||||
if (raw != nullptr) {
|
||||
response.error = raw->error;
|
||||
response.errorMessage = raw->error_message != nullptr ? raw->error_message : "";
|
||||
response.statusCode = raw->status_code;
|
||||
response.headers.reserve(raw->header_count);
|
||||
for (uint32_t i = 0; i < raw->header_count; ++i) {
|
||||
response.headers.push_back({
|
||||
.name = raw->headers[i].name != nullptr ? raw->headers[i].name : "",
|
||||
.value = raw->headers[i].value != nullptr ? raw->headers[i].value : "",
|
||||
});
|
||||
}
|
||||
if (raw->body != nullptr && raw->body_size != 0) {
|
||||
const auto* begin = static_cast<const uint8_t*>(raw->body);
|
||||
response.body.assign(begin, begin + raw->body_size);
|
||||
}
|
||||
response.downloadPath = raw->download_path != nullptr ? raw->download_path : "";
|
||||
}
|
||||
if (completion->callback) {
|
||||
completion->callback(std::move(response));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
class Pending {
|
||||
public:
|
||||
Pending() = default;
|
||||
Pending(HttpRequestHandle handle, ModResult result) : mHandle{handle}, mResult{result} {}
|
||||
~Pending() { reset(); }
|
||||
Pending(const Pending&) = delete;
|
||||
Pending& operator=(const Pending&) = delete;
|
||||
Pending(Pending&& other) noexcept { *this = std::move(other); }
|
||||
Pending& operator=(Pending&& other) noexcept {
|
||||
if (this != &other) {
|
||||
reset();
|
||||
mHandle = std::exchange(other.mHandle, 0);
|
||||
mResult = other.mResult;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
explicit operator bool() const {
|
||||
if (mResult != MOD_OK || mHandle == 0 || svc_http == nullptr ||
|
||||
!detail::completions.contains(mHandle))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
HttpProgress value = HTTP_PROGRESS_INIT;
|
||||
return svc_http->progress(mod_ctx, mHandle, &value) == MOD_OK;
|
||||
}
|
||||
ModResult result() const { return mResult; }
|
||||
HttpRequestHandle handle() const { return mHandle; }
|
||||
|
||||
HttpProgress progress() const {
|
||||
HttpProgress value = HTTP_PROGRESS_INIT;
|
||||
if (mHandle != 0 && svc_http != nullptr) {
|
||||
svc_http->progress(mod_ctx, mHandle, &value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
if (mHandle != 0 && svc_http != nullptr) {
|
||||
if (svc_http->cancel(mod_ctx, mHandle) != MOD_OK) {
|
||||
detail::completions.erase(mHandle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void detach() { mHandle = 0; }
|
||||
|
||||
private:
|
||||
void reset() {
|
||||
cancel();
|
||||
mHandle = 0;
|
||||
}
|
||||
|
||||
HttpRequestHandle mHandle = 0;
|
||||
ModResult mResult = MOD_UNAVAILABLE;
|
||||
};
|
||||
|
||||
inline Pending request(const Request& request, std::function<void(Response)> callback) {
|
||||
if (svc_http == nullptr) {
|
||||
return {0, MOD_UNAVAILABLE};
|
||||
}
|
||||
if (!callback) {
|
||||
return {0, MOD_INVALID_ARGUMENT};
|
||||
}
|
||||
|
||||
if (request.headers.size() > std::numeric_limits<uint32_t>::max()) {
|
||||
return {0, MOD_INVALID_ARGUMENT};
|
||||
}
|
||||
std::vector<HttpHeader> headers;
|
||||
headers.reserve(request.headers.size());
|
||||
for (const auto& header : request.headers) {
|
||||
headers.push_back({
|
||||
.name = header.name.c_str(),
|
||||
.value = header.value.c_str(),
|
||||
});
|
||||
}
|
||||
HttpRequestDesc desc = HTTP_REQUEST_DESC_INIT;
|
||||
desc.method = request.method;
|
||||
desc.url = request.url.c_str();
|
||||
desc.headers = headers.empty() ? nullptr : headers.data();
|
||||
desc.header_count = static_cast<uint32_t>(headers.size());
|
||||
desc.body = request.body.empty() ? nullptr : request.body.data();
|
||||
desc.body_size = request.body.size();
|
||||
desc.download_path = request.downloadPath.empty() ? nullptr : request.downloadPath.c_str();
|
||||
desc.connect_timeout_ms = request.connectTimeoutMs;
|
||||
desc.idle_timeout_ms = request.idleTimeoutMs;
|
||||
desc.total_timeout_ms = request.totalTimeoutMs;
|
||||
desc.max_body_bytes = request.maxBodyBytes;
|
||||
|
||||
auto completion = std::make_unique<detail::Completion>();
|
||||
auto* userData = completion.get();
|
||||
completion->callback = std::move(callback);
|
||||
HttpRequestHandle handle = 0;
|
||||
const auto result = svc_http->request(mod_ctx, &desc, detail::complete, userData, &handle);
|
||||
if (result != MOD_OK) {
|
||||
return {0, result};
|
||||
}
|
||||
completion->handle = handle;
|
||||
detail::completions.emplace(handle, std::move(completion));
|
||||
return {handle, result};
|
||||
}
|
||||
|
||||
} // namespace mods::http
|
||||
Reference in New Issue
Block a user