#pragma once
#include <cstddef>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
#include <windows.h>
#include <winhttp.h>
#include <minwinbase.h>
#include <functional>
#include <optional>
#include <thread>
#include <winnt.h>
#include <shlobj.h>
#include <format>
#include <fstream>
#pragma comment(lib, "winhttp.lib")
namespace WinHTTP::Util {
inline std::vector<std::string> split(const std::string& str, const std::string& delimiter) {
auto s = str;
std::vector<std::string> tokens;
size_t pos = 0;
std::string token;
while ((pos = s.find(delimiter)) != std::string::npos) {
token = s.substr(0, pos);
tokens.push_back(token);
s.erase(0, pos + delimiter.length());
}
tokens.push_back(s);
return tokens;
}
}
namespace WinHTTP {
class WinHTTP {
public:
#pragma region TYPES
enum class ProxyType {
DefaultProxy = WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
NoProxy = WINHTTP_ACCESS_TYPE_NO_PROXY,
NamedProxy = WINHTTP_ACCESS_TYPE_NAMED_PROXY,
AutomaticProxy = WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY,
};
enum class Error {
None = 0,
SessionCreationFailed,
SessionNotAvailable,
ConnectionFailed,
ConnectionNotAvailable,
RequestFailed,
RequestNotAvailable,
HeaderAddFailed,
};
class wstring_vector : public std::vector<std::wstring> {
public:
std::vector<LPCWSTR> to_lpcwstr() const {
std::vector<LPCWSTR> ret;
ret.reserve(this->size()); // Reserve space to avoid multiple allocations
for (const auto& data : *this) {
ret.push_back(data.c_str());
}
return ret;
}
};
enum class FormContentType {
Text,
File,
AttachedFile
};
class FormContent {
public:
std::string data;
const FormContentType type = FormContentType::Text;
const std::string additionalData = "text";
};
class FormData {
public:
std::string name;
FormContent content;
};
#pragma endregion
#pragma region CLASS_CONSTRUCTORS
explicit WinHTTP(const std::wstring& userAgent, ProxyType AccessType = ProxyType::DefaultProxy, const std::wstring& proxyName = L"", const std::wstring& proxyBypass = L"", DWORD flags = 0):
requestSent(false), allowMultiThread(false), error(Error::None), ownerThreadId(std::this_thread::get_id()) {
hSession = WinHttpOpen(userAgent.c_str(),
to_underlying(AccessType),
proxyName.empty() ? WINHTTP_NO_PROXY_NAME : proxyName.c_str(),
proxyBypass.empty() ? WINHTTP_NO_PROXY_BYPASS : proxyName.c_str(),
flags);
if(not hSession)
SetError(Error::SessionCreationFailed);
}
WinHTTP(WinHTTP& other) = delete;
WinHTTP(WinHTTP&& other) = delete;
~WinHTTP() {
if(hRequest) WinHttpCloseHandle(hRequest);
if(hConnect) WinHttpCloseHandle(hConnect);
if(hSession) WinHttpCloseHandle(hSession);
}
#pragma endregion
// Connects to the given server.
void Connect(const std::wstring& serverName, INTERNET_PORT port = INTERNET_DEFAULT_HTTP_PORT, DWORD Reserved = WINHTTP_DEFAULT_ACCEPT_TYPES) {
check_thread();
if_session_available<void>([&] {
hConnect = WinHttpConnect(hSession, serverName.c_str(), port, WINHTTP_DEFAULT_ACCEPT_TYPES);
if(not hConnect) {
SetError(Error::ConnectionFailed);
return;
}
error = Error::None;
});
}
// Opens a request to the server.
// When object is destroyed, request is also closed. No need to close it manually.
void OpenRequest(const std::wstring& verb, const std::wstring& objectName, const std::wstring& version = L"", const std::wstring& referrer = L"", wstring_vector accept_types = {}, DWORD flags = 0) {
check_thread();
if_connection_available<void>([&] {
hRequest = WinHttpOpenRequest(hConnect, verb.c_str(), objectName.c_str(), version.c_str(),
referrer.empty() ? NULL : referrer.c_str(), accept_types.empty() ? NULL : accept_types.to_lpcwstr().data(), flags);
});
}
// Sends a request to the server.
// Need an open request first.
bool SendRequest(const std::wstring& additional_headers = L"", DWORD headersLength = 0, LPVOID optional = WINHTTP_NO_REQUEST_DATA, DWORD optionalLength = 0, DWORD total_length = 0, DWORD_PTR context = NULL) {
check_thread();
if_request_available<void>([&] {
requestSent = WinHttpSendRequest(hRequest, additional_headers.empty() ? WINHTTP_NO_ADDITIONAL_HEADERS : additional_headers.c_str(), headersLength, optional, optionalLength, total_length, context);
}, [&] {
SetError(Error::RequestNotAvailable);
});
return requestSent;
}
// Sends a multipart form data to the server.
// Needs an open request first.
bool SendMultiPartFormRequest(std::vector<FormData> form_data, const std::wstring& additional_headers = L"", DWORD headersLength = 0) {
return if_request_available<bool>([&]() -> bool {
std::string boundary = "----Boundary" + std::to_string((rand() % 999999) + 100000);
std::stringstream formBody;
for(auto data : form_data) {
formBody << "--" + boundary + "\r\n";
formBody << std::format("Content-Disposition: form-data; name=\"{}\"", data.name);
if(data.content.type == FormContentType::Text) {
formBody << "\r\n\r\n" << data.content.data << "\r\n";
continue;
}
if(data.content.type == FormContentType::File) {
std::string base_filename = data.content.data.substr(data.content.data.find_last_of("/\\") + 1);
formBody << "; filename=\"" + base_filename + "\"\r\n";
std::string file_data = read_file_content(data.content.data);
formBody << std::format("Content-Type: {}\r\n\r\n", data.content.additionalData);
formBody << file_data + "\r\n";
}
if(data.content.type ==FormContentType::AttachedFile) {
auto adData = Util::split(data.content.additionalData, "|");
formBody << "; filename=\"" + adData[1] + "\"\r\n";
formBody << std::format("Content-Type: {}\r\n\r\n", adData[0]);
formBody << data.content.data + "\r\n";
}
}
formBody << "--" + boundary + "--\r\n";
std::wstring headers = L"Content-Type: multipart/form-data; boundary=" + std::wstring(boundary.begin(), boundary.end()) + L"\r\n";
if (not WinHttpAddRequestHeaders(hRequest, headers.c_str(), (ULONG)-1L, WINHTTP_ADDREQ_FLAG_ADD)) {
SetError(Error::HeaderAddFailed);
return false;
}
auto requestdata = formBody.str();
if (not WinHttpSendRequest(hRequest, additional_headers.empty() ? WINHTTP_NO_ADDITIONAL_HEADERS : additional_headers.c_str(), headersLength, (LPVOID)requestdata.data(), (DWORD)requestdata.size(), (DWORD)requestdata.size(), 0)) {
SetError(Error::RequestFailed);
return false;
}
return true;
}, []{ return false; });
}
static std::pair<DWORD, std::string> GetLastErrorMessage() {
DWORD errorCode = GetLastError();
LPSTR errorBuffer = nullptr;
FormatMessageA(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr,
errorCode,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPSTR)&errorBuffer,
0,
nullptr
);
std::string errorMessage = errorBuffer ? errorBuffer : "Unknown error";
LocalFree(errorBuffer);
return {errorCode, errorMessage.substr(0, errorMessage.length() - 2)};
}
// Receives a response from server. Needs an open request
// and a request must be sent already.
std::optional<std::string> ReceiveResponse(LPVOID reserved = NULL) {
check_thread();
return if_request_available<std::optional<std::string>>([&]() -> std::optional<std::string> {
if (!WinHttpReceiveResponse(hRequest, reserved)) {
return {};
}
std::stringstream ret;
DWORD dwSize = 0;
DWORD dwDownloaded = 0;
std::vector<char> buffer; // Use vector to manage memory automatically
do {
dwSize = 0;
if (WinHttpQueryDataAvailable(hRequest, &dwSize)) {
buffer.resize(dwSize); // Resize to required size
if (WinHttpReadData(hRequest, buffer.data(), dwSize, &dwDownloaded)) {
ret.write(buffer.data(), dwDownloaded); // Write to stringstream
}
}
} while (dwSize > 0);
return ret.str();
});
}
bool SessionAvailable() {
return hSession;
}
bool ConnectionAvailable() {
return hConnect;
}
bool RequestAvailable() {
return hRequest;
}
void SetError(Error e) {
check_thread();
error = e;
}
bool ErrorSet() {
return error != Error::None;
}
Error GetError() const {
return error;
}
std::string GetError(bool) {
switch(error) {
case Error::ConnectionFailed:
return "Connection failed!";
case Error::ConnectionNotAvailable:
return "Connection not available!";
case Error::RequestNotAvailable:
return "Request not available!";
case Error::RequestFailed:
return "Request failed!";
case Error::SessionCreationFailed:
return "Session creation failed!";
case Error::SessionNotAvailable:
return "Session not available!";
case Error::HeaderAddFailed:
return "Headers add failed!";
case Error::None:
return "None";
}
return "";
}
bool MultiThreadAllowed() {
return allowMultiThread;
}
void AllowMultiThread() {
check_thread();
allowMultiThread = true;
}
void DisallowMultiThread() {
allowMultiThread = false;
}
private:
template<typename Ret_>
Ret_ if_session_available(const std::function<Ret_()>& _if, const std::function<Ret_()>& _else = {}) {
if(not SessionAvailable()) {
SetError(Error::SessionNotAvailable);
if(_else)
return _else();
return Ret_{};
}
return _if();
}
template<typename Ret_>
Ret_ if_connection_available(const std::function<Ret_()>& _if, const std::function<Ret_()>& _else = {}) {
return if_session_available<Ret_>([&]() -> Ret_ {
if(not ConnectionAvailable()) {
SetError(Error::ConnectionNotAvailable);
if(_else)
return _else();
return Ret_{};
}
return _if();
});
}
template<typename Ret_>
Ret_ if_request_available(const std::function<Ret_()>& _if, const std::function<Ret_()>& _else = {}) {
return if_connection_available<Ret_>([&]() -> Ret_ {
if(not RequestAvailable()) {
SetError(Error::RequestNotAvailable);
if(_else)
return _else();
return Ret_{};
}
return _if();
});
}
template<>
void if_session_available<void>(const std::function<void()>& _if, const std::function<void()>& _else) {
if(not SessionAvailable()) {
SetError(Error::SessionNotAvailable);
if(_else)
_else();
return;
}
_if();
}
template<>
void if_connection_available<void>(const std::function<void()>& _if, const std::function<void()>& _else) {
if_session_available<void>([&]() -> void {
if(not ConnectionAvailable()) {
SetError(Error::ConnectionNotAvailable);
if(_else)
_else();
return;
}
_if();
});
}
template<>
void if_request_available<void>(const std::function<void()>& _if, const std::function<void()>& _else) {
if_connection_available<void>([&]() -> void {
if(not RequestAvailable()) {
SetError(Error::RequestNotAvailable);
if(_else)
_else();
return;
}
_if();
});
}
void check_thread() const {
if(allowMultiThread)
return;
if (std::this_thread::get_id() != ownerThreadId) {
throw std::runtime_error("Attempt to use WinHTTP class from a different thread. If you know what you are doing, you can suppress this error with AllowMultiThread() method.");
}
}
std::string read_file_content(const std::string& filePath) {
std::ifstream file(filePath, std::ios::binary | std::ios::ate);
if (!file) {
throw std::runtime_error("Failed to open file.");
}
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
std::string buffer(size, '\0');
if (!file.read(buffer.data(), size)) {
throw std::runtime_error("Failed to read file.");
}
return buffer;
}
template <typename T_>
constexpr std::underlying_type_t<T_> to_underlying(T_ obj) noexcept {
return static_cast<std::underlying_type_t<T_>>(obj);
}
HINTERNET hSession, hConnect, hRequest;
bool requestSent, allowMultiThread;
Error error;
std::thread::id ownerThreadId;
};
class HTTPBuilder {
private:
WinHTTP session;
class Response {
public:
Response(HTTPBuilder * owner) : owner(owner) {}
std::string Receive() {
auto resp = owner->session.ReceiveResponse();
if(not resp) {
throw std::runtime_error("Recieve failed!");
}
return *resp;
}
private:
HTTPBuilder * owner;
};
template<typename ReqType>
class SetTarget {
public:
SetTarget(HTTPBuilder * owner) : owner(owner) {}
ReqType Target(const std::wstring& target) {
return {owner, target};
}
private:
HTTPBuilder * owner;
};
template<typename ReqType>
class Request {
public:
Request() : owner(nullptr) {}
Request(HTTPBuilder * owner, const std::wstring& verb) : owner(owner), verb(verb) {}
Request(HTTPBuilder * owner, const std::wstring& verb, const std::wstring& target) : owner(owner), verb(verb), objectName(target) {}
ReqType& Version(const std::wstring& version) {
this->version = version;
return *static_cast<ReqType*>(this);
}
ReqType& Referrer(const std::wstring& referrer) {
this->referrer = referrer;
return *static_cast<ReqType*>(this);
}
ReqType& AcceptTypes(WinHTTP::wstring_vector accept_types) {
this->accept_types = accept_types;
return *static_cast<ReqType*>(this);
}
ReqType& Flags(DWORD flags) {
this->flags = flags;
return *static_cast<ReqType*>(this);
}
protected:
HTTPBuilder* owner;
const std::wstring verb = L"";
std::wstring objectName = L"", version = L"", referrer = L"";
WinHTTP::wstring_vector accept_types = {};
DWORD flags = 0;
};
class PostRequest : public Request<PostRequest> {
public:
PostRequest(HTTPBuilder *owner) : Request(owner, L"POST") {}
PostRequest(HTTPBuilder *owner, const std::wstring& target) : Request(owner, L"POST", target) {}
PostRequest& AddFormData(const std::string& key, const WinHTTP::FormContent& content) {
formData.emplace_back(key, content);
return *this;
}
Response& Send() {
if(not owner->session.ConnectionAvailable())
throw std::runtime_error("Connection not available! Error code: " + std::to_string(owner->session.GetLastErrorMessage().first));
if(formData.empty())
throw std::runtime_error("Form data must be set to send!");
owner->session.OpenRequest(verb, objectName, version, referrer, accept_types, flags);
if(not owner->session.RequestAvailable())
throw std::runtime_error("An error occured while opening request! Error code: " + std::to_string(owner->session.GetLastErrorMessage().first));
owner->session.SendMultiPartFormRequest(formData);
return *new Response{owner};
}
private:
std::vector<WinHTTP::FormData> formData;
};
class GetRequest : public Request<GetRequest> {
public:
GetRequest(HTTPBuilder *owner) : Request(owner, L"GET") {}
GetRequest(HTTPBuilder *owner, const std::wstring& target) : Request(owner, L"POST", target) {}
//Sends the request and returns a response
Response& Send() {
if(not owner->session.ConnectionAvailable())
throw std::runtime_error("Connection not available! Error code: " + std::to_string(owner->session.GetLastErrorMessage().first));
if(objectName.empty()) {
throw std::runtime_error("Target must be set!");
}
owner->session.OpenRequest(verb, objectName, version, referrer, accept_types, flags);
if(not owner->session.RequestAvailable())
throw std::runtime_error("An error occured while opening request! Error code: " + std::to_string(owner->session.GetLastErrorMessage().first));
owner->session.SendRequest();
return *new Response{owner};
}
private:
};
class Connection {
public:
Connection() : owner(nullptr) {}
Connection(HTTPBuilder* owner) : owner(owner) {}
SetTarget<GetRequest> GetRequest() {
return SetTarget<class GetRequest>{owner};
}
SetTarget<PostRequest> PostRequest() {
return SetTarget<class PostRequest>{owner};
}
private:
HTTPBuilder* owner;
};
public:
HTTPBuilder(const std::wstring& userAgent) : session(userAgent) {}
Connection& Connect(const std::wstring& serverName, INTERNET_PORT port = INTERNET_DEFAULT_HTTP_PORT, DWORD Reserved = WINHTTP_DEFAULT_ACCEPT_TYPES) {
session.Connect(serverName, port, Reserved);
return *new Connection{this};
}
};
}