diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f748ee3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,290 @@ +name: release + +on: + push: + tags: + - "v*.*.*" + +permissions: + contents: write + +jobs: + build-release: + name: build (${{ matrix.vixos }} / ${{ matrix.arch }}) + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-22.04 + vixos: linux + arch: x86_64 + + # ✅ Linux aarch64 via cross-compile on ubuntu-22.04 + - os: ubuntu-22.04 + vixos: linux + arch: aarch64 + + - os: macos-13 + vixos: macos + arch: x86_64 + - os: macos-14 + vixos: macos + arch: aarch64 + + - os: windows-2022 + vixos: windows + arch: x86_64 + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup CMake + uses: lukka/get-cmake@latest + + # ------------------------- + # Linux aarch64 toolchain (cross) + # ------------------------- + - name: Setup Linux aarch64 toolchain + if: runner.os == 'Linux' && matrix.arch == 'aarch64' + shell: bash + run: | + set -euxo pipefail + sudo apt-get update + sudo apt-get install -y \ + gcc-aarch64-linux-gnu g++-aarch64-linux-gnu \ + pkg-config cmake ninja-build + + # Optional: if you link system libs, uncomment: + # sudo dpkg --add-architecture arm64 + # sudo apt-get update + # sudo apt-get install -y libssl-dev:arm64 zlib1g-dev:arm64 libsqlite3-dev:arm64 + + cat > toolchain-aarch64.cmake <<'EOF' + set(CMAKE_SYSTEM_NAME Linux) + set(CMAKE_SYSTEM_PROCESSOR aarch64) + + set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc) + set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++) + + # Where cross libs/headers live + set(CMAKE_FIND_ROOT_PATH /usr/aarch64-linux-gnu) + + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + EOF + + # ------------------------- + # Configure + # ------------------------- + - name: Configure (Unix) + if: runner.os != 'Windows' + shell: bash + run: | + set -euxo pipefail + + if [ "${{ runner.os }}" = "Linux" ] && [ "${{ matrix.arch }}" = "aarch64" ]; then + cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DVIX_ENABLE_INSTALL=OFF \ + -DCMAKE_TOOLCHAIN_FILE="${GITHUB_WORKSPACE}/toolchain-aarch64.cmake" + else + cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DVIX_ENABLE_INSTALL=OFF + fi + + - name: Build (Unix) + if: runner.os != 'Windows' + shell: bash + run: | + set -euxo pipefail + cmake --build build -j + + - name: Configure (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + cmake -S . -B build -DVIX_ENABLE_INSTALL=OFF + + - name: Build (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + cmake --build build --config Release + + # ------------------------- + # Package artifact + # ------------------------- + - name: Package (Unix) + if: runner.os != 'Windows' + shell: bash + run: | + set -euxo pipefail + + BIN="" + for p in "build/vix" "build/bin/vix" "build/Release/vix"; do + if [ -f "$p" ]; then BIN="$p"; break; fi + done + + if [ -z "$BIN" ]; then + echo "Expected vix binary not found. Listing build/ ..." >&2 + ls -la build || true + find build -maxdepth 4 -type f -name vix -print || true + exit 1 + fi + + ASSET="vix-${{ matrix.vixos }}-${{ matrix.arch }}.tar.gz" + mkdir -p dist + cp "$BIN" dist/vix + chmod +x dist/vix + tar -C dist -czf "dist/$ASSET" vix + rm -f dist/vix + + - name: Package (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $candidates = @( + "build\vix.exe", + "build\Release\vix.exe", + "build\bin\vix.exe" + ) + + $bin = $null + foreach ($p in $candidates) { + if (Test-Path $p) { $bin = $p; break } + } + + if (!$bin) { + Write-Host "Expected vix.exe not found in build outputs" + if (Test-Path "build") { Get-ChildItem -Recurse build | Select-Object FullName } + exit 1 + } + + New-Item -ItemType Directory -Force -Path dist | Out-Null + Copy-Item $bin dist\vix.exe + $asset = "vix-windows-${{ matrix.arch }}.zip" + Compress-Archive -Path dist\vix.exe -DestinationPath "dist\$asset" -Force + Remove-Item dist\vix.exe -Force + + # ------------------------- + # minisign + sha256 + # ------------------------- + - name: Install minisign (Unix) + if: runner.os != 'Windows' + shell: bash + run: | + set -euxo pipefail + if command -v minisign >/dev/null 2>&1; then exit 0; fi + if [ "${{ runner.os }}" = "macOS" ]; then + brew update + brew install minisign + else + sudo apt-get update + sudo apt-get install -y minisign + fi + + - name: Install minisign (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + choco install minisign -y + refreshenv + if (-not (Get-Command minisign -ErrorAction SilentlyContinue)) { + throw "minisign not found in PATH after installation" + } + + - name: Sign + SHA256 (Unix) + if: runner.os != 'Windows' + shell: bash + env: + MINISIGN_PRIVATE_KEY: ${{ secrets.MINISIGN_PRIVATE_KEY }} + MINISIGN_PASSWORD: ${{ secrets.MINISIGN_PASSWORD }} + run: | + set -euxo pipefail + + ASSET_PATH="$(ls dist/vix-${{ matrix.vixos }}-${{ matrix.arch }}.* | head -n1)" + ASSET_NAME="$(basename "$ASSET_PATH")" + + cd dist + + printf "%s" "$MINISIGN_PRIVATE_KEY" > minisign.key + + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$ASSET_NAME" > "${ASSET_NAME}.sha256" + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$ASSET_NAME" > "${ASSET_NAME}.sha256" + else + echo "No sha256 tool found (sha256sum/shasum)" >&2 + exit 1 + fi + + echo "$MINISIGN_PASSWORD" | minisign -S -s minisign.key -m "$ASSET_NAME" -x "${ASSET_NAME}.minisig" + rm -f minisign.key + + - name: Sign + SHA256 (Windows) + if: runner.os == 'Windows' + shell: pwsh + env: + MINISIGN_PRIVATE_KEY: ${{ secrets.MINISIGN_PRIVATE_KEY }} + MINISIGN_PASSWORD: ${{ secrets.MINISIGN_PASSWORD }} + run: | + $asset = Get-ChildItem dist\* | Where-Object { $_.Name -eq "vix-windows-${{ matrix.arch }}.zip" } | Select-Object -First 1 + if (!$asset) { throw "asset not found" } + + $hash = (Get-FileHash -Algorithm SHA256 $asset.FullName).Hash.ToLower() + "$hash $($asset.Name)" | Out-File -Encoding ASCII "dist\$($asset.Name).sha256" + + $keyPath = "dist\minisign.key" + Set-Content -NoNewline -Encoding ASCII $keyPath $env:MINISIGN_PRIVATE_KEY + + $pwd = $env:MINISIGN_PASSWORD + $pwd | minisign -S -s $keyPath -m $asset.FullName -x "dist\$($asset.Name).minisig" + + Remove-Item $keyPath -Force + + # ------------------------- + # Upload artifacts for publish job + # ------------------------- + - name: Upload dist + uses: actions/upload-artifact@v4 + with: + name: dist-${{ matrix.vixos }}-${{ matrix.arch }} + path: dist/* + + publish: + name: publish (github release) + needs: build-release + runs-on: ubuntu-22.04 + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: dist-all + + - name: Flatten (no collisions) + shell: bash + run: | + set -euxo pipefail + mkdir -p dist + find dist-all -maxdepth 2 -type f -print0 | while IFS= read -r -d '' f; do + base="$(basename "$f")" + if [ -f "dist/$base" ]; then + echo "Collision while flattening: $base" >&2 + exit 1 + fi + cp -f "$f" "dist/$base" + done + ls -la dist + + - name: Create GitHub Release + upload + uses: softprops/action-gh-release@v2 + with: + files: dist/* diff --git a/.gitignore b/.gitignore index fc072a4..0edd615 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,7 @@ db.sql revoir.md main.cpp .vix-scripts +create-labels.sh +*.key +*.minisig +*.sha256 diff --git a/CMakeLists.txt b/CMakeLists.txt index e4180e8..70f402c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,8 +13,6 @@ cmake_minimum_required(VERSION 3.20) # cmake --build build -j # ==================================================================== -cmake_minimum_required(VERSION 3.20) - # Resolve umbrella version from git (preferred) set(_VIX_VERSION_FALLBACK "0.0.0") @@ -75,9 +73,8 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Copy compile_commands.json to repo root (tooling QoL) add_custom_target(copy-compile-commands ALL COMMAND ${CMAKE_COMMAND} -E copy_if_different - ${CMAKE_BINARY_DIR}/compile_commands.json - ${CMAKE_SOURCE_DIR}/compile_commands.json - BYPRODUCTS ${CMAKE_SOURCE_DIR}/compile_commands.json + "${CMAKE_BINARY_DIR}/compile_commands.json" + "${CMAKE_SOURCE_DIR}/compile_commands.json" COMMENT "Copy compile_commands.json to project root" ) @@ -113,7 +110,8 @@ option(VIX_DB_USE_MYSQL "Enable MySQL backend in vix_db" ON) option(VIX_DB_USE_SQLITE "Enable SQLite backend in vix_db" OFF) option(VIX_DB_USE_POSTGRES "Enable PostgreSQL backend in vix_db" OFF) option(VIX_DB_USE_REDIS "Enable Redis backend in vix_db" OFF) - +option(VIX_ENABLE_P2P "Build Vix P2P module" ON) +option(VIX_ENABLE_CACHE "Build Vix Cache module" ON) # ---------------------------------------------------- # Tooling / Static analysis @@ -343,6 +341,83 @@ else() message(FATAL_ERROR "Missing 'modules/core'. Run: git submodule update --init --recursive") endif() +# --- Net (required by P2P) --- +set(VIX_HAS_NET OFF) +if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/net/CMakeLists.txt") + message(STATUS "Adding 'modules/net'...") + add_subdirectory(modules/net net_build) + + if (TARGET vix::net OR TARGET vix_net) + set(VIX_HAS_NET ON) + if (TARGET vix_net AND NOT TARGET vix::net) + add_library(vix::net ALIAS vix_net) + endif() + else() + message(WARNING "Net module added but no vix::net target was exported.") + endif() +else() + message(STATUS "Net: not present (P2P will be disabled or will fetch standalone).") +endif() + +# --- Cache (optional, used by P2P) --- +set(VIX_HAS_CACHE OFF) +if (VIX_ENABLE_CACHE AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/cache/CMakeLists.txt") + message(STATUS "Adding 'modules/cache'...") + add_subdirectory(modules/cache cache_build) + + if (TARGET vix::cache OR TARGET vix_cache) + set(VIX_HAS_CACHE ON) + if (TARGET vix_cache AND NOT TARGET vix::cache) + add_library(vix::cache ALIAS vix_cache) + endif() + else() + message(WARNING "Cache module added but no vix::cache target was exported.") + endif() +else() + message(STATUS "Cache: disabled or not present.") +endif() + +# --- Sync (optional, used by P2P) --- +set(VIX_HAS_SYNC OFF) +if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/sync/CMakeLists.txt") + message(STATUS "Adding 'modules/sync'...") + add_subdirectory(modules/sync sync_build) + + if (TARGET vix::sync OR TARGET vix_sync) + set(VIX_HAS_SYNC ON) + if (TARGET vix_sync AND NOT TARGET vix::sync) + add_library(vix::sync ALIAS vix_sync) + endif() + endif() +else() + message(STATUS "Sync: not present.") +endif() + +# If net is missing, P2P must be disabled in umbrella (no auto-fetch here) +if (VIX_ENABLE_P2P AND NOT VIX_HAS_NET) + message(WARNING "P2P requested but Net module is missing -> disabling P2P in umbrella") + set(VIX_ENABLE_P2P OFF CACHE BOOL "Build Vix P2P module" FORCE) +endif() + +# --- P2P (optional) --- +set(VIX_HAS_P2P OFF) +if (VIX_ENABLE_P2P AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/p2p/CMakeLists.txt") + message(STATUS "Adding 'modules/p2p'...") + add_subdirectory(modules/p2p p2p_build) + + # Normalise: expose vix::p2p + if (TARGET vix::p2p OR TARGET vix_p2p) + set(VIX_HAS_P2P ON) + if (TARGET vix_p2p AND NOT TARGET vix::p2p) + add_library(vix::p2p ALIAS vix_p2p) + endif() + else() + message(WARNING "P2P module added but no vix::p2p target was exported.") + endif() +else() + message(STATUS "P2P: disabled or not present.") +endif() + # --- Middleware (optional) --- set(VIX_HAS_MIDDLEWARE OFF) if (VIX_ENABLE_MIDDLEWARE AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/middleware/CMakeLists.txt") @@ -418,7 +493,6 @@ endif() # --- CLI (optional) --- if (VIX_ENABLE_CLI AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/cli/CMakeLists.txt") message(STATUS "Adding 'modules/cli'...") - set(VIX_UMBRELLA_VERSION "${PROJECT_VERSION}" CACHE STRING "Umbrella version" FORCE) add_subdirectory(modules/cli cli_build) else() message(STATUS "CLI: disabled or not present.") @@ -459,6 +533,22 @@ target_link_libraries(vix INTERFACE ${JSON_TARGET} ) +if (TARGET vix::net) + target_link_libraries(vix INTERFACE vix::net) +endif() + +if (TARGET vix::cache) + target_link_libraries(vix INTERFACE vix::cache) +endif() + +if (TARGET vix::sync) + target_link_libraries(vix INTERFACE vix::sync) +endif() + +if (TARGET vix::p2p) + target_link_libraries(vix INTERFACE vix::p2p) +endif() + if (TARGET vix::db) target_link_libraries(vix INTERFACE vix::db) endif() @@ -665,6 +755,21 @@ if (VIX_ENABLE_HTTP_COMPRESSION AND ZLIB_FOUND) set(VIX_WITH_ZLIB ON) endif() +set(VIX_WITH_MYSQL OFF) +if (VIX_HAS_DB AND VIX_DB_USE_MYSQL) + set(VIX_WITH_MYSQL ON) +endif() + +set(VIX_WITH_CACHE OFF) +if (VIX_HAS_CACHE) + set(VIX_WITH_CACHE ON) +endif() + +set(VIX_WITH_P2P OFF) +if (VIX_HAS_P2P) + set(VIX_WITH_P2P ON) +endif() + if (TARGET vix::websocket OR TARGET vix_websocket) set(VIX_WITH_SQLITE ON) endif() @@ -701,6 +806,26 @@ if (VIX_ENABLE_INSTALL) FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h") endif() + if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/net/include") + install(DIRECTORY modules/net/include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h") + endif() + + if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/cache/include") + install(DIRECTORY modules/cache/include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h") + endif() + + if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/sync/include") + install(DIRECTORY modules/sync/include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h") + endif() + + if (VIX_HAS_P2P AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/p2p/include") + install(DIRECTORY modules/p2p/include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h") + endif() + if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/middleware/include") install(DIRECTORY modules/middleware/include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h") @@ -727,11 +852,17 @@ if (VIX_ENABLE_INSTALL) ) set(VIX_HAS_ORM ${VIX_HAS_ORM}) + set(VIX_DB_WITH_MYSQL OFF) if (VIX_HAS_DB AND VIX_DB_USE_MYSQL) set(VIX_DB_WITH_MYSQL ON) endif() + set(VIX_HAS_CACHE ${VIX_HAS_CACHE}) + set(VIX_HAS_P2P ${VIX_HAS_P2P}) + set(VIX_WITH_CACHE ${VIX_WITH_CACHE}) + set(VIX_WITH_P2P ${VIX_WITH_P2P}) + configure_package_config_file( "${CMAKE_CURRENT_SOURCE_DIR}/cmake/VixConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/VixConfig.cmake" @@ -751,8 +882,8 @@ if (VIX_ENABLE_INSTALL) # Install CLI executable (not exported) if (TARGET vix_cli) install(TARGETS vix_cli - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} - RENAME vix) + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + ) message(STATUS "CLI install: installing target 'vix_cli' as 'vix'") else() message(STATUS "CLI install: no 'vix_cli' target detected — CLI not installed") @@ -769,6 +900,7 @@ message(STATUS "JSON backend : ${_VIX_JSON_BACKEND}") message(STATUS "WebSocket built : ${VIX_HAS_WEBSOCKET}") message(STATUS "ORM packaged : ${VIX_HAS_ORM}") message(STATUS "DB built : ${VIX_HAS_DB}") +message(STATUS "P2P built : ${VIX_HAS_P2P}") message(STATUS "Middleware built : ${VIX_HAS_MIDDLEWARE}") message(STATUS "Examples : ${VIX_BUILD_EXAMPLES}") message(STATUS "Tests : ${VIX_BUILD_TESTS}") @@ -776,6 +908,6 @@ message(STATUS "Sanitizers : ${VIX_ENABLE_SANITIZERS}") message(STATUS "Coverage : ${VIX_ENABLE_COVERAGE}") message(STATUS "LTO : ${VIX_ENABLE_LTO}") message(STATUS "Install/Export : ${VIX_ENABLE_INSTALL}") -message(STATUS "Flags exported : JSON=${VIX_WITH_JSON} BOOST_FS=${VIX_WITH_BOOST_FS} OPENSSL=${VIX_WITH_OPENSSL} SQLITE=${VIX_WITH_SQLITE} MYSQL=${VIX_WITH_MYSQL} HAS_ORM=${VIX_HAS_ORM}") +message(STATUS "Flags exported : JSON=${VIX_WITH_JSON} BOOST_FS=${VIX_WITH_BOOST_FS} OPENSSL=${VIX_WITH_OPENSSL} SQLITE=${VIX_WITH_SQLITE} MYSQL=${VIX_WITH_MYSQL} HAS_ORM=${VIX_HAS_ORM} CACHE=${VIX_WITH_CACHE} P2P=${VIX_WITH_P2P}") message(STATUS "------------------------------------------------------") diff --git a/cmake/VixConfig.cmake.in b/cmake/VixConfig.cmake.in index cf89c54..5ecd8ec 100644 --- a/cmake/VixConfig.cmake.in +++ b/cmake/VixConfig.cmake.in @@ -30,12 +30,6 @@ if (@VIX_WITH_SQLITE@) endif() endif() -# Optional (only if referenced by exported targets) -find_package(MySQLCppConn QUIET CONFIG) -if (NOT TARGET MySQLCppConn::MySQLCppConn) - add_library(MySQLCppConn::MySQLCppConn INTERFACE IMPORTED) -endif() - if (@VIX_WITH_ZLIB@) find_dependency(ZLIB QUIET) if (ZLIB_FOUND AND NOT TARGET ZLIB::ZLIB) @@ -48,6 +42,15 @@ if (@VIX_WITH_ZLIB@) endif() endif() +# ---- MySQL (MUST be resolved BEFORE loading exported targets, if required) ---- +set(VIX_DB_WITH_MYSQL @VIX_DB_WITH_MYSQL@) +if (VIX_DB_WITH_MYSQL) + find_package(MySQLCppConn CONFIG REQUIRED) + if (NOT TARGET MySQLCppConn::MySQLCppConn) + message(FATAL_ERROR "Vix was built with MySQL support but MySQLCppConn::MySQLCppConn was not found.") + endif() +endif() + # ---- load exported targets ---- include("${CMAKE_CURRENT_LIST_DIR}/VixTargets.cmake") @@ -56,3 +59,17 @@ set(VIX_HAS_ORM @VIX_HAS_ORM@) if (VIX_HAS_ORM AND NOT TARGET vix::orm) set(VIX_HAS_ORM OFF) endif() + +set(VIX_HAS_CACHE @VIX_HAS_CACHE@) +if (VIX_HAS_CACHE AND NOT TARGET vix::cache) + set(VIX_HAS_CACHE OFF) +endif() + +set(VIX_WITH_CACHE @VIX_WITH_CACHE@) + +set(VIX_HAS_P2P @VIX_HAS_P2P@) +if (VIX_HAS_P2P AND NOT TARGET vix::p2p) + set(VIX_HAS_P2P OFF) +endif() + +set(VIX_WITH_P2P @VIX_WITH_P2P@) diff --git a/examples/mega_middleware_routes.cpp b/examples/mega_middleware_routes.cpp new file mode 100644 index 0000000..8f52188 --- /dev/null +++ b/examples/mega_middleware_routes.cpp @@ -0,0 +1,711 @@ +/** + * + * @file examples/http_middleware/mega_middleware_routes.cpp + * @author Gaspard Kirira + * + * Copyright 2026, Gaspard Kirira. All rights reserved. + * https://github.com/vixcpp/vix + * Use of this source code is governed by a MIT license + * that can be found in the License file. + * + * Vix.cpp + * + * ---------------------------------------------------------------------------- + * GOAL + * ---- + * A single, big example file you can drop into: + * vix/examples/http_middleware/mega_middleware_routes.cpp + * + * It demonstrates: + * - App routes (GET/POST/etc.) + * - App-level middleware (App::use / prefix install) + * - Context-based middleware via adapt_ctx() + * - Legacy HttpMiddleware via adapt() + * - Middleware chaining + conditional gating (when/protect_prefix) + * - Security: CORS, Rate limit, CSRF, Security headers, IP filter + * - Parsers: JSON, Form, Multipart, Multipart Save + * - Auth: API key, JWT, RBAC (admin) + * - HTTP cache middleware (GET cache) + * - RequestState usage (store data across middlewares+handler) + * - Error patterns: early return, status+payload returnable, res.send/json + * + * NOTES + * ----- + * This file intentionally repeats patterns and includes many routes so you can + * screenshot it and say: "this is how you write simple routes + middlewares". + * + * You can start it with: + * vix run examples/http_middleware/mega_middleware_routes.cpp + * + * Then test quickly: + * curl -i http://127.0.0.1:8080/ + * curl -i http://127.0.0.1:8080/api/ping + * curl -i -H "x-api-key: dev_key_123" http://127.0.0.1:8080/api/secure/whoami + * curl -i http://127.0.0.1:8080/dev/trace + * curl -i -H "x-vix-cache: bypass" http://127.0.0.1:8080/api/cache/demo + * + */ + +// ----------------------------- includes -------------------------------------- +#include + +// If your tree exposes these headers exactly as in your snippets: +#include +#include +#include + +// Some projects place these in different paths; keep includes minimal. +// ---------------------------------------------------------------------------- + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace vix; +namespace J = vix::json; + +// ----------------------------- tiny helpers ---------------------------------- + +static std::string env_or(std::string_view key, std::string fallback) +{ + const char *v = std::getenv(std::string(key).c_str()); + if (!v || !*v) + return fallback; + return std::string(v); +} + +static long long now_ms_wall() +{ + using namespace std::chrono; + const auto t = time_point_cast(system_clock::now()).time_since_epoch().count(); + return static_cast(t); +} + +static J::kvs ok_msg(std::string_view msg) +{ + return J::obj({ + "ok", + true, + "message", + std::string(msg), + }); +} + +static J::kvs err_msg(std::string_view msg, int status = 400) +{ + return J::obj({ + "ok", + false, + "status", + (long long)status, + "error", + std::string(msg), + }); +} + +// ------------------------- RequestState demo types --------------------------- +// RequestState is type-based storage (std::any). We'll store: +// - RequestId +// - AuthInfo +// - ParsedBody indicator +// - Debug timings + +struct RequestId +{ + std::string value; +}; + +struct AuthInfo +{ + bool authed{false}; + std::string subject; + std::string role; +}; + +struct ParseInfo +{ + bool parsed_json{false}; + bool parsed_form{false}; + bool parsed_multipart{false}; +}; + +struct TimingInfo +{ + long long t0_ms{0}; + long long t1_after_global_mw{0}; + long long t2_after_api_mw{0}; +}; + +// ----------------------------- demo middleware -------------------------------- +// 1) A very small "request id" middleware (Context-based style). +// Stores a RequestId in RequestState and sets a header. +// This is meant to be adapted with adapt_ctx(). + +static vix::middleware::MiddlewareFn mw_request_id() +{ + return [](vix::middleware::Context &ctx, vix::middleware::Next next) + { + // Generate a simple id: - + RequestId rid; + rid.value = std::to_string(now_ms_wall()) + "-" + std::to_string((std::uintptr_t)(&ctx)); + + ctx.req().emplace_state(std::move(rid)); + ctx.res().header("x-request-id", ctx.req().state().value); + + next(); // continue + }; +} + +// 2) Global timer middleware (Context-based). Stores t0. +static vix::middleware::MiddlewareFn mw_timing_start() +{ + return [](vix::middleware::Context &ctx, vix::middleware::Next next) + { + TimingInfo t; + t.t0_ms = now_ms_wall(); + ctx.req().emplace_state(t); + next(); + }; +} + +// 3) After-middleware "mark" (Context-based). Updates timing. +static vix::middleware::MiddlewareFn mw_timing_mark_global() +{ + return [](vix::middleware::Context &ctx, vix::middleware::Next next) + { + next(); + + if (ctx.req().has_state_type()) + { + auto &t = ctx.req().state(); + t.t1_after_global_mw = now_ms_wall(); + } + }; +} + +// 4) A strict "require header" middleware (legacy HttpMiddleware style). +// - If header missing: 401 and DO NOT call next() +// - Otherwise: next() +static vix::middleware::HttpMiddleware mw_require_header(std::string header, std::string expected) +{ + return [header = std::move(header), expected = std::move(expected)]( + vix::Request &req, vix::Response &res, vix::middleware::Next next) mutable + { + const std::string got = req.header(header); + if (got.empty() || got != expected) + { + res.status(401).json(J::obj({ + "ok", + false, + "error", + "unauthorized", + "hint", + "Missing or invalid header", + "required_header", + header, + })); + return; + } + next(); + }; +} + +// 5) A tiny "fake auth" middleware (Context-based). +// If "x-user" is present, treat as authenticated. +// If "x-role" is present, use it. +// Stores AuthInfo in RequestState. +// (This is purely for example. Real auth should use jwt/api_key middlewares.) +static vix::middleware::MiddlewareFn mw_fake_auth() +{ + return [](vix::middleware::Context &ctx, vix::middleware::Next next) + { + AuthInfo info; + const std::string user = ctx.req().header("x-user"); + const std::string role = ctx.req().header("x-role"); + + if (!user.empty()) + { + info.authed = true; + info.subject = user; + info.role = role.empty() ? "user" : role; + } + + ctx.req().emplace_state(std::move(info)); + next(); + }; +} + +// 6) Require "admin" role from stored AuthInfo +static vix::middleware::MiddlewareFn mw_require_admin() +{ + return [](vix::middleware::Context &ctx, vix::middleware::Next next) + { + if (!ctx.req().has_state_type()) + { + ctx.res().status(401).json(J::obj({"ok", false, "error", "unauthorized"})); + return; + } + + const auto &a = ctx.req().state(); + if (!a.authed) + { + ctx.res().status(401).json(J::obj({"ok", false, "error", "unauthorized"})); + return; + } + + if (a.role != "admin") + { + ctx.res().status(403).json(J::obj({ + "ok", + false, + "error", + "forbidden", + "hint", + "admin role required", + })); + return; + } + + next(); + }; +} + +// 7) ParseInfo marker middleware (Context-based) for JSON/Form/Multipart. +// This is for showing how state can be set by parser middlewares. +static vix::middleware::MiddlewareFn mw_mark_parsed_json() +{ + return [](vix::middleware::Context &ctx, vix::middleware::Next next) + { + if (!ctx.req().has_state_type()) + ctx.req().emplace_state(ParseInfo{}); + + auto &p = ctx.req().state(); + p.parsed_json = true; + next(); + }; +} + +static vix::middleware::MiddlewareFn mw_mark_parsed_form() +{ + return [](vix::middleware::Context &ctx, vix::middleware::Next next) + { + if (!ctx.req().has_state_type()) + ctx.req().emplace_state(ParseInfo{}); + + auto &p = ctx.req().state(); + p.parsed_form = true; + next(); + }; +} + +static vix::middleware::MiddlewareFn mw_mark_parsed_multipart() +{ + return [](vix::middleware::Context &ctx, vix::middleware::Next next) + { + if (!ctx.req().has_state_type()) + ctx.req().emplace_state(ParseInfo{}); + + auto &p = ctx.req().state(); + p.parsed_multipart = true; + next(); + }; +} + +// ------------------------------ routes ---------------------------------------- +// We keep routes in functions so main() stays clean (your preference). + +static void register_public_routes(vix::App &app) +{ + // GET / + app.get("/", [](Request &, Response &res) + { res.json(J::obj({ + "message", + "Vix.cpp middleware mega example 👋", + "hint", + "Try /api/ping, /api/cache/demo, /api/secure/whoami, /dev/trace", + })); }); + + // GET /hello + app.get("/hello", [](Request &, Response &res) + { res.json(ok_msg("Hello, Vix!")); }); + + // GET /txt + app.get("/txt", [](Request &, Response &) + { return "Hello world"; }); + + // GET /mix (shows "res.send()" takes precedence and return payload is ignored) + app.get("/mix", [](Request &, Response &res) + { + res.status(201).send("Created"); + return J::obj({"ignored", true}); }); + + // GET /users/{id} -> 404 + app.get("/users/{id}", [](Request &, Response &res) + { res.status(404).json(err_msg("User not found", 404)); }); +} + +static void register_api_routes(vix::App &app) +{ + // GET /api/ping + app.get("/api/ping", [](Request &, Response &res) + { + const bool hasRid = res.has_header(boost::beast::http::field::unknown) + ? false + : true; + (void)hasRid; + res.json(J::obj({ + "ok", true, + "pong", true, + "ts_ms", (long long)now_ms_wall(), + })); }); + + // GET /api/who + app.get("/api/who", [](Request &req, Response &res) + { + // RequestState demo + std::string rid = req.has_state_type() ? req.state().value : ""; + + bool authed = false; + std::string subject = "anonymous"; + std::string role = "guest"; + + if (req.has_state_type()) + { + const auto &a = req.state(); + authed = a.authed; + if (a.authed) + { + subject = a.subject; + role = a.role; + } + } + + res.json(J::obj({ + "ok", true, + "request_id", rid, + "authed", authed, + "subject", subject, + "role", role, + })); }); + + // GET /api/cache/demo + // Hits/miss should appear via x-vix-cache-status + app.get("/api/cache/demo", [](Request &, Response &res) + { res.json(J::obj({ + "ok", + true, + "cache_demo", + true, + "value", + "same response can be cached", + "ts_ms", + (long long)now_ms_wall(), + })); }); + + // GET /api/cache/heavy + app.get("/api/cache/heavy", [](Request &, Response &res) + { + // Fake heavy payload + std::vector items; + items.reserve(64); + for (int i = 0; i < 64; ++i) + { + items.emplace_back(J::obj({ + "id", (long long)i, + "name", std::string("item-") + std::to_string(i), + "ts_ms", (long long)now_ms_wall(), + })); + } + + return J::obj({ + "ok", true, + "items", J::array(std::move(items)), + }); }); + + // POST /api/echo/json (expects JSON parser middleware) + app.post("/api/echo/json", [](Request &req, Response &res) + { + // If your JSON parser stores parsed json in state (store_in_state=true), + // you'd read it from RequestState. Since type name depends on parser impl, + // we just demonstrate raw body + safe fallback. + const std::string body = req.body(); + res.json(J::obj({ + "ok", true, + "kind", "json", + "raw_body", body, + })); }); + + // POST /api/echo/form (expects form parser) + app.post("/api/echo/form", [](Request &req, Response &res) + { res.json(J::obj({ + "ok", + true, + "kind", + "form", + "raw_body", + req.body(), + })); }); + + // POST /api/echo/multipart (expects multipart_save) + app.post("/api/echo/multipart", [](Request &, Response &res) + { res.json(J::obj({ + "ok", + true, + "kind", + "multipart", + "hint", + "multipart_save middleware should store files info in state", + })); }); + + // GET /api/secure/whoami (protected by API key + fake auth for demo) + app.get("/api/secure/whoami", [](Request &req, Response &res) + { + std::string rid = req.has_state_type() ? req.state().value : ""; + res.json(J::obj({ + "ok", true, + "secure", true, + "request_id", rid, + "message", "You passed API key middleware", + })); }); + + // GET /api/admin/stats (protected by fake admin role middleware) + app.get("/api/admin/stats", [](Request &req, Response &res) + { + long long t0 = 0; + long long t1 = 0; + long long t2 = 0; + + if (req.has_state_type()) + { + const auto &t = req.state(); + t0 = t.t0_ms; + t1 = t.t1_after_global_mw; + t2 = t.t2_after_api_mw; + } + + res.json(J::obj({ + "ok", true, + "admin", true, + "timing", J::obj({ + "t0_ms", t0, + "t1_after_global_mw_ms", t1, + "t2_after_api_mw_ms", t2, + }), + })); }); +} + +static void register_dev_routes(vix::App &app) +{ + // GET /dev/trace + app.get("/dev/trace", [](Request &req, Response &res) + { + std::string rid = req.has_state_type() ? req.state().value : ""; + + bool hasParse = req.has_state_type(); + ParseInfo p{}; + if (hasParse) + p = req.state(); + + res.json(J::obj({ + "ok", true, + "dev", true, + "request_id", rid, + "parse_info", J::obj({ + "parsed_json", p.parsed_json, + "parsed_form", p.parsed_form, + "parsed_multipart", p.parsed_multipart, + }), + "hint", "Use /api/echo/json or /api/echo/form to see parse markers", + })); }); + + // GET /dev/boom (to see exception handling in dev html maybe) + app.get("/dev/boom", [](Request &, Response &) + { + throw std::runtime_error("boom: dev crash example"); + return "unreachable"; }); +} + +// ---------------------------- middleware install ------------------------------ +// We demonstrate: +// - Global middlewares +// - Prefix middlewares +// - Conditional middlewares +// - Chains + +static void install_global_middlewares(vix::App &app) +{ + using namespace vix::middleware::app; + + // (A) request id + timing start, as ctx-based middleware + app.use(adapt_ctx(mw_request_id())); + app.use(adapt_ctx(mw_timing_start())); + + // (B) security headers (ctx-based preset) + app.use(security_headers_dev(false)); + + // (C) permissive CORS for dev + app.use(cors_dev()); + + // (D) fake auth (for demo) + app.use(adapt_ctx(mw_fake_auth())); + + // (E) mark timing after global middlewares + app.use(adapt_ctx(mw_timing_mark_global())); +} + +static void install_api_middlewares(vix::App &app) +{ + using namespace vix::middleware::app; + + // Everything under /api/ gets: + // - rate limit + // - http cache (GET) for /api/cache/* + // - parsers (json/form/multipart) only where needed + // - API key for /api/secure/* + // - admin guard for /api/admin/* + // + // We'll do it with prefix install + protect_prefix. + + // 1) Rate limit for /api/ + install(app, "/api/", rate_limit_dev(120, std::chrono::minutes(1))); + + // 2) HTTP cache on /api/cache/ + // cfg.prefix is used by install_http_cache, but you also have install(). + // We'll use install_http_cache for the prefix. + { + HttpCacheAppConfig cfg; + cfg.prefix = "/api/cache/"; + cfg.only_get = true; + cfg.ttl_ms = 25'000; + cfg.allow_bypass = true; + cfg.bypass_header = "x-vix-cache"; + cfg.bypass_value = "bypass"; + cfg.add_debug_header = true; + cfg.debug_header = "x-vix-cache-status"; + cfg.vary_headers = {"accept-encoding", "accept"}; + + install_http_cache(app, std::move(cfg)); + } + + // 3) Mark "api middleware reached" timing (ctx-based) + install(app, "/api/", adapt_ctx([](vix::middleware::Context &ctx, vix::middleware::Next next) + { + next(); + if (ctx.req().has_state_type()) + { + ctx.req().state().t2_after_api_mw = now_ms_wall(); + } })); + + // 4) Parsers: + // For demo, we install JSON parser only on /api/echo/json + // and Form parser only on /api/echo/form, Multipart save on /api/echo/multipart + // + // Your presets json_dev/form_dev/multipart_save_dev create adapt_ctx wrappers already. + install_exact(app, "/api/echo/json", chain(json_dev(1024, true, true), adapt_ctx(mw_mark_parsed_json()))); + install_exact(app, "/api/echo/form", chain(form_dev(1024, true), adapt_ctx(mw_mark_parsed_form()))); + install_exact(app, "/api/echo/multipart", chain(multipart_save_dev("uploads", 10 * 1024 * 1024), adapt_ctx(mw_mark_parsed_multipart()))); + + // 5) Protect /api/secure/ with API key (preset) + install(app, "/api/secure/", api_key_dev("dev_key_123")); + + // 6) Protect /api/admin/ with "fake admin" chain + // Require x-user and x-role: admin + install(app, "/api/admin/", chain(adapt_ctx(mw_fake_auth()), adapt_ctx(mw_require_admin()))); + + // 7) Example: legacy HttpMiddleware adaptation (header gate) on exact path + // Protect /api/ping with x-demo: 1 + install_exact(app, "/api/ping", adapt(mw_require_header("x-demo", "1"))); +} + +static void install_dev_middlewares(vix::App &app) +{ + using namespace vix::middleware::app; + + // Example IP filter under /dev/ + install(app, "/dev/", ip_filter_dev("x-vix-ip", {"1.2.3.4"}, true)); + + // Example CSRF (dev) for /dev/ (usually for browser forms) + // install(app, "/dev/", csrf_dev()); + // (left commented to avoid blocking GETs if your csrf is strict) +} + +// --------------------------------- main --------------------------------------- + +int main() +{ + vix::App app; + + // ---------------------------- + // middlewares (global + prefix) + // ---------------------------- + install_global_middlewares(app); + install_api_middlewares(app); + install_dev_middlewares(app); + + // ---------------------------- + // routes + // ---------------------------- + register_public_routes(app); + register_api_routes(app); + register_dev_routes(app); + + // ---------------------------- + // extra "documentation route" + // ---------------------------- + app.get("/_routes", [](Request &, Response &res) + { + std::vector routes; + + auto push = [&](std::string_view method, std::string_view path, std::string_view note) + { + routes.emplace_back(J::obj({ + "method", std::string(method), + "path", std::string(path), + "note", std::string(note), + })); + }; + + push("GET", "/", "public hello"); + push("GET", "/hello", "simple route"); + push("GET", "/txt", "returns const char* (auto-send)"); + push("GET", "/mix", "res.send() wins over returned payload"); + + push("GET", "/api/ping", "needs header x-demo: 1 (legacy middleware adapted)"); + push("GET", "/api/who", "shows RequestState (AuthInfo/RequestId)"); + push("GET", "/api/cache/demo", "GET cache (x-vix-cache-status: hit/miss/bypass)"); + push("GET", "/api/cache/heavy", "bigger payload cached"); + push("POST", "/api/echo/json", "json parser + parse marker"); + push("POST", "/api/echo/form", "form parser + parse marker"); + push("POST", "/api/echo/multipart", "multipart save + parse marker"); + push("GET", "/api/secure/whoami", "requires x-api-key: dev_key_123"); + push("GET", "/api/admin/stats", "requires x-user + x-role: admin"); + + push("GET", "/dev/trace", "debug route; may be IP-filtered"); + push("GET", "/dev/boom", "throws to test dev error handling"); + + res.json(J::obj({ + "ok", true, + "count", (long long)routes.size(), + "routes", J::array(std::move(routes)), + "tips", J::array({ + "Use -i with curl to see x-request-id and x-vix-cache-status headers", + "For /api/ping add: -H 'x-demo: 1'", + "For /api/secure/whoami add: -H 'x-api-key: dev_key_123'", + "For /api/admin/stats add: -H 'x-user: gaspard' -H 'x-role: admin'", + "To bypass cache: -H 'x-vix-cache: bypass'", + }), + })); }); + + // ---------------------------- + // run + // ---------------------------- + app.run(8080); + return 0; +} diff --git a/modules/cache b/modules/cache index 4e602bb..9b76554 160000 --- a/modules/cache +++ b/modules/cache @@ -1 +1 @@ -Subproject commit 4e602bb26b76944f574ef90befc54902b9d427be +Subproject commit 9b76554627bdff0d2b28931bf0c2e6993af3a383 diff --git a/modules/cli b/modules/cli index 893a99c..e321b5e 160000 --- a/modules/cli +++ b/modules/cli @@ -1 +1 @@ -Subproject commit 893a99c5e25feda5b5cb832560733ce8cee3db91 +Subproject commit e321b5eff3822901f2cce9f7a41e1c553242bebb diff --git a/modules/db b/modules/db index 1962378..0f286d8 160000 --- a/modules/db +++ b/modules/db @@ -1 +1 @@ -Subproject commit 1962378ffb62deaadaf8b3e4d42b4f544772a80d +Subproject commit 0f286d893d7007141328461059dee428a864f977 diff --git a/modules/net b/modules/net index ae15113..156c083 160000 --- a/modules/net +++ b/modules/net @@ -1 +1 @@ -Subproject commit ae15113f8913d11e3aa22acb88ff74a9f1b53f33 +Subproject commit 156c083a4c0f6df0fa69afe862284cc933c575ee diff --git a/modules/p2p b/modules/p2p index b37254a..e40160e 160000 --- a/modules/p2p +++ b/modules/p2p @@ -1 +1 @@ -Subproject commit b37254a7b8c0db39cd887496a9b00a736299ae9c +Subproject commit e40160e1b52f1605cae2594a1892d3d286287063 diff --git a/modules/sync b/modules/sync index 602af19..dab5307 160000 --- a/modules/sync +++ b/modules/sync @@ -1 +1 @@ -Subproject commit 602af1979a08cde93484a8c67bb14ba467bfd959 +Subproject commit dab53070f4335e1961f6a3065c82bf68eb5d11e1 diff --git a/security/minisign.pub b/security/minisign.pub new file mode 100644 index 0000000..40a320a --- /dev/null +++ b/security/minisign.pub @@ -0,0 +1,3 @@ +untrusted comment: minisign public key EA176E4F73F4FBC1 +RWTB+/RzT24X6uPqrPGKrqODmbchU4N1G00fWzQSUc+qkz7pBUnEys58 + diff --git a/vixcpp.pub b/vixcpp.pub new file mode 100644 index 0000000..2291633 --- /dev/null +++ b/vixcpp.pub @@ -0,0 +1,2 @@ +untrusted comment: minisign public key EA176E4F73F4FBC1 +RWTB+/RzT24X6uPqrPGKrqODmbchU4N1G00fWzQSUc+qkz7pBUnEys58