mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-17 04:31:37 -04:00
UI: Add update checks (#715)
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
#include "http.hpp"
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <mutex>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace dusk::http {
|
||||
namespace {
|
||||
|
||||
struct CurlHeaders {
|
||||
curl_slist* list = nullptr;
|
||||
|
||||
~CurlHeaders() {
|
||||
if (list != nullptr) {
|
||||
curl_slist_free_all(list);
|
||||
}
|
||||
}
|
||||
|
||||
bool append(const std::string& header) {
|
||||
curl_slist* next = curl_slist_append(list, header.c_str());
|
||||
if (next == nullptr) {
|
||||
return false;
|
||||
}
|
||||
list = next;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
struct CurlContext {
|
||||
Response response;
|
||||
size_t maxBodyBytes = 0;
|
||||
bool tooLarge = false;
|
||||
};
|
||||
|
||||
void initialize_curl() {
|
||||
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||
}
|
||||
|
||||
std::string trim_header_value(std::string_view value) {
|
||||
while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) {
|
||||
value.remove_prefix(1);
|
||||
}
|
||||
while (!value.empty() &&
|
||||
(value.back() == '\r' || value.back() == '\n' || value.back() == ' ' ||
|
||||
value.back() == '\t')) {
|
||||
value.remove_suffix(1);
|
||||
}
|
||||
return std::string(value);
|
||||
}
|
||||
|
||||
size_t write_body(char* ptr, size_t size, size_t nmemb, void* userdata) {
|
||||
auto* context = static_cast<CurlContext*>(userdata);
|
||||
const size_t bytes = size * nmemb;
|
||||
if (bytes > context->maxBodyBytes ||
|
||||
context->response.body.size() > context->maxBodyBytes - bytes) {
|
||||
context->tooLarge = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
context->response.body.append(ptr, bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
size_t write_header(char* ptr, size_t size, size_t nmemb, void* userdata) {
|
||||
auto* context = static_cast<CurlContext*>(userdata);
|
||||
const std::string_view line(ptr, size * nmemb);
|
||||
if (line.starts_with("HTTP/")) {
|
||||
context->response.headers.clear();
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
const size_t colon = line.find(':');
|
||||
if (colon == std::string_view::npos) {
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
context->response.headers.push_back({
|
||||
.name = std::string(line.substr(0, colon)),
|
||||
.value = trim_header_value(line.substr(colon + 1)),
|
||||
});
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
Error map_curl_error(CURLcode code, bool tooLarge) {
|
||||
if (tooLarge) {
|
||||
return Error::TooLarge;
|
||||
}
|
||||
|
||||
switch (code) {
|
||||
case CURLE_OK:
|
||||
return Error::None;
|
||||
case CURLE_URL_MALFORMAT:
|
||||
return Error::InvalidUrl;
|
||||
case CURLE_UNSUPPORTED_PROTOCOL:
|
||||
return Error::UnsupportedScheme;
|
||||
case CURLE_OPERATION_TIMEDOUT:
|
||||
return Error::Timeout;
|
||||
default:
|
||||
return Error::Network;
|
||||
}
|
||||
}
|
||||
|
||||
long timeout_ms(std::chrono::milliseconds timeout) {
|
||||
return std::max<std::chrono::milliseconds::rep>(1, timeout.count());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool available() noexcept {
|
||||
return true;
|
||||
}
|
||||
|
||||
Backend backend() noexcept {
|
||||
return Backend::LibCurl;
|
||||
}
|
||||
|
||||
const char* backend_name() noexcept {
|
||||
return "libcurl";
|
||||
}
|
||||
|
||||
Result get(const Request& request) {
|
||||
if (request.url.empty()) {
|
||||
return {
|
||||
.error = Error::InvalidUrl,
|
||||
.message = "URL is empty",
|
||||
};
|
||||
}
|
||||
if (!request.url.starts_with("https://")) {
|
||||
return {
|
||||
.error = Error::UnsupportedScheme,
|
||||
.message = "Only https:// URLs are supported",
|
||||
};
|
||||
}
|
||||
|
||||
static std::once_flag initFlag;
|
||||
std::call_once(initFlag, initialize_curl);
|
||||
|
||||
CURL* curl = curl_easy_init();
|
||||
if (curl == nullptr) {
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Failed to create libcurl request",
|
||||
};
|
||||
}
|
||||
|
||||
CurlHeaders headers;
|
||||
for (const Header& header : request.headers) {
|
||||
if (!headers.append(header.name + ": " + header.value)) {
|
||||
curl_easy_cleanup(curl);
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Failed to allocate libcurl headers",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
CurlContext context{
|
||||
.maxBodyBytes = request.maxBodyBytes,
|
||||
};
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, request.url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers.list);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_ms(request.timeout));
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, timeout_ms(request.timeout));
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_body);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &context);
|
||||
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, write_header);
|
||||
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &context);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
#if CURL_AT_LEAST_VERSION(7, 85, 0)
|
||||
curl_easy_setopt(curl, CURLOPT_PROTOCOLS_STR, "https");
|
||||
curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS_STR, "https");
|
||||
#else
|
||||
curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);
|
||||
curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS);
|
||||
#endif
|
||||
|
||||
const CURLcode code = curl_easy_perform(curl);
|
||||
long statusCode = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &statusCode);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
context.response.statusCode = static_cast<int>(statusCode);
|
||||
if (code == CURLE_OK) {
|
||||
return {
|
||||
.response = std::move(context.response),
|
||||
};
|
||||
}
|
||||
|
||||
const Error error = map_curl_error(code, context.tooLarge);
|
||||
return {
|
||||
.error = error,
|
||||
.message = error == Error::TooLarge ? "Response body exceeded the configured limit"
|
||||
: curl_easy_strerror(code),
|
||||
.response = std::move(context.response),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace dusk::http
|
||||
@@ -0,0 +1,59 @@
|
||||
#ifndef DUSK_HTTP_HTTP_HPP
|
||||
#define DUSK_HTTP_HTTP_HPP
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::http {
|
||||
|
||||
enum class Backend {
|
||||
None,
|
||||
WinHttp,
|
||||
UrlSession,
|
||||
LibCurl,
|
||||
};
|
||||
|
||||
enum class Error {
|
||||
None,
|
||||
NoBackend,
|
||||
InvalidUrl,
|
||||
UnsupportedScheme,
|
||||
Timeout,
|
||||
TooLarge,
|
||||
Network,
|
||||
};
|
||||
|
||||
struct Header {
|
||||
std::string name;
|
||||
std::string value;
|
||||
};
|
||||
|
||||
struct Request {
|
||||
std::string url;
|
||||
std::vector<Header> headers;
|
||||
std::chrono::milliseconds timeout{10000};
|
||||
size_t maxBodyBytes = 1024 * 1024;
|
||||
};
|
||||
|
||||
struct Response {
|
||||
int statusCode = 0;
|
||||
std::vector<Header> headers;
|
||||
std::string body;
|
||||
};
|
||||
|
||||
struct Result {
|
||||
Error error = Error::None;
|
||||
std::string message;
|
||||
Response response;
|
||||
};
|
||||
|
||||
bool available() noexcept;
|
||||
Backend backend() noexcept;
|
||||
const char* backend_name() noexcept;
|
||||
Result get(const Request& request);
|
||||
|
||||
} // namespace dusk::http
|
||||
|
||||
#endif // DUSK_HTTP_HTTP_HPP
|
||||
@@ -0,0 +1,24 @@
|
||||
#include "http.hpp"
|
||||
|
||||
namespace dusk::http {
|
||||
|
||||
bool available() noexcept {
|
||||
return false;
|
||||
}
|
||||
|
||||
Backend backend() noexcept {
|
||||
return Backend::None;
|
||||
}
|
||||
|
||||
const char* backend_name() noexcept {
|
||||
return "none";
|
||||
}
|
||||
|
||||
Result get(const Request&) {
|
||||
return {
|
||||
.error = Error::NoBackend,
|
||||
.message = "No HTTP backend is available",
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace dusk::http
|
||||
@@ -0,0 +1,238 @@
|
||||
#include "http.hpp"
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
@interface DuskHttpRequestDelegate : NSObject <NSURLSessionDataDelegate, NSURLSessionTaskDelegate>
|
||||
@property(nonatomic) dispatch_semaphore_t semaphore;
|
||||
@property(nonatomic) size_t maxBodyBytes;
|
||||
@property(nonatomic, strong) NSMutableData* data;
|
||||
@property(nonatomic, strong) NSURLResponse* response;
|
||||
@property(nonatomic, strong) NSError* error;
|
||||
@property(nonatomic) BOOL tooLarge;
|
||||
- (instancetype)initWithMaxBodyBytes:(size_t)maxBodyBytes;
|
||||
@end
|
||||
|
||||
@implementation DuskHttpRequestDelegate
|
||||
|
||||
- (instancetype)initWithMaxBodyBytes:(size_t)maxBodyBytes {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_semaphore = dispatch_semaphore_create(0);
|
||||
_maxBodyBytes = maxBodyBytes;
|
||||
_data = [NSMutableData data];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession*)session
|
||||
task:(NSURLSessionTask*)task
|
||||
willPerformHTTPRedirection:(NSHTTPURLResponse*)response
|
||||
newRequest:(NSURLRequest*)request
|
||||
completionHandler:(void (^)(NSURLRequest*))completionHandler {
|
||||
if ([[request.URL.scheme lowercaseString] isEqualToString:@"https"]) {
|
||||
completionHandler(request);
|
||||
} else {
|
||||
completionHandler(nil);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession*)session
|
||||
dataTask:(NSURLSessionDataTask*)dataTask
|
||||
didReceiveResponse:(NSURLResponse*)response
|
||||
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
|
||||
self.response = response;
|
||||
completionHandler(NSURLSessionResponseAllow);
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession*)session
|
||||
dataTask:(NSURLSessionDataTask*)dataTask
|
||||
didReceiveData:(NSData*)data {
|
||||
if (data.length > self.maxBodyBytes ||
|
||||
self.data.length > self.maxBodyBytes - data.length) {
|
||||
self.tooLarge = YES;
|
||||
[dataTask cancel];
|
||||
return;
|
||||
}
|
||||
[self.data appendData:data];
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession*)session
|
||||
task:(NSURLSessionTask*)task
|
||||
didCompleteWithError:(NSError*)error {
|
||||
if (error != nil && !self.tooLarge) {
|
||||
self.error = error;
|
||||
}
|
||||
dispatch_semaphore_signal(self.semaphore);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
namespace dusk::http {
|
||||
namespace {
|
||||
|
||||
NSString* to_nsstring(std::string_view value) {
|
||||
return [[NSString alloc] initWithBytes:value.data()
|
||||
length:value.size()
|
||||
encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
std::string to_string(NSString* value) {
|
||||
if (value == nil) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const char* utf8 = [value UTF8String];
|
||||
return utf8 == nullptr ? std::string() : std::string(utf8);
|
||||
}
|
||||
|
||||
Error map_nsurl_error(NSError* error) {
|
||||
if (error == nil || ![error.domain isEqualToString:NSURLErrorDomain]) {
|
||||
return Error::Network;
|
||||
}
|
||||
|
||||
switch (error.code) {
|
||||
case NSURLErrorTimedOut:
|
||||
return Error::Timeout;
|
||||
case NSURLErrorBadURL:
|
||||
case NSURLErrorUnsupportedURL:
|
||||
return Error::InvalidUrl;
|
||||
default:
|
||||
return Error::Network;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch_time_t timeout_deadline(std::chrono::milliseconds timeout) {
|
||||
const auto milliseconds = std::max<std::chrono::milliseconds::rep>(1, timeout.count());
|
||||
return dispatch_time(DISPATCH_TIME_NOW,
|
||||
static_cast<int64_t>(milliseconds) * static_cast<int64_t>(NSEC_PER_MSEC));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool available() noexcept {
|
||||
return true;
|
||||
}
|
||||
|
||||
Backend backend() noexcept {
|
||||
return Backend::UrlSession;
|
||||
}
|
||||
|
||||
const char* backend_name() noexcept {
|
||||
return "NSURLSession";
|
||||
}
|
||||
|
||||
Result get(const Request& request) {
|
||||
@autoreleasepool {
|
||||
if (request.url.empty()) {
|
||||
return {
|
||||
.error = Error::InvalidUrl,
|
||||
.message = "URL is empty",
|
||||
};
|
||||
}
|
||||
if (!request.url.starts_with("https://")) {
|
||||
return {
|
||||
.error = Error::UnsupportedScheme,
|
||||
.message = "Only https:// URLs are supported",
|
||||
};
|
||||
}
|
||||
|
||||
NSString* urlString = to_nsstring(request.url);
|
||||
if (urlString == nil) {
|
||||
return {
|
||||
.error = Error::InvalidUrl,
|
||||
.message = "URL is not valid UTF-8",
|
||||
};
|
||||
}
|
||||
|
||||
NSURL* url = [NSURL URLWithString:urlString];
|
||||
if (url == nil || ![[url.scheme lowercaseString] isEqualToString:@"https"]) {
|
||||
return {
|
||||
.error = Error::InvalidUrl,
|
||||
.message = "Failed to parse URL",
|
||||
};
|
||||
}
|
||||
|
||||
NSMutableURLRequest* urlRequest = [NSMutableURLRequest requestWithURL:url];
|
||||
urlRequest.HTTPMethod = @"GET";
|
||||
urlRequest.timeoutInterval = request.timeout.count() / 1000.0;
|
||||
urlRequest.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
|
||||
for (const Header& header : request.headers) {
|
||||
NSString* name = to_nsstring(header.name);
|
||||
NSString* value = to_nsstring(header.value);
|
||||
if (name == nil || value == nil) {
|
||||
return {
|
||||
.error = Error::InvalidUrl,
|
||||
.message = "Request header is not valid UTF-8",
|
||||
};
|
||||
}
|
||||
[urlRequest setValue:value forHTTPHeaderField:name];
|
||||
}
|
||||
|
||||
NSURLSessionConfiguration* configuration =
|
||||
[NSURLSessionConfiguration ephemeralSessionConfiguration];
|
||||
configuration.timeoutIntervalForRequest = request.timeout.count() / 1000.0;
|
||||
configuration.timeoutIntervalForResource = request.timeout.count() / 1000.0;
|
||||
|
||||
DuskHttpRequestDelegate* delegate =
|
||||
[[DuskHttpRequestDelegate alloc] initWithMaxBodyBytes:request.maxBodyBytes];
|
||||
NSURLSession* session = [NSURLSession sessionWithConfiguration:configuration
|
||||
delegate:delegate
|
||||
delegateQueue:nil];
|
||||
NSURLSessionDataTask* task = [session dataTaskWithRequest:urlRequest];
|
||||
[task resume];
|
||||
|
||||
if (dispatch_semaphore_wait(delegate.semaphore, timeout_deadline(request.timeout)) != 0) {
|
||||
[task cancel];
|
||||
[session invalidateAndCancel];
|
||||
return {
|
||||
.error = Error::Timeout,
|
||||
.message = "Request timed out",
|
||||
};
|
||||
}
|
||||
|
||||
[session finishTasksAndInvalidate];
|
||||
|
||||
Response response;
|
||||
if ([delegate.response isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)delegate.response;
|
||||
response.statusCode = static_cast<int>(httpResponse.statusCode);
|
||||
NSDictionary* headers = httpResponse.allHeaderFields;
|
||||
for (id key in headers) {
|
||||
id value = headers[key];
|
||||
response.headers.push_back({
|
||||
.name = to_string([key description]),
|
||||
.value = to_string([value description]),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (delegate.data != nil && delegate.data.length > 0) {
|
||||
response.body.assign(static_cast<const char*>(delegate.data.bytes),
|
||||
static_cast<size_t>(delegate.data.length));
|
||||
}
|
||||
|
||||
if (delegate.tooLarge) {
|
||||
return {
|
||||
.error = Error::TooLarge,
|
||||
.message = "Response body exceeded the configured limit",
|
||||
.response = std::move(response),
|
||||
};
|
||||
}
|
||||
if (delegate.error != nil) {
|
||||
return {
|
||||
.error = map_nsurl_error(delegate.error),
|
||||
.message = to_string(delegate.error.localizedDescription),
|
||||
.response = std::move(response),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
.response = std::move(response),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk::http
|
||||
@@ -0,0 +1,320 @@
|
||||
#include "http.hpp"
|
||||
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#include <winhttp.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::http {
|
||||
namespace {
|
||||
|
||||
struct WinHttpHandle {
|
||||
HINTERNET handle = nullptr;
|
||||
|
||||
WinHttpHandle() = default;
|
||||
explicit WinHttpHandle(HINTERNET handle) : handle(handle) {}
|
||||
WinHttpHandle(const WinHttpHandle&) = delete;
|
||||
WinHttpHandle& operator=(const WinHttpHandle&) = delete;
|
||||
|
||||
~WinHttpHandle() {
|
||||
if (handle != nullptr) {
|
||||
WinHttpCloseHandle(handle);
|
||||
}
|
||||
}
|
||||
|
||||
operator HINTERNET() const { return handle; }
|
||||
};
|
||||
|
||||
std::wstring utf8_to_wide(std::string_view value) {
|
||||
if (value.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const int required = MultiByteToWideChar(
|
||||
CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), static_cast<int>(value.size()), nullptr, 0);
|
||||
if (required <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::wstring result(static_cast<size_t>(required), L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), static_cast<int>(value.size()),
|
||||
result.data(), required);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string wide_to_utf8(std::wstring_view value) {
|
||||
if (value.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const int required = WideCharToMultiByte(
|
||||
CP_UTF8, 0, value.data(), static_cast<int>(value.size()), nullptr, 0, nullptr, nullptr);
|
||||
if (required <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string result(static_cast<size_t>(required), '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), result.data(),
|
||||
required, nullptr, nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
DWORD timeout_ms(std::chrono::milliseconds timeout) {
|
||||
const auto count = std::max<std::chrono::milliseconds::rep>(1, timeout.count());
|
||||
return static_cast<DWORD>(
|
||||
std::min<std::chrono::milliseconds::rep>(count, std::numeric_limits<int>::max()));
|
||||
}
|
||||
|
||||
Error map_winhttp_error(DWORD error) {
|
||||
switch (error) {
|
||||
case ERROR_WINHTTP_TIMEOUT:
|
||||
return Error::Timeout;
|
||||
case ERROR_WINHTTP_INVALID_URL:
|
||||
case ERROR_WINHTTP_UNRECOGNIZED_SCHEME:
|
||||
return Error::InvalidUrl;
|
||||
case ERROR_WINHTTP_SECURE_FAILURE:
|
||||
case ERROR_WINHTTP_CANNOT_CONNECT:
|
||||
case ERROR_WINHTTP_CONNECTION_ERROR:
|
||||
default:
|
||||
return Error::Network;
|
||||
}
|
||||
}
|
||||
|
||||
Result fail_from_last_error(const char* message) {
|
||||
const DWORD error = GetLastError();
|
||||
return {
|
||||
.error = map_winhttp_error(error),
|
||||
.message = std::string(message) + " (" + std::to_string(error) + ")",
|
||||
};
|
||||
}
|
||||
|
||||
std::string trim_header_value(std::string_view value) {
|
||||
while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) {
|
||||
value.remove_prefix(1);
|
||||
}
|
||||
while (!value.empty() && (value.back() == '\r' || value.back() == '\n' || value.back() == ' ' ||
|
||||
value.back() == '\t'))
|
||||
{
|
||||
value.remove_suffix(1);
|
||||
}
|
||||
return std::string(value);
|
||||
}
|
||||
|
||||
void parse_headers(std::wstring_view rawHeaders, Response& response) {
|
||||
size_t start = 0;
|
||||
bool firstLine = true;
|
||||
while (start < rawHeaders.size()) {
|
||||
size_t end = rawHeaders.find(L"\r\n", start);
|
||||
if (end == std::wstring_view::npos) {
|
||||
end = rawHeaders.size();
|
||||
}
|
||||
|
||||
const std::wstring_view line = rawHeaders.substr(start, end - start);
|
||||
if (!line.empty() && !firstLine) {
|
||||
const size_t colon = line.find(L':');
|
||||
if (colon != std::wstring_view::npos) {
|
||||
response.headers.push_back({
|
||||
.name = wide_to_utf8(line.substr(0, colon)),
|
||||
.value = trim_header_value(wide_to_utf8(line.substr(colon + 1))),
|
||||
});
|
||||
}
|
||||
}
|
||||
firstLine = false;
|
||||
|
||||
if (end == rawHeaders.size()) {
|
||||
break;
|
||||
}
|
||||
start = end + 2;
|
||||
}
|
||||
}
|
||||
|
||||
bool read_status(HINTERNET request, Response& response) {
|
||||
DWORD statusCode = 0;
|
||||
DWORD statusCodeSize = sizeof(statusCode);
|
||||
if (!WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
|
||||
WINHTTP_HEADER_NAME_BY_INDEX, &statusCode, &statusCodeSize, WINHTTP_NO_HEADER_INDEX))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
response.statusCode = static_cast<int>(statusCode);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool read_headers(HINTERNET request, Response& response) {
|
||||
DWORD headerBytes = 0;
|
||||
WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX,
|
||||
WINHTTP_NO_OUTPUT_BUFFER, &headerBytes, WINHTTP_NO_HEADER_INDEX);
|
||||
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring rawHeaders(headerBytes / sizeof(wchar_t), L'\0');
|
||||
if (!WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX,
|
||||
rawHeaders.data(), &headerBytes, WINHTTP_NO_HEADER_INDEX))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!rawHeaders.empty() && rawHeaders.back() == L'\0') {
|
||||
rawHeaders.pop_back();
|
||||
}
|
||||
parse_headers(rawHeaders, response);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool available() noexcept {
|
||||
return true;
|
||||
}
|
||||
|
||||
Backend backend() noexcept {
|
||||
return Backend::WinHttp;
|
||||
}
|
||||
|
||||
const char* backend_name() noexcept {
|
||||
return "WinHTTP";
|
||||
}
|
||||
|
||||
Result get(const Request& request) {
|
||||
if (request.url.empty()) {
|
||||
return {
|
||||
.error = Error::InvalidUrl,
|
||||
.message = "URL is empty",
|
||||
};
|
||||
}
|
||||
|
||||
std::wstring wideUrl = utf8_to_wide(request.url);
|
||||
if (wideUrl.empty()) {
|
||||
return {
|
||||
.error = Error::InvalidUrl,
|
||||
.message = "URL is not valid UTF-8",
|
||||
};
|
||||
}
|
||||
|
||||
URL_COMPONENTS components{};
|
||||
components.dwStructSize = sizeof(components);
|
||||
components.dwSchemeLength = static_cast<DWORD>(-1);
|
||||
components.dwHostNameLength = static_cast<DWORD>(-1);
|
||||
components.dwUrlPathLength = static_cast<DWORD>(-1);
|
||||
components.dwExtraInfoLength = static_cast<DWORD>(-1);
|
||||
if (!WinHttpCrackUrl(wideUrl.c_str(), static_cast<DWORD>(wideUrl.size()), 0, &components)) {
|
||||
return fail_from_last_error("Failed to parse URL");
|
||||
}
|
||||
if (components.nScheme != INTERNET_SCHEME_HTTPS) {
|
||||
return {
|
||||
.error = Error::UnsupportedScheme,
|
||||
.message = "Only https:// URLs are supported",
|
||||
};
|
||||
}
|
||||
|
||||
const std::wstring host(components.lpszHostName, components.dwHostNameLength);
|
||||
std::wstring path;
|
||||
if (components.lpszUrlPath != nullptr && components.dwUrlPathLength > 0) {
|
||||
path.assign(components.lpszUrlPath, components.dwUrlPathLength);
|
||||
}
|
||||
if (components.lpszExtraInfo != nullptr && components.dwExtraInfoLength > 0) {
|
||||
path.append(components.lpszExtraInfo, components.dwExtraInfoLength);
|
||||
}
|
||||
if (path.empty()) {
|
||||
path = L"/";
|
||||
}
|
||||
|
||||
WinHttpHandle session(WinHttpOpen(L"Dusk", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
|
||||
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0));
|
||||
if (session.handle == nullptr) {
|
||||
return fail_from_last_error("Failed to create WinHTTP session");
|
||||
}
|
||||
|
||||
const DWORD timeout = timeout_ms(request.timeout);
|
||||
WinHttpSetTimeouts(session, timeout, timeout, timeout, timeout);
|
||||
|
||||
WinHttpHandle connection(WinHttpConnect(session, host.c_str(), components.nPort, 0));
|
||||
if (connection.handle == nullptr) {
|
||||
return fail_from_last_error("Failed to connect");
|
||||
}
|
||||
|
||||
WinHttpHandle httpRequest(WinHttpOpenRequest(connection, L"GET", path.c_str(), nullptr,
|
||||
WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE));
|
||||
if (httpRequest.handle == nullptr) {
|
||||
return fail_from_last_error("Failed to create request");
|
||||
}
|
||||
|
||||
DWORD redirectPolicy = WINHTTP_OPTION_REDIRECT_POLICY_DISALLOW_HTTPS_TO_HTTP;
|
||||
WinHttpSetOption(
|
||||
httpRequest, WINHTTP_OPTION_REDIRECT_POLICY, &redirectPolicy, sizeof(redirectPolicy));
|
||||
DWORD maxRedirects = 5;
|
||||
WinHttpSetOption(httpRequest, WINHTTP_OPTION_MAX_HTTP_AUTOMATIC_REDIRECTS, &maxRedirects,
|
||||
sizeof(maxRedirects));
|
||||
|
||||
for (const Header& header : request.headers) {
|
||||
const std::wstring wideHeader = utf8_to_wide(header.name + ": " + header.value);
|
||||
if (wideHeader.empty()) {
|
||||
return {
|
||||
.error = Error::InvalidUrl,
|
||||
.message = "Request header is not valid UTF-8",
|
||||
};
|
||||
}
|
||||
if (!WinHttpAddRequestHeaders(httpRequest, wideHeader.c_str(),
|
||||
static_cast<DWORD>(wideHeader.size()), WINHTTP_ADDREQ_FLAG_ADD))
|
||||
{
|
||||
return fail_from_last_error("Failed to add request header");
|
||||
}
|
||||
}
|
||||
|
||||
if (!WinHttpSendRequest(
|
||||
httpRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0))
|
||||
{
|
||||
return fail_from_last_error("Failed to send request");
|
||||
}
|
||||
if (!WinHttpReceiveResponse(httpRequest, nullptr)) {
|
||||
return fail_from_last_error("Failed to receive response");
|
||||
}
|
||||
|
||||
Response response;
|
||||
if (!read_status(httpRequest, response)) {
|
||||
return fail_from_last_error("Failed to read response status");
|
||||
}
|
||||
read_headers(httpRequest, response);
|
||||
|
||||
for (;;) {
|
||||
DWORD availableBytes = 0;
|
||||
if (!WinHttpQueryDataAvailable(httpRequest, &availableBytes)) {
|
||||
return fail_from_last_error("Failed to query response body");
|
||||
}
|
||||
if (availableBytes == 0) {
|
||||
break;
|
||||
}
|
||||
if (availableBytes > request.maxBodyBytes ||
|
||||
response.body.size() > request.maxBodyBytes - availableBytes)
|
||||
{
|
||||
return {
|
||||
.error = Error::TooLarge,
|
||||
.message = "Response body exceeded the configured limit",
|
||||
.response = std::move(response),
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<char> buffer(availableBytes);
|
||||
DWORD bytesRead = 0;
|
||||
if (!WinHttpReadData(httpRequest, buffer.data(), availableBytes, &bytesRead)) {
|
||||
return fail_from_last_error("Failed to read response body");
|
||||
}
|
||||
response.body.append(buffer.data(), bytesRead);
|
||||
}
|
||||
|
||||
return {
|
||||
.response = std::move(response),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace dusk::http
|
||||
Reference in New Issue
Block a user