From 337c91b70973bb16d369465ae4956705dce7d107 Mon Sep 17 00:00:00 2001 From: dank_meme01 <42031238+dankmeme01@users.noreply.github.com> Date: Thu, 7 Dec 2023 20:33:22 +0100 Subject: [PATCH] geode web utils suck!!! --- .gitignore | 4 +- CMakeLists.txt | 31 ++-- README.md | 13 +- android-build.md | 35 ++++ src/data/bytebuffer.hpp | 5 +- src/managers/central_server_manager.cpp | 4 + src/managers/central_server_manager.hpp | 1 + src/managers/game_server_manager.cpp | 2 +- src/managers/game_server_manager.hpp | 2 +- src/net/http/client.cpp | 117 ++++++++++++++ src/net/http/client.hpp | 28 ++++ src/net/http/request.cpp | 149 ++++++++++++++++++ src/net/http/request.hpp | 81 ++++++++++ src/net/http/structs.hpp | 41 +++++ src/ui/hooks/play_layer.hpp | 2 +- src/ui/menu/main/globed_menu_layer.cpp | 82 +++++----- src/ui/menu/main/globed_menu_layer.hpp | 11 +- src/ui/menu/main/server_list_cell.cpp | 29 ++-- src/ui/menu/main/signup_popup.cpp | 91 ++++++----- src/ui/menu/main/signup_popup.hpp | 1 + .../menu/server_switcher/add_server_popup.cpp | 3 + .../server_switcher/direct_connect_popup.cpp | 1 - src/ui/menu/server_switcher/server_cell.cpp | 7 + .../server_switcher/server_switcher_popup.cpp | 2 +- .../server_switcher/server_test_popup.cpp | 106 ++++++------- .../server_switcher/server_test_popup.hpp | 7 +- src/util/sync.hpp | 5 + 27 files changed, 677 insertions(+), 183 deletions(-) create mode 100644 android-build.md create mode 100644 src/net/http/client.cpp create mode 100644 src/net/http/client.hpp create mode 100644 src/net/http/request.cpp create mode 100644 src/net/http/request.hpp create mode 100644 src/net/http/structs.hpp diff --git a/.gitignore b/.gitignore index b1ecf72d..2b6227fb 100644 --- a/.gitignore +++ b/.gitignore @@ -67,4 +67,6 @@ server/game/flamegraph.svg server/game/perf.data server/game/perf.data.old server/game/test-flamegraph.sh -server/game/test-bench.sh \ No newline at end of file +server/game/test-bench.sh + +libs/openssl \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 2b162497..e078cd1b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,10 +7,11 @@ set(CMAKE_CXX_VISIBILITY_PRESET hidden) project(globed2 VERSION 1.0.0) option(ENABLE_DEBUG "Debug mode" OFF) -if (ENABLE_DEBUG) +if (ENABLE_DEBUG OR CMAKE_BUILD_TYPE STREQUAL "Debug") add_compile_definitions(GLOBED_DEBUG=1) + add_compile_definitions(GEODE_DEBUG=1) else() - # Enable LTO in release (2.5x less binary size, no noticable compile time hit) + # Enable LTO in release (2.5x less binary size, costs only a few extra seconds of build time) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) endif() @@ -29,18 +30,13 @@ if (CMAKE_HOST_SYSTEM MATCHES "Linux" AND CMAKE_SYSTEM_NAME STREQUAL "Windows") add_compile_options("-march=skylake") endif() -# enable exceptions on android +# enable exceptions on android (this doesn't work !!!!!!! fix me!!!) if (ANDROID) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fexceptions") endif() add_library(${PROJECT_NAME} SHARED ${SOURCES}) -# enable extra warnings -if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") - target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-c++11-compat -Wno-c++14-compat -Wno-c++17-compat -Wno-old-style-cast -Wno-implicit-int-float-conversion -Wno-global-constructors -Wno-pre-c++20-compat-pedantic -Wno-exit-time-destructors -Wno-reserved-identifier -Wno-reserved-macro-identifier -Wno-dollar-in-identifier-extension -Wno-ctad-maybe-unsupported -Wno-unsafe-buffer-usage -Wno-newline-eof -Wno-shadow -Wno-inconsistent-missing-destructor-override -Wno-float-conversion -Wno-shorten-64-to-32 -Wno-sign-conversion -Wno-suggest-destructor-override -Wno-suggest-override -Wno-overloaded-virtual -Wno-unused-parameter -Wno-undefined-func-template -Wno-non-virtual-dtor -Wno-sign-compare -Wno-header-hygiene) -endif() - if (NOT DEFINED ENV{GEODE_SDK}) message(FATAL_ERROR "Unable to find Geode SDK! Please define GEODE_SDK environment variable to point to Geode") else() @@ -52,13 +48,28 @@ add_subdirectory($ENV{GEODE_SDK} ${CMAKE_CURRENT_BINARY_DIR}/geode) # done so you can include root files with target_include_directories(${PROJECT_NAME} PRIVATE src/) +# libcurl ssl support +if (CMAKE_SYSTEM_NAME STREQUAL "Windows") + # on windows we just use schannel, no extra setup + set(CURL_USE_SCHANNEL ON) + set(CURL_USE_OPENSSL OFF) +elseif(ANDROID) + # on android we use geode provided openssl library and our own headers + set(CURL_USE_SCHANNEL OFF) + set(CURL_USE_OPENSSL ON) + set(OPENSSL_ROOT_DIR "${CMAKE_SOURCE_DIR}/libs/openssl") + set(OPENSSL_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/libs/openssl/include") + set(OPENSSL_CRYPTO_LIBRARY "$ENV{GEODE_SDK}/loader/include/link/android/libcrypto.a") + set(OPENSSL_SSL_LIBRARY "$ENV{GEODE_SDK}/loader/include/link/android/libssl.a") +endif() + # our favorite libraries CPMAddPackage("gh:camila314/UIBuilder#main") CPMAddPackage("gh:xiph/opus#master") CPMAddPackage("gh:dankmeme01/libsodium-cmake#master") +CPMAddPackage("gh:curl/curl#curl-8_5_0") -# TODO suppress -Wpointer-sign from libsodium -target_link_libraries(${PROJECT_NAME} UIBuilder opus sodium) +target_link_libraries(${PROJECT_NAME} UIBuilder opus sodium libcurl) # link to fmod on android if (ANDROID) diff --git a/README.md b/README.md index 39cf957b..5078688a 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,10 @@ Known issues: See the [server readme](./server/readme.md) for more information about the server and how you can host it. +## Building + +Building is the same as building any other Geode mod, except for Android. If you want to build the mod for Android, see the [android build readme](./android-build.md) for instructions on how to do it. + ## Credit ca7x3, Firee, Croozington, Coloride, Cvolton, mat, alk, maki, xTymon - thank you for being awesome, whether it's because you helped me directly, suggested ideas, helped with testing, or if I just found you awesome in general :D @@ -42,7 +46,8 @@ RobTop - thank you for releasing this awesome game :) Globed is licensed under the MIT license. All of the following libraries used in Globed have their own licenses you may want to read: -* [Geode](https://geode-sdk.org/) - [BSL 1.0](https://github.com/geode-sdk/geode/blob/main/LICENSE.txt) -* [UIBuilder](https://github.com/camila314/uibuilder) - [MIT](https://github.com/camila314/uibuilder/blob/main/LICENSE) -* [Opus](https://github.com/xiph/opus) - [BSD 3-Clause (?)](https://github.com/xiph/opus/blob/master/COPYING) -* [libsodium](https://github.com/jedisct1/libsodium) - [ISC](https://github.com/jedisct1/libsodium/blob/master/LICENSE) \ No newline at end of file +* Geode - [website](https://geode-sdk.org) - [repository](https://github.com/geode-sdk/geode) - [license](https://github.com/geode-sdk/geode/blob/main/LICENSE.txt) +* UIBuilder - [repository](https://github.com/camila314/uibuilder) - [license](https://github.com/camila314/uibuilder/blob/main/LICENSE) +* Opus - [website](https://opus-codec.org/) - [repository](https://github.com/xiph/opus) - [license](https://github.com/xiph/opus/blob/master/COPYING) +* libsodium - [website](https://libsodium.gitbook.io/doc/) - [repository](https://github.com/jedisct1/libsodium) - [license](https://github.com/jedisct1/libsodium/blob/master/LICENSE) +* Curl - [website](https://curl.se/) - [repository](https://github.com/curl/curl) - [license](https://curl.se/docs/copyright.html) \ No newline at end of file diff --git a/android-build.md b/android-build.md new file mode 100644 index 00000000..e7dc1fa1 --- /dev/null +++ b/android-build.md @@ -0,0 +1,35 @@ +## android building + +first it's time to clone openssl: + +```sh +cd libs/ +git clone https://github.com/openssl/openssl.git +cd openssl +``` + +then you want to configure it: + +```sh +export NDK_HOME=/opt/android-ndk # set path to your ndk if it's different +PATH=$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin:$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64:$PATH ./Configure android-arm -D__ANDROID_API__=24 +``` + +then you want to build it, BUT keep in mind there's no need to build the whole thing. all you want is for codegen to run, you won't need the actual built libraries as they are already provided by geode. + +```sh +make +``` + +after you stop seeing lines that look similar to these: + +```sh +/usr/bin/perl "-I." -Mconfigdata "util/dofile.pl" "-oMakefile" include/openssl/x509.h.in > include/openssl/x509.h +/usr/bin/perl "-I." -Mconfigdata "util/dofile.pl" "-oMakefile" include/openssl/x509_vfy.h.in > include/openssl/x509_vfy.h +/usr/bin/perl "-I." -Mconfigdata "util/dofile.pl" "-oMakefile" include/openssl/x509v3.h.in > include/openssl/x509v3.h +/usr/bin/perl "-I." -Mconfigdata "util/dofile.pl" "-oMakefile" test/provider_internal_test.cnf.in > test/provider_internal_test.cnf +``` + +you can Ctrl+C and stop the build. **If `make` fails and shows an error, that is fine! As long as the headers were generated properly, there is no need to worry.** + +after that's done you just proceed to build the mod like any other android mod. **make sure to set minimum SDK version to 24, or you will get linker errors!** \ No newline at end of file diff --git a/src/data/bytebuffer.hpp b/src/data/bytebuffer.hpp index cc13bfe2..40ad913f 100644 --- a/src/data/bytebuffer.hpp +++ b/src/data/bytebuffer.hpp @@ -284,7 +284,8 @@ class ByteBuffer { // Write an enum template void writeEnum(const E& val) { - using P = std::underlying_type::type; + // macos clang is so fire + using P = typename std::underlying_type::type; static_assert(util::data::IsPrimitive

