diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f66b8c0..0d2a18f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,13 +32,19 @@ jobs: - name: Build run: cmake --build cmake-build-win32 --target easy_http --config Release --parallel + - name: Prepare artifact files + shell: bash + run: | + mkdir -p stage/scripting/include + cp out/bin/Release/easy_http_amxx.dll stage/ + cp out/pdb/Release/easy_http_amxx.pdb stage/ + cp amxx/scripting/include/easy_http.inc amxx/scripting/include/easy_http_json.inc stage/scripting/include/ + - name: Upload Windows artifacts uses: actions/upload-artifact@v4 with: name: easy_http-windows-win32 - path: | - out/bin/Release/** - out/pdb/Release/** + path: stage/** if-no-files-found: error retention-days: 14 @@ -107,10 +113,16 @@ jobs: cmake --build . --target easy_http -j$(nproc) ' + - name: Prepare artifact files + run: | + mkdir -p stage/scripting/include + cp out/bin/Release/easy_http_amxx_i386.so stage/ + cp amxx/scripting/include/easy_http.inc amxx/scripting/include/easy_http_json.inc stage/scripting/include/ + - name: Upload Linux artifact uses: actions/upload-artifact@v4 with: name: easy_http-linux-i386 - path: out/bin/Release/easy_http_amxx_i386.so + path: stage/** if-no-files-found: error retention-days: 14 diff --git a/amxx/scripting/include/easy_http.inc b/amxx/scripting/include/easy_http.inc index 4d74127..8e17f69 100644 --- a/amxx/scripting/include/easy_http.inc +++ b/amxx/scripting/include/easy_http.inc @@ -122,16 +122,16 @@ native ezhttp_option_set_body(EzHttpOptions:options_id, const body[]); /** * Copies serialized string to the requests body. * - * @note Needs to be freed using ezjson_free() native. + * @note If using ezjson_move(), the C++ module will consume the JSON handle and free it. * * @param options_id Options identifier created via ezhttp_create_options(). - * @param json EzJSON handle. + * @param json EzJSONOwnership tag (use ezjson_move() or ezjson_copy()). * @param pretty True to format pretty JSON string, false to not. * * @return True if serialization was successful, false otherwise. * @error If passed handle is not a valid value. If passed options_id is not exists. */ -native bool:ezhttp_option_set_body_from_json(EzHttpOptions:options_id, EzJSON:json, bool:pretty = false); +native bool:ezhttp_option_set_body_from_json(EzHttpOptions:options_id, EzJSONOwnership:json, bool:pretty = false); /** * Appends a body to the HTTP request. diff --git a/amxx/scripting/include/easy_http_json.inc b/amxx/scripting/include/easy_http_json.inc index a635862..35a525a 100644 --- a/amxx/scripting/include/easy_http_json.inc +++ b/amxx/scripting/include/easy_http_json.inc @@ -203,6 +203,32 @@ native EzJSON:ezjson_deep_copy(const EzJSON:value); */ native bool:ezjson_free(&EzJSON:handle); +/** + * Transfers ownership of a JSON handle. + * The receiving native will consume and free the handle, invalidating it. + * + * @param handle EzJSON handle to move + * + * @return EzJSONOwnership tagged handle + */ +stock EzJSONOwnership:ezjson_move(EzJSON:handle) +{ + return EzJSONOwnership:handle; +} + +/** + * Copies a JSON handle and transfers ownership of the copy. + * The original handle remains untouched and valid. + * + * @param handle EzJSON handle to copy + * + * @return EzJSONOwnership tagged handle + */ +stock EzJSONOwnership:ezjson_copy(EzJSON:handle) +{ + return EzJSONOwnership:ezjson_deep_copy(handle); +} + /** * Gets string data. * diff --git a/amxx_test/scripting/ez_http_test.sma b/amxx_test/scripting/ez_http_test.sma index 202360f..7b679c8 100644 --- a/amxx_test/scripting/ez_http_test.sma +++ b/amxx_test/scripting/ez_http_test.sma @@ -622,8 +622,7 @@ START_ASYNC_TEST(test_post_body_json) ezjson_object_set_string(json_root, "StringField", "TestValue"); ezjson_object_set_number(json_root, "NumberField", 21); - ezhttp_option_set_body_from_json(opt, json_root); - ezjson_free(json_root); + ezhttp_option_set_body_from_json(opt, ezjson_move(json_root)); ezhttp_option_set_header(opt, "Content-Type", "application/json"); EZHTTP_OPTION_SET_TEST_DATA(opt) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bd0dccb..7d97a9d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,6 +5,10 @@ find_package(Threads REQUIRED) find_package(HLSDK REQUIRED) find_package(metamod REQUIRED) +if (UNIX) + find_package(OpenSSL REQUIRED) +endif() + if (AMXX_EASY_HTTP_BUILD_STATIC) set(LIBRARY_BUILD_TYPE STATIC) set(TARGET_LIBRARIES_SCOPE PUBLIC) @@ -70,6 +74,10 @@ add_library(easy_http::easy_http ALIAS ${TARGET_NAME}) target_include_directories(${TARGET_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ) +if (UNIX) + target_include_directories(${TARGET_NAME} PRIVATE ${OPENSSL_INCLUDE_DIR}) + target_link_libraries(${TARGET_NAME} PRIVATE OpenSSL::Crypto) +endif() target_compile_definitions(${TARGET_NAME} PRIVATE NOMINMAX diff --git a/src/EasyHttpModule.cpp b/src/EasyHttpModule.cpp index a585997..21ae385 100644 --- a/src/EasyHttpModule.cpp +++ b/src/EasyHttpModule.cpp @@ -259,12 +259,14 @@ void EasyHttpModule::ShutdownWithoutCallbacks() if (terminating_ez) { + terminating_ez->CancelAllRequests(); terminating_ez->ForgetAllRequests(); forgotten_easy_http_.emplace_back(std::move(terminating_ez)); } if (forgettable_ez) { + forgettable_ez->CancelAllRequests(); forgettable_ez->ForgetAllRequests(); forgotten_easy_http_.emplace_back(std::move(forgettable_ez)); } diff --git a/src/easy_http/EasyHttp.cpp b/src/easy_http/EasyHttp.cpp index 42597cd..8a786bc 100644 --- a/src/easy_http/EasyHttp.cpp +++ b/src/easy_http/EasyHttp.cpp @@ -143,10 +143,12 @@ EasyHttp::~EasyHttp() { std::lock_guard lock_guard(pending_requests_mutex_); + std::lock_guard lock_guard_completed(completed_requests_mutex_); stop_requested_ = true; } pending_requests_cv_.notify_all(); + completed_requests_cv_.notify_all(); for (auto &worker_thread : worker_threads_) { @@ -205,9 +207,12 @@ void EasyHttp::WorkerLoop() bool forgotten = pending_request.request_control->forgotten.load(); if (!forgotten) { - std::lock_guard lock_guard(completed_requests_mutex_); + std::unique_lock lock_guard(completed_requests_mutex_); + completed_requests_cv_.wait(lock_guard, [this]() + { return stop_requested_ || completed_requests_.size() < 1024; }); + forgotten = pending_request.request_control->forgotten.load(); - if (!forgotten) + if (!forgotten && !stop_requested_) { completed_requests_.push_back(CompletedRequest{ pending_request.request_control, @@ -232,6 +237,7 @@ bool EasyHttp::TryPopCompletedRequest(CompletedRequest &completed_request) completed_request = std::move(completed_requests_.front()); completed_requests_.pop_front(); + completed_requests_cv_.notify_one(); return true; } @@ -253,6 +259,7 @@ void EasyHttp::DropCompletedRequestsWithoutCallbacks() } completed_requests_.clear(); + completed_requests_cv_.notify_all(); } for (auto &request_control : completed_request_controls) @@ -374,6 +381,8 @@ void EasyHttp::SetSessionCommonOptions(cpr::Session &session, const std::shared_ if (options.connect_timeout) session.SetConnectTimeout(*options.connect_timeout); + + curl_easy_setopt(session.GetCurlHolder()->handle, CURLOPT_NOSIGNAL, 1L); } Response EasyHttp::SendRequest(const std::shared_ptr &request_control, RequestMethod method, const cpr::Url &url, const RequestOptions &options) @@ -537,6 +546,7 @@ Response EasyHttp::FtpUpload(cpr::Session &session, const std::shared_ptrcanceled.load()) @@ -597,7 +610,9 @@ Response EasyHttp::FtpDownloadSingle(cpr::Session &session, const std::shared_pt CURL *curl = session.GetCurlHolder()->handle; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_UPLOAD, 0L); curl_easy_setopt(curl, CURLOPT_TRANSFERTEXT, 0L); + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); if (options.require_secure) { curl_easy_setopt(curl, CURLOPT_USE_SSL, CURLUSESSL_ALL); @@ -606,6 +621,8 @@ Response EasyHttp::FtpDownloadSingle(cpr::Session &session, const std::shared_pt CURLcode curl_result = curl_easy_perform(curl); file.close(); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, nullptr); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, nullptr); Response response(session.Complete(curl_result)); if (request_control->canceled.load()) @@ -647,11 +664,13 @@ Response EasyHttp::FtpDownloadWildcard(cpr::Session &session, const std::shared_ CURL *curl = session.GetCurlHolder()->handle; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_UPLOAD, 0L); curl_easy_setopt(curl, CURLOPT_TRANSFERTEXT, 0L); curl_easy_setopt(curl, CURLOPT_WILDCARDMATCH, 1L); curl_easy_setopt(curl, CURLOPT_CHUNK_BGN_FUNCTION, OnFtpWildcardChunkBegin); curl_easy_setopt(curl, CURLOPT_CHUNK_END_FUNCTION, OnFtpWildcardChunkEnd); curl_easy_setopt(curl, CURLOPT_CHUNK_DATA, &context); + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); if (options.require_secure) { curl_easy_setopt(curl, CURLOPT_USE_SSL, CURLUSESSL_ALL); @@ -664,6 +683,8 @@ Response EasyHttp::FtpDownloadWildcard(cpr::Session &session, const std::shared_ curl_easy_setopt(curl, CURLOPT_CHUNK_BGN_FUNCTION, nullptr); curl_easy_setopt(curl, CURLOPT_CHUNK_END_FUNCTION, nullptr); curl_easy_setopt(curl, CURLOPT_CHUNK_DATA, nullptr); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, nullptr); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, nullptr); Response response(session.Complete(curl_result)); if (request_control->canceled.load()) diff --git a/src/easy_http/EasyHttp.h b/src/easy_http/EasyHttp.h index 49ef43a..eab082d 100644 --- a/src/easy_http/EasyHttp.h +++ b/src/easy_http/EasyHttp.h @@ -43,6 +43,7 @@ namespace ezhttp std::deque pending_requests_; std::mutex completed_requests_mutex_; + std::condition_variable completed_requests_cv_; std::deque completed_requests_; mutable std::mutex requests_mutex_; diff --git a/src/easy_http/UrlUtils.cpp b/src/easy_http/UrlUtils.cpp index 3de1955..fb69fba 100644 --- a/src/easy_http/UrlUtils.cpp +++ b/src/easy_http/UrlUtils.cpp @@ -10,6 +10,7 @@ namespace ezhttp CURLUcode rc; char* host = nullptr; + char* scheme = nullptr; rc = curl_url_set(curl_url_, CURLUPART_URL, url.c_str(), 0); if (rc != CURLUE_OK) @@ -19,7 +20,20 @@ namespace ezhttp if (rc != CURLUE_OK) return ""; - return { host }; + std::string result; + rc = curl_url_get(curl_url_, CURLUPART_SCHEME, &scheme, 0); + if (rc == CURLUE_OK && scheme) + { + result = std::string(scheme) + "://" + host; + curl_free(scheme); + } + else + { + result = host; + } + + curl_free(host); + return result; } void UrlUtils::InitializeIfNeeded() diff --git a/src/module.cpp b/src/module.cpp index 67c33a2..e9b776f 100644 --- a/src/module.cpp +++ b/src/module.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include @@ -13,6 +14,64 @@ #include "utils/amxx_utils.h" #include "utils/TraceLog.h" +#ifdef LINUX +#include +#include +#include +#include + +namespace +{ + std::unique_ptr g_OpenSslLocks; + + void OpenSslLockingCallback(int mode, int type, const char* /*file*/, int /*line*/) + { + if (mode & CRYPTO_LOCK) + g_OpenSslLocks[type].lock(); + else + g_OpenSslLocks[type].unlock(); + } + +#if OPENSSL_VERSION_NUMBER < 0x10000000L + unsigned long OpenSslIdCallback() + { + return static_cast(pthread_self()); + } +#else + void OpenSslIdCallback(CRYPTO_THREADID* id) + { + CRYPTO_THREADID_set_numeric(id, static_cast(pthread_self())); + } +#endif + + void InitializeOpenSslLocks() + { +#if OPENSSL_VERSION_NUMBER < 0x10100000L + g_OpenSslLocks = std::make_unique(CRYPTO_num_locks()); + CRYPTO_set_locking_callback(OpenSslLockingCallback); +#if OPENSSL_VERSION_NUMBER < 0x10000000L + CRYPTO_set_id_callback(OpenSslIdCallback); +#else + CRYPTO_THREADID_set_callback(OpenSslIdCallback); +#endif +#endif + } + + void CleanupOpenSslLocks() + { +#if OPENSSL_VERSION_NUMBER < 0x10100000L + CRYPTO_set_locking_callback(nullptr); +#if OPENSSL_VERSION_NUMBER < 0x10000000L + CRYPTO_set_id_callback(nullptr); +#else + CRYPTO_THREADID_set_callback(nullptr); +#endif + g_OpenSslLocks.reset(); +#endif + } +} +#endif + using namespace ezhttp; bool ValidateOptionsId(AMX *amx, OptionsId options_id); @@ -48,7 +107,7 @@ namespace void RefreshTraceLogSetting() { - ezhttp::trace::SetEnabled(CVAR_GET_FLOAT("ezhttp_trace_log") != 0.0f); + ezhttp::trace::SetEnabled(cvar_ezhttp_trace.value != 0.0f); } std::unique_ptr ReadCallbackData(AMX *amx, cell *params, int arg_data, int arg_data_len, int &data_len) @@ -63,7 +122,13 @@ namespace if (requested_len <= 0) return nullptr; - std::unique_ptr data = std::make_unique(requested_len); + if (requested_len > 1024) + { + MF_LogError(amx, AMX_ERR_NATIVE, "Callback data length %d exceeds maximum limit of 1024 cells", requested_len); + return nullptr; + } + + std::unique_ptr data = std::make_unique(requested_len); MF_CopyAmxMemory(data.get(), MF_GetAmxAddr(amx, params[arg_data]), requested_len); data_len = requested_len; @@ -73,6 +138,10 @@ namespace void CreateModules() { +#ifdef LINUX + InitializeOpenSslLocks(); +#endif + curl_global_init(CURL_GLOBAL_ALL); ezhttp::trace::Initialize(MF_BuildPathname("addons/amxmodx/logs/ezhttp_trace.log")); RefreshTraceLogSetting(); ezhttp::trace::Writef("module", "CreateModules begin"); @@ -89,6 +158,10 @@ void DestroyModules() g_JsonManager.reset(); ezhttp::trace::Writef("module", "DestroyModules done"); ezhttp::trace::Shutdown(); + curl_global_cleanup(); +#ifdef LINUX + CleanupOpenSslLocks(); +#endif } // native EzHttpOptions:ezhttp_create_options(bool:auto_destroy = true); @@ -141,15 +214,23 @@ cell AMX_NATIVE_CALL ezhttp_option_set_body(AMX *amx, cell *params) return 0; } -// native bool:ezhttp_option_set_body_from_json(EzHttpOptions:options_id, EzJSON:json, bool:pretty = false); +// native bool:ezhttp_option_set_body_from_json(EzHttpOptions:options_id, EzJSONOwnership:json, bool:pretty = false); cell AMX_NATIVE_CALL ezhttp_option_set_body_from_json(AMX *amx, cell *params) { auto options_id = (OptionsId)params[1]; auto json_handle = (JS_Handle)params[2]; auto pretty = (bool)params[3]; + // This native takes ownership of the JSON handle. If options_id is invalid, + // we must free the JSON handle here before returning to prevent a memory leak, + // as the calling script expects the native to have consumed it. if (!ValidateOptionsId(amx, options_id)) + { + if (g_JsonManager->IsValidHandle(json_handle)) + g_JsonManager->Free(json_handle); + return 0; + } if (!g_JsonManager->IsValidHandle(json_handle)) { @@ -159,10 +240,14 @@ cell AMX_NATIVE_CALL ezhttp_option_set_body_from_json(AMX *amx, cell *params) char *json_str = g_JsonManager->SerialToString(json_handle, pretty); if (json_str == nullptr) + { + g_JsonManager->Free(json_handle); return 0; + } g_EasyHttpModule->GetOptions(options_id).options_builder.SetBody(json_str); g_JsonManager->FreeString(json_str); + g_JsonManager->Free(json_handle); return 1; } @@ -259,9 +344,18 @@ cell AMX_NATIVE_CALL ezhttp_option_set_user_data(AMX *amx, cell *params) if (!ValidateOptionsId(amx, options_id)) return 0; + if (data_len < 0 || data_len > 1024) + { + MF_LogError(amx, AMX_ERR_NATIVE, "User data length %d is invalid (must be between 0 and 1024)", data_len); + return 0; + } + std::vector user_data; - user_data.resize(data_len); - MF_CopyAmxMemory(user_data.data(), data_addr, data_len); + if (data_len > 0) + { + user_data.resize(data_len); + MF_CopyAmxMemory(user_data.data(), data_addr, data_len); + } g_EasyHttpModule->GetOptions(options_id).user_data = user_data; return 0;