From f0285618c33bd0a48b2d80b905371589ade85c79 Mon Sep 17 00:00:00 2001 From: Erlite Date: Tue, 13 Dec 2022 17:51:39 +0100 Subject: 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 --- NorthstarDLL/httprequesthandler.cpp | 586 ++++++++++++++++++++++++++++++++++++ 1 file changed, 586 insertions(+) create mode 100644 NorthstarDLL/httprequesthandler.cpp (limited to 'NorthstarDLL/httprequesthandler.cpp') 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 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->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->AsyncCall( + "NSHandleFailedHttpRequest", handle, static_cast(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(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->AsyncCall( + "NSHandleSuccessfulHttpRequest", handle, static_cast(httpCode), bodyBuffer, headerBuffer); + } + else + { + // Pass CURL result code & error. + spdlog::error( + "curl_easy_perform() failed with code {}, error: {}", static_cast(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->AsyncCall( + "NSHandleFailedHttpRequest", handle, static_cast(result), curl_easy_strerror(result)); + } + } + + curl_easy_cleanup(curl); + curl_slist_free_all(headers); + curl_slist_free_all(host); + }); + + requestThread.detach(); + return handle; +} + +template void HttpRequestHandler::RegisterSQFuncs() +{ + g_pSquirrel->AddFuncRegistration( + "int", + "NS_InternalMakeHttpRequest", + "int method, string baseUrl, table > headers, table > 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); + + g_pSquirrel->AddFuncRegistration( + "bool", + "NSIsHttpEnabled", + "", + "Whether or not HTTP requests are enabled. You can opt-out by starting the game with -disablehttprequests.", + SQ_IsHttpEnabled); + + g_pSquirrel->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); +} + +// int NS_InternalMakeHttpRequest(int method, string baseUrl, table headers, table queryParams, +// string contentType, string body, int timeout, string userAgent) +template 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->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->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; + } + + HttpRequest request; + request.method = static_cast(g_pSquirrel->getinteger(sqvm, 1)); + request.baseUrl = g_pSquirrel->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 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 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->getstring(sqvm, 5); + request.body = g_pSquirrel->getstring(sqvm, 6); + request.timeout = g_pSquirrel->getinteger(sqvm, 7); + request.userAgent = g_pSquirrel->getstring(sqvm, 8); + + int handle = g_httpRequestHandler->MakeHttpRequest(request); + g_pSquirrel->pushinteger(sqvm, handle); + return SQRESULT_NOTNULL; +} + +// bool NSIsHttpEnabled() +template SQRESULT SQ_IsHttpEnabled(HSquirrelVM* sqvm) +{ + g_pSquirrel->pushbool(sqvm, !IsHttpDisabled()); + return SQRESULT_NOTNULL; +} + +// bool NSIsLocalHttpAllowed() +template SQRESULT SQ_IsLocalHttpAllowed(HSquirrelVM* sqvm) +{ + g_pSquirrel->pushbool(sqvm, IsLocalHttpAllowed()); + return SQRESULT_NOTNULL; +} + +ON_DLL_LOAD_RELIESON("client.dll", HttpRequestHandler_ClientInit, ClientSquirrel, (CModule module)) +{ + g_httpRequestHandler->RegisterSQFuncs(); + g_httpRequestHandler->RegisterSQFuncs(); +} + +ON_DLL_LOAD_RELIESON("server.dll", HttpRequestHandler_ServerInit, ServerSquirrel, (CModule module)) +{ + g_httpRequestHandler->RegisterSQFuncs(); +} + +ON_DLL_LOAD("engine.dll", HttpRequestHandler_Init, (CModule module)) +{ + g_httpRequestHandler = new HttpRequestHandler; + g_httpRequestHandler->StartHttpRequestHandler(); +} -- cgit v1.2.3