From b34041e32d0f8d471db9bf9f59702531c14c3351 Mon Sep 17 00:00:00 2001 From: Aleks Margarian Date: Fri, 29 May 2020 14:30:41 -0700 Subject: [PATCH] Version 1.3 - Added notification system for new updates and if files are missing (upon launch) - Remove "command line" version, it isn't really useful and wasn't updated - Added in depth logging (saved to logs folder) - Added a hint for when logging into the game so you don't get confused on what to use as the code - Removed extra buttons in the main window that were no longer needed and added confusion - Added stats (updated every 500 ms-ish). Has info on CPU, RAM, R/W speed, provide speed (to programs using the served files), download speed, "drive" latency (for programs reading the files), and thread count - Progress window has placeholders (instead of being empty) - Overhauled setup window - Rewrote config code to work better for backwards compatibility - Rewrote http code to use curl instead of httplib - Better memory handling when compressing chunks - Storage, manifest, chunks, auth, etc, classes are now OOP instead of C-style functions - EGL2 is now self signed with a certificate (and no longer compressed with upx) to deter any false positive antivirus detection - Refactored/rewrote MountedBuild.cpp to be cleaner - EGL2 now mounts to /game instead of the A: drive (should fix any admin access shenanigans) - Dang that's a lot of changes --- .gitignore | 5 +- CMakeLists.txt | 78 +- Logger.cpp | 15 + Logger.h | 73 + MountedBuild.cpp | 459 +- MountedBuild.h | 27 +- README.md | 12 +- Stats.cpp | 113 + Stats.h | 61 + cert.pfx | Bin 0 -> 2637 bytes cert.pwd | 1 + checks/symlink_workaround.cpp | 2 +- checks/winfspcheck.cpp | 5 +- checks/wintoast_handler.h | 24 + cmd/cmdmain.cpp | 229 - containers/file_sha.h | 8 +- containers/iterable_queue.h | 16 - containers/semaphore.h | 30 - filesystem/dirtree.h | 5 +- filesystem/egfs.cpp | 13 +- filesystem/egfs.h | 9 +- gui/UpdateChecker.cpp | 73 + gui/cApp.cpp | 121 +- gui/cApp.h | 7 +- gui/cAuth.cpp | 2 + gui/cAuth.h | 3 +- gui/cMain.cpp | 441 +- gui/cMain.h | 49 +- gui/cProgress.cpp | 26 +- gui/cProgress.h | 5 +- gui/cSetup.cpp | 256 +- gui/cSetup.h | 43 +- gui/guimain.cpp | 8 +- gui/resources.rc | 27 - gui/settings.cpp | 136 +- gui/settings.h | 45 +- gui/updateChecker.h | 54 + gui/wxLabelSlider.cpp | 111 + gui/wxLabelSlider.h | 47 + libraries/curlion/connection.cpp | 466 ++ libraries/curlion/connection.h | 589 +++ libraries/curlion/connection_manager.cpp | 284 ++ libraries/curlion/connection_manager.h | 119 + libraries/curlion/curlion.h | 10 + libraries/curlion/error.h | 44 + libraries/curlion/http_connection.cpp | 162 + libraries/curlion/http_connection.h | 95 + libraries/curlion/http_form.cpp | 86 + libraries/curlion/http_form.h | 130 + libraries/curlion/log.h | 81 + libraries/curlion/socket_factory.h | 35 + libraries/curlion/socket_watcher.h | 75 + libraries/curlion/timer.h | 39 + .../libdeflate}/libdeflate.h | 0 .../libdeflate}/libdeflatestatic.lib | Bin libraries/signtool/signtool.exe | Bin 0 -> 410056 bytes libraries/wintoast/wintoastlib.cpp | 1145 +++++ libraries/wintoast/wintoastlib.h | 217 + resources.rc | 27 + storage/compression.cpp | 215 +- storage/compression.h | 70 +- storage/sha.h | 2 +- storage/storage.cpp | 576 +-- storage/storage.h | 87 +- web/http.h | 3 +- web/http/Client.cpp | 217 + web/http/Client.h | 25 + web/httplib.cc | 4007 ----------------- web/httplib.h | 908 ---- web/manifest.cpp | 565 --- web/manifest.h | 47 - web/manifest/auth.cpp | 229 + web/manifest/auth.h | 27 + web/manifest/chunk.cpp | 23 + web/manifest/chunk.h | 15 + web/manifest/chunk_part.h | 11 + web/manifest/feature_level.h | 59 + web/manifest/file.cpp | 22 + web/manifest/file.h | 16 + web/manifest/manifest.cpp | 143 + web/manifest/manifest.h | 32 + web/url.hh | 72 - 82 files changed, 6575 insertions(+), 7039 deletions(-) create mode 100644 Logger.cpp create mode 100644 Logger.h create mode 100644 Stats.cpp create mode 100644 Stats.h create mode 100644 cert.pfx create mode 100644 cert.pwd create mode 100644 checks/wintoast_handler.h delete mode 100644 cmd/cmdmain.cpp delete mode 100644 containers/iterable_queue.h delete mode 100644 containers/semaphore.h create mode 100644 gui/UpdateChecker.cpp delete mode 100644 gui/resources.rc create mode 100644 gui/updateChecker.h create mode 100644 gui/wxLabelSlider.cpp create mode 100644 gui/wxLabelSlider.h create mode 100644 libraries/curlion/connection.cpp create mode 100644 libraries/curlion/connection.h create mode 100644 libraries/curlion/connection_manager.cpp create mode 100644 libraries/curlion/connection_manager.h create mode 100644 libraries/curlion/curlion.h create mode 100644 libraries/curlion/error.h create mode 100644 libraries/curlion/http_connection.cpp create mode 100644 libraries/curlion/http_connection.h create mode 100644 libraries/curlion/http_form.cpp create mode 100644 libraries/curlion/http_form.h create mode 100644 libraries/curlion/log.h create mode 100644 libraries/curlion/socket_factory.h create mode 100644 libraries/curlion/socket_watcher.h create mode 100644 libraries/curlion/timer.h rename {libdeflate => libraries/libdeflate}/libdeflate.h (100%) rename {libdeflate => libraries/libdeflate}/libdeflatestatic.lib (100%) create mode 100644 libraries/signtool/signtool.exe create mode 100644 libraries/wintoast/wintoastlib.cpp create mode 100644 libraries/wintoast/wintoastlib.h create mode 100644 resources.rc create mode 100644 web/http/Client.cpp create mode 100644 web/http/Client.h delete mode 100644 web/httplib.cc delete mode 100644 web/httplib.h delete mode 100644 web/manifest.cpp delete mode 100644 web/manifest.h create mode 100644 web/manifest/auth.cpp create mode 100644 web/manifest/auth.h create mode 100644 web/manifest/chunk.cpp create mode 100644 web/manifest/chunk.h create mode 100644 web/manifest/chunk_part.h create mode 100644 web/manifest/feature_level.h create mode 100644 web/manifest/file.cpp create mode 100644 web/manifest/file.h create mode 100644 web/manifest/manifest.cpp create mode 100644 web/manifest/manifest.h delete mode 100644 web/url.hh diff --git a/.gitignore b/.gitignore index 329817a..6b4a2b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ out/ -.vs/ \ No newline at end of file +.vs/ + +cert.pfx +cert.pwd \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 4069eb2..f34dcbd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,57 +2,77 @@ project (EGL2) -set(WITH_GUI ON CACHE BOOL "Compile with a GUI" FORCE) set(WX_DIR "J:\\Code\\wxWidgets" CACHE PATH "wxWidgets directory" FORCE) aux_source_directory(. FILE_SOURCES) aux_source_directory(filesystem FILESYSTEM_FILE_SOURCES) aux_source_directory(storage STORAGE_FILE_SOURCES) aux_source_directory(web WEB_FILE_SOURCES) +aux_source_directory(web/manifest MANIFEST_FILE_SOURCES) +aux_source_directory(web/http HTTP_FILE_SOURCES) aux_source_directory(checks CHECKS_FILE_SOURCES) +aux_source_directory(gui INTERFACE_FILE_SOURCES) +aux_source_directory(libraries/wintoast WINTOAST_FILE_SOURCES) +aux_source_directory(libraries/curlion CURLION_FILE_SOURCES) -if (WITH_GUI) - message("Building with GUI") +add_executable(EGL2 WIN32 + ${INTERFACE_FILE_SOURCES} + "resources.rc" + ${WINTOAST_FILE_SOURCES} + ${CURLION_FILE_SOURCES} + ${FILESYSTEM_FILE_SOURCES} + ${STORAGE_FILE_SOURCES} + ${WEB_FILE_SOURCES} + ${MANIFEST_FILE_SOURCES} + ${HTTP_FILE_SOURCES} + ${CHECKS_FILE_SOURCES} + ${FILE_SOURCES}) - aux_source_directory(gui INTERFACE_FILE_SOURCES) - add_executable(EGL2 WIN32 ${INTERFACE_FILE_SOURCES} "gui/resources.rc" ${FILESYSTEM_FILE_SOURCES} ${STORAGE_FILE_SOURCES} ${WEB_FILE_SOURCES} ${CHECKS_FILE_SOURCES} ${FILE_SOURCES}) - - set(wxWidgets_ROOT_DIR "${WX_DIR}") - set(wxWidgets_LIB_DIR "${WX_DIR}/lib/vc_x64_lib") - set(wxWidgets_EXCLUDE_COMMON_LIBRARIES TRUE) - - if (CMAKE_BUILD_TYPE EQUAL "DEBUG") - set(wxWidgets_USE_DEBUG ON) - else() - set(wxWidgets_USE_DEBUG OFF) - endif() - - set(wxWidgets_USE_STATIC ON) - set(wxWidgets_USE_UNICODE ON) - - find_package(wxWidgets REQUIRED COMPONENTS core base png zlib) - include(${wxWidgets_USE_FILE}) - target_link_libraries(EGL2 PUBLIC ${wxWidgets_LIBRARIES}) +set(wxWidgets_ROOT_DIR "${WX_DIR}") +set(wxWidgets_LIB_DIR "${WX_DIR}/lib/vc_x64_lib") +set(wxWidgets_EXCLUDE_COMMON_LIBRARIES TRUE) +set(wxWidgets_USE_STATIC ON) +set(wxWidgets_USE_UNICODE ON) +if (CMAKE_BUILD_TYPE STREQUAL "Debug") +set(wxWidgets_USE_DEBUG ON) else() - message("Building without GUI") - - aux_source_directory(cmd INTERFACE_FILE_SOURCES) - add_executable(EGL2 ${INTERFACE_FILE_SOURCES} ${FILESYSTEM_FILE_SOURCES} ${STORAGE_FILE_SOURCES} ${WEB_FILE_SOURCES} ${CHECKS_FILE_SOURCES} ${FILE_SOURCES}) +set(wxWidgets_USE_DEBUG OFF) endif() +set(Boost_USE_STATIC_LIBS ON) +add_definitions(-DBOOST_ASIO_DISABLE_CONCEPTS) + set_property(TARGET EGL2 PROPERTY CXX_STANDARD 20) find_package(OpenSSL REQUIRED) find_package(RapidJSON CONFIG REQUIRED) -find_package(ZLIB REQUIRED) find_package(lz4 REQUIRED) +find_package(zstd CONFIG REQUIRED) +find_package(CURL CONFIG REQUIRED) +find_package(BOOST 1.68.0 REQUIRED COMPONENTS asio) +find_package(wxWidgets REQUIRED COMPONENTS core base png zlib) +include(${wxWidgets_USE_FILE}) + +if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") +message(STATUS "Signing files") +file(READ "cert.pwd" SIGN_PASS) +add_custom_command(TARGET EGL2 POST_BUILD + COMMAND + "${CMAKE_SOURCE_DIR}/libraries/signtool/signtool" sign /f "${CMAKE_SOURCE_DIR}/cert.pfx" /p "${SIGN_PASS}" /fd sha1 /t http://timestamp.digicert.com /v $ && + "${CMAKE_SOURCE_DIR}/libraries/signtool/signtool" sign /f "${CMAKE_SOURCE_DIR}/cert.pfx" /p "${SIGN_PASS}" /fd sha256 /tr http://timestamp.digicert.com?td=sha256 /td sha256 /as /v $ +) +endif() set(CompilerFlags CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG + CMAKE_CXX_FLAGS_RELWITHDEBINFO + CMAKE_CXX_FLAGS_MINSIZEREL CMAKE_CXX_FLAGS_RELEASE CMAKE_C_FLAGS CMAKE_C_FLAGS_DEBUG + CMAKE_C_FLAGS_RELWITHDEBINFO + CMAKE_C_FLAGS_MINSIZEREL CMAKE_C_FLAGS_RELEASE ) foreach(CompilerFlag ${CompilerFlags}) @@ -60,5 +80,5 @@ foreach(CompilerFlag ${CompilerFlags}) endforeach() target_link_options(EGL2 PRIVATE "/DELAYLOAD:winfsp-x64.dll") -target_include_directories(EGL2 PRIVATE "$ENV{ProgramFiles\(x86\)}\\WinFsp\\inc" ${RAPIDJSON_INCLUDE_DIRS} "libdeflate") -target_link_libraries(EGL2 PRIVATE "$ENV{ProgramFiles\(x86\)}\\WinFsp\\lib\\winfsp-x64.lib" OpenSSL::SSL OpenSSL::Crypto Crypt32 ZLIB::ZLIB lz4::lz4 delayimp "${CMAKE_CURRENT_SOURCE_DIR}\\libdeflate\\libdeflatestatic.lib") \ No newline at end of file +target_include_directories(EGL2 PRIVATE "$ENV{ProgramFiles\(x86\)}\\WinFsp\\inc" ${Boost_LIBRARIES} ${RAPIDJSON_INCLUDE_DIRS} "libraries\\libdeflate" "libraries\\wintoast" "libraries\\curlion") +target_link_libraries(EGL2 PRIVATE "$ENV{ProgramFiles\(x86\)}\\WinFsp\\lib\\winfsp-x64.lib" ${wxWidgets_LIBRARIES} CURL::libcurl OpenSSL::Crypto Crypt32 libzstd lz4::lz4 delayimp "${CMAKE_CURRENT_SOURCE_DIR}\\libraries\\libdeflate\\libdeflatestatic.lib") \ No newline at end of file diff --git a/Logger.cpp b/Logger.cpp new file mode 100644 index 0000000..cdb9a05 --- /dev/null +++ b/Logger.cpp @@ -0,0 +1,15 @@ +#include "Logger.h" + +#define WIN32_LEAN_AND_MEAN +#include + +bool Logger::Setup() { + auto stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); + if (stdoutHandle != INVALID_HANDLE_VALUE) { + DWORD outMode; + if (GetConsoleMode(stdoutHandle, &outMode)) { + return SetConsoleMode(stdoutHandle, outMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING); + } + } + return false; +} \ No newline at end of file diff --git a/Logger.h b/Logger.h new file mode 100644 index 0000000..ec60e27 --- /dev/null +++ b/Logger.h @@ -0,0 +1,73 @@ +#pragma once + +#define LOG_DEBUG(str, ...) Logger::Log(Logger::LogLevel::DEBUG, LOG_SECTION, str, __VA_ARGS__) +#define LOG_INFO(str, ...) Logger::Log(Logger::LogLevel::INFO, LOG_SECTION, str, __VA_ARGS__) +#define LOG_WARN(str, ...) Logger::Log(Logger::LogLevel::WARN, LOG_SECTION, str, __VA_ARGS__) +#define LOG_ERROR(str, ...) Logger::Log(Logger::LogLevel::ERROR_, LOG_SECTION, str, __VA_ARGS__) +#define LOG_FATAL(str, ...) Logger::Log(Logger::LogLevel::FATAL, LOG_SECTION, str, __VA_ARGS__) + +#include +#include + +class Logger { +public: + enum class LogLevel : uint8_t { + UNKNOWN, + DEBUG, + INFO, + WARN, + ERROR_, + FATAL + }; + + static bool Setup(); + + template + static void Log(LogLevel level, const char* section, const char* str, Args... args) { + auto size = snprintf(nullptr, 0, str, args...) + 1; + auto buf = std::make_unique(size); + snprintf(buf.get(), size, str, args...); + Callback(level, section, buf.get()); + } + + static constexpr const char* LevelAsString(LogLevel level) { + switch (level) + { + case LogLevel::DEBUG: + return "Debug"; + case LogLevel::INFO: + return "Info "; + case LogLevel::WARN: + return "Warn "; + case LogLevel::ERROR_: + return "Error"; + case LogLevel::FATAL: + return "Fatal"; + default: + return "Unkwn"; + } + } + + static constexpr const char* LevelAsColor(LogLevel level) { + switch (level) + { + case LogLevel::DEBUG: + return "\33[0;37m"; + case LogLevel::INFO: + return "\33[0;92m"; + case LogLevel::WARN: + return "\33[0;93m"; + case LogLevel::ERROR_: + return "\33[0;91m"; + case LogLevel::FATAL: + return "\33[0;31m"; + default: + return "\33[0;95m"; + } + } + + static constexpr const char* ResetColor = "\33[0m"; + + using callback = std::function; + static inline callback Callback = nullptr; +}; \ No newline at end of file diff --git a/MountedBuild.cpp b/MountedBuild.cpp index 26725db..df61f7b 100644 --- a/MountedBuild.cpp +++ b/MountedBuild.cpp @@ -1,36 +1,105 @@ #include "MountedBuild.h" -#include "containers/iterable_queue.h" -#include "containers/semaphore.h" +#define GAME_DIR "workaround" +#define LOG_FLAGS 0 // can also be -1 for all flags +#define MB_SDDL_OWNER "S-1-5-18" // Local System +#define MB_SDDL_DATA "P(A;ID;FRFX;;;WD)" // Protected from inheritance, allows it and it's children to give read and execure access to everyone +#define SDDL_ROOT L"D:" MB_SDDL_DATA +#define SDDL_FILE L"O:" MB_SDDL_OWNER "G:" MB_SDDL_OWNER "D:" MB_SDDL_DATA + +#ifndef LOG_SECTION +#define LOG_SECTION "MountedBuild" +#endif + +#include "Logger.h" +#include "Stats.h" #include "containers/file_sha.h" +#include "web/manifest/manifest.h" +#include +#include +#include #include #include -#include -#define fail(format, ...) FspServiceLog(EVENTLOG_ERROR_TYPE, format, ##__VA_ARGS__) +MountedBuild::MountedBuild(Manifest manifest, fs::path mountDir, fs::path cachePath, uint32_t storageFlags, uint32_t memoryPoolCapacity) : + Build(manifest), + MountDir(mountDir), + CacheDir(cachePath), + StorageData(storageFlags, memoryPoolCapacity, CacheDir, Build.CloudDir) +{ + LOG_DEBUG("new (v: %s, mount: %s, cache: %s)", Build.BuildVersion.c_str(), MountDir.string().c_str(), CacheDir.string().c_str()); -#define SDDL_OWNER "S-1-5-18" // Local System -#define SDDL_DATA "P(A;ID;FRFX;;;WD)" // Protected from inheritance, allows it and it's children to give read and execure access to everyone -#define LOG_FLAGS 0 // can also be -1 for all flags + LOG_DEBUG("creating params"); + { + PVOID securityDescriptor; + ULONG securityDescriptorSize; + if (!ConvertStringSecurityDescriptorToSecurityDescriptor(SDDL_FILE, SDDL_REVISION_1, &securityDescriptor, &securityDescriptorSize)) { + LOG_ERROR("invalid sddl (%08x)", FspNtStatusFromWin32(GetLastError())); + } + + EGFS_PARAMS params; + + wcscpy_s(params.FileSystemName, L"EGFS"); + params.VolumePrefix[0] = '\0'; + wcscpy_s(params.VolumeLabel, L"EGL2"); + params.VolumeTotal = Build.GetInstallSize(); + params.VolumeFree = params.VolumeTotal - Build.GetDownloadSize(); + params.Security = securityDescriptor; + params.SecuritySize = securityDescriptorSize; + + params.OnRead = [this](PVOID Handle, PVOID Buffer, UINT64 offset, ULONG length, ULONG* bytesRead) { + FileRead(Handle, Buffer, offset, length, bytesRead); + }; + + params.SectorSize = 512; + params.SectorsPerAllocationUnit = 1; // sectors per cluster (in hardware terms) + params.VolumeCreationTime = 0; + params.VolumeSerialNumber = 0; + params.FileInfoTimeout = INFINITE; // https://github.com/billziss-gh/winfsp/issues/19#issuecomment-289853591 + params.CaseSensitiveSearch = true; + params.CasePreservedNames = true; + params.UnicodeOnDisk = true; + params.PersistentAcls = true; + params.ReparsePoints = true; + params.ReparsePointsAccessCheck = false; + params.PostCleanupWhenModifiedOnly = true; + params.FlushAndPurgeOnCleanup = false; + params.AllowOpenInKernelMode = true; + + params.LogFlags = 0; // -1 enables all (slowdowns imminent due to console spam, though) + + NTSTATUS Result; + Egfs = std::make_unique(¶ms, Result); + if (!NT_SUCCESS(Result)) { + LOG_ERROR("could not create egfs (%08x)", Result); + return; + } + LocalFree(securityDescriptor); + } -#define SDDL_ROOT "D:" SDDL_DATA -#define SDDL_MEMFS "O:" SDDL_OWNER "G:" SDDL_OWNER "D:" SDDL_DATA + LOG_DEBUG("adding files"); + for (auto& file : Build.FileManifestList) { + Egfs->AddFile(file.FileName, &file, file.GetFileSize()); + } + + LOG_DEBUG("setting mount point"); + { + PVOID rootSecurity; + if (!ConvertStringSecurityDescriptorToSecurityDescriptor(SDDL_ROOT, SDDL_REVISION_1, &rootSecurity, NULL)) { + LOG_ERROR("invalid root sddl (%08x)", FspNtStatusFromWin32(GetLastError())); + return; + } + Egfs->SetMountPoint(MountDir.native().c_str(), rootSecurity); + LocalFree(rootSecurity); + } -MountedBuild::MountedBuild(MANIFEST* manifest, fs::path mountDir, fs::path cachePath, ErrorHandler error) { - this->Manifest = manifest; - this->MountDir = mountDir; - this->CacheDir = cachePath; - this->Error = error; - this->Storage = nullptr; - this->Egfs = nullptr; + LOG_DEBUG("starting"); + Egfs->Start(); } MountedBuild::~MountedBuild() { - Unmount(); - if (Storage) { - StorageDelete(Storage); - } + } // 0: valid @@ -47,9 +116,9 @@ inline int ValidChunkFile(fs::path& CacheDir, fs::path ChunkPath) { return (name.size() == 2 && isxdigit(name[0]) && isxdigit(name[1])) ? 0 : 1; } -bool MountedBuild::SetupCacheDirectory() { +bool MountedBuild::SetupCacheDirectory(fs::path CacheDir) { if (!fs::is_directory(CacheDir) && !fs::create_directories(CacheDir)) { - LogError("can't create cachedir %s\n", CacheDir.string().c_str()); + LOG_ERROR("can't create cachedir %s", CacheDir.string().c_str()); return false; } @@ -58,32 +127,47 @@ bool MountedBuild::SetupCacheDirectory() { sprintf(cachePartFolder, "%02X", i); fs::create_directory(CacheDir / cachePartFolder); } + + auto oldGameDir = CacheDir / "game"; + if (fs::is_directory(oldGameDir)) { + LOG_INFO("Removing old game folder %s", oldGameDir.string().c_str()); + std::error_code ec; + for (auto& f : fs::recursive_directory_iterator(oldGameDir)) { + fs::permissions(f, fs::perms::_All_write, ec); + if (ec) { + LOG_INFO(f.path().string().c_str()); + LOG_ERROR(ec.message().c_str()); + } + } + fs::remove_all(oldGameDir, ec); + if (ec) { + LOG_ERROR("Could not remove old game folder: %s", ec.message().c_str()); + } + } return true; } -void inline PreloadFile(STORAGE* Storage, MANIFEST_FILE* File, uint32_t ThreadCount, cancel_flag& cancelFlag) { - MANIFEST_CHUNK_PART* ChunkParts; - uint32_t ChunkCount; - uint16_t ChunkStride; - ManifestFileGetChunks(File, &ChunkParts, &ChunkCount, &ChunkStride); - - iterable_queue threads; - for (int i = 0, n = 0; i < ChunkCount * ChunkStride && !cancelFlag.cancelled(); i += ChunkStride) { - auto Chunk = ManifestFileChunkGetChunk((MANIFEST_CHUNK_PART*)((char*)ChunkParts + i)); - if (StorageChunkDownloaded(Storage, Chunk)) { +void MountedBuild::PreloadFile(File& File, uint32_t ThreadCount, cancel_flag& cancelFlag) { + std::deque threads; + int n = 0; + for (auto& chunkPart : File.ChunkParts) { + if (cancelFlag.cancelled()) { + break; + } + if (StorageData.IsChunkDownloaded(chunkPart)) { continue; } // cheap semaphore, keeps thread count low instead of having 81k threads pile up while (threads.size() >= ThreadCount) { threads.front().join(); - threads.pop(); + threads.pop_front(); } - threads.push(std::thread(StorageDownloadChunk, Storage, Chunk, [&n, &ChunkCount](const char* buf, uint32_t bufSize) - { - printf("\r%d downloaded / %d total (%.2f%%)", ++n, ChunkCount, float(n * 100) / ChunkCount); - })); + + threads.emplace_back([&, this]() { + StorageData.GetChunkPart(chunkPart); + }); } if (cancelFlag.cancelled()) { @@ -96,16 +180,14 @@ void inline PreloadFile(STORAGE* Storage, MANIFEST_FILE* File, uint32_t ThreadCo thread.join(); } } - - printf("\r%d downloaded / %d total (%.2f%%)\n", ChunkCount, ChunkCount, float(100)); } -bool inline CompareFile(MANIFEST_FILE* File, fs::path FilePath) { +bool inline CompareFile(File& File, fs::path FilePath) { if (fs::status(FilePath).type() != fs::file_type::regular) { return false; } - if (fs::file_size(FilePath) != ManifestFileGetFileSize(File)) { + if (fs::file_size(FilePath) != File.GetFileSize()) { return false; } @@ -114,43 +196,36 @@ bool inline CompareFile(MANIFEST_FILE* File, fs::path FilePath) { return false; } - return !memcmp(FileSha, ManifestFileGetSha1(File), 20); + return !memcmp(FileSha, File.ShaHash, 20); } -bool MountedBuild::SetupGameDirectory(ProgressSetMaxHandler setMax, ProgressIncrHandler progress, cancel_flag& cancelFlag, uint32_t threadCount, EnforceSymlinkCreationHandler enforceSymlinkCreation) { - auto gameDir = CacheDir / "game"; +bool MountedBuild::SetupGameDirectory(uint32_t threadCount) { + auto gameDir = CacheDir / GAME_DIR; if (!fs::is_directory(gameDir) && !fs::create_directories(gameDir)) { - LogError("can't create gamedir\n"); + LOG_ERROR("can't create gamedir"); return false; } bool symlinkCreationEnforced = false; - MANIFEST_FILE* Files; - uint32_t FileCount; - uint16_t FileStride; - char FilenameBuffer[128]; - ManifestGetFiles(Manifest, &Files, &FileCount, &FileStride); - setMax(FileCount); - for (int i = 0; i < FileCount * FileStride && !cancelFlag.cancelled(); i += FileStride) { - auto File = (MANIFEST_FILE*)((char*)Files + i); - ManifestFileGetName(File, FilenameBuffer); - fs::path filePath = fs::path(FilenameBuffer); + cancel_flag flag; + for (auto& file : Build.FileManifestList) { + fs::path filePath = file.FileName; fs::path folderPath = filePath.parent_path(); if (!fs::create_directories(gameDir / folderPath) && !fs::is_directory(gameDir / folderPath)) { - LogError("can't create %s\n", (gameDir / folderPath).string().c_str()); + LOG_ERROR("can't create %s\n", (gameDir / folderPath).string().c_str()); goto continueFileLoop; } do { if (folderPath.filename() == "Binaries") { - if (!CompareFile(File, gameDir / filePath)) { - PreloadFile(Storage, File, threadCount, cancelFlag); + if (!CompareFile(file, gameDir / filePath)) { + PreloadFile(file, threadCount, flag); if (fs::status(gameDir / filePath).type() == fs::file_type::regular) { fs::permissions(gameDir / filePath, fs::perms::_All_write, fs::perm_options::add); // copying over a file from the drive gives it the read-only attribute, this overrides that } if (!fs::copy_file(MountDir / filePath, gameDir / filePath, fs::copy_options::overwrite_existing)) { - LogError("failed to copy %s\n", filePath.string().c_str()); + LOG_ERROR("failed to copy %s", filePath.string().c_str()); } } goto continueFileLoop; @@ -158,72 +233,62 @@ bool MountedBuild::SetupGameDirectory(ProgressSetMaxHandler setMax, ProgressIncr folderPath = folderPath.parent_path(); } while (folderPath != folderPath.root_path()); - if (!fs::is_symlink(gameDir / filePath)) { - if (!symlinkCreationEnforced) { - if (!enforceSymlinkCreation()) { - return false; - } - symlinkCreationEnforced = true; + if (fs::is_symlink(gameDir / filePath)) { + if (fs::read_symlink(gameDir / filePath) != MountDir / filePath) { + fs::remove(gameDir / filePath); // remove if exists and is invalid + fs::create_symlink(MountDir / filePath, gameDir / filePath); } + } + else { fs::create_symlink(MountDir / filePath, gameDir / filePath); } continueFileLoop: - progress(); - } - return true; -} - -bool MountedBuild::StartStorage(uint32_t storageFlags) { - if (Storage) { - return true; - } - char CloudDirHost[64]; - char CloudDirPath[64]; - ManifestGetCloudDir(Manifest, CloudDirHost, CloudDirPath); - if (!StorageCreate(storageFlags, CacheDir.native().c_str(), CloudDirHost, CloudDirPath, &Storage)) { - LogError("cannot create storage"); - return false; + ; } return true; } bool MountedBuild::PreloadAllChunks(ProgressSetMaxHandler setMax, ProgressIncrHandler progress, cancel_flag& cancelFlag, uint32_t threadCount) { - std::shared_ptr* ChunkList; - uint32_t ChunkCount; - ManifestGetChunks(Manifest, &ChunkList, &ChunkCount); - LogError("set chunk count to %u", ChunkCount); - setMax(ChunkCount); + LOG_DEBUG("preloading"); + setMax(GetMissingChunkCount()); - iterable_queue threads; + std::deque threads; - for (auto Chunk = ChunkList; Chunk != ChunkList + ChunkCount && !cancelFlag.cancelled(); Chunk++) { - if (StorageChunkDownloaded(Storage, Chunk->get())) { + int n = 0; + for (auto& chunk : Build.ChunkManifestList) { + if (cancelFlag.cancelled()) { + break; + } + if (StorageData.IsChunkDownloaded(chunk)) { //LogError("already downloaded", ChunkCount); - progress(); + //progress(); continue; } // cheap semaphore, keeps thread count low instead of having 81k threads pile up while (threads.size() >= threadCount) { threads.front().join(); - threads.pop(); + threads.pop_front(); } - threads.push(std::thread(StorageDownloadChunk, Storage, Chunk->get(), std::bind(progress))); + + threads.emplace_back([&, this]() { + StorageData.GetChunk(chunk); + progress(); + }); } if (cancelFlag.cancelled()) { for (auto& thread : threads) { thread.detach(); - progress(); } } else { for (auto& thread : threads) { thread.join(); - progress(); } } + LOG_DEBUG("preloaded"); return true; } @@ -231,26 +296,18 @@ bool MountedBuild::PreloadAllChunks(ProgressSetMaxHandler setMax, ProgressIncrHa #define NTOHLL(x) ((1==ntohl(1)) ? (x) : (((uint64_t)ntohl((x) & 0xFFFFFFFFUL)) << 32) | ntohl((uint32_t)((x) >> 32))) auto guidHash = [](const char* n) { return (*((uint64_t*)n)) ^ (*(((uint64_t*)n) + 1)); }; auto guidEqual = [](const char* a, const char* b) {return !memcmp(a, b, 16); }; -void MountedBuild::PurgeUnusedChunks(ProgressSetMaxHandler setMax, ProgressIncrHandler progress, cancel_flag& cancelFlag) { - std::shared_ptr* ChunkList; - uint32_t ChunkCount; - ManifestGetChunks(Manifest, &ChunkList, &ChunkCount); - +void MountedBuild::PurgeUnusedChunks() { + LOG_DEBUG("purging"); std::unordered_set ManifestGuids; - ManifestGuids.reserve(ChunkCount); - for (auto Chunk = ChunkList; Chunk != ChunkList + ChunkCount; Chunk++) { - ManifestGuids.insert(ManifestChunkGetGuid(Chunk->get())); + ManifestGuids.reserve(Build.ChunkManifestList.size()); + for (auto& chunk : Build.ChunkManifestList) { + ManifestGuids.insert(chunk->Guid); } - setMax(std::count_if(fs::recursive_directory_iterator(CacheDir), fs::recursive_directory_iterator(), [this](const fs::directory_entry& f) { return f.is_regular_file() && ValidChunkFile(CacheDir, f.path()) == 0; })); - char guidBuffer[16]; char guidBuffer2[16]; auto iterator = fs::recursive_directory_iterator(CacheDir); for (auto& p : iterator) { - if (cancelFlag.cancelled()) { - break; - } if (!p.is_regular_file()) { continue; } @@ -271,20 +328,20 @@ void MountedBuild::PurgeUnusedChunks(ProgressSetMaxHandler setMax, ProgressIncrH fs::remove(p); } } - progress(); } + LOG_DEBUG("purged"); } void MountedBuild::VerifyAllChunks(ProgressSetMaxHandler setMax, ProgressIncrHandler progress, cancel_flag& cancelFlag, uint32_t threadCount) { - std::shared_ptr* ChunkList; - uint32_t ChunkCount; - ManifestGetChunks(Manifest, &ChunkList, &ChunkCount); - setMax((std::min)(ChunkCount, (uint32_t)std::count_if(fs::recursive_directory_iterator(CacheDir), fs::recursive_directory_iterator(), [this](const fs::directory_entry& f) { return f.is_regular_file() && ValidChunkFile(CacheDir, f.path()) == 0; }))); + setMax((std::min)(Build.ChunkManifestList.size(), (size_t)std::count_if(fs::recursive_directory_iterator(CacheDir), fs::recursive_directory_iterator(), [this](const fs::directory_entry& f) { return f.is_regular_file() && ValidChunkFile(CacheDir, f.path()) == 0; }))); - iterable_queue threads; + std::deque threads; - for (auto Chunk = ChunkList; Chunk != ChunkList + ChunkCount && !cancelFlag.cancelled(); Chunk++) { - if (!StorageChunkDownloaded(Storage, Chunk->get())) { + for (auto& chunk : Build.ChunkManifestList) { + if (cancelFlag.cancelled()) { + break; + } + if (!StorageData.IsChunkDownloaded(chunk)) { continue; } @@ -292,10 +349,16 @@ void MountedBuild::VerifyAllChunks(ProgressSetMaxHandler setMax, ProgressIncrHan while (threads.size() >= threadCount) { threads.front().join(); progress(); - threads.pop(); + threads.pop_front(); } - threads.push(std::thread(StorageVerifyChunk, Storage, Chunk->get())); + threads.emplace_back(std::thread([=, this]() { + if (!StorageData.VerifyChunk(chunk)) { + LOG_WARN("Invalid hash"); + StorageData.DeleteChunk(chunk); + StorageData.GetChunk(chunk); + } + })); } if (cancelFlag.cancelled()) { @@ -311,13 +374,14 @@ void MountedBuild::VerifyAllChunks(ProgressSetMaxHandler setMax, ProgressIncrHan } } +uint32_t MountedBuild::GetMissingChunkCount() +{ + return std::count_if(Build.ChunkManifestList.begin(), Build.ChunkManifestList.end(), [&](auto& a) { return !StorageData.IsChunkDownloaded(a); }); +} + void MountedBuild::LaunchGame(const char* additionalArgs) { - char ExeBuf[MAX_PATH]; - char CmdBuf[512]; - ManifestGetLaunchInfo(Manifest, ExeBuf, CmdBuf); - strcat(CmdBuf, " "); - strcat(CmdBuf, additionalArgs); - fs::path exePath = CacheDir / "game" / ExeBuf; + std::string CmdBuf = Build.LaunchCommand + " " + additionalArgs; + fs::path exePath = CacheDir / GAME_DIR / Build.LaunchExe; PROCESS_INFORMATION pi; STARTUPINFOA si; @@ -326,152 +390,43 @@ void MountedBuild::LaunchGame(const char* additionalArgs) { memset(&si, 0, sizeof(si)); si.cb = sizeof(si); - CreateProcessA(exePath.string().c_str(), CmdBuf, NULL, NULL, FALSE, DETACHED_PROCESS, NULL, exePath.parent_path().string().c_str(), &si, &pi); + CreateProcessA(exePath.string().c_str(), (LPSTR)CmdBuf.c_str(), NULL, NULL, FALSE, DETACHED_PROCESS, NULL, exePath.parent_path().string().c_str(), &si, &pi); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } -bool MountedBuild::Mount() { - if (Mounted()) { - return true; - } - - FspDebugLogSetHandle(GetStdHandle(STD_OUTPUT_HANDLE)); - - { - PVOID securityDescriptor; - ULONG securityDescriptorSize; - if (!ConvertStringSecurityDescriptorToSecurityDescriptorA(SDDL_MEMFS, SDDL_REVISION_1, &securityDescriptor, &securityDescriptorSize)) { - fail(L"invalid sddl: %08x", FspNtStatusFromWin32(GetLastError())); - return false; - } - - auto downloadSize = ManifestDownloadSize(Manifest); - auto installSize = ManifestInstallSize(Manifest); - EGFS_PARAMS params; - - wcscpy_s(params.FileSystemName, L"EGFS"); - params.VolumePrefix[0] = 0; - // VolumePrefix stays 0 for now, maybe change it later - wcscpy_s(params.VolumeLabel, L"EGL2"); - params.VolumeTotal = installSize; - params.VolumeFree = installSize - downloadSize; - params.Security = securityDescriptor; - params.SecuritySize = securityDescriptorSize; - - params.OnRead = std::bind(&MountedBuild::FileRead, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5); - - params.SectorSize = 512; - params.SectorsPerAllocationUnit = 1; // sectors per cluster (in hardware terms) - params.VolumeCreationTime = 0; - params.VolumeSerialNumber = 0; - params.FileInfoTimeout = INFINITE; // https://github.com/billziss-gh/winfsp/issues/19#issuecomment-289853591 - params.CaseSensitiveSearch = true; - params.CasePreservedNames = true; - params.UnicodeOnDisk = true; - params.PersistentAcls = true; - params.ReparsePoints = true; - params.ReparsePointsAccessCheck = false; - params.PostCleanupWhenModifiedOnly = true; - params.FlushAndPurgeOnCleanup = false; - params.AllowOpenInKernelMode = true; - - params.LogFlags = 0; // -1 enables all (slowdowns imminent due to console spam, though) - - NTSTATUS Result; - Egfs = new EGFS(¶ms, Result); - if (!NT_SUCCESS(Result)) { - fail(L"could not create eglfs: %08x", Result); - return false; - } - LocalFree(securityDescriptor); - } - - { - MANIFEST_FILE* Files; - uint32_t FileCount; - uint16_t FileStride; - char FilenameBuffer[128]; - ManifestGetFiles(Manifest, &Files, &FileCount, &FileStride); - for (int i = 0; i < FileCount * FileStride; i += FileStride) { - auto File = (MANIFEST_FILE*)((char*)Files + i); - ManifestFileGetName(File, FilenameBuffer); - Egfs->AddFile(fs::path(FilenameBuffer), File, ManifestFileGetFileSize(File)); - } - } - - { - PVOID rootSecurity; - if (!ConvertStringSecurityDescriptorToSecurityDescriptorA(SDDL_ROOT, SDDL_REVISION_1, &rootSecurity, NULL)) { - fail(L"invalid root sddl: %08x", FspNtStatusFromWin32(GetLastError())); - return false; - } - Egfs->SetMountPoint(MountDir.native().c_str(), rootSecurity); - LocalFree(rootSecurity); - } - - Egfs->Start(); - return true; -} - -bool MountedBuild::Unmount() { - if (!Mounted()) { - return true; - } - - delete Egfs; - Egfs = nullptr; - - return true; -} - -bool MountedBuild::Mounted() { - return Egfs; -} - -void MountedBuild::LogError(const char* format, ...) -{ - va_list argp; - va_start(argp, format); - char* buf = new char[snprintf(nullptr, 0, format, argp) + 1]; - vsprintf(buf, format, argp); - va_end(argp); - Error(buf); - delete[] buf; -} - void MountedBuild::FileRead(PVOID Handle, PVOID Buffer, UINT64 offset, ULONG length, ULONG* bytesRead) { - auto File = (MANIFEST_FILE*)Handle; + auto startTime = std::chrono::steady_clock::now(); + + auto file = (File*)Handle; + //LOG_DEBUG("Reading %s, at %d: %d", file->FileName.c_str(), offset, length); uint32_t ChunkStartIndex, ChunkStartOffset; - if (ManifestFileGetChunkIndex(File, offset, &ChunkStartIndex, &ChunkStartOffset)) { - MANIFEST_CHUNK_PART* ChunkParts; - uint32_t ChunkPartCount, BytesRead = 0; - uint16_t StrideSize; - ManifestFileGetChunks(File, &ChunkParts, &ChunkPartCount, &StrideSize); - for (int i = ChunkStartIndex * StrideSize; i < ChunkPartCount * StrideSize; i += StrideSize) { - auto chunkPart = (MANIFEST_CHUNK_PART*)((char*)ChunkParts + i); - uint32_t ChunkOffset, ChunkSize; - ManifestFileChunkGetData(chunkPart, &ChunkOffset, &ChunkSize); - char* ChunkBuffer = new char[ChunkSize]; - StorageDownloadChunkPart(Storage, chunkPart, ChunkBuffer); - if (((int64_t)length - (int64_t)BytesRead) > (int64_t)ChunkSize - (int64_t)ChunkStartOffset) { // copy the entire buffer over - memcpy((char*)Buffer + BytesRead, ChunkBuffer + ChunkStartOffset, ChunkSize - ChunkStartOffset); - BytesRead += ChunkSize - ChunkStartOffset; + if (file->GetChunkIndex(offset, ChunkStartIndex, ChunkStartOffset)) { + uint32_t BytesRead = 0; + for (auto chunkPart = file->ChunkParts.begin() + ChunkStartIndex; chunkPart != file->ChunkParts.end(); chunkPart++) { + auto chunkBuffer = StorageData.GetChunkPart(*chunkPart); + if (((int64_t)length - (int64_t)BytesRead) > (int64_t)chunkPart->Size - (int64_t)ChunkStartOffset) { // copy the entire buffer over + //LOG_DEBUG("Copying to %d, size %d", BytesRead, chunkPart->Size - ChunkStartOffset); + memcpy((char*)Buffer + BytesRead, chunkBuffer.get() + ChunkStartOffset, chunkPart->Size - ChunkStartOffset); + BytesRead += chunkPart->Size - ChunkStartOffset; } else { // copy what it needs to fill up the rest - memcpy((char*)Buffer + BytesRead, ChunkBuffer + ChunkStartOffset, length - BytesRead); + //LOG_DEBUG("Copying to %d, size %d", BytesRead, length - BytesRead); + memcpy((char*)Buffer + BytesRead, chunkBuffer.get() + ChunkStartOffset, length - BytesRead); BytesRead += (int64_t)length - (int64_t)BytesRead; - delete[] ChunkBuffer; - *bytesRead = BytesRead; - return; + break; } - delete[] ChunkBuffer; ChunkStartOffset = 0; } + Stats::ProvideCount.fetch_add(BytesRead, std::memory_order_relaxed); *bytesRead = BytesRead; } else { *bytesRead = 0; } + + auto endTime = std::chrono::steady_clock::now(); + Stats::LatOpCount.fetch_add(1, std::memory_order_relaxed); + Stats::LatNsCount.fetch_add((endTime - startTime).count(), std::memory_order_relaxed); } \ No newline at end of file diff --git a/MountedBuild.h b/MountedBuild.h index 969c315..0e02610 100644 --- a/MountedBuild.h +++ b/MountedBuild.h @@ -1,8 +1,9 @@ #pragma once +#define NOMINMAX #include "containers/cancel_flag.h" #include "filesystem/egfs.h" -#include "web/manifest.h" +#include "web/manifest/manifest.h" #include "storage/storage.h" #include @@ -10,34 +11,28 @@ namespace fs = std::filesystem; typedef std::function ProgressSetMaxHandler; typedef std::function ProgressIncrHandler; -typedef std::function ErrorHandler; -typedef std::function EnforceSymlinkCreationHandler; class MountedBuild { public: - MountedBuild(MANIFEST* manifest, fs::path mountDir, fs::path cachePath, ErrorHandler error); + MountedBuild(Manifest manifest, fs::path mountDir, fs::path cachePath, uint32_t storageFlags, uint32_t memoryPoolCapacity); ~MountedBuild(); - bool SetupCacheDirectory(); - bool SetupGameDirectory(ProgressSetMaxHandler setMax, ProgressIncrHandler progress, cancel_flag& cancelFlag, uint32_t threadCount, EnforceSymlinkCreationHandler enforceSymlinkCreation); - bool StartStorage(uint32_t storageFlags); + static bool SetupCacheDirectory(fs::path CacheDir); + bool SetupGameDirectory(uint32_t threadCount); bool PreloadAllChunks(ProgressSetMaxHandler setMax, ProgressIncrHandler progress, cancel_flag& cancelFlag, uint32_t threadCount); - void PurgeUnusedChunks(ProgressSetMaxHandler setMax, ProgressIncrHandler progress, cancel_flag& cancelFlag); + void PurgeUnusedChunks(); void VerifyAllChunks(ProgressSetMaxHandler setMax, ProgressIncrHandler progress, cancel_flag& cancelFlag, uint32_t threadCount); + uint32_t GetMissingChunkCount(); void LaunchGame(const char* additionalArgs); - bool Mount(); - bool Unmount(); - bool Mounted(); private: - void LogError(const char* format, ...); + void PreloadFile(File& File, uint32_t ThreadCount, cancel_flag& cancelFlag); void FileRead(PVOID Handle, PVOID Buffer, UINT64 offset, ULONG length, ULONG* bytesRead); fs::path MountDir; fs::path CacheDir; - MANIFEST* Manifest; - STORAGE* Storage; - EGFS* Egfs; - ErrorHandler Error; + Manifest Build; + Storage StorageData; + std::unique_ptr Egfs; }; \ No newline at end of file diff --git a/README.md b/README.md index a4a1cb8..a186f12 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ - Trade performance for disk space, keep your game install compressed ## Installation / Getting Started +### This setup is currently outdated. An updated version will be put up soon. Right now, all you need to know is that: **Your install folder needs to be empty! This is separate from what the official launcher has!** The process can be a little awkward, so here's a simple step by step guide: |Description|Guide (Click one to view it)| |--|--| @@ -36,12 +37,13 @@ I use [CMake for Visual Studio](https://docs.microsoft.com/en-us/cpp/build/cmake - (Install these packages with `x64-windows-static`) - OpenSSL - RapidJSON - - zLib - - LZ4 - - wxWidgets (if compiling with a GUI) + - lz4 + - zstd + - curl + - boost + - wxWidgets - Make sure to set the subsystem from /MD to /MT when compiling - WinFsp with the "Developer" feature installed ### CMake Build Options - - `WITH_GUI` - if set to false, a command line version will be built instead. (Going to be honest, I haven't checked for any crashes/errors in the console version, but it should work) - - `WX_DIR` - if you want to build EGL2 with a GUI, set this path to your wxWidgets directory. \ No newline at end of file + - `WX_DIR` - Set this path to your wxWidgets directory. \ No newline at end of file diff --git a/Stats.cpp b/Stats.cpp new file mode 100644 index 0000000..eaab7a8 --- /dev/null +++ b/Stats.cpp @@ -0,0 +1,113 @@ +#include "Stats.h" + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include + +constexpr inline ULONGLONG ConvertTime(FILETIME& ft) { + return ULARGE_INTEGER{ ft.dwLowDateTime, ft.dwHighDateTime }.QuadPart; +} + +void Stats::StartUpdateThread(ch::milliseconds refreshRate, std::function updateCallback) { + UpdateThread = std::thread([refreshRate, updateCallback]() { + StatsUpdateData updateData; + memset(&updateData, 0, sizeof StatsUpdateData); + + auto refreshScale = 1000.f / refreshRate.count(); + + uint64_t prevSysIdle = 0; + uint64_t prevSysUse = 0; + uint64_t prevProcUse = 0; + FILETIME sysIdleFt, sysKernelFt, sysUserFt; + FILETIME procCreateFt, procExitFt, procKernelFt, procUserFt; + + PROCESS_MEMORY_COUNTERS_EX pmc; + + uint64_t prevRead = 0; + uint64_t prevWrite = 0; + uint64_t prevProvide = 0; + uint64_t prevDownload = 0; + uint64_t prevLatOp = 0; + uint64_t prevLatNs = 0; + while (!UpdateFlag.cancelled()) { + // cpu calculation + if (GetSystemTimes(&sysIdleFt, &sysKernelFt, &sysUserFt) && GetProcessTimes(GetCurrentProcess(), &procCreateFt, &procExitFt, &procKernelFt, &procUserFt)) { + uint64_t sysIdle = ConvertTime(sysIdleFt); + uint64_t sysUse = ConvertTime(sysKernelFt) + ConvertTime(sysUserFt); + uint64_t procUse = ConvertTime(procKernelFt) + ConvertTime(procUserFt); + + if (prevSysIdle) + { + uint64_t sysTotal = sysUse - prevSysUse; + uint64_t procTotal = procUse - prevProcUse; + + if (sysTotal > 0) { + updateData.cpu = float((double)procTotal / sysTotal * 100); + } + } + + prevSysIdle = sysIdle; + prevSysUse = sysUse; + prevProcUse = procUse; + } + + // ram calculation + if (GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS*)&pmc, sizeof(pmc))) { + updateData.ram = pmc.PrivateUsage; + } + + // thread count calculation + { + auto procId = GetCurrentProcessId(); + HANDLE hnd = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, procId); + if (hnd != INVALID_HANDLE_VALUE) { + THREADENTRY32 threadEntry; + threadEntry.dwSize = sizeof THREADENTRY32; + + if (Thread32First(hnd, &threadEntry)) { + int threadCount = 0; + do { + if (threadEntry.th32OwnerProcessID == procId) { + threadCount++; + } + } while (Thread32Next(hnd, &threadEntry)); + updateData.threads = threadCount; + } + + CloseHandle(hnd); + } + } + + // other atomic stuff to pass + uint64_t read = FileReadCount.load(std::memory_order_relaxed); + uint64_t write = FileWriteCount.load(std::memory_order_relaxed); + uint64_t provide = ProvideCount.load(std::memory_order_relaxed); + uint64_t download = DownloadCount.load(std::memory_order_relaxed); + uint64_t latOp = LatOpCount.load(std::memory_order_relaxed); + uint64_t latNs = LatNsCount.load(std::memory_order_relaxed); + + updateData.read = (read - prevRead) * refreshScale; + updateData.write = (write - prevWrite) * refreshScale; + updateData.provide = (provide - prevProvide) * refreshScale; + updateData.download = (download - prevDownload) * refreshScale; + updateData.latency = ((double)(latNs - prevLatNs) / (latOp - prevLatOp)) / 1000000 * refreshScale; + + prevRead = read; + prevWrite = write; + prevProvide = provide; + prevDownload = download; + prevLatOp = latOp; + prevLatNs = latNs; + + updateCallback(updateData); + + std::this_thread::sleep_for(refreshRate); + } + }); + UpdateThread.detach(); +} + +void Stats::StopUpdateThread() { + UpdateFlag.cancel(); +} \ No newline at end of file diff --git a/Stats.h b/Stats.h new file mode 100644 index 0000000..2eb3c0f --- /dev/null +++ b/Stats.h @@ -0,0 +1,61 @@ +#pragma once + +#include "containers/cancel_flag.h" + +#include +#include +#include +#include + +namespace ch = std::chrono; + +struct StatsUpdateData { +#define DEFINE_STAT(name, type) type name; + + DEFINE_STAT(cpu, float) + DEFINE_STAT(ram, size_t) + DEFINE_STAT(read, size_t) + DEFINE_STAT(write, size_t) + DEFINE_STAT(provide, size_t) + DEFINE_STAT(download, size_t) + DEFINE_STAT(latency, float) + DEFINE_STAT(threads, int) + +#undef DEFINE_STAT +}; + +class Stats { +public: + Stats() = delete; + Stats(const Stats&) = delete; + Stats& operator=(const Stats&) = delete; + + static inline const wxString GetReadableSize(size_t size) { + if (!size) { + return "0 B"; + } + + static constexpr const char* suffix[] = { "B", "KB", "MB", "GB", "TB", "PB", "EB" }; + int i = 0; + auto sizeD = (double)size; + while (sizeD >= 1024) { + sizeD /= 1024; + i++; + } + return wxString::Format("%.*f %s", 2 - (int)floor(log10(sizeD)), sizeD, suffix[i]); + } + + static void StartUpdateThread(ch::milliseconds refreshRate, std::function updateCallback); + static void StopUpdateThread(); + + static inline std::atomic_uint64_t ProvideCount = 0; // std::atomic_uint_fast64_t is the same in msvc + static inline std::atomic_uint64_t FileReadCount = 0; + static inline std::atomic_uint64_t FileWriteCount = 0; + static inline std::atomic_uint64_t DownloadCount = 0; + static inline std::atomic_uint64_t LatOpCount = 0; + static inline std::atomic_uint64_t LatNsCount = 0; + +private: + static inline std::thread UpdateThread; + static inline cancel_flag UpdateFlag; +}; \ No newline at end of file diff --git a/cert.pfx b/cert.pfx new file mode 100644 index 0000000000000000000000000000000000000000..8ea44377a5ae5a5a793b1700ce051ca50eddc954 GIT binary patch literal 2637 zcmZWqXH=6}6Mhpy=m;ht0RmYmf`A0+B8v2)fJi7Jz1KkKhzKtorMn2yMVj=^qJj&; zQba;;k=~>QrMfJzSnR*zZ_wIQ>5`MVuK% zTQ2g`On~GH2!+xRVIXXR*osa`$R*+H2mFrVXx1oxSE@)gG1PCp}mG0(@_~4^!v%hZ0z%|F&hx` zN45)eLFNCH8PgxBrttaLEg^hvcz~CieA{s4Bpsl#m7gEo~c$a1jyUooXkCbHzuN=;rX<+ z2x;huR8**86tCBRa9CYxX>++eTFY%>DY}Jbwdc@Wh`>It4afYnx0Nq|5HKP}^Xr-L zK;wG=&CD$iWd^Yme^g9bHm;Pk_Q%1yqCg{jX7xO?n-U*2ZqiGs8-e8zW7qDRas4y-hF#7y8rR_q8WPg#@8>P zVxJ?W!`g1YKHg!UY%!T7&l`v@fcUp(GW~qQq~;cX<8%UMA(GccaBU+j3jOZJlBSYx zZQraoi%Xn6ut)ZMdj&f}+J(FNPUg=iZJN;UmkR19eN}8ebspq1x9;tWVsHnZa-lP* zEp(%%cfpa}TfOTc38I^ddw5Q^Qsbsi)eLdQeQEX!D}=OiZ;suM z-LTO_`o_qbG9n9RqiI*p6yz5Rh-d|Ss9q|}^_6%8K}^-Sj((2sszq>Nxp&Hz%8nRK z-bjLMxT9gm5Z8wnwIz3hZ)YwpjE}x=#iK&qNQtkK1wroNJ4ww8g2gBW4uG>Zh zGbeNp0%bozKNZD}@{HziKVNsDGh#&e8%1mrCh;c zb2`r5(E{D^Jz(;v;mo*l3$`2px-Tq8TC2M4H>?r#R{HH%1w@@tA^u)^r;|~4{c5#w z{F{8m_cTsciWYZ1@zNfr>JjB697zahX|;T14T-%b$*#EjLzDpdaZTSInL~1uc`19l&hDz8_V@QGO$_a_`{lbP+?3m66 zJj#lYmH~;d1uPSsP6Q5tK|onns04sT{QG;-u}Q#TFbDtuSo8ltq=Y_?F5m;$17d(I zAOXk$(ts^s2RH!o6vTmIb`)F;kOHhJxilq{rA&Rv zX#w&>{tnk2h=0GS&l~mZ%b2zTBYhYJX z@g&L7VE8Mc@;miG8(p2cLWsp~GWOS3SCq6bqtO?c5+x8B8kw9!Y^1dAp(VpIq2d{< zxAO_a0fFnx2y|X*WRR+msjk|~r2P|fmMYbk`Hz}gvRbs%rz-I$D6fs)i`U>H&{mkA zCGFh%H%IUSHoA1GEYedy8E5H)Zd_(0Ae{?0 zr3+K)B*YBK5o5y1>h4AuI==SCZ|8W;jiiXgg^Ode8_VOQ^e^LSpyN3U*qB#k+T2Ioc}a$auC*rC9w zjBX%nE#aH3?B;ep)%^VN zH6ig?7ONTO@1K0_evzJQa@qIYYlUjuNA$F+PfDC9e$6--#6`#Iv_wQ#*$nCw`5$&< zO%#P0iPrZxd}9+FV~$TG%-FBuzO3EsLQPC4oM_{htpXPpU%$}cw|AEgyj$QG1|!m1 zgDMA;_vTex1|mCVX)ITK3DO+f{aO)vn4SR1(6)i<%qErGy^dC#v{E5zFb32SMRrXU zq=xN2))aMeZK?OP%5W}^9qf^JJ9IN?H>`-d2?(yC9)gXfrCf~qsjbw@ivjDq-7fiA zp>72i;<9*od-vceOnaX&vl z%-I{E4+<)m!S(P2lyLW=N2Aa(5dq(mQL+N{Z6$1-7HzW6BH(9=$gh?g6|Lj=oeM^% zI)Oxk6`pd%l;FBLCb{8G@%x*jMc&T0Ma)MEXd!~_lWJ!?Po(T(BrU@7wq_qi3e?So zG)UJQJD+6x|LQb74VUzD`W35y<;Bv#sYRHnpqwydDq_%`&%O9=4YWRy;~M`TEk1lChxx literal 0 HcmV?d00001 diff --git a/cert.pwd b/cert.pwd new file mode 100644 index 0000000..d3753ed --- /dev/null +++ b/cert.pwd @@ -0,0 +1 @@ +ztu2fJSLaX5eGbk3zN9i \ No newline at end of file diff --git a/checks/symlink_workaround.cpp b/checks/symlink_workaround.cpp index 7791432..dee8dca 100644 --- a/checks/symlink_workaround.cpp +++ b/checks/symlink_workaround.cpp @@ -1,6 +1,6 @@ #include "symlink_workaround.h" -#define DEVELOPER_MODE_REGKEY L"Software\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock" +#define DEVELOPER_MODE_REGKEY L"Software\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock" #define DEVELOPER_MODE_REGVALUE L"AllowDevelopmentWithoutDevLicense" #define WIN32_LEAN_AND_MEAN diff --git a/checks/winfspcheck.cpp b/checks/winfspcheck.cpp index a420ddf..975f2b8 100644 --- a/checks/winfspcheck.cpp +++ b/checks/winfspcheck.cpp @@ -1,15 +1,14 @@ #include "winfspcheck.h" +#include #define WIN32_LEAN_AND_MEAN #include #include -#include namespace fs = std::filesystem; -bool alreadyLoaded = false; - WinFspCheckResult LoadWinFsp() { + static bool alreadyLoaded = false; if (alreadyLoaded) { return WinFspCheckResult::LOADED; } diff --git a/checks/wintoast_handler.h b/checks/wintoast_handler.h new file mode 100644 index 0000000..fdd22a2 --- /dev/null +++ b/checks/wintoast_handler.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +using namespace WinToastLib; + +typedef std::function ToastHandlerClicked; +typedef std::function ToastHandlerDismissed; + +class ToastHandler : public IWinToastHandler { +public: + ToastHandler(ToastHandlerClicked clicked, ToastHandlerDismissed dismissed) : + Clicked(clicked), Dismissed(dismissed) { } + + void toastActivated() const override { Clicked(-1); } + void toastActivated(int actionIndex) const override { Clicked(actionIndex); } + void toastDismissed(IWinToastHandler::WinToastDismissalReason state) const override { Dismissed(state); } + void toastFailed() const override { } + +private: + ToastHandlerClicked Clicked; + ToastHandlerDismissed Dismissed; +}; \ No newline at end of file diff --git a/cmd/cmdmain.cpp b/cmd/cmdmain.cpp deleted file mode 100644 index 5a42fa7..0000000 --- a/cmd/cmdmain.cpp +++ /dev/null @@ -1,229 +0,0 @@ -#include "../MountedBuild.h" -#include "../checks/winfspcheck.h" - -static inline const char* humanSize(uint64_t bytes) -{ - char* suffix[] = { "B", "KB", "MB", "GB", "TB" }; - char length = sizeof(suffix) / sizeof(suffix[0]); - - int i = 0; - double dblBytes = bytes; - - if (bytes > 1024) { - for (i = 0; (bytes / 1024) > 0 && i < length - 1; i++, bytes /= 1024) - dblBytes = bytes / 1024.0; - } - - static char output[16]; - sprintf(output, "%.04lf %s", dblBytes, suffix[i]); - return output; -} - -#define argtos(v) if (arge > ++argp) v = *argp; else goto usage -#define argtol(v) if (arge > ++argp) v = wcstol_deflt(*argp, v); else goto usage - -static inline ULONG wcstol_deflt(wchar_t* w, ULONG deflt) -{ - wchar_t* endp; - ULONG ul = wcstol(w, &endp, 0); - return L'\0' != w[0] && L'\0' == *endp ? ul : deflt; -} - -int wmain(ULONG argc, PWSTR* argv) -{ - { - auto result = LoadWinFsp(); - if (result != WinFspCheckResult::LOADED) { - switch (result) - { - case WinFspCheckResult::NO_PATH: - printf("Could not get your Program Files (x86) folder. I honestly have no idea how you'd get this error.\n"); - break; - case WinFspCheckResult::NO_DLL: - printf("Could not find WinFsp's DLL in the driver's folder. Try reinstalling WinFsp.\n"); - break; - case WinFspCheckResult::CANNOT_LOAD: - printf("Could not load WinFsp's DLL in the driver's folder. Try reinstalling WinFsp.\n"); - break; - default: - printf("An unknown error occurred when trying to load WinFsp's DLL: %d\n", result); - break; - } - return 0; - } - } - - wchar_t** argp, ** arge; - - PWSTR GamePath = 0, CachePath = 0, MountPath = 0; - ULONG DownloadThreadCount = 0, CompressionMethod = 0, CompressionLevel = 0; - bool Verify = false, RemoveUnused = false; - for (argp = argv + 1, arge = argv + argc; arge > argp; argp++) - { - if (L'-' != argp[0][0]) - break; - switch (argp[0][1]) - { - case L'?': - goto usage; - case L'G': - argtos(GamePath); - break; - case L'P': - argtol(DownloadThreadCount); - break; - case L'C': - argtos(CachePath); - break; - case L'M': - argtos(MountPath); - break; - case L'R': - RemoveUnused = true; - break; - case L'V': - Verify = true; - break; - case 'S': - argtol(CompressionMethod); - break; - case 's': - argtol(CompressionLevel); - break; - default: - goto usage; - } - } - if (!CachePath || !MountPath) { - goto usage; - } - uint32_t StorageFlags = 0; - if (Verify) { - StorageFlags |= StorageVerifyHashes; - } - if (CompressionMethod) { - switch (CompressionMethod) - { - case 1: - StorageFlags |= StorageDecompressed; - break; - case 2: - StorageFlags |= StorageCompressed; - break; - case 3: - StorageFlags |= StorageCompressLZ4; - break; - case 4: - StorageFlags |= StorageCompressZlib; - break; - default: - wprintf(L"Unknown compression method %d\n", CompressionMethod); - goto usage; - break; - } - } - else { - StorageFlags |= StorageCompressLZ4; - } - if (CompressionLevel) { - switch (CompressionLevel) - { - case 1: - StorageFlags |= StorageCompressFastest; - break; - case 2: - StorageFlags |= StorageCompressFast; - break; - case 3: - StorageFlags |= StorageCompressNormal; - break; - case 4: - StorageFlags |= StorageCompressSlow; - break; - case 5: - StorageFlags |= StorageCompressSlowest; - break; - default: - wprintf(L"Unknown compression level %d\n", CompressionLevel); - goto usage; - break; - } - } - else { - StorageFlags |= StorageCompressSlowest; - } - - MANIFEST* manifest; - MANIFEST_AUTH* auth; - STORAGE* storage; - ManifestAuthGrab(&auth); - ManifestAuthGetManifest(auth, "", &manifest); - printf("Total download size: %s\n", humanSize(ManifestInstallSize(manifest))); - (void)getchar(); - printf("Making build\n"); - MountedBuild* Build = new MountedBuild(manifest, MountPath, CachePath, [](const char* error) {printf("%s\n", error); }); - - printf("Setting up cache dir\n"); - if (!Build->SetupCacheDirectory()) { - printf("failed to setup cache dir\n"); - } - - printf("starting storage\n"); - if (!Build->StartStorage(StorageFlags)) { - printf("failed to start storage\n"); - } - - if (RemoveUnused) { - cancel_flag flag; - Build->PurgeUnusedChunks([](uint32_t max) {}, []() {}, flag); - } - - if (DownloadThreadCount) { - printf("Predownloading\n"); - cancel_flag flag; - if (!Build->PreloadAllChunks([](uint32_t max) {}, []() {}, flag, DownloadThreadCount)) { - printf("failed to preload\n"); - } - } - - printf("Starting\n"); - if (!Build->Mount()) { - printf("failed to start\n"); - } - - if (GamePath) { - printf("Setting up game dir\n"); - cancel_flag flag; - if (!Build->SetupGameDirectory([](uint32_t max) {}, []() {}, flag, DownloadThreadCount ? DownloadThreadCount : 32, GamePath)) { - printf("failed to setup game dir\n"); - } - } - - printf("Started, press any key to close\n"); - (void)getchar(); - printf("Closing\n"); - delete Build; - printf("Closed\n"); - return STATUS_SUCCESS; - -usage: - static char usage[] = "" - "usage: EGL2.exe OPTIONS\n" - "\n" - "options:\n" - " -G GameMountDir [optional: make Fortnite launchable here]\n" - " -P ThreadCount [optional: predownload all chunks]\n" - " -C CacheDir [directory to place downloaded chunk files]\n" - " -M MountDir [drive (Y:) or directory to mount filesystem to]\n" - " -R [optional: before mounting, remove all chunks that aren't used in the manifest (for when updating)]\n" - " -V [optional: verify all read chunks to sha hashes and redownload if necessary]\n" - // 1: uncompressed, 2: as downloaded (zlib), 3: lz4, 4: zlib - " -S Compression [optional: Type of compression used (1-4)]\n" - // 1: fastest, 2: fast, 3: normal, 4: slow, 5: slowest - // only used when -S is 3 or 4 - " -s CompressionLvl [optional: Amount of compression used (1-5)]\n"; - - printf(usage); - - return STATUS_UNSUCCESSFUL; -} \ No newline at end of file diff --git a/containers/file_sha.h b/containers/file_sha.h index c241684..97001a2 100644 --- a/containers/file_sha.h +++ b/containers/file_sha.h @@ -1,13 +1,13 @@ #pragma once -#include - #include #include +#include + namespace fs = std::filesystem; -inline bool SHAFile(fs::path path, char OutHash[SHA_DIGEST_LENGTH]) { - constexpr int buffer_size = 1 << 13; +inline const bool SHAFile(fs::path path, char OutHash[SHA_DIGEST_LENGTH]) { + static constexpr int buffer_size = 1 << 14; // 16384 char buffer[buffer_size]; SHA_CTX ctx; diff --git a/containers/iterable_queue.h b/containers/iterable_queue.h deleted file mode 100644 index 33a93a8..0000000 --- a/containers/iterable_queue.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include - -template> -class iterable_queue : public std::queue -{ -public: - typedef typename Container::iterator iterator; - typedef typename Container::const_iterator const_iterator; - - iterator begin() { return this->c.begin(); } - iterator end() { return this->c.end(); } - const_iterator begin() const { return this->c.begin(); } - const_iterator end() const { return this->c.end(); } -}; \ No newline at end of file diff --git a/containers/semaphore.h b/containers/semaphore.h deleted file mode 100644 index 81f541f..0000000 --- a/containers/semaphore.h +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include -#include - -class semaphore -{ -private: - std::mutex mutex_; - std::condition_variable condition_; - unsigned long count_ = 0; // Initialized as locked. - -public: - semaphore(long initial_count) { - count_ = initial_count; - } - - void notify() { // a thread is released - std::lock_guard lock(mutex_); - ++count_; - condition_.notify_one(); - } - - void wait() { // wait for a thread to be released - std::unique_lock lock(mutex_); - while (!count_) // Handle spurious wake-ups. - condition_.wait(lock); - --count_; - } -}; \ No newline at end of file diff --git a/filesystem/dirtree.h b/filesystem/dirtree.h index 40715c4..8c52713 100644 --- a/filesystem/dirtree.h +++ b/filesystem/dirtree.h @@ -1,11 +1,12 @@ #pragma once +#include "egfs.h" + #include #include +#include #include #include -#include -#include "egfs.h" template class DirTree { diff --git a/filesystem/egfs.cpp b/filesystem/egfs.cpp index 8773e1c..2d6032f 100644 --- a/filesystem/egfs.cpp +++ b/filesystem/egfs.cpp @@ -13,9 +13,9 @@ EGFS::EGFS(EGFS_PARAMS* Params, NTSTATUS& ErrorCode) : BOOLEAN Inserted; OnRead = Params->OnRead; - Security = new char[Params->SecuritySize]; + Security = std::make_unique(Params->SecuritySize); SecuritySize = Params->SecuritySize; - memcpy(Security, Params->Security, Params->SecuritySize); + memcpy(Security.get(), Params->Security, Params->SecuritySize); FSP_FSCTL_VOLUME_PARAMS VolumeParams; memset(&VolumeParams, 0, sizeof VolumeParams); @@ -42,6 +42,8 @@ EGFS::EGFS(EGFS_PARAMS* Params, NTSTATUS& ErrorCode) : VolumeTotal = Params->VolumeTotal; VolumeFree = Params->VolumeFree; + FspDebugLogSetHandle(GetStdHandle(STD_OUTPUT_HANDLE)); + Result = FspFileSystemCreate(L"" FSP_FSCTL_DISK_DEVICE_NAME, &VolumeParams, &FspInterface, &FileSystem); if (!NT_SUCCESS(Result)) { @@ -59,7 +61,6 @@ EGFS::EGFS(EGFS_PARAMS* Params, NTSTATUS& ErrorCode) : EGFS::~EGFS() { if (FileSystem && Started()) { FspFileSystemDelete(FileSystem); - delete[] Security; } } @@ -70,7 +71,7 @@ bool EGFS::SetMountPoint(PCWSTR MountPoint, PVOID Security) { return NT_SUCCESS(FspFileSystemSetMountPointEx(FileSystem, (PWSTR)MountPoint, Security)); } -void EGFS::AddFile(fs::path& Path, PVOID Context, UINT64 FileSize) +void EGFS::AddFile(fs::path&& Path, PVOID Context, UINT64 FileSize) { Files.AddFile(Path.generic_wstring().c_str(), EGFS_FILE(FileSize, Context)); } @@ -224,7 +225,7 @@ NTSTATUS EGFS::GetSecurityByName(FSP_FILE_SYSTEM* FileSystem, PWSTR FileName, PU *PSecurityDescriptorSize = Egfs->SecuritySize; if (SecurityDescriptor) - memcpy(SecurityDescriptor, Egfs->Security, Egfs->SecuritySize); + memcpy(SecurityDescriptor, Egfs->Security.get(), Egfs->SecuritySize); } return STATUS_SUCCESS; @@ -351,7 +352,7 @@ NTSTATUS EGFS::GetSecurity(FSP_FILE_SYSTEM* FileSystem, PVOID FileContext, PSECU *PSecurityDescriptorSize = Egfs->SecuritySize; if (SecurityDescriptor) - memcpy(SecurityDescriptor, Egfs->Security, Egfs->SecuritySize); + memcpy(SecurityDescriptor, Egfs->Security.get(), Egfs->SecuritySize); return STATUS_SUCCESS; } diff --git a/filesystem/egfs.h b/filesystem/egfs.h index 3c59485..73e6a8e 100644 --- a/filesystem/egfs.h +++ b/filesystem/egfs.h @@ -2,10 +2,11 @@ #include "dirtree.h" +#include +#include +#include #include -#include -#include namespace fs = std::filesystem; typedef std::function EGFS_READ_CALLBACK; @@ -66,7 +67,7 @@ class EGFS { ~EGFS(); bool SetMountPoint(PCWSTR MountDir, PVOID Security); - void AddFile(fs::path& Path, PVOID Context, UINT64 FileSize); + void AddFile(fs::path&& Path, PVOID Context, UINT64 FileSize); bool Start(); bool Stop(); @@ -78,7 +79,7 @@ class EGFS { FSP_FILE_SYSTEM* FileSystem; EGFS_READ_CALLBACK OnRead; - PVOID Security; + std::unique_ptr Security; SIZE_T SecuritySize; WCHAR VolumeLabel[32]; diff --git a/gui/UpdateChecker.cpp b/gui/UpdateChecker.cpp new file mode 100644 index 0000000..77596c6 --- /dev/null +++ b/gui/UpdateChecker.cpp @@ -0,0 +1,73 @@ +#include "UpdateChecker.h" + +#ifndef LOG_SECTION +#define LOG_SECTION "UpdateChecker" +#endif + +#include "../Logger.h" + +UpdateChecker::UpdateChecker(fs::path cachePath, UpdateCallback callback, std::chrono::milliseconds checkInterval) : + Auth(cachePath), + Callback(callback), + CheckInterval(checkInterval) +{ + LOG_DEBUG("Initializing force update"); + ForceUpdate(); + LOG_DEBUG("Creating update thread"); + UpdateThread = std::thread(&UpdateChecker::Thread, this); + UpdateThread.detach(); // causes some exception when the deconstructer is trying to join it otherwise +} + +UpdateChecker::~UpdateChecker() { + UpdateFlag.cancel(); +} + +void UpdateChecker::SetInterval(std::chrono::milliseconds newInterval) +{ + CheckInterval.store(newInterval); + UpdateWakeup = std::chrono::steady_clock::time_point::min(); // force update +} + +bool UpdateChecker::ForceUpdate() { + auto info = Auth.GetLatestManifest(); + auto id = Auth.GetManifestId(info.first); + if (LatestId == id) { + return false; + } + LatestUrl = info.first; + LatestId = id; + LatestVersion = info.second; + return true; +} + +std::string& UpdateChecker::GetLatestId() +{ + return LatestId; +} + +std::string& UpdateChecker::GetLatestUrl() +{ + return LatestUrl; +} + +std::string& UpdateChecker::GetLatestVersion() +{ + return LatestVersion; +} + +Manifest UpdateChecker::GetManifest(const std::string& Url) +{ + return Auth.GetManifest(Url); +} + +void UpdateChecker::Thread() { + while (!UpdateFlag.cancelled()) { + do { + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } while (std::chrono::steady_clock::now() < UpdateWakeup); + if (ForceUpdate()) { + Callback(LatestUrl, LatestVersion); + } + UpdateWakeup = std::chrono::steady_clock::now() + CheckInterval.load(); + } +} \ No newline at end of file diff --git a/gui/cApp.cpp b/gui/cApp.cpp index 8946a07..23e94bd 100644 --- a/gui/cApp.cpp +++ b/gui/cApp.cpp @@ -1,22 +1,91 @@ #include "cApp.h" +#ifndef LOG_SECTION +#define LOG_SECTION "cApp" +#endif + +#include "../checks/symlink_workaround.h" #include "../checks/winfspcheck.h" +#include "../Logger.h" #include +#include +#include + +using namespace WinToastLib; #define MESSAGE_ERROR(format, ...) wxMessageBox(wxString::Format(format, __VA_ARGS__), "Error - EGL2", wxICON_ERROR | wxOK | wxCENTRE) cApp::cApp() { + const char* SetupError = nullptr; + fs::path LogPath; + { + PWSTR appDataFolder; + if (SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, NULL, &appDataFolder) != S_OK) { + SetupError = "Could not get the location of your AppData folder."; + goto setupExit; + } + DataFolder = appDataFolder; + CoTaskMemFree(appDataFolder); + } + DataFolder /= "EGL2"; + if (!fs::create_directories(DataFolder) && !fs::is_directory(DataFolder)) { + SetupError = "Could not create EGL2 data folder"; + goto setupExit; + } + + if (!fs::create_directories(DataFolder / "logs") && !fs::is_directory(DataFolder / "logs")) { + SetupError = "Could not create EGL2 logs folder"; + goto setupExit; + } + + if (!fs::create_directories(DataFolder / "manifests") && !fs::is_directory(DataFolder / "manifests")) { + SetupError = "Could not create EGL2 manifests folder"; + goto setupExit; + } + + { + auto logTime = std::time(nullptr); + std::stringstream ss; + ss << std::put_time(std::localtime(&logTime), "%F_%T.log"); // ISO 8601 without timezone information. + auto s = ss.str(); + std::replace(s.begin(), s.end(), ':', '-'); + LogPath = DataFolder / "logs" / s; + } + + Logger::Setup(); + Logger::Callback = [this](Logger::LogLevel level, const char* section, const char* str) { + printf("%s%s - %s: %s\n%s", Logger::LevelAsColor(level), Logger::LevelAsString(level), section, str, Logger::ResetColor); + if (LogFile) { + fprintf(LogFile, "%s - %s: %s\n", Logger::LevelAsString(level), section, str); + fflush(LogFile); + } + }; + LogFile = fopen(LogPath.string().c_str(), "w"); + if (!LogFile) { + MESSAGE_ERROR("Could not create a log file! Without it, I can't assist you with any issues."); + LOG_ERROR("Could not create log file!"); + } + + LOG_INFO("Starting cApp"); + return; + +setupExit: + MESSAGE_ERROR(SetupError); + exit(0); } cApp::~cApp() { - + LOG_INFO("Deconst cApp"); + fclose(LogFile); } bool cApp::OnInit() { + LOG_INFO("Loading WinFsp"); auto result = LoadWinFsp(); if (result != WinFspCheckResult::LOADED) { + LOG_FATAL("WinFsp failed with %d", result); switch (result) { case WinFspCheckResult::NO_PATH: @@ -34,24 +103,46 @@ bool cApp::OnInit() { } return false; } + LOG_DEBUG("Loaded WinFsp"); - fs::path DataFolder; - { - PWSTR appDataFolder; - if (SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, NULL, &appDataFolder) != S_OK) { - MESSAGE_ERROR("Could not get the location of your AppData folder.", result); - return false; - } - DataFolder = appDataFolder; - CoTaskMemFree(appDataFolder); + LOG_INFO("Setting up WinToast"); + if (!WinToast::isCompatible()) { + LOG_FATAL("WinToast isn't compatible"); + return false; } - DataFolder /= "EGL2"; - if (!fs::create_directories(DataFolder) && !fs::is_directory(DataFolder)) { - MESSAGE_ERROR("Could not create EGL2 folder.", result); + LOG_DEBUG("Configuring WinToast"); + WinToast::instance()->setAppName(L"EGL2"); + WinToast::instance()->setAppUserModelId(WinToast::configureAUMI(L"workingrobot", L"egl2")); + if (!WinToast::instance()->initialize()) { + LOG_FATAL("Could not intialize WinToast"); return false; } + LOG_DEBUG("Set up WinToast"); + + LOG_INFO("Setting up symlink workaround"); + if (!IsDeveloperModeEnabled()) { + if (!IsUserAdmin()) { + auto cmd = GetCommandLine(); + int l = wcslen(argv[0].wc_str()); + if (cmd == wcsstr(cmd, argv[0].wc_str())) + { + cmd = cmd + l; + while (*cmd && isspace(*cmd)) + ++cmd; + } + + ShellExecute(NULL, L"runas", argv[0].wc_str(), cmd, NULL, SW_SHOWDEFAULT); + return false; + } + else { + EnableDeveloperMode(); + LOG_DEBUG("Set up developer mode"); + } + } + + LOG_INFO("Setting up cMain"); + (new cMain(DataFolder / "config", DataFolder / "manifests"))->Show(); - m_frame1 = new cMain(DataFolder / "config", DataFolder); - m_frame1->Show(); + LOG_DEBUG("Set up cApp"); return true; } \ No newline at end of file diff --git a/gui/cApp.h b/gui/cApp.h index 81f1afc..2c78c8d 100644 --- a/gui/cApp.h +++ b/gui/cApp.h @@ -10,10 +10,9 @@ class cApp : public wxApp cApp(); ~cApp(); -private: - cMain* m_frame1 = nullptr; - -public: virtual bool OnInit(); + + fs::path DataFolder; + FILE* LogFile; }; diff --git a/gui/cAuth.cpp b/gui/cAuth.cpp index 7669fd2..d9aa74a 100644 --- a/gui/cAuth.cpp +++ b/gui/cAuth.cpp @@ -35,6 +35,8 @@ cAuth::cAuth(cMain* main) : wxModalWindow(main, wxID_ANY, "Launch Game - EGL2", codeSecInfo = new wxStaticText(panel, wxID_ANY, EXCHANGE_SEC); codeSecBox->Add(codeSecInfo, 1, wxEXPAND); + codeInput->SetHint("d9c230c0e0354a619249ba1156df5e63"); + auto sizerBtns = new wxBoxSizer(wxHORIZONTAL); sizerBtns->Add(codeLink, 1, wxEXPAND | wxRIGHT, 3); diff --git a/gui/cAuth.h b/gui/cAuth.h index fa33688..fbeb887 100644 --- a/gui/cAuth.h +++ b/gui/cAuth.h @@ -3,9 +3,8 @@ #include "cMain.h" #include "wxModalWindow.h" -#include #include - +#include class cAuth : public wxModalWindow { diff --git a/gui/cMain.cpp b/gui/cMain.cpp index b6eb239..c62f1cb 100644 --- a/gui/cMain.cpp +++ b/gui/cMain.cpp @@ -1,142 +1,223 @@ #include "cMain.h" -#define DESC_TEXT_DEFAULT "Hover over a button to see what it does" -#define DESC_TEXT_SETUP "Click this before running for the first time. Has options for setting up your installation." -#define DESC_TEXT_VERIFY "If you believe some chunks are invalid, click this to verify all chunks that are already downloaded. Redownloads any invalid chunks." -#define DESC_TEXT_PURGE "Deletes any chunks that aren't used anymore. Useful for slimming down your install after you've updated your game." -#define DESC_TEXT_PRELOAD "Downloads any chunks that you don't already have downloaded for the most recent version, a.k.a updating. It's reccomended to update your install before playing in case you have a hotfix available." -#define DESC_TEXT_START "Mount your install to a drive letter, and, if selected, move the files necessary to a folder in order to start your game." -#define DESC_TEXT_PLAY "When clicked, it will prompt you to provide your exchange code. After giving it, the game will launch and you'll be good to go." \ - "\n\nThis option is only available when playing is enabled." - -#define STATUS_NEED_SETUP "Setup where you want your game to be installed." -#define STATUS_NORMAL "Click start to mount." -#define STATUS_PLAYABLE "Started! Press \"Play\" to start playing!" -#define STATUS_UNPLAYABLE "Started! If you want to play, enable it in your setup!" - -#define LAUNCH_GAME_ARGS "-AUTH_LOGIN=unused AUTH_TYPE=exchangecode -epicapp=Fortnite -epicenv=Prod -epicportal -epiclocale=en-us -AUTH_PASSWORD=%s %s" +#define DESC_TEXT_DEFAULT "Hover over a button to see what it does" +#define DESC_TEXT_SETTINGS "Configure your install" +#define DESC_TEXT_VERIFY "Verifies your game (in case you have corrupt data)" +#define DESC_TEXT_UPDATE "Ensures you have the latest version" +#define DESC_TEXT_PLAY "When clicked, it will prompt you to provide your exchange code. After giving it, the game will launch and you'll be good to go." + +#define STATUS_STARTING "Starting up..." +#define STATUS_PLAYABLE "Started! Press \"Play\" to start playing!" + +#define CMAIN_W 450 +#define CMAIN_H 330 + +#define LAUNCH_GAME_ARGS "-AUTH_LOGIN=unused AUTH_TYPE=exchangecode -epicapp=Fortnite -epicenv=Prod -epicportal -epiclocale=en-us -AUTH_PASSWORD=%s %s" + +#define MOUNT_FOLDER "game" + +#ifndef LOG_SECTION +#define LOG_SECTION "cMain" +#endif -#include "cSetup.h" -#include "cProgress.h" -#include "cAuth.h" #include "../checks/symlink_workaround.h" +#include "../checks/wintoast_handler.h" +#include "../Logger.h" +#include "../Stats.h" +#include "cAuth.h" +#include "cProgress.h" +#include "cSetup.h" +#include +#include +#include #include #include -#include -#include - -#define BIND_BUTTON_DESC(btn, desc) \ - btn->Bind(wxEVT_MOTION, std::bind(&cMain::OnButtonHover, this, desc)); \ - btn->Bind(wxEVT_LEAVE_WINDOW, std::bind(&cMain::OnButtonHover, this, DESC_TEXT_DEFAULT)); - -cMain::cMain(fs::path settingsPath, fs::path manifestPath) : wxFrame(nullptr, wxID_ANY, "EGL2", wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_STYLE ^ (wxMAXIMIZE_BOX | wxRESIZE_BORDER)) { +#define SIDE_BUTTON_CREATE(name, text) \ + auto btn_frame_##name = new wxPanel(panel, wxID_ANY); \ + auto btn_sizer_##name = new wxBoxSizer(wxVERTICAL); \ + auto btn_##name = new wxButton(btn_frame_##name, wxID_ANY, text, wxDefaultPosition, wxSize(120, -1)); \ + btn_sizer_##name->Add(btn_##name, 1, wxEXPAND); \ + btn_frame_##name->SetSizerAndFit(btn_sizer_##name); +#define SIDE_BUTTON_BIND(name, func) \ + btn_##name->Bind(wxEVT_BUTTON, func); +#define SIDE_BUTTON_FRAME(name) btn_frame_##name +#define SIDE_BUTTON_OBJ(name) btn_##name +#define SIDE_BUTTON_DESC(name, desc) \ + btn_frame_##name->Bind(wxEVT_MOTION, std::bind(&cMain::OnButtonHover, this, desc)); \ + btn_##name->Bind(wxEVT_MOTION, std::bind(&cMain::OnButtonHover, this, desc)); \ + btn_frame_##name->Bind(wxEVT_LEAVE_WINDOW, std::bind(&cMain::OnButtonHover, this, DESC_TEXT_DEFAULT)); \ + btn_##name->Bind(wxEVT_LEAVE_WINDOW, std::bind(&cMain::OnButtonHover, this, DESC_TEXT_DEFAULT)); + +#define CREATE_STAT(name, displayName, range) \ + stat##name##Label = new wxStaticText(panel, wxID_ANY, displayName, wxDefaultPosition, wxDefaultSize, wxALIGN_RIGHT); \ + stat##name##Value = new wxGauge(panel, wxID_ANY, range, wxDefaultPosition, wxSize(105, -1)); \ + stat##name##Text = new wxStaticText(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(45, -1)); \ + statsSizer->Add(stat##name##Label, wxGBPosition(statColInd / 2, (statColInd & 0x1) * 3), wxGBSpan(1, 1), wxEXPAND); \ + statsSizer->Add(stat##name##Value, wxGBPosition(statColInd / 2, (statColInd & 0x1) * 3 + 1), wxGBSpan(1, 1), wxEXPAND); \ + statsSizer->Add(stat##name##Text, wxGBPosition(statColInd / 2, (statColInd & 0x1) * 3 + 2), wxGBSpan(1, 1), wxEXPAND); \ + statColInd++; +#define STAT_VALUE(name) stat##name##Value +#define STAT_TEXT(name) stat##name##Text + +cMain::cMain(fs::path settingsPath, fs::path manifestCachePath) : wxFrame(nullptr, wxID_ANY, "EGL2", wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_STYLE ^ (wxMAXIMIZE_BOX | wxRESIZE_BORDER)), + SettingsPath(settingsPath), + Settings(SettingsDefault()), + UpdateAvailable(false) { + LOG_DEBUG("Setting up (%s, %s)", settingsPath.string().c_str(), manifestCachePath.string().c_str()); this->SetIcon(wxICON(APP_ICON)); - this->SetMinSize(wxSize(450, 250)); - this->SetMaxSize(wxSize(450, 250)); + this->SetMinSize(wxSize(CMAIN_W, CMAIN_H)); + this->SetMaxSize(wxSize(CMAIN_W, CMAIN_H)); + + LOG_DEBUG("Setting up UI"); panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - auto grid = new wxGridBagSizer(); + SIDE_BUTTON_CREATE(settings, "Settings"); + SIDE_BUTTON_CREATE(verify, "Verify"); + SIDE_BUTTON_CREATE(play, "Play"); + + SIDE_BUTTON_BIND(settings, std::bind(&cMain::OnSettingsClicked, this, false)); + SIDE_BUTTON_BIND(verify, std::bind(&cMain::OnVerifyClicked, this)); + SIDE_BUTTON_BIND(play, std::bind(&cMain::OnPlayClicked, this)); + this->playBtn = SIDE_BUTTON_OBJ(play); + + SIDE_BUTTON_OBJ(verify)->Disable(); + SIDE_BUTTON_OBJ(play)->Disable(); + + SIDE_BUTTON_DESC(settings, DESC_TEXT_SETTINGS); + SIDE_BUTTON_DESC(verify, DESC_TEXT_VERIFY); + SIDE_BUTTON_DESC(play, DESC_TEXT_PLAY); - setupBtn = new wxButton(panel, wxID_ANY, "Setup"); - verifyBtn = new wxButton(panel, wxID_ANY, "Verify"); - purgeBtn = new wxButton(panel, wxID_ANY, "Purge"); - preloadBtn = new wxButton(panel, wxID_ANY, "Update"); - startBtn = new wxButton(panel, wxID_ANY, "Start"); - startFnBtn = new wxButton(panel, wxID_ANY, "Play"); statusBar = new wxStaticText(panel, wxID_ANY, wxEmptyString); selloutBar = new wxStaticText(panel, wxID_ANY, "Use code \"furry\"! (#ad)"); - setupBtn->Bind(wxEVT_BUTTON, std::bind(&cMain::OnSetupClicked, this)); - verifyBtn->Bind(wxEVT_BUTTON, std::bind(&cMain::OnVerifyClicked, this)); - purgeBtn->Bind(wxEVT_BUTTON, std::bind(&cMain::OnPurgeClicked, this)); - preloadBtn->Bind(wxEVT_BUTTON, std::bind(&cMain::OnPreloadClicked, this)); - startBtn->Bind(wxEVT_BUTTON, std::bind(&cMain::OnStartClicked, this)); - startFnBtn->Bind(wxEVT_BUTTON, std::bind(&cMain::OnPlayClicked, this)); - startFnBtn->Disable(); - - BIND_BUTTON_DESC(setupBtn, DESC_TEXT_SETUP); - BIND_BUTTON_DESC(verifyBtn, DESC_TEXT_VERIFY); - BIND_BUTTON_DESC(purgeBtn, DESC_TEXT_PURGE); - BIND_BUTTON_DESC(preloadBtn, DESC_TEXT_PRELOAD); - BIND_BUTTON_DESC(startBtn, DESC_TEXT_START); - BIND_BUTTON_DESC(startFnBtn, DESC_TEXT_PLAY); - descBox = new wxStaticBoxSizer(wxVERTICAL, panel, "Description"); descTxt = new wxStaticText(panel, wxID_ANY, DESC_TEXT_DEFAULT); descBox->Add(descTxt, 1, wxEXPAND); + statsBox = new wxStaticBoxSizer(wxVERTICAL, panel, "Stats"); + auto statsSizer = new wxGridBagSizer(2, 4); + int statColInd = 0; + CREATE_STAT(cpu, "CPU", 1000); // divide by 10 to get % + CREATE_STAT(ram, "RAM", 384 * 1024 * 1024); // 384 mb + CREATE_STAT(read, "Read", 256 * 1024 * 1024); // 256 mb/s + CREATE_STAT(write, "Write", 64 * 1024 * 1024); // 64 mb/s + CREATE_STAT(provide, "Provide", 256 * 1024 * 1024); // 256 mb/s + CREATE_STAT(download, "Download", 64 * 1024 * 1024); // 512 mbps + CREATE_STAT(latency, "Latency", 1000); // divide by 10 to get ms + CREATE_STAT(threads, "Threads", 128); // 128 threads (threads probably don't ruin performance, probably just shows overhead) + statsBox->Add(statsSizer, 1, wxEXPAND); + auto barSizer = new wxBoxSizer(wxHORIZONTAL); barSizer->Add(statusBar); barSizer->AddStretchSpacer(); barSizer->Add(selloutBar); - grid->Add(setupBtn, wxGBPosition(0, 0), wxGBSpan(1, 2), wxEXPAND); - grid->Add(verifyBtn, wxGBPosition(1, 0), wxGBSpan(1, 2), wxEXPAND); - grid->Add(purgeBtn, wxGBPosition(2, 0), wxGBSpan(1, 2), wxEXPAND); - grid->Add(preloadBtn, wxGBPosition(3, 0), wxGBSpan(1, 2), wxEXPAND); - grid->Add(startBtn, wxGBPosition(4, 0), wxGBSpan(1, 1), wxEXPAND); - grid->Add(startFnBtn, wxGBPosition(4, 1), wxGBSpan(1, 1), wxEXPAND); - grid->Add(descBox, wxGBPosition(0, 2), wxGBSpan(5, 1), wxEXPAND); - grid->Add(barSizer, wxGBPosition(5, 0), wxGBSpan(1, 3), wxEXPAND); + auto buttonSizer = new wxBoxSizer(wxVERTICAL); + buttonSizer->Add(SIDE_BUTTON_FRAME(settings), 1, wxEXPAND); + buttonSizer->Add(SIDE_BUTTON_FRAME(verify), 1, wxEXPAND); + buttonSizer->Add(SIDE_BUTTON_FRAME(play), 1, wxEXPAND); + + auto grid = new wxGridBagSizer(2, 2); + grid->Add(buttonSizer, wxGBPosition(0, 0), wxGBSpan(1, 1), wxEXPAND); + grid->Add(descBox, wxGBPosition(0, 1), wxGBSpan(1, 1), wxEXPAND); + grid->Add(statsBox, wxGBPosition(1, 0), wxGBSpan(1, 2), wxEXPAND); + grid->Add(barSizer, wxGBPosition(2, 0), wxGBSpan(1, 2), wxEXPAND); grid->AddGrowableCol(0, 1); grid->AddGrowableCol(1, 1); - grid->AddGrowableCol(2, 4); grid->AddGrowableRow(0); - grid->AddGrowableRow(1); - grid->AddGrowableRow(2); - grid->AddGrowableRow(3); - grid->AddGrowableRow(4); auto topSizer = new wxBoxSizer(wxVERTICAL); topSizer->Add(grid, wxSizerFlags(1).Expand().Border(wxALL, 5)); panel->SetSizerAndFit(topSizer); - this->SetSize(wxSize(450, 250)); + this->SetSize(wxSize(CMAIN_W, CMAIN_H)); - Bind(wxEVT_CLOSE_WINDOW, &cMain::OnClose, this); - - SettingsPath = settingsPath; - memset(&Settings, 0, sizeof(Settings)); + LOG_DEBUG("Getting settings"); auto settingsFp = fopen(SettingsPath.string().c_str(), "rb"); if (settingsFp) { + LOG_DEBUG("Reading settings file"); SettingsRead(&Settings, settingsFp); fclose(settingsFp); } else { - Settings.CompressionLevel = 4; // Slowest - Settings.CompressionMethod = 1; // Decompress - Settings.EnableGaming = true; - Settings.VerifyCache = true; + LOG_DEBUG("Using default settings"); } - { - auto valid = SettingsValidate(&Settings); - verifyBtn->Enable(valid); - purgeBtn->Enable(valid); - preloadBtn->Enable(valid); - startBtn->Enable(valid); - SetStatus(valid ? STATUS_NORMAL : STATUS_NEED_SETUP); + this->Show(); + LOG_DEBUG("Validating settings"); + if (!SettingsValidate(&Settings)) { + LOG_WARN("Invalid settings"); + OnSettingsClicked(true); + if (!SettingsValidate(&Settings)) { + LOG_FATAL("Cancelled out of setup, closing"); + this->Destroy(); + return; + } } - if (!ManifestAuthGrab(&ManifestAuth)) { - wxMessageBox("Could not setup an internet connection. Maybe you have a VPN or proxy connected?", "Error - EGL2", wxICON_ERROR | wxOK | wxCENTRE); - Destroy(); - } - ManifestCachePath = manifestPath; - ManifestAuthGetManifest(ManifestAuth, ManifestCachePath, &Manifest); + Stats::StartUpdateThread(ch::milliseconds(500), [this](StatsUpdateData& data) { + STAT_VALUE(cpu)->SetValue(1000); + STAT_VALUE(cpu)->SetValue(std::min(data.cpu * 10, 1000.f)); + STAT_TEXT(cpu)->SetLabel(data.cpu > 0 ? wxString::Format("%.*f%%", std::max(2 - (int)floor(log10(data.cpu)), 1), data.cpu) : "0.00%"); + + STAT_VALUE(ram)->SetValue(384 * 1024 * 1024); + STAT_VALUE(ram)->SetValue(std::min(data.ram, (size_t)384 * 1024 * 1024)); + STAT_TEXT(ram)->SetLabel(Stats::GetReadableSize(data.ram)); + + STAT_VALUE(read)->SetValue(256 * 1024 * 1024); + STAT_VALUE(read)->SetValue(std::min(data.read, (size_t)256 * 1024 * 1024)); + STAT_TEXT(read)->SetLabel(Stats::GetReadableSize(data.read)); + + STAT_VALUE(write)->SetValue(64 * 1024 * 1024); + STAT_VALUE(write)->SetValue(std::min(data.write, (size_t)64 * 1024 * 1024)); + STAT_TEXT(write)->SetLabel(Stats::GetReadableSize(data.write)); + + STAT_VALUE(provide)->SetValue(256 * 1024 * 1024); + STAT_VALUE(provide)->SetValue(std::min(data.provide, (size_t)256 * 1024 * 1024)); + STAT_TEXT(provide)->SetLabel(Stats::GetReadableSize(data.provide)); + + STAT_VALUE(download)->SetValue(64 * 1024 * 1024); + STAT_VALUE(download)->SetValue(std::min(data.download, (size_t)64 * 1024 * 1024)); + STAT_TEXT(download)->SetLabel(Stats::GetReadableSize(data.download)); + + STAT_VALUE(latency)->SetValue(1000); + STAT_VALUE(latency)->SetValue(std::min(data.latency * 10, 1000.f)); + STAT_TEXT(latency)->SetLabel(data.latency > 0 ? wxString::Format("%.*f ms", std::max(2 - (int)floor(log10(data.latency)), 1), data.latency) : "0 ms"); + + STAT_VALUE(threads)->SetValue(128); + STAT_VALUE(threads)->SetValue(std::min(data.threads, 128)); + STAT_TEXT(threads)->SetLabel(wxString::Format("%d", data.threads)); + }); + + Bind(wxEVT_CLOSE_WINDOW, &cMain::OnClose, this); + + std::thread([=]() { + SetStatus(STATUS_STARTING); + LOG_DEBUG("Creating update checker"); + Checker = std::make_unique(manifestCachePath, [this](const std::string& Url, const std::string& Version) { OnUpdate(Version, Url); }, SettingsGetUpdateInterval(&Settings)); + Mount(Checker->GetLatestUrl()); + LOG_DEBUG("Enabling buttons"); + SetStatus(STATUS_PLAYABLE); + SIDE_BUTTON_OBJ(verify)->Enable(); + SIDE_BUTTON_OBJ(play)->Enable(); + LOG_DEBUG("Checking chunk count"); + auto ct = Build->GetMissingChunkCount(); + LOG_DEBUG("%d missing chunks", ct); + if (ct) { + OnUpdate(Checker->GetLatestVersion()); + } + }).detach(); } cMain::~cMain() { - auto settingsFp = fopen(SettingsPath.string().c_str(), "wb"); - SettingsWrite(&Settings, settingsFp); - fclose(settingsFp); + } void cMain::OnButtonHover(const char* string) { @@ -148,17 +229,19 @@ void cMain::OnButtonHover(const char* string) { } } -void cMain::OnSetupClicked() { - cSetup(this, &Settings).ShowModal(); - auto valid = SettingsValidate(&Settings); - verifyBtn->Enable(valid); - purgeBtn->Enable(valid); - preloadBtn->Enable(valid); - startBtn->Enable(valid); - SetStatus(valid ? STATUS_NORMAL : STATUS_NEED_SETUP); - if (valid) { - auto& build = GetMountedBuild(); - build->SetupCacheDirectory(); +void cMain::OnSettingsClicked(bool onStartup) { + cSetup(this, &Settings, onStartup, [this](SETTINGS* settings) { + auto settingsFp = fopen(SettingsPath.string().c_str(), "wb"); + if (settingsFp) { + SettingsWrite(settings, settingsFp); + fclose(settingsFp); + } + else { + LOG_ERROR("Could not open settings file to write"); + } + }, &SettingsValidate).ShowModal(); + if (Checker) { + Checker->SetInterval(SettingsGetUpdateInterval(&Settings)); } } @@ -171,9 +254,7 @@ void cMain::OnSetupClicked() { \ wxWindowPtr progressPtr(progress); \ std::thread([=]() { \ - auto& b = GetMountedBuild(); \ - \ - b->##funcName( \ + Build->##funcName( \ [=](uint32_t m) { progressPtr->SetMaximum(m); }, \ [=]() { progressPtr->Increment(); }, \ *cancelled, __VA_ARGS__); \ @@ -185,69 +266,46 @@ void cMain::OnSetupClicked() { } void cMain::OnVerifyClicked() { - RUN_PROGRESS("Verifying", VerifyAllChunks, 64); -} - -void cMain::OnPurgeClicked() { - RUN_PROGRESS("Purging", PurgeUnusedChunks); + RUN_PROGRESS("Verifying", VerifyAllChunks, Settings.ThreadCount); } -void cMain::OnPreloadClicked() { - auto& build = GetMountedBuild(); - if (!build->Mounted()) { - build.reset(); - ManifestDelete(Manifest); - ManifestAuthGetManifest(ManifestAuth, ManifestCachePath, &Manifest); - } - - RUN_PROGRESS("Updating", PreloadAllChunks, 64); -} - -void cMain::OnStartClicked() { - auto& build = GetMountedBuild(); - if (build->Mounted()) { - Unmount(); +void cMain::OnPlayClicked() { + if (UpdateAvailable) { + BeginUpdate(); } else { - if (build->Mount()) { - setupBtn->Disable(); - startFnBtn->Enable(Settings.EnableGaming); - startBtn->SetLabel("Stop"); - SetStatus(Settings.EnableGaming ? STATUS_PLAYABLE : STATUS_UNPLAYABLE); - - if (Settings.EnableGaming) { - RUN_PROGRESS("Setting Up", SetupGameDirectory, 64, [=]() { - if (!IsDeveloperModeEnabled()) { - if (!IsUserAdmin()) { - wxMessageBox("In order to finish setting up Fortnite, please restart EGL2 in administator mode!", "Symlink Creation Error - EGL2", wxICON_ERROR | wxOK | wxCENTRE); - Unmount(); - return false; - } - else { - EnableDeveloperMode(); - return true; - } - } - return true; - }); - } + cAuth auth(this); + auth.ShowModal(); + if (!auth.GetCode().IsEmpty()) { + Build->LaunchGame(wxString::Format(LAUNCH_GAME_ARGS, auth.GetCode(), Settings.CommandArgs).c_str()); } } } -void cMain::OnPlayClicked() { - cAuth auth(this); - auth.ShowModal(); - if (!auth.GetCode().IsEmpty()) { - auto& build = GetMountedBuild(); - build->LaunchGame(wxString::Format(LAUNCH_GAME_ARGS, auth.GetCode(), Settings.CommandArgs).c_str()); - } -} - void cMain::OnClose(wxCloseEvent& evt) { - if (evt.CanVeto() && Build && Build->Mounted()) { - if (wxMessageBox("Your game is currently mounted. Do you want to exit now?", "Currently Mounted - EGL2", wxICON_QUESTION | wxYES_NO) != wxYES) + if (evt.CanVeto() && Build) { + bool gameRunning = false; + { + auto procId = GetCurrentProcessId(); + HANDLE hnd = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, procId); + if (hnd != INVALID_HANDLE_VALUE) { + PROCESSENTRY32 procEntry; + procEntry.dwSize = sizeof PROCESSENTRY32; + + if (Process32First(hnd, &procEntry)) { + do { + if (procEntry.th32ParentProcessID == procId) { + gameRunning = true; + break; + } + } while (Process32Next(hnd, &procEntry)); + } + + CloseHandle(hnd); + } + } + if (gameRunning && wxMessageBox("Fortnite is still running! Do you still want to exit?", "Currently Running - EGL2", wxICON_QUESTION | wxYES_NO) != wxYES) { evt.Veto(); return; @@ -260,30 +318,63 @@ void cMain::SetStatus(const char* string) { statusBar->SetLabel(string); } -#define DRIVE_ALPHABET "ABCDEFGHIJKLMNOPQRSTUVWXYZ" -inline fs::path GetMountDrive() { - auto usedDrives = GetLogicalDrives(); - for (int b = 0; b < strlen(DRIVE_ALPHABET); ++b) { - if (!((usedDrives >> b) & 1)) { - return std::string(1, DRIVE_ALPHABET[b]) + ':'; - } +void cMain::OnUpdate(const std::string& Version, const std::optional& Url) +{ + UpdateAvailable = true; + playBtn->SetLabel("Update"); + if (Url.has_value()) { + UpdateUrl = Url; + } + + WinToastTemplate templ = WinToastTemplate(WinToastTemplate::Text02); + templ.setTextField(L"New Fortnite Update!", WinToastTemplate::FirstLine); + templ.setTextField(wxString::Format("%s is now available!", UpdateChecker::GetReadableVersion(Version)), WinToastTemplate::SecondLine); + templ.addAction(L"Click to Update"); + + WinToast::WinToastError error; + if (!WinToast::instance()->showToast(templ, std::make_shared( + [=](int ind) { + this->BeginUpdate(); + }, + [](IWinToastHandler::WinToastDismissalReason s) { + + }), &error)) { + LOG_ERROR("Couldn't launch toast notification: %d", error); } - return "/"; // unsure what to do here honestly } -std::unique_ptr& cMain::GetMountedBuild() { - if (!Build) { - Build = std::make_unique(Manifest, GetMountDrive(), Settings.CacheDir, [](const char* error) {}); - Build->StartStorage(SettingsGetStorageFlags(&Settings)); +void cMain::BeginUpdate() +{ + if (!UpdateAvailable) { + return; } - return Build; + std::thread([this]() { + if (!UpdateUrl.has_value()) { + LOG_DEBUG("Downloading unavailable chunks"); + } + else { + LOG_DEBUG("Beginning update: %s", UpdateUrl->c_str()); + Mount(*UpdateUrl); + } + + this->CallAfter([this]() { + RUN_PROGRESS("Updating", PreloadAllChunks, Settings.BufferCount); + }); + + Build->PurgeUnusedChunks(); + LOG_DEBUG("Purged chunks"); + }).detach(); + + UpdateAvailable = false; + playBtn->SetLabel("Play"); + UpdateUrl.reset(); } -void cMain::Unmount() { - auto& build = GetMountedBuild(); - build->Unmount(); - setupBtn->Enable(); - startFnBtn->Disable(); - startBtn->SetLabel("Start"); - SetStatus(STATUS_NORMAL); +void cMain::Mount(const std::string& Url) { + LOG_INFO("Setting up cache directory"); + MountedBuild::SetupCacheDirectory(Settings.CacheDir); + LOG_INFO("Mounting new url: %s", Url.c_str()); + Build.reset(new MountedBuild(Checker->GetManifest(Url), fs::path(Settings.CacheDir) / MOUNT_FOLDER, Settings.CacheDir, SettingsGetStorageFlags(&Settings), Settings.BufferCount)); + LOG_INFO("Setting up game dir"); + Build->SetupGameDirectory(Settings.ThreadCount); } \ No newline at end of file diff --git a/gui/cMain.h b/gui/cMain.h index b6ac323..525ba1e 100644 --- a/gui/cMain.h +++ b/gui/cMain.h @@ -1,10 +1,15 @@ #pragma once +#define NOMINMAX #include "../MountedBuild.h" +#include "../web/manifest/auth.h" +#include "UpdateChecker.h" #include "settings.h" #include +#include + class cMain : public wxFrame { public: @@ -14,43 +19,55 @@ class cMain : public wxFrame protected: wxPanel* panel = nullptr; - wxButton* setupBtn = nullptr; - wxButton* verifyBtn = nullptr; - wxButton* purgeBtn = nullptr; - wxButton* preloadBtn = nullptr; - wxButton* startBtn = nullptr; - wxButton* startFnBtn = nullptr; + wxButton* playBtn = nullptr; wxStaticBoxSizer* descBox = nullptr; wxStaticText* descTxt = nullptr; + wxStaticBoxSizer* statsBox = nullptr; + +#define DEFINE_STAT(name) \ + wxStaticText* stat##name##Label; \ + wxGauge* stat##name##Value; \ + wxStaticText* stat##name##Text; + + DEFINE_STAT(cpu) + DEFINE_STAT(ram) + DEFINE_STAT(read) + DEFINE_STAT(write) + DEFINE_STAT(provide) + DEFINE_STAT(download) + DEFINE_STAT(latency) + DEFINE_STAT(threads) + +#undef DEFINE_STAT + wxStaticText* statusBar = nullptr; wxStaticText* selloutBar = nullptr; void OnButtonHover(const char* string); - void OnSetupClicked(); + void OnSettingsClicked(bool onStartup); void OnVerifyClicked(); - void OnPurgeClicked(); - void OnPreloadClicked(); - void OnStartClicked(); void OnPlayClicked(); void OnClose(wxCloseEvent& evt); void SetStatus(const char* string); + void OnUpdate(const std::string& Version, const std::optional& Url = std::nullopt); + void BeginUpdate(); + private: - std::unique_ptr& GetMountedBuild(); - void Unmount(); + void Mount(const std::string& Url); + bool UpdateAvailable; + std::optional UpdateUrl; + fs::path SettingsPath; SETTINGS Settings; + std::unique_ptr Checker; std::unique_ptr Build; - - MANIFEST_AUTH* ManifestAuth; - fs::path ManifestCachePath; - MANIFEST* Manifest; }; diff --git a/gui/cProgress.cpp b/gui/cProgress.cpp index c1d381c..6a42973 100644 --- a/gui/cProgress.cpp +++ b/gui/cProgress.cpp @@ -1,8 +1,8 @@ #include "cProgress.h" +#include #include -#include namespace ch = std::chrono; cProgress::cProgress(cMain* main, wxString taskName, cancel_flag& cancelFlag, float updateFreq, uint32_t maximum) : wxFrame(main, wxID_ANY, taskName + " - EGL2", wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_STYLE ^ (wxMAXIMIZE_BOX | wxRESIZE_BORDER)) { @@ -23,10 +23,10 @@ cProgress::cProgress(cMain* main, wxString taskName, cancel_flag& cancelFlag, fl progressBar = new wxGauge(panel, wxID_ANY, maxValue, wxDefaultPosition, wxDefaultSize, wxGA_HORIZONTAL | wxGA_SMOOTH); - progressPercent = new wxStaticText(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxALIGN_CENTRE_HORIZONTAL | wxST_NO_AUTORESIZE); - progressTotal = new wxStaticText(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxALIGN_CENTRE_HORIZONTAL | wxST_NO_AUTORESIZE); - progressTimeElapsed = new wxStaticText(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxALIGN_CENTRE_HORIZONTAL | wxST_NO_AUTORESIZE); - progressTimeETA = new wxStaticText(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxALIGN_CENTRE_HORIZONTAL | wxST_NO_AUTORESIZE); + progressPercent = new wxStaticText(panel, wxID_ANY, "0.00%", wxDefaultPosition, wxDefaultSize, wxALIGN_CENTRE_HORIZONTAL | wxST_NO_AUTORESIZE); + progressTotal = new wxStaticText(panel, wxID_ANY, "0 / 0", wxDefaultPosition, wxDefaultSize, wxALIGN_CENTRE_HORIZONTAL | wxST_NO_AUTORESIZE); + progressTimeElapsed = new wxStaticText(panel, wxID_ANY, "Elapsed: 00:00:00", wxDefaultPosition, wxDefaultSize, wxALIGN_CENTRE_HORIZONTAL | wxST_NO_AUTORESIZE); + progressTimeETA = new wxStaticText(panel, wxID_ANY, "ETA: 00:00:00", wxDefaultPosition, wxDefaultSize, wxALIGN_CENTRE_HORIZONTAL | wxST_NO_AUTORESIZE); progressCancelBtn = new wxButton(panel, wxID_ANY, "Cancel"); progressTaskbar = new wxAppProgressIndicator(this, maxValue); @@ -84,22 +84,24 @@ inline void cProgress::Update(bool force) { return; } lastUpdate = now; - progressBar->SetValue(value + 1); - progressBar->SetValue(value); - progressTaskbar->SetValue(value); + auto val = value.load(); + SetMaximum((std::max)(val, maxValue)); + progressBar->SetValue(val + 1); + progressBar->SetValue(val); + progressTaskbar->SetValue(val); { - progressPercent->SetLabel(wxString::Format("%.2f%%", float(value) * 100 / maxValue)); - progressTotal->SetLabel(wxString::Format("%u / %u", value.load(), maxValue)); + progressPercent->SetLabel(wxString::Format("%.2f%%", float(val) * 100 / maxValue)); + progressTotal->SetLabel(wxString::Format("%u / %u", val, maxValue)); auto elapsed = ch::duration_cast(now - startTime); progressTimeElapsed->SetLabel("Elapsed: " + FormatTime(elapsed)); auto& timePoint = etaTimePoints.front(); - auto eta = GetETA(now - timePoint.second, value - timePoint.first, maxValue - value); + auto eta = GetETA(now - timePoint.second, val - timePoint.first, maxValue - val); progressTimeETA->SetLabel("ETA: " + FormatTime(eta)); - etaTimePoints.push(std::make_pair(value.load(), now)); + etaTimePoints.push(std::make_pair(val, now)); if (etaTimePoints.size() > queueSize) { etaTimePoints.pop(); } diff --git a/gui/cProgress.h b/gui/cProgress.h index 34bef7c..b3b273b 100644 --- a/gui/cProgress.h +++ b/gui/cProgress.h @@ -1,11 +1,10 @@ #pragma once -#include "cMain.h" #include "../containers/cancel_flag.h" - -#include +#include "cMain.h" #include +#include class cProgress : public wxFrame { diff --git a/gui/cSetup.cpp b/gui/cSetup.cpp index 162b4fe..02dec40 100644 --- a/gui/cSetup.cpp +++ b/gui/cSetup.cpp @@ -11,14 +11,16 @@ "of data will need to be allocated on your hard drive." #include "settings.h" +#include "wxLabelSlider.h" +#include #include +#include static const char* compMethods[] = { - "Keep Compressed as Downloaded (zLib)", - "Decompress", - "Use LZ4", - "Use zLib" + "Zstandard (Not tested)", + "LZ4 (Recommended)", + "Decompressed" }; static const char* compLevels[] = { @@ -29,89 +31,151 @@ static const char* compLevels[] = { "Slowest" }; -cSetup::cSetup(cMain* main, SETTINGS* settings) : wxModalWindow(main, wxID_ANY, "Setup - EGL2", wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_STYLE ^ (wxMAXIMIZE_BOX | wxRESIZE_BORDER)) { - Settings = settings; +static const char* updateLevels[] = { + "1 second", + "5 seconds", + "10 seconds", + "30 seconds", + "1 minute", + "5 minutes", + "10 minutes", + "30 minutes", + "1 hour", +}; +#define DEFINE_SECTION(name, displayName) \ + auto sectionBox##name = new wxStaticBoxSizer(wxVERTICAL, panel, displayName); \ + auto sectionGrid##name = new wxGridBagSizer(2, 2); \ + auto sectionColInd##name = 0; \ + sectionGrid##name->Add(5, 1, wxGBPosition(0, 1)); \ + sectionBox##name->Add(sectionGrid##name, wxSizerFlags().Expand().Border(wxALL, 5)); + +#define ADD_ITEM_BROWSE(section, name, displayName, binder) \ + auto sectionLabel##name = new wxStaticText(panel, wxID_ANY, displayName); \ + auto sectionValue##name = new wxDirPickerCtrl(panel, wxID_ANY); \ + ReadBinds.emplace_back([sectionValue##name](SETTINGS* val) { \ + sectionValue##name->SetPath(val->##binder); \ + }); \ + WriteBinds.emplace_back([sectionValue##name](SETTINGS* val) { \ + strcpy_s(val->##binder, sectionValue##name->GetPath().c_str()); \ + }); \ + sectionGrid##section->Add(sectionLabel##name, wxGBPosition(sectionColInd##section, 0), wxGBSpan(1, 1), wxEXPAND); \ + sectionGrid##section->Add(sectionValue##name, wxGBPosition(sectionColInd##section, 2), wxGBSpan(1, 1), wxEXPAND); \ + sectionColInd##section++; + +#define ADD_ITEM_CHOICE(section, name, displayName, choices, under_type, binder) \ + auto sectionLabel##name = new wxStaticText(panel, wxID_ANY, displayName); \ + auto sectionValue##name = new wxChoice(panel, wxID_ANY); \ + ReadBinds.emplace_back([sectionValue##name](SETTINGS* val) { \ + sectionValue##name->SetSelection((int)val->##binder); \ + }); \ + WriteBinds.emplace_back([sectionValue##name](SETTINGS* val) { \ + val->##binder = (under_type)sectionValue##name->GetSelection(); \ + }); \ + sectionValue##name->Append(wxArrayString(_countof(choices), choices)); \ + sectionGrid##section->Add(sectionLabel##name, wxGBPosition(sectionColInd##section, 0), wxGBSpan(1, 1), wxEXPAND); \ + sectionGrid##section->Add(sectionValue##name, wxGBPosition(sectionColInd##section, 2), wxGBSpan(1, 1), wxEXPAND); \ + sectionColInd##section++; + +#define ADD_ITEM_TEXTSLIDER(section, name, displayName, choices, under_type, binder) \ + auto sectionLabel##name = new wxStaticText(panel, wxID_ANY, displayName); \ + auto sectionValue##name = new wxLabelSlider(panel, wxID_ANY, 0, wxArrayString(_countof(choices), choices), wxSL_HORIZONTAL | wxSL_AUTOTICKS); \ + ReadBinds.emplace_back([sectionValue##name](SETTINGS* val) { \ + sectionValue##name->SetValue((int)val->##binder); \ + }); \ + WriteBinds.emplace_back([sectionValue##name](SETTINGS* val) { \ + val->##binder = (under_type)sectionValue##name->GetValue(); \ + }); \ + sectionGrid##section->Add(sectionLabel##name, wxGBPosition(sectionColInd##section, 0), wxGBSpan(1, 1), wxEXPAND); \ + sectionGrid##section->Add(sectionValue##name, wxGBPosition(sectionColInd##section, 2), wxGBSpan(1, 1), wxEXPAND); \ + sectionColInd##section++; + +#define ADD_ITEM_SLIDER(section, name, displayName, minValue, maxValue, under_type, binder) \ + auto sectionLabel##name = new wxStaticText(panel, wxID_ANY, displayName); \ + auto sectionValue##name = new wxSlider(panel, wxID_ANY, minValue, minValue, maxValue, wxDefaultPosition, wxDefaultSize, wxSL_HORIZONTAL | wxSL_LABELS | wxSL_AUTOTICKS); \ + ReadBinds.emplace_back([sectionValue##name](SETTINGS* val) { \ + sectionValue##name->SetValue((int)val->##binder); \ + }); \ + WriteBinds.emplace_back([sectionValue##name](SETTINGS* val) { \ + val->##binder = (under_type)sectionValue##name->GetValue(); \ + }); \ + sectionGrid##section->Add(sectionLabel##name, wxGBPosition(sectionColInd##section, 0), wxGBSpan(1, 1), wxEXPAND); \ + sectionGrid##section->Add(sectionValue##name, wxGBPosition(sectionColInd##section, 2), wxGBSpan(1, 1), wxEXPAND); \ + sectionColInd##section++; + +#define ADD_ITEM_TEXT(section, name, displayName, binder) \ + auto sectionLabel##name = new wxStaticText(panel, wxID_ANY, displayName); \ + auto sectionValue##name = new wxTextCtrl(panel, wxID_ANY); \ + ReadBinds.emplace_back([sectionValue##name](SETTINGS* val) { \ + sectionValue##name->SetValue(val->##binder); \ + }); \ + WriteBinds.emplace_back([sectionValue##name](SETTINGS* val) { \ + strcpy_s(val->##binder, sectionValue##name->GetValue().c_str()); \ + }); \ + sectionGrid##section->Add(sectionLabel##name, wxGBPosition(sectionColInd##section, 0), wxGBSpan(1, 1), wxEXPAND); \ + sectionGrid##section->Add(sectionValue##name, wxGBPosition(sectionColInd##section, 2), wxGBSpan(1, 1), wxEXPAND); \ + sectionColInd##section++; + +#define APPEND_SECTION_FIRST(name) \ + sectionGrid##name->AddGrowableCol(2); \ + mainSizer->Add(sectionBox##name, wxSizerFlags().Expand().Border(wxUP | wxRIGHT | wxLEFT, 10)); +#define APPEND_SECTION(name) \ + mainSizer->AddSpacer(5); \ + sectionGrid##name->AddGrowableCol(2); \ + mainSizer->Add(sectionBox##name, wxSizerFlags().Expand().Border(wxRIGHT | wxLEFT, 10)); + +cSetup::cSetup(cMain* main, SETTINGS* settings, bool startupInvalid, cSetup::flush_callback callback, cSetup::validate_callback validator) : wxModalWindow(main, wxID_ANY, "Setup - EGL2", wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_STYLE ^ (wxMAXIMIZE_BOX | wxRESIZE_BORDER)), + Settings(settings), + OldSettings(*settings), + InvalidStartup(startupInvalid), + Callback(callback), + Validator(validator) { this->SetIcon(wxICON(APP_ICON)); this->SetMinSize(wxSize(500, -1)); this->SetMaxSize(wxSize(500, -1)); - wxPanel* panel = new wxPanel(this, wxID_ANY, + auto panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - auto settingsGrid = new wxGridBagSizer(2, 2); - - cacheDirTxt = new wxStaticText(panel, wxID_ANY, "Install Folder"); - cacheDirValue = new wxDirPickerCtrl(panel, wxID_ANY); - - compMethodTxt = new wxStaticText(panel, wxID_ANY, "Compression Method"); - compMethodValue = new wxChoice(panel, wxID_ANY); - compMethodValue->Append(wxArrayString(_countof(compMethods), compMethods)); - - compLevelTxt = new wxStaticText(panel, wxID_ANY, "Compression Level"); - compLevelValue = new wxChoice(panel, wxID_ANY); - compLevelValue->Append(wxArrayString(_countof(compLevels), compLevels)); - - verifyCacheCheckbox = new wxCheckBox(panel, wxID_ANY, "Verify chunks when read from"); - verifyCacheCheckbox->SetToolTip(VERIFY_TOOLTIP); - - gameDirCheckbox = new wxCheckBox(panel, wxID_ANY, "Enable playing of game"); - gameDirCheckbox->SetToolTip(GAME_TOOLTIP); - - settingsGrid->Add(cacheDirTxt, wxGBPosition(0, 0), wxGBSpan(1, 1), wxEXPAND); - settingsGrid->Add(cacheDirValue, wxGBPosition(0, 2), wxGBSpan(1, 1), wxEXPAND); - - settingsGrid->Add(compMethodTxt, wxGBPosition(1, 0), wxGBSpan(1, 1), wxEXPAND); - settingsGrid->Add(compLevelTxt, wxGBPosition(2, 0), wxGBSpan(1, 1), wxEXPAND); - settingsGrid->Add(compMethodValue, wxGBPosition(1, 2), wxGBSpan(1, 1), wxEXPAND); - settingsGrid->Add(compLevelValue, wxGBPosition(2, 2), wxGBSpan(1, 1), wxEXPAND); - - settingsGrid->AddGrowableCol(2); - settingsGrid->Add(5, 1, wxGBPosition(0, 1)); - - auto checkboxSizer = new wxBoxSizer(wxHORIZONTAL); + auto mainSizer = new wxBoxSizer(wxVERTICAL); - checkboxSizer->Add(verifyCacheCheckbox, 1); - checkboxSizer->Add(gameDirCheckbox, 1); + + DEFINE_SECTION(general, "General"); + ADD_ITEM_BROWSE(general, cacheDir, "Install Folder", CacheDir); + ADD_ITEM_CHOICE(general, compMethod, "Compression Method", compMethods, SettingsCompressionMethod, CompressionMethod); + ADD_ITEM_TEXTSLIDER(general, compLevel, "Compression Level", compLevels, SettingsCompressionLevel, CompressionLevel); + ADD_ITEM_TEXTSLIDER(general, updateInt, "Update Interval", updateLevels, SettingsUpdateInterval, UpdateInterval); - auto advancedBox = new wxStaticBoxSizer(wxVERTICAL, panel, "Advanced"); - auto advancedSizer = new wxGridBagSizer(2, 2); + DEFINE_SECTION(advanced, "Advanced"); + ADD_ITEM_SLIDER(advanced, bufCount, "Buffer Count", 1, 512, uint16_t, BufferCount); + ADD_ITEM_SLIDER(advanced, threadCount, "Thread Count", 1, 128, uint16_t, ThreadCount); + ADD_ITEM_TEXT(advanced, cmdArgs, "Command Arguments", CommandArgs); - cmdArgsTxt = new wxStaticText(panel, wxID_ANY, "Command Arguments"); - cmdArgsValue = new wxTextCtrl(panel, wxID_ANY); + APPEND_SECTION_FIRST(general); + APPEND_SECTION(advanced); - advancedSizer->Add(cmdArgsTxt, wxGBPosition(0, 0), wxGBSpan(1, 1), wxEXPAND); - advancedSizer->Add(cmdArgsValue, wxGBPosition(0, 2), wxGBSpan(1, 1), wxEXPAND); + auto applySettingsPanel = new wxBoxSizer(wxHORIZONTAL); - advancedSizer->AddGrowableCol(2); - advancedSizer->Add(5, 1, wxGBPosition(0, 1)); + auto okBtn = new wxButton(panel, wxID_ANY, "OK"); + auto cancelBtn = new wxButton(panel, wxID_ANY, "Cancel"); + okBtn->Bind(wxEVT_BUTTON, std::bind(&cSetup::OkClicked, this)); + cancelBtn->Bind(wxEVT_BUTTON, std::bind(&cSetup::CancelClicked, this)); + applySettingsPanel->Add(okBtn, wxSizerFlags().Border(wxRIGHT, 5)); + applySettingsPanel->Add(cancelBtn); - advancedBox->Add(advancedSizer, wxSizerFlags().Expand().Border(wxALL, 5)); + mainSizer->AddSpacer(5); + mainSizer->Add(applySettingsPanel, wxSizerFlags().Right().Border(wxDOWN | wxRIGHT, 10)); - auto sizer = new wxBoxSizer(wxVERTICAL); - - sizer->Add(settingsGrid, wxSizerFlags().Expand().Border(wxUP | wxRIGHT | wxLEFT, 10)); - sizer->AddSpacer(10); - sizer->Add(checkboxSizer, wxSizerFlags().Expand().Border(wxRIGHT | wxLEFT, 10)); - sizer->AddSpacer(10); - sizer->Add(advancedBox, wxSizerFlags().Expand().Border(wxDOWN | wxRIGHT | wxLEFT, 10)); - - panel->SetSizerAndFit(sizer); + panel->SetSizerAndFit(mainSizer); this->Fit(); wxToolTip::SetAutoPop(15000); ReadConfig(); - // disable when first 2 options are selected - compLevelValue->Enable(compMethodValue->GetSelection() >= 2); - compMethodValue->Bind(wxEVT_CHOICE, [this](wxCommandEvent& evt) { - compLevelValue->Enable(compMethodValue->GetSelection() >= 2); - }); - - Bind(wxEVT_CLOSE_WINDOW, std::bind(&cSetup::WriteConfig, this)); + Bind(wxEVT_CLOSE_WINDOW, &cSetup::CloseClicked, this); } cSetup::~cSetup() { @@ -119,25 +183,57 @@ cSetup::~cSetup() { } void cSetup::ReadConfig() { - cacheDirValue->SetPath(Settings->CacheDir); - cmdArgsValue->SetValue(Settings->CommandArgs); - - compMethodValue->SetSelection(Settings->CompressionMethod); - compLevelValue->SetSelection(Settings->CompressionLevel); - - verifyCacheCheckbox->SetValue(Settings->VerifyCache); - gameDirCheckbox->SetValue(Settings->EnableGaming); + for (auto& bind : ReadBinds) { + bind(Settings); + } } void cSetup::WriteConfig() { - strcpy_s(Settings->CacheDir, cacheDirValue->GetPath().c_str()); - strcpy_s(Settings->CommandArgs, cmdArgsValue->GetValue().c_str()); + for (auto& bind : WriteBinds) { + bind(Settings); + } +} - Settings->CompressionMethod = compMethodValue->GetSelection(); - Settings->CompressionLevel = compLevelValue->GetSelection(); +void cSetup::OkClicked() +{ + WriteConfig(); + if (Validator(Settings)) { // New values are valid + Callback(Settings); + this->Destroy(); + } +} - Settings->VerifyCache = verifyCacheCheckbox->IsChecked(); - Settings->EnableGaming = gameDirCheckbox->IsChecked(); +void cSetup::CancelClicked() +{ + if (InvalidStartup) { + this->Destroy(); + } + else if (Validator(&OldSettings)) { // Old values are valid, it's fine to throw out the current values + *Settings = OldSettings; + this->Destroy(); + } +} + +void cSetup::ApplyClicked() +{ + WriteConfig(); + if (Validator(Settings)) { // New values are valid + Callback(Settings); + } +} - this->Destroy(); +void cSetup::CloseClicked(wxCloseEvent& evt) +{ + if (Validator(&OldSettings)) { // Old values are valid, it's fine to throw out the current values + *Settings = OldSettings; + this->Destroy(); + } + else { + if (!InvalidStartup) { + evt.Veto(); + } + else { + this->Destroy(); + } + } } \ No newline at end of file diff --git a/gui/cSetup.h b/gui/cSetup.h index 44dfafc..cab8239 100644 --- a/gui/cSetup.h +++ b/gui/cSetup.h @@ -3,41 +3,36 @@ #include "cMain.h" #include "wxModalWindow.h" -#include +#include +#include #include +#include class cSetup : public wxModalWindow { public: - cSetup(cMain* main, SETTINGS* settings); - ~cSetup(); - -protected: - wxPanel* topPanel = nullptr; - - wxWindow* settingsContainer = nullptr; - wxWindow* checkboxContainer = nullptr; - wxStaticBoxSizer* advancedContainer = nullptr; - - wxStaticText* cacheDirTxt = nullptr; - wxDirPickerCtrl* cacheDirValue = nullptr; + using flush_callback = std::function; + using validate_callback = std::function; - wxStaticText* compMethodTxt = nullptr; - wxChoice* compMethodValue = nullptr; - - wxStaticText* compLevelTxt = nullptr; - wxChoice* compLevelValue = nullptr; - - wxCheckBox* verifyCacheCheckbox = nullptr; - wxCheckBox* gameDirCheckbox = nullptr; - - wxStaticText* cmdArgsTxt = nullptr; - wxTextCtrl* cmdArgsValue = nullptr; + cSetup(cMain* main, SETTINGS* settings, bool startupInvalid, flush_callback callback, validate_callback validator); + ~cSetup(); private: + std::vector> ReadBinds; + std::vector> WriteBinds; + SETTINGS* Settings; + bool InvalidStartup; + SETTINGS OldSettings; + flush_callback Callback; + validate_callback Validator; void ReadConfig(); void WriteConfig(); + + void OkClicked(); + void CancelClicked(); + void ApplyClicked(); + void CloseClicked(wxCloseEvent& evt); }; diff --git a/gui/guimain.cpp b/gui/guimain.cpp index e4e711f..4321bf7 100644 --- a/gui/guimain.cpp +++ b/gui/guimain.cpp @@ -1,3 +1,9 @@ #include "cApp.h" -wxIMPLEMENT_APP(cApp); \ No newline at end of file +//#define USE_CONSOLE + +#ifdef USE_CONSOLE +wxIMPLEMENT_APP_CONSOLE(cApp); +#else +wxIMPLEMENT_APP(cApp); +#endif \ No newline at end of file diff --git a/gui/resources.rc b/gui/resources.rc deleted file mode 100644 index 5a7e479..0000000 --- a/gui/resources.rc +++ /dev/null @@ -1,27 +0,0 @@ -APP_ICON ICON "../icon.ico" - -#include - -VS_VERSION_INFO VERSIONINFO -FILEVERSION 1, 2, 2, 0 -PRODUCTVERSION 1, 2, 2, 0 -{ - BLOCK "StringFileInfo" - { - BLOCK "040904b0" - { - VALUE "CompanyName", "WorkingRobot" - VALUE "FileDescription", "EGL2" - VALUE "FileVersion", "1.0.0.0" - VALUE "InternalName", "egl2" - VALUE "LegalCopyright", "EGL2 (c) by Aleks Margarian (WorkingRobot)" - VALUE "OriginalFilename", "EGL2.exe" - VALUE "ProductName", "EGL2" - VALUE "ProductVersion", "1.2.2.0" - } - } - BLOCK "VarFileInfo" - { - VALUE "Translation", 0x409, 1200 - } -} \ No newline at end of file diff --git a/gui/settings.cpp b/gui/settings.cpp index aab549b..20ce566 100644 --- a/gui/settings.cpp +++ b/gui/settings.cpp @@ -1,7 +1,5 @@ #include "settings.h" -#include - template inline T ReadValue(FILE* File) { char val[sizeof(T)]; @@ -33,22 +31,60 @@ inline bool ReadVersion(SETTINGS* Settings, FILE* File, SettingsVersion Version) ReadValue(File); // was MountDrive - Settings->CompressionMethod = ReadValue(File); - Settings->CompressionLevel = ReadValue(File); + switch (ReadValue(File)) + { + case 0: // downloaded + case 3: // explicit zlib + Settings->CompressionMethod = SettingsCompressionMethod::Zstandard; + break; + case 1: // decompressed + Settings->CompressionMethod = SettingsCompressionMethod::Decompressed; + break; + case 2: // lz4 + default: + Settings->CompressionMethod = SettingsCompressionMethod::LZ4; + break; + } + + Settings->CompressionLevel = (SettingsCompressionLevel)ReadValue(File); + + ReadValue(File); // was VerifyCache + ReadValue(File); // was EnableGaming + return true; + case SettingsVersion::SimplifyPathsAndCmdLine: + ReadString(Settings->CacheDir, File); - Settings->VerifyCache = ReadValue(File); - Settings->EnableGaming = ReadValue(File); + switch (ReadValue(File)) + { + case 0: // downloaded + case 3: // explicit zlib + Settings->CompressionMethod = SettingsCompressionMethod::Zstandard; + break; + case 1: // decompressed + Settings->CompressionMethod = SettingsCompressionMethod::Decompressed; + break; + case 2: // lz4 + default: + Settings->CompressionMethod = SettingsCompressionMethod::LZ4; + break; + } + + Settings->CompressionLevel = (SettingsCompressionLevel)ReadValue(File); + + ReadValue(File); // was VerifyCache + ReadValue(File); // was EnableGaming - strcpy(Settings->CommandArgs, ""); + ReadString(Settings->CommandArgs, File); return true; - case SettingsVersion::SimplifyPathsAndCmdLine: + case SettingsVersion::Version13: ReadString(Settings->CacheDir, File); - Settings->CompressionMethod = ReadValue(File); - Settings->CompressionLevel = ReadValue(File); + Settings->CompressionMethod = ReadValue(File); + Settings->CompressionLevel = ReadValue(File); + Settings->UpdateInterval = ReadValue(File); - Settings->VerifyCache = ReadValue(File); - Settings->EnableGaming = ReadValue(File); + Settings->BufferCount = ReadValue(File); + Settings->ThreadCount = ReadValue(File); ReadString(Settings->CommandArgs, File); return true; @@ -87,15 +123,28 @@ void SettingsWrite(SETTINGS* Settings, FILE* File) WriteString(Settings->CacheDir, File); - WriteValue(Settings->CompressionMethod, File); - WriteValue(Settings->CompressionLevel, File); + WriteValue(Settings->CompressionMethod, File); + WriteValue(Settings->CompressionLevel, File); + WriteValue(Settings->UpdateInterval, File); - WriteValue(Settings->VerifyCache, File); - WriteValue(Settings->EnableGaming, File); + WriteValue(Settings->BufferCount, File); + WriteValue(Settings->ThreadCount, File); WriteString(Settings->CommandArgs, File); } +SETTINGS SettingsDefault() { + return { + .CacheDir = "", + .CompressionMethod = SettingsCompressionMethod::Zstandard, + .CompressionLevel = SettingsCompressionLevel::Slow, + .UpdateInterval = SettingsUpdateInterval::Minute1, + .BufferCount = 128, + .ThreadCount = 64, + .CommandArgs = "-NOTEXTURESTREAMING -USEALLAVAILABLECORES" + }; +} + bool SettingsValidate(SETTINGS* Settings) { if (!fs::is_directory(Settings->CacheDir)) { return false; @@ -103,41 +152,60 @@ bool SettingsValidate(SETTINGS* Settings) { return true; } +std::chrono::milliseconds SettingsGetUpdateInterval(SETTINGS* Settings) +{ + switch (Settings->UpdateInterval) + { + case SettingsUpdateInterval::Second1: + return std::chrono::milliseconds(1 * 1000); + case SettingsUpdateInterval::Second5: + return std::chrono::milliseconds(5 * 1000); + case SettingsUpdateInterval::Second10: + return std::chrono::milliseconds(10 * 1000); + case SettingsUpdateInterval::Second30: + return std::chrono::milliseconds(30 * 1000); + case SettingsUpdateInterval::Minute1: + return std::chrono::milliseconds(1 * 60 * 1000); + case SettingsUpdateInterval::Minute5: + return std::chrono::milliseconds(5 * 60 * 1000); + case SettingsUpdateInterval::Minute10: + return std::chrono::milliseconds(10 * 60 * 1000); + case SettingsUpdateInterval::Minute30: + return std::chrono::milliseconds(30 * 60 * 1000); + case SettingsUpdateInterval::Hour1: + return std::chrono::milliseconds(60 * 60 * 1000); + } +} + uint32_t SettingsGetStorageFlags(SETTINGS* Settings) { - uint32_t StorageFlags = 0; - if (Settings->VerifyCache) { - StorageFlags |= StorageVerifyHashes; - } + uint32_t StorageFlags = StorageVerifyHashes; switch (Settings->CompressionMethod) { - case 0: - StorageFlags |= StorageCompressed; - break; - case 1: - StorageFlags |= StorageDecompressed; + case SettingsCompressionMethod::LZ4: + StorageFlags |= StorageLZ4; break; - case 2: - StorageFlags |= StorageCompressLZ4; + case SettingsCompressionMethod::Zstandard: + StorageFlags |= StorageZstd; break; - case 3: - StorageFlags |= StorageCompressZlib; + case SettingsCompressionMethod::Decompressed: + StorageFlags |= StorageDecompressed; break; } switch (Settings->CompressionLevel) { - case 0: + case SettingsCompressionLevel::Fastest: StorageFlags |= StorageCompressFastest; break; - case 1: + case SettingsCompressionLevel::Fast: StorageFlags |= StorageCompressFast; break; - case 2: + case SettingsCompressionLevel::Normal: StorageFlags |= StorageCompressNormal; break; - case 3: + case SettingsCompressionLevel::Slow: StorageFlags |= StorageCompressSlow; break; - case 4: + case SettingsCompressionLevel::Slowest: StorageFlags |= StorageCompressSlowest; break; } diff --git a/gui/settings.h b/gui/settings.h index 5ab9759..7fa061d 100644 --- a/gui/settings.h +++ b/gui/settings.h @@ -1,7 +1,7 @@ #pragma once #define FILE_CONFIG_MAGIC 0xE6219B27 -#define FILE_CONFIG_VERSION (uint16_t)SettingsVersion::Latest +#define FILE_CONFIG_VERSION (uint16_t)SettingsVersion::Version13 #include "../storage/storage.h" @@ -13,21 +13,56 @@ enum class SettingsVersion : uint16_t { // Adds CommandArgs SimplifyPathsAndCmdLine, + // Adds ThreadCount, BufferCount, and UpdateInterval + // Removes VerifyCache and EnableGaming + Version13, + LatestPlusOne, Latest = LatestPlusOne - 1 }; +enum class SettingsCompressionMethod : uint8_t { + Zstandard, + LZ4, + Decompressed +}; + +enum class SettingsCompressionLevel : uint8_t { + Fastest, + Fast, + Normal, + Slow, + Slowest +}; + +enum class SettingsUpdateInterval : uint8_t { + Second1, + Second5, + Second10, + Second30, + Minute1, + Minute5, + Minute10, + Minute30, + Hour1 +}; + struct SETTINGS { char CacheDir[_MAX_PATH + 1]; - uint8_t CompressionMethod; - uint8_t CompressionLevel; - bool VerifyCache; - bool EnableGaming; + SettingsCompressionMethod CompressionMethod; + SettingsCompressionLevel CompressionLevel; + SettingsUpdateInterval UpdateInterval; + + // Advanced + uint16_t BufferCount; + uint16_t ThreadCount; char CommandArgs[1024 + 1]; }; bool SettingsRead(SETTINGS* Settings, FILE* File); void SettingsWrite(SETTINGS* Settings, FILE* File); +SETTINGS SettingsDefault(); bool SettingsValidate(SETTINGS* Settings); +std::chrono::milliseconds SettingsGetUpdateInterval(SETTINGS* Settings); uint32_t SettingsGetStorageFlags(SETTINGS* Settings); \ No newline at end of file diff --git a/gui/updateChecker.h b/gui/updateChecker.h new file mode 100644 index 0000000..614ab6a --- /dev/null +++ b/gui/updateChecker.h @@ -0,0 +1,54 @@ +#pragma once +#include "../containers/cancel_flag.h" +#include "../web/manifest/auth.h" + +#include +#include +#include +#include + +namespace fs = std::filesystem; + +typedef std::function UpdateCallback; + +class UpdateChecker { +public: + UpdateChecker(fs::path cachePath, UpdateCallback callback, std::chrono::milliseconds checkInterval); + ~UpdateChecker(); + + void SetInterval(std::chrono::milliseconds newInterval); + + bool ForceUpdate(); + + std::string& GetLatestId(); + std::string& GetLatestUrl(); + std::string& GetLatestVersion(); + + static std::string GetReadableVersion(const std::string_view& version) { + auto start = version.find('-') + 1; + auto start2 = version.find('-', start); + auto end2 = version.find('-', start2 + 1) + 1; + auto end = version.find('-', end2); + + char buf[64]; + sprintf_s(buf, "%.*s (CL: %.*s)", start2 - start, version.data() + start, end - end2, version.data() + end2); + return buf; + } + + Manifest GetManifest(const std::string& Url); + +private: + void Thread(); + + std::atomic CheckInterval; + + ManifestAuth Auth; + std::string LatestId; + std::string LatestUrl; + std::string LatestVersion; + + UpdateCallback Callback; + std::thread UpdateThread; + std::chrono::steady_clock::time_point UpdateWakeup; + cancel_flag UpdateFlag; +}; \ No newline at end of file diff --git a/gui/wxLabelSlider.cpp b/gui/wxLabelSlider.cpp new file mode 100644 index 0000000..046b4fe --- /dev/null +++ b/gui/wxLabelSlider.cpp @@ -0,0 +1,111 @@ +#include "wxLabelSlider.h" + +wxLabelSlider::wxLabelSlider() : wxPanel() { } + +wxLabelSlider::wxLabelSlider(wxWindow* parent, wxWindowID id, int selectedIndex, const wxArrayString& values, long style, const wxString& name) : + wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(-1, 45)), + values(values) +{ + if (style & wxSL_SELRANGE) { + style ^= wxSL_SELRANGE; + } + + mLabel = style & wxSL_MIN_MAX_LABELS; + if (style & wxSL_MIN_MAX_LABELS) { + style ^= wxSL_MIN_MAX_LABELS; + } + + vLabel = style ^ wxSL_VALUE_LABEL; + if (style & wxSL_VALUE_LABEL) { + style ^= wxSL_VALUE_LABEL; + } + + slider = new wxSlider(this, id, selectedIndex, 0, values.size() - 1, wxDefaultPosition, wxSize(-1, 32), style | wxSL_HORIZONTAL, wxDefaultValidator, name); + sliderText = new wxStaticText(this, wxID_ANY, values[0], wxDefaultPosition, wxDefaultSize, wxALIGN_CENTRE_HORIZONTAL | wxST_NO_AUTORESIZE); + + sliderSizer = new wxBoxSizer(wxVERTICAL); + sliderSizer->Add(slider, 1, wxEXPAND); + sliderSizer->Add(sliderText, 0, wxEXPAND); + + panelSizer = new wxBoxSizer(wxHORIZONTAL); + + int labelSize; + if (mLabel) { + labelSize = std::max(GetTextExtent(values[0]).x, GetTextExtent(values[values.size() - 1]).x); + panelSizer->Add(new wxStaticText(this, wxID_ANY, values[0], wxDefaultPosition, wxSize(labelSize, -1), wxALIGN_CENTRE_HORIZONTAL), 0, wxALIGN_CENTRE); + } + panelSizer->Add(sliderSizer, 1, wxEXPAND); + if (mLabel) { + panelSizer->Add(new wxStaticText(this, wxID_ANY, values[values.size() - 1], wxDefaultPosition, wxSize(labelSize, -1), wxALIGN_CENTRE_HORIZONTAL), 0, wxALIGN_CENTRE); + } + + slider->Bind(wxEVT_SCROLL_TOP, &wxLabelSlider::_OnTop, this); + slider->Bind(wxEVT_SCROLL_BOTTOM, &wxLabelSlider::_OnBottom, this); + slider->Bind(wxEVT_SCROLL_LINEUP, &wxLabelSlider::_OnLineUp, this); + slider->Bind(wxEVT_SCROLL_LINEDOWN, &wxLabelSlider::_OnLineDown, this); + slider->Bind(wxEVT_SCROLL_PAGEUP, &wxLabelSlider::_OnPageUp, this); + slider->Bind(wxEVT_SCROLL_PAGEDOWN, &wxLabelSlider::_OnPageDown, this); + slider->Bind(wxEVT_SCROLL_THUMBTRACK, &wxLabelSlider::_OnThumbTrack, this); + slider->Bind(wxEVT_SCROLL_THUMBRELEASE, &wxLabelSlider::_OnThumbRelease, this); + slider->Bind(wxEVT_SCROLL_CHANGED, &wxLabelSlider::_OnChanged, this); + + SetSizer(panelSizer); + Refresh(); +} + +void wxLabelSlider::_OnTop(wxScrollEvent& evt) +{ + _CreateEvent(wxEVT_SCROLL_TOP); +} + +void wxLabelSlider::_OnBottom(wxScrollEvent& evt) +{ + _CreateEvent(wxEVT_SCROLL_BOTTOM); +} + +void wxLabelSlider::_OnLineUp(wxScrollEvent& evt) +{ + _CreateEvent(wxEVT_SCROLL_LINEUP); +} + +void wxLabelSlider::_OnLineDown(wxScrollEvent& evt) +{ + _CreateEvent(wxEVT_SCROLL_LINEDOWN); +} + +void wxLabelSlider::_OnPageUp(wxScrollEvent& evt) +{ + _CreateEvent(wxEVT_SCROLL_PAGEUP); +} + +void wxLabelSlider::_OnPageDown(wxScrollEvent& evt) +{ + _CreateEvent(wxEVT_SCROLL_PAGEDOWN); +} + +void wxLabelSlider::_OnThumbTrack(wxScrollEvent& evt) +{ + _CreateEvent(wxEVT_SCROLL_THUMBTRACK); +} + +void wxLabelSlider::_OnThumbRelease(wxScrollEvent& evt) +{ + _CreateEvent(wxEVT_SCROLL_THUMBRELEASE); +} + +void wxLabelSlider::_OnChanged(wxScrollEvent& evt) +{ + _CreateEvent(wxEVT_SCROLL_CHANGED); +} + +template +void wxLabelSlider::_CreateEvent(const EventTag& eventType) +{ + sliderText->SetLabel(values[slider->GetValue()]); + + auto evt = wxScrollEvent(eventType, GetId()); + evt.SetEventObject(this); + GetEventHandler()->ProcessEvent(evt); + slider->Refresh(); + Refresh(); +} diff --git a/gui/wxLabelSlider.h b/gui/wxLabelSlider.h new file mode 100644 index 0000000..70c6a4e --- /dev/null +++ b/gui/wxLabelSlider.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +class wxLabelSlider : public wxPanel { +public: + wxLabelSlider(); + + // only wxSL_AUTOTICKS, wxSL_MIN_MAX_LABELS, and wxSL_VALUE_LABEL flags are respected + wxLabelSlider(wxWindow* parent, + wxWindowID id, + int selectedIndex, + const wxArrayString& values, + long style = 0, + const wxString& name = wxSliderNameStr); + + int GetValue() { + return slider->GetValue(); + } + + void SetValue(int value) { + slider->SetValue(value); + _CreateEvent(wxEVT_SCROLL_CHANGED); + } + +private: + void _OnTop(wxScrollEvent& evt); + void _OnBottom(wxScrollEvent& evt); + void _OnLineUp(wxScrollEvent& evt); + void _OnLineDown(wxScrollEvent& evt); + void _OnPageUp(wxScrollEvent& evt); + void _OnPageDown(wxScrollEvent& evt); + void _OnThumbTrack(wxScrollEvent& evt); + void _OnThumbRelease(wxScrollEvent& evt); + void _OnChanged(wxScrollEvent& evt); + + template + void _CreateEvent(const EventTag& eventType); + + wxArrayString values; + bool mLabel, vLabel; + wxSlider* slider; + wxStaticText* sliderText; + wxBoxSizer* sliderSizer; + wxBoxSizer* panelSizer; +}; \ No newline at end of file diff --git a/libraries/curlion/connection.cpp b/libraries/curlion/connection.cpp new file mode 100644 index 0000000..73384ed --- /dev/null +++ b/libraries/curlion/connection.cpp @@ -0,0 +1,466 @@ +#include "connection.h" +#include "socket_factory.h" +#include "log.h" + +#ifdef WIN32 +#undef min +#endif + +namespace curlion { + +static inline LoggerProxy WriteConnectionLog(void* connection_identifier) { + return Log() << "Connection(" << connection_identifier << "): "; +} + + +Connection::Connection() : + is_running_(false), + dns_resolve_items_(nullptr), + request_body_read_length_(0), + result_(CURL_LAST) { + + handle_ = curl_easy_init(); + SetInitialOptions(); +} + + +Connection::~Connection() { + + ReleaseDnsResolveItems(); + curl_easy_cleanup(handle_); +} + + +void Connection::SetInitialOptions() { + + curl_easy_setopt(handle_, CURLOPT_READFUNCTION, CurlReadBodyCallback); + curl_easy_setopt(handle_, CURLOPT_READDATA, this); + curl_easy_setopt(handle_, CURLOPT_SEEKFUNCTION, CurlSeekBodyCallback); + curl_easy_setopt(handle_, CURLOPT_SEEKDATA, this); + curl_easy_setopt(handle_, CURLOPT_HEADERFUNCTION, CurlWriteHeaderCallback); + curl_easy_setopt(handle_, CURLOPT_HEADERDATA, this); + curl_easy_setopt(handle_, CURLOPT_WRITEFUNCTION, CurlWriteBodyCallback); + curl_easy_setopt(handle_, CURLOPT_WRITEDATA, this); + curl_easy_setopt(handle_, CURLOPT_XFERINFOFUNCTION, CurlProgressCallback); + curl_easy_setopt(handle_, CURLOPT_XFERINFODATA, this); +} + + +void Connection::Start() { + + if (! is_running_) { + WillStart(); + CURLcode result = curl_easy_perform(handle_); + DidFinish(result); + } +} + + +void Connection::ResetOptions() { + + if (! is_running_) { + curl_easy_reset(handle_); + SetInitialOptions(); + ResetOptionResources(); + } +} + + +void Connection::ResetOptionResources() { + + ReleaseDnsResolveItems(); + + request_body_.clear(); + request_body_read_length_ = 0; + + read_body_callback_ = nullptr; + seek_body_callback_ = nullptr; + write_header_callback_ = nullptr; + write_body_callback_ = nullptr; + progress_callback_ = nullptr; + debug_callback_ = nullptr; + finished_callback_ = nullptr; +} + + +void Connection::ReleaseDnsResolveItems() { + + if (dns_resolve_items_ != nullptr) { + curl_slist_free_all(dns_resolve_items_); + dns_resolve_items_ = nullptr; + } +} + + +void Connection::SetVerbose(bool verbose) { + curl_easy_setopt(handle_, CURLOPT_VERBOSE, verbose); +} + + +void Connection::SetUrl(const std::string& url) { + curl_easy_setopt(handle_, CURLOPT_URL, url.c_str()); +} + + +void Connection::SetProxy(const std::string& proxy) { + curl_easy_setopt(handle_, CURLOPT_PROXY, proxy.c_str()); +} + + +void Connection::SetProxyAccount(const std::string& username, const std::string& password) { + curl_easy_setopt(handle_, CURLOPT_PROXYUSERNAME, username.c_str()); + curl_easy_setopt(handle_, CURLOPT_PROXYPASSWORD, password.c_str()); +} + + +void Connection::SetConnectOnly(bool connect_only) { + curl_easy_setopt(handle_, CURLOPT_CONNECT_ONLY, connect_only); +} + + +void Connection::SetDnsResolveItems(const std::multimap& resolve_items) { + + ReleaseDnsResolveItems(); + + for (const auto& each_pair : resolve_items) { + + std::string item_string; + if (each_pair.second.empty()) { + item_string.append(1, '-'); + item_string.append(each_pair.first); + } + else { + item_string.append(each_pair.first); + item_string.append(1, ':'); + item_string.append(each_pair.second); + } + dns_resolve_items_ = curl_slist_append(dns_resolve_items_, item_string.c_str()); + } + + curl_easy_setopt(handle_, CURLOPT_RESOLVE, dns_resolve_items_); +} + + +void Connection::SetVerifyCertificate(bool verify) { + curl_easy_setopt(handle_, CURLOPT_SSL_VERIFYPEER, verify); +} + +void Connection::SetVerifyHost(bool verify) { + curl_easy_setopt(handle_, CURLOPT_SSL_VERIFYHOST, verify ? 2 : 0); +} + +void Connection::SetCertificateFilePath(const std::string& file_path) { + const char* path = file_path.empty() ? nullptr : file_path.c_str(); + curl_easy_setopt(handle_, CURLOPT_CAINFO, path); +} + +void Connection::SetReceiveBody(bool receive_body) { + curl_easy_setopt(handle_, CURLOPT_NOBODY, ! receive_body); +} + +void Connection::SetEnableProgress(bool enable) { + curl_easy_setopt(handle_, CURLOPT_NOPROGRESS, ! enable); +} + +void Connection::SetConnectTimeoutInMilliseconds(long milliseconds) { + curl_easy_setopt(handle_, CURLOPT_CONNECTTIMEOUT_MS, milliseconds); +} + +void Connection::SetLowSpeedTimeout(long low_speed_in_bytes_per_seond, long timeout_in_seconds) { + curl_easy_setopt(handle_, CURLOPT_LOW_SPEED_LIMIT, low_speed_in_bytes_per_seond); + curl_easy_setopt(handle_, CURLOPT_LOW_SPEED_TIME, timeout_in_seconds); +} + +void Connection::SetTimeoutInMilliseconds(long milliseconds) { + curl_easy_setopt(handle_, CURLOPT_TIMEOUT_MS, milliseconds); +} + +void Connection::SetDebugCallback(const DebugCallback& callback) { + + debug_callback_ = callback; + + if (debug_callback_ != nullptr) { + curl_easy_setopt(handle_, CURLOPT_DEBUGFUNCTION, CurlDebugCallback); + curl_easy_setopt(handle_, CURLOPT_DEBUGDATA, this); + } + else { + curl_easy_setopt(handle_, CURLOPT_DEBUGFUNCTION, nullptr); + curl_easy_setopt(handle_, CURLOPT_DEBUGDATA, nullptr); + } +} + +void Connection::WillStart() { + + is_running_ = true; + ResetResponseStates(); +} + + +void Connection::ResetResponseStates() { + + request_body_read_length_ = 0; + result_ = CURL_LAST; + response_header_.clear(); + response_body_.clear(); +} + + +void Connection::DidFinish(CURLcode result) { + + is_running_ = false; + result_ = result; + + if (finished_callback_) { + finished_callback_(this->shared_from_this()); + } +} + + +long Connection::GetResponseCode() const { + + long response_code = 0; + curl_easy_getinfo(handle_, CURLINFO_RESPONSE_CODE, &response_code); + return response_code; +} + + +bool Connection::ReadBody(char* body, std::size_t expected_length, std::size_t& actual_length) { + + WriteConnectionLog(this) << "Read body to buffer with size " << expected_length << '.'; + + bool is_succeeded = false; + + if (read_body_callback_) { + is_succeeded = read_body_callback_(this->shared_from_this(), body, expected_length, actual_length); + } + else { + + std::size_t remain_length = request_body_.length() - request_body_read_length_; + actual_length = std::min(remain_length, expected_length); + + std::memcpy(body, request_body_.data() + request_body_read_length_, actual_length); + request_body_read_length_ += actual_length; + + is_succeeded = true; + } + + if (is_succeeded) { + WriteConnectionLog(this) << "Read body done with size " << actual_length << '.'; + } + else { + WriteConnectionLog(this) << "Read body failed."; + } + + return is_succeeded; +} + + +bool Connection::SeekBody(SeekOrigin origin, curl_off_t offset) { + + WriteConnectionLog(this) + << "Seek body from " + << (origin == SeekOrigin::End ? "end" : + (origin == SeekOrigin::Current ? "current" : "begin")) + << " to offset " << offset << '.'; + + bool is_succeeded = false; + + if (read_body_callback_) { + + if (seek_body_callback_) { + is_succeeded = seek_body_callback_(this->shared_from_this(), origin, offset); + } + } + else { + + std::size_t original_position = 0; + switch (origin) { + case SeekOrigin::Begin: + break; + case SeekOrigin::Current: + original_position = request_body_read_length_; + break; + case SeekOrigin::End: + original_position = request_body_.length(); + break; + default: + //Shouldn't reach here. + break; + } + + std::size_t new_read_length = original_position + offset; + if (new_read_length <= request_body_.length()) { + request_body_read_length_ = new_read_length; + is_succeeded = true; + } + } + + WriteConnectionLog(this) << "Seek body " << (is_succeeded ? "done" : "failed") << '.'; + + return is_succeeded; +} + + +bool Connection::WriteHeader(const char* header, std::size_t length) { + + WriteConnectionLog(this) << "Write header with size " << length << '.'; + + bool is_succeeded = false; + + if (write_header_callback_) { + is_succeeded = write_header_callback_(this->shared_from_this(), header, length); + } + else { + response_header_.append(header, length); + is_succeeded = true; + } + + WriteConnectionLog(this) << "Write header " << (is_succeeded ? "done" : "failed") << '.'; + + return is_succeeded; +} + + +bool Connection::WriteBody(const char* body, std::size_t length) { + + WriteConnectionLog(this) << "Write body with size " << length << '.'; + + bool is_succeeded = false; + + if (write_body_callback_) { + is_succeeded = write_body_callback_(this->shared_from_this(), body, length); + } + else { + response_body_.append(body, length); + is_succeeded = true; + } + + WriteConnectionLog(this) << "Write body " << (is_succeeded ? "done" : "failed") << '.'; + + return is_succeeded; +} + + +bool Connection::Progress(curl_off_t total_download, + curl_off_t current_download, + curl_off_t total_upload, + curl_off_t current_upload) { + + WriteConnectionLog(this) << "Progress meter. " + << current_download << " downloaded, " << total_download << " expected; " + << current_upload << " uploaded, " << total_upload << " expected."; + + bool is_succeeded = true; + + if (progress_callback_) { + + is_succeeded = progress_callback_(this->shared_from_this(), + total_download, + current_download, + total_upload, + current_upload); + } + + if (! is_succeeded) { + WriteConnectionLog(this) << "Aborted by progress meter."; + } + + return is_succeeded; +} + + +void Connection::Debug(curlion::Connection::DebugDataType data_type, const char *data, std::size_t size) { + + if (debug_callback_ != nullptr) { + debug_callback_(shared_from_this(), data_type, data, size); + } +} + + +size_t Connection::CurlReadBodyCallback(char* buffer, size_t size, size_t nitems, void* instream) { + Connection* connection = static_cast(instream); + std::size_t actual_read_length = 0; + bool is_succeeded = connection->ReadBody(buffer, size * nitems, actual_read_length); + return is_succeeded ? actual_read_length : CURL_READFUNC_ABORT; +} + +int Connection::CurlSeekBodyCallback(void* userp, curl_off_t offset, int origin) { + SeekOrigin seek_origin = SeekOrigin::Begin; + switch (origin) { + case SEEK_SET: + break; + case SEEK_CUR: + seek_origin = SeekOrigin::Current; + break; + case SEEK_END: + seek_origin = SeekOrigin::End; + default: + return 1; + } + Connection* connection = static_cast(userp); + bool is_succeeded = connection->SeekBody(seek_origin, offset); + return is_succeeded ? 0 : 1; +} + +size_t Connection::CurlWriteHeaderCallback(char* buffer, size_t size, size_t nitems, void* userdata) { + std::size_t length = size * nitems; + Connection* connection = static_cast(userdata); + bool is_succeeded = connection->WriteHeader(buffer, length); + return is_succeeded ? length : 0; +} + +size_t Connection::CurlWriteBodyCallback(char* ptr, size_t size, size_t nmemb, void* v) { + std::size_t length = size * nmemb; + Connection* connection = static_cast(v); + bool is_succeeded = connection->WriteBody(ptr, length); + return is_succeeded ? length : 0; +} + +int Connection::CurlProgressCallback(void *clientp, + curl_off_t dltotal, + curl_off_t dlnow, + curl_off_t ultotal, + curl_off_t ulnow) { + + Connection* connection = static_cast(clientp); + bool is_succeeded = connection->Progress(dltotal, dlnow, ultotal, ulnow); + return is_succeeded ? 0 : -1; +} + +int Connection::CurlDebugCallback(CURL* handle, + curl_infotype type, + char* data, + size_t size, + void* userptr) { + + DebugDataType data_type = DebugDataType::Information; + switch (type) { + case CURLINFO_TEXT: + break; + case CURLINFO_HEADER_IN: + data_type = DebugDataType::ReceivedHeader; + break; + case CURLINFO_HEADER_OUT: + data_type = DebugDataType::SentHeader; + case CURLINFO_DATA_IN: + data_type = DebugDataType::ReceivedBody; + break; + case CURLINFO_DATA_OUT: + data_type = DebugDataType::SentBody; + break; + case CURLINFO_SSL_DATA_IN: + data_type = DebugDataType::ReceivedSslData; + break; + case CURLINFO_SSL_DATA_OUT: + data_type = DebugDataType::SentSslData; + break; + default: + break; + } + + Connection* connection = static_cast(userptr); + connection->Debug(data_type, data, size); + return 0; +} + +} diff --git a/libraries/curlion/connection.h b/libraries/curlion/connection.h new file mode 100644 index 0000000..cdcadf8 --- /dev/null +++ b/libraries/curlion/connection.h @@ -0,0 +1,589 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace curlion { + +/** + Connection used to send request to remote peer and receive its response. + It supports variety of network protocols, such as SMTP, IMAP and HTTP etc. + + Before start a connection, some setter methods must be call to set required + options, sush as SetUrl to set the URL connect to, and SetRequestBody to set + the body sent to remote peer. + + There are two ways to start a connection. The simplest one is to call + Connection::Start, this method would not return until the connection finishes. + + The more complicated way is to call ConnectionManager::StartConnection to start + a non-blocking connection. You should call SetFinishedCallback to set a callback + in order to receive a notification when the connection finishes. + + No mater which way you choose, when a connection is finish, you can call getter + methods to get the result, such as GetResult to get the result code, and + GetResponseBody to get the body received from remote peer. + + For HTTP, there is a derived class HttpConnection provides setter and getter + methods specific to HTTP. + + This is a encapsulation against libcurl's easy handle. + */ +class Connection : public std::enable_shared_from_this { +public: + /** + Original position for seeking request body. + */ + enum class SeekOrigin { + /** + Begin of request body. + */ + Begin, + + /** + Current position of request body. + */ + Current, + + /** + End of request body. + */ + End, + }; + + /** + Callback prototype for seeking request body. + + @param origin + Original position for seeking. + + @param offset + Offset related to original position. + + @return + Whether the seeking is succeeded. + */ + typedef std::function< + bool(const std::shared_ptr& connection, + SeekOrigin origin, + curl_off_t offset) + > SeekBodyCallback; + + /** + Calback prototype for reading request body. + + @param connection + The Connection instance. + + @param body + Body's buffer. Data need to be copied to here. + + @param expected_length + How many bytes are expected. Is is also the length of buffer. + + @param actual_length + Return how many bytes are copied to buffer. It could be less than expected_length. + Return 0 means whole request body is read. + + @return Whether the reading is succeeded. Return false would abort the connection. + */ + typedef std::function< + bool(const std::shared_ptr& connection, + char* body, + std::size_t expected_length, + std::size_t& actual_length) + > ReadBodyCallback; + + /** + Callback prototype for writing response header. + + @param connection + The Connection instance. + + @param header + Buffer contains header data. + + @param length + Buffer's length. + + @return + Whether the writing is succeeded. Return false would abort the connection. + */ + typedef std::function< + bool(const std::shared_ptr& connection, + const char* header, + std::size_t length) + > WriteHeaderCallback; + + /** + Callback prototype for writing response body. + + @param connection + The Connection instance. + + @param body + Buffer contains body data. + + @param length + Buffer's length + + @return + Whether the writing is succeeded. Return false would abort the connection. + */ + typedef std::function< + bool(const std::shared_ptr& connection, + const char* body, + std::size_t length) + > WriteBodyCallback; + + /** + Callback prototype for progress meter. + + @param connection + The Connection instance. + + @param total_download + The total number of bytes expected to be downloaded. + + @param current_download + The number of bytes downloaded so far. + + @param total_upload + The total number of bytes expected to be uploaded. + + @param current_upload + The number of bytes uploaded so far. + + @return + Return false would abort the connection. + */ + typedef std::function< + bool(const std::shared_ptr& connection, + curl_off_t total_download, + curl_off_t current_download, + curl_off_t total_upload, + curl_off_t current_upload) + > ProgressCallback; + + /** + Debug data type in debug callback. + */ + enum class DebugDataType { + + /** + The data is informational text. + */ + Information, + + /** + The data is header received from the peer. + */ + ReceivedHeader, + + /** + The data is header sent to the peer. + */ + SentHeader, + + /** + The data is body received from the peer. + */ + ReceivedBody, + + /** + The data is body sent to the peer. + */ + SentBody, + + /** + The data is SSL data received from the peer. + */ + ReceivedSslData, + + /** + The data is SSL data sent to the peer. + */ + SentSslData, + }; + + /** + Callback prototype for receiving debug information. + + @param connection + The connection instance. + + @param data_type + The type of data. + + @param data + The debug information data. + + @param size + The size of data. + */ + typedef std::function< + void(const std::shared_ptr& connection, + DebugDataType data_type, + const char* data, + std::size_t size) + > DebugCallback; + + /** + Callback prototype for connection finished. + + @param connection + The Connection instance. + */ + typedef std::function& connection)> FinishedCallback; + +public: + /** + Construct the Connection instance. + */ + Connection(); + + /** + Destruct the Connection instance. + + Destructing a running connection will abort it immediately. + */ + virtual ~Connection(); + + /** + Start the connection in blocking manner. + + This method does not return a value, you should call GetResult method to get the result. + If the connection is already started, this method takes no effects. + Use ConnectionManager to start connections if you wish a non-blocking manner. + */ + void Start(); + + /** + Reset all options to default. + + Calling this method while the connection is running takes no effect. + */ + void ResetOptions(); + + /** + Set whether to print detail information about connection to stderr. + + The default is false. + */ + void SetVerbose(bool verbose); + + /** + Set the URL used in connection. + */ + void SetUrl(const std::string& url); + + /** + Set the proxy used in connection. + */ + void SetProxy(const std::string& proxy); + + /** + Set authenticated account for proxy. + */ + void SetProxyAccount(const std::string& username, const std::string& password); + + /** + Set whether to connect to server only, don't tranfer any data. + + The default is false. + */ + void SetConnectOnly(bool connect_only); + + /** + Set custom host name to IP address resolve items to DNS cache. + + The first element of each item is a host and port pair, which must be in HOST:PORT format. + The second element of each item is an IP address, can be either IPv4 or IPv6 format. If IP + address is empty, this item is considered to be removed from DNS cache. + + Note that resolve items are applied to DNS cache only after the connection starts. That is, + later set resolve items will replace earlier set items. And once an item is added to DNS cache, + the only way to remove it is setting another item with the same host and port, along with an + empty IP address. + + This option is equal to set CURLOPT_RESOLVE option to libcurl. + */ + void SetDnsResolveItems(const std::multimap& resolve_items); + + /** + Set whether to verify the peer's SSL certificate. + + The default is true. + */ + void SetVerifyCertificate(bool verify); + + /** + Set whether to verify the certificate's name against host. + + The default is true. + */ + void SetVerifyHost(bool verify); + + /** + Set the path of file holding one or more certificates to verify the peer with. + */ + void SetCertificateFilePath(const std::string& file_path); + + /** + Set the body for request. + + Note that the body would be ignored once a callable read body callback is set. + */ + void SetRequestBody(const std::string& body) { + request_body_ = body; + } + + /** + Set whether to receive response body. + + This option must be set to false for those responses without body, + otherwise the connection would be blocked. + + The default is true. + */ + void SetReceiveBody(bool receive_body); + + /** + Set whether to enable the progress meter. + + When progress meter is disalbed, the progress callback would not be called. + + The default is false. + */ + void SetEnableProgress(bool enable); + + /** + Set timeout for the connect phase. + + Set to 0 to switch to the default timeout 300 seconds. + */ + void SetConnectTimeoutInMilliseconds(long milliseconds); + + /** + Set timeout for how long the connection can be idle. + + This option is a shortcut to SetLowSpeedTimeout, which low speed is set to 1. + */ + void SetIdleTimeoutInSeconds(long seconds) { + SetLowSpeedTimeout(1, seconds); + } + + /** + Set timeout for low speed transfer. + + If the average transfer speed is below specified speed for specified duration, the connection is + considered timeout. + + The default is no timeout. Set 0 to any one of the parameters to turn off the timeout. + + This option is equal to set both CURLOPT_LOW_SPEED_LIMIT and CURLOPT_LOW_SPEED_TIME options + to libcurl. Note that the timeout may be broken in some cases due to internal implementation + of libcurl. + */ + void SetLowSpeedTimeout(long low_speed_in_bytes_per_seond, long timeout_in_seconds); + + /** + Set timeout for the whole connection. + + The default is no timeout. Set 0 to turn off the timeout. + */ + void SetTimeoutInMilliseconds(long milliseconds); + + /** + Set callback for reading request body. + + If callback is callable, the request body set by SetRequestBody would be ignored. + */ + void SetReadBodyCallback(const ReadBodyCallback& callback) { + read_body_callback_ = callback; + } + + /** + Set callback for seeking request body. + + This callback is used to re-position the reading pointer while re-sending is needed. + It should be provided along with read body callback. + */ + void SetSeekBodyCallback(const SeekBodyCallback& callback) { + seek_body_callback_ = callback; + } + + /** + Set callback for writing response header. + + If callback is callable, GetResponseHeader would return empty string. + */ + void SetWriteHeaderCallback(const WriteHeaderCallback& callback) { + write_header_callback_ = callback; + } + + /** + Set callback for writing response body. + + If callback is callable, GetResponseBody would return empty string. + */ + void SetWriteBodyCallback(const WriteBodyCallback& callback) { + write_body_callback_ = callback; + } + + /** + Set callback for progress meter. + */ + void SetProgressCallback(const ProgressCallback& callback) { + progress_callback_ = callback; + } + + /** + Set callback for receiving debug information. + + SetVerbose method must be called with true to enable the debug callback. If a non-null callback + is set as the debug callback, all verbose output would be sent to debug callback instead of stderr. + */ + void SetDebugCallback(const DebugCallback& callback); + + /** + Set callback for connection finished. + + Use this callback to get informed when the connection finished. + */ + void SetFinishedCallback(const FinishedCallback& callback) { + finished_callback_ = callback; + } + + /** + Get the result code. + + An undefined value would be returned if the connection is not yet finished. + */ + CURLcode GetResult() const { + return result_; + } + + /** + Get the last response code. + + For HTTP, response code is the HTTP status code. + + The return value is undefined if the connection is not yet finished. + */ + long GetResponseCode() const; + + /** + Get response header. + + Note that empty string would be returned if a callable write header callback is set. + Undefined string content would be returned if the connection is not yet finished. + */ + const std::string& GetResponseHeader() const { + return response_header_; + } + + /** + Get response body. + + Note that empty string would be returned if a callable write body callback is set. + Undefined string content would be returned if the connection is not yet finished. + */ + const std::string& GetResponseBody() const { + return response_body_; + } + + /** + Get the underlying easy handle. + */ + CURL* GetHandle() const { + return handle_; + } + +//Methods be called from ConnectionManager. +private: + void WillStart(); + void DidFinish(CURLcode result); + +protected: + /** + Reset response states to default. + + This method is called when the connection needs to reset all response states, such as reseting + the content of response body to empty. This usually happens when the connection restarts. + + Derived classes can override this method the reset their response states, and they must call + the same method of base class. + */ + virtual void ResetResponseStates(); + + /** + Reset resources associated with curl options to default. + + This method is called when the connection needs to reset all option resources, such as reseting + the content of request body to empty. + + Derived classes can override this method to reset their option resources, and they must + call the same method of base class. + */ + virtual void ResetOptionResources(); + +private: + static size_t CurlReadBodyCallback(char* buffer, size_t size, size_t nitems, void* instream); + static int CurlSeekBodyCallback(void* userp, curl_off_t offset, int origin); + static size_t CurlWriteHeaderCallback(char* buffer, size_t size, size_t nitems, void* userdata); + static size_t CurlWriteBodyCallback(char* ptr, size_t size, size_t nmemb, void* v); + static int CurlProgressCallback(void *clientp, + curl_off_t dltotal, + curl_off_t dlnow, + curl_off_t ultotal, + curl_off_t ulnow); + static int CurlDebugCallback(CURL* handle, + curl_infotype type, + char* data, + size_t size, + void* userptr); + + void SetInitialOptions(); + void ReleaseDnsResolveItems(); + + bool ReadBody(char* body, std::size_t expected_length, std::size_t& actual_length); + bool SeekBody(SeekOrigin origin, curl_off_t offset); + bool WriteHeader(const char* header, std::size_t length); + bool WriteBody(const char* body, std::size_t length); + bool Progress(curl_off_t total_download, + curl_off_t current_download, + curl_off_t total_upload, + curl_off_t current_upload); + void Debug(DebugDataType data_type, const char* data, std::size_t size); + +private: + Connection(const Connection&) = delete; + Connection& operator=(const Connection&) = delete; + +private: + CURL* handle_; + bool is_running_; + + curl_slist* dns_resolve_items_; + std::string request_body_; + std::size_t request_body_read_length_; + ReadBodyCallback read_body_callback_; + SeekBodyCallback seek_body_callback_; + WriteHeaderCallback write_header_callback_; + WriteBodyCallback write_body_callback_; + ProgressCallback progress_callback_; + DebugCallback debug_callback_; + FinishedCallback finished_callback_; + CURLcode result_; + std::string response_header_; + std::string response_body_; + + friend class ConnectionManager; +}; + +} diff --git a/libraries/curlion/connection_manager.cpp b/libraries/curlion/connection_manager.cpp new file mode 100644 index 0000000..a3af14e --- /dev/null +++ b/libraries/curlion/connection_manager.cpp @@ -0,0 +1,284 @@ +#include "connection_manager.h" +#include "connection.h" +#include "error.h" +#include "log.h" +#include "socket_factory.h" +#include "socket_watcher.h" +#include "timer.h" + +namespace curlion { + +static inline LoggerProxy WriteManagerLog(void* manager_idenditifier) { + return Log() << "Manager(" << manager_idenditifier << "): "; +} + + +ConnectionManager::ConnectionManager(const std::shared_ptr& socket_factory, + const std::shared_ptr& socket_watcher, + const std::shared_ptr& timer) : + socket_factory_(socket_factory), + socket_watcher_(socket_watcher), + timer_(timer) { + + multi_handle_ = curl_multi_init(); + curl_multi_setopt(multi_handle_, CURLMOPT_TIMERFUNCTION, CurlTimerCallback); + curl_multi_setopt(multi_handle_, CURLMOPT_TIMERDATA, this); + curl_multi_setopt(multi_handle_, CURLMOPT_SOCKETFUNCTION, CurlSocketCallback); + curl_multi_setopt(multi_handle_, CURLMOPT_SOCKETDATA, this); +} + + +ConnectionManager::~ConnectionManager() { + + +} + + +std::error_condition ConnectionManager::StartConnection(const std::shared_ptr& connection) { + + std::error_condition error; + + CURL* easy_handle = connection->GetHandle(); + + auto iterator = running_connections_.find(easy_handle); + if (iterator != running_connections_.end()) { + WriteManagerLog(this) << "Try to start an already running connection(" << connection.get() << "). Ignored."; + return error; + } + + WriteManagerLog(this) << "Start a connection(" << connection.get() << ")."; + + if (socket_factory_ != nullptr) { + curl_easy_setopt(easy_handle, CURLOPT_OPENSOCKETFUNCTION, CurlOpenSocketCallback); + curl_easy_setopt(easy_handle, CURLOPT_OPENSOCKETDATA, this); + curl_easy_setopt(easy_handle, CURLOPT_CLOSESOCKETFUNCTION, CurlCloseSocketCallback); + curl_easy_setopt(easy_handle, CURLOPT_CLOSESOCKETDATA, this); + } + else { + curl_easy_setopt(easy_handle, CURLOPT_OPENSOCKETFUNCTION, nullptr); + curl_easy_setopt(easy_handle, CURLOPT_CLOSESOCKETFUNCTION, nullptr); + } + + connection->WillStart(); + + iterator = running_connections_.insert(std::make_pair(easy_handle, connection)).first; + + CURLMcode result = curl_multi_add_handle(multi_handle_, easy_handle); + if (result != CURLM_OK) { + WriteManagerLog(this) << "curl_multi_add_handle failed with result: " << result << '.'; + running_connections_.erase(iterator); + error.assign(result, CurlMultiErrorCategory()); + } + + return error; +} + + +std::error_condition ConnectionManager::AbortConnection(const std::shared_ptr& connection) { + + std::error_condition error; + + CURL* easy_handle = connection->GetHandle(); + + auto iterator = running_connections_.find(easy_handle); + if (iterator == running_connections_.end()) { + WriteManagerLog(this) << "Try to abort a not running connection(" << easy_handle << "). Ignored."; + return error; + } + + WriteManagerLog(this) << "Abort a connection(" << easy_handle << ")."; + + running_connections_.erase(iterator); + + CURLMcode result = curl_multi_remove_handle(multi_handle_, easy_handle); + if (result != CURLM_OK) { + WriteManagerLog(this) << "curl_multi_remove_handle failed with result: " << result << '.'; + error.assign(result, CurlMultiErrorCategory()); + } + + return error; +} + + +curl_socket_t ConnectionManager::OpenSocket(curlsocktype socket_type, curl_sockaddr* address) { + + WriteManagerLog(this) << "Open socket for " + << "type " << socket_type << "; " + << "address family " << address->family << ", " + << "socket type " << address->socktype << ", " + << "protocol " << address->protocol << '.'; + + curl_socket_t socket = socket_factory_->Open(socket_type, address); + + if (socket != CURL_SOCKET_BAD) { + WriteManagerLog(this) << "Socket(" << socket << ") is opened."; + } + else { + WriteManagerLog(this) << "Open socket failed."; + } + + return socket; +} + + +bool ConnectionManager::CloseSocket(curl_socket_t socket) { + + WriteManagerLog(this) << "Close socket(" << socket << ")."; + + bool is_succeeded = socket_factory_->Close(socket); + + if (is_succeeded) { + WriteManagerLog(this) << "Socket(" << socket << ") is closed."; + } + else { + WriteManagerLog(this) << "Close socket(" << socket << ") failed."; + } + + return is_succeeded; +} + + +void ConnectionManager::SetTimer(long timeout_ms) { + + WriteManagerLog(this) << "Set timer for " << timeout_ms << " milliseconds."; + + timer_->Stop(); + + if (timeout_ms >= 0) { + timer_->Start(timeout_ms, std::bind(&ConnectionManager::TimerTriggered, this)); + } +} + + +void ConnectionManager::TimerTriggered() { + + WriteManagerLog(this) << "Timer triggered."; + + int running_count = 0; + curl_multi_socket_action(multi_handle_, CURL_SOCKET_TIMEOUT, 0, &running_count); + CheckFinishedConnections(); +} + + +void ConnectionManager::WatchSocket(curl_socket_t socket, int action, void* socket_pointer) { + + static void* const kIsNotNewSocketTag = reinterpret_cast(1); + + //Ensure that StopWatching won't be called with a new socket that never watched. + if (socket_pointer == kIsNotNewSocketTag) { + WriteManagerLog(this) << "Socket(" << socket << ") is changed. Stop watching it."; + socket_watcher_->StopWatching(socket); + } + else { + WriteManagerLog(this) << "Socket(" << socket << ") is added."; + curl_multi_assign(multi_handle_, socket, kIsNotNewSocketTag); + } + + if (action == CURL_POLL_REMOVE) { + WriteManagerLog(this) << "Socket(" << socket << ") is removed."; + return; + } + + SocketWatcher::Event event = SocketWatcher::Event::Read; + switch (action) { + case CURL_POLL_IN: + break; + + case CURL_POLL_OUT: + event = SocketWatcher::Event::Write; + break; + + case CURL_POLL_INOUT: + event = SocketWatcher::Event::ReadWrite; + break; + } + + WriteManagerLog(this) << "Watch socket(" << socket << ") for " + << (event == SocketWatcher::Event::Read ? "read" : + (event == SocketWatcher::Event::Write ? "write" : "read/write")) + << " event."; + + socket_watcher_->Watch(socket, event, std::bind(&ConnectionManager::SocketEventTriggered, + this, + std::placeholders::_1, + std::placeholders::_2)); +} + + +void ConnectionManager::SocketEventTriggered(curl_socket_t socket, bool can_write) { + + WriteManagerLog(this) << "Socket(" << socket << ") " << (can_write ? "write" : "read") << " event triggered."; + + int action = can_write ? CURL_CSELECT_OUT : CURL_CSELECT_IN; + int running_count = 0; + curl_multi_socket_action(multi_handle_, socket, action, &running_count); + CheckFinishedConnections(); +} + + +void ConnectionManager::CheckFinishedConnections() { + + while (true) { + + int msg_count = 0; + CURLMsg* msg = curl_multi_info_read(multi_handle_, &msg_count); + if (msg == nullptr) { + break; + } + + if (msg->msg == CURLMSG_DONE) { + + curl_multi_remove_handle(multi_handle_, msg->easy_handle); + + auto iterator = running_connections_.find(msg->easy_handle); + if (iterator != running_connections_.end()) { + + auto connection = iterator->second; + running_connections_.erase(iterator); + + WriteManagerLog(this) + << "Connection(" << connection.get() << ") is finished with result " << msg->data.result << '.'; + + connection->DidFinish(msg->data.result); + } + } + } +} + + +curl_socket_t ConnectionManager::CurlOpenSocketCallback(void* clientp, + curlsocktype socket_type, + curl_sockaddr* address) { + + ConnectionManager* manager = static_cast(clientp); + return manager->OpenSocket(socket_type, address); +} + + +int ConnectionManager::CurlCloseSocketCallback(void* clientp, curl_socket_t socket) { + + ConnectionManager* manager = static_cast(clientp); + return manager->CloseSocket(socket); +} + + +int ConnectionManager::CurlTimerCallback(CURLM* multi_handle, long timeout_ms, void* user_pointer) { + + ConnectionManager* manager = static_cast(user_pointer); + manager->SetTimer(timeout_ms); + return 0; +} + + +int ConnectionManager::CurlSocketCallback(CURL* easy_handle, + curl_socket_t socket, + int action, + void* user_pointer, + void* socket_pointer) { + + ConnectionManager* manager = static_cast(user_pointer); + manager->WatchSocket(socket, action, socket_pointer); + return 0; +} + +} diff --git a/libraries/curlion/connection_manager.h b/libraries/curlion/connection_manager.h new file mode 100644 index 0000000..d14ab36 --- /dev/null +++ b/libraries/curlion/connection_manager.h @@ -0,0 +1,119 @@ +#pragma once + +#include +#include +#include +#include + +namespace curlion { + +class Connection; +class SocketFactory; +class SocketWatcher; +class Timer; + +/** + ConnectionManager manages connections' executions, including starting or + aborting connection. + + Call SartConnection method to start a connection. Call AbortConnection + method to stop a running connection. + + This class is not thread safe. + + This is a encapsulation against libcurl's multi handle. + */ +class ConnectionManager { +public: + /** + Construct the ConnectionManager instance. + */ + ConnectionManager(const std::shared_ptr& socket_factory, + const std::shared_ptr& socket_watcher, + const std::shared_ptr& timer); + + /** + Destruct the ConnectionManager instance. + + When this method got called, all running connections would be aborted. + */ + ~ConnectionManager(); + + /** + Start a connection. + + @param connection + The connection to start. Must not be nullptr. + + @return + Return an error on failure. + + This method will retain the connection, until it is finished or aborted. + + It is OK to call this method with the same Connection instance multiple times. + Nothing changed if the connection is running; Otherwise it will be restarted. + */ + std::error_condition StartConnection(const std::shared_ptr& connection); + + /** + Abort a running connection. + + @param connection + The connection to abort. Must not be nullptr. + + @return + Return an error on failure. + + Note that the connection's finished callback would not be triggered when it is + aborted. + + Is is OK to call this methods while the connection is not running, nothing + would happend. + + If this method fails, the connection is in an unknown condition, it should be + abondaned and never be reused again. + */ + std::error_condition AbortConnection(const std::shared_ptr& connection); + + /** + Get the underlying multi handle. + */ + CURLM* GetHandle() const { + return multi_handle_; + } + +private: + static curl_socket_t CurlOpenSocketCallback(void* clientp, curlsocktype socket_type, curl_sockaddr* address); + static int CurlCloseSocketCallback(void* clientp, curl_socket_t socket); + static int CurlTimerCallback(CURLM* multi_handle, long timeout_ms, void* user_pointer); + static int CurlSocketCallback(CURL* easy_handle, + curl_socket_t socket, + int action, + void* user_pointer, + void* socket_pointer); + + curl_socket_t OpenSocket(curlsocktype socket_type, curl_sockaddr* address); + bool CloseSocket(curl_socket_t socket); + + void SetTimer(long timeout_ms); + void TimerTriggered(); + + void WatchSocket(curl_socket_t socket, int action, void* socket_pointer); + void SocketEventTriggered(curl_socket_t socket, bool can_write); + + void CheckFinishedConnections(); + +private: + ConnectionManager(const ConnectionManager&) = delete; + ConnectionManager& operator=(const ConnectionManager&) = delete; + +private: + std::shared_ptr socket_factory_; + std::shared_ptr socket_watcher_; + std::shared_ptr timer_; + + CURLM* multi_handle_; + std::map> running_connections_; +}; + +} diff --git a/libraries/curlion/curlion.h b/libraries/curlion/curlion.h new file mode 100644 index 0000000..90572f7 --- /dev/null +++ b/libraries/curlion/curlion.h @@ -0,0 +1,10 @@ +#pragma once + +#include "connection.h" +#include "connection_manager.h" +#include "error.h" +#include "http_connection.h" +#include "http_form.h" +#include "socket_factory.h" +#include "socket_watcher.h" +#include "timer.h" diff --git a/libraries/curlion/error.h b/libraries/curlion/error.h new file mode 100644 index 0000000..fc94f8d --- /dev/null +++ b/libraries/curlion/error.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +namespace curlion { + +inline const std::error_category& CurlMultiErrorCategory() { + + class CurlMultiErrorCategory : public std::error_category { + public: + const char* name() const noexcept override { + return "CURLMcode"; + } + + std::string message(int condition) const override { + return std::string(); + } + }; + + static CurlMultiErrorCategory category; + return category; +} + + +inline const std::error_category& CurlFormErrorCategory() { + + class CurlFormErrorCategory : public std::error_category { + public: + const char* name() const noexcept override { + return "CURLFORMcode"; + } + + std::string message(int condition) const override { + return std::string(); + } + }; + + static CurlFormErrorCategory category; + return category; +} + + +} diff --git a/libraries/curlion/http_connection.cpp b/libraries/curlion/http_connection.cpp new file mode 100644 index 0000000..9e3c428 --- /dev/null +++ b/libraries/curlion/http_connection.cpp @@ -0,0 +1,162 @@ +#include "http_connection.h" +#include +#include "http_form.h" + +namespace curlion { + +static std::string MakeHttpHeaderLine(const std::string& field, const std::string& value); +static std::vector SplitString(const std::string& string, const std::string& delimiter); + + +HttpConnection::HttpConnection() : + request_headers_(nullptr), + has_parsed_response_headers_(false) { + +} + + +HttpConnection::~HttpConnection() { + ReleaseRequestHeaders(); +} + + +void HttpConnection::SetUsePost(bool use_post) { + curl_easy_setopt(GetHandle(), CURLOPT_POST, use_post); +} + + +void HttpConnection::SetRequestHeaders(const std::multimap& headers) { + + ReleaseRequestHeaders(); + + for (auto& each_header : headers) { + + std::string each_header_line = MakeHttpHeaderLine(each_header.first, each_header.second); + request_headers_ = curl_slist_append(request_headers_, each_header_line.c_str()); + } + + curl_easy_setopt(GetHandle(), CURLOPT_HTTPHEADER, request_headers_); +} + + +void HttpConnection::AddRequestHeader(const std::string& field, const std::string& value) { + + std::string header_line = MakeHttpHeaderLine(field, value); + request_headers_ = curl_slist_append(request_headers_, header_line.c_str()); + + curl_easy_setopt(GetHandle(), CURLOPT_HTTPHEADER, request_headers_); +} + + +void HttpConnection::SetRequestForm(const std::shared_ptr& form) { + + form_ = form; + + curl_httppost* handle = nullptr; + if (form_ != nullptr) { + handle = form_->GetHandle(); + } + + curl_easy_setopt(GetHandle(), CURLOPT_HTTPPOST, handle); +} + + +void HttpConnection::SetAutoRedirect(bool auto_redirect) { + curl_easy_setopt(GetHandle(), CURLOPT_FOLLOWLOCATION, auto_redirect); +} + + +void HttpConnection::SetMaxAutoRedirectCount(long count) { + curl_easy_setopt(GetHandle(), CURLOPT_MAXREDIRS, count); +} + + +const std::multimap& HttpConnection::GetResponseHeaders() const { + + if (! has_parsed_response_headers_) { + ParseResponseHeaders(); + has_parsed_response_headers_ = true; + } + + return response_headers_; +} + + +void HttpConnection::ParseResponseHeaders() const { + + std::vector lines = SplitString(GetResponseHeader(), "\r\n"); + + std::vector key_value_pair; + for (auto& each_line : lines) { + + key_value_pair = SplitString(each_line, ": "); + if (key_value_pair.size() < 2) { + continue; + } + + response_headers_.insert(std::make_pair(key_value_pair.at(0), key_value_pair.at(1))); + } +} + + +void HttpConnection::ResetResponseStates() { + + Connection::ResetResponseStates(); + + has_parsed_response_headers_ = false; + response_headers_.clear(); +} + + +void HttpConnection::ResetOptionResources() { + + Connection::ResetOptionResources(); + + ReleaseRequestHeaders(); + form_.reset(); +} + + +void HttpConnection::ReleaseRequestHeaders() { + + if (request_headers_ != nullptr) { + curl_slist_free_all(request_headers_); + request_headers_ = nullptr; + } +} + + +static std::string MakeHttpHeaderLine(const std::string& field, const std::string& value) { + + std::string header_line = field; + header_line.append(": "); + header_line.append(value); + + return header_line; +} + + +static std::vector SplitString(const std::string& string, const std::string& delimiter) { + + std::vector splitted_strings; + + std::size_t begin_index = 0; + std::size_t end_index = 0; + + while (begin_index < string.length()) { + + end_index = string.find(delimiter, begin_index); + + if (end_index == std::string::npos) { + end_index = string.length(); + } + + splitted_strings.push_back(string.substr(begin_index, end_index - begin_index)); + + begin_index = end_index + delimiter.length(); + } + + return splitted_strings; +} + +} \ No newline at end of file diff --git a/libraries/curlion/http_connection.h b/libraries/curlion/http_connection.h new file mode 100644 index 0000000..b0fe07c --- /dev/null +++ b/libraries/curlion/http_connection.h @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include "connection.h" + +namespace curlion { + +class HttpForm; + +/** + HttpConnection used to send HTTP request and received HTTP response. + + This class dervies from Connection, adds some setter and getter methods speicfic to HTTP. + */ +class HttpConnection : public Connection { +public: + /** + Construct the HttpConnection instance. + */ + HttpConnection(); + + /** + Destruct the HttpConnection instance. + */ + ~HttpConnection(); + + /** + Set whether to use HTTP POST method. + + The default is false, means using GET method. + */ + void SetUsePost(bool use_post); + + /** + Set HTTP request headers. + + The new headers would replace all headers previously set. + */ + void SetRequestHeaders(const std::multimap& headers); + + /** + Add a single HTTP request header. + */ + void AddRequestHeader(const std::string& field, const std::string& value); + + /** + Set a request form for HTTP POST. + + The default is nullptr. + */ + void SetRequestForm(const std::shared_ptr& form); + + /** + Set whether to auto-redirect when received HTTP 3xx response. + + Use SetMaxAutoRedirectCount to limit the redirction count. + + The default is false. + */ + void SetAutoRedirect(bool auto_redirect); + + /** + Set maximum number of auto-redirects allowed. + + Set to 0 to forbidden any redirect. + + The default is -1, meas no redirect limits. + */ + void SetMaxAutoRedirectCount(long count); + + /** + Get HTTP response headers. + + This is a wrapper method for GetResponseHeader, parses the raw header string to key value pairs. + Note that when auto-redirection is enabled, all headers in multiple responses would be contained. + */ + const std::multimap& GetResponseHeaders() const; + +protected: + void ResetResponseStates() override; + void ResetOptionResources() override; + +private: + void ParseResponseHeaders() const; + void ReleaseRequestHeaders(); + +private: + curl_slist* request_headers_; + std::shared_ptr form_; + mutable bool has_parsed_response_headers_; + mutable std::multimap response_headers_; +}; + +} \ No newline at end of file diff --git a/libraries/curlion/http_form.cpp b/libraries/curlion/http_form.cpp new file mode 100644 index 0000000..62e64b9 --- /dev/null +++ b/libraries/curlion/http_form.cpp @@ -0,0 +1,86 @@ +#include "http_form.h" + +namespace curlion { + +static std::vector CreateOptions(const std::shared_ptr& part); + +HttpForm::~HttpForm() { + + curl_formfree(handle_); +} + + +std::error_condition HttpForm::AddPart(const std::shared_ptr& part) { + + std::error_condition error; + + auto options = CreateOptions(part); + auto result = curl_formadd(&handle_, &handle_last_, CURLFORM_ARRAY, options.data(), CURLFORM_END); + + if (result == CURL_FORMADD_OK) { + parts_.push_back(part); + } + else { + error.assign(result, CurlFormErrorCategory()); + } + + return error; +} + + +static std::vector CreateOptions(const std::shared_ptr& part) { + + std::vector options; + + curl_forms name_option; + name_option.option = CURLFORM_PTRNAME; + name_option.value = part->name.c_str(); + options.push_back(name_option); + + const auto& content = part->content; + if ((! content.empty()) || (part->files.empty())) { + + curl_forms content_option; + content_option.option = CURLFORM_PTRCONTENTS; + content_option.value = content.c_str(); + options.push_back(content_option); + + curl_forms content_length_option; + content_length_option.option = CURLFORM_CONTENTSLENGTH; + content_length_option.value = reinterpret_cast(content.length()); + options.push_back(content_length_option); + } + else { + + for (const auto& each_file : part->files) { + + curl_forms path_option; + path_option.option = CURLFORM_FILE; + path_option.value = each_file->path.c_str(); + options.push_back(path_option); + + if (! each_file->name.empty()) { + curl_forms name_option; + name_option.option = CURLFORM_FILENAME; + name_option.value = each_file->name.c_str(); + options.push_back((name_option)); + } + + if (! each_file->content_type.empty()) { + curl_forms content_type_option; + content_type_option.option = CURLFORM_CONTENTTYPE; + content_type_option.value = each_file->content_type.c_str(); + options.push_back(content_type_option); + } + } + } + + curl_forms end_option; + end_option.option = CURLFORM_END; + end_option.value = nullptr; + options.push_back(end_option); + + return options; +} + +} \ No newline at end of file diff --git a/libraries/curlion/http_form.h b/libraries/curlion/http_form.h new file mode 100644 index 0000000..7dccf44 --- /dev/null +++ b/libraries/curlion/http_form.h @@ -0,0 +1,130 @@ +#pragma once + +#include +#include +#include +#include +#include "error.h" + +namespace curlion { + +/** + HttpForm used to build a multi-part form request content. + + The instance of this class is used in HttpConnection::SetRequestForm method. + */ +class HttpForm { +public: + /** + File represents information of a file in a part. + */ + class File { + public: + /** + Construct a empty file. + */ + File() { } + + /** + Construct a file with specified path. + + @param path + Path of the file. + */ + File(const std::string& path) : path(path) { } + + /** + Path of the file. + */ + std::string path; + + /** + Name of the file. + + If this field is empty, the name in path would be used. + */ + std::string name; + + /** + Content type of the file. + + If this field is empty, the content type is determinated by the content of file. + */ + std::string content_type; + }; + + /** + Part represents information of a single part in a multi-part form. + */ + class Part { + public: + /** + Construct an empty part. + */ + Part() { } + + /** + Construct a part with specified name and content. + + @param name + Name of the part. + + @param content + Content of the part. + */ + Part(const std::string& name, const std::string& content) : name(name), content(content) { } + + /** + Name of the part. + */ + std::string name; + + /** + Content of the part. + */ + std::string content; + + /** + Files in the part. + + Note that files and content can not coexist in a part, and content has a higher priority + than files. That is, if content is not empty, all files are ignored. + */ + std::vector> files; + }; + +public: + /** + Destruct the HttpForm instance. + */ + ~HttpForm(); + + /** + Add a part to the form. + + @param part + The part is added, must not be nullptr. + + @return + Return an error on failure. + */ + std::error_condition AddPart(const std::shared_ptr& part); + + /** + Get the handle of the form. + + @return + The curl_httppost* handle of the form. + */ + curl_httppost* GetHandle() const { + return handle_; + } + +private: + curl_httppost* handle_ = nullptr; + curl_httppost* handle_last_ = nullptr; + + std::vector> parts_; +}; + +} \ No newline at end of file diff --git a/libraries/curlion/log.h b/libraries/curlion/log.h new file mode 100644 index 0000000..d8911bf --- /dev/null +++ b/libraries/curlion/log.h @@ -0,0 +1,81 @@ +#pragma once + +/** + CURLION_VERBOSE macro controls whether to print debug information to stdout. + Enable it by changing its value to none zero. + + Be aware of that enabling this macro would produce much output that flood the console easily. + So it should be used for debug purpose only. + + This macro effects only when NDEBUG macro is not defined. + */ +#define CURLION_VERBOSE 0 + +#if (! defined(NDEBUG)) && (CURLION_VERBOSE) +#include +#include +#include +#endif + +namespace curlion { + +#if (! defined(NDEBUG)) && (CURLION_VERBOSE) + +class Logger { +public: + void Write(const std::string& log) { + std::cout << "curlion> " << log << std::endl; + } +}; + + +class LoggerProxy { +public: + LoggerProxy(const std::shared_ptr& logger) : logger_(logger), stream_(new std::ostringstream()) { + + } + + LoggerProxy(LoggerProxy&& other) : logger_(std::move(other.logger_)), stream_(std::move(other.stream_)) { + + } + + ~LoggerProxy() { + + if ( (logger_ != nullptr) && (stream_ != nullptr) ) { + logger_->Write(stream_->str()); + } + } + + template + LoggerProxy operator<<(const Type& value) { + *stream_ << value; + return std::move(*this); + } + +private: + std::shared_ptr logger_; + std::unique_ptr stream_; +}; + + +inline LoggerProxy Log() { + return LoggerProxy(std::make_shared()); +} + +#else + +class LoggerProxy { +public: + template + LoggerProxy operator<<(const Type& value) { + return *this; + } +}; + +inline LoggerProxy Log() { + return LoggerProxy(); +} + +#endif + +} \ No newline at end of file diff --git a/libraries/curlion/socket_factory.h b/libraries/curlion/socket_factory.h new file mode 100644 index 0000000..99cd691 --- /dev/null +++ b/libraries/curlion/socket_factory.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +namespace curlion { + +/** + SocketFactory is an interface used by Connection to open and close sockets. + */ +class SocketFactory { +public: + SocketFactory() { } + virtual ~SocketFactory() { } + + /** + Open a socket for specific type and address. + + Return a valid socket handle if succeeded; return CURL_SOCKET_BAD otherwise. + */ + virtual curl_socket_t Open(curlsocktype socket_type, const curl_sockaddr* address) = 0; + + /** + Close a socket. + + Return a bool indicates that whether successful. + */ + virtual bool Close(curl_socket_t socket) = 0; + +private: + SocketFactory(const SocketFactory&) = delete; + SocketFactory& operator=(const SocketFactory&) = delete; +}; + +} \ No newline at end of file diff --git a/libraries/curlion/socket_watcher.h b/libraries/curlion/socket_watcher.h new file mode 100644 index 0000000..ca5b13c --- /dev/null +++ b/libraries/curlion/socket_watcher.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include + +namespace curlion { + +/** + SocketWatcher is an interface used by ConnectionManager to trigger a notification when a socket + is ready to read or write data. + */ +class SocketWatcher { +public: + /** + Socket's event type to be watched. + */ + enum class Event { + + /** + Read event. + */ + Read, + + /** + Write event. + */ + Write, + + /** + Both read and write event. + */ + ReadWrite, + }; + + /** + Callback prototype for the socket event. + + @param socket + Socket handle triggers the event. + + @param can_write + Indicates the event type. True for write, false for read. + */ + typedef std::function EventCallback; + +public: + SocketWatcher() { } + virtual ~SocketWatcher() { } + + /** + Start watching a socket for specific event. + + The watching should be continually, until a corresponding StopWatching is called. callback should + be called when the event on socket is triggered every time. + + Is is guaranteed that Watch would not be call for the second time unless StopWatching is called. + + The implementation must ensure that the thread calling callback is the same one calling Watch. + */ + virtual void Watch(curl_socket_t socket, Event event, const EventCallback& callback) = 0; + + /** + Stop watching a socket's event. + + Once StopWatching is called, no callback should be called any more. + Note that the socket may be closed before passed to StopWatching. Check for this case if it matters. + */ + virtual void StopWatching(curl_socket_t socket) = 0; + +private: + SocketWatcher(const SocketWatcher&) = delete; + SocketWatcher& operator=(const SocketWatcher&) = delete; +}; + +} \ No newline at end of file diff --git a/libraries/curlion/timer.h b/libraries/curlion/timer.h new file mode 100644 index 0000000..c438a1b --- /dev/null +++ b/libraries/curlion/timer.h @@ -0,0 +1,39 @@ +#pragma once + +#include + +namespace curlion { + +/** + Timer is an interface used by ConnectionManager to trigger a notification after a period of time. + */ +class Timer { +public: + Timer() { } + virtual ~Timer() { } + + /** + Start the timer. + + callback should be called after timeout_ms milliseconds. In case of timeout_ms is 0, + call callback as soon as possible. + + Is is guaranteed that Start would not be call for the second time unless Stop is called. + + The implementation must ensure that the thread calling callback is the same one calling Start. + */ + virtual void Start(long timeout_ms, const std::function& callback) = 0; + + /** + Stop the timer. + + Once Stop is called, no callback should be called any more. + */ + virtual void Stop() = 0; + +private: + Timer(const Timer&) = delete; + Timer& operator=(const Timer&) = delete; +}; + +} \ No newline at end of file diff --git a/libdeflate/libdeflate.h b/libraries/libdeflate/libdeflate.h similarity index 100% rename from libdeflate/libdeflate.h rename to libraries/libdeflate/libdeflate.h diff --git a/libdeflate/libdeflatestatic.lib b/libraries/libdeflate/libdeflatestatic.lib similarity index 100% rename from libdeflate/libdeflatestatic.lib rename to libraries/libdeflate/libdeflatestatic.lib diff --git a/libraries/signtool/signtool.exe b/libraries/signtool/signtool.exe new file mode 100644 index 0000000000000000000000000000000000000000..fb3d1fc3d0f0b87d3f7a183423899c25cf2f72b2 GIT binary patch literal 410056 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𕻡`scnu3nN38in^iqWuxJ7p62e7$SvmX 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 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include "wintoastlib.h" +#include +#include +#include +#include + +#pragma comment(lib,"shlwapi") +#pragma comment(lib,"user32") + +#define WINTOASTDEBUG +#ifdef WINTOASTDEBUG +#define DEBUG_MSG(str) do { } while ( false ) +#else +#define DEBUG_MSG(str) do { std::wcout << str << std::endl; } while( false ) +#endif + +#define DEFAULT_SHELL_LINKS_PATH L"\\Microsoft\\Windows\\Start Menu\\Programs\\" +#define DEFAULT_LINK_FORMAT L".lnk" +#define STATUS_SUCCESS (0x00000000) + + + // Quickstart: Handling toast activations from Win32 apps in Windows 10 + // https://blogs.msdn.microsoft.com/tiles_and_toasts/2015/10/16/quickstart-handling-toast-activations-from-win32-apps-in-windows-10/ +using namespace WinToastLib; +namespace DllImporter { + + // Function load a function from library + template + HRESULT loadFunctionFromLibrary(HINSTANCE library, LPCSTR name, Function& func) { + if (!library) { + return E_INVALIDARG; + } + func = reinterpret_cast(GetProcAddress(library, name)); + return (func != nullptr) ? S_OK : E_FAIL; + } + + typedef HRESULT(FAR STDAPICALLTYPE* f_SetCurrentProcessExplicitAppUserModelID)(__in PCWSTR AppID); + typedef HRESULT(FAR STDAPICALLTYPE* f_PropVariantToString)(_In_ REFPROPVARIANT propvar, _Out_writes_(cch) PWSTR psz, _In_ UINT cch); + typedef HRESULT(FAR STDAPICALLTYPE* f_RoGetActivationFactory)(_In_ HSTRING activatableClassId, _In_ REFIID iid, _COM_Outptr_ void** factory); + typedef HRESULT(FAR STDAPICALLTYPE* f_WindowsCreateStringReference)(_In_reads_opt_(length + 1) PCWSTR sourceString, UINT32 length, _Out_ HSTRING_HEADER* hstringHeader, _Outptr_result_maybenull_ _Result_nullonfailure_ HSTRING* string); + typedef PCWSTR(FAR STDAPICALLTYPE* f_WindowsGetStringRawBuffer)(_In_ HSTRING string, _Out_ UINT32* length); + typedef HRESULT(FAR STDAPICALLTYPE* f_WindowsDeleteString)(_In_opt_ HSTRING string); + + static f_SetCurrentProcessExplicitAppUserModelID SetCurrentProcessExplicitAppUserModelID; + static f_PropVariantToString PropVariantToString; + static f_RoGetActivationFactory RoGetActivationFactory; + static f_WindowsCreateStringReference WindowsCreateStringReference; + static f_WindowsGetStringRawBuffer WindowsGetStringRawBuffer; + static f_WindowsDeleteString WindowsDeleteString; + + + template + _Check_return_ __inline HRESULT _1_GetActivationFactory(_In_ HSTRING activatableClassId, _COM_Outptr_ T** factory) { + return RoGetActivationFactory(activatableClassId, IID_INS_ARGS(factory)); + } + + template + inline HRESULT Wrap_GetActivationFactory(_In_ HSTRING activatableClassId, _Inout_ Details::ComPtrRef factory) noexcept { + return _1_GetActivationFactory(activatableClassId, factory.ReleaseAndGetAddressOf()); + } + + inline HRESULT initialize() { + HINSTANCE LibShell32 = LoadLibraryW(L"SHELL32.DLL"); + HRESULT hr = loadFunctionFromLibrary(LibShell32, "SetCurrentProcessExplicitAppUserModelID", SetCurrentProcessExplicitAppUserModelID); + if (SUCCEEDED(hr)) { + HINSTANCE LibPropSys = LoadLibraryW(L"PROPSYS.DLL"); + hr = loadFunctionFromLibrary(LibPropSys, "PropVariantToString", PropVariantToString); + if (SUCCEEDED(hr)) { + HINSTANCE LibComBase = LoadLibraryW(L"COMBASE.DLL"); + const bool succeded = SUCCEEDED(loadFunctionFromLibrary(LibComBase, "RoGetActivationFactory", RoGetActivationFactory)) + && SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsCreateStringReference", WindowsCreateStringReference)) + && SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsGetStringRawBuffer", WindowsGetStringRawBuffer)) + && SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsDeleteString", WindowsDeleteString)); + return succeded ? S_OK : E_FAIL; + } + } + return hr; + } +} + +class WinToastStringWrapper { +public: + WinToastStringWrapper(_In_reads_(length) PCWSTR stringRef, _In_ UINT32 length) noexcept { + HRESULT hr = DllImporter::WindowsCreateStringReference(stringRef, length, &_header, &_hstring); + if (!SUCCEEDED(hr)) { + RaiseException(static_cast(STATUS_INVALID_PARAMETER), EXCEPTION_NONCONTINUABLE, 0, nullptr); + } + } + + WinToastStringWrapper(_In_ const std::wstring& stringRef) noexcept { + HRESULT hr = DllImporter::WindowsCreateStringReference(stringRef.c_str(), static_cast(stringRef.length()), &_header, &_hstring); + if (FAILED(hr)) { + RaiseException(static_cast(STATUS_INVALID_PARAMETER), EXCEPTION_NONCONTINUABLE, 0, nullptr); + } + } + + ~WinToastStringWrapper() { + DllImporter::WindowsDeleteString(_hstring); + } + + inline HSTRING Get() const noexcept { + return _hstring; + } +private: + HSTRING _hstring; + HSTRING_HEADER _header; + +}; + +class InternalDateTime : public IReference { +public: + static INT64 Now() { + FILETIME now; + GetSystemTimeAsFileTime(&now); + return ((((INT64)now.dwHighDateTime) << 32) | now.dwLowDateTime); + } + + InternalDateTime(DateTime dateTime) : _dateTime(dateTime) {} + + InternalDateTime(INT64 millisecondsFromNow) { + _dateTime.UniversalTime = Now() + millisecondsFromNow * 10000; + } + + virtual ~InternalDateTime() = default; + + operator INT64() { + return _dateTime.UniversalTime; + } + + HRESULT STDMETHODCALLTYPE get_Value(DateTime* dateTime) { + *dateTime = _dateTime; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE QueryInterface(const IID& riid, void** ppvObject) { + if (!ppvObject) { + return E_POINTER; + } + if (riid == __uuidof(IUnknown) || riid == __uuidof(IReference)) { + *ppvObject = static_cast(static_cast*>(this)); + return S_OK; + } + return E_NOINTERFACE; + } + + ULONG STDMETHODCALLTYPE Release() { + return 1; + } + + ULONG STDMETHODCALLTYPE AddRef() { + return 2; + } + + HRESULT STDMETHODCALLTYPE GetIids(ULONG*, IID**) { + return E_NOTIMPL; + } + + HRESULT STDMETHODCALLTYPE GetRuntimeClassName(HSTRING*) { + return E_NOTIMPL; + } + + HRESULT STDMETHODCALLTYPE GetTrustLevel(TrustLevel*) { + return E_NOTIMPL; + } + +protected: + DateTime _dateTime; +}; + +namespace Util { + + typedef LONG NTSTATUS, * PNTSTATUS; + typedef NTSTATUS(WINAPI* RtlGetVersionPtr)(PRTL_OSVERSIONINFOW); + inline RTL_OSVERSIONINFOW getRealOSVersion() { + HMODULE hMod = ::GetModuleHandleW(L"ntdll.dll"); + if (hMod) { + RtlGetVersionPtr fxPtr = (RtlGetVersionPtr)::GetProcAddress(hMod, "RtlGetVersion"); + if (fxPtr != nullptr) { + RTL_OSVERSIONINFOW rovi = { 0 }; + rovi.dwOSVersionInfoSize = sizeof(rovi); + if (STATUS_SUCCESS == fxPtr(&rovi)) { + return rovi; + } + } + } + RTL_OSVERSIONINFOW rovi = { 0 }; + return rovi; + } + + inline HRESULT defaultExecutablePath(_In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) { + DWORD written = GetModuleFileNameExW(GetCurrentProcess(), nullptr, path, nSize); + DEBUG_MSG("Default executable path: " << path); + return (written > 0) ? S_OK : E_FAIL; + } + + + inline HRESULT defaultShellLinksDirectory(_In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) { + DWORD written = GetEnvironmentVariableW(L"APPDATA", path, nSize); + HRESULT hr = written > 0 ? S_OK : E_INVALIDARG; + if (SUCCEEDED(hr)) { + errno_t result = wcscat_s(path, nSize, DEFAULT_SHELL_LINKS_PATH); + hr = (result == 0) ? S_OK : E_INVALIDARG; + DEBUG_MSG("Default shell link path: " << path); + } + return hr; + } + + inline HRESULT defaultShellLinkPath(const std::wstring& appname, _In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) { + HRESULT hr = defaultShellLinksDirectory(path, nSize); + if (SUCCEEDED(hr)) { + const std::wstring appLink(appname + DEFAULT_LINK_FORMAT); + errno_t result = wcscat_s(path, nSize, appLink.c_str()); + hr = (result == 0) ? S_OK : E_INVALIDARG; + DEBUG_MSG("Default shell link file path: " << path); + } + return hr; + } + + + inline PCWSTR AsString(ComPtr& xmlDocument) { + HSTRING xml; + ComPtr ser; + HRESULT hr = xmlDocument.As(&ser); + hr = ser->GetXml(&xml); + if (SUCCEEDED(hr)) + return DllImporter::WindowsGetStringRawBuffer(xml, nullptr); + return nullptr; + } + + inline PCWSTR AsString(HSTRING hstring) { + return DllImporter::WindowsGetStringRawBuffer(hstring, nullptr); + } + + inline HRESULT setNodeStringValue(const std::wstring& string, IXmlNode* node, IXmlDocument* xml) { + ComPtr textNode; + HRESULT hr = xml->CreateTextNode(WinToastStringWrapper(string).Get(), &textNode); + if (SUCCEEDED(hr)) { + ComPtr stringNode; + hr = textNode.As(&stringNode); + if (SUCCEEDED(hr)) { + ComPtr appendedChild; + hr = node->AppendChild(stringNode.Get(), &appendedChild); + } + } + return hr; + } + + inline HRESULT setEventHandlers(_In_ IToastNotification* notification, _In_ std::shared_ptr eventHandler, _In_ INT64 expirationTime) { + EventRegistrationToken activatedToken, dismissedToken, failedToken; + HRESULT hr = notification->add_Activated( + Callback < Implements < RuntimeClassFlags, + ITypedEventHandler> >( + [eventHandler](IToastNotification*, IInspectable* inspectable) + { + IToastActivatedEventArgs* activatedEventArgs; + HRESULT hr = inspectable->QueryInterface(&activatedEventArgs); + if (SUCCEEDED(hr)) { + HSTRING argumentsHandle; + hr = activatedEventArgs->get_Arguments(&argumentsHandle); + if (SUCCEEDED(hr)) { + PCWSTR arguments = Util::AsString(argumentsHandle); + if (arguments && *arguments) { + eventHandler->toastActivated(static_cast(wcstol(arguments, nullptr, 10))); + return S_OK; + } + } + } + eventHandler->toastActivated(); + return S_OK; + }).Get(), &activatedToken); + + if (SUCCEEDED(hr)) { + hr = notification->add_Dismissed(Callback < Implements < RuntimeClassFlags, + ITypedEventHandler> >( + [eventHandler, expirationTime](IToastNotification*, IToastDismissedEventArgs* e) + { + ToastDismissalReason reason; + if (SUCCEEDED(e->get_Reason(&reason))) + { + if (reason == ToastDismissalReason_UserCanceled && expirationTime && InternalDateTime::Now() >= expirationTime) + reason = ToastDismissalReason_TimedOut; + eventHandler->toastDismissed(static_cast(reason)); + } + return S_OK; + }).Get(), &dismissedToken); + if (SUCCEEDED(hr)) { + hr = notification->add_Failed(Callback < Implements < RuntimeClassFlags, + ITypedEventHandler> >( + [eventHandler](IToastNotification*, IToastFailedEventArgs*) + { + eventHandler->toastFailed(); + return S_OK; + }).Get(), &failedToken); + } + } + return hr; + } + + inline HRESULT addAttribute(_In_ IXmlDocument* xml, const std::wstring& name, IXmlNamedNodeMap* attributeMap) { + ComPtr srcAttribute; + HRESULT hr = xml->CreateAttribute(WinToastStringWrapper(name).Get(), &srcAttribute); + if (SUCCEEDED(hr)) { + ComPtr node; + hr = srcAttribute.As(&node); + if (SUCCEEDED(hr)) { + ComPtr pNode; + hr = attributeMap->SetNamedItem(node.Get(), &pNode); + } + } + return hr; + } + + inline HRESULT createElement(_In_ IXmlDocument* xml, _In_ const std::wstring& root_node, _In_ const std::wstring& element_name, _In_ const std::vector& attribute_names) { + ComPtr rootList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(root_node).Get(), &rootList); + if (SUCCEEDED(hr)) { + ComPtr root; + hr = rootList->Item(0, &root); + if (SUCCEEDED(hr)) { + ComPtr audioElement; + hr = xml->CreateElement(WinToastStringWrapper(element_name).Get(), &audioElement); + if (SUCCEEDED(hr)) { + ComPtr audioNodeTmp; + hr = audioElement.As(&audioNodeTmp); + if (SUCCEEDED(hr)) { + ComPtr audioNode; + hr = root->AppendChild(audioNodeTmp.Get(), &audioNode); + if (SUCCEEDED(hr)) { + ComPtr attributes; + hr = audioNode->get_Attributes(&attributes); + if (SUCCEEDED(hr)) { + for (const auto& it : attribute_names) { + hr = addAttribute(xml, it, attributes.Get()); + } + } + } + } + } + } + } + return hr; + } +} + +WinToast* WinToast::instance() { + static WinToast instance; + return &instance; +} + +WinToast::WinToast() : + _isInitialized(false), + _hasCoInitialized(false) +{ + if (!isCompatible()) { + DEBUG_MSG(L"Warning: Your system is not compatible with this library "); + } +} + +WinToast::~WinToast() { + if (_hasCoInitialized) { + CoUninitialize(); + } +} + +void WinToast::setAppName(_In_ const std::wstring& appName) { + _appName = appName; +} + + +void WinToast::setAppUserModelId(_In_ const std::wstring& aumi) { + _aumi = aumi; + DEBUG_MSG(L"Default App User Model Id: " << _aumi.c_str()); +} + +bool WinToast::isCompatible() { + DllImporter::initialize(); + return !((DllImporter::SetCurrentProcessExplicitAppUserModelID == nullptr) + || (DllImporter::PropVariantToString == nullptr) + || (DllImporter::RoGetActivationFactory == nullptr) + || (DllImporter::WindowsCreateStringReference == nullptr) + || (DllImporter::WindowsDeleteString == nullptr)); +} + +bool WinToastLib::WinToast::isSupportingModernFeatures() { + constexpr auto MinimumSupportedVersion = 6; + return Util::getRealOSVersion().dwMajorVersion > MinimumSupportedVersion; + +} +std::wstring WinToast::configureAUMI(_In_ const std::wstring& companyName, + _In_ const std::wstring& productName, + _In_ const std::wstring& subProduct, + _In_ const std::wstring& versionInformation) +{ + std::wstring aumi = companyName; + aumi += L"." + productName; + if (subProduct.length() > 0) { + aumi += L"." + subProduct; + if (versionInformation.length() > 0) { + aumi += L"." + versionInformation; + } + } + + if (aumi.length() > SCHAR_MAX) { + DEBUG_MSG("Error: max size allowed for AUMI: 128 characters."); + } + return aumi; +} + +const std::wstring& WinToast::strerror(WinToastError error) { + static const std::unordered_map Labels = { + {WinToastError::NoError, L"No error. The process was executed correctly"}, + {WinToastError::NotInitialized, L"The library has not been initialized"}, + {WinToastError::SystemNotSupported, L"The OS does not support WinToast"}, + {WinToastError::ShellLinkNotCreated, L"The library was not able to create a Shell Link for the app"}, + {WinToastError::InvalidAppUserModelID, L"The AUMI is not a valid one"}, + {WinToastError::InvalidParameters, L"The parameters used to configure the library are not valid normally because an invalid AUMI or App Name"}, + {WinToastError::NotDisplayed, L"The toast was created correctly but WinToast was not able to display the toast"}, + {WinToastError::UnknownError, L"Unknown error"} + }; + + const auto iter = Labels.find(error); + assert(iter != Labels.end()); + return iter->second; +} + +enum WinToast::ShortcutResult WinToast::createShortcut() { + if (_aumi.empty() || _appName.empty()) { + DEBUG_MSG(L"Error: App User Model Id or Appname is empty!"); + return SHORTCUT_MISSING_PARAMETERS; + } + + if (!isCompatible()) { + DEBUG_MSG(L"Your OS is not compatible with this library! =("); + return SHORTCUT_INCOMPATIBLE_OS; + } + + if (!_hasCoInitialized) { + HRESULT initHr = CoInitializeEx(nullptr, COINIT::COINIT_MULTITHREADED); + if (initHr != RPC_E_CHANGED_MODE) { + if (FAILED(initHr) && initHr != S_FALSE) { + DEBUG_MSG(L"Error on COM library initialization!"); + return SHORTCUT_COM_INIT_FAILURE; + } + else { + _hasCoInitialized = true; + } + } + } + + bool wasChanged; + HRESULT hr = validateShellLinkHelper(wasChanged); + if (SUCCEEDED(hr)) + return wasChanged ? SHORTCUT_WAS_CHANGED : SHORTCUT_UNCHANGED; + + hr = createShellLinkHelper(); + return SUCCEEDED(hr) ? SHORTCUT_WAS_CREATED : SHORTCUT_CREATE_FAILED; +} + +bool WinToast::initialize(_Out_ WinToastError* error) { + _isInitialized = false; + setError(error, WinToastError::NoError); + + if (!isCompatible()) { + setError(error, WinToastError::SystemNotSupported); + DEBUG_MSG(L"Error: system not supported."); + return false; + } + + + if (_aumi.empty() || _appName.empty()) { + setError(error, WinToastError::InvalidParameters); + DEBUG_MSG(L"Error while initializing, did you set up a valid AUMI and App name?"); + return false; + } + + if (createShortcut() < 0) { + setError(error, WinToastError::ShellLinkNotCreated); + DEBUG_MSG(L"Error while attaching the AUMI to the current proccess =("); + return false; + } + + if (FAILED(DllImporter::SetCurrentProcessExplicitAppUserModelID(_aumi.c_str()))) { + setError(error, WinToastError::InvalidAppUserModelID); + DEBUG_MSG(L"Error while attaching the AUMI to the current proccess =("); + return false; + } + + ComPtr notificationManager; + HRESULT hr = DllImporter::Wrap_GetActivationFactory(WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), ¬ificationManager); + if (SUCCEEDED(hr)) { + ComPtr notifier; + hr = notificationManager->CreateToastNotifierWithId(WinToastStringWrapper(_aumi).Get(), ¬ifier); + if (SUCCEEDED(hr)) { + ComPtr notificationFactory; + hr = DllImporter::Wrap_GetActivationFactory(WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotification).Get(), ¬ificationFactory); + if (SUCCEEDED(hr)) { + _isInitialized = true; + } + } + } + + return _isInitialized; +} + +bool WinToast::isInitialized() const { + return _isInitialized; +} + +const std::wstring& WinToast::appName() const { + return _appName; +} + +const std::wstring& WinToast::appUserModelId() const { + return _aumi; +} + + +HRESULT WinToast::validateShellLinkHelper(_Out_ bool& wasChanged) { + WCHAR path[MAX_PATH] = { L'\0' }; + Util::defaultShellLinkPath(_appName, path); + // Check if the file exist + DWORD attr = GetFileAttributesW(path); + if (attr >= 0xFFFFFFF) { + DEBUG_MSG("Error, shell link not found. Try to create a new one in: " << path); + return E_FAIL; + } + + // Let's load the file as shell link to validate. + // - Create a shell link + // - Create a persistant file + // - Load the path as data for the persistant file + // - Read the property AUMI and validate with the current + // - Review if AUMI is equal. + ComPtr shellLink; + HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)); + if (SUCCEEDED(hr)) { + ComPtr persistFile; + hr = shellLink.As(&persistFile); + if (SUCCEEDED(hr)) { + hr = persistFile->Load(path, STGM_READWRITE); + if (SUCCEEDED(hr)) { + ComPtr propertyStore; + hr = shellLink.As(&propertyStore); + if (SUCCEEDED(hr)) { + PROPVARIANT appIdPropVar; + hr = propertyStore->GetValue(PKEY_AppUserModel_ID, &appIdPropVar); + if (SUCCEEDED(hr)) { + WCHAR AUMI[MAX_PATH]; + hr = DllImporter::PropVariantToString(appIdPropVar, AUMI, MAX_PATH); + wasChanged = false; + if (FAILED(hr) || _aumi != AUMI) { + // AUMI Changed for the same app, let's update the current value! =) + wasChanged = true; + PropVariantClear(&appIdPropVar); + hr = InitPropVariantFromString(_aumi.c_str(), &appIdPropVar); + if (SUCCEEDED(hr)) { + hr = propertyStore->SetValue(PKEY_AppUserModel_ID, appIdPropVar); + if (SUCCEEDED(hr)) { + hr = propertyStore->Commit(); + if (SUCCEEDED(hr) && SUCCEEDED(persistFile->IsDirty())) { + hr = persistFile->Save(path, TRUE); + } + } + } + } + PropVariantClear(&appIdPropVar); + } + } + } + } + } + return hr; +} + + + +HRESULT WinToast::createShellLinkHelper() { + WCHAR exePath[MAX_PATH]{ L'\0' }; + WCHAR slPath[MAX_PATH]{ L'\0' }; + Util::defaultShellLinkPath(_appName, slPath); + Util::defaultExecutablePath(exePath); + ComPtr shellLink; + HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)); + if (SUCCEEDED(hr)) { + hr = shellLink->SetPath(exePath); + if (SUCCEEDED(hr)) { + hr = shellLink->SetArguments(L""); + if (SUCCEEDED(hr)) { + hr = shellLink->SetWorkingDirectory(exePath); + if (SUCCEEDED(hr)) { + ComPtr propertyStore; + hr = shellLink.As(&propertyStore); + if (SUCCEEDED(hr)) { + PROPVARIANT appIdPropVar; + hr = InitPropVariantFromString(_aumi.c_str(), &appIdPropVar); + if (SUCCEEDED(hr)) { + hr = propertyStore->SetValue(PKEY_AppUserModel_ID, appIdPropVar); + if (SUCCEEDED(hr)) { + hr = propertyStore->Commit(); + if (SUCCEEDED(hr)) { + ComPtr persistFile; + hr = shellLink.As(&persistFile); + if (SUCCEEDED(hr)) { + hr = persistFile->Save(slPath, TRUE); + } + } + } + PropVariantClear(&appIdPropVar); + } + } + } + } + } + } + return hr; +} + +INT64 WinToast::showToast(_In_ const WinToastTemplate& toast, _In_ std::shared_ptr handler, _Out_ WinToastError* error) { + setError(error, WinToastError::NoError); + INT64 id = -1; + if (!isInitialized()) { + setError(error, WinToastError::NotInitialized); + DEBUG_MSG("Error when launching the toast. WinToast is not initialized."); + return id; + } + if (!handler) { + setError(error, WinToastError::InvalidHandler); + DEBUG_MSG("Error when launching the toast. Handler cannot be nullptr."); + return id; + } + + ComPtr notificationManager; + HRESULT hr = DllImporter::Wrap_GetActivationFactory(WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), ¬ificationManager); + if (SUCCEEDED(hr)) { + ComPtr notifier; + hr = notificationManager->CreateToastNotifierWithId(WinToastStringWrapper(_aumi).Get(), ¬ifier); + if (SUCCEEDED(hr)) { + ComPtr notificationFactory; + hr = DllImporter::Wrap_GetActivationFactory(WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotification).Get(), ¬ificationFactory); + if (SUCCEEDED(hr)) { + ComPtr xmlDocument; + HRESULT hr = notificationManager->GetTemplateContent(ToastTemplateType(toast.type()), &xmlDocument); + if (SUCCEEDED(hr)) { + for (std::size_t i = 0, fieldsCount = toast.textFieldsCount(); i < fieldsCount && SUCCEEDED(hr); i++) { + hr = setTextFieldHelper(xmlDocument.Get(), toast.textField(WinToastTemplate::TextField(i)), i); + } + + // Modern feature are supported Windows > Windows 10 + if (SUCCEEDED(hr) && isSupportingModernFeatures()) { + + // Note that we do this *after* using toast.textFieldsCount() to + // iterate/fill the template's text fields, since we're adding yet another text field. + if (SUCCEEDED(hr) + && !toast.attributionText().empty()) { + hr = setAttributionTextFieldHelper(xmlDocument.Get(), toast.attributionText()); + } + + std::array buf; + for (std::size_t i = 0, actionsCount = toast.actionsCount(); i < actionsCount && SUCCEEDED(hr); i++) { + _snwprintf_s(buf.data(), buf.size(), _TRUNCATE, L"%zd", i); + hr = addActionHelper(xmlDocument.Get(), toast.actionLabel(i), buf.data()); + } + + if (SUCCEEDED(hr)) { + hr = (toast.audioPath().empty() && toast.audioOption() == WinToastTemplate::AudioOption::Default) + ? hr : setAudioFieldHelper(xmlDocument.Get(), toast.audioPath(), toast.audioOption()); + } + + if (SUCCEEDED(hr) && toast.duration() != WinToastTemplate::Duration::System) { + hr = addDurationHelper(xmlDocument.Get(), + (toast.duration() == WinToastTemplate::Duration::Short) ? L"short" : L"long"); + } + + } + else { + DEBUG_MSG("Modern features (Actions/Sounds/Attributes) not supported in this os version"); + } + + if (SUCCEEDED(hr)) { + hr = toast.hasImage() ? setImageFieldHelper(xmlDocument.Get(), toast.imagePath()) : hr; + if (SUCCEEDED(hr)) { + ComPtr notification; + hr = notificationFactory->CreateToastNotification(xmlDocument.Get(), ¬ification); + if (SUCCEEDED(hr)) { + INT64 expiration = 0, relativeExpiration = toast.expiration(); + if (relativeExpiration > 0) { + InternalDateTime expirationDateTime(relativeExpiration); + expiration = expirationDateTime; + hr = notification->put_ExpirationTime(&expirationDateTime); + } + + if (SUCCEEDED(hr)) { + hr = Util::setEventHandlers(notification.Get(), std::shared_ptr(handler), expiration); + if (FAILED(hr)) { + setError(error, WinToastError::InvalidHandler); + } + } + + if (SUCCEEDED(hr)) { + GUID guid; + hr = CoCreateGuid(&guid); + if (SUCCEEDED(hr)) { + id = guid.Data1; + _buffer[id] = notification; + DEBUG_MSG("xml: " << Util::AsString(xmlDocument)); + hr = notifier->Show(notification.Get()); + if (FAILED(hr)) { + setError(error, WinToastError::NotDisplayed); + } + } + } + } + } + } + } + } + } + } + return FAILED(hr) ? -1 : id; +} + +ComPtr WinToast::notifier(_In_ bool* succeded) const { + ComPtr notificationManager; + ComPtr notifier; + HRESULT hr = DllImporter::Wrap_GetActivationFactory(WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), ¬ificationManager); + if (SUCCEEDED(hr)) { + hr = notificationManager->CreateToastNotifierWithId(WinToastStringWrapper(_aumi).Get(), ¬ifier); + } + *succeded = SUCCEEDED(hr); + return notifier; +} + +bool WinToast::hideToast(_In_ INT64 id) { + if (!isInitialized()) { + DEBUG_MSG("Error when hiding the toast. WinToast is not initialized."); + return false; + } + + if (_buffer.find(id) != _buffer.end()) { + auto succeded = false; + auto notify = notifier(&succeded); + if (succeded) { + auto result = notify->Hide(_buffer[id].Get()); + _buffer.erase(id); + return SUCCEEDED(result); + } + } + return false; +} + +void WinToast::clear() { + auto succeded = false; + auto notify = notifier(&succeded); + if (succeded) { + auto end = _buffer.end(); + for (auto it = _buffer.begin(); it != end; ++it) { + notify->Hide(it->second.Get()); + } + _buffer.clear(); + } +} + +// +// Available as of Windows 10 Anniversary Update +// Ref: https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/adaptive-interactive-toasts +// +// NOTE: This will add a new text field, so be aware when iterating over +// the toast's text fields or getting a count of them. +// +HRESULT WinToast::setAttributionTextFieldHelper(_In_ IXmlDocument* xml, _In_ const std::wstring& text) { + Util::createElement(xml, L"binding", L"text", { L"placement" }); + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"text").Get(), &nodeList); + if (SUCCEEDED(hr)) { + UINT32 nodeListLength; + hr = nodeList->get_Length(&nodeListLength); + if (SUCCEEDED(hr)) { + for (UINT32 i = 0; i < nodeListLength; i++) { + ComPtr textNode; + hr = nodeList->Item(i, &textNode); + if (SUCCEEDED(hr)) { + ComPtr attributes; + hr = textNode->get_Attributes(&attributes); + if (SUCCEEDED(hr)) { + ComPtr editedNode; + if (SUCCEEDED(hr)) { + hr = attributes->GetNamedItem(WinToastStringWrapper(L"placement").Get(), &editedNode); + if (FAILED(hr) || !editedNode) { + continue; + } + hr = Util::setNodeStringValue(L"attribution", editedNode.Get(), xml); + if (SUCCEEDED(hr)) { + return setTextFieldHelper(xml, text, i); + } + } + } + } + } + } + } + return hr; +} + +HRESULT WinToast::addDurationHelper(_In_ IXmlDocument* xml, _In_ const std::wstring& duration) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"toast").Get(), &nodeList); + if (SUCCEEDED(hr)) { + UINT32 length; + hr = nodeList->get_Length(&length); + if (SUCCEEDED(hr)) { + ComPtr toastNode; + hr = nodeList->Item(0, &toastNode); + if (SUCCEEDED(hr)) { + ComPtr toastElement; + hr = toastNode.As(&toastElement); + if (SUCCEEDED(hr)) { + hr = toastElement->SetAttribute(WinToastStringWrapper(L"duration").Get(), + WinToastStringWrapper(duration).Get()); + } + } + } + } + return hr; +} + +HRESULT WinToast::setTextFieldHelper(_In_ IXmlDocument* xml, _In_ const std::wstring& text, _In_ UINT32 pos) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"text").Get(), &nodeList); + if (SUCCEEDED(hr)) { + ComPtr node; + hr = nodeList->Item(pos, &node); + if (SUCCEEDED(hr)) { + hr = Util::setNodeStringValue(text, node.Get(), xml); + } + } + return hr; +} + + +HRESULT WinToast::setImageFieldHelper(_In_ IXmlDocument* xml, _In_ const std::wstring& path) { + assert(path.size() < MAX_PATH); + + wchar_t imagePath[MAX_PATH] = L"file:///"; + HRESULT hr = StringCchCatW(imagePath, MAX_PATH, path.c_str()); + if (SUCCEEDED(hr)) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"image").Get(), &nodeList); + if (SUCCEEDED(hr)) { + ComPtr node; + hr = nodeList->Item(0, &node); + if (SUCCEEDED(hr)) { + ComPtr attributes; + hr = node->get_Attributes(&attributes); + if (SUCCEEDED(hr)) { + ComPtr editedNode; + hr = attributes->GetNamedItem(WinToastStringWrapper(L"src").Get(), &editedNode); + if (SUCCEEDED(hr)) { + Util::setNodeStringValue(imagePath, editedNode.Get(), xml); + } + } + } + } + } + return hr; +} + +HRESULT WinToast::setAudioFieldHelper(_In_ IXmlDocument* xml, _In_ const std::wstring& path, _In_opt_ WinToastTemplate::AudioOption option) { + std::vector attrs; + if (!path.empty()) attrs.push_back(L"src"); + if (option == WinToastTemplate::AudioOption::Loop) attrs.push_back(L"loop"); + if (option == WinToastTemplate::AudioOption::Silent) attrs.push_back(L"silent"); + Util::createElement(xml, L"toast", L"audio", attrs); + + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"audio").Get(), &nodeList); + if (SUCCEEDED(hr)) { + ComPtr node; + hr = nodeList->Item(0, &node); + if (SUCCEEDED(hr)) { + ComPtr attributes; + hr = node->get_Attributes(&attributes); + if (SUCCEEDED(hr)) { + ComPtr editedNode; + if (!path.empty()) { + if (SUCCEEDED(hr)) { + hr = attributes->GetNamedItem(WinToastStringWrapper(L"src").Get(), &editedNode); + if (SUCCEEDED(hr)) { + hr = Util::setNodeStringValue(path, editedNode.Get(), xml); + } + } + } + + if (SUCCEEDED(hr)) { + switch (option) { + case WinToastTemplate::AudioOption::Loop: + hr = attributes->GetNamedItem(WinToastStringWrapper(L"loop").Get(), &editedNode); + if (SUCCEEDED(hr)) { + hr = Util::setNodeStringValue(L"true", editedNode.Get(), xml); + } + break; + case WinToastTemplate::AudioOption::Silent: + hr = attributes->GetNamedItem(WinToastStringWrapper(L"silent").Get(), &editedNode); + if (SUCCEEDED(hr)) { + hr = Util::setNodeStringValue(L"true", editedNode.Get(), xml); + } + default: + break; + } + } + } + } + } + return hr; +} + +HRESULT WinToast::addActionHelper(_In_ IXmlDocument* xml, _In_ const std::wstring& content, _In_ const std::wstring& arguments) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"actions").Get(), &nodeList); + if (SUCCEEDED(hr)) { + UINT32 length; + hr = nodeList->get_Length(&length); + if (SUCCEEDED(hr)) { + ComPtr actionsNode; + if (length > 0) { + hr = nodeList->Item(0, &actionsNode); + } + else { + hr = xml->GetElementsByTagName(WinToastStringWrapper(L"toast").Get(), &nodeList); + if (SUCCEEDED(hr)) { + hr = nodeList->get_Length(&length); + if (SUCCEEDED(hr)) { + ComPtr toastNode; + hr = nodeList->Item(0, &toastNode); + if (SUCCEEDED(hr)) { + ComPtr toastElement; + hr = toastNode.As(&toastElement); + if (SUCCEEDED(hr)) + hr = toastElement->SetAttribute(WinToastStringWrapper(L"template").Get(), WinToastStringWrapper(L"ToastGeneric").Get()); + if (SUCCEEDED(hr)) + hr = toastElement->SetAttribute(WinToastStringWrapper(L"duration").Get(), WinToastStringWrapper(L"long").Get()); + if (SUCCEEDED(hr)) { + ComPtr actionsElement; + hr = xml->CreateElement(WinToastStringWrapper(L"actions").Get(), &actionsElement); + if (SUCCEEDED(hr)) { + hr = actionsElement.As(&actionsNode); + if (SUCCEEDED(hr)) { + ComPtr appendedChild; + hr = toastNode->AppendChild(actionsNode.Get(), &appendedChild); + } + } + } + } + } + } + } + if (SUCCEEDED(hr)) { + ComPtr actionElement; + hr = xml->CreateElement(WinToastStringWrapper(L"action").Get(), &actionElement); + if (SUCCEEDED(hr)) + hr = actionElement->SetAttribute(WinToastStringWrapper(L"content").Get(), WinToastStringWrapper(content).Get()); + if (SUCCEEDED(hr)) + hr = actionElement->SetAttribute(WinToastStringWrapper(L"arguments").Get(), WinToastStringWrapper(arguments).Get()); + if (SUCCEEDED(hr)) { + ComPtr actionNode; + hr = actionElement.As(&actionNode); + if (SUCCEEDED(hr)) { + ComPtr appendedChild; + hr = actionsNode->AppendChild(actionNode.Get(), &appendedChild); + } + } + } + } + } + return hr; +} + +void WinToast::setError(_Out_ WinToastError* error, _In_ WinToastError value) { + if (error) { + *error = value; + } +} + +WinToastTemplate::WinToastTemplate(_In_ WinToastTemplateType type) : _type(type) { + static constexpr std::size_t TextFieldsCount[] = { 1, 2, 2, 3, 1, 2, 2, 3 }; + _textFields = std::vector(TextFieldsCount[type], L""); +} + +WinToastTemplate::~WinToastTemplate() { + _textFields.clear(); +} + +void WinToastTemplate::setTextField(_In_ const std::wstring& txt, _In_ WinToastTemplate::TextField pos) { + const auto position = static_cast(pos); + assert(position < _textFields.size()); + _textFields[position] = txt; +} + +void WinToastTemplate::setImagePath(_In_ const std::wstring& imgPath) { + _imagePath = imgPath; +} + +void WinToastTemplate::setAudioPath(_In_ const std::wstring& audioPath) { + _audioPath = audioPath; +} + +void WinToastTemplate::setAudioPath(_In_ AudioSystemFile file) { + static const std::unordered_map Files = { + {AudioSystemFile::DefaultSound, L"ms-winsoundevent:Notification.Default"}, + {AudioSystemFile::IM, L"ms-winsoundevent:Notification.IM"}, + {AudioSystemFile::Mail, L"ms-winsoundevent:Notification.Mail"}, + {AudioSystemFile::Reminder, L"ms-winsoundevent:Notification.Reminder"}, + {AudioSystemFile::SMS, L"ms-winsoundevent:Notification.SMS"}, + {AudioSystemFile::Alarm, L"ms-winsoundevent:Notification.Looping.Alarm"}, + {AudioSystemFile::Alarm2, L"ms-winsoundevent:Notification.Looping.Alarm2"}, + {AudioSystemFile::Alarm3, L"ms-winsoundevent:Notification.Looping.Alarm3"}, + {AudioSystemFile::Alarm4, L"ms-winsoundevent:Notification.Looping.Alarm4"}, + {AudioSystemFile::Alarm5, L"ms-winsoundevent:Notification.Looping.Alarm5"}, + {AudioSystemFile::Alarm6, L"ms-winsoundevent:Notification.Looping.Alarm6"}, + {AudioSystemFile::Alarm7, L"ms-winsoundevent:Notification.Looping.Alarm7"}, + {AudioSystemFile::Alarm8, L"ms-winsoundevent:Notification.Looping.Alarm8"}, + {AudioSystemFile::Alarm9, L"ms-winsoundevent:Notification.Looping.Alarm9"}, + {AudioSystemFile::Alarm10, L"ms-winsoundevent:Notification.Looping.Alarm10"}, + {AudioSystemFile::Call, L"ms-winsoundevent:Notification.Looping.Call"}, + {AudioSystemFile::Call1, L"ms-winsoundevent:Notification.Looping.Call1"}, + {AudioSystemFile::Call2, L"ms-winsoundevent:Notification.Looping.Call2"}, + {AudioSystemFile::Call3, L"ms-winsoundevent:Notification.Looping.Call3"}, + {AudioSystemFile::Call4, L"ms-winsoundevent:Notification.Looping.Call4"}, + {AudioSystemFile::Call5, L"ms-winsoundevent:Notification.Looping.Call5"}, + {AudioSystemFile::Call6, L"ms-winsoundevent:Notification.Looping.Call6"}, + {AudioSystemFile::Call7, L"ms-winsoundevent:Notification.Looping.Call7"}, + {AudioSystemFile::Call8, L"ms-winsoundevent:Notification.Looping.Call8"}, + {AudioSystemFile::Call9, L"ms-winsoundevent:Notification.Looping.Call9"}, + {AudioSystemFile::Call10, L"ms-winsoundevent:Notification.Looping.Call10"}, + }; + const auto iter = Files.find(file); + assert(iter != Files.end()); + _audioPath = iter->second; +} + +void WinToastTemplate::setAudioOption(_In_ WinToastTemplate::AudioOption audioOption) { + _audioOption = audioOption; +} + +void WinToastTemplate::setFirstLine(const std::wstring& text) { + setTextField(text, WinToastTemplate::FirstLine); +} + +void WinToastTemplate::setSecondLine(const std::wstring& text) { + setTextField(text, WinToastTemplate::SecondLine); +} + +void WinToastTemplate::setThirdLine(const std::wstring& text) { + setTextField(text, WinToastTemplate::ThirdLine); +} + +void WinToastTemplate::setDuration(_In_ Duration duration) { + _duration = duration; +} + +void WinToastTemplate::setExpiration(_In_ INT64 millisecondsFromNow) { + _expiration = millisecondsFromNow; +} + +void WinToastTemplate::setAttributionText(_In_ const std::wstring& attributionText) { + _attributionText = attributionText; +} + +void WinToastTemplate::addAction(_In_ const std::wstring& label) { + _actions.push_back(label); +} + +std::size_t WinToastTemplate::textFieldsCount() const { + return _textFields.size(); +} + +std::size_t WinToastTemplate::actionsCount() const { + return _actions.size(); +} + +bool WinToastTemplate::hasImage() const { + return _type < WinToastTemplateType::Text01; +} + +const std::vector& WinToastTemplate::textFields() const { + return _textFields; +} + +const std::wstring& WinToastTemplate::textField(_In_ TextField pos) const { + const auto position = static_cast(pos); + assert(position < _textFields.size()); + return _textFields[position]; +} + +const std::wstring& WinToastTemplate::actionLabel(_In_ std::size_t position) const { + assert(position < _actions.size()); + return _actions[position]; +} + +const std::wstring& WinToastTemplate::imagePath() const { + return _imagePath; +} + +const std::wstring& WinToastTemplate::audioPath() const { + return _audioPath; +} + +const std::wstring& WinToastTemplate::attributionText() const { + return _attributionText; +} + +INT64 WinToastTemplate::expiration() const { + return _expiration; +} + +WinToastTemplate::WinToastTemplateType WinToastTemplate::type() const { + return _type; +} + +WinToastTemplate::AudioOption WinToastTemplate::audioOption() const { + return _audioOption; +} + +WinToastTemplate::Duration WinToastTemplate::duration() const { + return _duration; +} \ No newline at end of file diff --git a/libraries/wintoast/wintoastlib.h b/libraries/wintoast/wintoastlib.h new file mode 100644 index 0000000..6b31cc3 --- /dev/null +++ b/libraries/wintoast/wintoastlib.h @@ -0,0 +1,217 @@ +/* * Copyright (C) 2016-2019 Mohammed Boujemaoui + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef WINTOASTLIB_H +#define WINTOASTLIB_H +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace Microsoft::WRL; +using namespace ABI::Windows::Data::Xml::Dom; +using namespace ABI::Windows::Foundation; +using namespace ABI::Windows::UI::Notifications; +using namespace Windows::Foundation; + + +namespace WinToastLib { + + class IWinToastHandler { + public: + enum WinToastDismissalReason { + UserCanceled = ToastDismissalReason::ToastDismissalReason_UserCanceled, + ApplicationHidden = ToastDismissalReason::ToastDismissalReason_ApplicationHidden, + TimedOut = ToastDismissalReason::ToastDismissalReason_TimedOut + }; + virtual ~IWinToastHandler() = default; + virtual void toastActivated() const = 0; + virtual void toastActivated(int actionIndex) const = 0; + virtual void toastDismissed(WinToastDismissalReason state) const = 0; + virtual void toastFailed() const = 0; + }; + + class WinToastTemplate { + public: + enum Duration { System, Short, Long }; + enum AudioOption { Default = 0, Silent, Loop }; + enum TextField { FirstLine = 0, SecondLine, ThirdLine }; + enum WinToastTemplateType { + ImageAndText01 = ToastTemplateType::ToastTemplateType_ToastImageAndText01, + ImageAndText02 = ToastTemplateType::ToastTemplateType_ToastImageAndText02, + ImageAndText03 = ToastTemplateType::ToastTemplateType_ToastImageAndText03, + ImageAndText04 = ToastTemplateType::ToastTemplateType_ToastImageAndText04, + Text01 = ToastTemplateType::ToastTemplateType_ToastText01, + Text02 = ToastTemplateType::ToastTemplateType_ToastText02, + Text03 = ToastTemplateType::ToastTemplateType_ToastText03, + Text04 = ToastTemplateType::ToastTemplateType_ToastText04, + }; + + enum AudioSystemFile { + DefaultSound, + IM, + Mail, + Reminder, + SMS, + Alarm, + Alarm2, + Alarm3, + Alarm4, + Alarm5, + Alarm6, + Alarm7, + Alarm8, + Alarm9, + Alarm10, + Call, + Call1, + Call2, + Call3, + Call4, + Call5, + Call6, + Call7, + Call8, + Call9, + Call10, + }; + + + WinToastTemplate(_In_ WinToastTemplateType type = WinToastTemplateType::ImageAndText02); + ~WinToastTemplate(); + + void setFirstLine(_In_ const std::wstring& text); + void setSecondLine(_In_ const std::wstring& text); + void setThirdLine(_In_ const std::wstring& text); + void setTextField(_In_ const std::wstring& txt, _In_ TextField pos); + void setAttributionText(_In_ const std::wstring& attributionText); + void setImagePath(_In_ const std::wstring& imgPath); + void setAudioPath(_In_ WinToastTemplate::AudioSystemFile audio); + void setAudioPath(_In_ const std::wstring& audioPath); + void setAudioOption(_In_ WinToastTemplate::AudioOption audioOption); + void setDuration(_In_ Duration duration); + void setExpiration(_In_ INT64 millisecondsFromNow); + void addAction(_In_ const std::wstring& label); + + std::size_t textFieldsCount() const; + std::size_t actionsCount() const; + bool hasImage() const; + const std::vector& textFields() const; + const std::wstring& textField(_In_ TextField pos) const; + const std::wstring& actionLabel(_In_ std::size_t pos) const; + const std::wstring& imagePath() const; + const std::wstring& audioPath() const; + const std::wstring& attributionText() const; + INT64 expiration() const; + WinToastTemplateType type() const; + WinToastTemplate::AudioOption audioOption() const; + Duration duration() const; + private: + std::vector _textFields{}; + std::vector _actions{}; + std::wstring _imagePath{}; + std::wstring _audioPath{}; + std::wstring _attributionText{}; + INT64 _expiration{ 0 }; + AudioOption _audioOption{ WinToastTemplate::AudioOption::Default }; + WinToastTemplateType _type{ WinToastTemplateType::Text01 }; + Duration _duration{ Duration::System }; + }; + + class WinToast { + public: + enum WinToastError { + NoError = 0, + NotInitialized, + SystemNotSupported, + ShellLinkNotCreated, + InvalidAppUserModelID, + InvalidParameters, + InvalidHandler, + NotDisplayed, + UnknownError + }; + + enum ShortcutResult { + SHORTCUT_UNCHANGED = 0, + SHORTCUT_WAS_CHANGED = 1, + SHORTCUT_WAS_CREATED = 2, + + SHORTCUT_MISSING_PARAMETERS = -1, + SHORTCUT_INCOMPATIBLE_OS = -2, + SHORTCUT_COM_INIT_FAILURE = -3, + SHORTCUT_CREATE_FAILED = -4 + }; + + WinToast(void); + virtual ~WinToast(); + static WinToast* instance(); + static bool isCompatible(); + static bool isSupportingModernFeatures(); + static std::wstring configureAUMI(_In_ const std::wstring& companyName, + _In_ const std::wstring& productName, + _In_ const std::wstring& subProduct = std::wstring(), + _In_ const std::wstring& versionInformation = std::wstring()); + static const std::wstring& strerror(_In_ WinToastError error); + virtual bool initialize(_Out_ WinToastError* error = nullptr); + virtual bool isInitialized() const; + virtual bool hideToast(_In_ INT64 id); + virtual INT64 showToast(_In_ const WinToastTemplate& toast, _In_ std::shared_ptr handler, _Out_ WinToastError* error = nullptr); + virtual void clear(); + virtual enum ShortcutResult createShortcut(); + + const std::wstring& appName() const; + const std::wstring& appUserModelId() const; + void setAppUserModelId(_In_ const std::wstring& aumi); + void setAppName(_In_ const std::wstring& appName); + + protected: + bool _isInitialized{ false }; + bool _hasCoInitialized{ false }; + std::wstring _appName{}; + std::wstring _aumi{}; + std::map> _buffer{}; + + HRESULT validateShellLinkHelper(_Out_ bool& wasChanged); + HRESULT createShellLinkHelper(); + HRESULT setImageFieldHelper(_In_ IXmlDocument* xml, _In_ const std::wstring& path); + HRESULT setAudioFieldHelper(_In_ IXmlDocument* xml, _In_ const std::wstring& path, _In_opt_ WinToastTemplate::AudioOption option = WinToastTemplate::AudioOption::Default); + HRESULT setTextFieldHelper(_In_ IXmlDocument* xml, _In_ const std::wstring& text, _In_ UINT32 pos); + HRESULT setAttributionTextFieldHelper(_In_ IXmlDocument* xml, _In_ const std::wstring& text); + HRESULT addActionHelper(_In_ IXmlDocument* xml, _In_ const std::wstring& action, _In_ const std::wstring& arguments); + HRESULT addDurationHelper(_In_ IXmlDocument* xml, _In_ const std::wstring& duration); + ComPtr notifier(_In_ bool* succeded) const; + void setError(_Out_ WinToastError* error, _In_ WinToastError value); + }; +} +#endif // WINTOASTLIB_H \ No newline at end of file diff --git a/resources.rc b/resources.rc new file mode 100644 index 0000000..b60489e --- /dev/null +++ b/resources.rc @@ -0,0 +1,27 @@ +APP_ICON ICON "icon.ico" + +#include + +VS_VERSION_INFO VERSIONINFO +FILEVERSION 1, 3, 0, 0 +PRODUCTVERSION 1, 3, 0, 0 +{ + BLOCK "StringFileInfo" + { + BLOCK "040904b0" + { + VALUE "CompanyName", "WorkingRobot" + VALUE "FileDescription", "EGL2" + VALUE "FileVersion", "1.0.0.0" + VALUE "InternalName", "egl2" + VALUE "LegalCopyright", "EGL2 (c) by Aleks Margarian (WorkingRobot)" + VALUE "OriginalFilename", "EGL2.exe" + VALUE "ProductName", "EGL2" + VALUE "ProductVersion", "1.3.0.0" + } + } + BLOCK "VarFileInfo" + { + VALUE "Translation", 0x409, 1200 + } +} \ No newline at end of file diff --git a/storage/compression.cpp b/storage/compression.cpp index 2e68d60..8fcef88 100644 --- a/storage/compression.cpp +++ b/storage/compression.cpp @@ -1,115 +1,168 @@ #include "compression.h" +#ifndef LOG_SECTION +#define LOG_SECTION "Compressor" +#endif + +#include "../Logger.h" #include "storage.h" -#include -#include +Compressor::Compressor(uint32_t storageFlags) : + StorageFlags(storageFlags) { + switch (StorageFlags & StorageCompMethodMask) + { + case StorageDecompressed: + { + CompressFunc = std::make_pair, size_t>; + break; + } + case StorageZstd: + { + switch (StorageFlags & StorageCompLevelMask) + { + case StorageCompressFastest: + CLevel = -3; + break; + case StorageCompressFast: + CLevel = -1; + break; + case StorageCompressNormal: + CLevel = 1; + break; + case StorageCompressSlow: + CLevel = 3; + break; + case StorageCompressSlowest: + CLevel = 5; + break; + } + CCtx = std::make_unique>([]() { return ZSTD_createCCtx(); }, [](void* cctx) { ZSTD_freeCCtx((ZSTD_CCtx*)cctx); }); + + CompressFunc = [this](std::shared_ptr& buffer, size_t buffer_size) { + auto outBuf = std::shared_ptr(new char[ZSTD_COMPRESSBOUND(buffer_size)]); + size_t outSize; + { + std::unique_lock lock; + auto& cctx = CCtx->GetCtx(lock); + outSize = ZSTD_compressCCtx((ZSTD_CCtx*)cctx, outBuf.get(), ZSTD_COMPRESSBOUND(buffer_size), buffer.get(), buffer_size, CLevel); + } + return std::make_pair(outBuf, outSize); + }; + break; + } + case StorageLZ4: + { + switch (StorageFlags & StorageCompLevelMask) + { + case StorageCompressFastest: + CLevel = LZ4HC_CLEVEL_MIN; + break; + case StorageCompressFast: + CLevel = 6; + break; + case StorageCompressNormal: + CLevel = LZ4HC_CLEVEL_DEFAULT; + break; + case StorageCompressSlow: + CLevel = LZ4HC_CLEVEL_OPT_MIN; + break; + case StorageCompressSlowest: + CLevel = LZ4HC_CLEVEL_MAX; + break; + } + + CCtx = std::make_unique>([]() { return _aligned_malloc(LZ4_sizeofStateHC(), 8); }, &_aligned_free); + + CompressFunc = [this](std::shared_ptr& buffer, size_t buffer_size) { + auto outBuf = std::shared_ptr(new char[LZ4_COMPRESSBOUND(buffer_size)]); + size_t outSize; + { + std::unique_lock lock; + auto& cctx = CCtx->GetCtx(lock); + outSize = LZ4_compress_HC_extStateHC(cctx, buffer.get(), outBuf.get(), buffer_size, LZ4_COMPRESSBOUND(buffer_size), CLevel); + } + return std::make_pair(outBuf, outSize); + }; + break; + } + } + + ZlibDCtx = std::make_unique>(&libdeflate_alloc_decompressor, &libdeflate_free_decompressor); + ZstdDCtx = std::make_unique>([]() {return ZSTD_createDCtx(); }, [](ZSTD_DCtx* ctx) { ZSTD_freeDCtx(ctx); }); +} + +Compressor::~Compressor() { + +} -bool ZlibDecompress(FILE* File, DECOMPRESS_ALLOCATOR Allocator) { +Compressor::buffer_value Compressor::StorageCompress(std::shared_ptr buffer, size_t buffer_size) +{ + return CompressFunc(buffer, buffer_size); +} + +Compressor::buffer_value Compressor::ZlibDecompress(FILE* File, size_t& inBufSize) +{ uint32_t uncompressedSize; fread(&uncompressedSize, sizeof(uint32_t), 1, File); auto pos = ftell(File); fseek(File, 0, SEEK_END); - auto inBufSize = ftell(File) - pos; + inBufSize = ftell(File) - pos; fseek(File, pos, SEEK_SET); - char* inBuffer = new char[inBufSize]; - fread(inBuffer, 1, inBufSize, File); + auto inBuffer = std::make_unique(inBufSize); + fread(inBuffer.get(), 1, inBufSize, File); - auto decompressor = libdeflate_alloc_decompressor(); + auto outBuffer = std::shared_ptr(new char[uncompressedSize]); - auto& outBuffer = Allocator(uncompressedSize); - libdeflate_zlib_decompress(decompressor, inBuffer, inBufSize, outBuffer.get(), uncompressedSize, NULL); + { + std::unique_lock lock; + auto& dctx = ZlibDCtx->GetCtx(lock); + libdeflate_zlib_decompress(dctx, inBuffer.get(), inBufSize, outBuffer.get(), uncompressedSize, NULL); + } - libdeflate_free_decompressor(decompressor); - delete[] inBuffer; - return true; + return std::make_pair(outBuffer, uncompressedSize); } -bool LZ4Decompress(FILE* File, DECOMPRESS_ALLOCATOR Allocator) { +Compressor::buffer_value Compressor::ZstdDecompress(FILE* File, size_t& inBufSize) +{ uint32_t uncompressedSize; fread(&uncompressedSize, sizeof(uint32_t), 1, File); auto pos = ftell(File); fseek(File, 0, SEEK_END); - auto inBufSize = ftell(File) - pos; + inBufSize = ftell(File) - pos; fseek(File, pos, SEEK_SET); - char* inBuffer = new char[inBufSize]; - fread(inBuffer, 1, inBufSize, File); + auto inBuffer = std::make_unique(inBufSize); + fread(inBuffer.get(), 1, inBufSize, File); - auto& outBuffer = Allocator(uncompressedSize); - LZ4_decompress_fast(inBuffer, outBuffer.get(), uncompressedSize); - - delete[] inBuffer; - return true; -} + auto outBuffer = std::shared_ptr(new char[uncompressedSize]); -bool ZlibCompress(uint32_t Flags, const char* Buffer, uint32_t BufferSize, char** POutBuffer, uint32_t* POutBufferSize) { - int compression_level; - if (Flags & StorageCompressFastest) { - compression_level = 1; - } - else if (Flags & StorageCompressFast) { - compression_level = 4; - } - else if (Flags & StorageCompressNormal) { - compression_level = 6; - } - else if (Flags & StorageCompressSlow) { - compression_level = 9; - } - else if (Flags & StorageCompressSlowest) { - compression_level = 12; + { + std::unique_lock lock; + auto& dctx = ZstdDCtx->GetCtx(lock); + ZSTD_decompressDCtx(dctx, outBuffer.get(), uncompressedSize, inBuffer.get(), inBufSize); } - else { - return false; - } - - auto compressor = libdeflate_alloc_compressor(compression_level); - - uint32_t outBufSize = libdeflate_zlib_compress_bound(compressor, BufferSize); - char* outBuffer = new char[outBufSize]; - - uint32_t compressedSize = libdeflate_zlib_compress(compressor, Buffer, BufferSize, outBuffer, outBufSize); - - *POutBuffer = outBuffer; - *POutBufferSize = compressedSize; - libdeflate_free_compressor(compressor); + return std::make_pair(outBuffer, uncompressedSize); } -bool LZ4Compress(uint32_t Flags, const char* Buffer, uint32_t BufferSize, char** POutBuffer, uint32_t* POutBufferSize) { - int compression_level; - if (Flags & StorageCompressFastest) { - compression_level = LZ4HC_CLEVEL_MIN; - } - else if (Flags & StorageCompressFast) { - compression_level = 6; - } - else if (Flags & StorageCompressNormal) { - compression_level = LZ4HC_CLEVEL_DEFAULT; - } - else if (Flags & StorageCompressSlow) { - compression_level = LZ4HC_CLEVEL_OPT_MIN; // uses "HC" at this point - } - else if (Flags & StorageCompressSlowest) { - compression_level = LZ4HC_CLEVEL_MAX; - } - else { - return false; - } +Compressor::buffer_value Compressor::LZ4Decompress(FILE* File, size_t& inBufSize) +{ + uint32_t uncompressedSize; + fread(&uncompressedSize, sizeof(uint32_t), 1, File); - uint32_t outBufSize = LZ4_COMPRESSBOUND(BufferSize); - char* outBuffer = new char[outBufSize]; - - uint32_t compressedSize = LZ4_compress_HC(Buffer, outBuffer, BufferSize, outBufSize, compression_level); + auto pos = ftell(File); + fseek(File, 0, SEEK_END); + inBufSize = ftell(File) - pos; + fseek(File, pos, SEEK_SET); - *POutBuffer = outBuffer; - *POutBufferSize = compressedSize; -} + auto inBuffer = std::make_unique(inBufSize); + fread(inBuffer.get(), 1, inBufSize, File); + + auto outBuffer = std::shared_ptr(new char[uncompressedSize]); + LZ4_decompress_safe(inBuffer.get(), outBuffer.get(), inBufSize, uncompressedSize); -void DeleteCompressBuffer(char* OutBuffer) { - delete[] OutBuffer; + return std::make_pair(outBuffer, uncompressedSize); } \ No newline at end of file diff --git a/storage/compression.h b/storage/compression.h index 666e2bf..214d0d9 100644 --- a/storage/compression.h +++ b/storage/compression.h @@ -1,14 +1,74 @@ #pragma once +#include +#include #include #include +#include +#include +#include +#include -typedef std::function& (uint32_t size)> DECOMPRESS_ALLOCATOR; +template +class CtxManager { +public: + typedef std::function create_ctx; + typedef std::function delete_ctx; -bool ZlibDecompress(FILE* File, DECOMPRESS_ALLOCATOR Allocator); + CtxManager(create_ctx create, delete_ctx delete_) : + CreateCtx(create), + DeleteCtx(delete_) { } -bool LZ4Decompress(FILE* File, DECOMPRESS_ALLOCATOR Allocator); + ~CtxManager() { + for (auto& ctx : Ctx) { + DeleteCtx(ctx); + } + } -bool ZlibCompress(uint32_t Flags, const char* Buffer, uint32_t BufferSize, char** OutBuffer, uint32_t* POutBufferSize); + T& GetCtx(std::unique_lock& lock) { + std::unique_lock lck(Mutex); -bool LZ4Compress(uint32_t Flags, const char* Buffer, uint32_t BufferSize, char** OutBuffer, uint32_t* POutBufferSize); + auto mtxIt = Mutexes.begin(); + for (auto ctxIt = Ctx.begin(); ctxIt != Ctx.end(); ctxIt++, mtxIt++) { + lock = std::unique_lock(*mtxIt, std::try_to_lock); + if (lock.owns_lock()) { + return *ctxIt; + } + } + + lock = std::unique_lock(Mutexes.emplace_back()); + return Ctx.emplace_back(CreateCtx()); + } + +private: + create_ctx CreateCtx; + delete_ctx DeleteCtx; + + std::mutex Mutex; + std::deque Ctx; + std::deque Mutexes; +}; + +class Compressor { +public: + using buffer_value = std::pair, size_t>; + + Compressor(uint32_t storageFlags); + ~Compressor(); + + buffer_value StorageCompress(std::shared_ptr buffer, size_t buffer_size); + + buffer_value ZlibDecompress(FILE* File, size_t& inBufSize); + buffer_value ZstdDecompress(FILE* File, size_t& inBufSize); + buffer_value LZ4Decompress(FILE* File, size_t& inBufSize); + +private: + std::function, size_t)> CompressFunc; + + int CLevel; + std::unique_ptr> CCtx; + std::unique_ptr> ZlibDCtx; + std::unique_ptr> ZstdDCtx; + //std::unique_ptr> LZ4DCtx; lz4 decompression doesn't use a ctx + uint32_t StorageFlags; +}; \ No newline at end of file diff --git a/storage/sha.h b/storage/sha.h index fb6686f..861eec6 100644 --- a/storage/sha.h +++ b/storage/sha.h @@ -1,7 +1,7 @@ #pragma once -#include #include +#include bool VerifyHash(const char* input, uint32_t inputSize, const char Sha[20]) { char calculatedHash[20]; diff --git a/storage/storage.cpp b/storage/storage.cpp index 96d4845..be6e7a8 100644 --- a/storage/storage.cpp +++ b/storage/storage.cpp @@ -1,122 +1,170 @@ #include "storage.h" -#define STORAGE_CHUNKS_RESERVE 64 // number of chunks to keep in memory at the same time +#ifndef LOG_SECTION +#define LOG_SECTION "Storage" +#endif -#include "../web/http.h" -#include "../containers/iterable_queue.h" -#include "compression.h" +#include "../Logger.h" +#include "../Stats.h" #include "sha.h" #include -#include -#include -namespace fs = std::filesystem; +Storage::Storage(uint32_t Flags, uint32_t ChunkPoolCapacity, fs::path CacheLocation, std::string CloudDir) : + Flags(Flags), + ChunkPoolCapacity(ChunkPoolCapacity), + CachePath(CacheLocation), + CloudDir(CloudDir), + Compressor(Flags) +{ } -enum class CHUNK_STATUS { - Unavailable, // Readable from download - Grabbing, // Downloading - Available, // Readable from local copy - Reading, // Reading from local copy - Readable // Readable from memory -}; +Storage::~Storage() +{ + +} -typedef struct _CHUNK_POOL_DATA { - std::unique_ptr Buffer; - std::condition_variable CV; - std::mutex Mutex; - CHUNK_STATUS Status; -} CHUNK_POOL_DATA; - -auto hash = [](const char* n) { return (*((uint64_t*)n)) ^ (*(((uint64_t*)n) + 1)); }; -auto equal = [](const char* a, const char* b) {return !memcmp(a, b, 16); }; -// this can be an ordered map, but i'm unsure about the memory usage of this, even if all 81k chunks are read -typedef iterable_queue>> STORAGE_CHUNK_POOL_LOOKUP; - -typedef struct _STORAGE { - fs::path cachePath; - uint32_t flags; - std::string CloudDir; // CloudDir also includes the /ChunksV3/ part, though - std::unique_ptr Client; - std::unique_ptr ChunkPoolMutex; - STORAGE_CHUNK_POOL_LOOKUP ChunkPool; -} STORAGE; - -bool StorageCreate( - uint32_t Flags, - const wchar_t* CacheLocation, - const char* ChunkHost, - const char* CloudDir, - STORAGE** PStorage) { - STORAGE* Storage = new STORAGE; - Storage->cachePath = fs::path(CacheLocation); - Storage->flags = Flags; - Storage->CloudDir = CloudDir; - Storage->Client = std::make_unique(ChunkHost); - Storage->ChunkPoolMutex = std::make_unique(); - *PStorage = Storage; - return true; +bool Storage::IsChunkDownloaded(std::shared_ptr Chunk) +{ + return fs::status(CachePath / Chunk->GetFilePath()).type() == fs::file_type::regular; } -void StorageDelete(STORAGE* Storage) { - // delete buffers and stuff in pool lookup table - delete Storage; +bool Storage::IsChunkDownloaded(ChunkPart& ChunkPart) +{ + return IsChunkDownloaded(ChunkPart.Chunk); } -inline CHUNK_STATUS StorageGetChunkStatus(STORAGE* Storage, char Guid[16]) { - char GuidString[33]; - sprintf(GuidString, "%016llX%016llX", ntohll(*(uint64_t*)Guid), ntohll(*(uint64_t*)(Guid + 8))); +bool Storage::VerifyChunk(std::shared_ptr Chunk) +{ + Compressor::buffer_value chunkData; + if (!ReadChunk(CachePath / Chunk->GetFilePath(), chunkData)) { + return false; + } + Stats::ProvideCount.fetch_add(chunkData.second, std::memory_order_relaxed); + return VerifyHash(chunkData.first.get(), chunkData.second, Chunk->ShaHash); +} - char GuidFolder[3]; - memcpy(GuidFolder, GuidString, 2); - GuidFolder[2] = '\0'; +void Storage::DeleteChunk(std::shared_ptr Chunk) +{ + fs::remove(CachePath / Chunk->GetFilePath()); +} + +std::shared_ptr Storage::GetChunk(std::shared_ptr Chunk) +{ + auto& data = GetPoolData(Chunk); + switch (data.Status) + { + case CHUNK_STATUS::Unavailable: + redownloadChunk: + { + // download + data.Status = CHUNK_STATUS::Grabbing; + + auto chunkData = DownloadChunk(Chunk); + { + std::unique_lock lck(data.Mutex); + data.Buffer = chunkData.first; + data.BufferSize = chunkData.second; + data.Status = CHUNK_STATUS::Readable; + data.CV.notify_all(); + } + + break; + } + case CHUNK_STATUS::Available: + { + // read from file + data.Status = CHUNK_STATUS::Reading; - if (fs::status(Storage->cachePath / GuidFolder / GuidString).type() != fs::file_type::regular) { - return CHUNK_STATUS::Unavailable; + Compressor::buffer_value chunkData; + + if (!ReadChunk(CachePath / Chunk->GetFilePath(), chunkData) || (Flags & StorageVerifyHashes && !VerifyHash(chunkData.first.get(), chunkData.second, Chunk->ShaHash))) { + DeleteChunk(Chunk); + data.Status = CHUNK_STATUS::Unavailable; + goto redownloadChunk; + } + + { + std::unique_lock lck(data.Mutex); + data.Buffer = chunkData.first; + data.BufferSize = chunkData.second; + data.Status = CHUNK_STATUS::Readable; + data.CV.notify_all(); + } + break; + } + case CHUNK_STATUS::Grabbing: // downloading from server, wait until mutex releases + case CHUNK_STATUS::Reading: // reading from file, wait until mutex releases + { + std::unique_lock lck(data.Mutex); + while (data.Status != CHUNK_STATUS::Readable) data.CV.wait(lck); + + break; + } + case CHUNK_STATUS::Readable: // available in memory pool + { + break; + } + default: + // h o w + break; + } + return data.Buffer; +} + +std::shared_ptr Storage::GetChunkPart(ChunkPart& ChunkPart) +{ + auto chunk = GetChunk(ChunkPart.Chunk); + if (ChunkPart.Offset == 0 && ChunkPart.Size == ChunkPart.Chunk->Size) { + return chunk; } else { - return CHUNK_STATUS::Available; + auto partBuffer = std::shared_ptr(new char[ChunkPart.Size]); + memcpy(partBuffer.get(), chunk.get() + ChunkPart.Offset, ChunkPart.Size); + return partBuffer; } } -std::weak_ptr StorageGetPoolData(STORAGE* Storage, char Guid[16]) { - std::lock_guard statusLock(*Storage->ChunkPoolMutex); - for (auto& chunk : Storage->ChunkPool) { - if (!memcmp(Guid, chunk.first, 16)) { +CHUNK_POOL_DATA& Storage::GetPoolData(std::shared_ptr Chunk) +{ + std::lock_guard statusLock(ChunkPoolMutex); + for (auto& chunk : ChunkPool) { + if (!memcmp(Chunk->Guid, chunk.first, 16)) { return chunk.second; } } - if (Storage->ChunkPool.size() == STORAGE_CHUNKS_RESERVE) + while (ChunkPool.size() >= ChunkPoolCapacity) { - Storage->ChunkPool.pop(); + ChunkPool.pop_front(); } - auto data = std::make_shared(); - - data->Buffer = std::unique_ptr(); - data->Status = StorageGetChunkStatus(Storage, Guid); - - Storage->ChunkPool.push(std::make_pair(Guid, data)); - return data; + auto& data = ChunkPool.emplace_back(); + data.first = Chunk->Guid; + data.second.Buffer = std::unique_ptr(); + data.second.Status = GetUnpooledChunkStatus(Chunk); + return data.second; } -std::unique_ptr& StorageGetBuffer(STORAGE* Storage, char Guid[16]) { - return StorageGetPoolData(Storage, Guid).lock()->Buffer; -} - -void StorageSetBuffer(std::shared_ptr data, uint32_t size) { - data->Buffer.reset(new char[size]); +CHUNK_STATUS Storage::GetUnpooledChunkStatus(std::shared_ptr Chunk) +{ + return IsChunkDownloaded(Chunk) ? CHUNK_STATUS::Available : CHUNK_STATUS::Unavailable; } +enum { + ChunkFlagDecompressed = 0x01, + ChunkFlagZstd = 0x02, + ChunkFlagZlib = 0x04, + ChunkFlagLZ4 = 0x08, + ChunkFlagCompMask = 0xF +}; #pragma pack(push, 1) -typedef struct _STORAGE_CHUNK_HEADER { +struct CHUNK_HEADER { uint16_t version; uint16_t flags; -} STORAGE_CHUNK_HEADER; +}; #define CHUNK_HEADER_MAGIC 0xB1FE3AA2 -typedef struct _STORAGE_CDN_CHUNK_HEADER { +struct CDN_CHUNK_HEADER { uint32_t Magic; uint32_t Version; uint32_t HeaderSize; @@ -124,140 +172,51 @@ typedef struct _STORAGE_CDN_CHUNK_HEADER { char Guid[16]; uint64_t RollingHash; uint8_t StoredAs; // EChunkStorageFlags -} STORAGE_CDN_CHUNK_HEADER; +}; -typedef struct _STORAGE_CDN_CHUNK_HEADER_V2 { +struct CDN_CHUNK_HEADER_V2 { char SHAHash[20]; uint8_t HashType; // EChunkHashFlags -} STORAGE_CDN_CHUNK_HEADER_V2; +}; -typedef struct _STORAGE_CDN_CHUNK_HEADER_V3 { +struct CDN_CHUNK_HEADER_V3 { uint32_t DataSizeUncompressed; -} STORAGE_CDN_CHUNK_HEADER_V3; +}; #pragma pack(pop) -inline bool StorageRead(fs::path path, std::function&(uint32_t size)> allocator) { - auto fp = fopen(path.string().c_str(), "rb"); - STORAGE_CHUNK_HEADER header; - fread(&header, sizeof(STORAGE_CHUNK_HEADER), 1, fp); - if (header.version != 0) { - printf("bad version!\n"); - return false; - } - if (header.flags & StorageDecompressed) { - auto pos = ftell(fp); - fseek(fp, 0, SEEK_END); - auto inBufSize = ftell(fp) - pos; - fseek(fp, pos, SEEK_SET); - - auto& buffer = allocator(inBufSize); - fread(buffer.get(), 1, inBufSize, fp); - fclose(fp); - return true; - } - else if (header.flags & StorageCompressZlib) { // zlib compressed - auto result = ZlibDecompress(fp, allocator); - - fclose(fp); - return result; - } - else if (header.flags & StorageCompressLZ4) { // lz4 compressed - auto result = LZ4Decompress(fp, allocator); - - fclose(fp); - return result; - } - fclose(fp); - printf("unknown read flag!\n"); - return false; -} - -inline void StorageWrite(const char* Path, uint16_t Flags, uint32_t DecompressedSize, const char* Buffer, uint32_t BufferSize) { - auto fp = fopen(Path, "wb"); - if (!fp) - printf("ERRNO %s: %d\n", Path, errno); - STORAGE_CHUNK_HEADER chunkHeader; - chunkHeader.version = 0; - chunkHeader.flags = Flags; - fwrite(&chunkHeader, sizeof(STORAGE_CHUNK_HEADER), 1, fp); - if (!(Flags & StorageDecompressed)) { // Compressed chunks write the decompressed size - fwrite(&DecompressedSize, sizeof(uint32_t), 1, fp); - } - fwrite(Buffer, 1, BufferSize, fp); - fclose(fp); -} - -bool StorageChunkDownloaded(STORAGE* Storage, MANIFEST_CHUNK* Chunk) { - return StorageGetChunkStatus(Storage, ManifestChunkGetGuid(Chunk)) != CHUNK_STATUS::Unavailable; -} - -bool StorageVerifyChunk(STORAGE* Storage, MANIFEST_CHUNK* Chunk) { - if (StorageChunkDownloaded(Storage, Chunk)) { - auto guid = ManifestChunkGetGuid(Chunk); - - char GuidString[33]; - sprintf(GuidString, "%016llX%016llX", ntohll(*(uint64_t*)guid), ntohll(*(uint64_t*)(guid + 8))); - - char GuidFolder[3]; - memcpy(GuidFolder, GuidString, 2); - GuidFolder[2] = '\0'; - - std::unique_ptr _buf; - uint32_t _bufSize; - StorageRead(Storage->cachePath / GuidFolder / GuidString, [&](uint32_t size) -> std::unique_ptr& { - _buf.reset(new char[size]); - _bufSize = size; - return _buf; - }); - - if (!VerifyHash(_buf.get(), _bufSize, ManifestChunkGetSha1(Chunk))) { - fs::remove(Storage->cachePath / GuidFolder / GuidString); - StorageDownloadChunk(Storage, Chunk, [](const char* buf, uint32_t bufSize) {}); - } - return true; - } - else { - return false; - } -} - -void StorageDownloadChunk(STORAGE* Storage, MANIFEST_CHUNK* Chunk, std::function DataCallback) { +Compressor::buffer_value Storage::DownloadChunk(std::shared_ptr Chunk) +{ std::vector chunkData; { - chunkData.reserve(8192); - bool chunkDataReserved = false; - - char UrlBuffer[256]; - strcpy(UrlBuffer, Storage->CloudDir.c_str()); - ManifestChunkAppendUrl(Chunk, UrlBuffer); - Storage->Client->Get(UrlBuffer, - [&](const char* data, uint64_t data_length) { - chunkData.insert(chunkData.end(), data, data + data_length); - return true; - }, - [&](uint64_t len, uint64_t total) { - if (!chunkDataReserved) { - chunkData.reserve(total); - chunkDataReserved = true; - } - return true; - } - ); + auto chunkConn = Client::CreateConnection(); + chunkConn->SetUrl(CloudDir + Chunk->GetUrl()); + chunkConn->Start(); + + if (chunkConn->GetResponseCode() != 200) { + LOG_ERROR("Response code %d", chunkConn->GetResponseCode()); + LOG_WARN("Retrying..."); + return DownloadChunk(Chunk); + } + + chunkData.reserve(chunkConn->GetResponseBody().size()); + Stats::DownloadCount.fetch_add(chunkConn->GetResponseBody().size(), std::memory_order_relaxed); + std::copy(chunkConn->GetResponseBody().begin(), chunkConn->GetResponseBody().end(), std::back_inserter(chunkData)); } - uint32_t decompressedSize = 1024 * 1024; + size_t decompressedSize = 1024 * 1024; - auto headerv1 = *(STORAGE_CDN_CHUNK_HEADER*)chunkData.data(); - auto chunkPos = sizeof(STORAGE_CDN_CHUNK_HEADER); + auto headerv1 = *(CDN_CHUNK_HEADER*)chunkData.data(); + auto chunkPos = sizeof(CDN_CHUNK_HEADER); if (headerv1.Magic != CHUNK_HEADER_MAGIC) { - printf("magic invalid\n"); - return; + LOG_ERROR("Downloaded chunk (%s) magic invalid: %08X", Chunk->GetGuid(), headerv1.Magic); + LOG_WARN("Retrying..."); + return DownloadChunk(Chunk); } if (headerv1.Version >= 2) { - auto headerv2 = *(STORAGE_CDN_CHUNK_HEADER_V2*)(chunkData.data() + chunkPos); - chunkPos += sizeof(STORAGE_CDN_CHUNK_HEADER_V2); + auto headerv2 = *(CDN_CHUNK_HEADER_V2*)(chunkData.data() + chunkPos); + chunkPos += sizeof(CDN_CHUNK_HEADER_V2); if (headerv1.Version >= 3) { - auto headerv3 = *(STORAGE_CDN_CHUNK_HEADER_V3*)(chunkData.data() + chunkPos); + auto headerv3 = *(CDN_CHUNK_HEADER_V3*)(chunkData.data() + chunkPos); decompressedSize = headerv3.DataSizeUncompressed; if (headerv1.Version > 3) { // version past 3 @@ -268,162 +227,115 @@ void StorageDownloadChunk(STORAGE* Storage, MANIFEST_CHUNK* Chunk, std::function if (headerv1.StoredAs & 0x02) // encrypted { - printf("encrypted?\n"); - return; // no support yet, i have never seen this used in practice + LOG_ERROR("Downloaded chunk (%s) is encrypted", Chunk->GetGuid()); + //return; // no support yet, i have never seen this used in practice } - char GuidString[33]; - { - auto guid = ManifestChunkGetGuid(Chunk); - sprintf(GuidString, "%016llX%016llX", ntohll(*(uint64_t*)guid), ntohll(*(uint64_t*)(guid + 8))); - } - - char GuidFolder[3]; - memcpy(GuidFolder, GuidString, 2); - GuidFolder[2] = '\0'; - - auto guidPathStr = (Storage->cachePath / GuidFolder / GuidString).string(); - auto guidPath = guidPathStr.c_str(); + auto guidPath = CachePath / Chunk->GetFilePath(); auto bufferPtr = chunkData.data() + chunkPos; + auto data = std::shared_ptr(new char[decompressedSize]); if (headerv1.StoredAs & 0x01) // compressed { - auto data = std::make_unique(decompressedSize); - { - auto decompressor = libdeflate_alloc_decompressor(); - auto result = libdeflate_zlib_decompress(decompressor, bufferPtr, headerv1.DataSizeCompressed, data.get(), decompressedSize, NULL); - DataCallback(data.get(), decompressedSize); - libdeflate_free_decompressor(decompressor); - } - - if (Storage->flags & StorageCompressed) { - StorageWrite(guidPath, StorageCompressZlib, decompressedSize, bufferPtr, headerv1.DataSizeCompressed); - } - else if (Storage->flags & StorageDecompressed) { - StorageWrite(guidPath, StorageDecompressed, 0, data.get(), decompressedSize); - } - else if (Storage->flags & StorageCompressZlib) { - char* compressedBuffer; - uint32_t compressedBufferSize; - ZlibCompress(Storage->flags, data.get(), decompressedSize, &compressedBuffer, &compressedBufferSize); - StorageWrite(guidPath, StorageCompressZlib, decompressedSize, compressedBuffer, compressedBufferSize); - delete[] compressedBuffer; - } - else if (Storage->flags & StorageCompressLZ4) { - char* compressedBuffer; - uint32_t compressedBufferSize; - LZ4Compress(Storage->flags, data.get(), decompressedSize, &compressedBuffer, &compressedBufferSize); - StorageWrite(guidPath, StorageCompressLZ4, decompressedSize, compressedBuffer, compressedBufferSize); - delete[] compressedBuffer; - } + auto decompressor = libdeflate_alloc_decompressor(); // TODO: use ctxmanager for this + auto result = libdeflate_zlib_decompress(decompressor, bufferPtr, headerv1.DataSizeCompressed, data.get(), decompressedSize, NULL); + libdeflate_free_decompressor(decompressor); } else { - DataCallback(bufferPtr, decompressedSize); - - if (Storage->flags & (StorageCompressed | StorageDecompressed)) { - StorageWrite(guidPath, StorageDecompressed, 0, bufferPtr, decompressedSize); - } - else if (Storage->flags & StorageCompressZlib) { - char* compressedBuffer; - uint32_t compressedBufferSize; - ZlibCompress(Storage->flags, bufferPtr, decompressedSize, &compressedBuffer, &compressedBufferSize); - StorageWrite(guidPath, StorageCompressZlib, decompressedSize, compressedBuffer, compressedBufferSize); - delete[] compressedBuffer; - } - else if (Storage->flags & StorageCompressLZ4) { - char* compressedBuffer; - uint32_t compressedBufferSize; - LZ4Compress(Storage->flags, bufferPtr, decompressedSize, &compressedBuffer, &compressedBufferSize); - StorageWrite(guidPath, StorageCompressLZ4, decompressedSize, compressedBuffer, compressedBufferSize); - delete[] compressedBuffer; - } + memcpy(data.get(), bufferPtr, decompressedSize); } + WriteChunk(guidPath, decompressedSize, Compressor.StorageCompress(data, decompressedSize)); + return std::make_pair(data, decompressedSize); } -// thread safe, downloads if needed, etc. -void StorageDownloadChunkPart(STORAGE* Storage, MANIFEST_CHUNK* Chunk, uint32_t Offset, uint32_t Size, char* Buffer) { - auto guid = ManifestChunkGetGuid(Chunk); - auto data = StorageGetPoolData(Storage, guid).lock(); - switch (data->Status) +bool Storage::ReadChunk(fs::path Path, Compressor::buffer_value& ReadBuffer) +{ + auto fp = fopen(Path.string().c_str(), "rb"); + CHUNK_HEADER header; + fread(&header, sizeof(CHUNK_HEADER), 1, fp); + if (header.version != 0) { + LOG_ERROR("Bad chunk version for %s: %hu", Path.string().c_str(), header.version); + return false; + } + switch (header.flags & ChunkFlagCompMask) { - case CHUNK_STATUS::Unavailable: -redownloadChunk: + case ChunkFlagDecompressed: { - // download - data->Status = CHUNK_STATUS::Grabbing; - - StorageDownloadChunk(Storage, Chunk, [&](const char* Buffer_, uint32_t BufferSize) { - std::unique_lock lck(data->Mutex); - StorageSetBuffer(data, BufferSize); - memcpy(data->Buffer.get(), Buffer_, BufferSize); + auto pos = ftell(fp); + fseek(fp, 0, SEEK_END); + auto inBufSize = ftell(fp) - pos; + fseek(fp, pos, SEEK_SET); - memcpy(Buffer, Buffer_ + Offset, Size); + auto buffer = std::shared_ptr(new char[inBufSize]); + fread(buffer.get(), 1, inBufSize, fp); + fclose(fp); - data->Status = CHUNK_STATUS::Readable; - data->CV.notify_all(); - }); - break; + ReadBuffer = std::make_pair(buffer, inBufSize); + Stats::FileReadCount.fetch_add(inBufSize, std::memory_order_relaxed); + return true; } - case CHUNK_STATUS::Available: + case ChunkFlagZstd: { - // read from file - data->Status = CHUNK_STATUS::Reading; - - char GuidString[33]; - sprintf(GuidString, "%016llX%016llX", ntohll(*(uint64_t*)guid), ntohll(*(uint64_t*)(guid + 8))); - - char GuidFolder[3]; - memcpy(GuidFolder, GuidString, 2); - GuidFolder[2] = '\0'; - - char* _buf; - uint32_t _bufSize; - StorageRead(Storage->cachePath / GuidFolder / GuidString, [&](uint32_t size) -> std::unique_ptr& { - auto data = StorageGetPoolData(Storage, guid).lock(); - StorageSetBuffer(data, size); - _buf = data->Buffer.get(); - _bufSize = size; - return data->Buffer; - }); - - if (Storage->flags & StorageVerifyHashes) { - if (!VerifyHash(_buf, _bufSize, ManifestChunkGetSha1(Chunk))) { - fs::remove(Storage->cachePath / GuidFolder / GuidString); - data->Status = CHUNK_STATUS::Unavailable; - StorageDownloadChunkPart(Storage, Chunk, Offset, Size, Buffer); - return; - } - } - - std::unique_lock lck(data->Mutex); - data->Status = CHUNK_STATUS::Readable; - data->CV.notify_all(); + size_t inBufSize; + auto result = Compressor.ZstdDecompress(fp, inBufSize); - memcpy(Buffer, StorageGetBuffer(Storage, guid).get() + Offset, Size); - break; + fclose(fp); + ReadBuffer = result; + Stats::FileReadCount.fetch_add(inBufSize, std::memory_order_relaxed); + return true; } - case CHUNK_STATUS::Grabbing: // downloading from server, wait until mutex releases - case CHUNK_STATUS::Reading: // reading from file, wait until mutex releases + case ChunkFlagZlib: { - std::unique_lock lck(data->Mutex); - while (data->Status != CHUNK_STATUS::Readable) data->CV.wait(lck); + size_t inBufSize; + auto result = Compressor.ZlibDecompress(fp, inBufSize); - memcpy(Buffer, StorageGetBuffer(Storage, guid).get() + Offset, Size); - break; + fclose(fp); + ReadBuffer = result; + Stats::FileReadCount.fetch_add(inBufSize, std::memory_order_relaxed); + return true; } - case CHUNK_STATUS::Readable: // available in memory pool + case ChunkFlagLZ4: { - memcpy(Buffer, StorageGetBuffer(Storage, guid).get() + Offset, Size); - break; + size_t inBufSize; + auto result = Compressor.LZ4Decompress(fp, inBufSize); + + fclose(fp); + ReadBuffer = result; + Stats::FileReadCount.fetch_add(inBufSize, std::memory_order_relaxed); + return true; } default: - // h o w - break; + { + fclose(fp); + LOG_ERROR("Unknown read flag for %s: %hu", Path.string().c_str(), header.flags); + return false; + } } } -void StorageDownloadChunkPart(STORAGE* Storage, MANIFEST_CHUNK_PART* ChunkPart, char* Buffer) { - uint32_t Offset, Size; - ManifestFileChunkGetData(ChunkPart, &Offset, &Size); - StorageDownloadChunkPart(Storage, ManifestFileChunkGetChunk(ChunkPart), Offset, Size, Buffer); -} \ No newline at end of file +void Storage::WriteChunk(fs::path Path, uint32_t DecompressedSize, Compressor::buffer_value& Buffer) +{ + auto fp = fopen(Path.string().c_str(), "wb"); + CHUNK_HEADER chunkHeader; + chunkHeader.version = 0; + switch (Flags & StorageCompMethodMask) + { + case StorageDecompressed: + chunkHeader.flags = ChunkFlagDecompressed; + break; + case StorageZstd: + chunkHeader.flags = ChunkFlagZstd; + break; + case StorageLZ4: + chunkHeader.flags = ChunkFlagLZ4; + break; + } + fwrite(&chunkHeader, sizeof(CHUNK_HEADER), 1, fp); + if ((Flags & StorageCompMethodMask) != StorageDecompressed) { // Compressed chunks write the decompressed size + fwrite(&DecompressedSize, sizeof(uint32_t), 1, fp); + } + fwrite(Buffer.first.get(), 1, Buffer.second, fp); + fclose(fp); + + Stats::FileWriteCount.fetch_add(Buffer.second, std::memory_order_relaxed); +} diff --git a/storage/storage.h b/storage/storage.h index 82d42d0..e12c2bd 100644 --- a/storage/storage.h +++ b/storage/storage.h @@ -1,38 +1,79 @@ #pragma once -#include "../web/manifest.h" +#include "../web/http.h" +#include "../web/manifest/manifest.h" +#include "compression.h" +#include +#include +#include +#include #include - -typedef struct _STORAGE STORAGE; +#include +namespace fs = std::filesystem; enum { StorageDecompressed = 0x00000001, // Chunks decompressed to solid blocks - StorageCompressed = 0x00000002, // Chunks stay compressed in their downloaded form (this flag isn't used in chunk cache files) - - StorageCompressZlib = 0x00000004, // Chunks are recompressed with Zlib - StorageCompressLZ4 = 0x00000008, // Chunks are recompressed with LZ4 + StorageZstd = 0x00000002, // Chunks are recompressed with Zlib + StorageLZ4 = 0x00000003, // Chunks are recompressed with LZ4 + StorageCompMethodMask = 0x0000000F, // Chunks are recompressed with LZ4 StorageCompressFastest = 0x00000010, // Zlib = 1 StorageCompressFast = 0x00000020, // Zlib = 4 - StorageCompressNormal = 0x00000040, // Zlib = 6 - StorageCompressSlow = 0x00000080, // Zlib = 9 - StorageCompressSlowest = 0x00000100, // Zlib = 12 + StorageCompressNormal = 0x00000030, // Zlib = 6 + StorageCompressSlow = 0x00000040, // Zlib = 9 + StorageCompressSlowest = 0x00000050, // Zlib = 12 + StorageCompLevelMask = 0x000000F0, // Chunks are recompressed with LZ4 StorageVerifyHashes = 0x00001000, // Verify SHA hashes of downloaded chunks when reading and redownload if invalid }; -bool StorageCreate( - uint32_t Flags, - const wchar_t* CacheLocation, - const char* ChunkHost, - const char* CloudDir, - STORAGE** PStorage); -void StorageDelete(STORAGE* Storage); - -bool StorageChunkDownloaded(STORAGE* Storage, MANIFEST_CHUNK* Chunk); -bool StorageVerifyChunk(STORAGE* Storage, MANIFEST_CHUNK* Chunk); -void StorageDownloadChunk(STORAGE* Storage, MANIFEST_CHUNK* Chunk, std::function DataCallback); -void StorageDownloadChunkPart(STORAGE* Storage, MANIFEST_CHUNK* Chunk, uint32_t Offset, uint32_t Size, char* Buffer); -void StorageDownloadChunkPart(STORAGE* Storage, MANIFEST_CHUNK_PART* ChunkPart, char* Buffer); +enum class CHUNK_STATUS { + Unavailable, // Readable from download + Grabbing, // Downloading + Available, // Readable from local copy + Reading, // Reading from local copy + Readable // Readable from memory +}; + +struct CHUNK_POOL_DATA { + std::shared_ptr Buffer; + uint32_t BufferSize; + std::condition_variable CV; + std::mutex Mutex; + std::atomic Status; +}; + +auto hash = [](const char* n) { return (*((uint64_t*)n)) ^ (*(((uint64_t*)n) + 1)); }; +auto equal = [](const char* a, const char* b) {return !memcmp(a, b, 16); }; +// this can be an ordered map, but i'm unsure about the memory usage of this, even if all 81k chunks are read +typedef std::deque> STORAGE_CHUNK_POOL_LOOKUP; + +class Storage { +public: + Storage(uint32_t Flags, uint32_t ChunkPoolCapacity, fs::path CacheLocation, std::string CloudDir); + ~Storage(); + + bool IsChunkDownloaded(std::shared_ptr Chunk); + bool IsChunkDownloaded(ChunkPart& ChunkPart); + bool VerifyChunk(std::shared_ptr Chunk); + void DeleteChunk(std::shared_ptr Chunk); + std::shared_ptr GetChunk(std::shared_ptr Chunk); + std::shared_ptr GetChunkPart(ChunkPart& ChunkPart); + +private: + CHUNK_POOL_DATA& GetPoolData(std::shared_ptr Chunk); + CHUNK_STATUS GetUnpooledChunkStatus(std::shared_ptr Chunk); + Compressor::buffer_value DownloadChunk(std::shared_ptr Chunk); + bool ReadChunk(fs::path Path, Compressor::buffer_value& ReadBuffer); + void WriteChunk(fs::path Path, uint32_t DecompressedSize, Compressor::buffer_value& Buffer); + + fs::path CachePath; + uint32_t Flags; + std::string CloudDir; // CloudDir also includes the /ChunksV3/ part, though + Compressor Compressor; + std::mutex ChunkPoolMutex; + STORAGE_CHUNK_POOL_LOOKUP ChunkPool; + uint32_t ChunkPoolCapacity; +}; \ No newline at end of file diff --git a/web/http.h b/web/http.h index f3caf80..eb62354 100644 --- a/web/http.h +++ b/web/http.h @@ -1,4 +1,3 @@ #pragma once -#include "httplib.h" -#include "url.hh" \ No newline at end of file +#include "http/Client.h" \ No newline at end of file diff --git a/web/http/Client.cpp b/web/http/Client.cpp new file mode 100644 index 0000000..a507edc --- /dev/null +++ b/web/http/Client.cpp @@ -0,0 +1,217 @@ +#include "Client.h" + +#include + +class AsioTimer : public curlion::Timer { +public: + explicit AsioTimer(boost::asio::io_service& io_service) : timer_(io_service) { + + } + + void Start(long timeout_ms, const std::function& callback) override { + + timer_.expires_from_now(boost::posix_time::milliseconds(timeout_ms)); + timer_.async_wait(std::bind(&AsioTimer::TimerTriggered, this, callback, std::placeholders::_1)); + } + + void Stop() override { + + timer_.cancel(); + } + +private: + void TimerTriggered(const std::function& callback, const boost::system::error_code& error) { + + if (error == boost::asio::error::operation_aborted) { + return; + } + + callback(); + } + +private: + boost::asio::deadline_timer timer_; +}; + +class AsioSingleSocketWatcher : public std::enable_shared_from_this { +public: + AsioSingleSocketWatcher(const std::shared_ptr& socket, + curlion::SocketWatcher::Event event, + const curlion::SocketWatcher::EventCallback& callback) : + socket_(socket), + event_(event), + callback_(callback), + is_stopped_(false) { + + + } + + void Start() { + + Watch(); + } + + void Stop() { + + //Stop may be called inside an event callback, so a flag variable + //is needed for stopping the watching. + is_stopped_ = true; + + //The socket may be closed before calling Stop, check for + //preventing exception. + if (socket_->is_open()) { + socket_->cancel(); + } + } + +private: + void Watch() { + + bool watch_read_event = (event_ == curlion::SocketWatcher::Event::Read) || (event_ == curlion::SocketWatcher::Event::ReadWrite); + bool watch_write_event = (event_ == curlion::SocketWatcher::Event::Write) || (event_ == curlion::SocketWatcher::Event::ReadWrite); + + if (watch_read_event) { + + auto bridge_callback = std::bind(&AsioSingleSocketWatcher::EventTriggered, + this->shared_from_this(), + false, + std::placeholders::_1, + std::placeholders::_2); + + socket_->async_read_some(boost::asio::null_buffers(), bridge_callback); + } + + if (watch_write_event) { + + auto bridge_callback = std::bind(&AsioSingleSocketWatcher::EventTriggered, + this->shared_from_this(), + true, + std::placeholders::_1, + std::placeholders::_2); + + socket_->async_write_some(boost::asio::null_buffers(), bridge_callback); + } + } + + void EventTriggered(bool can_write, + const boost::system::error_code& error, + std::size_t size) { + + if (error == boost::asio::error::operation_aborted) { + return; + } + + callback_(socket_->native_handle(), can_write); + + //End watching once stopped. + if (!is_stopped_) { + Watch(); + } + } + +private: + std::shared_ptr socket_; + curlion::SocketWatcher::Event event_; + curlion::SocketWatcher::EventCallback callback_; + bool is_stopped_; +}; + +class AsioSocketManager : public curlion::SocketFactory, public curlion::SocketWatcher { +public: + explicit AsioSocketManager(boost::asio::io_service& io_service) : io_service_(io_service) { + + } + + + curl_socket_t Open(curlsocktype socket_type, const curl_sockaddr* address) override { + + if ((socket_type != CURLSOCKTYPE_IPCXN) || (address->family != AF_INET)) { + return CURL_SOCKET_BAD; + } + + auto asio_socket = std::make_shared(io_service_); + + boost::system::error_code error; + asio_socket->open(boost::asio::ip::tcp::v4(), error); + + if (error) { + return CURL_SOCKET_BAD; + } + + sockets_.insert(std::make_pair(asio_socket->native_handle(), asio_socket)); + return asio_socket->native_handle(); + } + + + bool Close(curl_socket_t socket) override { + + auto iterator = sockets_.find(socket); + if (iterator == sockets_.end()) { + return false; + } + + auto asio_socket = iterator->second; + sockets_.erase(iterator); + + boost::system::error_code error; + asio_socket->close(error); + return !error; + } + + + void Watch(curl_socket_t socket, Event event, const EventCallback& callback) override { + + auto iterator = sockets_.find(socket); + if (iterator == sockets_.end()) { + return; + } + + auto watcher = std::make_shared(iterator->second, event, callback); + watcher->Start(); + watchers_.insert(std::make_pair(socket, watcher)); + } + + + void StopWatching(curl_socket_t socket) override { + + auto iterator = watchers_.find(socket); + if (iterator == watchers_.end()) { + return; + } + + auto watcher = iterator->second; + watcher->Stop(); + watchers_.erase(iterator); + } + +private: + std::map> sockets_; + std::map> watchers_; + boost::asio::io_service& io_service_; +}; + +Client::Client() +{ + io_service = new boost::asio::io_service; + // auto work = std::make_shared(io_service); + + auto timer = std::make_shared(*(boost::asio::io_service*)io_service); + auto socket_manager = std::make_shared(*(boost::asio::io_service*)io_service); + + connection_manager = std::make_unique(socket_manager, socket_manager, timer); +} + +Client::~Client() +{ + delete io_service; +} + +std::error_condition Client::StartConnection(const std::shared_ptr& connection) +{ + return connection_manager->StartConnection(connection); +} + +std::error_condition Client::AbortConnection(const std::shared_ptr& connection) +{ + return connection_manager->AbortConnection(connection); +} \ No newline at end of file diff --git a/web/http/Client.h b/web/http/Client.h new file mode 100644 index 0000000..fdbacf8 --- /dev/null +++ b/web/http/Client.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +class Client { +public: + Client(); + ~Client(); + + std::error_condition StartConnection(const std::shared_ptr& connection); + std::error_condition AbortConnection(const std::shared_ptr& connection); + + static std::shared_ptr CreateConnection() { + auto conn = std::make_shared(); + //conn->SetVerbose(true); + //conn->SetProxy("127.0.0.1:8888"); + //conn->SetVerifyCertificate(false); + return conn; + } + +private: + void* io_service; + std::unique_ptr connection_manager; +}; \ No newline at end of file diff --git a/web/httplib.cc b/web/httplib.cc deleted file mode 100644 index d901514..0000000 --- a/web/httplib.cc +++ /dev/null @@ -1,4007 +0,0 @@ -#include "httplib.h" -namespace httplib { - - /* - * Implementation - */ - - namespace detail { - - bool is_hex(char c, int& v) { - if (0x20 <= c && isdigit(c)) { - v = c - '0'; - return true; - } - else if ('A' <= c && c <= 'F') { - v = c - 'A' + 10; - return true; - } - else if ('a' <= c && c <= 'f') { - v = c - 'a' + 10; - return true; - } - return false; - } - - bool from_hex_to_i(const std::string& s, size_t i, size_t cnt, - int& val) { - if (i >= s.size()) { return false; } - - val = 0; - for (; cnt; i++, cnt--) { - if (!s[i]) { return false; } - int v = 0; - if (is_hex(s[i], v)) { - val = val * 16 + v; - } - else { - return false; - } - } - return true; - } - - std::string from_i_to_hex(size_t n) { - const char* charset = "0123456789abcdef"; - std::string ret; - do { - ret = charset[n & 15] + ret; - n >>= 4; - } while (n > 0); - return ret; - } - - size_t to_utf8(int code, char* buff) { - if (code < 0x0080) { - buff[0] = (code & 0x7F); - return 1; - } - else if (code < 0x0800) { - buff[0] = static_cast(0xC0 | ((code >> 6) & 0x1F)); - buff[1] = static_cast(0x80 | (code & 0x3F)); - return 2; - } - else if (code < 0xD800) { - buff[0] = static_cast(0xE0 | ((code >> 12) & 0xF)); - buff[1] = static_cast(0x80 | ((code >> 6) & 0x3F)); - buff[2] = static_cast(0x80 | (code & 0x3F)); - return 3; - } - else if (code < 0xE000) { // D800 - DFFF is invalid... - return 0; - } - else if (code < 0x10000) { - buff[0] = static_cast(0xE0 | ((code >> 12) & 0xF)); - buff[1] = static_cast(0x80 | ((code >> 6) & 0x3F)); - buff[2] = static_cast(0x80 | (code & 0x3F)); - return 3; - } - else if (code < 0x110000) { - buff[0] = static_cast(0xF0 | ((code >> 18) & 0x7)); - buff[1] = static_cast(0x80 | ((code >> 12) & 0x3F)); - buff[2] = static_cast(0x80 | ((code >> 6) & 0x3F)); - buff[3] = static_cast(0x80 | (code & 0x3F)); - return 4; - } - - // NOTREACHED - return 0; - } - - // NOTE: This code came up with the following stackoverflow post: - // https://stackoverflow.com/questions/180947/base64-decode-snippet-in-c - std::string base64_encode(const std::string& in) { - static const auto lookup = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - - std::string out; - out.reserve(in.size()); - - int val = 0; - int valb = -6; - - for (auto c : in) { - val = (val << 8) + static_cast(c); - valb += 8; - while (valb >= 0) { - out.push_back(lookup[(val >> valb) & 0x3F]); - valb -= 6; - } - } - - if (valb > -6) { out.push_back(lookup[((val << 8) >> (valb + 8)) & 0x3F]); } - - while (out.size() % 4) { - out.push_back('='); - } - - return out; - } - - bool is_file(const std::string& path) { - struct stat st; - return stat(path.c_str(), &st) >= 0 && S_ISREG(st.st_mode); - } - - bool is_dir(const std::string& path) { - struct stat st; - return stat(path.c_str(), &st) >= 0 && S_ISDIR(st.st_mode); - } - - bool is_valid_path(const std::string& path) { - size_t level = 0; - size_t i = 0; - - // Skip slash - while (i < path.size() && path[i] == '/') { - i++; - } - - while (i < path.size()) { - // Read component - auto beg = i; - while (i < path.size() && path[i] != '/') { - i++; - } - - auto len = i - beg; - assert(len > 0); - - if (!path.compare(beg, len, ".")) { - ; - } - else if (!path.compare(beg, len, "..")) { - if (level == 0) { return false; } - level--; - } - else { - level++; - } - - // Skip slash - while (i < path.size() && path[i] == '/') { - i++; - } - } - - return true; - } - - void read_file(const std::string& path, std::string& out) { - std::ifstream fs(path, std::ios_base::binary); - fs.seekg(0, std::ios_base::end); - auto size = fs.tellg(); - fs.seekg(0); - out.resize(static_cast(size)); - fs.read(&out[0], size); - } - - std::string file_extension(const std::string& path) { - std::smatch m; - static auto re = std::regex("\\.([a-zA-Z0-9]+)$"); - if (std::regex_search(path, m, re)) { return m[1].str(); } - return std::string(); - } - - template void split(const char* b, const char* e, char d, Fn fn) { - int i = 0; - int beg = 0; - - while (e ? (b + i != e) : (b[i] != '\0')) { - if (b[i] == d) { - fn(&b[beg], &b[i]); - beg = i + 1; - } - i++; - } - - if (i) { fn(&b[beg], &b[i]); } - } - - // NOTE: until the read size reaches `fixed_buffer_size`, use `fixed_buffer` - // to store data. The call can set memory on stack for performance. - class stream_line_reader { - public: - stream_line_reader(Stream& strm, char* fixed_buffer, size_t fixed_buffer_size) - : strm_(strm), fixed_buffer_(fixed_buffer), - fixed_buffer_size_(fixed_buffer_size) {} - - const char* ptr() const { - if (glowable_buffer_.empty()) { - return fixed_buffer_; - } - else { - return glowable_buffer_.data(); - } - } - - size_t size() const { - if (glowable_buffer_.empty()) { - return fixed_buffer_used_size_; - } - else { - return glowable_buffer_.size(); - } - } - - bool end_with_crlf() const { - auto end = ptr() + size(); - return size() >= 2 && end[-2] == '\r' && end[-1] == '\n'; - } - - bool getline() { - fixed_buffer_used_size_ = 0; - glowable_buffer_.clear(); - - for (size_t i = 0;; i++) { - char byte; - auto n = strm_.read(&byte, 1); - - if (n < 0) { - return false; - } - else if (n == 0) { - if (i == 0) { - return false; - } - else { - break; - } - } - - append(byte); - - if (byte == '\n') { break; } - } - - return true; - } - - private: - void append(char c) { - if (fixed_buffer_used_size_ < fixed_buffer_size_ - 1) { - fixed_buffer_[fixed_buffer_used_size_++] = c; - fixed_buffer_[fixed_buffer_used_size_] = '\0'; - } - else { - if (glowable_buffer_.empty()) { - assert(fixed_buffer_[fixed_buffer_used_size_] == '\0'); - glowable_buffer_.assign(fixed_buffer_, fixed_buffer_used_size_); - } - glowable_buffer_ += c; - } - } - - Stream& strm_; - char* fixed_buffer_; - const size_t fixed_buffer_size_; - size_t fixed_buffer_used_size_ = 0; - std::string glowable_buffer_; - }; - - int close_socket(socket_t sock) { -#ifdef _WIN32 - return closesocket(sock); -#else - return close(sock); -#endif - } - - int select_read(socket_t sock, time_t sec, time_t usec) { -#ifdef CPPHTTPLIB_USE_POLL - struct pollfd pfd_read; - pfd_read.fd = sock; - pfd_read.events = POLLIN; - - auto timeout = static_cast(sec * 1000 + usec / 1000); - - return poll(&pfd_read, 1, timeout); -#else - fd_set fds; - FD_ZERO(&fds); - FD_SET(sock, &fds); - - timeval tv; - tv.tv_sec = static_cast(sec); - tv.tv_usec = static_cast(usec); - - return select(static_cast(sock + 1), &fds, nullptr, nullptr, &tv); -#endif - } - - int select_write(socket_t sock, time_t sec, time_t usec) { -#ifdef CPPHTTPLIB_USE_POLL - struct pollfd pfd_read; - pfd_read.fd = sock; - pfd_read.events = POLLOUT; - - auto timeout = static_cast(sec * 1000 + usec / 1000); - - return poll(&pfd_read, 1, timeout); -#else - fd_set fds; - FD_ZERO(&fds); - FD_SET(sock, &fds); - - timeval tv; - tv.tv_sec = static_cast(sec); - tv.tv_usec = static_cast(usec); - - return select(static_cast(sock + 1), nullptr, &fds, nullptr, &tv); -#endif - } - - bool wait_until_socket_is_ready(socket_t sock, time_t sec, time_t usec) { -#ifdef CPPHTTPLIB_USE_POLL - struct pollfd pfd_read; - pfd_read.fd = sock; - pfd_read.events = POLLIN | POLLOUT; - - auto timeout = static_cast(sec * 1000 + usec / 1000); - - if (poll(&pfd_read, 1, timeout) > 0 && - pfd_read.revents & (POLLIN | POLLOUT)) { - int error = 0; - socklen_t len = sizeof(error); - return getsockopt(sock, SOL_SOCKET, SO_ERROR, - reinterpret_cast(&error), &len) >= 0 && - !error; - } - return false; -#else - fd_set fdsr; - FD_ZERO(&fdsr); - FD_SET(sock, &fdsr); - - auto fdsw = fdsr; - auto fdse = fdsr; - - timeval tv; - tv.tv_sec = static_cast(sec); - tv.tv_usec = static_cast(usec); - - if (select(static_cast(sock + 1), &fdsr, &fdsw, &fdse, &tv) > 0 && - (FD_ISSET(sock, &fdsr) || FD_ISSET(sock, &fdsw))) { - int error = 0; - socklen_t len = sizeof(error); - return getsockopt(sock, SOL_SOCKET, SO_ERROR, - reinterpret_cast(&error), &len) >= 0 && - !error; - } - return false; -#endif - } - - class SocketStream : public Stream { - public: - SocketStream(socket_t sock, time_t read_timeout_sec, - time_t read_timeout_usec); - ~SocketStream() override; - - bool is_readable() const override; - bool is_writable() const override; - ssize_t read(char* ptr, size_t size) override; - ssize_t write(const char* ptr, size_t size) override; - std::string get_remote_addr() const override; - - private: - socket_t sock_; - time_t read_timeout_sec_; - time_t read_timeout_usec_; - }; - -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - class SSLSocketStream : public Stream { - public: - SSLSocketStream(socket_t sock, SSL* ssl, time_t read_timeout_sec, - time_t read_timeout_usec); - ~SSLSocketStream() override; - - bool is_readable() const override; - bool is_writable() const override; - ssize_t read(char* ptr, size_t size) override; - ssize_t write(const char* ptr, size_t size) override; - std::string get_remote_addr() const override; - - private: - socket_t sock_; - SSL* ssl_; - time_t read_timeout_sec_; - time_t read_timeout_usec_; - }; -#endif - - class BufferStream : public Stream { - public: - BufferStream() = default; - ~BufferStream() override = default; - - bool is_readable() const override; - bool is_writable() const override; - ssize_t read(char* ptr, size_t size) override; - ssize_t write(const char* ptr, size_t size) override; - std::string get_remote_addr() const override; - - const std::string& get_buffer() const; - - private: - std::string buffer; - size_t position = 0; - }; - - template - bool process_socket(bool is_client_request, socket_t sock, - size_t keep_alive_max_count, time_t read_timeout_sec, - time_t read_timeout_usec, T callback) { - assert(keep_alive_max_count > 0); - - auto ret = false; - - if (keep_alive_max_count > 1) { - auto count = keep_alive_max_count; - while (count > 0 && - (is_client_request || - select_read(sock, CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND, - CPPHTTPLIB_KEEPALIVE_TIMEOUT_USECOND) > 0)) { - SocketStream strm(sock, read_timeout_sec, read_timeout_usec); - auto last_connection = count == 1; - auto connection_close = false; - - ret = callback(strm, last_connection, connection_close); - if (!ret || connection_close) { break; } - - count--; - } - } - else { // keep_alive_max_count is 0 or 1 - SocketStream strm(sock, read_timeout_sec, read_timeout_usec); - auto dummy_connection_close = false; - ret = callback(strm, true, dummy_connection_close); - } - - return ret; - } - - template - bool process_and_close_socket(bool is_client_request, socket_t sock, - size_t keep_alive_max_count, - time_t read_timeout_sec, - time_t read_timeout_usec, T callback) { - auto ret = process_socket(is_client_request, sock, keep_alive_max_count, - read_timeout_sec, read_timeout_usec, callback); - close_socket(sock); - return ret; - } - - int shutdown_socket(socket_t sock) { -#ifdef _WIN32 - return shutdown(sock, SD_BOTH); -#else - return shutdown(sock, SHUT_RDWR); -#endif - } - - template - socket_t create_socket(const char* host, int port, Fn fn, - int socket_flags = 0) { -#ifdef _WIN32 -#define SO_SYNCHRONOUS_NONALERT 0x20 -#define SO_OPENTYPE 0x7008 - - int opt = SO_SYNCHRONOUS_NONALERT; - setsockopt(INVALID_SOCKET, SOL_SOCKET, SO_OPENTYPE, (char*)&opt, - sizeof(opt)); -#endif - - // Get address info - struct addrinfo hints; - struct addrinfo* result; - - memset(&hints, 0, sizeof(struct addrinfo)); - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - hints.ai_flags = socket_flags; - hints.ai_protocol = 0; - - auto service = std::to_string(port); - - if (getaddrinfo(host, service.c_str(), &hints, &result)) { - return INVALID_SOCKET; - } - - for (auto rp = result; rp; rp = rp->ai_next) { - // Create a socket -#ifdef _WIN32 - auto sock = WSASocketW(rp->ai_family, rp->ai_socktype, rp->ai_protocol, - nullptr, 0, WSA_FLAG_NO_HANDLE_INHERIT); - /** - * Since the WSA_FLAG_NO_HANDLE_INHERIT is only supported on Windows 7 SP1 - * and above the socket creation fails on older Windows Systems. - * - * Let's try to create a socket the old way in this case. - * - * Reference: - * https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketa - * - * WSA_FLAG_NO_HANDLE_INHERIT: - * This flag is supported on Windows 7 with SP1, Windows Server 2008 R2 with - * SP1, and later - * - */ - if (sock == INVALID_SOCKET) { - sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); - } -#else - auto sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); -#endif - if (sock == INVALID_SOCKET) { continue; } - -#ifndef _WIN32 - if (fcntl(sock, F_SETFD, FD_CLOEXEC) == -1) { continue; } -#endif - - // Make 'reuse address' option available - int yes = 1; - setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&yes), - sizeof(yes)); -#ifdef SO_REUSEPORT - setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, reinterpret_cast(&yes), - sizeof(yes)); -#endif - - // bind or connect - if (fn(sock, *rp)) { - freeaddrinfo(result); - return sock; - } - - close_socket(sock); - } - - freeaddrinfo(result); - return INVALID_SOCKET; - } - - void set_nonblocking(socket_t sock, bool nonblocking) { -#ifdef _WIN32 - auto flags = nonblocking ? 1UL : 0UL; - ioctlsocket(sock, FIONBIO, &flags); -#else - auto flags = fcntl(sock, F_GETFL, 0); - fcntl(sock, F_SETFL, - nonblocking ? (flags | O_NONBLOCK) : (flags & (~O_NONBLOCK))); -#endif - } - - bool is_connection_error() { -#ifdef _WIN32 - return WSAGetLastError() != WSAEWOULDBLOCK; -#else - return errno != EINPROGRESS; -#endif - } - - bool bind_ip_address(socket_t sock, const char* host) { - struct addrinfo hints; - struct addrinfo* result; - - memset(&hints, 0, sizeof(struct addrinfo)); - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - hints.ai_protocol = 0; - - if (getaddrinfo(host, "0", &hints, &result)) { return false; } - - auto ret = false; - for (auto rp = result; rp; rp = rp->ai_next) { - const auto& ai = *rp; - if (!::bind(sock, ai.ai_addr, static_cast(ai.ai_addrlen))) { - ret = true; - break; - } - } - - freeaddrinfo(result); - return ret; - } - - std::string if2ip(const std::string& ifn) { -#ifndef _WIN32 - struct ifaddrs* ifap; - getifaddrs(&ifap); - for (auto ifa = ifap; ifa; ifa = ifa->ifa_next) { - if (ifa->ifa_addr && ifn == ifa->ifa_name) { - if (ifa->ifa_addr->sa_family == AF_INET) { - auto sa = reinterpret_cast(ifa->ifa_addr); - char buf[INET_ADDRSTRLEN]; - if (inet_ntop(AF_INET, &sa->sin_addr, buf, INET_ADDRSTRLEN)) { - freeifaddrs(ifap); - return std::string(buf, INET_ADDRSTRLEN); - } - } - } - } - freeifaddrs(ifap); -#endif - return std::string(); - } - - socket_t create_client_socket(const char* host, int port, - time_t timeout_sec, - const std::string& intf) { - return create_socket( - host, port, [&](socket_t sock, struct addrinfo& ai) -> bool { - if (!intf.empty()) { - auto ip = if2ip(intf); - if (ip.empty()) { ip = intf; } - if (!bind_ip_address(sock, ip.c_str())) { return false; } - } - - set_nonblocking(sock, true); - - auto ret = - ::connect(sock, ai.ai_addr, static_cast(ai.ai_addrlen)); - if (ret < 0) { - if (is_connection_error() || - !wait_until_socket_is_ready(sock, timeout_sec, 0)) { - close_socket(sock); - return false; - } - } - - set_nonblocking(sock, false); - return true; - }); - } - - std::string get_remote_addr(socket_t sock) { - struct sockaddr_storage addr; - socklen_t len = sizeof(addr); - - if (!getpeername(sock, reinterpret_cast(&addr), &len)) { - std::array ipstr{}; - - if (!getnameinfo(reinterpret_cast(&addr), len, - ipstr.data(), static_cast(ipstr.size()), - nullptr, 0, NI_NUMERICHOST)) { - return ipstr.data(); - } - } - - return std::string(); - } - - const char* - find_content_type(const std::string& path, - const std::map& user_data) { - auto ext = file_extension(path); - - auto it = user_data.find(ext); - if (it != user_data.end()) { return it->second.c_str(); } - - if (ext == "txt") { - return "text/plain"; - } - else if (ext == "html" || ext == "htm") { - return "text/html"; - } - else if (ext == "css") { - return "text/css"; - } - else if (ext == "jpeg" || ext == "jpg") { - return "image/jpg"; - } - else if (ext == "png") { - return "image/png"; - } - else if (ext == "gif") { - return "image/gif"; - } - else if (ext == "svg") { - return "image/svg+xml"; - } - else if (ext == "ico") { - return "image/x-icon"; - } - else if (ext == "json") { - return "application/json"; - } - else if (ext == "pdf") { - return "application/pdf"; - } - else if (ext == "js") { - return "application/javascript"; - } - else if (ext == "wasm") { - return "application/wasm"; - } - else if (ext == "xml") { - return "application/xml"; - } - else if (ext == "xhtml") { - return "application/xhtml+xml"; - } - return nullptr; - } - - const char* status_message(int status) { - switch (status) { - case 100: return "Continue"; - case 200: return "OK"; - case 202: return "Accepted"; - case 204: return "No Content"; - case 206: return "Partial Content"; - case 301: return "Moved Permanently"; - case 302: return "Found"; - case 303: return "See Other"; - case 304: return "Not Modified"; - case 400: return "Bad Request"; - case 401: return "Unauthorized"; - case 403: return "Forbidden"; - case 404: return "Not Found"; - case 413: return "Payload Too Large"; - case 414: return "Request-URI Too Long"; - case 415: return "Unsupported Media Type"; - case 416: return "Range Not Satisfiable"; - case 417: return "Expectation Failed"; - case 503: return "Service Unavailable"; - - default: - case 500: return "Internal Server Error"; - } - } - -#ifdef CPPHTTPLIB_ZLIB_SUPPORT - bool can_compress(const std::string& content_type) { - return !content_type.find("text/") || content_type == "image/svg+xml" || - content_type == "application/javascript" || - content_type == "application/json" || - content_type == "application/xml" || - content_type == "application/xhtml+xml"; - } - - bool compress(std::string& content) { - z_stream strm; - strm.zalloc = Z_NULL; - strm.zfree = Z_NULL; - strm.opaque = Z_NULL; - - auto ret = deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8, - Z_DEFAULT_STRATEGY); - if (ret != Z_OK) { return false; } - - strm.avail_in = static_cast(content.size()); - strm.next_in = - const_cast(reinterpret_cast(content.data())); - - std::string compressed; - - std::array buff{}; - do { - strm.avail_out = buff.size(); - strm.next_out = reinterpret_cast(buff.data()); - ret = deflate(&strm, Z_FINISH); - assert(ret != Z_STREAM_ERROR); - compressed.append(buff.data(), buff.size() - strm.avail_out); - } while (strm.avail_out == 0); - - assert(ret == Z_STREAM_END); - assert(strm.avail_in == 0); - - content.swap(compressed); - - deflateEnd(&strm); - return true; - } - - class decompressor { - public: - decompressor() { - std::memset(&strm, 0, sizeof(strm)); - strm.zalloc = Z_NULL; - strm.zfree = Z_NULL; - strm.opaque = Z_NULL; - - // 15 is the value of wbits, which should be at the maximum possible value - // to ensure that any gzip stream can be decoded. The offset of 32 specifies - // that the stream type should be automatically detected either gzip or - // deflate. - is_valid_ = inflateInit2(&strm, 32 + 15) == Z_OK; - } - - ~decompressor() { inflateEnd(&strm); } - - bool is_valid() const { return is_valid_; } - - template - bool decompress(const char* data, size_t data_length, T callback) { - int ret = Z_OK; - - strm.avail_in = static_cast(data_length); - strm.next_in = const_cast(reinterpret_cast(data)); - - std::array buff{}; - do { - strm.avail_out = buff.size(); - strm.next_out = reinterpret_cast(buff.data()); - - ret = inflate(&strm, Z_NO_FLUSH); - assert(ret != Z_STREAM_ERROR); - switch (ret) { - case Z_NEED_DICT: - case Z_DATA_ERROR: - case Z_MEM_ERROR: inflateEnd(&strm); return false; - } - - if (!callback(buff.data(), buff.size() - strm.avail_out)) { - return false; - } - } while (strm.avail_out == 0); - - return ret == Z_OK || ret == Z_STREAM_END; - } - - private: - bool is_valid_; - z_stream strm; - }; -#endif - - bool has_header(const Headers& headers, const char* key) { - return headers.find(key) != headers.end(); - } - - const char* get_header_value(const Headers& headers, const char* key, - size_t id = 0, const char* def = nullptr) { - auto it = headers.find(key); - std::advance(it, static_cast(id)); - if (it != headers.end()) { return it->second.c_str(); } - return def; - } - - uint64_t get_header_value_uint64(const Headers& headers, const char* key, - uint64_t def = 0) { - auto it = headers.find(key); - if (it != headers.end()) { - return std::strtoull(it->second.data(), nullptr, 10); - } - return def; - } - - bool read_headers(Stream& strm, Headers& headers) { - const auto bufsiz = 2048; - char buf[bufsiz]; - stream_line_reader line_reader(strm, buf, bufsiz); - - for (;;) { - if (!line_reader.getline()) { return false; } - - // Check if the line ends with CRLF. - if (line_reader.end_with_crlf()) { - // Blank line indicates end of headers. - if (line_reader.size() == 2) { break; } - } - else { - continue; // Skip invalid line. - } - - // Skip trailing spaces and tabs. - auto end = line_reader.ptr() + line_reader.size() - 2; - while (line_reader.ptr() < end && (end[-1] == ' ' || end[-1] == '\t')) { - end--; - } - - // Horizontal tab and ' ' are considered whitespace and are ignored when on - // the left or right side of the header value: - // - https://stackoverflow.com/questions/50179659/ - // - https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html - static const std::regex re(R"(([^:]+):[\t ]*(.+))"); - - std::cmatch m; - if (std::regex_match(line_reader.ptr(), end, m, re)) { - auto key = std::string(m[1]); - auto val = std::string(m[2]); - headers.emplace(key, val); - } - } - - return true; - } - - bool read_content_with_length(Stream& strm, uint64_t len, - Progress progress, ContentReceiver out) { - char buf[CPPHTTPLIB_RECV_BUFSIZ]; - - uint64_t r = 0; - while (r < len) { - auto read_len = static_cast(len - r); - auto n = strm.read(buf, std::min(read_len, CPPHTTPLIB_RECV_BUFSIZ)); - if (n <= 0) { return false; } - - if (!out(buf, static_cast(n))) { return false; } - - r += static_cast(n); - - if (progress) { - if (!progress(r, len)) { return false; } - } - } - - return true; - } - - void skip_content_with_length(Stream& strm, uint64_t len) { - char buf[CPPHTTPLIB_RECV_BUFSIZ]; - uint64_t r = 0; - while (r < len) { - auto read_len = static_cast(len - r); - auto n = strm.read(buf, std::min(read_len, CPPHTTPLIB_RECV_BUFSIZ)); - if (n <= 0) { return; } - r += static_cast(n); - } - } - - bool read_content_without_length(Stream& strm, ContentReceiver out) { - char buf[CPPHTTPLIB_RECV_BUFSIZ]; - for (;;) { - auto n = strm.read(buf, CPPHTTPLIB_RECV_BUFSIZ); - if (n < 0) { - return false; - } - else if (n == 0) { - return true; - } - if (!out(buf, static_cast(n))) { return false; } - } - - return true; - } - - bool read_content_chunked(Stream& strm, ContentReceiver out) { - const auto bufsiz = 16; - char buf[bufsiz]; - - stream_line_reader line_reader(strm, buf, bufsiz); - - if (!line_reader.getline()) { return false; } - - auto chunk_len = std::stoul(line_reader.ptr(), 0, 16); - - while (chunk_len > 0) { - if (!read_content_with_length(strm, chunk_len, nullptr, out)) { - return false; - } - - if (!line_reader.getline()) { return false; } - - if (strcmp(line_reader.ptr(), "\r\n")) { break; } - - if (!line_reader.getline()) { return false; } - - chunk_len = std::stoul(line_reader.ptr(), 0, 16); - } - - if (chunk_len == 0) { - // Reader terminator after chunks - if (!line_reader.getline() || strcmp(line_reader.ptr(), "\r\n")) - return false; - } - - return true; - } - - bool is_chunked_transfer_encoding(const Headers& headers) { - return !strcasecmp(get_header_value(headers, "Transfer-Encoding", 0, ""), - "chunked"); - } - - template - bool read_content(Stream& strm, T& x, size_t payload_max_length, int& status, - Progress progress, ContentReceiver receiver) { - - ContentReceiver out = [&](const char* buf, size_t n) { - return receiver(buf, n); - }; - -#ifdef CPPHTTPLIB_ZLIB_SUPPORT - decompressor decompressor; - - std::string content_encoding = x.get_header_value("Content-Encoding"); - if (content_encoding.find("gzip") != std::string::npos || - content_encoding.find("deflate") != std::string::npos) { - if (!decompressor.is_valid()) { - status = 500; - return false; - } - - out = [&](const char* buf, size_t n) { - return decompressor.decompress( - buf, n, [&](const char* buf, size_t n) { return receiver(buf, n); }); - }; - } -#else - if (x.get_header_value("Content-Encoding") == "gzip") { - status = 415; - return false; - } -#endif - - auto ret = true; - auto exceed_payload_max_length = false; - - if (is_chunked_transfer_encoding(x.headers)) { - ret = read_content_chunked(strm, out); - } - else if (!has_header(x.headers, "Content-Length")) { - ret = read_content_without_length(strm, out); - } - else { - auto len = get_header_value_uint64(x.headers, "Content-Length", 0); - if (len > payload_max_length) { - exceed_payload_max_length = true; - skip_content_with_length(strm, len); - ret = false; - } - else if (len > 0) { - ret = read_content_with_length(strm, len, progress, out); - } - } - - if (!ret) { status = exceed_payload_max_length ? 413 : 400; } - - return ret; - } - - template - ssize_t write_headers(Stream& strm, const T& info, - const Headers& headers) { - ssize_t write_len = 0; - for (const auto& x : info.headers) { - if (x.first == "EXCEPTION_WHAT") { continue; } - auto len = - strm.write_format("%s: %s\r\n", x.first.c_str(), x.second.c_str()); - if (len < 0) { return len; } - write_len += len; - } - for (const auto& x : headers) { - auto len = - strm.write_format("%s: %s\r\n", x.first.c_str(), x.second.c_str()); - if (len < 0) { return len; } - write_len += len; - } - auto len = strm.write("\r\n"); - if (len < 0) { return len; } - write_len += len; - return write_len; - } - - ssize_t write_content(Stream& strm, ContentProvider content_provider, - size_t offset, size_t length) { - size_t begin_offset = offset; - size_t end_offset = offset + length; - while (offset < end_offset) { - ssize_t written_length = 0; - - DataSink data_sink; - data_sink.write = [&](const char* d, size_t l) { - offset += l; - written_length = strm.write(d, l); - }; - data_sink.done = [&](void) { written_length = -1; }; - data_sink.is_writable = [&](void) { return strm.is_writable(); }; - - content_provider(offset, end_offset - offset, data_sink); - if (written_length < 0) { return written_length; } - } - return static_cast(offset - begin_offset); - } - - template - ssize_t write_content_chunked(Stream& strm, - ContentProvider content_provider, - T is_shutting_down) { - size_t offset = 0; - auto data_available = true; - ssize_t total_written_length = 0; - while (data_available && !is_shutting_down()) { - ssize_t written_length = 0; - - DataSink data_sink; - data_sink.write = [&](const char* d, size_t l) { - data_available = l > 0; - offset += l; - - // Emit chunked response header and footer for each chunk - auto chunk = from_i_to_hex(l) + "\r\n" + std::string(d, l) + "\r\n"; - written_length = strm.write(chunk); - }; - data_sink.done = [&](void) { - data_available = false; - written_length = strm.write("0\r\n\r\n"); - }; - data_sink.is_writable = [&](void) { return strm.is_writable(); }; - - content_provider(offset, 0, data_sink); - - if (written_length < 0) { return written_length; } - total_written_length += written_length; - } - return total_written_length; - } - - template - bool redirect(T& cli, const Request& req, Response& res, - const std::string& path) { - Request new_req = req; - new_req.path = path; - new_req.redirect_count -= 1; - - Response new_res; - - auto ret = cli.send(new_req, new_res); - if (ret) { res = new_res; } - return ret; - } - - std::string encode_url(const std::string& s) { - std::string result; - - for (size_t i = 0; s[i]; i++) { - switch (s[i]) { - case ' ': result += "%20"; break; - case '+': result += "%2B"; break; - case '\r': result += "%0D"; break; - case '\n': result += "%0A"; break; - case '\'': result += "%27"; break; - case ',': result += "%2C"; break; - // case ':': result += "%3A"; break; // ok? probably... - case ';': result += "%3B"; break; - default: - auto c = static_cast(s[i]); - if (c >= 0x80) { - result += '%'; - char hex[4]; - auto len = snprintf(hex, sizeof(hex) - 1, "%02X", c); - assert(len == 2); - result.append(hex, static_cast(len)); - } - else { - result += s[i]; - } - break; - } - } - - return result; - } - - std::string decode_url(const std::string& s, - bool convert_plus_to_space) { - std::string result; - - for (size_t i = 0; i < s.size(); i++) { - if (s[i] == '%' && i + 1 < s.size()) { - if (s[i + 1] == 'u') { - int val = 0; - if (from_hex_to_i(s, i + 2, 4, val)) { - // 4 digits Unicode codes - char buff[4]; - size_t len = to_utf8(val, buff); - if (len > 0) { result.append(buff, len); } - i += 5; // 'u0000' - } - else { - result += s[i]; - } - } - else { - int val = 0; - if (from_hex_to_i(s, i + 1, 2, val)) { - // 2 digits hex codes - result += static_cast(val); - i += 2; // '00' - } - else { - result += s[i]; - } - } - } - else if (convert_plus_to_space && s[i] == '+') { - result += ' '; - } - else { - result += s[i]; - } - } - - return result; - } - - std::string params_to_query_str(const Params& params) { - std::string query; - - for (auto it = params.begin(); it != params.end(); ++it) { - if (it != params.begin()) { query += "&"; } - query += it->first; - query += "="; - query += detail::encode_url(it->second); - } - - return query; - } - - void parse_query_text(const std::string& s, Params& params) { - split(&s[0], &s[s.size()], '&', [&](const char* b, const char* e) { - std::string key; - std::string val; - split(b, e, '=', [&](const char* b2, const char* e2) { - if (key.empty()) { - key.assign(b2, e2); - } - else { - val.assign(b2, e2); - } - }); - params.emplace(decode_url(key, true), decode_url(val, true)); - }); - } - - bool parse_multipart_boundary(const std::string& content_type, - std::string& boundary) { - auto pos = content_type.find("boundary="); - if (pos == std::string::npos) { return false; } - - boundary = content_type.substr(pos + 9); - return true; - } - - bool parse_range_header(const std::string& s, Ranges& ranges) { - static auto re_first_range = std::regex(R"(bytes=(\d*-\d*(?:,\s*\d*-\d*)*))"); - std::smatch m; - if (std::regex_match(s, m, re_first_range)) { - auto pos = static_cast(m.position(1)); - auto len = static_cast(m.length(1)); - bool all_valid_ranges = true; - split(&s[pos], &s[pos + len], ',', [&](const char* b, const char* e) { - if (!all_valid_ranges) return; - static auto re_another_range = std::regex(R"(\s*(\d*)-(\d*))"); - std::cmatch cm; - if (std::regex_match(b, e, cm, re_another_range)) { - ssize_t first = -1; - if (!cm.str(1).empty()) { - first = static_cast(std::stoll(cm.str(1))); - } - - ssize_t last = -1; - if (!cm.str(2).empty()) { - last = static_cast(std::stoll(cm.str(2))); - } - - if (first != -1 && last != -1 && first > last) { - all_valid_ranges = false; - return; - } - ranges.emplace_back(std::make_pair(first, last)); - } - }); - return all_valid_ranges; - } - return false; - } - - class MultipartFormDataParser { - public: - MultipartFormDataParser() {} - - void set_boundary(const std::string& boundary) { boundary_ = boundary; } - - bool is_valid() const { return is_valid_; } - - template - bool parse(const char* buf, size_t n, T content_callback, U header_callback) { - static const std::regex re_content_type(R"(^Content-Type:\s*(.*?)\s*$)", - std::regex_constants::icase); - - static const std::regex re_content_disposition( - "^Content-Disposition:\\s*form-data;\\s*name=\"(.*?)\"(?:;\\s*filename=" - "\"(.*?)\")?\\s*$", - std::regex_constants::icase); - - buf_.append(buf, n); // TODO: performance improvement - - while (!buf_.empty()) { - switch (state_) { - case 0: { // Initial boundary - auto pattern = dash_ + boundary_ + crlf_; - if (pattern.size() > buf_.size()) { return true; } - auto pos = buf_.find(pattern); - if (pos != 0) { - is_done_ = true; - return false; - } - buf_.erase(0, pattern.size()); - off_ += pattern.size(); - state_ = 1; - break; - } - case 1: { // New entry - clear_file_info(); - state_ = 2; - break; - } - case 2: { // Headers - auto pos = buf_.find(crlf_); - while (pos != std::string::npos) { - // Empty line - if (pos == 0) { - if (!header_callback(file_)) { - is_valid_ = false; - is_done_ = false; - return false; - } - buf_.erase(0, crlf_.size()); - off_ += crlf_.size(); - state_ = 3; - break; - } - - auto header = buf_.substr(0, pos); - { - std::smatch m; - if (std::regex_match(header, m, re_content_type)) { - file_.content_type = m[1]; - } - else if (std::regex_match(header, m, re_content_disposition)) { - file_.name = m[1]; - file_.filename = m[2]; - } - } - - buf_.erase(0, pos + crlf_.size()); - off_ += pos + crlf_.size(); - pos = buf_.find(crlf_); - } - break; - } - case 3: { // Body - { - auto pattern = crlf_ + dash_; - if (pattern.size() > buf_.size()) { return true; } - - auto pos = buf_.find(pattern); - if (pos == std::string::npos) { pos = buf_.size(); } - if (!content_callback(buf_.data(), pos)) { - is_valid_ = false; - is_done_ = false; - return false; - } - - off_ += pos; - buf_.erase(0, pos); - } - - { - auto pattern = crlf_ + dash_ + boundary_; - if (pattern.size() > buf_.size()) { return true; } - - auto pos = buf_.find(pattern); - if (pos != std::string::npos) { - if (!content_callback(buf_.data(), pos)) { - is_valid_ = false; - is_done_ = false; - return false; - } - - off_ += pos + pattern.size(); - buf_.erase(0, pos + pattern.size()); - state_ = 4; - } - else { - if (!content_callback(buf_.data(), pattern.size())) { - is_valid_ = false; - is_done_ = false; - return false; - } - - off_ += pattern.size(); - buf_.erase(0, pattern.size()); - } - } - break; - } - case 4: { // Boundary - if (crlf_.size() > buf_.size()) { return true; } - if (buf_.find(crlf_) == 0) { - buf_.erase(0, crlf_.size()); - off_ += crlf_.size(); - state_ = 1; - } - else { - auto pattern = dash_ + crlf_; - if (pattern.size() > buf_.size()) { return true; } - if (buf_.find(pattern) == 0) { - buf_.erase(0, pattern.size()); - off_ += pattern.size(); - is_valid_ = true; - state_ = 5; - } - else { - is_done_ = true; - return true; - } - } - break; - } - case 5: { // Done - is_valid_ = false; - return false; - } - } - } - - return true; - } - - private: - void clear_file_info() { - file_.name.clear(); - file_.filename.clear(); - file_.content_type.clear(); - } - - const std::string dash_ = "--"; - const std::string crlf_ = "\r\n"; - std::string boundary_; - - std::string buf_; - size_t state_ = 0; - size_t is_valid_ = false; - size_t is_done_ = false; - size_t off_ = 0; - MultipartFormData file_; - }; - - std::string to_lower(const char* beg, const char* end) { - std::string out; - auto it = beg; - while (it != end) { - out += static_cast(::tolower(*it)); - it++; - } - return out; - } - - std::string make_multipart_data_boundary() { - static const char data[] = - "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - - std::random_device seed_gen; - std::mt19937 engine(seed_gen()); - - std::string result = "--cpp-httplib-multipart-data-"; - - for (auto i = 0; i < 16; i++) { - result += data[engine() % (sizeof(data) - 1)]; - } - - return result; - } - - std::pair - get_range_offset_and_length(const Request& req, size_t content_length, - size_t index) { - auto r = req.ranges[index]; - - if (r.first == -1 && r.second == -1) { - return std::make_pair(0, content_length); - } - - auto slen = static_cast(content_length); - - if (r.first == -1) { - r.first = slen - r.second; - r.second = slen - 1; - } - - if (r.second == -1) { r.second = slen - 1; } - - return std::make_pair(r.first, r.second - r.first + 1); - } - - std::string make_content_range_header_field(size_t offset, size_t length, - size_t content_length) { - std::string field = "bytes "; - field += std::to_string(offset); - field += "-"; - field += std::to_string(offset + length - 1); - field += "/"; - field += std::to_string(content_length); - return field; - } - - template - bool process_multipart_ranges_data(const Request& req, Response& res, - const std::string& boundary, - const std::string& content_type, - SToken stoken, CToken ctoken, - Content content) { - for (size_t i = 0; i < req.ranges.size(); i++) { - ctoken("--"); - stoken(boundary); - ctoken("\r\n"); - if (!content_type.empty()) { - ctoken("Content-Type: "); - stoken(content_type); - ctoken("\r\n"); - } - - auto offsets = get_range_offset_and_length(req, res.body.size(), i); - auto offset = offsets.first; - auto length = offsets.second; - - ctoken("Content-Range: "); - stoken(make_content_range_header_field(offset, length, res.body.size())); - ctoken("\r\n"); - ctoken("\r\n"); - if (!content(offset, length)) { return false; } - ctoken("\r\n"); - } - - ctoken("--"); - stoken(boundary); - ctoken("--\r\n"); - - return true; - } - - std::string make_multipart_ranges_data(const Request& req, Response& res, - const std::string& boundary, - const std::string& content_type) { - std::string data; - - process_multipart_ranges_data( - req, res, boundary, content_type, - [&](const std::string& token) { data += token; }, - [&](const char* token) { data += token; }, - [&](size_t offset, size_t length) { - data += res.body.substr(offset, length); - return true; - }); - - return data; - } - - size_t - get_multipart_ranges_data_length(const Request& req, Response& res, - const std::string& boundary, - const std::string& content_type) { - size_t data_length = 0; - - process_multipart_ranges_data( - req, res, boundary, content_type, - [&](const std::string& token) { data_length += token.size(); }, - [&](const char* token) { data_length += strlen(token); }, - [&](size_t /*offset*/, size_t length) { - data_length += length; - return true; - }); - - return data_length; - } - - bool write_multipart_ranges_data(Stream& strm, const Request& req, - Response& res, - const std::string& boundary, - const std::string& content_type) { - return process_multipart_ranges_data( - req, res, boundary, content_type, - [&](const std::string& token) { strm.write(token); }, - [&](const char* token) { strm.write(token); }, - [&](size_t offset, size_t length) { - return write_content(strm, res.content_provider, offset, length) >= 0; - }); - } - - std::pair - get_range_offset_and_length(const Request& req, const Response& res, - size_t index) { - auto r = req.ranges[index]; - - if (r.second == -1) { - r.second = static_cast(res.content_length) - 1; - } - - return std::make_pair(r.first, r.second - r.first + 1); - } - - bool expect_content(const Request& req) { - if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH" || - req.method == "PRI") { - return true; - } - // TODO: check if Content-Length is set - return false; - } - -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - template - std::string message_digest(const std::string& s, Init init, - Update update, Final final, - size_t digest_length) { - using namespace std; - - std::vector md(digest_length, 0); - CTX ctx; - init(&ctx); - update(&ctx, s.data(), s.size()); - final(md.data(), &ctx); - - stringstream ss; - for (auto c : md) { - ss << setfill('0') << setw(2) << hex << (unsigned int)c; - } - return ss.str(); - } - - std::string MD5(const std::string& s) { - return message_digest(s, MD5_Init, MD5_Update, MD5_Final, - MD5_DIGEST_LENGTH); - } - - std::string SHA_256(const std::string& s) { - return message_digest(s, SHA256_Init, SHA256_Update, SHA256_Final, - SHA256_DIGEST_LENGTH); - } - - std::string SHA_512(const std::string& s) { - return message_digest(s, SHA512_Init, SHA512_Update, SHA512_Final, - SHA512_DIGEST_LENGTH); - } -#endif - -#ifdef _WIN32 - class WSInit { - public: - WSInit() { - WSADATA wsaData; - WSAStartup(0x0002, &wsaData); - } - - ~WSInit() { WSACleanup(); } - }; - - static WSInit wsinit_; -#endif - - } // namespace detail - - // Header utilities - std::pair make_range_header(Ranges ranges) { - std::string field = "bytes="; - auto i = 0; - for (auto r : ranges) { - if (i != 0) { field += ", "; } - if (r.first != -1) { field += std::to_string(r.first); } - field += '-'; - if (r.second != -1) { field += std::to_string(r.second); } - i++; - } - return std::make_pair("Range", field); - } - - std::pair - make_basic_authentication_header(const std::string& username, - const std::string& password, - bool is_proxy = false) { - auto field = "Basic " + detail::base64_encode(username + ":" + password); - auto key = is_proxy ? "Proxy-Authorization" : "Authorization"; - return std::make_pair(key, field); - } - -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - std::pair make_digest_authentication_header( - const Request& req, const std::map& auth, - size_t cnonce_count, const std::string& cnonce, const std::string& username, - const std::string& password, bool is_proxy = false) { - using namespace std; - - string nc; - { - stringstream ss; - ss << setfill('0') << setw(8) << hex << cnonce_count; - nc = ss.str(); - } - - auto qop = auth.at("qop"); - if (qop.find("auth-int") != std::string::npos) { - qop = "auth-int"; - } - else { - qop = "auth"; - } - - std::string algo = "MD5"; - if (auth.find("algorithm") != auth.end()) { algo = auth.at("algorithm"); } - - string response; - { - auto H = algo == "SHA-256" - ? detail::SHA_256 - : algo == "SHA-512" ? detail::SHA_512 : detail::MD5; - - auto A1 = username + ":" + auth.at("realm") + ":" + password; - - auto A2 = req.method + ":" + req.path; - if (qop == "auth-int") { A2 += ":" + H(req.body); } - - response = H(H(A1) + ":" + auth.at("nonce") + ":" + nc + ":" + cnonce + - ":" + qop + ":" + H(A2)); - } - - auto field = "Digest username=\"hello\", realm=\"" + auth.at("realm") + - "\", nonce=\"" + auth.at("nonce") + "\", uri=\"" + req.path + - "\", algorithm=" + algo + ", qop=" + qop + ", nc=\"" + nc + - "\", cnonce=\"" + cnonce + "\", response=\"" + response + "\""; - - auto key = is_proxy ? "Proxy-Authorization" : "Authorization"; - return std::make_pair(key, field); - } -#endif - - bool parse_www_authenticate(const httplib::Response& res, - std::map& auth, - bool is_proxy) { - auto auth_key = is_proxy ? "Proxy-Authenticate" : "WWW-Authenticate"; - if (res.has_header(auth_key)) { - static auto re = std::regex(R"~((?:(?:,\s*)?(.+?)=(?:"(.*?)"|([^,]*))))~"); - auto s = res.get_header_value(auth_key); - auto pos = s.find(' '); - if (pos != std::string::npos) { - auto type = s.substr(0, pos); - if (type == "Basic") { - return false; - } - else if (type == "Digest") { - s = s.substr(pos + 1); - auto beg = std::sregex_iterator(s.begin(), s.end(), re); - for (auto i = beg; i != std::sregex_iterator(); ++i) { - auto m = *i; - auto key = s.substr(static_cast(m.position(1)), - static_cast(m.length(1))); - auto val = m.length(2) > 0 - ? s.substr(static_cast(m.position(2)), - static_cast(m.length(2))) - : s.substr(static_cast(m.position(3)), - static_cast(m.length(3))); - auth[key] = val; - } - return true; - } - } - } - return false; - } - - // https://stackoverflow.com/questions/440133/how-do-i-create-a-random-alpha-numeric-string-in-c/440240#answer-440240 - std::string random_string(size_t length) { - auto randchar = []() -> char { - const char charset[] = "0123456789" - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz"; - const size_t max_index = (sizeof(charset) - 1); - return charset[static_cast(rand()) % max_index]; - }; - std::string str(length, 0); - std::generate_n(str.begin(), length, randchar); - return str; - } - - // Request implementation - bool Request::has_header(const char* key) const { - return detail::has_header(headers, key); - } - - std::string Request::get_header_value(const char* key, size_t id) const { - return detail::get_header_value(headers, key, id, ""); - } - - size_t Request::get_header_value_count(const char* key) const { - auto r = headers.equal_range(key); - return static_cast(std::distance(r.first, r.second)); - } - - void Request::set_header(const char* key, const char* val) { - headers.emplace(key, val); - } - - void Request::set_header(const char* key, const std::string& val) { - headers.emplace(key, val); - } - - bool Request::has_param(const char* key) const { - return params.find(key) != params.end(); - } - - std::string Request::get_param_value(const char* key, size_t id) const { - auto it = params.find(key); - std::advance(it, static_cast(id)); - if (it != params.end()) { return it->second; } - return std::string(); - } - - size_t Request::get_param_value_count(const char* key) const { - auto r = params.equal_range(key); - return static_cast(std::distance(r.first, r.second)); - } - - bool Request::is_multipart_form_data() const { - const auto& content_type = get_header_value("Content-Type"); - return !content_type.find("multipart/form-data"); - } - - bool Request::has_file(const char* key) const { - return files.find(key) != files.end(); - } - - MultipartFormData Request::get_file_value(const char* key) const { - auto it = files.find(key); - if (it != files.end()) { return it->second; } - return MultipartFormData(); - } - - // Response implementation - bool Response::has_header(const char* key) const { - return headers.find(key) != headers.end(); - } - - std::string Response::get_header_value(const char* key, - size_t id) const { - return detail::get_header_value(headers, key, id, ""); - } - - size_t Response::get_header_value_count(const char* key) const { - auto r = headers.equal_range(key); - return static_cast(std::distance(r.first, r.second)); - } - - void Response::set_header(const char* key, const char* val) { - headers.emplace(key, val); - } - - void Response::set_header(const char* key, const std::string& val) { - headers.emplace(key, val); - } - - void Response::set_redirect(const char* url) { - set_header("Location", url); - status = 302; - } - - void Response::set_content(const char* s, size_t n, - const char* content_type) { - body.assign(s, n); - set_header("Content-Type", content_type); - } - - void Response::set_content(const std::string& s, - const char* content_type) { - body = s; - set_header("Content-Type", content_type); - } - - void Response::set_content_provider( - size_t in_length, - std::function provider, - std::function resource_releaser) { - assert(in_length > 0); - content_length = in_length; - content_provider = [provider](size_t offset, size_t length, DataSink& sink) { - provider(offset, length, sink); - }; - content_provider_resource_releaser = resource_releaser; - } - - void Response::set_chunked_content_provider( - std::function provider, - std::function resource_releaser) { - content_length = 0; - content_provider = [provider](size_t offset, size_t, DataSink& sink) { - provider(offset, sink); - }; - content_provider_resource_releaser = resource_releaser; - } - - // Rstream implementation - ssize_t Stream::write(const char* ptr) { - return write(ptr, strlen(ptr)); - } - - ssize_t Stream::write(const std::string& s) { - return write(s.data(), s.size()); - } - - template - ssize_t Stream::write_format(const char* fmt, const Args&... args) { - std::array buf; - -#if defined(_MSC_VER) && _MSC_VER < 1900 - auto sn = _snprintf_s(buf, bufsiz, buf.size() - 1, fmt, args...); -#else - auto sn = snprintf(buf.data(), buf.size() - 1, fmt, args...); -#endif - if (sn <= 0) { return sn; } - - auto n = static_cast(sn); - - if (n >= buf.size() - 1) { - std::vector glowable_buf(buf.size()); - - while (n >= glowable_buf.size() - 1) { - glowable_buf.resize(glowable_buf.size() * 2); -#if defined(_MSC_VER) && _MSC_VER < 1900 - n = static_cast(_snprintf_s(&glowable_buf[0], glowable_buf.size(), - glowable_buf.size() - 1, fmt, - args...)); -#else - n = static_cast( - snprintf(&glowable_buf[0], glowable_buf.size() - 1, fmt, args...)); -#endif - } - return write(&glowable_buf[0], n); - } - else { - return write(buf.data(), n); - } - } - - namespace detail { - - // Socket stream implementation - SocketStream::SocketStream(socket_t sock, time_t read_timeout_sec, - time_t read_timeout_usec) - : sock_(sock), read_timeout_sec_(read_timeout_sec), - read_timeout_usec_(read_timeout_usec) {} - - SocketStream::~SocketStream() {} - - bool SocketStream::is_readable() const { - return detail::select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0; - } - - bool SocketStream::is_writable() const { - return detail::select_write(sock_, 0, 0) > 0; - } - - ssize_t SocketStream::read(char* ptr, size_t size) { - if (is_readable()) { return recv(sock_, ptr, size, 0); } - return -1; - } - - ssize_t SocketStream::write(const char* ptr, size_t size) { - if (is_writable()) { return send(sock_, ptr, size, 0); } - return -1; - } - - std::string SocketStream::get_remote_addr() const { - return detail::get_remote_addr(sock_); - } - - // Buffer stream implementation - bool BufferStream::is_readable() const { return true; } - - bool BufferStream::is_writable() const { return true; } - - ssize_t BufferStream::read(char* ptr, size_t size) { -#if defined(_MSC_VER) && _MSC_VER < 1900 - auto len_read = buffer._Copy_s(ptr, size, size, position); -#else - auto len_read = buffer.copy(ptr, size, position); -#endif - position += static_cast(len_read); - return static_cast(len_read); - } - - ssize_t BufferStream::write(const char* ptr, size_t size) { - buffer.append(ptr, size); - return static_cast(size); - } - - std::string BufferStream::get_remote_addr() const { return ""; } - - const std::string& BufferStream::get_buffer() const { return buffer; } - - } // namespace detail - - // HTTP server implementation - Server::Server() - : keep_alive_max_count_(CPPHTTPLIB_KEEPALIVE_MAX_COUNT), - read_timeout_sec_(CPPHTTPLIB_READ_TIMEOUT_SECOND), - read_timeout_usec_(CPPHTTPLIB_READ_TIMEOUT_USECOND), - payload_max_length_(CPPHTTPLIB_PAYLOAD_MAX_LENGTH), is_running_(false), - svr_sock_(INVALID_SOCKET) { -#ifndef _WIN32 - signal(SIGPIPE, SIG_IGN); -#endif - new_task_queue = [] { return new ThreadPool(CPPHTTPLIB_THREAD_POOL_COUNT); }; - } - - Server::~Server() {} - - Server& Server::Get(const char* pattern, Handler handler) { - get_handlers_.push_back(std::make_pair(std::regex(pattern), handler)); - return *this; - } - - Server& Server::Post(const char* pattern, Handler handler) { - post_handlers_.push_back(std::make_pair(std::regex(pattern), handler)); - return *this; - } - - Server& Server::Post(const char* pattern, - HandlerWithContentReader handler) { - post_handlers_for_content_reader_.push_back( - std::make_pair(std::regex(pattern), handler)); - return *this; - } - - Server& Server::Put(const char* pattern, Handler handler) { - put_handlers_.push_back(std::make_pair(std::regex(pattern), handler)); - return *this; - } - - Server& Server::Put(const char* pattern, - HandlerWithContentReader handler) { - put_handlers_for_content_reader_.push_back( - std::make_pair(std::regex(pattern), handler)); - return *this; - } - - Server& Server::Patch(const char* pattern, Handler handler) { - patch_handlers_.push_back(std::make_pair(std::regex(pattern), handler)); - return *this; - } - - Server& Server::Patch(const char* pattern, - HandlerWithContentReader handler) { - patch_handlers_for_content_reader_.push_back( - std::make_pair(std::regex(pattern), handler)); - return *this; - } - - Server& Server::Delete(const char* pattern, Handler handler) { - delete_handlers_.push_back(std::make_pair(std::regex(pattern), handler)); - return *this; - } - - Server& Server::Options(const char* pattern, Handler handler) { - options_handlers_.push_back(std::make_pair(std::regex(pattern), handler)); - return *this; - } - - bool Server::set_base_dir(const char* dir, const char* mount_point) { - return set_mount_point(mount_point, dir); - } - - bool Server::set_mount_point(const char* mount_point, const char* dir) { - if (detail::is_dir(dir)) { - std::string mnt = mount_point ? mount_point : "/"; - if (!mnt.empty() && mnt[0] == '/') { - base_dirs_.emplace_back(mnt, dir); - return true; - } - } - return false; - } - - bool Server::remove_mount_point(const char* mount_point) { - for (auto it = base_dirs_.begin(); it != base_dirs_.end(); ++it) { - if (it->first == mount_point) { - base_dirs_.erase(it); - return true; - } - } - return false; - } - - void Server::set_file_extension_and_mimetype_mapping(const char* ext, - const char* mime) { - file_extension_and_mimetype_map_[ext] = mime; - } - - void Server::set_file_request_handler(Handler handler) { - file_request_handler_ = std::move(handler); - } - - void Server::set_error_handler(Handler handler) { - error_handler_ = std::move(handler); - } - - void Server::set_logger(Logger logger) { logger_ = std::move(logger); } - - void - Server::set_expect_100_continue_handler(Expect100ContinueHandler handler) { - expect_100_continue_handler_ = std::move(handler); - } - - void Server::set_keep_alive_max_count(size_t count) { - keep_alive_max_count_ = count; - } - - void Server::set_read_timeout(time_t sec, time_t usec) { - read_timeout_sec_ = sec; - read_timeout_usec_ = usec; - } - - void Server::set_payload_max_length(size_t length) { - payload_max_length_ = length; - } - - bool Server::bind_to_port(const char* host, int port, int socket_flags) { - if (bind_internal(host, port, socket_flags) < 0) return false; - return true; - } - int Server::bind_to_any_port(const char* host, int socket_flags) { - return bind_internal(host, 0, socket_flags); - } - - bool Server::listen_after_bind() { return listen_internal(); } - - bool Server::listen(const char* host, int port, int socket_flags) { - return bind_to_port(host, port, socket_flags) && listen_internal(); - } - - bool Server::is_running() const { return is_running_; } - - void Server::stop() { - if (is_running_) { - assert(svr_sock_ != INVALID_SOCKET); - std::atomic sock(svr_sock_.exchange(INVALID_SOCKET)); - detail::shutdown_socket(sock); - detail::close_socket(sock); - } - } - - bool Server::parse_request_line(const char* s, Request& req) { - const static std::regex re( - "(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH|PRI) " - "(([^?]+)(?:\\?(.*?))?) (HTTP/1\\.[01])\r\n"); - - std::cmatch m; - if (std::regex_match(s, m, re)) { - req.version = std::string(m[5]); - req.method = std::string(m[1]); - req.target = std::string(m[2]); - req.path = detail::decode_url(m[3], false); - - // Parse query text - auto len = std::distance(m[4].first, m[4].second); - if (len > 0) { detail::parse_query_text(m[4], req.params); } - - return true; - } - - return false; - } - - bool Server::write_response(Stream& strm, bool last_connection, - const Request& req, Response& res) { - assert(res.status != -1); - - if (400 <= res.status && error_handler_) { error_handler_(req, res); } - - detail::BufferStream bstrm; - - // Response line - if (!bstrm.write_format("HTTP/1.1 %d %s\r\n", res.status, - detail::status_message(res.status))) { - return false; - } - - // Headers - if (last_connection || req.get_header_value("Connection") == "close") { - res.set_header("Connection", "close"); - } - - if (!last_connection && req.get_header_value("Connection") == "Keep-Alive") { - res.set_header("Connection", "Keep-Alive"); - } - - if (!res.has_header("Content-Type") && - (!res.body.empty() || res.content_length > 0)) { - res.set_header("Content-Type", "text/plain"); - } - - if (!res.has_header("Accept-Ranges") && req.method == "HEAD") { - res.set_header("Accept-Ranges", "bytes"); - } - - std::string content_type; - std::string boundary; - - if (req.ranges.size() > 1) { - boundary = detail::make_multipart_data_boundary(); - - auto it = res.headers.find("Content-Type"); - if (it != res.headers.end()) { - content_type = it->second; - res.headers.erase(it); - } - - res.headers.emplace("Content-Type", - "multipart/byteranges; boundary=" + boundary); - } - - if (res.body.empty()) { - if (res.content_length > 0) { - size_t length = 0; - if (req.ranges.empty()) { - length = res.content_length; - } - else if (req.ranges.size() == 1) { - auto offsets = - detail::get_range_offset_and_length(req, res.content_length, 0); - auto offset = offsets.first; - length = offsets.second; - auto content_range = detail::make_content_range_header_field( - offset, length, res.content_length); - res.set_header("Content-Range", content_range); - } - else { - length = detail::get_multipart_ranges_data_length(req, res, boundary, - content_type); - } - res.set_header("Content-Length", std::to_string(length)); - } - else { - if (res.content_provider) { - res.set_header("Transfer-Encoding", "chunked"); - } - else { - res.set_header("Content-Length", "0"); - } - } - } - else { - if (req.ranges.empty()) { - ; - } - else if (req.ranges.size() == 1) { - auto offsets = - detail::get_range_offset_and_length(req, res.body.size(), 0); - auto offset = offsets.first; - auto length = offsets.second; - auto content_range = detail::make_content_range_header_field( - offset, length, res.body.size()); - res.set_header("Content-Range", content_range); - res.body = res.body.substr(offset, length); - } - else { - res.body = - detail::make_multipart_ranges_data(req, res, boundary, content_type); - } - -#ifdef CPPHTTPLIB_ZLIB_SUPPORT - // TODO: 'Accept-Encoding' has gzip, not gzip;q=0 - const auto& encodings = req.get_header_value("Accept-Encoding"); - if (encodings.find("gzip") != std::string::npos && - detail::can_compress(res.get_header_value("Content-Type"))) { - if (detail::compress(res.body)) { - res.set_header("Content-Encoding", "gzip"); - } - } -#endif - - auto length = std::to_string(res.body.size()); - res.set_header("Content-Length", length); - } - - if (!detail::write_headers(bstrm, res, Headers())) { return false; } - - // Flush buffer - auto& data = bstrm.get_buffer(); - strm.write(data.data(), data.size()); - - // Body - if (req.method != "HEAD") { - if (!res.body.empty()) { - if (!strm.write(res.body)) { return false; } - } - else if (res.content_provider) { - if (!write_content_with_provider(strm, req, res, boundary, - content_type)) { - return false; - } - } - } - - // Log - if (logger_) { logger_(req, res); } - - return true; - } - - bool - Server::write_content_with_provider(Stream& strm, const Request& req, - Response& res, const std::string& boundary, - const std::string& content_type) { - if (res.content_length) { - if (req.ranges.empty()) { - if (detail::write_content(strm, res.content_provider, 0, - res.content_length) < 0) { - return false; - } - } - else if (req.ranges.size() == 1) { - auto offsets = - detail::get_range_offset_and_length(req, res.content_length, 0); - auto offset = offsets.first; - auto length = offsets.second; - if (detail::write_content(strm, res.content_provider, offset, length) < - 0) { - return false; - } - } - else { - if (!detail::write_multipart_ranges_data(strm, req, res, boundary, - content_type)) { - return false; - } - } - } - else { - auto is_shutting_down = [this]() { - return this->svr_sock_ == INVALID_SOCKET; - }; - if (detail::write_content_chunked(strm, res.content_provider, - is_shutting_down) < 0) { - return false; - } - } - return true; - } - - bool Server::read_content(Stream& strm, bool last_connection, - Request& req, Response& res) { - MultipartFormDataMap::iterator cur; - auto ret = read_content_core( - strm, last_connection, req, res, - // Regular - [&](const char* buf, size_t n) { - if (req.body.size() + n > req.body.max_size()) { return false; } - req.body.append(buf, n); - return true; - }, - // Multipart - [&](const MultipartFormData& file) { - cur = req.files.emplace(file.name, file); - return true; - }, - [&](const char* buf, size_t n) { - auto& content = cur->second.content; - if (content.size() + n > content.max_size()) { return false; } - content.append(buf, n); - return true; - }); - - const auto& content_type = req.get_header_value("Content-Type"); - if (!content_type.find("application/x-www-form-urlencoded")) { - detail::parse_query_text(req.body, req.params); - } - - return ret; - } - - bool Server::read_content_with_content_receiver( - Stream& strm, bool last_connection, Request& req, Response& res, - ContentReceiver receiver, MultipartContentHeader multipart_header, - ContentReceiver multipart_receiver) { - return read_content_core(strm, last_connection, req, res, receiver, - multipart_header, multipart_receiver); - } - - bool Server::read_content_core(Stream& strm, bool last_connection, - Request& req, Response& res, - ContentReceiver receiver, - MultipartContentHeader mulitpart_header, - ContentReceiver multipart_receiver) { - detail::MultipartFormDataParser multipart_form_data_parser; - ContentReceiver out; - - if (req.is_multipart_form_data()) { - const auto& content_type = req.get_header_value("Content-Type"); - std::string boundary; - if (!detail::parse_multipart_boundary(content_type, boundary)) { - res.status = 400; - return write_response(strm, last_connection, req, res); - } - - multipart_form_data_parser.set_boundary(boundary); - out = [&](const char* buf, size_t n) { - return multipart_form_data_parser.parse(buf, n, multipart_receiver, - mulitpart_header); - }; - } - else { - out = receiver; - } - - if (!detail::read_content(strm, req, payload_max_length_, res.status, - Progress(), out)) { - return write_response(strm, last_connection, req, res); - } - - if (req.is_multipart_form_data()) { - if (!multipart_form_data_parser.is_valid()) { - res.status = 400; - return write_response(strm, last_connection, req, res); - } - } - - return true; - } - - bool Server::handle_file_request(Request& req, Response& res, - bool head) { - for (const auto& kv : base_dirs_) { - const auto& mount_point = kv.first; - const auto& base_dir = kv.second; - - // Prefix match - if (!req.path.find(mount_point)) { - std::string sub_path = "/" + req.path.substr(mount_point.size()); - if (detail::is_valid_path(sub_path)) { - auto path = base_dir + sub_path; - if (path.back() == '/') { path += "index.html"; } - - if (detail::is_file(path)) { - detail::read_file(path, res.body); - auto type = - detail::find_content_type(path, file_extension_and_mimetype_map_); - if (type) { res.set_header("Content-Type", type); } - res.status = 200; - if (!head && file_request_handler_) { - file_request_handler_(req, res); - } - return true; - } - } - } - } - return false; - } - - socket_t Server::create_server_socket(const char* host, int port, - int socket_flags) const { - return detail::create_socket( - host, port, - [](socket_t sock, struct addrinfo& ai) -> bool { - if (::bind(sock, ai.ai_addr, static_cast(ai.ai_addrlen))) { - return false; - } - if (::listen(sock, 5)) { // Listen through 5 channels - return false; - } - return true; - }, - socket_flags); - } - - int Server::bind_internal(const char* host, int port, int socket_flags) { - if (!is_valid()) { return -1; } - - svr_sock_ = create_server_socket(host, port, socket_flags); - if (svr_sock_ == INVALID_SOCKET) { return -1; } - - if (port == 0) { - struct sockaddr_storage address; - socklen_t len = sizeof(address); - if (getsockname(svr_sock_, reinterpret_cast(&address), - &len) == -1) { - return -1; - } - if (address.ss_family == AF_INET) { - return ntohs(reinterpret_cast(&address)->sin_port); - } - else if (address.ss_family == AF_INET6) { - return ntohs( - reinterpret_cast(&address)->sin6_port); - } - else { - return -1; - } - } - else { - return port; - } - } - - bool Server::listen_internal() { - auto ret = true; - is_running_ = true; - - { - std::unique_ptr task_queue(new_task_queue()); - - for (;;) { - if (svr_sock_ == INVALID_SOCKET) { - // The server socket was closed by 'stop' method. - break; - } - - auto val = detail::select_read(svr_sock_, 0, 100000); - - if (val == 0) { // Timeout - continue; - } - - socket_t sock = accept(svr_sock_, nullptr, nullptr); - - if (sock == INVALID_SOCKET) { - if (errno == EMFILE) { - // The per-process limit of open file descriptors has been reached. - // Try to accept new connections after a short sleep. - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - continue; - } - if (svr_sock_ != INVALID_SOCKET) { - detail::close_socket(svr_sock_); - ret = false; - } - else { - ; // The server socket was closed by user. - } - break; - } - -#if __cplusplus > 201703L - task_queue->enqueue([=, this]() { process_and_close_socket(sock); }); -#else - task_queue->enqueue([=]() { process_and_close_socket(sock); }); -#endif - } - - task_queue->shutdown(); - } - - is_running_ = false; - return ret; - } - - bool Server::routing(Request& req, Response& res, Stream& strm, - bool last_connection) { - // File handler - bool is_head_request = req.method == "HEAD"; - if ((req.method == "GET" || is_head_request) && - handle_file_request(req, res, is_head_request)) { - return true; - } - - if (detail::expect_content(req)) { - // Content reader handler - { - ContentReader reader( - [&](ContentReceiver receiver) { - return read_content_with_content_receiver( - strm, last_connection, req, res, receiver, nullptr, nullptr); - }, - [&](MultipartContentHeader header, ContentReceiver receiver) { - return read_content_with_content_receiver( - strm, last_connection, req, res, nullptr, header, receiver); - }); - - if (req.method == "POST") { - if (dispatch_request_for_content_reader( - req, res, reader, post_handlers_for_content_reader_)) { - return true; - } - } - else if (req.method == "PUT") { - if (dispatch_request_for_content_reader( - req, res, reader, put_handlers_for_content_reader_)) { - return true; - } - } - else if (req.method == "PATCH") { - if (dispatch_request_for_content_reader( - req, res, reader, patch_handlers_for_content_reader_)) { - return true; - } - } - } - - // Read content into `req.body` - if (!read_content(strm, last_connection, req, res)) { return false; } - } - - // Regular handler - if (req.method == "GET" || req.method == "HEAD") { - return dispatch_request(req, res, get_handlers_); - } - else if (req.method == "POST") { - return dispatch_request(req, res, post_handlers_); - } - else if (req.method == "PUT") { - return dispatch_request(req, res, put_handlers_); - } - else if (req.method == "DELETE") { - return dispatch_request(req, res, delete_handlers_); - } - else if (req.method == "OPTIONS") { - return dispatch_request(req, res, options_handlers_); - } - else if (req.method == "PATCH") { - return dispatch_request(req, res, patch_handlers_); - } - - res.status = 400; - return false; - } - - bool Server::dispatch_request(Request& req, Response& res, - Handlers& handlers) { - - try { - for (const auto& x : handlers) { - const auto& pattern = x.first; - const auto& handler = x.second; - - if (std::regex_match(req.path, req.matches, pattern)) { - handler(req, res); - return true; - } - } - } - catch (const std::exception & ex) { - res.status = 500; - res.set_header("EXCEPTION_WHAT", ex.what()); - } - catch (...) { - res.status = 500; - res.set_header("EXCEPTION_WHAT", "UNKNOWN"); - } - return false; - } - - bool Server::dispatch_request_for_content_reader( - Request& req, Response& res, ContentReader content_reader, - HandlersForContentReader& handlers) { - for (const auto& x : handlers) { - const auto& pattern = x.first; - const auto& handler = x.second; - - if (std::regex_match(req.path, req.matches, pattern)) { - handler(req, res, content_reader); - return true; - } - } - return false; - } - - bool - Server::process_request(Stream& strm, bool last_connection, - bool& connection_close, - const std::function& setup_request) { - std::array buf{}; - - detail::stream_line_reader line_reader(strm, buf.data(), buf.size()); - - // Connection has been closed on client - if (!line_reader.getline()) { return false; } - - Request req; - Response res; - - res.version = "HTTP/1.1"; - - // Check if the request URI doesn't exceed the limit - if (line_reader.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) { - Headers dummy; - detail::read_headers(strm, dummy); - res.status = 414; - return write_response(strm, last_connection, req, res); - } - - // Request line and headers - if (!parse_request_line(line_reader.ptr(), req) || - !detail::read_headers(strm, req.headers)) { - res.status = 400; - return write_response(strm, last_connection, req, res); - } - - if (req.get_header_value("Connection") == "close") { - connection_close = true; - } - - if (req.version == "HTTP/1.0" && - req.get_header_value("Connection") != "Keep-Alive") { - connection_close = true; - } - - req.set_header("REMOTE_ADDR", strm.get_remote_addr()); - - if (req.has_header("Range")) { - const auto& range_header_value = req.get_header_value("Range"); - if (!detail::parse_range_header(range_header_value, req.ranges)) { - // TODO: error - } - } - - if (setup_request) { setup_request(req); } - - if (req.get_header_value("Expect") == "100-continue") { - auto status = 100; - if (expect_100_continue_handler_) { - status = expect_100_continue_handler_(req, res); - } - switch (status) { - case 100: - case 417: - strm.write_format("HTTP/1.1 %d %s\r\n\r\n", status, - detail::status_message(status)); - break; - default: return write_response(strm, last_connection, req, res); - } - } - - // Rounting - if (routing(req, res, strm, last_connection)) { - if (res.status == -1) { res.status = req.ranges.empty() ? 200 : 206; } - } - else { - if (res.status == -1) { res.status = 404; } - } - - return write_response(strm, last_connection, req, res); - } - - bool Server::is_valid() const { return true; } - - bool Server::process_and_close_socket(socket_t sock) { - return detail::process_and_close_socket( - false, sock, keep_alive_max_count_, read_timeout_sec_, read_timeout_usec_, - [this](Stream& strm, bool last_connection, bool& connection_close) { - return process_request(strm, last_connection, connection_close, - nullptr); - }); - } - - // HTTP client implementation - Client::Client(const std::string& host, int port, - const std::string& client_cert_path, - const std::string& client_key_path) - : host_(host), port_(port), - host_and_port_(host_ + ":" + std::to_string(port_)), - client_cert_path_(client_cert_path), client_key_path_(client_key_path) {} - - Client::~Client() {} - - bool Client::is_valid() const { return true; } - - socket_t Client::create_client_socket() const { - if (!proxy_host_.empty()) { - return detail::create_client_socket(proxy_host_.c_str(), proxy_port_, - timeout_sec_, interface_); - } - return detail::create_client_socket(host_.c_str(), port_, timeout_sec_, - interface_); - } - - bool Client::read_response_line(Stream& strm, Response& res) { - std::array buf; - - detail::stream_line_reader line_reader(strm, buf.data(), buf.size()); - - if (!line_reader.getline()) { return false; } - - const static std::regex re("(HTTP/1\\.[01]) (\\d+?) .*\r\n"); - - std::cmatch m; - if (std::regex_match(line_reader.ptr(), m, re)) { - res.version = std::string(m[1]); - res.status = std::stoi(std::string(m[2])); - } - - return true; - } - - bool Client::send(const Request& req, Response& res) { - auto sock = create_client_socket(); - if (sock == INVALID_SOCKET) { return false; } - -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - if (is_ssl() && !proxy_host_.empty()) { - bool error; - if (!connect(sock, res, error)) { return error; } - } -#endif - - return process_and_close_socket( - sock, 1, [&](Stream& strm, bool last_connection, bool& connection_close) { - return handle_request(strm, req, res, last_connection, - connection_close); - }); - } - - bool Client::send(const std::vector& requests, - std::vector& responses) { - size_t i = 0; - while (i < requests.size()) { - auto sock = create_client_socket(); - if (sock == INVALID_SOCKET) { return false; } - -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - if (is_ssl() && !proxy_host_.empty()) { - Response res; - bool error; - if (!connect(sock, res, error)) { return false; } - } -#endif - - if (!process_and_close_socket(sock, requests.size() - i, - [&](Stream& strm, bool last_connection, - bool& connection_close) -> bool { - auto& req = requests[i++]; - auto res = Response(); - auto ret = handle_request(strm, req, res, - last_connection, - connection_close); - if (ret) { - responses.emplace_back(std::move(res)); - } - return ret; - })) { - return false; - } - } - - return true; - } - - bool Client::handle_request(Stream& strm, const Request& req, - Response& res, bool last_connection, - bool& connection_close) { - if (req.path.empty()) { return false; } - - bool ret; - - if (!is_ssl() && !proxy_host_.empty()) { - auto req2 = req; - req2.path = "http://" + host_and_port_ + req.path; - ret = process_request(strm, req2, res, last_connection, connection_close); - } - else { - ret = process_request(strm, req, res, last_connection, connection_close); - } - - if (!ret) { return false; } - - if (300 < res.status && res.status < 400 && follow_location_) { - ret = redirect(req, res); - } - -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - if (res.status == 401 || res.status == 407) { - auto is_proxy = res.status == 407; - const auto& username = - is_proxy ? proxy_digest_auth_username_ : digest_auth_username_; - const auto& password = - is_proxy ? proxy_digest_auth_password_ : digest_auth_password_; - - if (!username.empty() && !password.empty()) { - std::map auth; - if (parse_www_authenticate(res, auth, is_proxy)) { - Request new_req = req; - auto key = is_proxy ? "Proxy-Authorization" : "WWW-Authorization"; - new_req.headers.erase(key); - new_req.headers.insert(make_digest_authentication_header( - req, auth, 1, random_string(10), username, password, is_proxy)); - - Response new_res; - - ret = send(new_req, new_res); - if (ret) { res = new_res; } - } - } - } -#endif - - return ret; - } - -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - bool Client::connect(socket_t sock, Response& res, bool& error) { - error = true; - Response res2; - - if (!detail::process_socket( - true, sock, 1, read_timeout_sec_, read_timeout_usec_, - [&](Stream& strm, bool /*last_connection*/, bool& connection_close) { - Request req2; - req2.method = "CONNECT"; - req2.path = host_and_port_; - return process_request(strm, req2, res2, false, connection_close); - })) { - detail::close_socket(sock); - error = false; - return false; - } - - if (res2.status == 407) { - if (!proxy_digest_auth_username_.empty() && - !proxy_digest_auth_password_.empty()) { - std::map auth; - if (parse_www_authenticate(res2, auth, true)) { - Response res3; - if (!detail::process_socket( - true, sock, 1, read_timeout_sec_, read_timeout_usec_, - [&](Stream& strm, bool /*last_connection*/, - bool& connection_close) { - Request req3; - req3.method = "CONNECT"; - req3.path = host_and_port_; - req3.headers.insert(make_digest_authentication_header( - req3, auth, 1, random_string(10), - proxy_digest_auth_username_, proxy_digest_auth_password_, - true)); - return process_request(strm, req3, res3, false, - connection_close); - })) { - detail::close_socket(sock); - error = false; - return false; - } - } - } - else { - res = res2; - return false; - } - } - - return true; - } -#endif - - bool Client::redirect(const Request& req, Response& res) { - if (req.redirect_count == 0) { return false; } - - auto location = res.get_header_value("location"); - if (location.empty()) { return false; } - - const static std::regex re( - R"(^(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*(?:\?[^#]*)?)(?:#.*)?)"); - - std::smatch m; - if (!regex_match(location, m, re)) { return false; } - - auto scheme = is_ssl() ? "https" : "http"; - - auto next_scheme = m[1].str(); - auto next_host = m[2].str(); - auto next_path = m[3].str(); - if (next_scheme.empty()) { next_scheme = scheme; } - if (next_scheme.empty()) { next_scheme = scheme; } - if (next_host.empty()) { next_host = host_; } - if (next_path.empty()) { next_path = "/"; } - - if (next_scheme == scheme && next_host == host_) { - return detail::redirect(*this, req, res, next_path); - } - else { - if (next_scheme == "https") { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - SSLClient cli(next_host.c_str()); - cli.copy_settings(*this); - return detail::redirect(cli, req, res, next_path); -#else - return false; -#endif - } - else { - Client cli(next_host.c_str()); - cli.copy_settings(*this); - return detail::redirect(cli, req, res, next_path); - } - } - } - - bool Client::write_request(Stream& strm, const Request& req, - bool last_connection) { - detail::BufferStream bstrm; - - // Request line - const auto& path = detail::encode_url(req.path); - - bstrm.write_format("%s %s HTTP/1.1\r\n", req.method.c_str(), path.c_str()); - - // Additonal headers - Headers headers; - if (last_connection) { headers.emplace("Connection", "close"); } - - if (!req.has_header("Host")) { - if (is_ssl()) { - if (port_ == 443) { - headers.emplace("Host", host_); - } - else { - headers.emplace("Host", host_and_port_); - } - } - else { - if (port_ == 80) { - headers.emplace("Host", host_); - } - else { - headers.emplace("Host", host_and_port_); - } - } - } - - if (!req.has_header("Accept")) { headers.emplace("Accept", "*/*"); } - - if (!req.has_header("User-Agent")) { - headers.emplace("User-Agent", "cpp-httplib/0.5"); - } - - if (req.body.empty()) { - if (req.content_provider) { - auto length = std::to_string(req.content_length); - headers.emplace("Content-Length", length); - } - else { - headers.emplace("Content-Length", "0"); - } - } - else { - if (!req.has_header("Content-Type")) { - headers.emplace("Content-Type", "text/plain"); - } - - if (!req.has_header("Content-Length")) { - auto length = std::to_string(req.body.size()); - headers.emplace("Content-Length", length); - } - } - - if (!basic_auth_username_.empty() && !basic_auth_password_.empty()) { - headers.insert(make_basic_authentication_header( - basic_auth_username_, basic_auth_password_, false)); - } - - if (!proxy_basic_auth_username_.empty() && - !proxy_basic_auth_password_.empty()) { - headers.insert(make_basic_authentication_header( - proxy_basic_auth_username_, proxy_basic_auth_password_, true)); - } - - detail::write_headers(bstrm, req, headers); - - // Flush buffer - auto& data = bstrm.get_buffer(); - strm.write(data.data(), data.size()); - - // Body - if (req.body.empty()) { - if (req.content_provider) { - size_t offset = 0; - size_t end_offset = req.content_length; - - DataSink data_sink; - data_sink.write = [&](const char* d, size_t l) { - auto written_length = strm.write(d, l); - offset += static_cast(written_length); - }; - data_sink.is_writable = [&](void) { return strm.is_writable(); }; - - while (offset < end_offset) { - req.content_provider(offset, end_offset - offset, data_sink); - } - } - } - else { - strm.write(req.body); - } - - return true; - } - - std::shared_ptr Client::send_with_content_provider( - const char* method, const char* path, const Headers& headers, - const std::string& body, size_t content_length, - ContentProvider content_provider, const char* content_type) { - Request req; - req.method = method; - req.headers = headers; - req.path = path; - - req.headers.emplace("Content-Type", content_type); - -#ifdef CPPHTTPLIB_ZLIB_SUPPORT - if (compress_) { - if (content_provider) { - size_t offset = 0; - - DataSink data_sink; - data_sink.write = [&](const char* data, size_t data_len) { - req.body.append(data, data_len); - offset += data_len; - }; - data_sink.is_writable = [&](void) { return true; }; - - while (offset < content_length) { - content_provider(offset, content_length - offset, data_sink); - } - } - else { - req.body = body; - } - - if (!detail::compress(req.body)) { return nullptr; } - req.headers.emplace("Content-Encoding", "gzip"); - } - else -#endif - { - if (content_provider) { - req.content_length = content_length; - req.content_provider = content_provider; - } - else { - req.body = body; - } - } - - auto res = std::make_shared(); - - return send(req, *res) ? res : nullptr; - } - - bool Client::process_request(Stream& strm, const Request& req, - Response& res, bool last_connection, - bool& connection_close) { - // Send request - if (!write_request(strm, req, last_connection)) { return false; } - - // Receive response and headers - if (!read_response_line(strm, res) || - !detail::read_headers(strm, res.headers)) { - return false; - } - - if (res.get_header_value("Connection") == "close" || - res.version == "HTTP/1.0") { - connection_close = true; - } - - if (req.response_handler) { - if (!req.response_handler(res)) { return false; } - } - - // Body - if (req.method != "HEAD" && req.method != "CONNECT") { - ContentReceiver out = [&](const char* buf, size_t n) { - if (res.body.size() + n > res.body.max_size()) { return false; } - res.body.append(buf, n); - return true; - }; - - if (req.content_receiver) { - out = [&](const char* buf, size_t n) { - return req.content_receiver(buf, n); - }; - } - - int dummy_status; - if (!detail::read_content(strm, res, std::numeric_limits::max(), - dummy_status, req.progress, out)) { - return false; - } - } - - // Log - if (logger_) { logger_(req, res); } - - return true; - } - - bool Client::process_and_close_socket( - socket_t sock, size_t request_count, - std::function - callback) { - request_count = std::min(request_count, keep_alive_max_count_); - return detail::process_and_close_socket(true, sock, request_count, - read_timeout_sec_, read_timeout_usec_, - callback); - } - - bool Client::is_ssl() const { return false; } - - std::shared_ptr Client::Get(const char* path) { - return Get(path, Headers(), Progress()); - } - - std::shared_ptr Client::Get(const char* path, - Progress progress) { - return Get(path, Headers(), std::move(progress)); - } - - std::shared_ptr Client::Get(const char* path, - const Headers& headers) { - return Get(path, headers, Progress()); - } - - std::shared_ptr - Client::Get(const char* path, const Headers& headers, Progress progress) { - Request req; - req.method = "GET"; - req.path = path; - req.headers = headers; - req.progress = std::move(progress); - - auto res = std::make_shared(); - return send(req, *res) ? res : nullptr; - } - - std::shared_ptr Client::Get(const char* path, - ContentReceiver content_receiver) { - return Get(path, Headers(), nullptr, std::move(content_receiver), Progress()); - } - - std::shared_ptr Client::Get(const char* path, - ContentReceiver content_receiver, - Progress progress) { - return Get(path, Headers(), nullptr, std::move(content_receiver), - std::move(progress)); - } - - std::shared_ptr Client::Get(const char* path, - const Headers& headers, - ContentReceiver content_receiver) { - return Get(path, headers, nullptr, std::move(content_receiver), Progress()); - } - - std::shared_ptr Client::Get(const char* path, - const Headers& headers, - ContentReceiver content_receiver, - Progress progress) { - return Get(path, headers, nullptr, std::move(content_receiver), - std::move(progress)); - } - - std::shared_ptr Client::Get(const char* path, - const Headers& headers, - ResponseHandler response_handler, - ContentReceiver content_receiver) { - return Get(path, headers, std::move(response_handler), content_receiver, - Progress()); - } - - std::shared_ptr Client::Get(const char* path, - const Headers& headers, - ResponseHandler response_handler, - ContentReceiver content_receiver, - Progress progress) { - Request req; - req.method = "GET"; - req.path = path; - req.headers = headers; - req.response_handler = std::move(response_handler); - req.content_receiver = std::move(content_receiver); - req.progress = std::move(progress); - - auto res = std::make_shared(); - return send(req, *res) ? res : nullptr; - } - - std::shared_ptr Client::Head(const char* path) { - return Head(path, Headers()); - } - - std::shared_ptr Client::Head(const char* path, - const Headers& headers) { - Request req; - req.method = "HEAD"; - req.headers = headers; - req.path = path; - - auto res = std::make_shared(); - - return send(req, *res) ? res : nullptr; - } - - std::shared_ptr Client::Post(const char* path, - const std::string& body, - const char* content_type) { - return Post(path, Headers(), body, content_type); - } - - std::shared_ptr Client::Post(const char* path, - const Headers& headers, - const std::string& body, - const char* content_type) { - return send_with_content_provider("POST", path, headers, body, 0, nullptr, - content_type); - } - - std::shared_ptr Client::Post(const char* path, - const Params& params) { - return Post(path, Headers(), params); - } - - std::shared_ptr Client::Post(const char* path, - size_t content_length, - ContentProvider content_provider, - const char* content_type) { - return Post(path, Headers(), content_length, content_provider, content_type); - } - - std::shared_ptr - Client::Post(const char* path, const Headers& headers, size_t content_length, - ContentProvider content_provider, const char* content_type) { - return send_with_content_provider("POST", path, headers, std::string(), - content_length, content_provider, - content_type); - } - - std::shared_ptr - Client::Post(const char* path, const Headers& headers, const Params& params) { - auto query = detail::params_to_query_str(params); - return Post(path, headers, query, "application/x-www-form-urlencoded"); - } - - std::shared_ptr - Client::Post(const char* path, const MultipartFormDataItems& items) { - return Post(path, Headers(), items); - } - - std::shared_ptr - Client::Post(const char* path, const Headers& headers, - const MultipartFormDataItems& items) { - auto boundary = detail::make_multipart_data_boundary(); - - std::string body; - - for (const auto& item : items) { - body += "--" + boundary + "\r\n"; - body += "Content-Disposition: form-data; name=\"" + item.name + "\""; - if (!item.filename.empty()) { - body += "; filename=\"" + item.filename + "\""; - } - body += "\r\n"; - if (!item.content_type.empty()) { - body += "Content-Type: " + item.content_type + "\r\n"; - } - body += "\r\n"; - body += item.content + "\r\n"; - } - - body += "--" + boundary + "--\r\n"; - - std::string content_type = "multipart/form-data; boundary=" + boundary; - return Post(path, headers, body, content_type.c_str()); - } - - std::shared_ptr Client::Put(const char* path, - const std::string& body, - const char* content_type) { - return Put(path, Headers(), body, content_type); - } - - std::shared_ptr Client::Put(const char* path, - const Headers& headers, - const std::string& body, - const char* content_type) { - return send_with_content_provider("PUT", path, headers, body, 0, nullptr, - content_type); - } - - std::shared_ptr Client::Put(const char* path, - size_t content_length, - ContentProvider content_provider, - const char* content_type) { - return Put(path, Headers(), content_length, content_provider, content_type); - } - - std::shared_ptr - Client::Put(const char* path, const Headers& headers, size_t content_length, - ContentProvider content_provider, const char* content_type) { - return send_with_content_provider("PUT", path, headers, std::string(), - content_length, content_provider, - content_type); - } - - std::shared_ptr Client::Put(const char* path, - const Params& params) { - return Put(path, Headers(), params); - } - - std::shared_ptr - Client::Put(const char* path, const Headers& headers, const Params& params) { - auto query = detail::params_to_query_str(params); - return Put(path, headers, query, "application/x-www-form-urlencoded"); - } - - std::shared_ptr Client::Patch(const char* path, - const std::string& body, - const char* content_type) { - return Patch(path, Headers(), body, content_type); - } - - std::shared_ptr Client::Patch(const char* path, - const Headers& headers, - const std::string& body, - const char* content_type) { - return send_with_content_provider("PATCH", path, headers, body, 0, nullptr, - content_type); - } - - std::shared_ptr Client::Patch(const char* path, - size_t content_length, - ContentProvider content_provider, - const char* content_type) { - return Patch(path, Headers(), content_length, content_provider, content_type); - } - - std::shared_ptr - Client::Patch(const char* path, const Headers& headers, size_t content_length, - ContentProvider content_provider, const char* content_type) { - return send_with_content_provider("PATCH", path, headers, std::string(), - content_length, content_provider, - content_type); - } - - std::shared_ptr Client::Delete(const char* path) { - return Delete(path, Headers(), std::string(), nullptr); - } - - std::shared_ptr Client::Delete(const char* path, - const std::string& body, - const char* content_type) { - return Delete(path, Headers(), body, content_type); - } - - std::shared_ptr Client::Delete(const char* path, - const Headers& headers) { - return Delete(path, headers, std::string(), nullptr); - } - - std::shared_ptr Client::Delete(const char* path, - const Headers& headers, - const std::string& body, - const char* content_type) { - Request req; - req.method = "DELETE"; - req.headers = headers; - req.path = path; - - if (content_type) { req.headers.emplace("Content-Type", content_type); } - req.body = body; - - auto res = std::make_shared(); - - return send(req, *res) ? res : nullptr; - } - - std::shared_ptr Client::Options(const char* path) { - return Options(path, Headers()); - } - - std::shared_ptr Client::Options(const char* path, - const Headers& headers) { - Request req; - req.method = "OPTIONS"; - req.path = path; - req.headers = headers; - - auto res = std::make_shared(); - - return send(req, *res) ? res : nullptr; - } - - void Client::set_timeout_sec(time_t timeout_sec) { - timeout_sec_ = timeout_sec; - } - - void Client::set_read_timeout(time_t sec, time_t usec) { - read_timeout_sec_ = sec; - read_timeout_usec_ = usec; - } - - void Client::set_keep_alive_max_count(size_t count) { - keep_alive_max_count_ = count; - } - - void Client::set_basic_auth(const char* username, const char* password) { - basic_auth_username_ = username; - basic_auth_password_ = password; - } - -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - void Client::set_digest_auth(const char* username, - const char* password) { - digest_auth_username_ = username; - digest_auth_password_ = password; - } -#endif - - void Client::set_follow_location(bool on) { follow_location_ = on; } - - void Client::set_compress(bool on) { compress_ = on; } - - void Client::set_interface(const char* intf) { interface_ = intf; } - - void Client::set_proxy(const char* host, int port) { - proxy_host_ = host; - proxy_port_ = port; - } - - void Client::set_proxy_basic_auth(const char* username, - const char* password) { - proxy_basic_auth_username_ = username; - proxy_basic_auth_password_ = password; - } - -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - void Client::set_proxy_digest_auth(const char* username, - const char* password) { - proxy_digest_auth_username_ = username; - proxy_digest_auth_password_ = password; - } -#endif - - void Client::set_logger(Logger logger) { logger_ = std::move(logger); } - - /* - * SSL Implementation - */ -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - namespace detail { - - template - bool process_and_close_socket_ssl( - bool is_client_request, socket_t sock, size_t keep_alive_max_count, - time_t read_timeout_sec, time_t read_timeout_usec, SSL_CTX* ctx, - std::mutex& ctx_mutex, U SSL_connect_or_accept, V setup, T callback) { - assert(keep_alive_max_count > 0); - - SSL* ssl = nullptr; - { - std::lock_guard guard(ctx_mutex); - ssl = SSL_new(ctx); - } - - if (!ssl) { - close_socket(sock); - return false; - } - - auto bio = BIO_new_socket(static_cast(sock), BIO_NOCLOSE); - SSL_set_bio(ssl, bio, bio); - - if (!setup(ssl)) { - SSL_shutdown(ssl); - { - std::lock_guard guard(ctx_mutex); - SSL_free(ssl); - } - - close_socket(sock); - return false; - } - - auto ret = false; - - if (SSL_connect_or_accept(ssl) == 1) { - if (keep_alive_max_count > 1) { - auto count = keep_alive_max_count; - while (count > 0 && - (is_client_request || - detail::select_read(sock, CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND, - CPPHTTPLIB_KEEPALIVE_TIMEOUT_USECOND) > 0)) { - SSLSocketStream strm(sock, ssl, read_timeout_sec, read_timeout_usec); - auto last_connection = count == 1; - auto connection_close = false; - - ret = callback(ssl, strm, last_connection, connection_close); - if (!ret || connection_close) { break; } - - count--; - } - } - else { - SSLSocketStream strm(sock, ssl, read_timeout_sec, read_timeout_usec); - auto dummy_connection_close = false; - ret = callback(ssl, strm, true, dummy_connection_close); - } - } - - SSL_shutdown(ssl); - { - std::lock_guard guard(ctx_mutex); - SSL_free(ssl); - } - - close_socket(sock); - - return ret; - } - -#if OPENSSL_VERSION_NUMBER < 0x10100000L - static std::shared_ptr> openSSL_locks_; - - class SSLThreadLocks { - public: - SSLThreadLocks() { - openSSL_locks_ = - std::make_shared>(CRYPTO_num_locks()); - CRYPTO_set_locking_callback(locking_callback); - } - - ~SSLThreadLocks() { CRYPTO_set_locking_callback(nullptr); } - - private: - static void locking_callback(int mode, int type, const char* /*file*/, - int /*line*/) { - auto& locks = *openSSL_locks_; - if (mode & CRYPTO_LOCK) { - locks[type].lock(); - } - else { - locks[type].unlock(); - } - } - }; - -#endif - - class SSLInit { - public: - SSLInit() { -#if OPENSSL_VERSION_NUMBER < 0x1010001fL - SSL_load_error_strings(); - SSL_library_init(); -#else - OPENSSL_init_ssl( - OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL); -#endif - } - - ~SSLInit() { -#if OPENSSL_VERSION_NUMBER < 0x1010001fL - ERR_free_strings(); -#endif - } - - private: -#if OPENSSL_VERSION_NUMBER < 0x10100000L - SSLThreadLocks thread_init_; -#endif - }; - - // SSL socket stream implementation - SSLSocketStream::SSLSocketStream(socket_t sock, SSL* ssl, - time_t read_timeout_sec, - time_t read_timeout_usec) - : sock_(sock), ssl_(ssl), read_timeout_sec_(read_timeout_sec), - read_timeout_usec_(read_timeout_usec) {} - - SSLSocketStream::~SSLSocketStream() {} - - bool SSLSocketStream::is_readable() const { - return detail::select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0; - } - - bool SSLSocketStream::is_writable() const { - return detail::select_write(sock_, 0, 0) > 0; - } - - ssize_t SSLSocketStream::read(char* ptr, size_t size) { - if (SSL_pending(ssl_) > 0 || - select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0) { - return SSL_read(ssl_, ptr, static_cast(size)); - } - return -1; - } - - ssize_t SSLSocketStream::write(const char* ptr, size_t size) { - if (is_writable()) { return SSL_write(ssl_, ptr, static_cast(size)); } - return -1; - } - - std::string SSLSocketStream::get_remote_addr() const { - return detail::get_remote_addr(sock_); - } - - static SSLInit sslinit_; - - } // namespace detail - - // SSL HTTP server implementation - SSLServer::SSLServer(const char* cert_path, const char* private_key_path, - const char* client_ca_cert_file_path, - const char* client_ca_cert_dir_path) { - ctx_ = SSL_CTX_new(SSLv23_server_method()); - - if (ctx_) { - SSL_CTX_set_options(ctx_, - SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | - SSL_OP_NO_COMPRESSION | - SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); - - // auto ecdh = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1); - // SSL_CTX_set_tmp_ecdh(ctx_, ecdh); - // EC_KEY_free(ecdh); - - if (SSL_CTX_use_certificate_chain_file(ctx_, cert_path) != 1 || - SSL_CTX_use_PrivateKey_file(ctx_, private_key_path, SSL_FILETYPE_PEM) != - 1) { - SSL_CTX_free(ctx_); - ctx_ = nullptr; - } - else if (client_ca_cert_file_path || client_ca_cert_dir_path) { - // if (client_ca_cert_file_path) { - // auto list = SSL_load_client_CA_file(client_ca_cert_file_path); - // SSL_CTX_set_client_CA_list(ctx_, list); - // } - - SSL_CTX_load_verify_locations(ctx_, client_ca_cert_file_path, - client_ca_cert_dir_path); - - SSL_CTX_set_verify( - ctx_, - SSL_VERIFY_PEER | - SSL_VERIFY_FAIL_IF_NO_PEER_CERT, // SSL_VERIFY_CLIENT_ONCE, - nullptr); - } - } - } - - SSLServer::~SSLServer() { - if (ctx_) { SSL_CTX_free(ctx_); } - } - - bool SSLServer::is_valid() const { return ctx_; } - - bool SSLServer::process_and_close_socket(socket_t sock) { - return detail::process_and_close_socket_ssl( - false, sock, keep_alive_max_count_, read_timeout_sec_, read_timeout_usec_, - ctx_, ctx_mutex_, SSL_accept, [](SSL* /*ssl*/) { return true; }, - [this](SSL* ssl, Stream& strm, bool last_connection, - bool& connection_close) { - return process_request(strm, last_connection, connection_close, - [&](Request& req) { req.ssl = ssl; }); - }); - } - - // SSL HTTP client implementation - SSLClient::SSLClient(const std::string& host, int port, - const std::string& client_cert_path, - const std::string& client_key_path) - : Client(host, port, client_cert_path, client_key_path) { - ctx_ = SSL_CTX_new(SSLv23_client_method()); - - detail::split(&host_[0], &host_[host_.size()], '.', - [&](const char* b, const char* e) { - host_components_.emplace_back(std::string(b, e)); - }); - if (!client_cert_path.empty() && !client_key_path.empty()) { - if (SSL_CTX_use_certificate_file(ctx_, client_cert_path.c_str(), - SSL_FILETYPE_PEM) != 1 || - SSL_CTX_use_PrivateKey_file(ctx_, client_key_path.c_str(), - SSL_FILETYPE_PEM) != 1) { - SSL_CTX_free(ctx_); - ctx_ = nullptr; - } - } - } - - SSLClient::~SSLClient() { - if (ctx_) { SSL_CTX_free(ctx_); } - } - - bool SSLClient::is_valid() const { return ctx_; } - - void SSLClient::set_ca_cert_path(const char* ca_cert_file_path, - const char* ca_cert_dir_path) { - if (ca_cert_file_path) { ca_cert_file_path_ = ca_cert_file_path; } - if (ca_cert_dir_path) { ca_cert_dir_path_ = ca_cert_dir_path; } - } - - void SSLClient::enable_server_certificate_verification(bool enabled) { - server_certificate_verification_ = enabled; - } - - long SSLClient::get_openssl_verify_result() const { - return verify_result_; - } - - SSL_CTX* SSLClient::ssl_context() const noexcept { return ctx_; } - - bool SSLClient::process_and_close_socket( - socket_t sock, size_t request_count, - std::function - callback) { - - request_count = std::min(request_count, keep_alive_max_count_); - - return is_valid() && - detail::process_and_close_socket_ssl( - true, sock, request_count, read_timeout_sec_, read_timeout_usec_, - ctx_, ctx_mutex_, - [&](SSL* ssl) { - if (ca_cert_file_path_.empty()) { - SSL_CTX_set_verify(ctx_, SSL_VERIFY_NONE, nullptr); - } - else { - if (!SSL_CTX_load_verify_locations( - ctx_, ca_cert_file_path_.c_str(), nullptr)) { - return false; - } - SSL_CTX_set_verify(ctx_, SSL_VERIFY_PEER, nullptr); - } - - if (SSL_connect(ssl) != 1) { return false; } - - if (server_certificate_verification_) { - verify_result_ = SSL_get_verify_result(ssl); - - if (verify_result_ != X509_V_OK) { return false; } - - auto server_cert = SSL_get_peer_certificate(ssl); - - if (server_cert == nullptr) { return false; } - - if (!verify_host(server_cert)) { - X509_free(server_cert); - return false; - } - X509_free(server_cert); - } - - return true; - }, - [&](SSL* ssl) { - SSL_set_tlsext_host_name(ssl, host_.c_str()); - return true; - }, - [&](SSL* /*ssl*/, Stream& strm, bool last_connection, - bool& connection_close) { - return callback(strm, last_connection, connection_close); - }); - } - - bool SSLClient::is_ssl() const { return true; } - - bool SSLClient::verify_host(X509* server_cert) const { - /* Quote from RFC2818 section 3.1 "Server Identity" - - If a subjectAltName extension of type dNSName is present, that MUST - be used as the identity. Otherwise, the (most specific) Common Name - field in the Subject field of the certificate MUST be used. Although - the use of the Common Name is existing practice, it is deprecated and - Certification Authorities are encouraged to use the dNSName instead. - - Matching is performed using the matching rules specified by - [RFC2459]. If more than one identity of a given type is present in - the certificate (e.g., more than one dNSName name, a match in any one - of the set is considered acceptable.) Names may contain the wildcard - character * which is considered to match any single domain name - component or component fragment. E.g., *.a.com matches foo.a.com but - not bar.foo.a.com. f*.com matches foo.com but not bar.com. - - In some cases, the URI is specified as an IP address rather than a - hostname. In this case, the iPAddress subjectAltName must be present - in the certificate and must exactly match the IP in the URI. - - */ - return verify_host_with_subject_alt_name(server_cert) || - verify_host_with_common_name(server_cert); - } - - bool - SSLClient::verify_host_with_subject_alt_name(X509* server_cert) const { - auto ret = false; - - auto type = GEN_DNS; - - struct in6_addr addr6; - struct in_addr addr; - size_t addr_len = 0; - -#ifndef __MINGW32__ - if (inet_pton(AF_INET6, host_.c_str(), &addr6)) { - type = GEN_IPADD; - addr_len = sizeof(struct in6_addr); - } - else if (inet_pton(AF_INET, host_.c_str(), &addr)) { - type = GEN_IPADD; - addr_len = sizeof(struct in_addr); - } -#endif - - auto alt_names = static_cast( - X509_get_ext_d2i(server_cert, NID_subject_alt_name, nullptr, nullptr)); - - if (alt_names) { - auto dsn_matched = false; - auto ip_mached = false; - - auto count = sk_GENERAL_NAME_num(alt_names); - - for (auto i = 0; i < count && !dsn_matched; i++) { - auto val = sk_GENERAL_NAME_value(alt_names, i); - if (val->type == type) { - auto name = (const char*)ASN1_STRING_get0_data(val->d.ia5); - auto name_len = (size_t)ASN1_STRING_length(val->d.ia5); - - if (strlen(name) == name_len) { - switch (type) { - case GEN_DNS: dsn_matched = check_host_name(name, name_len); break; - - case GEN_IPADD: - if (!memcmp(&addr6, name, addr_len) || - !memcmp(&addr, name, addr_len)) { - ip_mached = true; - } - break; - } - } - } - } - - if (dsn_matched || ip_mached) { ret = true; } - } - - GENERAL_NAMES_free((STACK_OF(GENERAL_NAME)*)alt_names); - - return ret; - } - - bool SSLClient::verify_host_with_common_name(X509* server_cert) const { - const auto subject_name = X509_get_subject_name(server_cert); - - if (subject_name != nullptr) { - char name[BUFSIZ]; - auto name_len = X509_NAME_get_text_by_NID(subject_name, NID_commonName, - name, sizeof(name)); - - if (name_len != -1) { - return check_host_name(name, static_cast(name_len)); - } - } - - return false; - } - - bool SSLClient::check_host_name(const char* pattern, - size_t pattern_len) const { - if (host_.size() == pattern_len && host_ == pattern) { return true; } - - // Wildcard match - // https://bugs.launchpad.net/ubuntu/+source/firefox-3.0/+bug/376484 - std::vector pattern_components; - detail::split(&pattern[0], &pattern[pattern_len], '.', - [&](const char* b, const char* e) { - pattern_components.emplace_back(std::string(b, e)); - }); - - if (host_components_.size() != pattern_components.size()) { return false; } - - auto itr = pattern_components.begin(); - for (const auto& h : host_components_) { - auto& p = *itr; - if (p != h && p != "*") { - auto partial_match = (p.size() > 0 && p[p.size() - 1] == '*' && - !p.compare(0, p.size() - 1, h)); - if (!partial_match) { return false; } - } - ++itr; - } - - return true; - } -#endif - -} // namespace httplib diff --git a/web/httplib.h b/web/httplib.h deleted file mode 100644 index 1861cd9..0000000 --- a/web/httplib.h +++ /dev/null @@ -1,908 +0,0 @@ -// -// httplib.h -// -// Copyright (c) 2020 Yuji Hirose. All rights reserved. -// MIT License -// - -#ifndef CPPHTTPLIB_HTTPLIB_H -#define CPPHTTPLIB_HTTPLIB_H - -/* - * Configuration - */ - -#define CPPHTTPLIB_ZLIB_SUPPORT -#define CPPHTTPLIB_OPENSSL_SUPPORT - -#ifndef CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND -#define CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND 5 -#endif - -#ifndef CPPHTTPLIB_KEEPALIVE_TIMEOUT_USECOND -#define CPPHTTPLIB_KEEPALIVE_TIMEOUT_USECOND 0 -#endif - -#ifndef CPPHTTPLIB_KEEPALIVE_MAX_COUNT -#define CPPHTTPLIB_KEEPALIVE_MAX_COUNT 5 -#endif - -#ifndef CPPHTTPLIB_READ_TIMEOUT_SECOND -#define CPPHTTPLIB_READ_TIMEOUT_SECOND 5 -#endif - -#ifndef CPPHTTPLIB_READ_TIMEOUT_USECOND -#define CPPHTTPLIB_READ_TIMEOUT_USECOND 0 -#endif - -#ifndef CPPHTTPLIB_REQUEST_URI_MAX_LENGTH -#define CPPHTTPLIB_REQUEST_URI_MAX_LENGTH 8192 -#endif - -#ifndef CPPHTTPLIB_REDIRECT_MAX_COUNT -#define CPPHTTPLIB_REDIRECT_MAX_COUNT 20 -#endif - -#ifndef CPPHTTPLIB_PAYLOAD_MAX_LENGTH -#define CPPHTTPLIB_PAYLOAD_MAX_LENGTH (std::numeric_limits::max()) -#endif - -#ifndef CPPHTTPLIB_RECV_BUFSIZ -#define CPPHTTPLIB_RECV_BUFSIZ size_t(8192u) -#endif - -#ifndef CPPHTTPLIB_THREAD_POOL_COUNT -#define CPPHTTPLIB_THREAD_POOL_COUNT 64 -// (std::max(1u, std::thread::hardware_concurrency() - 1)) -#endif - - /* - * Headers - */ - -#ifdef _WIN32 -#ifndef _CRT_SECURE_NO_WARNINGS -#define _CRT_SECURE_NO_WARNINGS -#endif //_CRT_SECURE_NO_WARNINGS - -#ifndef _CRT_NONSTDC_NO_DEPRECATE -#define _CRT_NONSTDC_NO_DEPRECATE -#endif //_CRT_NONSTDC_NO_DEPRECATE - -#if defined(_MSC_VER) -#ifdef _WIN64 -using ssize_t = __int64; -#else -using ssize_t = int; -#endif - -#if _MSC_VER < 1900 -#define snprintf _snprintf_s -#endif -#endif // _MSC_VER - -#ifndef S_ISREG -#define S_ISREG(m) (((m)&S_IFREG) == S_IFREG) -#endif // S_ISREG - -#ifndef S_ISDIR -#define S_ISDIR(m) (((m)&S_IFDIR) == S_IFDIR) -#endif // S_ISDIR - -#ifndef NOMINMAX -#define NOMINMAX -#endif // NOMINMAX - -#include -#include -#include - -#ifndef WSA_FLAG_NO_HANDLE_INHERIT -#define WSA_FLAG_NO_HANDLE_INHERIT 0x80 -#endif - -#ifdef _MSC_VER -#pragma comment(lib, "ws2_32.lib") -#endif - -#ifndef strcasecmp -#define strcasecmp _stricmp -#endif // strcasecmp - -using socket_t = SOCKET; -#ifdef CPPHTTPLIB_USE_POLL -#define poll(fds, nfds, timeout) WSAPoll(fds, nfds, timeout) -#endif - -#else // not _WIN32 - -#include -#include -#include -#include -#include -#ifdef CPPHTTPLIB_USE_POLL -#include -#endif -#include -#include -#include -#include -#include - -using socket_t = int; -#define INVALID_SOCKET (-1) -#endif //_WIN32 - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT -#include -#include -#include -#include - -#include -#include - -// #if OPENSSL_VERSION_NUMBER < 0x1010100fL -// #error Sorry, OpenSSL versions prior to 1.1.1 are not supported -// #endif - -#if OPENSSL_VERSION_NUMBER < 0x10100000L -#include -inline const unsigned char* ASN1_STRING_get0_data(const ASN1_STRING* asn1) { - return M_ASN1_STRING_data(asn1); -} -#endif -#endif - -#ifdef CPPHTTPLIB_ZLIB_SUPPORT -#include -#endif - -/* - * Declaration - */ -namespace httplib { - - namespace detail { - - struct ci { - bool operator()(const std::string& s1, const std::string& s2) const { - return std::lexicographical_compare( - s1.begin(), s1.end(), s2.begin(), s2.end(), - [](char c1, char c2) { return ::tolower(c1) < ::tolower(c2); }); - } - }; - - } // namespace detail - - using Headers = std::multimap; - - using Params = std::multimap; - using Match = std::smatch; - - using Progress = std::function; - - struct Response; - using ResponseHandler = std::function; - - struct MultipartFormData { - std::string name; - std::string content; - std::string filename; - std::string content_type; - }; - using MultipartFormDataItems = std::vector; - using MultipartFormDataMap = std::multimap; - - class DataSink { - public: - DataSink() = default; - DataSink(const DataSink&) = delete; - DataSink& operator=(const DataSink&) = delete; - DataSink(DataSink&&) = delete; - DataSink& operator=(DataSink&&) = delete; - - std::function write; - std::function done; - std::function is_writable; - }; - - using ContentProvider = - std::function; - - using ContentReceiver = - std::function; - - using MultipartContentHeader = - std::function; - - class ContentReader { - public: - using Reader = std::function; - using MultipartReader = std::function; - - ContentReader(Reader reader, MultipartReader muitlpart_reader) - : reader_(reader), muitlpart_reader_(muitlpart_reader) {} - - bool operator()(MultipartContentHeader header, - ContentReceiver receiver) const { - return muitlpart_reader_(header, receiver); - } - - bool operator()(ContentReceiver receiver) const { return reader_(receiver); } - - Reader reader_; - MultipartReader muitlpart_reader_; - }; - - using Range = std::pair; - using Ranges = std::vector; - - struct Request { - std::string method; - std::string path; - Headers headers; - std::string body; - - // for server - std::string version; - std::string target; - Params params; - MultipartFormDataMap files; - Ranges ranges; - Match matches; - - // for client - size_t redirect_count = CPPHTTPLIB_REDIRECT_MAX_COUNT; - ResponseHandler response_handler; - ContentReceiver content_receiver; - Progress progress; - -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - const SSL* ssl; -#endif - - bool has_header(const char* key) const; - std::string get_header_value(const char* key, size_t id = 0) const; - size_t get_header_value_count(const char* key) const; - void set_header(const char* key, const char* val); - void set_header(const char* key, const std::string& val); - - bool has_param(const char* key) const; - std::string get_param_value(const char* key, size_t id = 0) const; - size_t get_param_value_count(const char* key) const; - - bool is_multipart_form_data() const; - - bool has_file(const char* key) const; - MultipartFormData get_file_value(const char* key) const; - - // private members... - size_t content_length; - ContentProvider content_provider; - }; - - struct Response { - std::string version; - int status = -1; - Headers headers; - std::string body; - - bool has_header(const char* key) const; - std::string get_header_value(const char* key, size_t id = 0) const; - size_t get_header_value_count(const char* key) const; - void set_header(const char* key, const char* val); - void set_header(const char* key, const std::string& val); - - void set_redirect(const char* url); - void set_content(const char* s, size_t n, const char* content_type); - void set_content(const std::string& s, const char* content_type); - - void set_content_provider( - size_t length, - std::function - provider, - std::function resource_releaser = [] {}); - - void set_chunked_content_provider( - std::function provider, - std::function resource_releaser = [] {}); - - Response() = default; - Response(const Response&) = default; - Response& operator=(const Response&) = default; - Response(Response&&) = default; - Response& operator=(Response&&) = default; - ~Response() { - if (content_provider_resource_releaser) { - content_provider_resource_releaser(); - } - } - - // private members... - size_t content_length = 0; - ContentProvider content_provider; - std::function content_provider_resource_releaser; - }; - - class Stream { - public: - virtual ~Stream() = default; - - virtual bool is_readable() const = 0; - virtual bool is_writable() const = 0; - - virtual ssize_t read(char* ptr, size_t size) = 0; - virtual ssize_t write(const char* ptr, size_t size) = 0; - virtual std::string get_remote_addr() const = 0; - - template - ssize_t write_format(const char* fmt, const Args&... args); - ssize_t write(const char* ptr); - ssize_t write(const std::string& s); - }; - - class TaskQueue { - public: - TaskQueue() = default; - virtual ~TaskQueue() = default; - virtual void enqueue(std::function fn) = 0; - virtual void shutdown() = 0; - }; - - class ThreadPool : public TaskQueue { - public: - explicit ThreadPool(size_t n) : shutdown_(false) { - while (n) { - threads_.emplace_back(worker(*this)); - n--; - } - } - - ThreadPool(const ThreadPool&) = delete; - ~ThreadPool() override = default; - - void enqueue(std::function fn) override { - std::unique_lock lock(mutex_); - jobs_.push_back(fn); - cond_.notify_one(); - } - - void shutdown() override { - // Stop all worker threads... - { - std::unique_lock lock(mutex_); - shutdown_ = true; - } - - cond_.notify_all(); - - // Join... - for (auto& t : threads_) { - t.join(); - } - } - - private: - struct worker { - explicit worker(ThreadPool& pool) : pool_(pool) {} - - void operator()() { - for (;;) { - std::function fn; - { - std::unique_lock lock(pool_.mutex_); - - pool_.cond_.wait( - lock, [&] { return !pool_.jobs_.empty() || pool_.shutdown_; }); - - if (pool_.shutdown_ && pool_.jobs_.empty()) { break; } - - fn = pool_.jobs_.front(); - pool_.jobs_.pop_front(); - } - - assert(true == static_cast(fn)); - fn(); - } - } - - ThreadPool& pool_; - }; - friend struct worker; - - std::vector threads_; - std::list> jobs_; - - bool shutdown_; - - std::condition_variable cond_; - std::mutex mutex_; - }; - - using Logger = std::function; - - class Server { - public: - using Handler = std::function; - using HandlerWithContentReader = std::function; - using Expect100ContinueHandler = - std::function; - - Server(); - - virtual ~Server(); - - virtual bool is_valid() const; - - Server& Get(const char* pattern, Handler handler); - Server& Post(const char* pattern, Handler handler); - Server& Post(const char* pattern, HandlerWithContentReader handler); - Server& Put(const char* pattern, Handler handler); - Server& Put(const char* pattern, HandlerWithContentReader handler); - Server& Patch(const char* pattern, Handler handler); - Server& Patch(const char* pattern, HandlerWithContentReader handler); - Server& Delete(const char* pattern, Handler handler); - Server& Options(const char* pattern, Handler handler); - - [[deprecated]] bool set_base_dir(const char* dir, - const char* mount_point = nullptr); - bool set_mount_point(const char* mount_point, const char* dir); - bool remove_mount_point(const char* mount_point); - void set_file_extension_and_mimetype_mapping(const char* ext, - const char* mime); - void set_file_request_handler(Handler handler); - - void set_error_handler(Handler handler); - void set_logger(Logger logger); - - void set_expect_100_continue_handler(Expect100ContinueHandler handler); - - void set_keep_alive_max_count(size_t count); - void set_read_timeout(time_t sec, time_t usec); - void set_payload_max_length(size_t length); - - bool bind_to_port(const char* host, int port, int socket_flags = 0); - int bind_to_any_port(const char* host, int socket_flags = 0); - bool listen_after_bind(); - - bool listen(const char* host, int port, int socket_flags = 0); - - bool is_running() const; - void stop(); - - std::function new_task_queue; - - protected: - bool process_request(Stream& strm, bool last_connection, - bool& connection_close, - const std::function& setup_request); - - size_t keep_alive_max_count_; - time_t read_timeout_sec_; - time_t read_timeout_usec_; - size_t payload_max_length_; - - private: - using Handlers = std::vector>; - using HandlersForContentReader = - std::vector>; - - socket_t create_server_socket(const char* host, int port, - int socket_flags) const; - int bind_internal(const char* host, int port, int socket_flags); - bool listen_internal(); - - bool routing(Request& req, Response& res, Stream& strm, bool last_connection); - bool handle_file_request(Request& req, Response& res, bool head = false); - bool dispatch_request(Request& req, Response& res, Handlers& handlers); - bool dispatch_request_for_content_reader(Request& req, Response& res, - ContentReader content_reader, - HandlersForContentReader& handlers); - - bool parse_request_line(const char* s, Request& req); - bool write_response(Stream& strm, bool last_connection, const Request& req, - Response& res); - bool write_content_with_provider(Stream& strm, const Request& req, - Response& res, const std::string& boundary, - const std::string& content_type); - bool read_content(Stream& strm, bool last_connection, Request& req, - Response& res); - bool read_content_with_content_receiver( - Stream& strm, bool last_connection, Request& req, Response& res, - ContentReceiver receiver, MultipartContentHeader multipart_header, - ContentReceiver multipart_receiver); - bool read_content_core(Stream& strm, bool last_connection, Request& req, - Response& res, ContentReceiver receiver, - MultipartContentHeader mulitpart_header, - ContentReceiver multipart_receiver); - - virtual bool process_and_close_socket(socket_t sock); - - std::atomic is_running_; - std::atomic svr_sock_; - std::vector> base_dirs_; - std::map file_extension_and_mimetype_map_; - Handler file_request_handler_; - Handlers get_handlers_; - Handlers post_handlers_; - HandlersForContentReader post_handlers_for_content_reader_; - Handlers put_handlers_; - HandlersForContentReader put_handlers_for_content_reader_; - Handlers patch_handlers_; - HandlersForContentReader patch_handlers_for_content_reader_; - Handlers delete_handlers_; - Handlers options_handlers_; - Handler error_handler_; - Logger logger_; - Expect100ContinueHandler expect_100_continue_handler_; - }; - - class Client { - public: - explicit Client(const std::string& host, int port = 80, - const std::string& client_cert_path = std::string(), - const std::string& client_key_path = std::string()); - - virtual ~Client(); - - virtual bool is_valid() const; - - std::shared_ptr Get(const char* path); - - std::shared_ptr Get(const char* path, const Headers& headers); - - std::shared_ptr Get(const char* path, Progress progress); - - std::shared_ptr Get(const char* path, const Headers& headers, - Progress progress); - - std::shared_ptr Get(const char* path, - ContentReceiver content_receiver); - - std::shared_ptr Get(const char* path, const Headers& headers, - ContentReceiver content_receiver); - - std::shared_ptr - Get(const char* path, ContentReceiver content_receiver, Progress progress); - - std::shared_ptr Get(const char* path, const Headers& headers, - ContentReceiver content_receiver, - Progress progress); - - std::shared_ptr Get(const char* path, const Headers& headers, - ResponseHandler response_handler, - ContentReceiver content_receiver); - - std::shared_ptr Get(const char* path, const Headers& headers, - ResponseHandler response_handler, - ContentReceiver content_receiver, - Progress progress); - - std::shared_ptr Head(const char* path); - - std::shared_ptr Head(const char* path, const Headers& headers); - - std::shared_ptr Post(const char* path, const std::string& body, - const char* content_type); - - std::shared_ptr Post(const char* path, const Headers& headers, - const std::string& body, - const char* content_type); - - std::shared_ptr Post(const char* path, size_t content_length, - ContentProvider content_provider, - const char* content_type); - - std::shared_ptr Post(const char* path, const Headers& headers, - size_t content_length, - ContentProvider content_provider, - const char* content_type); - - std::shared_ptr Post(const char* path, const Params& params); - - std::shared_ptr Post(const char* path, const Headers& headers, - const Params& params); - - std::shared_ptr Post(const char* path, - const MultipartFormDataItems& items); - - std::shared_ptr Post(const char* path, const Headers& headers, - const MultipartFormDataItems& items); - - std::shared_ptr Put(const char* path, const std::string& body, - const char* content_type); - - std::shared_ptr Put(const char* path, const Headers& headers, - const std::string& body, - const char* content_type); - - std::shared_ptr Put(const char* path, size_t content_length, - ContentProvider content_provider, - const char* content_type); - - std::shared_ptr Put(const char* path, const Headers& headers, - size_t content_length, - ContentProvider content_provider, - const char* content_type); - - std::shared_ptr Put(const char* path, const Params& params); - - std::shared_ptr Put(const char* path, const Headers& headers, - const Params& params); - - std::shared_ptr Patch(const char* path, const std::string& body, - const char* content_type); - - std::shared_ptr Patch(const char* path, const Headers& headers, - const std::string& body, - const char* content_type); - - std::shared_ptr Patch(const char* path, size_t content_length, - ContentProvider content_provider, - const char* content_type); - - std::shared_ptr Patch(const char* path, const Headers& headers, - size_t content_length, - ContentProvider content_provider, - const char* content_type); - - std::shared_ptr Delete(const char* path); - - std::shared_ptr Delete(const char* path, const std::string& body, - const char* content_type); - - std::shared_ptr Delete(const char* path, const Headers& headers); - - std::shared_ptr Delete(const char* path, const Headers& headers, - const std::string& body, - const char* content_type); - - std::shared_ptr Options(const char* path); - - std::shared_ptr Options(const char* path, const Headers& headers); - - bool send(const Request& req, Response& res); - - bool send(const std::vector& requests, - std::vector& responses); - - void set_timeout_sec(time_t timeout_sec); - - void set_read_timeout(time_t sec, time_t usec); - - void set_keep_alive_max_count(size_t count); - - void set_basic_auth(const char* username, const char* password); - -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - void set_digest_auth(const char* username, const char* password); -#endif - - void set_follow_location(bool on); - - void set_compress(bool on); - - void set_interface(const char* intf); - - void set_proxy(const char* host, int port); - - void set_proxy_basic_auth(const char* username, const char* password); - -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - void set_proxy_digest_auth(const char* username, const char* password); -#endif - - void set_logger(Logger logger); - - protected: - bool process_request(Stream& strm, const Request& req, Response& res, - bool last_connection, bool& connection_close); - - const std::string host_; - const int port_; - const std::string host_and_port_; - - // Settings - std::string client_cert_path_; - std::string client_key_path_; - - time_t timeout_sec_ = 300; - time_t read_timeout_sec_ = CPPHTTPLIB_READ_TIMEOUT_SECOND; - time_t read_timeout_usec_ = CPPHTTPLIB_READ_TIMEOUT_USECOND; - - size_t keep_alive_max_count_ = CPPHTTPLIB_KEEPALIVE_MAX_COUNT; - - std::string basic_auth_username_; - std::string basic_auth_password_; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - std::string digest_auth_username_; - std::string digest_auth_password_; -#endif - - bool follow_location_ = false; - - bool compress_ = false; - - std::string interface_; - - std::string proxy_host_; - int proxy_port_; - - std::string proxy_basic_auth_username_; - std::string proxy_basic_auth_password_; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - std::string proxy_digest_auth_username_; - std::string proxy_digest_auth_password_; -#endif - - Logger logger_; - - void copy_settings(const Client& rhs) { - client_cert_path_ = rhs.client_cert_path_; - client_key_path_ = rhs.client_key_path_; - timeout_sec_ = rhs.timeout_sec_; - read_timeout_sec_ = rhs.read_timeout_sec_; - read_timeout_usec_ = rhs.read_timeout_usec_; - keep_alive_max_count_ = rhs.keep_alive_max_count_; - basic_auth_username_ = rhs.basic_auth_username_; - basic_auth_password_ = rhs.basic_auth_password_; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - digest_auth_username_ = rhs.digest_auth_username_; - digest_auth_password_ = rhs.digest_auth_password_; -#endif - follow_location_ = rhs.follow_location_; - compress_ = rhs.compress_; - interface_ = rhs.interface_; - proxy_host_ = rhs.proxy_host_; - proxy_port_ = rhs.proxy_port_; - proxy_basic_auth_username_ = rhs.proxy_basic_auth_username_; - proxy_basic_auth_password_ = rhs.proxy_basic_auth_password_; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - proxy_digest_auth_username_ = rhs.proxy_digest_auth_username_; - proxy_digest_auth_password_ = rhs.proxy_digest_auth_password_; -#endif - logger_ = rhs.logger_; - } - - private: - socket_t create_client_socket() const; - bool read_response_line(Stream& strm, Response& res); - bool write_request(Stream& strm, const Request& req, bool last_connection); - bool redirect(const Request& req, Response& res); - bool handle_request(Stream& strm, const Request& req, Response& res, - bool last_connection, bool& connection_close); -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - bool connect(socket_t sock, Response& res, bool& error); -#endif - - std::shared_ptr send_with_content_provider( - const char* method, const char* path, const Headers& headers, - const std::string& body, size_t content_length, - ContentProvider content_provider, const char* content_type); - - virtual bool process_and_close_socket( - socket_t sock, size_t request_count, - std::function - callback); - - virtual bool is_ssl() const; - }; - - inline void Get(std::vector& requests, const char* path, - const Headers& headers) { - Request req; - req.method = "GET"; - req.path = path; - req.headers = headers; - requests.emplace_back(std::move(req)); - } - - inline void Get(std::vector& requests, const char* path) { - Get(requests, path, Headers()); - } - - inline void Post(std::vector& requests, const char* path, - const Headers& headers, const std::string& body, - const char* content_type) { - Request req; - req.method = "POST"; - req.path = path; - req.headers = headers; - req.headers.emplace("Content-Type", content_type); - req.body = body; - requests.emplace_back(std::move(req)); - } - - inline void Post(std::vector& requests, const char* path, - const std::string& body, const char* content_type) { - Post(requests, path, Headers(), body, content_type); - } - -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - class SSLServer : public Server { - public: - SSLServer(const char* cert_path, const char* private_key_path, - const char* client_ca_cert_file_path = nullptr, - const char* client_ca_cert_dir_path = nullptr); - - ~SSLServer() override; - - bool is_valid() const override; - - private: - bool process_and_close_socket(socket_t sock) override; - - SSL_CTX* ctx_; - std::mutex ctx_mutex_; - }; - - class SSLClient : public Client { - public: - SSLClient(const std::string& host, int port = 443, - const std::string& client_cert_path = std::string(), - const std::string& client_key_path = std::string()); - - ~SSLClient() override; - - bool is_valid() const override; - - void set_ca_cert_path(const char* ca_ceert_file_path, - const char* ca_cert_dir_path = nullptr); - - void enable_server_certificate_verification(bool enabled); - - long get_openssl_verify_result() const; - - SSL_CTX* ssl_context() const noexcept; - - private: - bool process_and_close_socket( - socket_t sock, size_t request_count, - std::function - callback) override; - bool is_ssl() const override; - - bool verify_host(X509* server_cert) const; - bool verify_host_with_subject_alt_name(X509* server_cert) const; - bool verify_host_with_common_name(X509* server_cert) const; - bool check_host_name(const char* pattern, size_t pattern_len) const; - - SSL_CTX* ctx_; - std::mutex ctx_mutex_; - std::vector host_components_; - - std::string ca_cert_file_path_; - std::string ca_cert_dir_path_; - bool server_certificate_verification_ = false; - long verify_result_ = 0; - }; -#endif - - -} // namespace httplib - -#endif // CPPHTTPLIB_HTTPLIB_H \ No newline at end of file diff --git a/web/manifest.cpp b/web/manifest.cpp deleted file mode 100644 index 5aeda4b..0000000 --- a/web/manifest.cpp +++ /dev/null @@ -1,565 +0,0 @@ -#include "manifest.h" - -#include "http.h" -#include - -#include -#include -namespace fs = std::filesystem; - -typedef struct _MANIFEST_CHUNK { - char Guid[16]; - uint64_t Hash; - char ShaHash[20]; - uint8_t Group; - uint64_t Size; -} MANIFEST_CHUNK; - -typedef struct _MANIFEST_CHUNK_PART { - std::shared_ptr Chunk; - uint32_t Offset; - uint32_t Size; -} MANIFEST_CHUNK_PART; - -typedef std::unique_ptr MANIFEST_CHUNK_PARTS; -typedef struct _MANIFEST_FILE { - char FileName[128]; - char ShaHash[20]; - MANIFEST_CHUNK_PARTS ChunkParts; - uint32_t ChunkPartCount; -} MANIFEST_FILE; - -enum class EFeatureLevel : int32_t -{ - // The original version. - Original = 0, - // Support for custom fields. - CustomFields, - // Started storing the version number. - StartStoringVersion, - // Made after data files where renamed to include the hash value, these chunks now go to ChunksV2. - DataFileRenames, - // Manifest stores whether build was constructed with chunk or file data. - StoresIfChunkOrFileData, - // Manifest stores group number for each chunk/file data for reference so that external readers don't need to know how to calculate them. - StoresDataGroupNumbers, - // Added support for chunk compression, these chunks now go to ChunksV3. NB: Not File Data Compression yet. - ChunkCompressionSupport, - // Manifest stores product prerequisites info. - StoresPrerequisitesInfo, - // Manifest stores chunk download sizes. - StoresChunkFileSizes, - // Manifest can optionally be stored using UObject serialization and compressed. - StoredAsCompressedUClass, - // These two features were removed and never used. - UNUSED_0, - UNUSED_1, - // Manifest stores chunk data SHA1 hash to use in place of data compare, for faster generation. - StoresChunkDataShaHashes, - // Manifest stores Prerequisite Ids. - StoresPrerequisiteIds, - // The first minimal binary format was added. UObject classes will no longer be saved out when binary selected. - StoredAsBinaryData, - // Temporary level where manifest can reference chunks with dynamic window size, but did not serialize them. Chunks from here onwards are stored in ChunksV4. - VariableSizeChunksWithoutWindowSizeChunkInfo, - // Manifest can reference chunks with dynamic window size, and also serializes them. - VariableSizeChunks, - // Manifest stores a unique build id for exact matching of build data. - StoresUniqueBuildId, - - // !! Always after the latest version entry, signifies the latest version plus 1 to allow the following Latest alias. - LatestPlusOne, - // An alias for the actual latest version value. - Latest = (LatestPlusOne - 1), - // An alias to provide the latest version of a manifest supported by file data (nochunks). - LatestNoChunks = StoresChunkFileSizes, - // An alias to provide the latest version of a manifest supported by a json serialized format. - LatestJson = StoresPrerequisiteIds, - // An alias to provide the first available version of optimised delta manifest saving. - FirstOptimisedDelta = StoresUniqueBuildId, - - // JSON manifests were stored with a version of 255 during a certain CL range due to a bug. - // We will treat this as being StoresChunkFileSizes in code. - BrokenJsonVersion = 255, - // This is for UObject default, so that we always serialize it. - Invalid = -1 -}; - -typedef std::unique_ptr MANIFEST_FILE_LIST; -typedef std::unique_ptr[]> MANIFEST_CHUNK_LIST; -auto guidHash = [](const char* n) { return (*((uint64_t*)n)) ^ (*(((uint64_t*)n) + 1)); }; -auto guidEqual = [](const char* a, const char* b) {return !memcmp(a, b, 16); }; -typedef std::unordered_map MANIFEST_CHUNK_LOOKUP; -typedef struct _MANIFEST { - EFeatureLevel FeatureLevel; - bool bIsFileData; - uint32_t AppID; - std::string AppName; - std::string BuildVersion; - std::string LaunchExe; - std::string LaunchCommand; - //std::set PrereqIds; - //std::string PrereqName; - //std::string PrereqPath; - //std::string PrereqArgs; - MANIFEST_FILE_LIST FileManifestList; - uint32_t FileCount; - MANIFEST_CHUNK_LIST ChunkManifestList; - uint32_t ChunkCount; - - std::string CloudDirHost; - std::string CloudDirPath; -} MANIFEST; - -inline const uint8_t GetByteValue(const char Char) -{ - if (Char >= '0' && Char <= '9') - { - return Char - '0'; - } - else if (Char >= 'A' && Char <= 'F') - { - return (Char - 'A') + 10; - } - return (Char - 'a') + 10; -} - -inline int HexToBytes(const char* hex, char* output) { - int NumBytes = 0; - while (*hex) - { - output[NumBytes] = GetByteValue(*hex++) << 4; - output[NumBytes] += GetByteValue(*hex++); - ++NumBytes; - } - return NumBytes; -} - -inline void urlencode(const const char* s, std::ostringstream& e) -{ - static const char lookup[] = "0123456789abcdef"; - for (int i = 0, ix = strlen(s); i < ix; i++) - { - const char& c = s[i]; - if ((48 <= c && c <= 57) ||//0-9 - (65 <= c && c <= 90) ||//abc...xyz - (97 <= c && c <= 122) || //ABC...XYZ - (c == '-' || c == '_' || c == '.' || c == '~') - ) - { - e << c; - } - else - { - e << '%'; - e << lookup[(c & 0xF0) >> 4]; - e << lookup[(c & 0x0F)]; - } - } -} - -inline void GetDownloadFile(rapidjson::Value& manifest, std::string* id) { - auto uriStr = std::string(manifest["uri"].GetString()); - *id = std::string(uriStr.begin() + uriStr.find_last_of('/') + 1, uriStr.end()); -} - -inline void GetDownloadUrl(rapidjson::Value& manifest, std::string* host, std::string* uri) { - rapidjson::Value& uri_val = manifest["uri"]; - Uri url_v = Uri::Parse(uri_val.GetString()); - *host = url_v.Host; - if (!manifest.HasMember("queryParams")) { - *uri = url_v.Path; - } - else { - rapidjson::Value& queryParams = manifest["queryParams"]; - std::ostringstream oss; - oss << url_v.Path << "?"; - for (auto& itr : queryParams.GetArray()) { - urlencode(itr["name"].GetString(), oss); - oss << "="; - urlencode(itr["value"].GetString(), oss); - oss << "&"; - } - oss.seekp(-1, std::ios_base::end); // remove last & - oss << '\0'; - *uri = oss.str(); - } -} - -inline int random(int min, int max) //range : [min, max) -{ - static bool first = true; - if (first) - { - srand(time(NULL)); //seeding for the first time only! - first = false; - } - return min + rand() % ((max + 1) - min); -} - -inline const void HashToBytes(const char* data, char* output) { - int size = strlen(data) / 3; - char buf[4]; - buf[3] = '\0'; - for (int i = 0; i < size; i++) - { - buf[0] = data[i * 3]; - buf[1] = data[i * 3 + 1]; - buf[2] = data[i * 3 + 2]; - output[i] = atoi(buf); - } -} - -bool ManifestGrab(const char* elementsResponse, fs::path CacheFolder, MANIFEST** PManifest) { - std::string host, url; - fs::path cachePath; - { - rapidjson::Document elements; - elements.Parse(elementsResponse); - if (elements.HasParseError()) { - printf("%d %zu\n", elements.GetParseError(), elements.GetErrorOffset()); - } - rapidjson::Value& v = elements["elements"].GetArray()[0]; - - //rapidjson::Value& hash = v["hash"]; (hash is unused atm) - //char* ManifestHash = new char[hash.GetStringLength() / 2]; - //HexToBytes(hash.GetString(), ManifestHash); - - rapidjson::Value& manifest = v["manifests"][random(0, v["manifests"].GetArray().Size() - 1)]; - GetDownloadUrl(manifest, &host, &url); - std::string id; - GetDownloadFile(manifest, &id); - if (!CacheFolder.empty()) { - cachePath = CacheFolder / id; - } - } - httplib::Client c(host); - - rapidjson::Document manifestDoc; - printf("getting manifest\n"); - if (!CacheFolder.empty() && fs::status(cachePath).type() == fs::file_type::regular) { - auto fp = fopen(cachePath.string().c_str(), "rb"); - fseek(fp, 0, SEEK_END); - long manifestSize = ftell(fp); - rewind(fp); - auto manifestStr = new char[manifestSize + 1]; - fread(manifestStr, 1, manifestSize, fp); - fclose(fp); - manifestStr[manifestSize] = '\0'; - manifestDoc.Parse(manifestStr); - delete[] manifestStr; - } - else { - bool manifestStrReserved = false; - std::vector manifestStr; - manifestStr.reserve(8192); - c.Get(url.c_str(), - [&](const char* data, uint64_t data_length) { - manifestStr.insert(manifestStr.end(), data, data + data_length); - return true; - }, - [&](uint64_t len, uint64_t total) { - if (!manifestStrReserved) { - manifestStr.reserve(total); - printf("reserved for %llu\n", total); - manifestStrReserved = true; - } - static int n = 0; - if (!(n++ % 400)) { - printf("\r%lld / %lld bytes => %.1f%% complete", - len, total, - float(len * 100) / total); - } - return true; - }); - - if (!CacheFolder.empty()) { - auto fp = fopen(cachePath.string().c_str(), "wb"); - fwrite(manifestStr.data(), 1, manifestStr.size(), fp); - fclose(fp); - } - - manifestStr.push_back('\0'); - manifestDoc.Parse(manifestStr.data()); - } - printf("\n"); - printf("parsing\n"); - printf("parsed\n"); - - if (manifestDoc.HasParseError()) { - printf("JSON Parse Error %d @ %zu\n", manifestDoc.GetParseError(), manifestDoc.GetErrorOffset()); - } - - MANIFEST* Manifest = new MANIFEST(); - HashToBytes(manifestDoc["ManifestFileVersion"].GetString(), (char*)&Manifest->FeatureLevel); - Manifest->bIsFileData = manifestDoc["bIsFileData"].GetBool(); - HashToBytes(manifestDoc["AppID"].GetString(), (char*)&Manifest->AppID); - Manifest->AppName = manifestDoc["AppNameString"].GetString(); - Manifest->BuildVersion = manifestDoc["BuildVersionString"].GetString(); - Manifest->LaunchExe = manifestDoc["LaunchExeString"].GetString(); - Manifest->LaunchCommand = manifestDoc["LaunchCommand"].GetString(); - - Manifest->CloudDirHost = host; -#define CHUNK_DIR(dir) "/Chunks" dir "/" - const char* ChunksDir; - if (Manifest->FeatureLevel < EFeatureLevel::DataFileRenames) { - ChunksDir = CHUNK_DIR(""); - } - else if (Manifest->FeatureLevel < EFeatureLevel::ChunkCompressionSupport) { - ChunksDir = CHUNK_DIR("V2"); - } - else if (Manifest->FeatureLevel < EFeatureLevel::VariableSizeChunksWithoutWindowSizeChunkInfo) { - ChunksDir = CHUNK_DIR("V3"); - } - else { - ChunksDir = CHUNK_DIR("V4"); - } -#undef CHUNK_DIR - Manifest->CloudDirPath = url.substr(0, url.find_last_of('/')) + ChunksDir; - - MANIFEST_CHUNK_LOOKUP ChunkManifestLookup; // used to speed up lookups instead of doing a linear search over everything - { - rapidjson::Value& HashList = manifestDoc["ChunkHashList"]; - rapidjson::Value& ShaList = manifestDoc["ChunkShaList"]; - rapidjson::Value& GroupList = manifestDoc["DataGroupList"]; - rapidjson::Value& SizeList = manifestDoc["ChunkFilesizeList"]; - - Manifest->ChunkCount = HashList.MemberCount(); - Manifest->ChunkManifestList = std::make_unique[]>(Manifest->ChunkCount); - ChunkManifestLookup.reserve(Manifest->ChunkCount); - - printf("%d chunks\n", Manifest->ChunkCount); - int i = 0; - for (rapidjson::Value::ConstMemberIterator hashItr = HashList.MemberBegin(), shaItr = ShaList.MemberBegin(), groupItr = GroupList.MemberBegin(), sizeItr = SizeList.MemberBegin(); - i != Manifest->ChunkCount; ++i, ++hashItr, ++shaItr, ++groupItr, ++sizeItr) - { - auto chunk = std::make_shared(); - HexToBytes(hashItr->name.GetString(), chunk->Guid); - HashToBytes(hashItr->value.GetString(), (char*)&chunk->Hash); - HashToBytes(sizeItr->value.GetString(), (char*)&chunk->Size); - HexToBytes(shaItr->value.GetString(), (char*)&chunk->ShaHash); - chunk->Group = atoi(groupItr->value.GetString()); - - Manifest->ChunkManifestList[i] = chunk; - ChunkManifestLookup[chunk->Guid] = i; - } - } - - { - rapidjson::Value& FileList = manifestDoc["FileManifestList"]; - Manifest->FileCount = FileList.Size(); - Manifest->FileManifestList = std::make_unique(Manifest->FileCount); - - int i = 0; - for (auto& fileManifest : FileList.GetArray()) { - auto& file = Manifest->FileManifestList[i++]; - strcpy(file.FileName, fileManifest["Filename"].GetString()); - HashToBytes(fileManifest["FileHash"].GetString(), (char*)&file.ShaHash); - file.ChunkPartCount = fileManifest["FileChunkParts"].Size(); - file.ChunkParts = std::make_unique(file.ChunkPartCount); - - int j = 0; - for (auto& fileChunk : fileManifest["FileChunkParts"].GetArray()) { - MANIFEST_CHUNK_PART part; - char guidBuffer[16]; - HexToBytes(fileChunk["Guid"].GetString(), guidBuffer); - part.Chunk = Manifest->ChunkManifestList[ChunkManifestLookup[guidBuffer]]; - HashToBytes(fileChunk["Offset"].GetString(), (char*)&part.Offset); - HashToBytes(fileChunk["Size"].GetString(), (char*)&part.Size); - file.ChunkParts[j++] = part; - } - } - } - - *PManifest = Manifest; - return true; -} - -uint64_t ManifestDownloadSize(MANIFEST* Manifest) { - return std::accumulate(Manifest->ChunkManifestList.get(), Manifest->ChunkManifestList.get() + Manifest->ChunkCount, 0ull, - [](uint64_t sum, const std::shared_ptr& curr) { - return sum + curr->Size; - }); -} - -uint64_t ManifestInstallSize(MANIFEST* Manifest) { - return std::accumulate(Manifest->FileManifestList.get(), Manifest->FileManifestList.get() + Manifest->FileCount, 0ull, - [](uint64_t sum, MANIFEST_FILE& file) { - return std::accumulate(file.ChunkParts.get(), file.ChunkParts.get() + file.ChunkPartCount, sum, - [](uint64_t sum, MANIFEST_CHUNK_PART& part) { - return sum + part.Size; - }); - }); -} - -void ManifestGetFiles(MANIFEST* Manifest, MANIFEST_FILE** PFileList, uint32_t* PFileCount, uint16_t* PStrideSize) { - *PFileList = Manifest->FileManifestList.get(); - *PFileCount = Manifest->FileCount; - *PStrideSize = sizeof(MANIFEST_FILE); -} - -void ManifestGetChunks(MANIFEST* Manifest, std::shared_ptr** PChunkList, uint32_t* PChunkCount) { - *PChunkList = Manifest->ChunkManifestList.get(); - *PChunkCount = Manifest->ChunkCount; -} - -MANIFEST_FILE* ManifestGetFile(MANIFEST* Manifest, const char* Filename) { - for (int i = 0; i < Manifest->FileCount; ++i) { - if (!strcmp(Filename, Manifest->FileManifestList[i].FileName)) { - return &Manifest->FileManifestList[i]; - } - } - return nullptr; -} - -void ManifestGetCloudDir(MANIFEST* Manifest, char* CloudDirHostBuffer, char* CloudDirPathBuffer) { - strcpy(CloudDirHostBuffer, Manifest->CloudDirHost.c_str()); - strcpy(CloudDirPathBuffer, Manifest->CloudDirPath.c_str()); -} - -void ManifestGetLaunchInfo(MANIFEST* Manifest, char* ExeBuffer, char* CommandBuffer) { - strcpy(ExeBuffer, Manifest->LaunchExe.c_str()); - strcpy(CommandBuffer, Manifest->LaunchCommand.c_str()); -} - -void ManifestDelete(MANIFEST* Manifest) { - delete Manifest; -} - -typedef struct _MANIFEST_AUTH { - std::string AccessToken; - time_t ExpiresAt; -} MANIFEST_AUTH; - -inline int ParseInt(const char* value) -{ - return std::strtol(value, nullptr, 10); -} - -bool UpdateManifest(MANIFEST_AUTH* Auth) { - httplib::SSLClient client("account-public-service-prod03.ol.epicgames.com"); - static const httplib::Headers headers = { - { "Authorization", "basic MzhkYmZjMzE5NjAyNGQ1OTgwMzg2YTM3YjdjNzkyYmI6YTYyODBiODctZTQ1ZS00MDliLTk2ODEtOGYxNWViN2RiY2Y1" } - }; - static const httplib::Params params = { - { "grant_type", "client_credentials" } - }; - rapidjson::Document d; - auto ptr = client.Post("/account/api/oauth/token", headers, params); - if (!ptr) { - return false; - } - d.Parse(ptr->body.c_str()); - - rapidjson::Value& token = d["access_token"]; - Auth->AccessToken = token.GetString(); - - rapidjson::Value& expires_at = d["expires_at"]; - auto expires_str = expires_at.GetString(); - constexpr const size_t expectedLength = sizeof("YYYY-MM-DDTHH:MM:SSZ") - 1; - static_assert(expectedLength == 20, "Unexpected ISO 8601 date/time length"); - - if (expires_at.GetStringLength() < expectedLength) - { - return false; - } - - std::tm time = { 0 }; - time.tm_year = ParseInt(&expires_str[0]) - 1900; - time.tm_mon = ParseInt(&expires_str[5]) - 1; - time.tm_mday = ParseInt(&expires_str[8]); - time.tm_hour = ParseInt(&expires_str[11]); - time.tm_min = ParseInt(&expires_str[14]); - time.tm_sec = ParseInt(&expires_str[17]); - time.tm_isdst = 0; - const int millis = expires_at.GetStringLength() > 20 ? ParseInt(&expires_str[20]) : 0; - Auth->ExpiresAt = std::mktime(&time) * 1000 + millis; - - return true; -} - -bool ManifestAuthGrab(MANIFEST_AUTH** PManifestAuth) { - MANIFEST_AUTH* Auth = new MANIFEST_AUTH; - if (!UpdateManifest(Auth)) { - return false; - } - *PManifestAuth = Auth; - return true; -} - -void ManifestAuthDelete(MANIFEST_AUTH* ManifestAuth) { - delete ManifestAuth; -} - -bool ManifestAuthGetManifest(MANIFEST_AUTH* ManifestAuth, fs::path CachePath, MANIFEST** PManifest) { - if (ManifestAuth->ExpiresAt < time(NULL)) { - UpdateManifest(ManifestAuth); - } - - httplib::SSLClient client("launcher-public-service-prod-m.ol.epicgames.com"); - char* authHeader = new char[7 + ManifestAuth->AccessToken.size() + 1]; - sprintf(authHeader, "bearer %s", ManifestAuth->AccessToken.c_str()); - - httplib::Headers headers = { - { "Authorization", authHeader } - }; - ManifestGrab(client.Post("/launcher/api/public/assets/v2/platform/Windows/catalogItem/4fe75bbc5a674f4f9b356b5c90567da5/app/Fortnite/label/Live/", headers, "", "application/json")->body.c_str(), CachePath, PManifest); - delete[] authHeader; - return true; -} - -void ManifestFileGetName(MANIFEST_FILE* File, char* FilenameBuffer) { - strcpy(FilenameBuffer, File->FileName); -} - -void ManifestFileGetChunks(MANIFEST_FILE* File, MANIFEST_CHUNK_PART** PChunkPartList, uint32_t* PChunkPartCount, uint16_t* PStrideSize) { - *PChunkPartList = File->ChunkParts.get(); - *PChunkPartCount = File->ChunkPartCount; - *PStrideSize = sizeof(MANIFEST_CHUNK_PART); -} - -bool ManifestFileGetChunkIndex(MANIFEST_FILE* File, uint64_t Offset, uint32_t* ChunkIndex, uint32_t* ChunkOffset) { - for (int i = 0; i < File->ChunkPartCount; ++i) { - if (Offset < File->ChunkParts[i].Size) { - *ChunkIndex = i; - *ChunkOffset = Offset; - return true; - } - Offset -= File->ChunkParts[i].Size; - } - return false; -} - -uint64_t ManifestFileGetFileSize(MANIFEST_FILE* File) { - return std::accumulate(File->ChunkParts.get(), File->ChunkParts.get() + File->ChunkPartCount, 0ull, - [](uint64_t sum, const MANIFEST_CHUNK_PART& curr) { - return sum + curr.Size; - }); -} - -char* ManifestFileGetSha1(MANIFEST_FILE* File) { - return File->ShaHash; -} - -MANIFEST_CHUNK* ManifestFileChunkGetChunk(MANIFEST_CHUNK_PART* ChunkPart) { - return ChunkPart->Chunk.get(); -} - -void ManifestFileChunkGetData(MANIFEST_CHUNK_PART* ChunkPart, uint32_t* POffset, uint32_t* PSize) { - *POffset = ChunkPart->Offset; - *PSize = ChunkPart->Size; -} - -char* ManifestChunkGetGuid(MANIFEST_CHUNK* Chunk) { - return Chunk->Guid; -} - -char* ManifestChunkGetSha1(MANIFEST_CHUNK* Chunk) { - return Chunk->ShaHash; -} - -// Example input UrlBuffer: https://epicgames-download1.akamaized.net/Builds/Fortnite/CloudDir/ChunksV3/ -// Make sure UrlBuffer has some extra space in front as well -void ManifestChunkAppendUrl(MANIFEST_CHUNK* Chunk, char* UrlBuffer) { - sprintf(UrlBuffer, "%s%02d/%016llX_%016llX%016llX.chunk", UrlBuffer, Chunk->Group, Chunk->Hash, ntohll(*(uint64_t*)Chunk->Guid), ntohll(*(uint64_t*)(Chunk->Guid + 8))); -} \ No newline at end of file diff --git a/web/manifest.h b/web/manifest.h deleted file mode 100644 index 3ff87b8..0000000 --- a/web/manifest.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include -#include -namespace fs = std::filesystem; - -// Grabbing and parsing manifest - -typedef struct _MANIFEST MANIFEST; -typedef struct _MANIFEST_FILE MANIFEST_FILE; -typedef struct _MANIFEST_CHUNK MANIFEST_CHUNK; -typedef struct _MANIFEST_CHUNK_PART MANIFEST_CHUNK_PART; - -uint64_t ManifestDownloadSize(MANIFEST* Manifest); -uint64_t ManifestInstallSize(MANIFEST* Manifest); -void ManifestGetFiles(MANIFEST* Manifest, MANIFEST_FILE** PFileList, uint32_t* PFileCount, uint16_t* PStrideSize); -void ManifestGetChunks(MANIFEST* Manifest, std::shared_ptr** PChunkList, uint32_t* PChunkCount); -MANIFEST_FILE* ManifestGetFile(MANIFEST* Manifest, const char* Filename); -void ManifestGetCloudDir(MANIFEST* Manifest, char* CloudDirHostBuffer, char* CloudDirPathBuffer); -void ManifestGetLaunchInfo(MANIFEST* Manifest, char* ExeBuffer, char* CommandBuffer); -void ManifestDelete(MANIFEST* Manifest); - -// Authentication - -typedef struct _MANIFEST_AUTH MANIFEST_AUTH; -bool ManifestAuthGrab(MANIFEST_AUTH** PManifestAuth); -void ManifestAuthDelete(MANIFEST_AUTH* ManifestAuth); -bool ManifestAuthGetManifest(MANIFEST_AUTH* ManifestAuth, fs::path CachePath, MANIFEST** PManifest); - -// Files - -void ManifestFileGetName(MANIFEST_FILE* File, char* FilenameBuffer); -void ManifestFileGetChunks(MANIFEST_FILE* File, MANIFEST_CHUNK_PART** PChunkPartList, uint32_t* PChunkPartCount, uint16_t* PStrideSize); -bool ManifestFileGetChunkIndex(MANIFEST_FILE* File, uint64_t Offset, uint32_t* ChunkIndex, uint32_t* ChunkOffset); -uint64_t ManifestFileGetFileSize(MANIFEST_FILE* File); -char* ManifestFileGetSha1(MANIFEST_FILE* File); - -// File Chunks - -MANIFEST_CHUNK* ManifestFileChunkGetChunk(MANIFEST_CHUNK_PART* ChunkPart); -void ManifestFileChunkGetData(MANIFEST_CHUNK_PART* ChunkPart, uint32_t* POffset, uint32_t* PSize); - -// Chunks - -char* ManifestChunkGetGuid(MANIFEST_CHUNK* Chunk); -char* ManifestChunkGetSha1(MANIFEST_CHUNK* Chunk); -void ManifestChunkAppendUrl(MANIFEST_CHUNK* Chunk, char* UrlBuffer); diff --git a/web/manifest/auth.cpp b/web/manifest/auth.cpp new file mode 100644 index 0000000..b94f732 --- /dev/null +++ b/web/manifest/auth.cpp @@ -0,0 +1,229 @@ +#include "auth.h" + +#include "../../Logger.h" +#include "../../web/http.h" + +#include +#include + +#ifndef LOG_SECTION +#define LOG_SECTION "Auth" +#endif + +inline int random(int min, int max) //range : [min, max) +{ + static bool first = true; + if (first) + { + srand(time(NULL)); //seeding for the first time only! + first = false; + } + return min + rand() % ((max + 1) - min); +} + +inline void urlencode(const const char* s, std::ostringstream& e) +{ + static const char lookup[] = "0123456789abcdef"; + for (int i = 0, ix = strlen(s); i < ix; i++) + { + const char& c = s[i]; + if ((48 <= c && c <= 57) ||//0-9 + (65 <= c && c <= 90) ||//abc...xyz + (97 <= c && c <= 122) || //ABC...XYZ + (c == '-' || c == '_' || c == '.' || c == '~') + ) + { + e << c; + } + else + { + e << '%'; + e << lookup[(c & 0xF0) >> 4]; + e << lookup[(c & 0x0F)]; + } + } +} + +ManifestAuth::ManifestAuth(fs::path& cachePath) : + CachePath(cachePath), + ExpiresAt() +{ + UpdateIfExpired(true); +} + +ManifestAuth::~ManifestAuth() +{ +} + +std::pair ManifestAuth::GetLatestManifest() +{ + UpdateIfExpired(); + + auto manifestConn = Client::CreateConnection(); + manifestConn->SetUrl("https://launcher-public-service-prod-m.ol.epicgames.com/launcher/api/public/assets/v2/platform/Windows/catalogItem/4fe75bbc5a674f4f9b356b5c90567da5/app/Fortnite/label/Live/"); + manifestConn->SetUsePost(true); + + auto authHeader = std::make_unique(7 + AccessToken.size() + 1); + sprintf(authHeader.get(), "bearer %s", AccessToken.c_str()); + manifestConn->AddRequestHeader("Authorization", authHeader.get()); + manifestConn->AddRequestHeader("Content-Type", "application/json"); + + manifestConn->Start(); + + if (manifestConn->GetResponseCode() != 200) { + if (manifestConn->GetResponseCode() == 401) { // unauthorized + UpdateIfExpired(true); + return GetLatestManifest(); + } + LOG_ERROR("Response code %d", manifestConn->GetResponseCode()); + LOG_WARN("Retrying..."); + return GetLatestManifest(); + } + + rapidjson::Document elements; + elements.Parse(manifestConn->GetResponseBody().c_str()); + if (elements.HasParseError()) { + LOG_ERROR("Getting manifest url: JSON Parse Error %d @ %zu", elements.GetParseError(), elements.GetErrorOffset()); + LOG_WARN("Retrying..."); + return GetLatestManifest(); + } + rapidjson::Value& v = elements["elements"].GetArray()[0]; + + rapidjson::Value& manifest = v["manifests"][random(0, v["manifests"].GetArray().Size() - 1)]; + + rapidjson::Value& uri_val = manifest["uri"]; + if (!manifest.HasMember("queryParams")) { + return std::make_pair(uri_val.GetString(), v["buildVersion"].GetString()); + } + else { + rapidjson::Value& queryParams = manifest["queryParams"]; + std::ostringstream oss; + oss << uri_val.GetString() << "?"; + for (auto& itr : queryParams.GetArray()) { + urlencode(itr["name"].GetString(), oss); + oss << "="; + urlencode(itr["value"].GetString(), oss); + oss << "&"; + } + oss.seekp(-1, std::ios_base::end); // remove last & + oss << '\0'; + return std::make_pair(oss.str(), v["buildVersion"].GetString()); + } +} + +std::string ManifestAuth::GetManifestId(const std::string& Url) +{ + auto UrlEnd = Url.find(".manifest"); + return std::string(Url.begin() + Url.find_last_of("/", UrlEnd) + 1, Url.begin() + UrlEnd); +} + +bool ManifestAuth::IsManifestCached(const std::string& Url) +{ + return fs::status(CachePath / GetManifestId(Url)).type() == fs::file_type::regular; +} + +Manifest ManifestAuth::GetManifest(const std::string& Url) +{ + rapidjson::Document manifestDoc; + if (IsManifestCached(Url)) { + LOG_DEBUG("Manifest is cached"); + auto fp = fopen((CachePath / GetManifestId(Url)).string().c_str(), "rb"); + fseek(fp, 0, SEEK_END); + long manifestSize = ftell(fp); + rewind(fp); + auto manifestStr = std::make_unique(manifestSize); + fread(manifestStr.get(), 1, manifestSize, fp); + fclose(fp); + + LOG_DEBUG("Parsing manifest"); + manifestDoc.Parse(manifestStr.get(), manifestSize); + } + else { + LOG_DEBUG("Manifest is not cached"); + auto manifestConn = Client::CreateConnection(); + manifestConn->SetUrl(Url); + manifestConn->Start(); + + if (manifestConn->GetResponseCode() != 200) { + LOG_ERROR("Response code %d", manifestConn->GetResponseCode()); + LOG_WARN("Retrying..."); + return GetManifest(Url); + } + + auto fp = fopen((CachePath / GetManifestId(Url)).string().c_str(), "wb"); + fwrite(manifestConn->GetResponseBody().data(), 1, manifestConn->GetResponseBody().size(), fp); + fclose(fp); + + LOG_DEBUG("Parsing manifest"); + manifestDoc.Parse(manifestConn->GetResponseBody().c_str()); + } + + if (manifestDoc.HasParseError()) { + LOG_ERROR("Reading manifest: JSON Parse Error %d @ %zu", manifestDoc.GetParseError(), manifestDoc.GetErrorOffset()); + LOG_DEBUG("Removing cached file"); + fs::remove(CachePath / GetManifestId(Url)); + LOG_WARN("Retrying..."); + return GetManifest(Url); + } + + return Manifest(manifestDoc, Url); +} + +inline int ParseInt(const char* value) +{ + return std::strtol(value, nullptr, 10); +} + +void ManifestAuth::UpdateIfExpired(bool force) +{ + if (!force && ExpiresAt > time(nullptr)) { + return; + } + LOG_DEBUG("Updating auth"); + + auto tokenConn = Client::CreateConnection(); + + tokenConn->SetUsePost(true); + tokenConn->SetUrl("https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/token"); + tokenConn->SetRequestBody("grant_type=client_credentials"); + tokenConn->AddRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + tokenConn->AddRequestHeader("Authorization", "basic MzRhMDJjZjhmNDQxNGUyOWIxNTkyMTg3NmRhMzZmOWE6ZGFhZmJjY2M3Mzc3NDUwMzlkZmZlNTNkOTRmYzc2Y2Y="); + tokenConn->Start(); + + if (tokenConn->GetResponseCode() != 200) { + LOG_ERROR("Response code %d", tokenConn->GetResponseCode()); + LOG_WARN("Retrying..."); + return UpdateIfExpired(true); + } + + rapidjson::Document d; + d.Parse(tokenConn->GetResponseBody().c_str()); + if (d.HasParseError()) { + LOG_ERROR("Getting auth creds: JSON Parse Error %d @ %zu", d.GetParseError(), d.GetErrorOffset()); + } + + rapidjson::Value& token = d["access_token"]; + AccessToken = token.GetString(); + + rapidjson::Value& expires_at = d["expires_at"]; + auto expires_str = expires_at.GetString(); + constexpr const auto expectedLength = sizeof("YYYY-MM-DDTHH:MM:SSZ") - 1; + static_assert(expectedLength == 20, "Unexpected ISO 8601 date/time length"); + + if (expires_at.GetStringLength() < expectedLength) + { + LOG_WARN("Could not parse expires_at value, using expires_in value"); + ExpiresAt = time(nullptr) + d["expires_in"].GetInt(); + return; + } + + std::tm time = { 0 }; + time.tm_year = ParseInt(&expires_str[0]) - 1900; + time.tm_mon = ParseInt(&expires_str[5]) - 1; + time.tm_mday = ParseInt(&expires_str[8]); + time.tm_hour = ParseInt(&expires_str[11]); + time.tm_min = ParseInt(&expires_str[14]); + time.tm_sec = ParseInt(&expires_str[17]); + time.tm_isdst = 0; + ExpiresAt = std::mktime(&time); +} diff --git a/web/manifest/auth.h b/web/manifest/auth.h new file mode 100644 index 0000000..4dbb352 --- /dev/null +++ b/web/manifest/auth.h @@ -0,0 +1,27 @@ +#pragma once + +#include "manifest.h" + +#include +#include + +namespace fs = std::filesystem; + +class ManifestAuth { +public: + ManifestAuth(fs::path& cachePath); + ~ManifestAuth(); + + std::pair ManifestAuth::GetLatestManifest(); + std::string GetManifestId(const std::string& Url); + bool IsManifestCached(const std::string& Url); + Manifest GetManifest(const std::string& Url); + +private: + void UpdateIfExpired(bool force = false); + + fs::path CachePath; + + std::string AccessToken; + time_t ExpiresAt; +}; \ No newline at end of file diff --git a/web/manifest/chunk.cpp b/web/manifest/chunk.cpp new file mode 100644 index 0000000..1f5c0bb --- /dev/null +++ b/web/manifest/chunk.cpp @@ -0,0 +1,23 @@ +#include "chunk.h" + +#define WIN32_LEAN_AND_MEAN +#include + +std::string Chunk::GetGuid() { + char GuidBuffer[33]; + sprintf(GuidBuffer, "%016llX%016llX", ntohll(*(uint64_t*)Guid), ntohll(*(uint64_t*)(Guid + 8))); + return GuidBuffer; +} + +std::string Chunk::GetFilePath() { + char PathBuffer[53]; + sprintf(PathBuffer, "FF/%016llX%016llX", ntohll(*(uint64_t*)Guid), ntohll(*(uint64_t*)(Guid + 8))); + memcpy(PathBuffer, PathBuffer + 3, 2); + return PathBuffer; +} + +std::string Chunk::GetUrl() { + char UrlBuffer[59]; + sprintf(UrlBuffer, "%02d/%016llX_%016llX%016llX.chunk", Group, Hash, ntohll(*(uint64_t*)Guid), ntohll(*(uint64_t*)(Guid + 8))); + return UrlBuffer; +} \ No newline at end of file diff --git a/web/manifest/chunk.h b/web/manifest/chunk.h new file mode 100644 index 0000000..3aa6e3a --- /dev/null +++ b/web/manifest/chunk.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +struct Chunk { + char Guid[16]; + uint64_t Hash; + char ShaHash[20]; + uint8_t Group; + uint64_t Size; + + std::string GetGuid(); + std::string GetFilePath(); + std::string GetUrl(); +}; \ No newline at end of file diff --git a/web/manifest/chunk_part.h b/web/manifest/chunk_part.h new file mode 100644 index 0000000..9c4def8 --- /dev/null +++ b/web/manifest/chunk_part.h @@ -0,0 +1,11 @@ +#pragma once + +#include "chunk.h" + +#include + +struct ChunkPart { + std::shared_ptr Chunk; + uint32_t Offset; + uint32_t Size; +}; \ No newline at end of file diff --git a/web/manifest/feature_level.h b/web/manifest/feature_level.h new file mode 100644 index 0000000..73ff8ab --- /dev/null +++ b/web/manifest/feature_level.h @@ -0,0 +1,59 @@ +#pragma once + +#include + +enum class EFeatureLevel : int32_t +{ + // The original version. + Original = 0, + // Support for custom fields. + CustomFields, + // Started storing the version number. + StartStoringVersion, + // Made after data files where renamed to include the hash value, these chunks now go to ChunksV2. + DataFileRenames, + // Manifest stores whether build was constructed with chunk or file data. + StoresIfChunkOrFileData, + // Manifest stores group number for each chunk/file data for reference so that external readers don't need to know how to calculate them. + StoresDataGroupNumbers, + // Added support for chunk compression, these chunks now go to ChunksV3. NB: Not File Data Compression yet. + ChunkCompressionSupport, + // Manifest stores product prerequisites info. + StoresPrerequisitesInfo, + // Manifest stores chunk download sizes. + StoresChunkFileSizes, + // Manifest can optionally be stored using UObject serialization and compressed. + StoredAsCompressedUClass, + // These two features were removed and never used. + UNUSED_0, + UNUSED_1, + // Manifest stores chunk data SHA1 hash to use in place of data compare, for faster generation. + StoresChunkDataShaHashes, + // Manifest stores Prerequisite Ids. + StoresPrerequisiteIds, + // The first minimal binary format was added. UObject classes will no longer be saved out when binary selected. + StoredAsBinaryData, + // Temporary level where manifest can reference chunks with dynamic window size, but did not serialize them. Chunks from here onwards are stored in ChunksV4. + VariableSizeChunksWithoutWindowSizeChunkInfo, + // Manifest can reference chunks with dynamic window size, and also serializes them. + VariableSizeChunks, + // Manifest stores a unique build id for exact matching of build data. + StoresUniqueBuildId, + + // !! Always after the latest version entry, signifies the latest version plus 1 to allow the following Latest alias. + LatestPlusOne, + // An alias for the actual latest version value. + Latest = (LatestPlusOne - 1), + // An alias to provide the latest version of a manifest supported by file data (nochunks). + LatestNoChunks = StoresChunkFileSizes, + // An alias to provide the latest version of a manifest supported by a json serialized format. + LatestJson = StoresPrerequisiteIds, + // An alias to provide the first available version of optimised delta manifest saving. + FirstOptimisedDelta = StoresUniqueBuildId, + + // JSON manifests were stored with a version of 255 during a certain CL range due to a bug. + // We will treat this as being StoresChunkFileSizes in code. + BrokenJsonVersion = 255, + // This is for UObject default, so that we always serialize it. + Invalid = -1 +}; \ No newline at end of file diff --git a/web/manifest/file.cpp b/web/manifest/file.cpp new file mode 100644 index 0000000..0fdabad --- /dev/null +++ b/web/manifest/file.cpp @@ -0,0 +1,22 @@ +#include "file.h" + +#include + +uint64_t File::GetFileSize() { + return std::accumulate(ChunkParts.begin(), ChunkParts.end(), 0ull, + [](uint64_t sum, const ChunkPart& curr) { + return sum + curr.Size; + }); +} + +bool File::GetChunkIndex(uint64_t Offset, uint32_t& ChunkIndex, uint32_t& ChunkOffset) +{ + for (ChunkIndex = 0; ChunkIndex < ChunkParts.size(); ++ChunkIndex) { + if (Offset < ChunkParts[ChunkIndex].Size) { + ChunkOffset = Offset; + return true; + } + Offset -= ChunkParts[ChunkIndex].Size; + } + return false; +} diff --git a/web/manifest/file.h b/web/manifest/file.h new file mode 100644 index 0000000..5840170 --- /dev/null +++ b/web/manifest/file.h @@ -0,0 +1,16 @@ +#pragma once + +#include "chunk_part.h" + +#include +#include + +struct File { + std::string FileName; + char ShaHash[20]; + std::vector ChunkParts; + + uint64_t GetFileSize(); + + bool GetChunkIndex(uint64_t Offset, uint32_t& ChunkIndex, uint32_t& ChunkOffset); +}; \ No newline at end of file diff --git a/web/manifest/manifest.cpp b/web/manifest/manifest.cpp new file mode 100644 index 0000000..1106b68 --- /dev/null +++ b/web/manifest/manifest.cpp @@ -0,0 +1,143 @@ +#include "manifest.h" + +#include +#include + +inline const void HashToBytes(const char* data, char* output) { + int size = strlen(data) / 3; + char buf[4]; + buf[3] = '\0'; + for (int i = 0; i < size; i++) + { + buf[0] = data[i * 3]; + buf[1] = data[i * 3 + 1]; + buf[2] = data[i * 3 + 2]; + output[i] = atoi(buf); + } +} + +inline const uint8_t GetByteValue(const char Char) +{ + if (Char >= '0' && Char <= '9') + { + return Char - '0'; + } + else if (Char >= 'A' && Char <= 'F') + { + return (Char - 'A') + 10; + } + return (Char - 'a') + 10; +} + +inline const int HexToBytes(const char* hex, char* output) { + int NumBytes = 0; + while (*hex) + { + output[NumBytes] = GetByteValue(*hex++) << 4; + output[NumBytes] += GetByteValue(*hex++); + ++NumBytes; + } + return NumBytes; +} + +auto guidHash = [](const char* n) { return (*((uint64_t*)n)) ^ (*(((uint64_t*)n) + 1)); }; +auto guidEqual = [](const char* a, const char* b) {return !memcmp(a, b, 16); }; +typedef std::unordered_map MANIFEST_CHUNK_LOOKUP; + +Manifest::Manifest(const rapidjson::Document& jsonData, const std::string& url) +{ + HashToBytes(jsonData["ManifestFileVersion"].GetString(), (char*)&FeatureLevel); + bIsFileData = jsonData["bIsFileData"].GetBool(); + HashToBytes(jsonData["AppID"].GetString(), (char*)&AppID); + AppName = jsonData["AppNameString"].GetString(); + BuildVersion = jsonData["BuildVersionString"].GetString(); + LaunchExe = jsonData["LaunchExeString"].GetString(); + LaunchCommand = jsonData["LaunchCommand"].GetString(); + +#define CHUNK_DIR(dir) "/Chunks" dir "/" + const char* ChunksDir; + if (FeatureLevel < EFeatureLevel::DataFileRenames) { + ChunksDir = CHUNK_DIR(""); + } + else if (FeatureLevel < EFeatureLevel::ChunkCompressionSupport) { + ChunksDir = CHUNK_DIR("V2"); + } + else if (FeatureLevel < EFeatureLevel::VariableSizeChunksWithoutWindowSizeChunkInfo) { + ChunksDir = CHUNK_DIR("V3"); + } + else { + ChunksDir = CHUNK_DIR("V4"); + } +#undef CHUNK_DIR + CloudDir = url.substr(0, url.find_last_of('/')) + ChunksDir; + + MANIFEST_CHUNK_LOOKUP ChunkManifestLookup; // used to speed up lookups instead of doing a linear search over everything + { + const rapidjson::Value& HashList = jsonData["ChunkHashList"]; + const rapidjson::Value& ShaList = jsonData["ChunkShaList"]; + const rapidjson::Value& GroupList = jsonData["DataGroupList"]; + const rapidjson::Value& SizeList = jsonData["ChunkFilesizeList"]; + + ChunkManifestList.reserve(HashList.MemberCount()); + ChunkManifestLookup.reserve(HashList.MemberCount()); + + int i = 0; + for (rapidjson::Value::ConstMemberIterator hashItr = HashList.MemberBegin(), shaItr = ShaList.MemberBegin(), groupItr = GroupList.MemberBegin(), sizeItr = SizeList.MemberBegin(); + i != HashList.MemberCount(); ++i, ++hashItr, ++shaItr, ++groupItr, ++sizeItr) + { + auto& chunk = ChunkManifestList.emplace_back(std::make_shared()); + HexToBytes(hashItr->name.GetString(), chunk->Guid); + HashToBytes(hashItr->value.GetString(), (char*)&chunk->Hash); + HashToBytes(sizeItr->value.GetString(), (char*)&chunk->Size); + HexToBytes(shaItr->value.GetString(), (char*)&chunk->ShaHash); + chunk->Group = atoi(groupItr->value.GetString()); + + ChunkManifestLookup[chunk->Guid] = i; + } + } + + { + const rapidjson::Value& FileList = jsonData["FileManifestList"]; + FileManifestList.reserve(FileList.Size()); + + for (auto& fileManifest : FileList.GetArray()) { + File file; + file.FileName = fileManifest["Filename"].GetString(); + HashToBytes(fileManifest["FileHash"].GetString(), (char*)&file.ShaHash); + file.ChunkParts.reserve(fileManifest["FileChunkParts"].Size()); + + for (auto& fileChunk : fileManifest["FileChunkParts"].GetArray()) { + auto& part = file.ChunkParts.emplace_back(); + char guidBuffer[16]; + HexToBytes(fileChunk["Guid"].GetString(), guidBuffer); + part.Chunk = ChunkManifestList[ChunkManifestLookup[guidBuffer]]; + HashToBytes(fileChunk["Offset"].GetString(), (char*)&part.Offset); + HashToBytes(fileChunk["Size"].GetString(), (char*)&part.Size); + } + FileManifestList.emplace_back(file); + } + } +} + +Manifest::~Manifest() +{ +} + +uint64_t Manifest::GetDownloadSize() +{ + return std::accumulate(ChunkManifestList.begin(), ChunkManifestList.end(), 0ull, + [](uint64_t sum, const std::shared_ptr& curr) { + return sum + curr->Size; + }); +} + +uint64_t Manifest::GetInstallSize() +{ + return std::accumulate(FileManifestList.begin(), FileManifestList.end(), 0ull, + [](uint64_t sum, File& file) { + return std::accumulate(file.ChunkParts.begin(), file.ChunkParts.end(), sum, + [](uint64_t sum, ChunkPart& part) { + return sum + part.Size; + }); + }); +} diff --git a/web/manifest/manifest.h b/web/manifest/manifest.h new file mode 100644 index 0000000..09ac4bf --- /dev/null +++ b/web/manifest/manifest.h @@ -0,0 +1,32 @@ +#pragma once + +#include "feature_level.h" +#include "file.h" + +#include +#include + +struct Manifest { +public: + Manifest(const rapidjson::Document& jsonData, const std::string& url); + ~Manifest(); + + uint64_t GetDownloadSize(); + uint64_t GetInstallSize(); + + EFeatureLevel FeatureLevel; + bool bIsFileData; + uint32_t AppID; + std::string AppName; + std::string BuildVersion; + std::string LaunchExe; + std::string LaunchCommand; + //std::set PrereqIds; + //std::string PrereqName; + //std::string PrereqPath; + //std::string PrereqArgs; + std::vector FileManifestList; + std::vector> ChunkManifestList; + + std::string CloudDir; +}; \ No newline at end of file diff --git a/web/url.hh b/web/url.hh deleted file mode 100644 index 2bb8d2b..0000000 --- a/web/url.hh +++ /dev/null @@ -1,72 +0,0 @@ -// Taken from https://stackoverflow.com/a/11044337 - -#include -#include // find - -struct Uri -{ -public: - std::string QueryString, Path, Protocol, Host, Port; - - static Uri Parse(const std::string& uri) - { - Uri result; - - typedef std::string::const_iterator iterator_t; - - if (uri.length() == 0) - return result; - - iterator_t uriEnd = uri.end(); - - // get query start - iterator_t queryStart = std::find(uri.begin(), uriEnd, '?'); - - // protocol - iterator_t protocolStart = uri.begin(); - iterator_t protocolEnd = std::find(protocolStart, uriEnd, ':'); //"://"); - - if (protocolEnd != uriEnd) - { - std::string prot = &*(protocolEnd); - if ((prot.length() > 3) && (prot.substr(0, 3) == "://")) - { - result.Protocol = std::string(protocolStart, protocolEnd); - protocolEnd += 3; // :// - } - else - protocolEnd = uri.begin(); // no protocol - } - else - protocolEnd = uri.begin(); // no protocol - - // host - iterator_t hostStart = protocolEnd; - iterator_t pathStart = std::find(hostStart, uriEnd, '/'); // get pathStart - - iterator_t hostEnd = std::find(protocolEnd, - (pathStart != uriEnd) ? pathStart : queryStart, - ':'); // check for port - - result.Host = std::string(hostStart, hostEnd); - - // port - if ((hostEnd != uriEnd) && ((&*(hostEnd))[0] == ':')) // we have a port - { - hostEnd++; - iterator_t portEnd = (pathStart != uriEnd) ? pathStart : queryStart; - result.Port = std::string(hostEnd, portEnd); - } - - // path - if (pathStart != uriEnd) - result.Path = std::string(pathStart, queryStart); - - // query - if (queryStart != uriEnd) - result.QueryString = std::string(queryStart, uri.end()); - - return result; - - } // Parse -}; // uri \ No newline at end of file