diff --git a/examples/WebServices/main.cpp b/examples/WebServices/main.cpp new file mode 100644 index 000000000..d54d2ae7b --- /dev/null +++ b/examples/WebServices/main.cpp @@ -0,0 +1,26 @@ +#include +#include +#include + +int main() +{ + Nz::Application app; + + std::unique_ptr webService = Nz::Network::Instance()->InstantiateWebService(); + + std::unique_ptr webRequest = webService->CreateGetRequest("https://google.com", [&](const Nz::WebRequestResult& result) + { + if (result) + std::cout << "Got a response (" << result.GetDownloadedSize() << " bytes, " << result.GetDownloadSpeed() << " bytes/s" << ")" << std::endl; + else + std::cout << "Web request failed (code " << result.GetReponseCode() << "): " << result.GetErrorMessage() << std::endl; + + Nz::ApplicationBase::Instance()->Quit(); + }); + + webService->QueueRequest(std::move(webRequest)); + + app.AddUpdaterFunc([&] { webService->Poll(); }); + + return app.Run(); +} diff --git a/examples/WebServices/xmake.lua b/examples/WebServices/xmake.lua new file mode 100644 index 000000000..af4e506d9 --- /dev/null +++ b/examples/WebServices/xmake.lua @@ -0,0 +1,3 @@ +target("WebServices") + add_deps("NazaraNetwork") + add_files("main.cpp") diff --git a/include/Nazara/Network.hpp b/include/Nazara/Network.hpp index 5af8da000..2d2eabe27 100644 --- a/include/Nazara/Network.hpp +++ b/include/Nazara/Network.hpp @@ -47,5 +47,8 @@ #include #include #include +#include +#include +#include #endif // NAZARA_GLOBAL_NETWORK_HPP diff --git a/include/Nazara/Network/CurlFunctions.hpp b/include/Nazara/Network/CurlFunctions.hpp new file mode 100644 index 000000000..f75ff454f --- /dev/null +++ b/include/Nazara/Network/CurlFunctions.hpp @@ -0,0 +1,34 @@ +// Copyright (C) 2023 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Network module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +// no header guards + +#if !defined(NAZARA_CURL_FUNCTION) +#error You must define NAZARA_CURL_FUNCTION before including this file +#endif + +#ifndef NAZARA_CURL_FUNCTION_LAST +#define NAZARA_CURL_FUNCTION_LAST(F) NAZARA_CURL_FUNCTION(F) +#endif + +NAZARA_CURL_FUNCTION(easy_cleanup) +NAZARA_CURL_FUNCTION(easy_getinfo) +NAZARA_CURL_FUNCTION(easy_init) +NAZARA_CURL_FUNCTION(easy_setopt) +NAZARA_CURL_FUNCTION(easy_strerror) +NAZARA_CURL_FUNCTION(global_cleanup) +NAZARA_CURL_FUNCTION(global_init) +NAZARA_CURL_FUNCTION(multi_add_handle) +NAZARA_CURL_FUNCTION(multi_cleanup) +NAZARA_CURL_FUNCTION(multi_info_read) +NAZARA_CURL_FUNCTION(multi_init) +NAZARA_CURL_FUNCTION(multi_perform) +NAZARA_CURL_FUNCTION(multi_remove_handle) +NAZARA_CURL_FUNCTION(multi_strerror) +NAZARA_CURL_FUNCTION(slist_append) +NAZARA_CURL_FUNCTION(slist_free_all) +NAZARA_CURL_FUNCTION_LAST(version_info) + +#undef NAZARA_CURL_FUNCTION +#undef NAZARA_CURL_FUNCTION_LAST diff --git a/include/Nazara/Network/CurlLibrary.hpp b/include/Nazara/Network/CurlLibrary.hpp new file mode 100644 index 000000000..ab98923fd --- /dev/null +++ b/include/Nazara/Network/CurlLibrary.hpp @@ -0,0 +1,44 @@ +// Copyright (C) 2023 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Network module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_NETWORK_CURLLIBRARY_HPP +#define NAZARA_NETWORK_CURLLIBRARY_HPP + +#include +#include +#include + +namespace Nz +{ + class NAZARA_NETWORK_API CurlLibrary + { + public: + inline CurlLibrary(); + CurlLibrary(const CurlLibrary&) = delete; + CurlLibrary(CurlLibrary&&) = delete; + inline ~CurlLibrary(); + + inline bool IsLoaded() const; + + bool Load(); + + void Unload(); + + CurlLibrary& operator=(const CurlLibrary&) = delete; + CurlLibrary& operator=(CurlLibrary&&) = delete; + +#define NAZARA_CURL_FUNCTION(name) decltype(&::curl_##name) name; +#include + + private: + DynLib m_library; + bool m_isInitialized; + }; +} + +#include + +#endif // NAZARA_NETWORK_CURLLIBRARY_HPP diff --git a/include/Nazara/Network/CurlLibrary.inl b/include/Nazara/Network/CurlLibrary.inl new file mode 100644 index 000000000..9991aaa6c --- /dev/null +++ b/include/Nazara/Network/CurlLibrary.inl @@ -0,0 +1,25 @@ +// Copyright (C) 2023 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Network module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include + +namespace Nz +{ + inline CurlLibrary::CurlLibrary() : + m_isInitialized(false) + { + } + + inline CurlLibrary::~CurlLibrary() + { + Unload(); + } + + inline bool CurlLibrary::IsLoaded() const + { + return m_isInitialized; + } +} + +#include diff --git a/include/Nazara/Network/Network.hpp b/include/Nazara/Network/Network.hpp index c040bca85..c90d09906 100644 --- a/include/Nazara/Network/Network.hpp +++ b/include/Nazara/Network/Network.hpp @@ -10,9 +10,12 @@ #include #include #include +#include namespace Nz { + class WebService; + class NAZARA_NETWORK_API Network : public ModuleBase { friend ModuleBase; @@ -20,12 +23,22 @@ namespace Nz public: using Dependencies = TypeList; - struct Config {}; + struct Config; - Network(Config /*config*/); + Network(Config config); ~Network(); + std::unique_ptr InstantiateWebService(); + + struct Config + { + // Initialize web services and fails module initialization if it failed to initialize them + bool webServices = false; + }; + private: + std::unique_ptr m_curlLibrary; + static Network* s_instance; }; } diff --git a/include/Nazara/Network/WebRequest.hpp b/include/Nazara/Network/WebRequest.hpp new file mode 100644 index 000000000..534428fbb --- /dev/null +++ b/include/Nazara/Network/WebRequest.hpp @@ -0,0 +1,75 @@ +// Copyright (C) 2023 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Network module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_NETWORK_WEBREQUEST_HPP +#define NAZARA_NETWORK_WEBREQUEST_HPP + +#include +#include +#include +#include +#include +#include +#include + +struct curl_slist; + +namespace Nz +{ + class WebService; + + class NAZARA_NETWORK_API WebRequest + { + friend WebService; + + public: + using DataCallback = std::function; + using ResultCallback = std::function; + + WebRequest(WebService& owner); + WebRequest(const WebRequest&) = delete; + WebRequest(WebRequest&&) = default; + ~WebRequest(); + + void ForceProtocol(NetProtocol protocol); + + inline void SetDataCallback(DataCallback callback); + inline void SetHeader(std::string header, std::string value); + void SetJSonContent(std::string_view encodedJSon); + void SetMaximumFileSize(UInt64 maxFileSize); + inline void SetResultCallback(ResultCallback callback); + void SetServiceName(const std::string_view& serviceName); + void SetURL(const std::string& url); + + void SetupGet(); + void SetupPost(); + + WebRequest& operator=(const WebRequest&) = delete; + WebRequest& operator=(WebRequest&&) = default; + + static std::unique_ptr Get(const std::string& url, ResultCallback callback = nullptr); + static std::unique_ptr Post(const std::string& url, ResultCallback callback = nullptr); + + private: + inline bool OnBodyResponse(const char* data, std::size_t length); + CURL* Prepare(); + inline void TriggerCallback(); + inline void TriggerCallback(std::string errorMessage); + + std::string m_responseBody; + std::unordered_map m_headers; + WebService& m_webService; + DataCallback m_dataCallback; + MovablePtr m_curlHandle; + MovablePtr m_headerList; + ResultCallback m_resultCallback; + bool m_isUserAgentSet; + }; +} + +#include + +#endif // NAZARA_NETWORK_WEBREQUEST_HPP diff --git a/include/Nazara/Network/WebRequest.inl b/include/Nazara/Network/WebRequest.inl new file mode 100644 index 000000000..993af4594 --- /dev/null +++ b/include/Nazara/Network/WebRequest.inl @@ -0,0 +1,47 @@ +// Copyright (C) 2023 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Network module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include + +namespace Nz +{ + inline void WebRequest::SetDataCallback(DataCallback callback) + { + m_dataCallback = std::move(callback); + } + + inline void WebRequest::SetResultCallback(ResultCallback callback) + { + m_resultCallback = std::move(callback); + } + + inline void WebRequest::SetHeader(std::string header, std::string value) + { + m_headers.insert_or_assign(std::move(header), std::move(value)); + } + + inline bool WebRequest::OnBodyResponse(const char* data, std::size_t length) + { + if (!m_dataCallback) + { + m_responseBody.append(data, length); + return true; + } + + return m_dataCallback(data, length); + } + + inline void WebRequest::TriggerCallback() + { + m_resultCallback(WebRequestResult(m_webService, m_curlHandle.Get(), std::move(m_responseBody))); + m_responseBody.clear(); + } + + inline void WebRequest::TriggerCallback(std::string errorMessage) + { + m_resultCallback(WebRequestResult(m_webService, std::move(errorMessage))); + } +} + +#include diff --git a/include/Nazara/Network/WebRequestResult.hpp b/include/Nazara/Network/WebRequestResult.hpp new file mode 100644 index 000000000..184cc2c60 --- /dev/null +++ b/include/Nazara/Network/WebRequestResult.hpp @@ -0,0 +1,56 @@ +// Copyright (C) 2023 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Network module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_NETWORK_WEBREQUESTRESULT_HPP +#define NAZARA_NETWORK_WEBREQUESTRESULT_HPP + +#include +#include +#include +#include + +using CURL = void; + +namespace Nz +{ + class WebService; + + class NAZARA_NETWORK_API WebRequestResult + { + friend class WebRequest; + + public: + WebRequestResult(const WebRequestResult&) = delete; + WebRequestResult(WebRequestResult&&) = delete; + ~WebRequestResult() = default; + + inline std::string& GetBody(); + inline const std::string& GetBody() const; + Nz::UInt64 GetDownloadedSize() const; + Nz::UInt64 GetDownloadSpeed() const; + inline const std::string& GetErrorMessage() const; + long GetReponseCode() const; + + inline bool HasSucceeded() const; + + inline explicit operator bool() const; + + WebRequestResult& operator=(const WebRequestResult&) = delete; + WebRequestResult& operator=(WebRequestResult&&) = delete; + + private: + inline WebRequestResult(WebService& webService, CURL* curl, std::string body); + inline WebRequestResult(WebService& webService, std::string errMessage); + + CURL* m_curlHandle; + WebService& m_webService; + std::string m_bodyOrErr; + }; +} + +#include + +#endif // NAZARA_NETWORK_WEBREQUESTRESULT_HPP diff --git a/include/Nazara/Network/WebRequestResult.inl b/include/Nazara/Network/WebRequestResult.inl new file mode 100644 index 000000000..cd1be954b --- /dev/null +++ b/include/Nazara/Network/WebRequestResult.inl @@ -0,0 +1,53 @@ +// Copyright (C) 2023 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Network module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include + +namespace Nz +{ + inline WebRequestResult::WebRequestResult(WebService& webService, CURL* curl, std::string body) : + m_curlHandle(curl), + m_webService(webService), + m_bodyOrErr(std::move(body)) + { + } + + inline WebRequestResult::WebRequestResult(WebService& webService, std::string errMessage) : + m_curlHandle(nullptr), + m_webService(webService), + m_bodyOrErr(std::move(errMessage)) + { + } + + inline std::string& WebRequestResult::GetBody() + { + assert(HasSucceeded()); + return m_bodyOrErr; + } + + inline const std::string& WebRequestResult::GetBody() const + { + assert(HasSucceeded()); + return m_bodyOrErr; + } + + inline const std::string& WebRequestResult::GetErrorMessage() const + { + assert(!HasSucceeded()); + return m_bodyOrErr; + } + + inline bool WebRequestResult::HasSucceeded() const + { + return m_curlHandle != nullptr; + } + + inline WebRequestResult::operator bool() const + { + return HasSucceeded(); + } + +} + +#include diff --git a/include/Nazara/Network/WebService.hpp b/include/Nazara/Network/WebService.hpp new file mode 100644 index 000000000..5237747aa --- /dev/null +++ b/include/Nazara/Network/WebService.hpp @@ -0,0 +1,58 @@ +// Copyright (C) 2023 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Network module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_NETWORK_WEBSERVICE_HPP +#define NAZARA_NETWORK_WEBSERVICE_HPP + +#include +#include +#include + +using CURLM = void; + +namespace Nz +{ + class CurlLibrary; + class Logger; + + class NAZARA_NETWORK_API WebService + { + friend WebRequest; + friend WebRequestResult; + + public: + WebService(const CurlLibrary& library); + WebService(const WebService&) = delete; + WebService(WebService&&) = delete; + ~WebService(); + + inline std::unique_ptr AllocateRequest(); + + inline std::unique_ptr CreateGetRequest(const std::string& url, WebRequest::ResultCallback callback); + inline std::unique_ptr CreatePostRequest(const std::string& url, WebRequest::ResultCallback callback); + + inline const std::string& GetUserAgent() const; + + void Poll(); + + void QueueRequest(std::unique_ptr&& request); + + WebService& operator=(const WebService&) = delete; + WebService& operator=(WebService&&) = delete; + + private: + inline const CurlLibrary& GetCurlLibrary() const; + + std::string m_userAgent; + std::unordered_map> m_activeRequests; + const CurlLibrary& m_curl; + MovablePtr m_curlMulti; + }; +} + +#include + +#endif // NAZARA_NETWORK_WEBSERVICE_HPP diff --git a/include/Nazara/Network/WebService.inl b/include/Nazara/Network/WebService.inl new file mode 100644 index 000000000..9b274df32 --- /dev/null +++ b/include/Nazara/Network/WebService.inl @@ -0,0 +1,45 @@ +// Copyright (C) 2023 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Network module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include + +namespace Nz +{ + inline std::unique_ptr WebService::AllocateRequest() + { + return std::make_unique(*this); + } + + inline std::unique_ptr WebService::CreateGetRequest(const std::string& url, WebRequest::ResultCallback callback) + { + std::unique_ptr request = AllocateRequest(); + request->SetURL(url); + request->SetResultCallback(std::move(callback)); + request->SetupGet(); + + return request; + } + + inline std::unique_ptr WebService::CreatePostRequest(const std::string& url, WebRequest::ResultCallback callback) + { + std::unique_ptr request = AllocateRequest(); + request->SetURL(url); + request->SetResultCallback(std::move(callback)); + request->SetupPost(); + + return request; + } + + inline const std::string& WebService::GetUserAgent() const + { + return m_userAgent; + } + + const CurlLibrary& WebService::GetCurlLibrary() const + { + return m_curl; + } +} + +#include diff --git a/src/Nazara/Network/CurlLibrary.cpp b/src/Nazara/Network/CurlLibrary.cpp new file mode 100644 index 000000000..aeb1680be --- /dev/null +++ b/src/Nazara/Network/CurlLibrary.cpp @@ -0,0 +1,90 @@ +// Copyright (C) 2023 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Network module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include +#include +#include +#include +#include + +namespace Nz +{ + bool CurlLibrary::Load() + { + Unload(); + + CallOnExit unloadOnFailure([this] { Unload(); }); + + auto PostLoad = [&] + { + if (global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) + { + NazaraError("failed to initialize libcurl"); + return false; + } + m_isInitialized = true; + + unloadOnFailure.Reset(); + return true; + }; + +#ifndef NAZARA_NETWORK_LIBCURL_LINK + for (const char* libname : { "libcurl" NAZARA_DYNLIB_EXTENSION, "libcurl-d" NAZARA_DYNLIB_EXTENSION }) + { + ErrorFlags errFlags(Nz::ErrorMode::Silent); + if (m_library.Load(libname)) + break; + } + + if (!m_library.IsLoaded()) + { + NazaraError("failed to load libcurl: " + m_library.GetLastError()); + return false; + } + + auto LoadSymbol = [this](const char* name, bool optional) + { + DynLibFunc funcPtr = m_library.GetSymbol(name); + if (!funcPtr && !optional) + throw std::runtime_error(std::string("failed to load ") + name); + + return funcPtr; + }; + + try + { +#define NAZARA_CURL_FUNCTION(name) name = reinterpret_cast(LoadSymbol("curl_" #name, false)); +#include + } + catch (const std::exception& e) + { + NazaraError(e.what()); + return false; + } + + return PostLoad(); +#else + // libcurl is linked to the executable + +#define NAZARA_CURL_FUNCTION(name) name = &::curl_##name; +#include + + return PostLoad(); +#endif + } + + void CurlLibrary::Unload() + { + if (!m_library.IsLoaded()) + return; + + if (m_isInitialized) + global_cleanup(); + +#define NAZARA_CURL_FUNCTION(name) name = nullptr; +#include + + m_library.Unload(); + } +} diff --git a/src/Nazara/Network/Network.cpp b/src/Nazara/Network/Network.cpp index 139c1c043..0709aa1a5 100644 --- a/src/Nazara/Network/Network.cpp +++ b/src/Nazara/Network/Network.cpp @@ -7,7 +7,9 @@ #include #include #include +#include #include +#include #include #if defined(NAZARA_PLATFORM_WINDOWS) @@ -30,7 +32,7 @@ namespace Nz * \brief Network class that represents the module initializer of Network */ - Network::Network(Config /*config*/) : + Network::Network(Config config) : ModuleBase("Network", this) { // Initialize module here @@ -39,14 +41,37 @@ namespace Nz if (!NetPacket::Initialize()) throw std::runtime_error("failed to initialize packets"); + + if (config.webServices) + { + m_curlLibrary = std::make_unique(); + if (!m_curlLibrary->Load()) + throw std::runtime_error("failed to initialize curl"); + } } Network::~Network() { // Uninitialize module here + m_curlLibrary.reset(); + NetPacket::Uninitialize(); SocketImpl::Uninitialize(); } + std::unique_ptr Network::InstantiateWebService() + { + if (!m_curlLibrary) + { + std::unique_ptr curlLibrary = std::make_unique(); + if (!curlLibrary->Load()) + throw std::runtime_error("failed to initialize curl"); + + m_curlLibrary = std::move(curlLibrary); + } + + return std::make_unique(*m_curlLibrary); + } + Network* Network::s_instance = nullptr; } diff --git a/src/Nazara/Network/WebRequest.cpp b/src/Nazara/Network/WebRequest.cpp new file mode 100644 index 000000000..b23be915e --- /dev/null +++ b/src/Nazara/Network/WebRequest.cpp @@ -0,0 +1,133 @@ +// Copyright (C) 2023 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Network module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include +#include //< include last because of curl/curl.h +#include +#include + +namespace Nz +{ + WebRequest::WebRequest(WebService& webService) : + m_webService(webService), + m_isUserAgentSet(false) + { + auto& libcurl = m_webService.GetCurlLibrary(); + + m_curlHandle = libcurl.easy_init(); + } + + WebRequest::~WebRequest() + { + auto& libcurl = m_webService.GetCurlLibrary(); + if (m_curlHandle) + libcurl.easy_cleanup(m_curlHandle); + + if (m_headerList) + libcurl.slist_free_all(m_headerList); + } + + void WebRequest::ForceProtocol(Nz::NetProtocol protocol) + { + assert(protocol != Nz::NetProtocol::Unknown); + + auto& libcurl = m_webService.GetCurlLibrary(); + switch (protocol) + { + case Nz::NetProtocol::Any: + libcurl.easy_setopt(m_curlHandle, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_WHATEVER); + break; + + case Nz::NetProtocol::IPv4: + libcurl.easy_setopt(m_curlHandle, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); + break; + + case Nz::NetProtocol::IPv6: + libcurl.easy_setopt(m_curlHandle, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6); + break; + + case Nz::NetProtocol::Unknown: + break; + } + } + + void WebRequest::SetJSonContent(std::string_view encodedJSon) + { + auto& libcurl = m_webService.GetCurlLibrary(); + + SetHeader("Content-Type", "application/json"); + libcurl.easy_setopt(m_curlHandle, CURLOPT_POSTFIELDSIZE_LARGE, curl_off_t(encodedJSon.size())); + libcurl.easy_setopt(m_curlHandle, CURLOPT_COPYPOSTFIELDS, encodedJSon.data()); + } + + void WebRequest::SetMaximumFileSize(Nz::UInt64 maxFileSize) + { + auto& libcurl = m_webService.GetCurlLibrary(); + + curl_off_t maxSize = maxFileSize; + libcurl.easy_setopt(m_curlHandle, CURLOPT_MAXFILESIZE_LARGE, maxSize); + } + + void WebRequest::SetServiceName(const std::string_view& serviceName) + { + auto& libcurl = m_webService.GetCurlLibrary(); + + if (!serviceName.empty()) + { + //TODO Nz::StackString? + std::string userAgent = m_webService.GetUserAgent(); + userAgent.reserve(userAgent.size() + 3 + serviceName.size()); + userAgent.append(" - "); + userAgent.append(serviceName.data(), serviceName.size()); + + libcurl.easy_setopt(m_curlHandle, CURLOPT_USERAGENT, userAgent.data()); + } + else + libcurl.easy_setopt(m_curlHandle, CURLOPT_USERAGENT, m_webService.GetUserAgent().c_str()); + + m_isUserAgentSet = true; + } + + void WebRequest::SetURL(const std::string& url) + { + auto& libcurl = m_webService.GetCurlLibrary(); + + libcurl.easy_setopt(m_curlHandle, CURLOPT_URL, url.data()); + } + + void WebRequest::SetupGet() + { + auto& libcurl = m_webService.GetCurlLibrary(); + + libcurl.easy_setopt(m_curlHandle, CURLOPT_HTTPGET, long(1)); + } + + void WebRequest::SetupPost() + { + auto& libcurl = m_webService.GetCurlLibrary(); + + libcurl.easy_setopt(m_curlHandle, CURLOPT_POST, long(1)); + } + + CURL* WebRequest::Prepare() + { + auto& libcurl = m_webService.GetCurlLibrary(); + + if (!m_headers.empty()) + { + for (auto&& [header, value] : m_headers) + { + std::string headerValue = (!value.empty()) ? header + ": " + value : header + ";"; + m_headerList = libcurl.slist_append(m_headerList, headerValue.c_str()); + } + + libcurl.easy_setopt(m_curlHandle, CURLOPT_HTTPHEADER, m_headerList.Get()); + } + + if (!m_isUserAgentSet) + libcurl.easy_setopt(m_curlHandle, CURLOPT_USERAGENT, m_webService.GetUserAgent().c_str()); + + return m_curlHandle; + } +} diff --git a/src/Nazara/Network/WebRequestResult.cpp b/src/Nazara/Network/WebRequestResult.cpp new file mode 100644 index 000000000..1d0cfbb65 --- /dev/null +++ b/src/Nazara/Network/WebRequestResult.cpp @@ -0,0 +1,47 @@ +// Copyright (C) 2023 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Network module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include +#include //< include last because of curl/curl.h +#include +#include + +namespace Nz +{ + Nz::UInt64 WebRequestResult::GetDownloadedSize() const + { + assert(HasSucceeded()); + + auto& libcurl = m_webService.GetCurlLibrary(); + + curl_off_t downloadedSize = 0; + libcurl.easy_getinfo(m_curlHandle, CURLINFO_SIZE_DOWNLOAD_T, &downloadedSize); + + return downloadedSize; + } + + std::size_t WebRequestResult::GetDownloadSpeed() const + { + assert(HasSucceeded()); + + auto& libcurl = m_webService.GetCurlLibrary(); + + curl_off_t downloadSpeed = 0; + libcurl.easy_getinfo(m_curlHandle, CURLINFO_SPEED_DOWNLOAD_T, &downloadSpeed); + + return downloadSpeed; + } + + long WebRequestResult::GetReponseCode() const + { + assert(HasSucceeded()); + + auto& libcurl = m_webService.GetCurlLibrary(); + + long responseCode; + libcurl.easy_getinfo(m_curlHandle, CURLINFO_RESPONSE_CODE, &responseCode); + + return responseCode; + } +} diff --git a/src/Nazara/Network/WebService.cpp b/src/Nazara/Network/WebService.cpp new file mode 100644 index 000000000..8576422d3 --- /dev/null +++ b/src/Nazara/Network/WebService.cpp @@ -0,0 +1,99 @@ +// Copyright (C) 2023 Jérôme "Lynix" Leclercq (lynix680@gmail.com) +// This file is part of the "Nazara Engine - Network module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include +#include +#include +#include //< include last because of curl/curl.h +#include +#include + +namespace Nz +{ + WebService::WebService(const CurlLibrary& curl) : + m_curl(curl) + { + curl_version_info_data* curlVersionData = m_curl.version_info(CURLVERSION_NOW); + + m_userAgent = fmt::format("Nazara WebService - curl/{}", curlVersionData->version); + + m_curlMulti = m_curl.multi_init(); + } + + WebService::~WebService() + { + if (m_curlMulti) + { + for (auto&& [handle, request] : m_activeRequests) + m_curl.multi_remove_handle(m_curlMulti, handle); + + m_curl.multi_cleanup(m_curlMulti); + } + } + + void WebService::Poll() + { + assert(m_curlMulti); + + int reportedActiveRequest; + CURLMcode err = m_curl.multi_perform(m_curlMulti, &reportedActiveRequest); + if (err != CURLM_OK) + { + NazaraError(fmt::format("[WebService] curl_multi_perform failed with {0}: {1}", UnderlyingCast(err), m_curl.multi_strerror(err))); + return; + } + + CURLMsg* m; + do + { + int msgq; + m = m_curl.multi_info_read(m_curlMulti, &msgq); + if (m && (m->msg == CURLMSG_DONE)) + { + CURL* handle = m->easy_handle; + + auto it = m_activeRequests.find(handle); + assert(it != m_activeRequests.end()); + + WebRequest& request = *it->second; + + if (m->data.result == CURLE_OK) + request.TriggerCallback(); + else + request.TriggerCallback(m_curl.easy_strerror(m->data.result)); + + m_curl.multi_remove_handle(m_curlMulti, handle); + + m_activeRequests.erase(handle); + } + } + while (m); + } + + void WebService::QueueRequest(std::unique_ptr&& request) + { + assert(m_curlMulti); + assert(request); + + CURL* handle = request->Prepare(); + + curl_write_callback writeCallback = [](char* ptr, std::size_t size, std::size_t nmemb, void* userdata) -> std::size_t + { + WebRequest* request = static_cast(userdata); + + std::size_t totalSize = size * nmemb; + if (!request->OnBodyResponse(ptr, totalSize)) + return 0; + + return totalSize; + }; + + m_curl.easy_setopt(handle, CURLOPT_WRITEFUNCTION, writeCallback); + m_curl.easy_setopt(handle, CURLOPT_WRITEDATA, request.get()); + + m_activeRequests.emplace(handle, std::move(request)); + + m_curl.multi_add_handle(m_curlMulti, handle); + } +} diff --git a/xmake.lua b/xmake.lua index 5a6eb3da9..8dfe864d5 100644 --- a/xmake.lua +++ b/xmake.lua @@ -127,7 +127,15 @@ local modules = { Network = { Option = "network", Deps = {"NazaraCore"}, - Custom = function() + Packages = { "fmt" }, + Custom = function () + if has_config("link_curl") then + add_defines("NAZARA_NETWORK_CURL_LINK") + add_packages("libcurl") + else + add_packages("libcurl", { links = {} }) + end + if is_plat("windows", "mingw") then add_syslinks("ws2_32") end @@ -216,6 +224,7 @@ option("compile_shaders", { description = "Compile nzsl shaders into an includab option("embed_rendererbackends", { description = "Embed renderer backend code into NazaraRenderer instead of loading them dynamically", default = is_plat("wasm") or false }) option("embed_resources", { description = "Turn builtin resources into includable headers", default = true }) option("embed_plugins", { description = "Embed enabled plugins code as static libraries", default = is_plat("wasm") or false }) +option("link_curl", { description = "Link libcurl in the executable instead of dynamically loading it", default = false }) option("link_openal", { description = "Link OpenAL in the executable instead of dynamically loading it", default = is_plat("wasm") or false }) option("override_runtime", { description = "Override vs runtime to MD in release and MDd in debug", default = true }) option("usepch", { description = "Use precompiled headers to speedup compilation", default = false }) @@ -258,6 +267,14 @@ if has_config("joltphysics") then add_requires("ordered_map") end +if has_config("network") then + if has_config("link_curl") then + add_requires("libcurl") + else + add_requires("libcurl", { configs = { shared = true }}) + end +end + if has_config("platform") then add_requires("libsdl >=2.26.0") end diff --git a/xmake/actions/generateheaders.lua b/xmake/actions/generateheaders.lua index a48c81f84..9b0a37583 100644 --- a/xmake/actions/generateheaders.lua +++ b/xmake/actions/generateheaders.lua @@ -76,6 +76,8 @@ on_run(function () paths["Core"].Excludes["AppEntitySystemComponent.hpp"] = { Define = "NAZARA_ENTT" } paths["Core"].Excludes["EnttSystemGraph.hpp"] = { Define = "NAZARA_ENTT" } paths["Core"].Excludes["EnttWorld.hpp"] = { Define = "NAZARA_ENTT" } + paths["Network"].Excludes["CurlLibrary.hpp"] = true + paths["Network"].Excludes["CurlFunctions.hpp"] = true paths["OpenGLRenderer"].Excludes["Wrapper.hpp"] = true paths["VulkanRenderer"].Excludes["Wrapper.hpp"] = true