, "enum underlying type must be a primitive"); this->writePrimitive

(static_cast

(val)); @@ -294,7 +295,7 @@ class ByteBuffer { // TODO no validation, likely not planning to add but still important to know template E readEnum() { - using P = std::underlying_type::type; + using P = typename std::underlying_type::type; static_assert(util::data::IsPrimitive

, "enum underlying type must be a primitive"); return static_cast(this->readPrimitive

()); diff --git a/src/managers/central_server_manager.cpp b/src/managers/central_server_manager.cpp index b11ad93e..91aa10a9 100644 --- a/src/managers/central_server_manager.cpp +++ b/src/managers/central_server_manager.cpp @@ -56,6 +56,10 @@ std::optional CentralServerManager::getActive() { return servers->at(idx); } +int CentralServerManager::getActiveIndex() { + return _activeIdx.load(); +} + std::vector CentralServerManager::getAllServers() { return *_servers.lock(); } diff --git a/src/managers/central_server_manager.hpp b/src/managers/central_server_manager.hpp index 6cc4149f..382c9ad8 100644 --- a/src/managers/central_server_manager.hpp +++ b/src/managers/central_server_manager.hpp @@ -29,6 +29,7 @@ class CentralServerManager { void setActive(int index); // get the current active server, thread safe std::optional getActive(); + int getActiveIndex(); // get all central servers, thread safe std::vector getAllServers(); diff --git a/src/managers/game_server_manager.cpp b/src/managers/game_server_manager.cpp index 122515c9..1cc5a97c 100644 --- a/src/managers/game_server_manager.cpp +++ b/src/managers/game_server_manager.cpp @@ -33,7 +33,7 @@ size_t GameServerManager::count() { return _data.lock()->servers.size(); } -void GameServerManager::setActive(const std::string id) { +void GameServerManager::setActive(const std::string& id) { auto data = _data.lock(); data->active = id; } diff --git a/src/managers/game_server_manager.hpp b/src/managers/game_server_manager.hpp index d8a72363..9975cd7a 100644 --- a/src/managers/game_server_manager.hpp +++ b/src/managers/game_server_manager.hpp @@ -37,7 +37,7 @@ class GameServerManager { void clear(); size_t count(); - void setActive(const std::string id); + void setActive(const std::string& id); std::string active(); void clearActive(); diff --git a/src/net/http/client.cpp b/src/net/http/client.cpp new file mode 100644 index 00000000..9732085b --- /dev/null +++ b/src/net/http/client.cpp @@ -0,0 +1,117 @@ +#include "client.hpp" + +#include + +GLOBED_SINGLETON_DEF(GHTTPClient) + +GHTTPClient::GHTTPClient() { + curl = curl_easy_init(); + GLOBED_REQUIRE(curl != nullptr, "cURL failed to initialize") + + threadHandle = std::thread(&GHTTPClient::threadFunc, this); +} + +GHTTPClient::~GHTTPClient() { + _running = false; + if (threadHandle.joinable()) threadHandle.join(); + + if (curl) { + curl_easy_cleanup(curl); + curl = nullptr; + } + + geode::log::debug("HTTP client thread halted"); +} + +void GHTTPClient::send(GHTTPRequestHandle request) { + requests.push(request); +} + +void GHTTPClient::threadFunc() { + while (_running) { + if (!requests.waitForMessages(util::time::secs(1))) continue; + + auto request = requests.pop(); + auto response = this->performRequest(request); + + geode::Loader::get()->queueInMainThread([response, request] { + request.maybeCallback(response); + }); + } +} + +static size_t writeCallback(void* contents, size_t size, size_t nmemb, std::ostringstream* stream) { + size_t totalSize = size * nmemb; + *stream << std::string(static_cast(contents), totalSize); + return totalSize; +} + +GHTTPResponse GHTTPClient::performRequest(GHTTPRequestHandle handle) { + GHTTPResponse response; + + GHTTPRequest& req = *handle.handle.get(); + + // clear leftover data from previous request + curl_easy_reset(curl); + + switch (req.rType) { + case GHTTPRequestType::GET: + break; + case GHTTPRequestType::POST: + curl_easy_setopt(curl, CURLOPT_POST, 1L); + break; + case GHTTPRequestType::PUT: + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); + break; + case GHTTPRequestType::DELETE_: + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); + break; + } + + if (!req.rData.empty()) { + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req.rData.c_str()); + } + + curl_easy_setopt(curl, CURLOPT_USERAGENT, req.rUserAgent.c_str()); + curl_easy_setopt(curl, CURLOPT_URL, req.rUrl.c_str()); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, req.rFollowRedirects); + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, (long)req.rTimeout); + + // security is for nerds + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + + // http headers + struct curl_slist* headerList = nullptr; + if (!req.rHeaders.empty()) { + for (const auto& header: req.rHeaders) { + headerList = curl_slist_append(headerList, header.c_str()); + } + + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headerList); + } + + std::ostringstream ss; + + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ss); + + response.resCode = curl_easy_perform(curl); + response.failed = response.resCode != CURLE_OK; + + if (response.failed) { + response.failMessage = curl_easy_strerror(response.resCode); + } else { + long httpCode; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + + response.statusCode = httpCode; + response.response = ss.str(); + } + + if (headerList != nullptr) { + curl_slist_free_all(headerList); + } + + return response; +} \ No newline at end of file diff --git a/src/net/http/client.hpp b/src/net/http/client.hpp new file mode 100644 index 00000000..fa4879de --- /dev/null +++ b/src/net/http/client.hpp @@ -0,0 +1,28 @@ +#pragma once +#include + +#include +#include + +#include "request.hpp" +#include + +class GHTTPClient { + GLOBED_SINGLETON(GHTTPClient) + GHTTPClient(); + ~GHTTPClient(); + + // add the request to the queue of pending requests + void send(GHTTPRequestHandle request); + +protected: + CURL* curl; + std::thread threadHandle; + + util::sync::AtomicBool _running = true; + util::sync::SmartMessageQueue requests; + + void threadFunc(); + + GHTTPResponse performRequest(GHTTPRequestHandle req); +}; \ No newline at end of file diff --git a/src/net/http/request.cpp b/src/net/http/request.cpp new file mode 100644 index 00000000..4b0ab0a2 --- /dev/null +++ b/src/net/http/request.cpp @@ -0,0 +1,149 @@ +#include "request.hpp" + +#include "client.hpp" + +void GHTTPRequestHandle::discardResult() const { + handle->_discard = true; +} + +void GHTTPRequestHandle::maybeCallback(const GHTTPResponse& response) const { + if (!handle->_discard) { + handle->callcb(response); + } +} + +void GHTTPRequest::callcb(const GHTTPResponse& response) const { + callback(response); +} + +GHTTPRequest::GHTTPRequest(GHTTPRequestType type) : rType(type) {} +GHTTPRequest::GHTTPRequest() {} + +GHTTPRequest GHTTPRequest::get() { + return GHTTPRequest(GHTTPRequestType::GET); +} + +GHTTPRequest GHTTPRequest::get(const std::string& url) { + auto req = GHTTPRequest(GHTTPRequestType::GET); + req.rUrl = url; + return req; +} + +GHTTPRequest GHTTPRequest::post() { + return GHTTPRequest(GHTTPRequestType::POST); +} + +GHTTPRequest GHTTPRequest::post(const std::string& url) { + auto req = GHTTPRequest(GHTTPRequestType::POST); + req.rUrl = url; + return req; +} + +GHTTPRequest GHTTPRequest::post(const std::string& url, const std::string& data) { + auto req = GHTTPRequest(GHTTPRequestType::POST); + req.rUrl = url; + req.rData = data; + return req; +} + +GHTTPRequest GHTTPRequest::put() { + return GHTTPRequest(GHTTPRequestType::PUT); +} + +GHTTPRequest GHTTPRequest::put(const std::string& url) { + auto req = GHTTPRequest(GHTTPRequestType::PUT); + req.rUrl = url; + return req; +} + +GHTTPRequest GHTTPRequest::put(const std::string& url, const std::string& data) { + auto req = GHTTPRequest(GHTTPRequestType::PUT); + req.rUrl = url; + req.rData = data; + return req; +} + +GHTTPRequest GHTTPRequest::delete_() { + return GHTTPRequest(GHTTPRequestType::DELETE_); +} + +GHTTPRequest GHTTPRequest::delete_(const std::string& url) { + auto req = GHTTPRequest(GHTTPRequestType::DELETE_); + req.rUrl = url; + return req; +} + +GHTTPRequest& GHTTPRequest::url(const std::string& addr) { + rUrl = addr; + return *this; +} + +GHTTPRequest& GHTTPRequest::userAgent(const std::string& agent) { + rUserAgent = agent; + return *this; +} + +GHTTPRequest& GHTTPRequest::followRedirects(bool follow) { + rFollowRedirects = follow; + return *this; +} + +GHTTPRequest& GHTTPRequest::data(const std::string& dataStr) { + rData = dataStr; + return *this; +} + +GHTTPRequest& GHTTPRequest::timeout(uint32_t timeoutMs) { + rTimeout = timeoutMs; + return *this; +} + +GHTTPRequest& GHTTPRequest::contentType(const std::string& ctype) { + return this->header("Content-Type", ctype); +} + +GHTTPRequest& GHTTPRequest::contentType(GHTTPContentType ctype) { + const char* headerValue; + switch (ctype) { + case GHTTPContentType::FORM_URLENCODED: + headerValue = "application/x-www-form-urlencoded"; + break; + case GHTTPContentType::JSON: + headerValue = "application/json"; + break; + case GHTTPContentType::TEXT: + headerValue = "text/plain"; + break; + case GHTTPContentType::DATA: + headerValue = "application/octet-stream"; + break; + } + + return this->header("Content-Type", headerValue); +} + +GHTTPRequest& GHTTPRequest::header(const std::string& key, const std::string& value) { + return this->header(key + ": " + value); +} + +GHTTPRequest& GHTTPRequest::header(const std::string& header) { + rHeaders.push_back(header); + return *this; +} + +GHTTPRequest& GHTTPRequest::then(CallbackFunc cbFunc) { + callback = cbFunc; + return *this; +} + +GHTTPRequestHandle GHTTPRequest::send(GHTTPClient& client) { + auto sptr = std::make_shared(*this); + auto handle = GHTTPRequestHandle(sptr); + + client.send(handle); + return handle; +} + +GHTTPRequestHandle GHTTPRequest::send() { + return this->send(GHTTPClient::get()); +} diff --git a/src/net/http/request.hpp b/src/net/http/request.hpp new file mode 100644 index 00000000..ab31a951 --- /dev/null +++ b/src/net/http/request.hpp @@ -0,0 +1,81 @@ +#pragma once +#include + +#include + +#include "structs.hpp" +#include +#include + +class GHTTPClient; +class GHTTPRequest; + +class GHTTPRequestHandle { +public: + GHTTPRequestHandle(std::shared_ptr handle) : handle(handle) {} + void discardResult() const; + // calls the callback if the `discardResult` hasn't been called earlier + void maybeCallback(const GHTTPResponse& response) const; + + std::shared_ptr handle; +}; + +class GHTTPRequest { +public: + using CallbackFunc = std::function; + + // the functions you call at the start + static GHTTPRequest get(); + static GHTTPRequest get(const std::string& url); + static GHTTPRequest post(); + static GHTTPRequest post(const std::string& url); + static GHTTPRequest post(const std::string& url, const std::string& data); + static GHTTPRequest put(); + static GHTTPRequest put(const std::string& url); + static GHTTPRequest put(const std::string& url, const std::string& data); + static GHTTPRequest delete_(); + static GHTTPRequest delete_(const std::string& url); + + GHTTPRequest& url(const std::string& addr); + GHTTPRequest& userAgent(const std::string& agent); + GHTTPRequest& followRedirects(bool follow = true); + GHTTPRequest& data(const std::string& dataStr); + template + GHTTPRequest& timeout(util::time::duration duration) { + return this->timeout(util::time::asMillis(duration)); + } + GHTTPRequest& timeout(uint32_t timeoutMs); + + // these two set the Content-Type header + GHTTPRequest& contentType(const std::string& ctype); + GHTTPRequest& contentType(GHTTPContentType ctype); + + // for header() either pass two args for key and value, or one in format "Authorization: xxx" + GHTTPRequest& header(const std::string& key, const std::string& value); + GHTTPRequest& header(const std::string& header); + + // set the callback + GHTTPRequest& then(CallbackFunc cbFunc); + + // Shorthand for GHTTPRequest::send(this) + GHTTPRequestHandle send(GHTTPClient& client); + GHTTPRequestHandle send(); + +protected: + friend class GHTTPClient; + friend class GHTTPRequestHandle; + + CallbackFunc callback; + GHTTPRequestType rType; + std::string rUrl, rUserAgent = "GHTTPClient/1.0", rData; + uint32_t rTimeout = 0; + std::vector rHeaders; + bool rFollowRedirects = true; + util::sync::AtomicBool _discard = false; + + GHTTPRequest(GHTTPRequestType type); + GHTTPRequest(); + + // calls the callback + void callcb(const GHTTPResponse& response) const; +}; diff --git a/src/net/http/structs.hpp b/src/net/http/structs.hpp new file mode 100644 index 00000000..6eeb99f3 --- /dev/null +++ b/src/net/http/structs.hpp @@ -0,0 +1,41 @@ +#pragma once + +extern "C" { +#include +} + +struct GHTTPResponse { + // curlcode indicating the result, is always set + CURLcode resCode; + + // failed is *only* true when a cURL error happened. If the server responded with 4xx or 5xx, it is false. + bool failed; + // is set to be a human readable string if `failed == true` + std::string failMessage; + + // Is set to the HTTP status code if `failed == false`, otherwise is set to 0. + int statusCode = 0; + // is set to be a response if `failed == false`, otherwise is empty + std::string response; + + // returns `true` if any kind of failure occurred, whether a curl error or a non 2xx status code + bool anyfail() const { + return failed || !(statusCode >= 200 && statusCode < 300); + } + + // returns `failMessage` if a curl error occurred, otherwise `response` if the status code isn't 2xx + std::string anyfailmsg() const { + return failed ? failMessage : response; + } +}; + +enum class GHTTPRequestType { + GET, POST, PUT, DELETE_ +}; + +enum class GHTTPContentType { + FORM_URLENCODED, + DATA, + JSON, + TEXT, +}; diff --git a/src/ui/hooks/play_layer.hpp b/src/ui/hooks/play_layer.hpp index f680697c..4b2911e7 100644 --- a/src/ui/hooks/play_layer.hpp +++ b/src/ui/hooks/play_layer.hpp @@ -52,7 +52,7 @@ class $modify(GlobedPlayLayer, PlayLayer) { this->setupEventListeners(); - GlobedAudioManager::get().setActiveRecordingDevice(2); + // GlobedAudioManager::get().setActiveRecordingDevice(2); // schedule stuff // TODO - handle sending SyncPlayerMetadataPacket. diff --git a/src/ui/menu/main/globed_menu_layer.cpp b/src/ui/menu/main/globed_menu_layer.cpp index 9f621e97..81021231 100644 --- a/src/ui/menu/main/globed_menu_layer.cpp +++ b/src/ui/menu/main/globed_menu_layer.cpp @@ -1,6 +1,5 @@ #include "globed_menu_layer.hpp" -#include #include #include "server_list_cell.hpp" @@ -126,9 +125,7 @@ bool GlobedMenuLayer::init() { } GlobedMenuLayer::~GlobedMenuLayer() { - if (serverRequestHandle.has_value()) { - serverRequestHandle->get()->cancel(); - } + this->cancelWebRequest(); } CCArray* GlobedMenuLayer::createServerList() { @@ -167,6 +164,7 @@ void GlobedMenuLayer::refreshServerList(float) { // if we recently switched a central server, redo everything if (csm.recentlySwitched) { csm.recentlySwitched = false; + this->cancelWebRequest(); this->requestServerList(); } @@ -206,9 +204,7 @@ void GlobedMenuLayer::refreshServerList(float) { } void GlobedMenuLayer::requestServerList() { - if (serverRequestHandle.has_value()) { - return; - } + this->cancelWebRequest(); auto& csm = CentralServerManager::get(); @@ -224,48 +220,52 @@ void GlobedMenuLayer::requestServerList() { return; } - serverRequestHandle = web::AsyncWebRequest() + serverRequestHandle = GHTTPRequest::get(fmt::format("{}/servers", centralUrl.value().url)) .userAgent(util::net::webUserAgent()) - .fetch(fmt::format("{}/servers", centralUrl.value().url)) - .json() - .then([this](json::Value response) { + .timeout(util::time::secs(3)) + .then([this](const GHTTPResponse& response) { this->serverRequestHandle = std::nullopt; + if (response.anyfail()) { + ErrorQueues::get().error(fmt::format("Failed to fetch servers: {}", response.anyfailmsg())); - auto& gsm = GameServerManager::get(); - gsm.clear(); - gsm.pendingChanges = true; - - try { - auto serverList = response.as_array(); - for (const auto& obj : serverList) { - auto server = obj.as_object(); - gsm.addServer( - server["id"].as_string(), - server["name"].as_string(), - server["address"].as_string(), - server["region"].as_string() - ); - } - } catch (const std::exception& e) { - ErrorQueues::get().error("Failed to parse server list: {}", e.what()); + auto& gsm = GameServerManager::get(); + gsm.clear(); + gsm.pendingChanges = true; + } else { + auto jsonResponse = json::Value::from_str(response.response); + + auto& gsm = GameServerManager::get(); gsm.clear(); + gsm.pendingChanges = true; + + try { + auto serverList = jsonResponse.as_array(); + for (const auto& obj : serverList) { + auto server = obj.as_object(); + gsm.addServer( + server["id"].as_string(), + server["name"].as_string(), + server["address"].as_string(), + server["region"].as_string() + ); + } + } catch (const std::exception& e) { + ErrorQueues::get().error("Failed to parse server list: {}", e.what()); + gsm.clear(); + } } }) - .expect([this](std::string error) { - // why the fuck does geode call the error if the request was cancelled???? - if (error.find("was aborted by an") != std::string::npos) { - return; - } - - this->serverRequestHandle = std::nullopt; + .send(); - ErrorQueues::get().error(fmt::format("Failed to fetch servers: {}", error)); + geode::log::debug("rqeuested server list"); +} - auto& gsm = GameServerManager::get(); - gsm.clear(); - gsm.pendingChanges = true; - }) - .send(); +void GlobedMenuLayer::cancelWebRequest() { + if (serverRequestHandle.has_value()) { + serverRequestHandle->discardResult(); + serverRequestHandle = std::nullopt; + return; + } } void GlobedMenuLayer::keyBackClicked() { diff --git a/src/ui/menu/main/globed_menu_layer.hpp b/src/ui/menu/main/globed_menu_layer.hpp index 229a9f92..db00fce4 100644 --- a/src/ui/menu/main/globed_menu_layer.hpp +++ b/src/ui/menu/main/globed_menu_layer.hpp @@ -1,8 +1,8 @@ #pragma once #include -#include #include "signup_layer.hpp" +#include class GlobedMenuLayer : public cocos2d::CCLayer { public: @@ -17,12 +17,15 @@ class GlobedMenuLayer : public cocos2d::CCLayer { private: GJListLayer* listLayer; GlobedSignupLayer* signupLayer; - std::optional serverRequestHandle; + std::optional serverRequestHandle; + cocos2d::CCSequence* timeoutSequence; bool init(); cocos2d::CCArray* createServerList(); - void refreshServerList(float _); + void refreshServerList(float dt); void requestServerList(); void keyBackClicked(); - void pingServers(float _); + void pingServers(float dt); + + void cancelWebRequest(); }; \ No newline at end of file diff --git a/src/ui/menu/main/server_list_cell.cpp b/src/ui/menu/main/server_list_cell.cpp index 30af2399..a2dc8c13 100644 --- a/src/ui/menu/main/server_list_cell.cpp +++ b/src/ui/menu/main/server_list_cell.cpp @@ -1,14 +1,15 @@ #include "server_list_cell.hpp" -#include #include +#include #include #include #include #include #include #include +#include using namespace geode::prelude; @@ -134,20 +135,20 @@ void ServerListCell::requestTokenAndConnect() { ); // both callbacks should be safe even if GlobedMenuLayer was closed. - web::AsyncWebRequest() - .postRequest() + GHTTPRequest::post(url) .userAgent(util::net::webUserAgent()) - .fetch(url).text() - .then([&am, gsview = this->gsview](const std::string& response) { - *am.authToken.lock() = response; - NetworkManager::get().connectWithView(gsview); - }) - .expect([&am](const std::string& error) { - ErrorQueues::get().error(fmt::format( - "Failed to generate a session token! Please try to login and connect again.\n\nServer response: {}", - error - )); - am.clearAuthKey(); + .timeout(util::time::secs(3)) + .then([&am, gsview = this->gsview](const GHTTPResponse& response) { + if (response.anyfail()) { + ErrorQueues::get().error(fmt::format( + "Failed to generate a session token! Please try to login and connect again.\n\nServer response: {}", + response.anyfailmsg() + )); + am.clearAuthKey(); + } else { + *am.authToken.lock() = response.response; + NetworkManager::get().connectWithView(gsview); + } }) .send(); } diff --git a/src/ui/menu/main/signup_popup.cpp b/src/ui/menu/main/signup_popup.cpp index 72696969..57aad2aa 100644 --- a/src/ui/menu/main/signup_popup.cpp +++ b/src/ui/menu/main/signup_popup.cpp @@ -1,8 +1,8 @@ #include "signup_popup.hpp" #include -#include +#include #include #include #include @@ -30,30 +30,31 @@ bool GlobedSignupPopup::setup() { auto url = csm.getActive()->url + "/challenge/new?aid=" + std::to_string(am.gdData.lock()->accountId); - web::AsyncWebRequest() - .postRequest() + GHTTPRequest::post(url) .userAgent(util::net::webUserAgent()) - .fetch(url).text() - .then([this](const std::string& response) { - auto colonPos = response.find(':'); - auto part1 = response.substr(0, colonPos); - auto part2 = response.substr(colonPos + 1); + .then([this](const GHTTPResponse& resp) { + if (resp.anyfail()) { + auto error = resp.anyfailmsg(); + if (error.empty()) { + this->onFailure("Creating challenge failed: server sent an empty response."); + } else { + this->onFailure("Creating challenge failed: " + util::formatting::formatErrorMessage(error) + ""); + } + } else { + auto response = resp.response; + auto colonPos = response.find(':'); + auto part1 = response.substr(0, colonPos); + auto part2 = response.substr(colonPos + 1); - int levelId; + int levelId; - if (part1 == "none") { - levelId = -1; - } else { - levelId = std::stoi(part1); - } + if (part1 == "none") { + levelId = -1; + } else { + levelId = std::stoi(part1); + } - this->onChallengeCreated(levelId, part2); - }) - .expect([this](const std::string& error) { - if (error.empty()) { - this->onFailure("Creating challenge failed: server sent an empty response."); - } else { - this->onFailure("Creating challenge failed: " + util::formatting::formatErrorMessage(error) + ""); + this->onChallengeCreated(levelId, part2); } }) .send(); @@ -117,30 +118,31 @@ void GlobedSignupPopup::onChallengeCompleted(const std::string& authcode) { gdData->accountName, authcode); - web::AsyncWebRequest() - .postRequest() + GHTTPRequest::post(url) .userAgent(util::net::webUserAgent()) - .fetch(url).text() - .then([this, &am](const std::string& response) { - // we are good! the authkey has been created and can be saved now. - auto colonPos = response.find(':'); - auto commentId = response.substr(0, colonPos); - - log::info("Authkey created successfully, saving."); - auto authkey = util::crypto::base64Decode(response.substr(colonPos + 1)); - am.storeAuthKey(util::crypto::simpleHash(authkey)); - this->onSuccess(); - - // delete the comment to cleanup - if (commentId != "none") { - GameLevelManager::get()->deleteComment(std::stoi(commentId), CommentType::Level, storedLevelId); - } - }) - .expect([this](const std::string& error) { - if (error.empty()) { - this->onFailure("Verifying challenge failed: server sent an empty response."); + .then([this, &am](const GHTTPResponse& resp) { + if (resp.anyfail()) { + auto error = resp.anyfailmsg(); + if (error.empty()) { + this->onFailure("Creating challenge failed: server sent an empty response."); + } else { + this->onFailure("Creating challenge failed: " + util::formatting::formatErrorMessage(error) + ""); + } } else { - this->onFailure("Verifying challenge failed: " + util::formatting::formatErrorMessage(error) + ""); + auto response = resp.response; + // we are good! the authkey has been created and can be saved now. + auto colonPos = response.find(':'); + auto commentId = response.substr(0, colonPos); + + log::info("Authkey created successfully, saving."); + auto authkey = util::crypto::base64Decode(response.substr(colonPos + 1)); + am.storeAuthKey(util::crypto::simpleHash(authkey)); + this->onSuccess(); + + // delete the comment to cleanup + if (commentId != "none") { + GameLevelManager::get()->deleteComment(std::stoi(commentId), CommentType::Level, storedLevelId); + } } }) .send(); @@ -158,6 +160,9 @@ void GlobedSignupPopup::onFailure(const std::string& message) { void GlobedSignupPopup::keyDown(cocos2d::enumKeyCodes) { // do nothing; the popup should be impossible to close manually } +void GlobedSignupPopup::keyBackClicked() { + // do nothing +}; GlobedSignupPopup* GlobedSignupPopup::create() { auto ret = new GlobedSignupPopup; diff --git a/src/ui/menu/main/signup_popup.hpp b/src/ui/menu/main/signup_popup.hpp index 549bb748..e9212349 100644 --- a/src/ui/menu/main/signup_popup.hpp +++ b/src/ui/menu/main/signup_popup.hpp @@ -16,6 +16,7 @@ class GlobedSignupPopup : public geode::Popup<>, public CommentUploadDelegate { bool setup() override; void keyDown(cocos2d::enumKeyCodes key) override; + void keyBackClicked() override; void onFailure(const std::string& message); void onSuccess(); diff --git a/src/ui/menu/server_switcher/add_server_popup.cpp b/src/ui/menu/server_switcher/add_server_popup.cpp index 3c21defb..99a07e64 100644 --- a/src/ui/menu/server_switcher/add_server_popup.cpp +++ b/src/ui/menu/server_switcher/add_server_popup.cpp @@ -78,6 +78,7 @@ bool AddServerPopup::setup(int modifyingIndex, ServerSwitcherPopup* parent) { // we try to connect to the server and see if the versions match ServerTestPopup::create(url, this)->show(); + this->retain(); testedName = name; testedUrl = url; }) @@ -104,12 +105,14 @@ void AddServerPopup::onTestSuccess() { csm.modifyServer(modifyingIndex, newServer); } + this->release(); this->parent->reloadList(); this->onClose(this); } void AddServerPopup::onTestFailure(const std::string& message) { FLAlertLayer::create("Globed error", message.c_str(), "Ok")->show(); + this->release(); this->onClose(this); } diff --git a/src/ui/menu/server_switcher/direct_connect_popup.cpp b/src/ui/menu/server_switcher/direct_connect_popup.cpp index c16b1cc4..367e073f 100644 --- a/src/ui/menu/server_switcher/direct_connect_popup.cpp +++ b/src/ui/menu/server_switcher/direct_connect_popup.cpp @@ -50,7 +50,6 @@ bool DirectConnectionPopup::setup(ServerSwitcherPopup* parent) { auto& gsm = GameServerManager::get(); gsm.clear(); gsm.addServer(GameServerManager::STANDALONE_ID, "Server", addr, "unknown"); - gsm.pendingChanges = true; auto& nm = NetworkManager::get(); diff --git a/src/ui/menu/server_switcher/server_cell.cpp b/src/ui/menu/server_switcher/server_cell.cpp index c7f976e2..7bbbc9ca 100644 --- a/src/ui/menu/server_switcher/server_cell.cpp +++ b/src/ui/menu/server_switcher/server_cell.cpp @@ -76,6 +76,13 @@ bool CentralServerListCell::init(const CentralServer& data, int index, ServerSwi Build::createSpriteName("checkpoint_01_001.png") .intoMenuItem([this](auto) { auto& csm = CentralServerManager::get(); + + // do nothing if we are already connected to this server + if (csm.getActiveIndex() == this->index) { + this->parent->close(); + return; + } + csm.setActive(this->index); csm.recentlySwitched = true; diff --git a/src/ui/menu/server_switcher/server_switcher_popup.cpp b/src/ui/menu/server_switcher/server_switcher_popup.cpp index b0bc12bf..d11f7d30 100644 --- a/src/ui/menu/server_switcher/server_switcher_popup.cpp +++ b/src/ui/menu/server_switcher/server_switcher_popup.cpp @@ -65,7 +65,7 @@ void ServerSwitcherPopup::reloadList() { auto& csm = CentralServerManager::get(); auto servers = csm.getAllServers(); - for (const auto [id, server] : util::collections::enumerate(servers)) { + for (const auto& [id, server] : util::collections::enumerate(servers)) { auto* cell = CentralServerListCell::create(server, id, this); cells->addObject(cell); } diff --git a/src/ui/menu/server_switcher/server_test_popup.cpp b/src/ui/menu/server_switcher/server_test_popup.cpp index 542350a6..618e5c9b 100644 --- a/src/ui/menu/server_switcher/server_test_popup.cpp +++ b/src/ui/menu/server_switcher/server_test_popup.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include using namespace geode::prelude; @@ -21,84 +22,77 @@ bool ServerTestPopup::setup(const std::string& url, AddServerPopup* parent) { .scale(0.35f) .parent(m_mainLayer); - sentRequestHandle = web::AsyncWebRequest() + sentRequestHandle = GHTTPRequest::get(fmt::format("{}/version", url)) .userAgent(util::net::webUserAgent()) - .fetch(fmt::format("{}/version", url)).text() - .then([this](const std::string& response) { + .timeout(util::time::secs(5)) + .then([this](const GHTTPResponse& resp) { sentRequestHandle = std::nullopt; - if (timeoutSequence) { - this->stopAction(timeoutSequence); - timeoutSequence = nullptr; - } - - int protocol = 0; - std::from_chars(response.data(), response.data() + response.size(), protocol); - if (protocol != NetworkManager::PROTOCOL_VERSION) { - this->parent->onTestFailure(fmt::format( - "Failed to add the server due to version mismatch. Client protocol version: v{}, server: v{}", - NetworkManager::PROTOCOL_VERSION, - protocol - )); + if (resp.anyfail()) { + std::string error; + + if (resp.resCode == CURLE_OPERATION_TIMEDOUT) { + error = "Failed to contact the server, no response was received after 5 seconds."; + } else { + auto msg = resp.anyfailmsg(); + if (msg.empty()) { + error = "Error retrieving data from the server: server sent an empty response."; + } else { + error = "Error retrieving data from the server: " + util::formatting::formatErrorMessage(msg) + ""; + } + } + + this->parent->onTestFailure(error); } else { - this->parent->onTestSuccess(); - } - - this->onClose(this); - }) - .expect([this](const std::string& error) { - // why the fuck does geode call the error if the request was cancelled???? - if (error.find("was aborted by an") != std::string::npos) { - return; + auto response = resp.response; + int protocol = 0; +#ifdef GLOBED_ANDROID + // this is such a meme im crying + std::istringstream iss(response); + iss >> protocol; + if (iss.fail() || !iss.eof()) { + protocol = 0; + } +#else + std::from_chars(response.data(), response.data() + response.size(), protocol); +#endif + + if (protocol != NetworkManager::PROTOCOL_VERSION) { + this->parent->onTestFailure(fmt::format( + "Failed to add the server due to version mismatch. Client protocol version: v{}, server: v{}", + NetworkManager::PROTOCOL_VERSION, + protocol + )); + } else { + this->parent->onTestSuccess(); + } } - sentRequestHandle = std::nullopt; - if (timeoutSequence) { - this->stopAction(timeoutSequence); - timeoutSequence = nullptr; - } - - if (error.empty()) { - this->parent->onTestFailure("Failed to make a request to the server: server sent empty response."); - } else { - this->parent->onTestFailure("Failed to make a request to the server: " + util::formatting::formatErrorMessage(error) + ""); - } this->onClose(this); }) .send(); - // cancel the request after 5 seconds if no response - timeoutSequence = CCSequence::create( - CCDelayTime::create(5.0f), - CCCallFunc::create(this, callfunc_selector(ServerTestPopup::onTimeout)), - nullptr - ); - - this->runAction(timeoutSequence); - return true; } ServerTestPopup::~ServerTestPopup() { - if (sentRequestHandle.has_value()) { - sentRequestHandle->get()->cancel(); - } + this->cancelRequest(); } void ServerTestPopup::onTimeout() { - if (timeoutSequence) { - this->stopAction(timeoutSequence); - timeoutSequence = nullptr; - } - - if (sentRequestHandle.has_value()) { - sentRequestHandle->get()->cancel(); - } + this->cancelRequest(); this->parent->onTestFailure("Failed to add the server: timed out while waiting for a response."); this->onClose(this); } +void ServerTestPopup::cancelRequest() { + if (sentRequestHandle.has_value()) { + sentRequestHandle->discardResult(); + sentRequestHandle = std::nullopt; + } +} + ServerTestPopup* ServerTestPopup::create(const std::string& url, AddServerPopup* parent) { auto ret = new ServerTestPopup; if (ret && ret->init(POPUP_WIDTH, POPUP_HEIGHT, url, parent)) { diff --git a/src/ui/menu/server_switcher/server_test_popup.hpp b/src/ui/menu/server_switcher/server_test_popup.hpp index d508386e..06fb5a86 100644 --- a/src/ui/menu/server_switcher/server_test_popup.hpp +++ b/src/ui/menu/server_switcher/server_test_popup.hpp @@ -1,7 +1,7 @@ #pragma once #include -#include +#include class AddServerPopup; @@ -17,8 +17,9 @@ class ServerTestPopup : public geode::Popup protected: AddServerPopup* parent; - std::optional sentRequestHandle; - cocos2d::CCSequence* timeoutSequence; + std::optional sentRequestHandle; bool setup(const std::string&, AddServerPopup* parent) override; + + void cancelRequest(); }; \ No newline at end of file diff --git a/src/util/sync.hpp b/src/util/sync.hpp index deeae5e8..dbf1ad1b 100644 --- a/src/util/sync.hpp +++ b/src/util/sync.hpp @@ -166,6 +166,11 @@ class RelaxedAtomic { store(val); return *this; } + + // enable copying + RelaxedAtomic(RelaxedAtomic& other) { + this->store(other.load()); + } private: std::atomic value; };