From 0f58d40f521bb8aaeff47c6ac02b9ed43bd081e8 Mon Sep 17 00:00:00 2001 From: mbusc Date: Thu, 28 Aug 2025 15:22:00 +0200 Subject: [PATCH] Tests: add live integration test (optional via PRIVATEBIN_IT), fix WinHTTP host/port + TLS opts, robust JSON parser (meta.time_to_live), CTest wiring; Add LLVM/clang-cl coverage option and docs; add build_thinkpad.bat; README updates --- CMakeLists.txt | 47 ++++++- README.md | 90 +++++++++++- Testing/Temporary/CTestCostData.txt | 1 + .../Testing/Temporary/CTestCostData.txt | 1 + build_thinkpad.bat | 128 ++++++++++++++++++ src/http_client.cpp | 95 +++++++++++-- src/json_parser.cpp | 14 +- tests/CMakeLists.txt | 29 ++-- tests/test_basic.cpp | 91 +++++++++++-- vcpkg.json | 10 +- 10 files changed, 467 insertions(+), 39 deletions(-) create mode 100644 Testing/Temporary/CTestCostData.txt create mode 100644 build_tests/Testing/Temporary/CTestCostData.txt create mode 100644 build_thinkpad.bat diff --git a/CMakeLists.txt b/CMakeLists.txt index 69bec48..d375c5d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,4 +64,49 @@ install(TARGETS privatebinapi ARCHIVE DESTINATION lib ) -install(FILES ${HEADERS} DESTINATION include/privatebinapi) \ No newline at end of file +install(FILES ${HEADERS} DESTINATION include/privatebinapi) + +# Tests +include(CTest) +enable_testing() +add_subdirectory(tests) + +# ===================== LLVM/clang-cl Coverage (optional) ===================== +option(ENABLE_LLVM_COVERAGE "Enable LLVM/clang-cl coverage instrumentation" OFF) + +if(ENABLE_LLVM_COVERAGE) + if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang") + message(FATAL_ERROR "ENABLE_LLVM_COVERAGE requires clang/clang-cl as compiler (CMAKE_CXX_COMPILER_ID=Clang)") + endif() + + # Instrumentation flags + add_compile_options(-fprofile-instr-generate -fcoverage-mapping) + add_link_options(-fprofile-instr-generate) + + # Helper variables for report tools (can be overridden from environment) + set(LLVM_PROFDATA "llvm-profdata" CACHE STRING "Path to llvm-profdata") + set(LLVM_COV "llvm-cov" CACHE STRING "Path to llvm-cov") + + # Custom target to run tests and produce coverage report + # Usage: cmake --build build --target coverage_llvm --config Release + add_custom_target( + coverage_llvm + COMMAND ${CMAKE_COMMAND} -E env + LLVM_PROFILE_FILE=${CMAKE_BINARY_DIR}/coverage/%p-%m.profraw + ctest -C $,${CMAKE_BUILD_TYPE},$> --output-on-failure + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/coverage + COMMAND ${LLVM_PROFDATA} merge -sparse ${CMAKE_BINARY_DIR}/coverage/*.profraw -o ${CMAKE_BINARY_DIR}/coverage/merged.profdata + COMMAND ${LLVM_COV} report + $ + --instr-profile=${CMAKE_BINARY_DIR}/coverage/merged.profdata + --ignore-filename-regex="(vcpkg|external|CMakeFiles)" + COMMAND ${LLVM_COV} show + $ + --instr-profile=${CMAKE_BINARY_DIR}/coverage/merged.profdata + --ignore-filename-regex="(vcpkg|external|CMakeFiles)" + --format=html + --output-dir=${CMAKE_BINARY_DIR}/coverage/html + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + DEPENDS privatebinapi + ) +endif() \ No newline at end of file diff --git a/README.md b/README.md index 84dc146..1ec9dc6 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ This library provides a simple C++ interface for interacting with PrivateBin ser ### Prerequisites - CMake 3.10+ -- C++17-fähiger Compiler (MSVC 2022 bzw. GCC/Clang) +- C++17-capable compiler (MSVC 2022 or GCC/Clang) - Git -- vcpkg (wird bei Makefile-Nutzung automatisch gebootstrapped) +- vcpkg (automatically bootstrapped by the Makefile) ### Dependencies @@ -38,7 +38,7 @@ This library provides a simple C++ interface for interacting with PrivateBin ser make ``` -2) Beispiel bauen und ausführen: +2) Build and run the example: ``` make example @@ -70,6 +70,29 @@ cmake -S example -B example/build -DCMAKE_BUILD_TYPE=Release cmake --build example/build --config Release ``` +### Windows (PowerShell) – Build via build_thinkpad.bat + +For systems with Visual Studio 2022 Build Tools (C++ workload) and vcpkg in the user profile, there is a robust build script: + +``` +cd C:\Users\mbusc\source\repos\lib-privatebin +./build_thinkpad.bat +``` + +Notes: +- Requires Visual Studio 2022 Build Tools with C++ tools and the Windows 11 SDK. If `VsDevCmd.bat` is found, the script automatically initializes the MSVC environment. +- vcpkg is bootstrapped if needed; missing dependencies (`cryptopp`, `nlohmann-json`) are installed. +- If vcpkg requires a baseline, the script sets it automatically. + +Edition/Path notes: +- If you use another VS edition (e.g., Professional/Enterprise), adjust the paths in `build_thinkpad.bat`: + - `C:\Program Files\Microsoft Visual Studio\2022\Professional\Common7\Tools\VsDevCmd.bat` + - `C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat` +- Alternatively, you can use `vswhere.exe` to discover the installation path: + ```powershell + & 'C:\Program Files\Microsoft Visual Studio\Installer\vswhere.exe' -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + ``` + ## Usage ### API Functions @@ -142,4 +165,63 @@ int main() { ## License -This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file +This project is licensed under the MIT License - see the LICENSE file for details. + +## Troubleshooting + +- vcpkg requires a baseline / "this vcpkg instance requires a manifest with a specified baseline" + - Run in the repo root to add an initial builtin baseline to `vcpkg.json`: + ```powershell + $env:VCPKG_ROOT = "$env:USERPROFILE\vcpkg" + & "$env:VCPKG_ROOT\vcpkg.exe" x-update-baseline --add-initial-baseline + ``` + - Then configure/build again. + +- Visual Studio instance not found / "could not find specified instance of Visual Studio" + - Ensure VS 2022 Build Tools or Community with C++ tools and Windows 11 SDK are installed. + - Use the Developer Command Prompt (VsDevCmd): + ```powershell + cmd /c "call `"C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools\\Common7\\Tools\\VsDevCmd.bat`" -arch=x64 && build.bat" + ``` + - Or delete the build folder to clear stale CMake cache and reconfigure: + ```powershell + Remove-Item -Recurse -Force build + ``` + +- `vswhere.exe` not found + - Add the VS Installer directory to PATH for the current session: + ```powershell + $env:PATH = 'C:\\Program Files\\Microsoft Visual Studio\\Installer;' + $env:PATH + ``` + - Or install/download `vswhere` from Microsoft and place it under the Installer folder. + +- `cryptoppConfig.cmake` / `cryptopp-config.cmake` not found during CMake configure + - Make sure CMake uses vcpkg's toolchain file and that the ports are installed for the active triplet: + ```powershell + $env:VCPKG_ROOT = "$env:USERPROFILE\vcpkg" + cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" + & "$env:VCPKG_ROOT\vcpkg.exe" install cryptopp nlohmann-json --triplet x64-windows + ``` + +- PowerShell line continuation issues / parser errors + - Prefer single-line commands in PowerShell (avoid backticks if unsure). The README uses single-line examples for reliability. + +## LLVM/clang-cl Coverage (Windows) + +Requirements: +- clang/clang-cl toolchain installed +- LLVM tools on PATH (`llvm-profdata`, `llvm-cov`) + +Configure with coverage: +```powershell +cmd /c "call ""C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\Common7\Tools\VsDevCmd.bat"" -arch=x64 && cmake -S . -B build-llvm -G "Ninja" -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang-cl -DENABLE_LLVM_COVERAGE=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=""%USERPROFILE%\vcpkg\scripts\buildsystems\vcpkg.cmake"" && cmake --build build-llvm --config Release" +``` + +Run coverage target (executes tests, merges profiles, generates HTML report): +```powershell +cmd /c "call ""C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\Common7\Tools\VsDevCmd.bat"" -arch=x64 && cmake --build build-llvm --target coverage_llvm --config Release && start build-llvm\coverage\html\index.html" +``` + +Notes: +- If Ninja is not available, you can use the VS generator, but Ninja is recommended for clang. +- You can override `LLVM_PROFDATA` and `LLVM_COV` cache variables to absolute tool paths if needed. \ No newline at end of file diff --git a/Testing/Temporary/CTestCostData.txt b/Testing/Temporary/CTestCostData.txt new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/Testing/Temporary/CTestCostData.txt @@ -0,0 +1 @@ +--- diff --git a/build_tests/Testing/Temporary/CTestCostData.txt b/build_tests/Testing/Temporary/CTestCostData.txt new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/build_tests/Testing/Temporary/CTestCostData.txt @@ -0,0 +1 @@ +--- diff --git a/build_thinkpad.bat b/build_thinkpad.bat new file mode 100644 index 0000000..cae737d --- /dev/null +++ b/build_thinkpad.bat @@ -0,0 +1,128 @@ +@echo off +setlocal ENABLEDELAYEDEXPANSION +echo Building PrivateBin API C++ DLL... + +REM Proaktiv: VS-Entwicklungsumgebung initialisieren, wenn bekannt +set "VSINSTALL_HINT=C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools" +set "VSDEVCMD_HINT=%VSINSTALL_HINT%\Common7\Tools\VsDevCmd.bat" +if not exist "%VSDEVCMD_HINT%" ( + set "VSINSTALL_HINT=C:\Program Files\Microsoft Visual Studio\2022\Community" + set "VSDEVCMD_HINT=%VSINSTALL_HINT%\Common7\Tools\VsDevCmd.bat" +) +if exist "%VSDEVCMD_HINT%" ( + echo Initializing MSVC environment from "%VSDEVCMD_HINT%" for x64... + call "%VSDEVCMD_HINT%" -arch=x64 >nul + set "VCPKG_VISUAL_STUDIO_PATH=%VSINSTALL_HINT%" +) + +REM Ensure MSVC environment is loaded (cl.exe). Try via vswhere if not present. +where cl >nul 2>&1 +if errorlevel 1 ( + REM Ensure vswhere is on PATH (both PF and PF(x86)) + set "PATH=%ProgramFiles%\Microsoft Visual Studio\Installer;%ProgramFiles(x86)%\Microsoft Visual Studio\Installer;%PATH%" + REM First try well-known VsDevCmd from BuildTools/Community to avoid vswhere dependency + set "VSINSTALL=C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools" + set "VSDEVCMD_CANDIDATE=%VSINSTALL%\Common7\Tools\VsDevCmd.bat" + if not exist "%VSDEVCMD_CANDIDATE%" ( + set "VSINSTALL=C:\Program Files\Microsoft Visual Studio\2022\Community" + set "VSDEVCMD_CANDIDATE=%VSINSTALL%\Common7\Tools\VsDevCmd.bat" + ) + if exist "%VSDEVCMD_CANDIDATE%" ( + echo Initializing MSVC environment from "%VSDEVCMD_CANDIDATE%" for x64... + call "%VSDEVCMD_CANDIDATE%" -arch=x64 >nul + REM Provide VS path hint for vcpkg + set "VCPKG_VISUAL_STUDIO_PATH=%VSINSTALL%" + REM Re-check if cl is now available; if so, skip vswhere path + where cl >nul 2>&1 + if not errorlevel 1 goto :after_vs_env + ) + + set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" + if not exist "%VSWHERE%" ( + set "VSWHERE=%ProgramFiles%\Microsoft Visual Studio\Installer\vswhere.exe" + ) + if not exist "%VSWHERE%" ( + set "VSWHERE=c:\tools\vswhere.exe" + ) + if exist "%VSWHERE%" ( + for /f "usebackq tokens=*" %%i in (`"%VSWHERE%" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VSINSTALL=%%i" + if defined VSINSTALL ( + set "VSDEVCMD=%VSINSTALL%\Common7\Tools\VsDevCmd.bat" + if exist "%VSDEVCMD%" ( + echo Initializing MSVC environment from "%VSDEVCMD%" for x64... + call "%VSDEVCMD%" -arch=x64 >nul + set "VCPKG_VISUAL_STUDIO_PATH=%VSINSTALL%" + ) else ( + echo VsDevCmd.bat not found under "%VSINSTALL%". cl.exe may be unavailable. + ) + ) else ( + echo Visual Studio with C++ tools not found. Install "Desktop development with C++" workload. + ) + ) else ( + echo vswhere.exe not found. Consider installing Visual Studio 2022 with C++ tools. + ) +) + +:after_vs_env + +REM Detect or bootstrap vcpkg +if not defined VCPKG_ROOT ( + set "VCPKG_ROOT=%USERPROFILE%\vcpkg" +) + +if not exist "%VCPKG_ROOT%\scripts\buildsystems\vcpkg.cmake" ( + echo vcpkg not found at "%VCPKG_ROOT%". Cloning and bootstrapping... + if not exist "%VCPKG_ROOT%" ( + git clone https://github.com/microsoft/vcpkg.git "%VCPKG_ROOT%" + if errorlevel 1 ( + echo Failed to clone vcpkg + exit /b 1 + ) + ) + call "%VCPKG_ROOT%\bootstrap-vcpkg.bat" + if errorlevel 1 ( + echo Failed to bootstrap vcpkg + exit /b 1 + ) +) + +REM Ensure vcpkg manifest/config has a baseline; create vcpkg-configuration.json if missing +REM Baseline wird außerhalb des Skripts gesetzt (vcpkg x-update-baseline) + +REM Reset build directory to avoid stale CMake cache (e.g., CMAKE_GENERATOR_INSTANCE) +if exist "build" rd /s /q build +mkdir build +pushd build + +REM Generate build files with CMake and vcpkg (manifest mode with auto-baseline) +set "VCPKG_DEFAULT_TRIPLET=x64-windows" +REM Ensure no stale generator instance is leaking from environment +set "CMAKE_GENERATOR_INSTANCE=" + +REM Ensure vcpkg manifest has a builtin-baseline (adds if missing) +if exist "vcpkg.json" ( + "%VCPKG_ROOT%\vcpkg.exe" x-update-baseline --add-initial-baseline 1>nul 2>nul +) + +REM Use user vcpkg toolchain +set "TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake" +cmake .. -G "Visual Studio 17 2022" -A x64 -DCMAKE_TOOLCHAIN_FILE="%TOOLCHAIN_FILE%" +if errorlevel 1 ( + echo CMake configuration failed + popd + exit /b 1 +) + +REM Build the project +cmake --build . --config Release +set BUILD_ERROR=%ERRORLEVEL% + +if %BUILD_ERROR% EQU 0 ( + echo Build completed successfully! +) else ( + echo Build failed with error level %BUILD_ERROR% +) + +popd +exit /b %BUILD_ERROR% + diff --git a/src/http_client.cpp b/src/http_client.cpp index 38dd78b..0a1de07 100644 --- a/src/http_client.cpp +++ b/src/http_client.cpp @@ -1,11 +1,30 @@ #include "http_client.h" #include #include +#include #ifdef WINDOWS #include #include #pragma comment(lib, "winhttp.lib") +static std::string last_winhttp_error(const char* where) { + DWORD err = GetLastError(); + LPVOID lpMsgBuf = nullptr; + FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + err, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPSTR)&lpMsgBuf, + 0, NULL); + std::ostringstream os; + os << "[WinHTTP] " << where << " failed, error=" << err; + if (lpMsgBuf) { + os << ": " << (char*)lpMsgBuf; + LocalFree(lpMsgBuf); + } + return os.str(); +} static std::wstring utf8_to_wide(const std::string& s) { if (s.empty()) return std::wstring(); @@ -56,14 +75,28 @@ bool HttpClient::get(const std::string& url, std::string& response) { WINHTTP_NO_PROXY_BYPASS, 0); if (!hSession) { + std::cerr << last_winhttp_error("WinHttpOpen(GET)") << std::endl; return false; } + // Force modern TLS versions to avoid handshake failures on some hosts + DWORD protocols = WINHTTP_FLAG_SECURE_PROTOCOL_TLS1 | WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1 | + WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 | 0x00002000 /* TLS1_3 if available */; + WinHttpSetOption(hSession, WINHTTP_OPTION_SECURE_PROTOCOLS, &protocols, sizeof(protocols)); + // Disable HTTP/2 if it causes issues + #ifdef WINHTTP_DISABLE_FEATURE_HTTP2 + DWORD features = WINHTTP_DISABLE_FEATURE_HTTP2; + WinHttpSetOption(hSession, WINHTTP_OPTION_DISABLE_FEATURE, &features, sizeof(features)); + #endif - // Specify an HTTP server - HINTERNET hConnect = WinHttpConnect(hSession, urlComp.lpszHostName, - urlComp.nPort, 0); + // Specify an HTTP server (host must be null-terminated; urlComp provides length) + std::wstring host = (urlComp.lpszHostName && urlComp.dwHostNameLength > 0) + ? std::wstring(urlComp.lpszHostName, urlComp.dwHostNameLength) + : std::wstring(); + INTERNET_PORT port = urlComp.nPort ? urlComp.nPort : ((urlComp.nScheme == INTERNET_SCHEME_HTTPS) ? INTERNET_DEFAULT_HTTPS_PORT : INTERNET_DEFAULT_HTTP_PORT); + HINTERNET hConnect = WinHttpConnect(hSession, host.c_str(), port, 0); if (!hConnect) { + std::cerr << last_winhttp_error("WinHttpConnect(GET)") << std::endl; WinHttpCloseHandle(hSession); return false; } @@ -86,7 +119,9 @@ bool HttpClient::get(const std::string& url, std::string& response) { WINHTTP_FLAG_SECURE : 0); // Set headers per API requirement LPCWSTR headers = L"X-Requested-With: JSONHttpRequest\r\nAccept: application/json"; - WinHttpAddRequestHeaders(hRequest, headers, -1L, WINHTTP_ADDREQ_FLAG_ADD); + if (!WinHttpAddRequestHeaders(hRequest, headers, -1L, WINHTTP_ADDREQ_FLAG_ADD)) { + std::cerr << last_winhttp_error("WinHttpAddRequestHeaders(GET)") << std::endl; + } if (!hRequest) { WinHttpCloseHandle(hConnect); @@ -99,6 +134,7 @@ bool HttpClient::get(const std::string& url, std::string& response) { WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0)) { + std::cerr << last_winhttp_error("WinHttpSendRequest(GET)") << std::endl; WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); @@ -107,6 +143,7 @@ bool HttpClient::get(const std::string& url, std::string& response) { // End the request if (!WinHttpReceiveResponse(hRequest, NULL)) { + std::cerr << last_winhttp_error("WinHttpReceiveResponse(GET)") << std::endl; WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); @@ -122,6 +159,7 @@ bool HttpClient::get(const std::string& url, std::string& response) { // Check for available data dwSize = 0; if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) { + std::cerr << last_winhttp_error("WinHttpQueryDataAvailable(GET)") << std::endl; WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); @@ -140,6 +178,7 @@ bool HttpClient::get(const std::string& url, std::string& response) { // Read the data ZeroMemory(pszOutBuffer, dwSize + 1); if (!WinHttpReadData(hRequest, (LPVOID)pszOutBuffer, dwSize, &dwDownloaded)) { + std::cerr << last_winhttp_error("WinHttpReadData(GET)") << std::endl; delete[] pszOutBuffer; WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); @@ -189,14 +228,26 @@ bool HttpClient::post(const std::string& url, const std::string& data, std::stri WINHTTP_NO_PROXY_BYPASS, 0); if (!hSession) { + std::cerr << last_winhttp_error("WinHttpOpen(POST)") << std::endl; return false; } + DWORD protocols2 = WINHTTP_FLAG_SECURE_PROTOCOL_TLS1 | WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1 | + WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 | 0x00002000; + WinHttpSetOption(hSession, WINHTTP_OPTION_SECURE_PROTOCOLS, &protocols2, sizeof(protocols2)); + #ifdef WINHTTP_DISABLE_FEATURE_HTTP2 + DWORD features2 = WINHTTP_DISABLE_FEATURE_HTTP2; + WinHttpSetOption(hSession, WINHTTP_OPTION_DISABLE_FEATURE, &features2, sizeof(features2)); + #endif - // Specify an HTTP server - HINTERNET hConnect = WinHttpConnect(hSession, urlComp.lpszHostName, - urlComp.nPort, 0); + // Specify an HTTP server (ensure host is null-terminated) + std::wstring host2 = (urlComp.lpszHostName && urlComp.dwHostNameLength > 0) + ? std::wstring(urlComp.lpszHostName, urlComp.dwHostNameLength) + : std::wstring(); + INTERNET_PORT port2 = urlComp.nPort ? urlComp.nPort : ((urlComp.nScheme == INTERNET_SCHEME_HTTPS) ? INTERNET_DEFAULT_HTTPS_PORT : INTERNET_DEFAULT_HTTP_PORT); + HINTERNET hConnect = WinHttpConnect(hSession, host2.c_str(), port2, 0); if (!hConnect) { + std::cerr << last_winhttp_error("WinHttpConnect(POST)") << std::endl; WinHttpCloseHandle(hSession); return false; } @@ -211,6 +262,7 @@ bool HttpClient::post(const std::string& url, const std::string& data, std::stri WINHTTP_FLAG_SECURE : 0); if (!hRequest) { + std::cerr << "[WinHTTP] WinHttpOpenRequest(POST) failed" << std::endl; WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return false; @@ -224,6 +276,7 @@ bool HttpClient::post(const std::string& url, const std::string& data, std::stri WINHTTP_ADDREQ_FLAG_ADD); if (!bResults) { + std::cerr << last_winhttp_error("WinHttpAddRequestHeaders(POST)") << std::endl; WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); @@ -236,6 +289,7 @@ bool HttpClient::post(const std::string& url, const std::string& data, std::stri WINHTTP_NO_ADDITIONAL_HEADERS, 0, (LPVOID)data.c_str(), (DWORD)data.length(), (DWORD)data.length(), 0)) { + std::cerr << last_winhttp_error("WinHttpSendRequest(POST)") << std::endl; WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); @@ -244,6 +298,7 @@ bool HttpClient::post(const std::string& url, const std::string& data, std::stri // End the request if (!WinHttpReceiveResponse(hRequest, NULL)) { + std::cerr << last_winhttp_error("WinHttpReceiveResponse(POST)") << std::endl; WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); @@ -259,6 +314,7 @@ bool HttpClient::post(const std::string& url, const std::string& data, std::stri // Check for available data dwSize = 0; if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) { + std::cerr << last_winhttp_error("WinHttpQueryDataAvailable(POST)") << std::endl; WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); @@ -277,6 +333,7 @@ bool HttpClient::post(const std::string& url, const std::string& data, std::stri // Read the data ZeroMemory(pszOutBuffer, dwSize + 1); if (!WinHttpReadData(hRequest, (LPVOID)pszOutBuffer, dwSize, &dwDownloaded)) { + std::cerr << last_winhttp_error("WinHttpReadData(POST)") << std::endl; delete[] pszOutBuffer; WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); @@ -326,14 +383,26 @@ bool HttpClient::delete_req(const std::string& url, const std::string& data, std WINHTTP_NO_PROXY_BYPASS, 0); if (!hSession) { + std::cerr << last_winhttp_error("WinHttpOpen(DEL)") << std::endl; return false; } + DWORD protocols3 = WINHTTP_FLAG_SECURE_PROTOCOL_TLS1 | WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1 | + WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 | 0x00002000; + WinHttpSetOption(hSession, WINHTTP_OPTION_SECURE_PROTOCOLS, &protocols3, sizeof(protocols3)); + #ifdef WINHTTP_DISABLE_FEATURE_HTTP2 + DWORD features3 = WINHTTP_DISABLE_FEATURE_HTTP2; + WinHttpSetOption(hSession, WINHTTP_OPTION_DISABLE_FEATURE, &features3, sizeof(features3)); + #endif - // Specify an HTTP server - HINTERNET hConnect = WinHttpConnect(hSession, urlComp.lpszHostName, - urlComp.nPort, 0); + // Specify an HTTP server (ensure host is null-terminated) + std::wstring host3 = (urlComp.lpszHostName && urlComp.dwHostNameLength > 0) + ? std::wstring(urlComp.lpszHostName, urlComp.dwHostNameLength) + : std::wstring(); + INTERNET_PORT port3 = urlComp.nPort ? urlComp.nPort : ((urlComp.nScheme == INTERNET_SCHEME_HTTPS) ? INTERNET_DEFAULT_HTTPS_PORT : INTERNET_DEFAULT_HTTP_PORT); + HINTERNET hConnect = WinHttpConnect(hSession, host3.c_str(), port3, 0); if (!hConnect) { + std::cerr << last_winhttp_error("WinHttpConnect(DEL)") << std::endl; WinHttpCloseHandle(hSession); return false; } @@ -348,6 +417,7 @@ bool HttpClient::delete_req(const std::string& url, const std::string& data, std WINHTTP_FLAG_SECURE : 0); if (!hRequest) { + std::cerr << "[WinHTTP] WinHttpOpenRequest(DEL) failed" << std::endl; WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return false; @@ -361,6 +431,7 @@ bool HttpClient::delete_req(const std::string& url, const std::string& data, std WINHTTP_ADDREQ_FLAG_ADD); if (!bResults) { + std::cerr << last_winhttp_error("WinHttpAddRequestHeaders(DEL)") << std::endl; WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); @@ -372,6 +443,7 @@ bool HttpClient::delete_req(const std::string& url, const std::string& data, std WINHTTP_NO_ADDITIONAL_HEADERS, 0, (LPVOID)data.c_str(), (DWORD)data.length(), (DWORD)data.length(), 0)) { + std::cerr << last_winhttp_error("WinHttpSendRequest(DEL)") << std::endl; WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); @@ -380,6 +452,7 @@ bool HttpClient::delete_req(const std::string& url, const std::string& data, std // End the request if (!WinHttpReceiveResponse(hRequest, NULL)) { + std::cerr << last_winhttp_error("WinHttpReceiveResponse(DEL)") << std::endl; WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); @@ -395,6 +468,7 @@ bool HttpClient::delete_req(const std::string& url, const std::string& data, std // Check for available data dwSize = 0; if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) { + std::cerr << last_winhttp_error("WinHttpQueryDataAvailable(DEL)") << std::endl; WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); @@ -413,6 +487,7 @@ bool HttpClient::delete_req(const std::string& url, const std::string& data, std // Read the data ZeroMemory(pszOutBuffer, dwSize + 1); if (!WinHttpReadData(hRequest, (LPVOID)pszOutBuffer, dwSize, &dwDownloaded)) { + std::cerr << last_winhttp_error("WinHttpReadData(DEL)") << std::endl; delete[] pszOutBuffer; WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); diff --git a/src/json_parser.cpp b/src/json_parser.cpp index 62d9fa4..e5ce5c3 100644 --- a/src/json_parser.cpp +++ b/src/json_parser.cpp @@ -105,8 +105,18 @@ bool JsonParser::parse_paste_json(const json& json_data, } } - // Extract expiration - expiration = json_data.at("meta").at("expire"); + // Extract expiration; servers may return either meta.expire (string) + // or meta.time_to_live (integer seconds). Both are optional for our use. + try { + expiration = json_data.at("meta").at("expire"); + } catch (...) { + try { + auto ttl = json_data.at("meta").at("time_to_live").get(); + expiration = std::to_string(ttl); + } catch (...) { + expiration.clear(); + } + } return true; } catch (...) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 605b4dc..3c8ddc3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,23 +3,26 @@ project(PrivateBinAPITests) set(CMAKE_CXX_STANDARD 17) -# Find the privatebinapi library -find_library(PRIVATEBINAPI_LIB privatebinapi - PATHS ${CMAKE_CURRENT_SOURCE_DIR}/../build) - -# If not found, build it as part of the project -if(NOT PRIVATEBINAPI_LIB) - add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/.. ${CMAKE_CURRENT_BINARY_DIR}/privatebinapi) - set(PRIVATEBINAPI_LIB privatebinapi) -endif() - # Create test executable add_executable(test_basic test_basic.cpp) -# Link with the privatebinapi library -target_link_libraries(test_basic ${PRIVATEBINAPI_LIB}) +# Link with the already-defined privatebinapi target from the root project +target_link_libraries(test_basic PRIVATE privatebinapi) + +# Ensure the DLL is available next to the test executable on Windows +if(WIN32) + add_dependencies(test_basic privatebinapi) + add_custom_command(TARGET test_basic POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $ + ) +endif() # Include directories target_include_directories(test_basic PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include -) \ No newline at end of file +) + +# Register the test with CTest +add_test(NAME test_basic COMMAND test_basic) \ No newline at end of file diff --git a/tests/test_basic.cpp b/tests/test_basic.cpp index fe4c17d..2503390 100644 --- a/tests/test_basic.cpp +++ b/tests/test_basic.cpp @@ -1,15 +1,90 @@ +// Network integration test hitting a live PrivateBin instance (optional) +// Server under test: https://privatebin.medisoftware.org/ +// Enable by setting environment variable PRIVATEBIN_IT=1 + #include "privatebinapi.h" #include +#include #include +#include + +static bool extract_paste_id_and_key(const std::string& full_url, std::string& paste_id, std::string& key) { + // Expected format: https://host/path?PASTEID#BASE58_KEY + const std::size_t qpos = full_url.find('?'); + const std::size_t hpos = full_url.find('#'); + if (qpos == std::string::npos || hpos == std::string::npos || hpos <= qpos + 1) { + return false; + } + paste_id = full_url.substr(qpos + 1, hpos - (qpos + 1)); + key = full_url.substr(hpos + 1); + return !paste_id.empty() && !key.empty(); +} int main() { - // Test Base58 encoding/decoding - std::cout << "Testing Base58 encoding/decoding..." << std::endl; - - // This is just a basic test to verify the API compiles and links - // In a real test, we would test actual functionality - - std::cout << "API test completed successfully!" << std::endl; - + const char* it = std::getenv("PRIVATEBIN_IT"); + if (!it || std::string(it) == "0") { + std::cout << "[test] PRIVATEBIN_IT not set; skipping integration test." << std::endl; + return 0; // treat as success when integration testing is disabled + } + const char* server = "https://privatebin.medisoftware.org/"; + const char* content = "Integration test from lib-privatebin"; + const char* password = nullptr; // no password + const char* expiration = "5min"; // short-lived + const char* format = "plaintext"; + const int burn_after_reading = 0; + const int open_discussion = 0; + + char* paste_url = nullptr; + char* delete_token = nullptr; + + std::cout << "[test] create_paste..." << std::endl; + int rc = create_paste(server, content, password, expiration, format, + burn_after_reading, open_discussion, &paste_url, &delete_token); + if (rc != 0 || paste_url == nullptr || delete_token == nullptr) { + std::cerr << "[test][ERROR] create_paste failed, rc=" << rc << std::endl; + if (paste_url) free_string(paste_url); + if (delete_token) free_string(delete_token); + return 1; + } + + std::string full_url = paste_url; + std::string paste_id; + std::string key; + const bool ok_parse = extract_paste_id_and_key(full_url, paste_id, key); + if (!ok_parse) { + std::cerr << "[test][ERROR] failed to parse paste id/key from URL: " << full_url << std::endl; + free_string(paste_url); + free_string(delete_token); + return 1; + } + + std::cout << "[test] get_paste... id=" << paste_id << std::endl; + char* fetched = nullptr; + rc = get_paste(server, paste_id.c_str(), key.c_str(), &fetched); + if (rc != 0 || fetched == nullptr) { + std::cerr << "[test][ERROR] get_paste failed, rc=" << rc << std::endl; + free_string(paste_url); + free_string(delete_token); + return 1; + } + std::string fetched_str = fetched; + assert(fetched_str == content && "fetched content mismatch"); + + std::cout << "[test] delete_paste..." << std::endl; + rc = delete_paste(server, paste_id.c_str(), delete_token); + if (rc != 0) { + std::cerr << "[test][ERROR] delete_paste failed, rc=" << rc << std::endl; + free_string(paste_url); + free_string(delete_token); + free_string(fetched); + return 1; + } + + // cleanup + free_string(paste_url); + free_string(delete_token); + free_string(fetched); + + std::cout << "[test] all API functions passed." << std::endl; return 0; } \ No newline at end of file diff --git a/vcpkg.json b/vcpkg.json index 2607a79..3a06867 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1 +1,9 @@ -{"name": "privatebin-api", "version-string": "1.0.0", "dependencies": ["cryptopp", "nlohmann-json"]} +{ + "name": "privatebin-api", + "version-string": "1.0.0", + "dependencies": [ + "cryptopp", + "nlohmann-json" + ], + "builtin-baseline": "120deac3062162151622ca4860575a33844ba10b" +}