diff options
author | Erlite <ys.aameziane@gmail.com> | 2022-12-13 17:51:39 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-12-13 17:51:39 +0100 |
commit | f0285618c33bd0a48b2d80b905371589ade85c79 (patch) | |
tree | 6359a75e987c3e5e1ee6311a7cd3b5e4c83fa7ca /NorthstarDLL | |
parent | 5fde176a9a3cc746f8aab24fb850e5b90aa51710 (diff) | |
download | NorthstarLauncher-f0285618c33bd0a48b2d80b905371589ade85c79.tar.gz NorthstarLauncher-f0285618c33bd0a48b2d80b905371589ade85c79.zip |
Implement HTTP requests for Squirrel scripts (#344)
* Native HttpRequestHandler to make HTTP requests from Squirrel
* Init handler & register SQ funcs
# Conflicts:
# NorthstarDLL/NorthstarDLL.vcxproj
# NorthstarDLL/NorthstarDLL.vcxproj.filters
* Allow redirects, fix crashing with buffer.
* Also read response header
* Remove leftover header debug stuffs
* Prevent private network requests unless -allowlocalhttp is in the command line.
* Minor tweak
* Support all HTTP methods, private network check changes.
* Add OPTIONS, timeout & user-agent overrides.
* Add -disablehttprequests, final tweaks.
* Fix one of the private ipv4 ranges
* Native HttpRequestHandler to make HTTP requests from Squirrel
* Init handler & register SQ funcs
# Conflicts:
# NorthstarDLL/NorthstarDLL.vcxproj
# NorthstarDLL/NorthstarDLL.vcxproj.filters
* Allow redirects, fix crashing with buffer.
* Also read response header
* Remove leftover header debug stuffs
* Prevent private network requests unless -allowlocalhttp is in the command line.
* Minor tweak
* Support all HTTP methods, private network check changes.
* Add OPTIONS, timeout & user-agent overrides.
* Add -disablehttprequests, final tweaks.
* Fix one of the private ipv4 ranges
* Update to latest and fix issues mentionned in previous PR
* Cleanup some return statements
* Also cache IsLocalHttpAllowed(), query param array work
* Cache command line args in ctor, more header/query array work
* Support for multiple values with same key in headers & query params
* Format check made this look disgusting, don't blame me.
* More format fixes
* Tweakerino
* Tell clang-format to segfault itself
* Add -disablehttpssl to ignore SSL verifications
* Fix headers being written to body with custom headers
* Remove useless comments
Diffstat (limited to 'NorthstarDLL')
-rw-r--r-- | NorthstarDLL/NorthstarDLL.vcxproj | 2 | ||||
-rw-r--r-- | NorthstarDLL/NorthstarDLL.vcxproj.filters | 23 | ||||
-rw-r--r-- | NorthstarDLL/httprequesthandler.cpp | 586 | ||||
-rw-r--r-- | NorthstarDLL/httprequesthandler.h | 132 |
4 files changed, 743 insertions, 0 deletions
diff --git a/NorthstarDLL/NorthstarDLL.vcxproj b/NorthstarDLL/NorthstarDLL.vcxproj index f7c16f2a..e0dae8a2 100644 --- a/NorthstarDLL/NorthstarDLL.vcxproj +++ b/NorthstarDLL/NorthstarDLL.vcxproj @@ -395,6 +395,7 @@ <ClInclude Include="bitbuf.h" />
<ClInclude Include="bits.h" />
<ClInclude Include="crashhandler.h" />
+ <ClInclude Include="httprequesthandler.h" />
<ClInclude Include="keyvalues.h" />
<ClInclude Include="loghooks.h" />
<ClInclude Include="squirrelclasstypes.h" />
@@ -455,6 +456,7 @@ <ClCompile Include="concommand.cpp" />
<ClCompile Include="diskvmtfixes.cpp" />
<ClCompile Include="exploitfixes_lzss.cpp" />
+ <ClCompile Include="httprequesthandler.cpp" />
<ClCompile Include="kb_act.cpp" />
<ClCompile Include="keyvalues.cpp" />
<ClCompile Include="limits.cpp" />
diff --git a/NorthstarDLL/NorthstarDLL.vcxproj.filters b/NorthstarDLL/NorthstarDLL.vcxproj.filters index 1de56ce5..a100b2b0 100644 --- a/NorthstarDLL/NorthstarDLL.vcxproj.filters +++ b/NorthstarDLL/NorthstarDLL.vcxproj.filters @@ -1116,14 +1116,34 @@ <ClInclude Include="..\include\spdlog\sinks\tcp_sink.h">
<Filter>Header Files\include\spdlog\sinks</Filter>
</ClInclude>
+
+ <ClInclude Include="squirrelautobind.h">
+ <Filter>Header Files\Squirrel</Filter>
+ </ClInclude>
+ <ClInclude Include="keyvalues.h">
+ <Filter>Header Files</Filter>
+ </ClInclude>
+
<ClInclude Include="..\include\spdlog\sinks\win_eventlog_sink.h">
<Filter>Header Files\include\spdlog\sinks</Filter>
+
</ClInclude>
<ClInclude Include="..\include\spdlog\sinks\wincolor_sink.h">
<Filter>Header Files\include\spdlog\sinks</Filter>
</ClInclude>
+
+ <ClInclude Include="loghooks.h">
+ <Filter>Header Files\Console</Filter>
+ </ClInclude>
+ <ClInclude Include="squirrelclasstypes.h">
+ <Filter>Header Files\Squirrel</Filter>
+ </ClInclude>
<ClInclude Include="..\include\spdlog\sinks\wincolor_sink-inl.h">
<Filter>Header Files\include\spdlog\sinks</Filter>
+
+ </ClInclude>
+ <ClInclude Include="httprequesthandler.h">
+ <Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
@@ -1352,6 +1372,9 @@ <ClCompile Include="loghooks.cpp">
<Filter>Source Files\Console</Filter>
</ClCompile>
+ <ClCompile Include="httprequesthandler.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
</ItemGroup>
<ItemGroup>
<MASM Include="audio_asm.asm">
diff --git a/NorthstarDLL/httprequesthandler.cpp b/NorthstarDLL/httprequesthandler.cpp new file mode 100644 index 00000000..2681b4a3 --- /dev/null +++ b/NorthstarDLL/httprequesthandler.cpp @@ -0,0 +1,586 @@ +#include "pch.h" +#include "httprequesthandler.h" +#include "version.h" +#include "squirrel.h" +#include "tier0.h" + +HttpRequestHandler* g_httpRequestHandler; + +bool IsHttpDisabled() +{ + const static bool bIsHttpDisabled = Tier0::CommandLine()->FindParm("-disablehttprequests"); + return bIsHttpDisabled; +} + +bool IsLocalHttpAllowed() +{ + const static bool bIsLocalHttpAllowed = Tier0::CommandLine()->FindParm("-allowlocalhttp"); + return bIsLocalHttpAllowed; +} + +bool DisableHttpSsl() +{ + const static bool bDisableHttpSsl = Tier0::CommandLine()->FindParm("-disablehttpssl"); + return bDisableHttpSsl; +} + +HttpRequestHandler::HttpRequestHandler() +{ + // Cache the launch parameters as early as possible in order to avoid possible exploits that change them at runtime. + IsHttpDisabled(); + IsLocalHttpAllowed(); + DisableHttpSsl(); +} + +void HttpRequestHandler::StartHttpRequestHandler() +{ + if (IsRunning()) + { + spdlog::warn("%s was called while IsRunning() is true!", __FUNCTION__); + return; + } + + m_bIsHttpRequestHandlerRunning = true; + spdlog::info("HttpRequestHandler started."); +} + +void HttpRequestHandler::StopHttpRequestHandler() +{ + if (!IsRunning()) + { + spdlog::warn("%s was called while IsRunning() is false", __FUNCTION__); + return; + } + + m_bIsHttpRequestHandlerRunning = false; + spdlog::info("HttpRequestHandler stopped."); +} + +bool IsHttpDestinationHostAllowed(const std::string& host, std::string& outHostname, std::string& outAddress, std::string& outPort) +{ + CURLU* url = curl_url(); + if (!url) + { + spdlog::error("Failed to call curl_url() for http request."); + return false; + } + + if (curl_url_set(url, CURLUPART_URL, host.c_str(), CURLU_DEFAULT_SCHEME) != CURLUE_OK) + { + spdlog::error("Failed to parse destination URL for http request."); + + curl_url_cleanup(url); + return false; + } + + char* urlHostname = nullptr; + if (curl_url_get(url, CURLUPART_HOST, &urlHostname, 0) != CURLUE_OK) + { + spdlog::error("Failed to parse hostname from destination URL for http request."); + + curl_url_cleanup(url); + return false; + } + + char* urlScheme = nullptr; + if (curl_url_get(url, CURLUPART_SCHEME, &urlScheme, CURLU_DEFAULT_SCHEME) != CURLUE_OK) + { + spdlog::error("Failed to parse scheme from destination URL for http request."); + + curl_url_cleanup(url); + curl_free(urlHostname); + return false; + } + + char* urlPort = nullptr; + if (curl_url_get(url, CURLUPART_PORT, &urlPort, CURLU_DEFAULT_PORT) != CURLUE_OK) + { + spdlog::error("Failed to parse port from destination URL for http request."); + + curl_url_cleanup(url); + curl_free(urlHostname); + curl_free(urlScheme); + return false; + } + + // Resolve the hostname into an address. + addrinfo* result; + addrinfo hints; + std::memset(&hints, 0, sizeof(addrinfo)); + hints.ai_family = AF_UNSPEC; + + if (getaddrinfo(urlHostname, urlScheme, &hints, &result) != 0) + { + spdlog::error("Failed to resolve http request destination {} using getaddrinfo().", urlHostname); + + curl_url_cleanup(url); + curl_free(urlHostname); + curl_free(urlScheme); + curl_free(urlPort); + return false; + } + + bool bFoundIPv6 = false; + sockaddr_in* sockaddr_ipv4 = nullptr; + for (addrinfo* info = result; info; info = info->ai_next) + { + if (info->ai_family == AF_INET) + { + sockaddr_ipv4 = (sockaddr_in*)info->ai_addr; + break; + } + + bFoundIPv6 = bFoundIPv6 || info->ai_family == AF_INET6; + } + + if (sockaddr_ipv4 == nullptr) + { + if (bFoundIPv6) + { + spdlog::error("Only IPv4 destinations are supported for HTTP requests. To allow IPv6, launch the game using -allowlocalhttp."); + } + else + { + spdlog::error("Failed to resolve http request destination {} into a valid IPv4 address.", urlHostname); + } + + curl_free(urlHostname); + curl_free(urlScheme); + curl_free(urlPort); + curl_url_cleanup(url); + + return false; + } + + // Fast checks for private ranges of IPv4. + // clang-format off + { + auto addrBytes = sockaddr_ipv4->sin_addr.S_un.S_un_b; + + if (addrBytes.s_b1 == 10 // 10.0.0.0 - 10.255.255.255 (Class A Private) + || addrBytes.s_b1 == 172 && addrBytes.s_b2 >= 16 && addrBytes.s_b2 <= 31 // 172.16.0.0 - 172.31.255.255 (Class B Private) + || addrBytes.s_b1 == 192 && addrBytes.s_b2 == 168 // 192.168.0.0 - 192.168.255.255 (Class C Private) + || addrBytes.s_b1 == 192 && addrBytes.s_b2 == 0 && addrBytes.s_b3 == 0 // 192.0.0.0 - 192.0.0.255 (IETF Assignment) + || addrBytes.s_b1 == 192 && addrBytes.s_b2 == 0 && addrBytes.s_b3 == 2 // 192.0.2.0 - 192.0.2.255 (TEST-NET-1) + || addrBytes.s_b1 == 192 && addrBytes.s_b2 == 88 && addrBytes.s_b3 == 99 // 192.88.99.0 - 192.88.99.255 (IPv4-IPv6 Relay) + || addrBytes.s_b1 == 192 && addrBytes.s_b2 >= 18 && addrBytes.s_b2 <= 19 // 192.18.0.0 - 192.19.255.255 (Internet Benchmark) + || addrBytes.s_b1 == 192 && addrBytes.s_b2 == 51 && addrBytes.s_b3 == 100 // 192.51.100.0 - 192.51.100.255 (TEST-NET-2) + || addrBytes.s_b1 == 203 && addrBytes.s_b2 == 0 && addrBytes.s_b3 == 113 // 203.0.113.0 - 203.0.113.255 (TEST-NET-3) + || addrBytes.s_b1 == 169 && addrBytes.s_b2 == 254 // 169.254.00 - 169.254.255.255 (Link-local/APIPA) + || addrBytes.s_b1 == 127 // 127.0.0.0 - 127.255.255.255 (Loopback) + || addrBytes.s_b1 == 0 // 0.0.0.0 - 0.255.255.255 (Current network) + || addrBytes.s_b1 == 100 && addrBytes.s_b2 >= 64 && addrBytes.s_b2 <= 127 // 100.64.0.0 - 100.127.255.255 (Shared address space) + || sockaddr_ipv4->sin_addr.S_un.S_addr == 0xFFFFFFFF // 255.255.255.255 (Broadcast) + || addrBytes.s_b1 >= 224 && addrBytes.s_b2 <= 239 // 224.0.0.0 - 239.255.255.255 (Multicast) + || addrBytes.s_b1 == 233 && addrBytes.s_b2 == 252 && addrBytes.s_b3 == 0 // 233.252.0.0 - 233.252.0.255 (MCAST-TEST-NET) + || addrBytes.s_b1 >= 240 && addrBytes.s_b4 <= 254) // 240.0.0.0 - 255.255.255.254 (Future Use Class E) + { + curl_free(urlHostname); + curl_free(urlScheme); + curl_free(urlPort); + curl_url_cleanup(url); + + return false; + } + } + + // clang-format on + + char resolvedStr[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &sockaddr_ipv4->sin_addr, resolvedStr, INET_ADDRSTRLEN); + + // Use the resolved address as the new request host. + outHostname = urlHostname; + outAddress = resolvedStr; + outPort = urlPort; + + freeaddrinfo(result); + + curl_free(urlHostname); + curl_free(urlScheme); + curl_free(urlPort); + curl_url_cleanup(url); + + return true; +} + +size_t HttpCurlWriteToStringBufferCallback(char* contents, size_t size, size_t nmemb, void* userp) +{ + ((std::string*)userp)->append((char*)contents, size * nmemb); + return size * nmemb; +} + +template <ScriptContext context> int HttpRequestHandler::MakeHttpRequest(const HttpRequest& requestParameters) +{ + if (!IsRunning()) + { + spdlog::warn("%s was called while IsRunning() is false!", __FUNCTION__); + return -1; + } + + if (IsHttpDisabled()) + { + spdlog::warn("NS_InternalMakeHttpRequest called while the game is running with -disablehttprequests." + " Please check if requests are allowed using NSIsHttpEnabled() first."); + return -1; + } + + bool bAllowLocalHttp = IsLocalHttpAllowed(); + + // This handle will be returned to Squirrel so it can wait for the response and assign a callback for it. + int handle = ++m_iLastRequestHandle; + + std::thread requestThread( + [this, handle, requestParameters, bAllowLocalHttp]() + { + std::string hostname, resolvedAddress, resolvedPort; + + if (!bAllowLocalHttp) + { + if (!IsHttpDestinationHostAllowed(requestParameters.baseUrl, hostname, resolvedAddress, resolvedPort)) + { + spdlog::warn( + "HttpRequestHandler::MakeHttpRequest attempted to make a request to a private network. This is only allowed when " + "running the game with -allowlocalhttp."); + g_pSquirrel<context>->AsyncCall( + "NSHandleFailedHttpRequest", + handle, + (int)0, + "Cannot make HTTP requests to private network hosts without -allowlocalhttp. Check your console for more " + "information."); + return; + } + } + + CURL* curl = curl_easy_init(); + if (!curl) + { + spdlog::error("HttpRequestHandler::MakeHttpRequest failed to init libcurl for request."); + g_pSquirrel<context>->AsyncCall( + "NSHandleFailedHttpRequest", handle, static_cast<int>(CURLE_FAILED_INIT), curl_easy_strerror(CURLE_FAILED_INIT)); + return; + } + + // HEAD has no body. + if (requestParameters.method == HttpRequestMethod::HRM_HEAD) + { + curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); + } + + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, HttpRequestMethod::ToString(requestParameters.method).c_str()); + + // Only resolve to IPv4 if we don't allow private network requests. + curl_slist* host = nullptr; + if (!bAllowLocalHttp) + { + curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); + host = curl_slist_append(host, fmt::format("{}:{}:{}", hostname, resolvedPort, resolvedAddress).c_str()); + curl_easy_setopt(curl, CURLOPT_RESOLVE, host); + } + + // Ensure we only allow HTTP or HTTPS. + curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); + + // Allow redirects + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L); + + // Check if the url already contains a query. + // If so, we'll know to append with & instead of start with ? + std::string queryUrl = requestParameters.baseUrl; + bool bUrlContainsQuery = false; + + // If this fails, just ignore the parsing and trust what the user wants to query. + // Probably will fail but handling it here would be annoying. + CURLU* curlUrl = curl_url(); + if (curlUrl) + { + if (curl_url_set(curlUrl, CURLUPART_URL, queryUrl.c_str(), CURLU_DEFAULT_SCHEME) == CURLUE_OK) + { + char* currentQuery; + if (curl_url_get(curlUrl, CURLUPART_QUERY, ¤tQuery, 0) == CURLUE_OK) + { + if (currentQuery && std::strlen(currentQuery) != 0) + { + bUrlContainsQuery = true; + } + } + + curl_free(currentQuery); + } + + curl_url_cleanup(curlUrl); + } + + // GET requests, or POST-like requests with an empty body, can have query parameters. + // Append them to the base url. + if (HttpRequestMethod::CanHaveQueryParameters(requestParameters.method) && + !HttpRequestMethod::UsesCurlPostOptions(requestParameters.method) || + requestParameters.body.empty()) + { + bool isFirstValue = true; + for (const auto& kv : requestParameters.queryParameters) + { + char* key = curl_easy_escape(curl, kv.first.c_str(), kv.first.length()); + + for (const std::string& queryValue : kv.second) + { + char* value = curl_easy_escape(curl, queryValue.c_str(), queryValue.length()); + + if (isFirstValue && !bUrlContainsQuery) + { + queryUrl.append(fmt::format("?{}={}", key, value)); + isFirstValue = false; + } + else + { + queryUrl.append(fmt::format("&{}={}", key, value)); + } + + curl_free(value); + } + + curl_free(key); + } + } + + // If this method uses POST-like curl options, set those and set the body. + // The body won't be sent if it's empty anyway, meaning the query parameters above, if any, would be. + if (HttpRequestMethod::UsesCurlPostOptions(requestParameters.method)) + { + // Grab the body and set it as a POST field + curl_easy_setopt(curl, CURLOPT_POST, 1L); + + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, requestParameters.body.length()); + curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, requestParameters.body.c_str()); + } + + // Set the full URL for this http request. + curl_easy_setopt(curl, CURLOPT_URL, queryUrl.c_str()); + + std::string bodyBuffer; + std::string headerBuffer; + + // Set up buffers to write the response headers and body. + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, HttpCurlWriteToStringBufferCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &bodyBuffer); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, HttpCurlWriteToStringBufferCallback); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &headerBuffer); + + // Add all the headers for the request. + curl_slist* headers = nullptr; + + // Content-Type header for POST-like requests. + if (HttpRequestMethod::UsesCurlPostOptions(requestParameters.method) && !requestParameters.body.empty()) + { + headers = curl_slist_append(headers, fmt::format("Content-Type: {}", requestParameters.contentType).c_str()); + } + + for (const auto& kv : requestParameters.headers) + { + for (const std::string& headerValue : kv.second) + { + headers = curl_slist_append(headers, fmt::format("{}: {}", kv.first, headerValue).c_str()); + } + } + + if (headers != nullptr) + { + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + } + + // Disable SSL checks if requested by the user. + if (DisableHttpSsl()) + { + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYSTATUS, 0L); + } + + // Enforce the Northstar user agent, unless an override was specified. + if (requestParameters.userAgent.empty()) + { + curl_easy_setopt(curl, CURLOPT_USERAGENT, &NSUserAgent); + } + else + { + curl_easy_setopt(curl, CURLOPT_USERAGENT, requestParameters.userAgent.c_str()); + } + + // Set the timeout for this request. Max 60 seconds so mods can't just spin up native threads all the time. + curl_easy_setopt(curl, CURLOPT_TIMEOUT, std::clamp<long>(requestParameters.timeout, 1, 60)); + + CURLcode result = curl_easy_perform(curl); + if (IsRunning()) + { + if (result == CURLE_OK) + { + // While the curl request is OK, it could return a non success code. + // Squirrel side will handle firing the correct callback. + long httpCode = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + g_pSquirrel<context>->AsyncCall( + "NSHandleSuccessfulHttpRequest", handle, static_cast<int>(httpCode), bodyBuffer, headerBuffer); + } + else + { + // Pass CURL result code & error. + spdlog::error( + "curl_easy_perform() failed with code {}, error: {}", static_cast<int>(result), curl_easy_strerror(result)); + + // If it's an SSL issue, tell the user they may disable SSL checks using -disablehttpssl. + if (result == CURLE_PEER_FAILED_VERIFICATION || result == CURLE_SSL_CERTPROBLEM || + result == CURLE_SSL_INVALIDCERTSTATUS) + { + spdlog::error("You can try disabling SSL verifications for this issue using the -disablehttpssl launch argument. " + "Keep in mind this is potentially dangerous!"); + } + + g_pSquirrel<context>->AsyncCall( + "NSHandleFailedHttpRequest", handle, static_cast<int>(result), curl_easy_strerror(result)); + } + } + + curl_easy_cleanup(curl); + curl_slist_free_all(headers); + curl_slist_free_all(host); + }); + + requestThread.detach(); + return handle; +} + +template <ScriptContext context> void HttpRequestHandler::RegisterSQFuncs() +{ + g_pSquirrel<context>->AddFuncRegistration( + "int", + "NS_InternalMakeHttpRequest", + "int method, string baseUrl, table<string, array<string> > headers, table<string, array<string> > queryParams, string contentType, " + "string body, " + "int timeout, string userAgent", + "[Internal use only] Passes the HttpRequest struct fields to be reconstructed in native and used for an http request", + SQ_InternalMakeHttpRequest<context>); + + g_pSquirrel<context>->AddFuncRegistration( + "bool", + "NSIsHttpEnabled", + "", + "Whether or not HTTP requests are enabled. You can opt-out by starting the game with -disablehttprequests.", + SQ_IsHttpEnabled<context>); + + g_pSquirrel<context>->AddFuncRegistration( + "bool", + "NSIsLocalHttpAllowed", + "", + "Whether or not HTTP requests can be made to a private network address. You can enable this by starting the game with " + "-allowlocalhttp.", + SQ_IsLocalHttpAllowed<context>); +} + +// int NS_InternalMakeHttpRequest(int method, string baseUrl, table<string, string> headers, table<string, string> queryParams, +// string contentType, string body, int timeout, string userAgent) +template <ScriptContext context> SQRESULT SQ_InternalMakeHttpRequest(HSquirrelVM* sqvm) +{ + if (!g_httpRequestHandler || !g_httpRequestHandler->IsRunning()) + { + spdlog::warn("NS_InternalMakeHttpRequest called while the http request handler isn't running."); + g_pSquirrel<context>->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; + } + + if (IsHttpDisabled()) + { + spdlog::warn("NS_InternalMakeHttpRequest called while the game is running with -disablehttprequests." + " Please check if requests are allowed using NSIsHttpEnabled() first."); + g_pSquirrel<context>->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; + } + + HttpRequest request; + request.method = static_cast<HttpRequestMethod::Type>(g_pSquirrel<context>->getinteger(sqvm, 1)); + request.baseUrl = g_pSquirrel<context>->getstring(sqvm, 2); + + // Read the tables for headers and query parameters. + SQTable* headerTable = sqvm->_stackOfCurrentFunction[3]._VAL.asTable; + for (int idx = 0; idx < headerTable->_numOfNodes; ++idx) + { + tableNode* node = &headerTable->_nodes[idx]; + + if (node->key._Type == OT_STRING && node->val._Type == OT_ARRAY) + { + SQArray* valueArray = node->val._VAL.asArray; + std::vector<std::string> headerValues; + + for (int vIdx = 0; vIdx < valueArray->_usedSlots; ++vIdx) + { + if (valueArray->_values[vIdx]._Type == OT_STRING) + { + headerValues.push_back(valueArray->_values[vIdx]._VAL.asString->_val); + } + } + + request.headers[node->key._VAL.asString->_val] = headerValues; + } + } + + SQTable* queryTable = sqvm->_stackOfCurrentFunction[4]._VAL.asTable; + for (int idx = 0; idx < queryTable->_numOfNodes; ++idx) + { + tableNode* node = &queryTable->_nodes[idx]; + + if (node->key._Type == OT_STRING && node->val._Type == OT_ARRAY) + { + SQArray* valueArray = node->val._VAL.asArray; + std::vector<std::string> queryValues; + + for (int vIdx = 0; vIdx < valueArray->_usedSlots; ++vIdx) + { + if (valueArray->_values[vIdx]._Type == OT_STRING) + { + queryValues.push_back(valueArray->_values[vIdx]._VAL.asString->_val); + } + } + + request.queryParameters[node->key._VAL.asString->_val] = queryValues; + } + } + + request.contentType = g_pSquirrel<context>->getstring(sqvm, 5); + request.body = g_pSquirrel<context>->getstring(sqvm, 6); + request.timeout = g_pSquirrel<context>->getinteger(sqvm, 7); + request.userAgent = g_pSquirrel<context>->getstring(sqvm, 8); + + int handle = g_httpRequestHandler->MakeHttpRequest<context>(request); + g_pSquirrel<context>->pushinteger(sqvm, handle); + return SQRESULT_NOTNULL; +} + +// bool NSIsHttpEnabled() +template <ScriptContext context> SQRESULT SQ_IsHttpEnabled(HSquirrelVM* sqvm) +{ + g_pSquirrel<context>->pushbool(sqvm, !IsHttpDisabled()); + return SQRESULT_NOTNULL; +} + +// bool NSIsLocalHttpAllowed() +template <ScriptContext context> SQRESULT SQ_IsLocalHttpAllowed(HSquirrelVM* sqvm) +{ + g_pSquirrel<context>->pushbool(sqvm, IsLocalHttpAllowed()); + return SQRESULT_NOTNULL; +} + +ON_DLL_LOAD_RELIESON("client.dll", HttpRequestHandler_ClientInit, ClientSquirrel, (CModule module)) +{ + g_httpRequestHandler->RegisterSQFuncs<ScriptContext::CLIENT>(); + g_httpRequestHandler->RegisterSQFuncs<ScriptContext::UI>(); +} + +ON_DLL_LOAD_RELIESON("server.dll", HttpRequestHandler_ServerInit, ServerSquirrel, (CModule module)) +{ + g_httpRequestHandler->RegisterSQFuncs<ScriptContext::SERVER>(); +} + +ON_DLL_LOAD("engine.dll", HttpRequestHandler_Init, (CModule module)) +{ + g_httpRequestHandler = new HttpRequestHandler; + g_httpRequestHandler->StartHttpRequestHandler(); +} diff --git a/NorthstarDLL/httprequesthandler.h b/NorthstarDLL/httprequesthandler.h new file mode 100644 index 00000000..0f888b6e --- /dev/null +++ b/NorthstarDLL/httprequesthandler.h @@ -0,0 +1,132 @@ +#pragma once + +#include "pch.h" + +enum class ScriptContext; + +// These definitions below should match on the Squirrel side so we can easily pass them along through a function. + +/** + * Allowed methods for an HttpRequest. + */ +namespace HttpRequestMethod +{ + enum Type + { + HRM_GET = 0, + HRM_POST = 1, + HRM_HEAD = 2, + HRM_PUT = 3, + HRM_DELETE = 4, + HRM_PATCH = 5, + HRM_OPTIONS = 6, + }; + + /** Returns the HTTP string representation of the given method. */ + inline std::string ToString(HttpRequestMethod::Type method) + { + switch (method) + { + case HttpRequestMethod::HRM_GET: + return "GET"; + case HttpRequestMethod::HRM_POST: + return "POST"; + case HttpRequestMethod::HRM_HEAD: + return "HEAD"; + case HttpRequestMethod::HRM_PUT: + return "PUT"; + case HttpRequestMethod::HRM_DELETE: + return "DELETE"; + case HttpRequestMethod::HRM_PATCH: + return "PATCH"; + case HttpRequestMethod::HRM_OPTIONS: + return "OPTIONS"; + default: + return "INVALID"; + } + } + + /** Whether or not the given method should be treated like a POST for curlopts. */ + bool UsesCurlPostOptions(HttpRequestMethod::Type method) + { + switch (method) + { + case HttpRequestMethod::HRM_POST: + case HttpRequestMethod::HRM_PUT: + case HttpRequestMethod::HRM_DELETE: + case HttpRequestMethod::HRM_PATCH: + return true; + default: + return false; + } + } + + /** Whether or not the given http request method can have query parameters in the URL. */ + bool CanHaveQueryParameters(HttpRequestMethod::Type method) + { + return method == HttpRequestMethod::HRM_GET || UsesCurlPostOptions(method); + } +}; // namespace HttpRequestMethod + +/** Contains data about an http request that has been queued. */ +struct HttpRequest +{ + /** Method used for this http request. */ + HttpRequestMethod::Type method; + + /** Base URL of this http request. */ + std::string baseUrl; + + /** Headers used for this http request. Some may get overridden or ignored. */ + std::unordered_map<std::string, std::vector<std::string>> headers; + + /** Query parameters for this http request. */ + std::unordered_map<std::string, std::vector<std::string>> queryParameters; + + /** The content type of this http request. Defaults to text/plain & UTF-8 charset. */ + std::string contentType = "text/plain; charset=utf-8"; + + /** The body of this http request. If set, will override queryParameters.*/ + std::string body; + + /** The timeout for the http request, in seconds. Must be between 1 and 60. */ + int timeout; + + /** If set, the override to use for the User-Agent header. */ + std::string userAgent; +}; + +/** + * Handles making HTTP requests and sending the responses back to Squirrel. + */ +class HttpRequestHandler +{ + public: + HttpRequestHandler(); + + // Start/Stop the HTTP request handler. Right now this doesn't do much. + void StartHttpRequestHandler(); + void StopHttpRequestHandler(); + + // Whether or not this http request handler is currently running. + bool IsRunning() const + { + return m_bIsHttpRequestHandlerRunning; + } + + /** + * Creates a new thread to execute an HTTP request. + * @param requestParameters The parameters to use for this http request. + * @returns The handle for the http request being sent, or -1 if the request failed. + */ + template <ScriptContext context> int MakeHttpRequest(const HttpRequest& requestParameters); + + /** Registers the HTTP request Squirrel functions for the given script context. */ + template <ScriptContext context> void RegisterSQFuncs(); + + private: + int m_iLastRequestHandle = 0; + std::atomic_bool m_bIsHttpRequestHandlerRunning = false; +}; + +extern HttpRequestHandler* g_httpRequestHandler; |