From 1143ff606b00e66eb24b0260370f17b2316228cf Mon Sep 17 00:00:00 2001 From: Gaspard Kirira Date: Fri, 26 Dec 2025 14:56:03 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20v1.17.0=20=E2=80=94=20vix=20install,=20?= =?UTF-8?q?middleware=20integration,=20and=20full=20middleware=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ➜ Example: vix install --path ./dist/blog@1.0.0.vixpkg command for project setup - Integrate middleware module into default runtime - Enable middleware usage out-of-the-box - Add full middleware examples: auth, cors, csrf, rate limit, ip filter, body limit, compression, cache, etag, headers, static files, form parsing - Update CMake, VixConfig, and documentation --- CMakeLists.txt | 93 ++++++++ README.md | 4 +- cmake/VixConfig.cmake.in | 25 ++- config/config copy.json | 24 +++ config/config.json | 29 +-- examples/auth/api_key_app_simple.cpp | 83 +++++++ examples/auth/generate_token/jwt_gen.cpp | 98 +++++++++ examples/auth/jwt/jwt_app_simple.cpp | 66 ++++++ examples/auth/jwt/jwt_pipeline_example.cpp | 75 +++++++ examples/auth/rbac/rbac_app_simple.cpp | 109 ++++++++++ examples/body_limit/body_limit_app.cpp | 77 +++++++ .../body_limit/body_limit_should_apply.cpp | 103 +++++++++ examples/body_limit/index.html | 202 ++++++++++++++++++ .../cache/http_cache_app_custom_cache.cpp | 65 ++++++ examples/cache/http_cache_app_debug.cpp | 74 +++++++ examples/cache/http_cache_app_simple.cpp | 47 ++++ .../compression/compression_app_simple.cpp | 73 +++++++ examples/cors/cors_app_basic.cpp | 23 ++ examples/cors/cors_app_strict.cpp | 39 ++++ examples/csrf/csrf_pipeline_demo.cpp | 82 +++++++ examples/csrf/csrf_strict_server.cpp | 43 ++++ examples/csrf/index.html | 115 ++++++++++ examples/csrf/security_cors_csrf_server.cpp | 83 +++++++ examples/etag/etag_app_simple.cpp | 50 +++++ examples/form/form_app_simple.cpp | 47 ++++ examples/form/json_app_simple.cpp | 65 ++++++ examples/form/json_app_strict.cpp | 63 ++++++ examples/form/multipart_app_simple.cpp | 54 +++++ examples/group_builder/group_app_example.cpp | 87 ++++++++ .../group_builder/group_builder_example.cpp | 49 +++++ examples/headers/headers_pipeline_demo.cpp | 62 ++++++ examples/headers/index.html | 167 +++++++++++++++ .../security_cors_csrf_headers_server.cpp | 94 ++++++++ examples/headers/security_headers_server.cpp | 36 ++++ .../ip_filter/ip_filter_pipeline_demo.cpp | 112 ++++++++++ examples/ip_filter/ip_filter_server.cpp | 64 ++++++ examples/main.cpp | 10 + .../middleware_http/http_cache_example.cpp | 27 +++ examples/rate_limit/index.html | 195 +++++++++++++++++ .../rate_limit/rate_limit_pipeline_demo.cpp | 84 ++++++++ examples/rate_limit/rate_limit_server.cpp | 30 +++ .../security_cors_ip_rate_server.cpp | 112 ++++++++++ .../static_files/static_files_app_simple.cpp | 51 +++++ modules/cli | 2 +- modules/core | 2 +- modules/middleware | 2 +- modules/utils | 2 +- modules/websocket | 2 +- 48 files changed, 3072 insertions(+), 29 deletions(-) create mode 100644 config/config copy.json create mode 100644 examples/auth/api_key_app_simple.cpp create mode 100644 examples/auth/generate_token/jwt_gen.cpp create mode 100644 examples/auth/jwt/jwt_app_simple.cpp create mode 100644 examples/auth/jwt/jwt_pipeline_example.cpp create mode 100644 examples/auth/rbac/rbac_app_simple.cpp create mode 100644 examples/body_limit/body_limit_app.cpp create mode 100644 examples/body_limit/body_limit_should_apply.cpp create mode 100644 examples/body_limit/index.html create mode 100644 examples/cache/http_cache_app_custom_cache.cpp create mode 100644 examples/cache/http_cache_app_debug.cpp create mode 100644 examples/cache/http_cache_app_simple.cpp create mode 100644 examples/compression/compression_app_simple.cpp create mode 100644 examples/cors/cors_app_basic.cpp create mode 100644 examples/cors/cors_app_strict.cpp create mode 100644 examples/csrf/csrf_pipeline_demo.cpp create mode 100644 examples/csrf/csrf_strict_server.cpp create mode 100644 examples/csrf/index.html create mode 100644 examples/csrf/security_cors_csrf_server.cpp create mode 100644 examples/etag/etag_app_simple.cpp create mode 100644 examples/form/form_app_simple.cpp create mode 100644 examples/form/json_app_simple.cpp create mode 100644 examples/form/json_app_strict.cpp create mode 100644 examples/form/multipart_app_simple.cpp create mode 100644 examples/group_builder/group_app_example.cpp create mode 100644 examples/group_builder/group_builder_example.cpp create mode 100644 examples/headers/headers_pipeline_demo.cpp create mode 100644 examples/headers/index.html create mode 100644 examples/headers/security_cors_csrf_headers_server.cpp create mode 100644 examples/headers/security_headers_server.cpp create mode 100644 examples/ip_filter/ip_filter_pipeline_demo.cpp create mode 100644 examples/ip_filter/ip_filter_server.cpp create mode 100644 examples/middleware_http/http_cache_example.cpp create mode 100644 examples/rate_limit/index.html create mode 100644 examples/rate_limit/rate_limit_pipeline_demo.cpp create mode 100644 examples/rate_limit/rate_limit_server.cpp create mode 100644 examples/rate_limit/security_cors_ip_rate_server.cpp create mode 100644 examples/static_files/static_files_app_simple.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 19ca80d..bf35c39 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,6 +68,9 @@ option(VIX_ENABLE_WEBSOCKET "Build Vix WebSocket module" ON) option(VIX_ENABLE_CLI "Build Vix CLI module" ON) option(VIX_FORCE_FETCH_JSON "Fetch JSON backend if modules/json missing" ON) +option(VIX_BENCH_MODE "Disable heavy security/logging checks for benchmarks" OFF) +option(VIX_ENABLE_MIDDLEWARE "Build Vix middleware module" ON) +option(VIX_ENABLE_HTTP_COMPRESSION "Enable HTTP compression middleware deps (zlib/brotli)" ON) # ---------------------------------------------------- # Tooling / Static analysis @@ -184,6 +187,35 @@ endif() add_library(vix INTERFACE) add_library(vix::vix ALIAS vix) +# ---------------------------------------------------- +# Optional deps: HTTP compression (zlib + brotli) +# Propagate defs/libs to all consumers through vix::vix +# ---------------------------------------------------- +if (VIX_ENABLE_HTTP_COMPRESSION) + # ---- zlib (gzip) ---- + find_package(ZLIB QUIET) + if (ZLIB_FOUND) + message(STATUS "Compression: zlib found -> gzip enabled") + target_compile_definitions(vix INTERFACE VIX_HAS_ZLIB=1) + target_link_libraries(vix INTERFACE ZLIB::ZLIB) + else() + message(STATUS "Compression: zlib not found -> gzip disabled") + endif() + + # ---- brotli (br) ---- + find_library(BROTLI_ENC NAMES brotlienc) + find_library(BROTLI_COMMON NAMES brotlicommon) + + if (BROTLI_ENC AND BROTLI_COMMON) + message(STATUS "Compression: brotli found -> br enabled") + target_compile_definitions(vix INTERFACE VIX_HAS_BROTLI=1) + target_link_libraries(vix INTERFACE ${BROTLI_ENC} ${BROTLI_COMMON}) + else() + message(STATUS "Compression: brotli not found -> br disabled") + endif() +endif() + + # Attach warnings/coverage to umbrella (propagates to consumers) if (TARGET vix_warnings) target_link_libraries(vix INTERFACE vix_warnings) @@ -268,6 +300,48 @@ else() message(FATAL_ERROR "Missing 'modules/core'. Run: git submodule update --init --recursive") endif() +# --- Middleware (optional) --- +set(VIX_HAS_MIDDLEWARE OFF) +if (VIX_ENABLE_MIDDLEWARE AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/middleware/CMakeLists.txt") + message(STATUS "Adding 'modules/middleware'...") + add_subdirectory(modules/middleware middleware_build) + + # Normalise: expose vix::middleware + if (TARGET vix::middleware OR TARGET vix_middleware) + set(VIX_HAS_MIDDLEWARE ON) + if (TARGET vix_middleware AND NOT TARGET vix::middleware) + add_library(vix::middleware ALIAS vix_middleware) + endif() + else() + message(WARNING "Middleware module added but no vix::middleware target was exported.") + endif() +else() + message(STATUS "Middleware: disabled or not present.") +endif() + +# Bench mode (compile-time flags) +if (VIX_BENCH_MODE) + message(STATUS "Bench mode enabled: defining VIX_BENCH_MODE") + + # Resolve the real core target (vix::core is an ALIAS in your tree) + set(_VIX_CORE_REAL "") + + if (TARGET vix_core) + set(_VIX_CORE_REAL vix_core) + elseif (TARGET vix-core) + set(_VIX_CORE_REAL vix-core) + elseif (TARGET core) + set(_VIX_CORE_REAL core) + endif() + + if (_VIX_CORE_REAL) + target_compile_definitions(${_VIX_CORE_REAL} PUBLIC VIX_BENCH_MODE=1) + else() + message(WARNING "Bench mode requested but could not find real core target (tried: vix_core, vix-core, core).") + endif() +endif() + + # --- WebSocket (optional) --- set(VIX_HAS_WEBSOCKET OFF) if (VIX_ENABLE_WEBSOCKET AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/websocket/CMakeLists.txt") @@ -330,6 +404,14 @@ elseif (TARGET vix_websocket) target_link_libraries(vix INTERFACE vix::websocket) endif() +# Link middleware only if it exists +if (TARGET vix::middleware) + target_link_libraries(vix INTERFACE vix::middleware) +elseif (TARGET vix_middleware) + add_library(vix::middleware ALIAS vix_middleware) + target_link_libraries(vix INTERFACE vix::middleware) +endif() + # Propagate sanitizers to consumers (umbrella) if (TARGET vix_sanitizers) target_link_libraries(vix INTERFACE vix_sanitizers) @@ -511,6 +593,11 @@ set(VIX_WITH_OPENSSL ON) # adjust if you ever disable SSL in core set(VIX_WITH_MYSQL OFF) # do NOT require MySQL for consumers by default set(VIX_WITH_SQLITE OFF) # will be set ON only if websocket exists +set(VIX_WITH_ZLIB OFF) +if (VIX_ENABLE_HTTP_COMPRESSION AND ZLIB_FOUND) + set(VIX_WITH_ZLIB ON) +endif() + if (TARGET vix::websocket OR TARGET vix_websocket) set(VIX_WITH_SQLITE ON) endif() @@ -542,6 +629,11 @@ if (VIX_ENABLE_INSTALL) 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") + endif() + # Export umbrella target (modules must also export their targets into VixTargets) install(TARGETS vix EXPORT VixTargets) @@ -605,6 +697,7 @@ message(STATUS "Project version : ${PROJECT_VERSION}") message(STATUS "JSON backend : ${_VIX_JSON_BACKEND}") message(STATUS "WebSocket built : ${VIX_HAS_WEBSOCKET}") message(STATUS "ORM packaged : ${VIX_HAS_ORM}") +message(STATUS "Middleware built : ${VIX_HAS_MIDDLEWARE}") message(STATUS "Examples : ${VIX_BUILD_EXAMPLES}") message(STATUS "Tests : ${VIX_BUILD_TESTS}") message(STATUS "Sanitizers : ${VIX_ENABLE_SANITIZERS}") diff --git a/README.md b/README.md index 27b5005..8f2690b 100644 --- a/README.md +++ b/README.md @@ -123,8 +123,8 @@ using namespace Vix; int main() { App app; - app.get("/", [](Request req, Response res) { - res.json({ "message", "Hello world" }); + app.get("/", [](Request&, Response& res){ + res.send("Hello from Vix.cpp πŸš€"); }); app.run(8080); diff --git a/cmake/VixConfig.cmake.in b/cmake/VixConfig.cmake.in index 3aab15c..d8af6cb 100644 --- a/cmake/VixConfig.cmake.in +++ b/cmake/VixConfig.cmake.in @@ -7,15 +7,32 @@ include(CMakeFindDependencyMacro) # MUST be defined BEFORE including VixTargets.cmake # ------------------------------------------------------- -# MySQLCppConn is ONLY needed if vix::orm was built with MySQL, -# but VixTargets.cmake may reference MySQLCppConn::MySQLCppConn. -# We DO NOT force consumers to have MySQL installed. -# If the real package exists, it will define the target; otherwise we create a stub. find_package(MySQLCppConn QUIET CONFIG) if (NOT TARGET MySQLCppConn::MySQLCppConn) add_library(MySQLCppConn::MySQLCppConn INTERFACE IMPORTED) endif() +# ------------------------------------------------------- +# 0bis) Compression deps (optional) β€” MUST exist before VixTargets.cmake +# ------------------------------------------------------- +if (@VIX_WITH_ZLIB@) + find_dependency(ZLIB QUIET) + + # Safety net: if ZLIB was found but did not create the target + if (ZLIB_FOUND AND NOT TARGET ZLIB::ZLIB) + add_library(ZLIB::ZLIB INTERFACE IMPORTED) + target_include_directories(ZLIB::ZLIB INTERFACE "${ZLIB_INCLUDE_DIRS}") + target_link_libraries(ZLIB::ZLIB INTERFACE "${ZLIB_LIBRARIES}") + endif() + + # If Vix exports ZLIB::ZLIB, we must have it + if (NOT TARGET ZLIB::ZLIB) + message(FATAL_ERROR + "Vix was built with gzip support (VIX_WITH_ZLIB=ON), but ZLIB::ZLIB is not available on this system." + ) + endif() +endif() + # ------------------------------------------------------- # 1) Load exported Vix targets # ------------------------------------------------------- diff --git a/config/config copy.json b/config/config copy.json new file mode 100644 index 0000000..a938217 --- /dev/null +++ b/config/config copy.json @@ -0,0 +1,24 @@ +{ + "database": { + "default": { + "ENGINE": "mysql", + "NAME": "mydb", + "USER": "myuser", + "PASSWORD": "", + "HOST": "localhost", + "PORT": 3306 + } + }, + + "server": { + "port": 8080, + "request_timeout": 5000 + }, + + "websocket": { + "port": 9090, + "max_message_size": 65536, + "idle_timeout": 60, + "ping_interval": 30 + } +} diff --git a/config/config.json b/config/config.json index a938217..9e5fa6f 100644 --- a/config/config.json +++ b/config/config.json @@ -1,24 +1,17 @@ { - "database": { - "default": { - "ENGINE": "mysql", - "NAME": "mydb", - "USER": "myuser", - "PASSWORD": "", - "HOST": "localhost", - "PORT": 3306 - } - }, - "server": { "port": 8080, - "request_timeout": 5000 + "request_timeout": 2000, + "io_threads": 0 + }, + "logging": { + "async": true, + "queue_max": 20000, + "drop_on_overflow": true }, - - "websocket": { - "port": 9090, - "max_message_size": 65536, - "idle_timeout": 60, - "ping_interval": 30 + "waf": { + "mode": "basic", + "max_target_len": 4096, + "max_body_bytes": 1048576 } } diff --git a/examples/auth/api_key_app_simple.cpp b/examples/auth/api_key_app_simple.cpp new file mode 100644 index 0000000..ff7e2bb --- /dev/null +++ b/examples/auth/api_key_app_simple.cpp @@ -0,0 +1,83 @@ +// ============================================================================ +// api_key_app_simple.cpp β€” API Key auth example (Vix.cpp) +// ---------------------------------------------------------------------------- +// Run: +// vix run api_key_app_simple.cpp +// +// Test: +// # Missing key -> 401 +// curl -i http://localhost:8080/secure +// +// # Invalid key -> 403 +// curl -i -H "x-api-key: wrong" http://localhost:8080/secure +// curl -i "http://localhost:8080/secure?api_key=wrong" +// +// # Valid key -> 200 +// curl -i -H "x-api-key: secret" http://localhost:8080/secure +// curl -i "http://localhost:8080/secure?api_key=secret" +// ============================================================================ + +#include +#include + +#include +#include + +using namespace vix; + +static void print_help() +{ + std::cout + << "Vix API Key example running:\n" + << " http://localhost:8080/\n" + << " http://localhost:8080/secure\n\n" + << "Valid key:\n" + << " secret\n\n" + << "Try:\n" + << " curl -i http://localhost:8080/secure\n" + << " curl -i -H \"x-api-key: wrong\" http://localhost:8080/secure\n" + << " curl -i -H \"x-api-key: secret\" http://localhost:8080/secure\n" + << " curl -i \"http://localhost:8080/secure?api_key=wrong\"\n" + << " curl -i \"http://localhost:8080/secure?api_key=secret\"\n\n"; +} + +int main() +{ + App app; + + // --------------------------------------------------------------------- + // API key protection (preset) + // - Header: x-api-key + // - Query : ?api_key= + // - Allowed key: "secret" + // --------------------------------------------------------------------- + app.use("/secure", middleware::app::api_key_dev("secret")); + // app.use( + // "/secure", + // middleware::app::api_key_auth({ + // .header = "x-api-key", + // .query_param = "api_key", + // .allowed_keys = {"secret"}, + // })); + + // --------------------------------------------------------------------- + // Routes + // --------------------------------------------------------------------- + app.get("/", [](Request &, Response &res) + { res.send( + "API Key example:\n" + " /secure requires x-api-key: secret OR ?api_key=secret\n"); }); + + app.get("/secure", [](Request &req, Response &res) + { + auto &key = req.state(); + + res.json({ + "ok", true, + "api_key", key.value + }); }); + + print_help(); + app.run(8080); + return 0; +} diff --git a/examples/auth/generate_token/jwt_gen.cpp b/examples/auth/generate_token/jwt_gen.cpp new file mode 100644 index 0000000..e740191 --- /dev/null +++ b/examples/auth/generate_token/jwt_gen.cpp @@ -0,0 +1,98 @@ +// ============================================================================ +// jwt_gen.cpp β€” generate HS256 JWT tokens for rbac_app_simple.cpp (Vix.cpp) +// ---------------------------------------------------------------------------- +// Build: +// g++ -std=c++20 jwt_gen.cpp -lssl -lcrypto -O2 && ./a.out +// +// Prints: +// TOKEN_OK (admin + products:write) +// TOKEN_NO_PERM (admin but missing products:write) +// +// NOTE: secret must match rbac_app_simple.cpp (dev_secret). +// ============================================================================ + +#include +#include +#include + +#include +#include +#include + +static std::string b64url_encode(const unsigned char *data, size_t len) +{ + std::string b64; + b64.resize(4 * ((len + 2) / 3)); + + int out_len = EVP_EncodeBlock( + reinterpret_cast(&b64[0]), + data, + static_cast(len)); + + b64.resize(static_cast(out_len)); + + for (char &c : b64) + { + if (c == '+') + c = '-'; + else if (c == '/') + c = '_'; + } + while (!b64.empty() && b64.back() == '=') + b64.pop_back(); + + return b64; +} + +static std::string hmac_sha256_b64url(std::string_view msg, std::string_view secret) +{ + unsigned int out_len = 0; + unsigned char out[EVP_MAX_MD_SIZE]; + + HMAC(EVP_sha256(), + secret.data(), + static_cast(secret.size()), + reinterpret_cast(msg.data()), + msg.size(), + out, + &out_len); + + return b64url_encode(out, static_cast(out_len)); +} + +static std::string make_jwt_hs256(const nlohmann::json &payload, const std::string &secret) +{ + nlohmann::json header = {{"alg", "HS256"}, {"typ", "JWT"}}; + + const std::string h = header.dump(); + const std::string p = payload.dump(); + + const std::string h64 = b64url_encode(reinterpret_cast(h.data()), h.size()); + const std::string p64 = b64url_encode(reinterpret_cast(p.data()), p.size()); + + const std::string signing = h64 + "." + p64; + const std::string sig = hmac_sha256_b64url(signing, secret); + + return signing + "." + sig; +} + +int main() +{ + const std::string secret = "dev_secret"; + + nlohmann::json ok = { + {"sub", "user123"}, + {"roles", {"admin"}}, + {"perms", {"products:write", "orders:read"}}}; + + nlohmann::json no_perm = { + {"sub", "user123"}, + {"roles", {"admin"}}, + {"perms", {"orders:read"}}}; + + std::cout << "TOKEN_OK:\n" + << make_jwt_hs256(ok, secret) << "\n\n"; + std::cout << "TOKEN_NO_PERM:\n" + << make_jwt_hs256(no_perm, secret) << "\n"; + return 0; +} diff --git a/examples/auth/jwt/jwt_app_simple.cpp b/examples/auth/jwt/jwt_app_simple.cpp new file mode 100644 index 0000000..e6d58d0 --- /dev/null +++ b/examples/auth/jwt/jwt_app_simple.cpp @@ -0,0 +1,66 @@ +// ============================================================================ +// jwt_app_simple.cpp β€” JWT middleware (App) super simple +// ---------------------------------------------------------------------------- +// Run: +// vix run jwt_app_simple.cpp +// +// Test: +// curl -i http://localhost:8080/ +// curl -i http://localhost:8080/secure +// +// Valid token (HS256, secret=dev_secret): +// TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwicm9sZXMiOlsiYWRtaW4iXX0.3HK5b1sXMbxkjC3Tllwtcuzxm-1OI0D184Fuav0-XQo" +// +// curl -i -H "Authorization: Bearer $TOKEN" http://localhost:8080/secure +// ============================================================================ +#include +#include + +#include +#include + +using namespace vix; + +static const std::string kToken = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiJ1c2VyMTIzIiwicm9sZXMiOlsiYWRtaW4iXX0." + "3HK5b1sXMbxkjC3Tllwtcuzxm-1OI0D184Fuav0-XQo"; // HS256, secret=dev_secret + +int main() +{ + App app; + + // πŸ” Protect ONLY /secure (dev preset: verify_exp = false) + app.use("/secure", middleware::app::jwt_dev("dev_secret")); + + app.get("/", [](Request &, Response &res) + { res.send( + "JWT example:\n" + " GET /secure requires Bearer token.\n" + "\n" + "Try:\n" + " curl -i http://localhost:8080/secure\n" + " curl -i -H \"Authorization: Bearer \" http://localhost:8080/secure\n"); }); + + app.get("/secure", [](Request &req, Response &res) + { + auto &claims = req.state(); + res.json({"ok", true, + "sub", claims.subject, + "roles", claims.roles}); + res.status(501).json({"ok", false, + "error", "JWT middleware not enabled (VIX_ENABLE_JWT)"}); }); + + std::cout + << "Vix JWT example running:\n" + << " http://localhost:8080/\n" + << " http://localhost:8080/secure\n\n" + << "Use this token:\n" + << " " << kToken << "\n\n" + << "Test:\n" + << " curl -i -H \"Authorization: Bearer " << kToken + << "\" http://localhost:8080/secure\n"; + + app.run(8080); + return 0; +} diff --git a/examples/auth/jwt/jwt_pipeline_example.cpp b/examples/auth/jwt/jwt_pipeline_example.cpp new file mode 100644 index 0000000..561d07c --- /dev/null +++ b/examples/auth/jwt/jwt_pipeline_example.cpp @@ -0,0 +1,75 @@ +// ============================================================================ +// jwt_example.cpp β€” Minimal JWT middleware example (Vix.cpp) +// ---------------------------------------------------------------------------- +// Run: +// vix run jwt_example.cpp +// ============================================================================ + +#include +#include +#include + +#include + +#include +#include + +using namespace vix::middleware; + +static vix::vhttp::RawRequest make_req_with_bearer(std::string token) +{ + namespace http = boost::beast::http; + + vix::vhttp::RawRequest req{http::verb::get, "/secure", 11}; + req.set(http::field::host, "localhost"); + req.set("authorization", "Bearer " + token); + return req; +} + +int main() +{ + namespace http = boost::beast::http; + + // Same secret as the middleware config + const std::string secret = "dev_secret"; + + // A valid HS256 token (payload: {"sub":"user123","roles":["admin"]}) + // signed with secret "dev_secret". + const std::string token = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiJ1c2VyMTIzIiwicm9sZXMiOlsiYWRtaW4iXX0." + "3HK5b1sXMbxkjC3Tllwtcuzxm-1OI0D184Fuav0-XQo"; + + // 1) Build pipeline + HttpPipeline p; + + // 2) Install JWT middleware + auth::JwtOptions opt{}; + opt.secret = secret; + opt.verify_exp = false; // keep example simple + p.use(auth::jwt(opt)); + + // 3) Run a request through the pipeline + auto raw = make_req_with_bearer(token); + http::response res; + + vix::vhttp::Request req(raw, {}); + vix::vhttp::ResponseWrapper w(res); + + p.run(req, w, [&](Request &r, Response &) + { + // Claims are available after jwt() success + auto &claims = r.state(); + + assert(claims.subject == "user123"); + assert(!claims.roles.empty()); + assert(claims.roles[0] == "admin"); + + w.status(200).text("OK"); }); + + assert(res.result_int() == 200); + assert(res.body() == "OK"); + + std::cout << "[OK] jwt example: subject=" << "user123" << " role=admin\n"; + return 0; +} diff --git a/examples/auth/rbac/rbac_app_simple.cpp b/examples/auth/rbac/rbac_app_simple.cpp new file mode 100644 index 0000000..d1e70ac --- /dev/null +++ b/examples/auth/rbac/rbac_app_simple.cpp @@ -0,0 +1,109 @@ +// ============================================================================ +// rbac_app_simple.cpp β€” RBAC (roles + perms) example (Vix.cpp) +// ---------------------------------------------------------------------------- +// Run: +// vix run rbac_app_simple.cpp +// +// Test: +// curl -i http://localhost:8080/ +// curl -i http://localhost:8080/admin +// +// Valid token (admin + products:write): +// curl -i -H "Authorization: Bearer " http://localhost:8080/admin +// +// Invalid token (admin but missing products:write): +// curl -i -H "Authorization: Bearer " http://localhost:8080/admin +// ============================================================================ + +#include +#include + +#include + +#include +#include +#include + +using namespace vix; + +// HS256, secret=dev_secret +// payload: {"sub":"user123","roles":["admin"],"perms":["products:write","orders:read"]} +static const std::string TOKEN_OK = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiJ1c2VyMTIzIiwicm9sZXMiOlsiYWRtaW4iXSwicGVybXMiOlsicHJvZHVjdHM6d3JpdGUiLCJvcmRlcnM6cmVhZCJdfQ." + "w1y3nA2F1kq0oJ0x8wWc5wQx8zF4h2d6V7mYp0jYk3Q"; + +// HS256, secret=dev_secret +// payload: {"sub":"user123","roles":["admin"],"perms":["orders:read"]} (missing products:write) +static const std::string TOKEN_NO_PERM = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiJ1c2VyMTIzIiwicm9sZXMiOlsiYWRtaW4iXSwicGVybXMiOlsib3JkZXJzOnJlYWQiXX0." + "qVqWmQmHf4yqPzvYzGf9m3jv9oGzW0Q8c8qkQkqkQkQ"; + +int main() +{ + App app; + + // 1) JWT auth (puts JwtClaims into request state) + vix::middleware::auth::JwtOptions jwt_opt{}; + jwt_opt.secret = "dev_secret"; + jwt_opt.verify_exp = false; + + // 2) RBAC: build Authz from JwtClaims, then enforce rules + vix::middleware::auth::RbacOptions rbac_opt{}; + rbac_opt.require_auth = true; + rbac_opt.use_resolver = false; // keep the example simple + + auto jwt_mw = vix::middleware::app::adapt_ctx(vix::middleware::auth::jwt(jwt_opt)); + auto ctx_mw = vix::middleware::app::adapt_ctx(vix::middleware::auth::rbac_context(rbac_opt)); + auto role_mw = vix::middleware::app::adapt_ctx(vix::middleware::auth::require_role("admin")); + auto perm_mw = vix::middleware::app::adapt_ctx(vix::middleware::auth::require_perm("products:write")); + + // Protect only /admin + app.use(vix::middleware::app::when( + [](const Request &req) + { return req.path() == "/admin"; }, + std::move(jwt_mw))); + app.use(vix::middleware::app::when( + [](const Request &req) + { return req.path() == "/admin"; }, + std::move(ctx_mw))); + app.use(vix::middleware::app::when( + [](const Request &req) + { return req.path() == "/admin"; }, + std::move(role_mw))); + app.use(vix::middleware::app::when( + [](const Request &req) + { return req.path() == "/admin"; }, + std::move(perm_mw))); + + // Public + app.get("/", [](Request &, Response &res) + { res.send("RBAC example: /admin requires role=admin + perm=products:write"); }); + + // Protected + app.get("/admin", [](Request &req, Response &res) + { + auto& authz = req.state(); + + res.json({ + "ok", true, + "sub", authz.subject, + "has_admin", authz.has_role("admin"), + "has_products_write", authz.has_perm("products:write") + }); }); + + std::cout + << "Vix RBAC example running:\n" + << " http://localhost:8080/\n" + << " http://localhost:8080/admin\n\n" + << "TOKEN_OK:\n " << TOKEN_OK << "\n\n" + << "TOKEN_NO_PERM:\n " << TOKEN_NO_PERM << "\n\n" + << "Try:\n" + << " curl -i http://localhost:8080/admin\n" + << " curl -i -H \"Authorization: Bearer " << TOKEN_OK << "\" http://localhost:8080/admin\n" + << " curl -i -H \"Authorization: Bearer " << TOKEN_NO_PERM << "\" http://localhost:8080/admin\n"; + + app.run(8080); + return 0; +} diff --git a/examples/body_limit/body_limit_app.cpp b/examples/body_limit/body_limit_app.cpp new file mode 100644 index 0000000..a75237d --- /dev/null +++ b/examples/body_limit/body_limit_app.cpp @@ -0,0 +1,77 @@ +// ============================================================================ +// body_limit_app.cpp β€” Body limit middleware example (Vix.cpp) +// +// Run: +// vix run vix/examples/body_limit_app.cpp +// +// Tests: +// +// 1) Small body (OK) +// curl -i -X POST http://localhost:8080/api/echo \ +// -H "Content-Type: text/plain" \ +// --data "hello" +// +// 2) Large body (413 Payload Too Large) +// python3 - <<'PY' +// import requests +// print(requests.post("http://localhost:8080/api/echo", data="x"*64).status_code) +// PY +// +// 3) GET ignored by default (apply_to_get=false) +// curl -i http://localhost:8080/api/ping +// +// 4) Strict mode: require Content-Length (411) (see route /api/strict) +// curl -i -X POST http://localhost:8080/api/strict \ +// -H "Transfer-Encoding: chunked" \ +// -H "Content-Type: text/plain" \ +// --data "hello" +// +// ============================================================================ + +#include +#include + +using namespace vix; + +static void register_routes(App &app) +{ + app.get("/", [](Request &, Response &res) + { res.send("body_limit example: /api/ping, /api/echo, /api/strict"); }); + + app.get("/api/ping", [](Request &, Response &res) + { res.json({"ok", true, "msg", "pong"}); }); + + app.post("/api/echo", [](Request &req, Response &res) + { res.json({"ok", true, + "bytes", static_cast(req.body().size()), + "content_type", req.header("content-type")}); }); + + app.post("/api/strict", [](Request &req, Response &res) + { res.json({"ok", true, + "msg", "strict accepted", + "bytes", static_cast(req.body().size())}); }); +} + +int main() +{ + App app; + + // /api: max 32 bytes (demo), chunked allowed + app.use("/api", middleware::app::body_limit_dev( + 32, // max_bytes + false, // apply_to_get + true // allow_chunked + )); + + // /api/strict: max 32 bytes, chunked NOT allowed => 411 if missing Content-Length + app.use("/api/strict", middleware::app::body_limit_dev( + 32, // max_bytes + false, // apply_to_get + false // allow_chunked (strict) + )); + + register_routes(app); + + app.run(8080); + return 0; +} diff --git a/examples/body_limit/body_limit_should_apply.cpp b/examples/body_limit/body_limit_should_apply.cpp new file mode 100644 index 0000000..ea057af --- /dev/null +++ b/examples/body_limit/body_limit_should_apply.cpp @@ -0,0 +1,103 @@ +// ============================================================================ +// body_limit_should_apply.cpp β€” CORS + conditional body limit via should_apply() +// +// Run: +// vix run vix/examples/body_limit_should_apply.cpp +// +// Front (serve from another origin): +// python3 -m http.server 5173 --bind 127.0.0.1 +// open http://localhost:5173 +// +// Curl tests: +// curl -i http://localhost:8080/health +// curl -i -X POST http://localhost:8080/upload --data "hello" +// curl -i -X POST http://localhost:8080/upload --data "0123456789abcdefX" # 17 bytes => 413 +// +// CORS preflight tests: +// curl -i -X OPTIONS http://localhost:8080/api/echo \ +// -H "Origin: http://localhost:5173" \ +// -H "Access-Control-Request-Method: POST" \ +// -H "Access-Control-Request-Headers: content-type" +// +// ============================================================================ + +#include +#include + +using namespace vix; + +// ------------------------------------------------------------ +// Install CORS + OPTIONS routes (so browser preflight works) +// ------------------------------------------------------------ +static void install_cors(App &app) +{ + app.use("/", middleware::app::cors_ip_demo({"http://localhost:5173", + "http://127.0.0.1:5173", + "http://0.0.0.0:5173"})); + + auto options_noop = [](Request &, Response &res) + { + res.status(204).send(); + }; + + app.options("/api/ping", options_noop); + app.options("/api/echo", options_noop); + app.options("/api/strict", options_noop); + app.options("/upload", options_noop); +} + +// ------------------------------------------------------------ +// Install body limit (write methods only) β€” via alias preset +// ------------------------------------------------------------ +static void install_body_limit(App &app) +{ + // Applies only to POST/PUT/PATCH (alias) + app.use("/", middleware::app::body_limit_write_dev(16)); +} + +// ------------------------------------------------------------ +// Routes +// ------------------------------------------------------------ +static void install_routes(App &app) +{ + app.get("/health", [](Request &, Response &res) + { res.json({"ok", true}); }); + + app.get("/api/ping", [](Request &, Response &res) + { + res.header("X-Request-Id", "req_ping_1"); + res.json({"ok", true, "msg", "pong"}); }); + + app.post("/api/echo", [](Request &req, Response &res) + { + res.header("X-Request-Id", "req_echo_1"); + res.json({ + "ok", true, + "path", req.path(), + "bytes", static_cast(req.body().size()), + "body", req.body() + }); }); + + app.post("/api/strict", [](Request &req, Response &res) + { res.json({"ok", true, "bytes", static_cast(req.body().size())}); }); + + app.post("/upload", [](Request &req, Response &res) + { res.json({"ok", true, "bytes", static_cast(req.body().size())}); }); +} + +static void run_app() +{ + App app; + + install_cors(app); + install_body_limit(app); + install_routes(app); + + app.run(8080); +} + +int main() +{ + run_app(); + return 0; +} diff --git a/examples/body_limit/index.html b/examples/body_limit/index.html new file mode 100644 index 0000000..ff12cbf --- /dev/null +++ b/examples/body_limit/index.html @@ -0,0 +1,202 @@ + + + + + Vix.cpp β€” body_limit playground + + + + + +

Vix.cpp β€” body_limit playground

+

+ Frontend served on http://localhost:5173
+ API expected on http://localhost:8080 +

+ +
+

GET (ignored by body_limit)

+ +
+ +
+

POST /api/echo (max 32 bytes)

+ + + +
+ +
+

POST /api/strict (Content-Length required)

+ + +
+ +
+

Result

+
No request yet.
+
+ +
body_limit middleware demo β€” Vix.cpp
+ + + + diff --git a/examples/cache/http_cache_app_custom_cache.cpp b/examples/cache/http_cache_app_custom_cache.cpp new file mode 100644 index 0000000..886bd61 --- /dev/null +++ b/examples/cache/http_cache_app_custom_cache.cpp @@ -0,0 +1,65 @@ +// ============================================================================ +// http_cache_app_custom_cache.cpp β€” HTTP Cache (Custom Cache Injection) +// ---------------------------------------------------------------------------- +// Run: +// vix run examples/http_cache_app_custom_cache.cpp +// +// Test: +// curl -i "http://localhost:8080/api/slow" +// curl -i "http://localhost:8080/api/slow" # second call should be cached +// +// Notes: +// - Injects a custom Cache instance (MemoryStore + CachePolicy) +// - Still uses App-level middleware adapter (no RawRequest exposed) +// ============================================================================ + +#include +#include + +#include +#include + +using namespace vix; + +static void register_routes(App &app) +{ + app.get("/api/slow", [](Request &, Response &res) + { + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + res.text("slow response (origin)"); }); + + app.get("/", [](Request &, Response &res) + { res.text("home (not cached)"); }); +} + +int main() +{ + App app; + + // Build a default cache instance (MemoryStore + policy) with ttl + auto cache = middleware::app::make_default_cache({ + .ttl_ms = 30'000, + }); + + // Install middleware using injected cache + app.use("/api/", middleware::app::http_cache_mw({ + .prefix = "/api/", + .only_get = true, + .ttl_ms = 30'000, + + .allow_bypass = true, + .bypass_header = "x-vix-cache", + .bypass_value = "bypass", + + .vary_headers = {}, + .cache = cache, + + .add_debug_header = true, + .debug_header = "x-vix-cache-status", + })); + + register_routes(app); + + app.run(8080); + return 0; +} diff --git a/examples/cache/http_cache_app_debug.cpp b/examples/cache/http_cache_app_debug.cpp new file mode 100644 index 0000000..9756b60 --- /dev/null +++ b/examples/cache/http_cache_app_debug.cpp @@ -0,0 +1,74 @@ +// ============================================================================ +// http_cache_app_debug.cpp β€” HTTP Cache (Debug + Vary) +// ---------------------------------------------------------------------------- +// Run: +// vix run examples/http_cache_app_debug.cpp +// +// Test: +// curl -i "http://localhost:8080/api/users" +// curl -i "http://localhost:8080/api/users" # cached +// curl -i -H "Accept-Language: fr" "http://localhost:8080/api/users" +// curl -i -H "Accept-Language: en" "http://localhost:8080/api/users" # different cache key +// +// # 1) MISS +// curl -i "http://localhost:8080/api/users" + +// # 2) HIT +// curl -i "http://localhost:8080/api/users" + +// # 3) BYPASS (force origin) +// curl -i -H "x-vix-cache: bypass" "http://localhost:8080/api/users" +// Notes: +// - Adds debug header x-vix-cache-status (HIT/MISS/BYPASS depending on impl) +// - Demonstrates vary headers (Accept-Language) +// ============================================================================ + +#include +#include + +using namespace vix; + +static void register_routes(App &app) +{ + app.get("/", [](Request &, Response &res) + { res.text("home (not cached)"); }); + + app.get("/api/users", [](Request &req, Response &res) + { + // βœ… Request API you actually have: + // - req.has_header(name) + // - req.header(name) -> std::string + const std::string lang = + req.has_header("accept-language") ? req.header("accept-language") : "none"; + + // βœ… Response API you actually have: + // res.json(vix::json::kvs) or res.json({tokens...}) + res.status(200).json(vix::json::obj({ + "message", "users from origin", + "accept_language", lang + })); }); +} + +int main() +{ + App app; + + app.use("/api/", middleware::app::http_cache({ + .ttl_ms = 30'000, + .allow_bypass = true, + .bypass_header = "x-vix-cache", + .bypass_value = "bypass", + + // Create different cache entries per language header + .vary_headers = {"accept-language"}, + + // Useful for demo/learning + .add_debug_header = true, + .debug_header = "x-vix-cache-status", + })); + + register_routes(app); + + app.run(8080); + return 0; +} diff --git a/examples/cache/http_cache_app_simple.cpp b/examples/cache/http_cache_app_simple.cpp new file mode 100644 index 0000000..928759c --- /dev/null +++ b/examples/cache/http_cache_app_simple.cpp @@ -0,0 +1,47 @@ +// ============================================================================ +// http_cache_app_simple.cpp β€” HTTP Cache (Simple) +// ---------------------------------------------------------------------------- +// Run: +// vix run examples/http_cache_app_simple.cpp +// +// Test: +// curl -i "http://localhost:8080/api/users" +// curl -i "http://localhost:8080/api/users" # should be cached +// curl -i -H "x-vix-cache: bypass" "http://localhost:8080/api/users" # bypass +// +// Notes: +// - Caches GET /api/* for 30s +// - Bypass via header x-vix-cache: bypass or query api_key-style if supported +// ============================================================================ + +#include +#include + +using namespace vix; + +static void register_routes(App &app) +{ + app.get("/", [](Request &, Response &res) + { res.text("home (not cached)"); }); + + app.get("/api/users", [](Request &, Response &res) + { res.text("users from origin"); }); +} + +int main() +{ + App app; + + // Cache GET requests under /api/* + app.use("/api/", middleware::app::http_cache({ + .ttl_ms = 30'000, + .allow_bypass = true, + .bypass_header = "x-vix-cache", + .bypass_value = "bypass", + })); + + register_routes(app); + + app.run(8080); + return 0; +} diff --git a/examples/compression/compression_app_simple.cpp b/examples/compression/compression_app_simple.cpp new file mode 100644 index 0000000..cf0dc05 --- /dev/null +++ b/examples/compression/compression_app_simple.cpp @@ -0,0 +1,73 @@ +// ============================================================================ +// compression_app_simple.cpp β€” Compression middleware (App) example (Vix.cpp) +// ---------------------------------------------------------------------------- +// Run: +// vix run compression_app_simple.cpp +// +// Test: +// # No Accept-Encoding => Vary may still appear (add_vary=true), but no planned header +// curl -i http://localhost:8080/x +// +// # With Accept-Encoding and big enough body => Vary + (debug) X-Vix-Compression: planned +// curl -i -H "Accept-Encoding: gzip, br" http://localhost:8080/x +// +// # Small body (< min_size) => no planned header +// curl -i -H "Accept-Encoding: gzip" http://localhost:8080/small +// +// # Inspect only headers +// curl -s -D - -o /dev/null -H "Accept-Encoding: gzip, br" http://localhost:8080/x +// ============================================================================ + +#include +#include +#include + +#include +#include + +using namespace vix; + +static void print_help() +{ + std::cout + << "Vix Compression example running:\n" + << " http://localhost:8080/\n" + << " http://localhost:8080/x\n" + << " http://localhost:8080/small\n\n" + << "Try:\n" + << " curl -i http://localhost:8080/x\n" + << " curl -i -H \"Accept-Encoding: gzip, br\" http://localhost:8080/x\n" + << " curl -i -H \"Accept-Encoding: gzip\" http://localhost:8080/small\n"; +} + +int main() +{ + App app; + + // Install compression middleware globally + auto mw = vix::middleware::app::adapt_ctx( + vix::middleware::performance::compression({ + .min_size = 8, // same as smoke test + .add_vary = true, + .enabled = true, + })); + + app.use(std::move(mw)); + + app.get("/", [](Request &, Response &res) + { res.send("Compression middleware installed. Try /x with Accept-Encoding."); }); + + // Big body => should trigger "planned" (debug) if Accept-Encoding asks gzip/br + app.get("/x", [](Request &, Response &res) + { res.status(200).send(std::string(20, 'a')); }); + + // Small body => should NOT trigger "planned" + app.get("/small", [](Request &, Response &res) + { + res.status(200).send("aaaa"); // 4 bytes + }); + + print_help(); + app.run(8080); + return 0; +} diff --git a/examples/cors/cors_app_basic.cpp b/examples/cors/cors_app_basic.cpp new file mode 100644 index 0000000..b5e455d --- /dev/null +++ b/examples/cors/cors_app_basic.cpp @@ -0,0 +1,23 @@ +// ============================================================================ +// cors_app_basic.cpp β€” Basic CORS example (Vix.cpp) +// curl -i http://localhost:8080/api -H "Origin: https://example.com" +// ============================================================================ + +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.use("/api", middleware::app::cors_dev({"https://example.com"})); + + app.get("/api", [](Request &, Response &res) + { + res.header("X-Request-Id", "req_123"); + res.json({ "ok", true }); }); + + app.run(8080); +} diff --git a/examples/cors/cors_app_strict.cpp b/examples/cors/cors_app_strict.cpp new file mode 100644 index 0000000..b2f743f --- /dev/null +++ b/examples/cors/cors_app_strict.cpp @@ -0,0 +1,39 @@ +// ============================================================================ +// cors_app_strict.cpp β€” Strict CORS + controlled preflight (Vix.cpp) +// # allowed +// curl -i -X OPTIONS http://localhost:8080/api \ +// -H "Origin: https://example.com" \ +// -H "Access-Control-Request-Method: POST" +// +// # blocked +// curl -i -X OPTIONS http://localhost:8080/api \ +// -H "Origin: https://evil.com" \ +// -H "Access-Control-Request-Method: POST" +// ============================================================================ + +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // πŸ”’ Apply CORS only on /api prefix + app.use("/api", middleware::app::cors_dev({"https://example.com"})); + + // βœ… Explicit OPTIONS route (lets middleware answer preflight) + app.options("/api", [](Request &, Response &res) + { + // Optional debug marker (only if this handler executes) + res.header("X-OPTIONS-HIT", "1"); + res.status(204).send(); }); + + app.get("/api", [](Request &, Response &res) + { + res.header("X-Request-Id", "req_123"); + res.json({ "ok", true }); }); + + app.run(8080); +} diff --git a/examples/csrf/csrf_pipeline_demo.cpp b/examples/csrf/csrf_pipeline_demo.cpp new file mode 100644 index 0000000..206110b --- /dev/null +++ b/examples/csrf/csrf_pipeline_demo.cpp @@ -0,0 +1,82 @@ +// ============================================================================ +// csrf_pipeline_demo.cpp β€” CSRF pipeline demo (Vix.cpp) +// ---------------------------------------------------------------------------- +// Run: +// vix run csrf_pipeline_demo.cpp +// ============================================================================ + +#include +#include + +#include + +#include +#include + +using namespace vix::middleware; + +static vix::vhttp::RawRequest make_post(bool ok) +{ + namespace http = boost::beast::http; + + vix::vhttp::RawRequest req{http::verb::post, "/api/update", 11}; + req.set(http::field::host, "localhost"); + + // Cookie + header must match + req.set("Cookie", "csrf_token=abc"); + req.set("x-csrf-token", ok ? "abc" : "wrong"); + + req.body() = "x=1"; + req.prepare_payload(); + return req; +} + +int main() +{ + namespace http = boost::beast::http; + + // FAIL + { + auto raw = make_post(false); + http::response res; + + vix::vhttp::Request req(raw, {}); + vix::vhttp::ResponseWrapper w(res); + + HttpPipeline p; + p.use(vix::middleware::security::csrf()); // MiddlewareFn(Context&, Next) + + int final_calls = 0; + p.run(req, w, [&](Request &, Response &) + { + final_calls++; + w.ok().text("OK"); }); + + assert(final_calls == 0); + assert(res.result_int() == 403); + } + + // OK + { + auto raw = make_post(true); + http::response res; + + vix::vhttp::Request req(raw, {}); + vix::vhttp::ResponseWrapper w(res); + + HttpPipeline p; + p.use(vix::middleware::security::csrf()); + + int final_calls = 0; + p.run(req, w, [&](Request &, Response &) + { + final_calls++; + w.ok().text("OK"); }); + + assert(final_calls == 1); + assert(res.result_int() == 200); + } + + std::cout << "[OK] csrf pipeline demo\n"; + return 0; +} diff --git a/examples/csrf/csrf_strict_server.cpp b/examples/csrf/csrf_strict_server.cpp new file mode 100644 index 0000000..21f78d3 --- /dev/null +++ b/examples/csrf/csrf_strict_server.cpp @@ -0,0 +1,43 @@ +// ============================================================================ +// csrf_strict_server.cpp β€” CSRF middleware example (Vix.cpp) +// ---------------------------------------------------------------------------- +// Run: +// vix run csrf_strict_server.cpp +// +// Test: +// # 1) Get token (cookie) +// curl -i -c cookies.txt http://localhost:8080/api/csrf +// +// # 2) FAIL: missing header +// curl -i -b cookies.txt -X POST http://localhost:8080/api/update -d "x=1" +// +// # 3) FAIL: wrong token +// curl -i -b cookies.txt -X POST http://localhost:8080/api/update \ +// -H "x-csrf-token: wrong" -d "x=1" +// +// # 4) OK: header token matches cookie token +// curl -i -b cookies.txt -X POST http://localhost:8080/api/update \ +// -H "x-csrf-token: abc" -d "x=1" +// ============================================================================ + +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.use("/api", middleware::app::csrf_dev()); + + app.get("/api/csrf", [](Request &, Response &res) + { + res.header("Set-Cookie", "csrf_token=abc; Path=/; SameSite=Lax"); + res.json({ "csrf_token", "abc" }); }); + + app.post("/api/update", [](Request &, Response &res) + { res.json({"ok", true, "message", "CSRF passed βœ…"}); }); + + app.run(8080); +} diff --git a/examples/csrf/index.html b/examples/csrf/index.html new file mode 100644 index 0000000..2643a76 --- /dev/null +++ b/examples/csrf/index.html @@ -0,0 +1,115 @@ + + + + + Vix β€” CORS + CSRF demo + + + + + +

Vix β€” CORS + CSRF test

+ +

+ Backend: http://localhost:8080
+ Origin: http://localhost:5173 (or any static server) +

+ + + + + +
Ready.
+ + + + diff --git a/examples/csrf/security_cors_csrf_server.cpp b/examples/csrf/security_cors_csrf_server.cpp new file mode 100644 index 0000000..8b04f59 --- /dev/null +++ b/examples/csrf/security_cors_csrf_server.cpp @@ -0,0 +1,83 @@ +// ============================================================================ +// security_cors_csrf_server.cpp β€” CORS + CSRF (Vix.cpp) +// ---------------------------------------------------------------------------- +// Goal: +// - OPTIONS handled by CORS middleware (preflight) +// - POST protected by CSRF (cookie token must match header token) +// - Both middlewares only apply to "/api" prefix +// +// Run: +// vix run security_cors_csrf_server.cpp +// +// Tests: +// +// # Preflight (ALLOWED origin => 204 + CORS headers) +// curl -i -X OPTIONS http://localhost:8080/api/update \ +// -H "Origin: https://example.com" \ +// -H "Access-Control-Request-Method: POST" \ +// -H "Access-Control-Request-Headers: Content-Type, X-CSRF-Token" +// +// # Preflight (BLOCKED origin => 403) +// curl -i -X OPTIONS http://localhost:8080/api/update \ +// -H "Origin: https://evil.com" \ +// -H "Access-Control-Request-Method: POST" +// +// # Get CSRF cookie (sets csrf_token=abc) +// curl -i -c cookies.txt http://localhost:8080/api/csrf \ +// -H "Origin: https://example.com" +// +// # FAIL (missing header) +// curl -i -b cookies.txt -X POST http://localhost:8080/api/update \ +// -H "Origin: https://example.com" \ +// -d "x=1" +// +// # FAIL (wrong token) +// curl -i -b cookies.txt -X POST http://localhost:8080/api/update \ +// -H "Origin: https://example.com" \ +// -H "X-CSRF-Token: wrong" \ +// -d "x=1" +// +// # OK (correct token) +// curl -i -b cookies.txt -X POST http://localhost:8080/api/update \ +// -H "Origin: https://example.com" \ +// -H "X-CSRF-Token: abc" \ +// -d "x=1" +// ============================================================================ + +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // Apply on /api (order matters) + middleware::app::protect_prefix(app, "/api", + middleware::app::cors_dev({"https://example.com"})); + + // CSRF expects: cookie "csrf_token" and header "x-csrf-token" by default + middleware::app::protect_prefix(app, "/api", + middleware::app::csrf_dev("csrf_token", "x-csrf-token", false)); + // ou strict: + // middleware::app::protect_prefix(app, "/api", + // middleware::app::csrf_strict_dev("csrf_token", "x-csrf-token")); + + // Routes + app.get("/api/csrf", [](Request &, Response &res) + { + res.header("Set-Cookie", "csrf_token=abc; Path=/; SameSite=Lax"); + res.header("X-Request-Id", "req_123"); + res.json({ "csrf_token", "abc" }); }); + + app.post("/api/update", [](Request &, Response &res) + { + res.header("X-Request-Id", "req_456"); + res.json({ "ok", true, "message", "CORS βœ… + CSRF βœ…" }); }); + + app.get("/", [](Request &, Response &res) + { res.send("Welcome"); }); + + app.run(8080); +} diff --git a/examples/etag/etag_app_simple.cpp b/examples/etag/etag_app_simple.cpp new file mode 100644 index 0000000..f811109 --- /dev/null +++ b/examples/etag/etag_app_simple.cpp @@ -0,0 +1,50 @@ +// ============================================================================ +// etag_app_simple.cpp β€” ETag middleware example (Vix.cpp) +// ---------------------------------------------------------------------------- +// Run: +// vix run etag_app_simple.cpp +// +// Test: +// curl -i http://localhost:8080/x +// curl -i -H 'If-None-Match: ' http://localhost:8080/x +// curl -I http://localhost:8080/x +// ============================================================================ + +#include +#include +#include + +#include +#include + +using namespace vix; + +static void print_help() +{ + std::cout + << "Vix ETag example running:\n" + << " http://localhost:8080/x\n\n" + << "Try:\n" + << " curl -i http://localhost:8080/x\n" + << " curl -i -H 'If-None-Match: ' http://localhost:8080/x\n" + << " curl -I http://localhost:8080/x\n"; +} + +int main() +{ + App app; + + // Install ETag middleware globally + auto mw = vix::middleware::app::adapt_ctx( + vix::middleware::performance::etag({.weak = true, + .add_cache_control_if_missing = false, + .min_body_size = 1})); + app.use(std::move(mw)); + + app.head("/x", [](Request &, Response &res) + { res.status(200); }); + + print_help(); + app.run(8080); + return 0; +} diff --git a/examples/form/form_app_simple.cpp b/examples/form/form_app_simple.cpp new file mode 100644 index 0000000..df086da --- /dev/null +++ b/examples/form/form_app_simple.cpp @@ -0,0 +1,47 @@ +// ============================================================================ +// form_app_simple.cpp β€” Form parser (App) simple example (Vix.cpp) +// ---------------------------------------------------------------------------- +// Run: +// vix run form_app_simple.cpp +// +// Test: +// # OK +// curl -i -X POST "http://localhost:8080/form" \ +// -H "Content-Type: application/x-www-form-urlencoded" \ +// --data "a=1&b=hello+world" +// +// # Missing/invalid content-type -> 415 +// curl -i -X POST "http://localhost:8080/form" \ +// -H "Content-Type: text/plain" \ +// --data "a=1&b=hello+world" +// +// # Payload too large -> 413 (max_bytes demo) +// curl -i -X POST "http://localhost:8080/form" \ +// -H "Content-Type: application/x-www-form-urlencoded" \ +// --data "$(python3 - <<'PY'\nprint('a=' + 'x'*200)\nPY)" +// ============================================================================ + +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.use("/form", middleware::app::form_dev(128)); + + app.get("/", [](Request &, Response &res) + { res.send("POST /form (application/x-www-form-urlencoded)"); }); + + app.post("/form", [](Request &req, Response &res) + { + auto& fb = req.state(); + + auto it = fb.fields.find("b"); + res.send(it == fb.fields.end() ? "" : it->second); }); + + app.run(8080); + return 0; +} diff --git a/examples/form/json_app_simple.cpp b/examples/form/json_app_simple.cpp new file mode 100644 index 0000000..7b2d7ce --- /dev/null +++ b/examples/form/json_app_simple.cpp @@ -0,0 +1,65 @@ +// ============================================================================ +// json_app_simple.cpp β€” JSON parser (App) simple example (Vix.cpp) +// ---------------------------------------------------------------------------- +// Run: +// vix run json_app_simple.cpp +// +// Test: +// # OK +// curl -i -X POST http://localhost:8080/json \ +// -H "Content-Type: application/json; charset=utf-8" \ +// --data '{"x":1}' +// +// # Invalid JSON -> 400 +// curl -i -X POST http://localhost:8080/json \ +// -H "Content-Type: application/json" \ +// --data '{"x":}' +// +// # Invalid content-type -> 415 +// curl -i -X POST http://localhost:8080/json \ +// -H "Content-Type: text/plain" \ +// --data '{"x":1}' +// +// # Empty body (allowed here) -> 200 + {} +// curl -i -X POST http://localhost:8080/json \ +// -H "Content-Type: application/json" \ +// --data "" +// +// # Payload too large -> 413 (max_bytes demo) +// BIG="$(python3 -c 'print("{\"x\":\"" + "a"*300 + "\"}")')" +// curl -i -X POST http://localhost:8080/json \ +// -H "Content-Type: application/json" \ +// --data "$BIG" +// ============================================================================ + +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // 1-liner like Node/FastAPI + app.use("/json", middleware::app::json_dev( + /*max_bytes=*/256, + /*allow_empty=*/true, + /*require_content_type=*/true)); + + app.get("/", [](Request &, Response &res) + { res.send("POST /json with application/json"); }); + + app.post("/json", [](Request &req, Response &res) + { + auto &jb = req.state(); + + // keep it simple: just echo the parsed JSON + res.json({ + "ok", true, + "raw", jb.value.dump() + }); }); + + app.run(8080); + return 0; +} diff --git a/examples/form/json_app_strict.cpp b/examples/form/json_app_strict.cpp new file mode 100644 index 0000000..b04384d --- /dev/null +++ b/examples/form/json_app_strict.cpp @@ -0,0 +1,63 @@ +// ============================================================================ +// json_app_strict.cpp β€” JSON parser (App) strict example (Vix.cpp) +// ---------------------------------------------------------------------------- +// Run: +// vix run json_app_strict.cpp +// +// Test: +// # OK +// curl -i -X POST http://localhost:8080/json \ +// -H "Content-Type: application/json; charset=utf-8" \ +// --data '{"x":1}' +// +// # Empty body -> 400 (empty_body) +// curl -i -X POST http://localhost:8080/json \ +// -H "Content-Type: application/json" \ +// --data "" +// +// # Invalid JSON -> 400 (invalid_json) +// curl -i -X POST http://localhost:8080/json \ +// -H "Content-Type: application/json" \ +// --data '{"x":}' +// +// # Invalid content-type -> 415 +// curl -i -X POST http://localhost:8080/json \ +// -H "Content-Type: text/plain" \ +// --data '{"x":1}' +// +// # Payload too large -> 413 (max_bytes demo) +// BIG="$(python3 -c 'print("{\"x\":\"" + "a"*300 + "\"}")')" +// curl -i -X POST http://localhost:8080/json \ +// -H "Content-Type: application/json" \ +// --data "$BIG" +// ============================================================================ +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // STRICT JSON: Content-Type required + body required (allow_empty=false) + app.use("/json", middleware::app::json_dev( + /*max_bytes=*/256, + /*allow_empty=*/false, + /*require_content_type=*/true)); + + app.get("/", [](Request &, Response &res) + { res.send("POST /json requires a non-empty JSON body."); }); + + app.post("/json", [](Request &req, Response &res) + { + auto &jb = req.state(); + + if (jb.value.contains("x")) + res.status(200).send(jb.value["x"].dump()); + else + res.status(200).send(jb.value.dump()); }); + + app.run(8080); + return 0; +} diff --git a/examples/form/multipart_app_simple.cpp b/examples/form/multipart_app_simple.cpp new file mode 100644 index 0000000..60883c6 --- /dev/null +++ b/examples/form/multipart_app_simple.cpp @@ -0,0 +1,54 @@ +// ============================================================================ +// multipart_app_simple.cpp β€” Multipart parser (App) simple example (Vix.cpp) +// ---------------------------------------------------------------------------- +// Run: +// vix run multipart_app_simple.cpp +// +// Test: +// # OK (multipart with boundary) +// curl -i -X POST "http://localhost:8080/mp" \ +// -F "a=1" -F "b=hello" +// +// # Missing boundary (force a raw header without boundary) -> 400 +// curl -i -X POST "http://localhost:8080/mp" \ +// -H "Content-Type: multipart/form-data" \ +// --data "x" +// +// # Invalid content-type -> 415 +// curl -i -X POST "http://localhost:8080/mp" \ +// -H "Content-Type: text/plain" \ +// --data "x" +// +// # Payload too large -> 413 (max_bytes demo) +// BIG="$(python3 -c 'print(\"x\"*300)')" +// curl -i -X POST "http://localhost:8080/mp" \ +// -H "Content-Type: multipart/form-data; boundary=----X" \ +// --data "----X\r\n${BIG}\r\n----X--\r\n" +// ============================================================================ + +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.use("/mp", middleware::app::cors_dev()); + app.use("/mp", middleware::app::multipart_save_dev("uploads")); + + app.options("/mp", [](Request &, Response &res) + { res.status(204).send(""); }); + + app.get("/", [](Request &, Response &res) + { res.send("POST /mp multipart/form-data (saves files to ./uploads/)"); }); + + app.post("/mp", [](Request &req, Response &res) + { + auto &form = req.state(); + res.json(middleware::app::multipart_json(form)); }); + + app.run(8080); + return 0; +} \ No newline at end of file diff --git a/examples/group_builder/group_app_example.cpp b/examples/group_builder/group_app_example.cpp new file mode 100644 index 0000000..fbc65e6 --- /dev/null +++ b/examples/group_builder/group_app_example.cpp @@ -0,0 +1,87 @@ +// ============================================================================ +// group_app_example.cpp β€” Groups + protect() demo (Vix.cpp) +// ---------------------------------------------------------------------------- +// Run: +// vix run group_app_example.cpp +// +// Test: +// curl -i http://localhost:8080/ +// curl -i http://localhost:8080/api/public +// curl -i http://localhost:8080/api/secure +// curl -i -H "x-api-key: secret" http://localhost:8080/api/secure +// +// NOTE: +// /api/admin/dashboard uses JWT + RBAC (admin role). +// ============================================================================ + +#include +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // Root + app.get("/", [](Request &, Response &res) + { res.send("Welcome. Try /api/public, /api/secure, /api/admin/dashboard"); }); + + // GROUP: /api + app.group("/api", [&](App::Group &api) + { + // Public API + api.get("/public", [](Request &, Response &res) + { + res.send("Public API endpoint"); + }); + + // Protect /api/secure with API key + api.protect("/secure", middleware::app::api_key_dev("secret")); + + api.get("/secure", [](Request &req, Response &res) + { + auto &k = req.state(); + res.json({ + "ok", true, + "api_key", k.value + }); + }); + + // Nested group: /api/admin (JWT + RBAC) + api.group("/admin", [&](App::Group &admin) + { + // Apply auth to the whole group + admin.use(middleware::app::jwt_auth("dev_secret")); + admin.use(middleware::app::rbac_admin()); // role=admin + + admin.get("/dashboard", [](Request &req, Response &res) + { + auto &authz = req.state(); + res.json({ + "ok", true, + "sub", authz.subject, + "role", "admin" + }); + }); + }); }); + + // Help + std::cout + << "Vix Groups example running:\n" + << " http://localhost:8080/\n" + << " http://localhost:8080/api/public\n" + << " http://localhost:8080/api/secure\n" + << " http://localhost:8080/api/admin/dashboard\n\n" + << "API KEY:\n" + << " secret\n\n" + << "Try:\n" + << " curl -i http://localhost:8080/api/public\n" + << " curl -i http://localhost:8080/api/secure\n" + << " curl -i -H \"x-api-key: secret\" http://localhost:8080/api/secure\n" + << " curl -i \"http://localhost:8080/api/secure?api_key=secret\"\n"; + + app.run(8080); + return 0; +} diff --git a/examples/group_builder/group_builder_example.cpp b/examples/group_builder/group_builder_example.cpp new file mode 100644 index 0000000..0956478 --- /dev/null +++ b/examples/group_builder/group_builder_example.cpp @@ -0,0 +1,49 @@ +// ============================================================================ +// group_builder_example.cpp β€” group() builder style (Vix.cpp) +// ---------------------------------------------------------------------------- +// Run: +// vix run group_builder_example.cpp +// +// Test: +// curl -i http://localhost:8080/api/public +// curl -i http://localhost:8080/api/secure +// curl -i -H "x-api-key: secret" http://localhost:8080/api/secure +// curl -i "http://localhost:8080/api/secure?api_key=secret" +// ============================================================================ +#include +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // Create /api group + auto api = app.group("/api"); + + // Public endpoint + api.get("/public", [](Request &, Response &res) + { res.send("Public endpoint"); }); + + // πŸ” Protect all following /api routes with API key (DEV preset) + api.use(middleware::app::api_key_dev("secret")); + + // Secure endpoint + api.get("/secure", [](Request &req, Response &res) + { + auto &k = req.state(); + res.json({ + "ok", true, + "api_key", k.value + }); }); + + std::cout + << "Running:\n" + << " http://localhost:8080/api/public\n" + << " http://localhost:8080/api/secure\n"; + + app.run(8080); + return 0; +} diff --git a/examples/headers/headers_pipeline_demo.cpp b/examples/headers/headers_pipeline_demo.cpp new file mode 100644 index 0000000..acb6511 --- /dev/null +++ b/examples/headers/headers_pipeline_demo.cpp @@ -0,0 +1,62 @@ +// ============================================================================ +// headers_pipeline_demo.cpp β€” Security headers pipeline demo (Vix.cpp) +// ---------------------------------------------------------------------------- +// Run: +// vix run headers_pipeline_demo.cpp +// ============================================================================ + +#include +#include + +#include +#include +#include + +using namespace vix::middleware; + +static vix::vhttp::RawRequest make_req() +{ + namespace http = boost::beast::http; + vix::vhttp::RawRequest req{http::verb::get, "/x", 11}; + req.set(http::field::host, "localhost"); + req.prepare_payload(); + return req; +} + +int main() +{ + namespace http = boost::beast::http; + + auto raw = make_req(); + http::response res; + + vix::vhttp::Request req(raw, {}); + vix::vhttp::ResponseWrapper w(res); + + HttpPipeline p; + + // Default headers() adds: + // - X-Content-Type-Options: nosniff + // - X-Frame-Options: DENY + // - Referrer-Policy: no-referrer + // - Permissions-Policy: ... + p.use(vix::middleware::security::headers()); + + int final_calls = 0; + p.run(req, w, [&](Request &, Response &) + { + final_calls++; + w.ok().text("OK"); }); + + assert(final_calls == 1); + assert(res.result_int() == 200); + + // Must exist + assert(res.find("X-Content-Type-Options") != res.end()); + assert(res.find("X-Frame-Options") != res.end()); + assert(res.find("Referrer-Policy") != res.end()); + assert(res.find("Permissions-Policy") != res.end()); + + std::cout << "[OK] security headers pipeline demo\n"; + return 0; +} diff --git a/examples/headers/index.html b/examples/headers/index.html new file mode 100644 index 0000000..1d61b4c --- /dev/null +++ b/examples/headers/index.html @@ -0,0 +1,167 @@ + + + + + + Vix Security Demo β€” CORS + CSRF + Headers + + + +

CORS + CSRF + Security Headers (Vix.cpp)

+ +

+ Server: http://localhost:8080 (must be running)
+ This page should be served from another origin (ex: + http://localhost:5173). +

+ +
+ + + + +
+ +
Ready.
+ + + + diff --git a/examples/headers/security_cors_csrf_headers_server.cpp b/examples/headers/security_cors_csrf_headers_server.cpp new file mode 100644 index 0000000..488a6e8 --- /dev/null +++ b/examples/headers/security_cors_csrf_headers_server.cpp @@ -0,0 +1,94 @@ +// ============================================================================ +// security_cors_csrf_headers_server.cpp β€” CORS + CSRF + Security Headers (Vix.cpp) +// ---------------------------------------------------------------------------- +// Goal (realistic app): +// - OPTIONS handled by CORS middleware (preflight) +// - POST protected by CSRF (cookie token must match header token) +// - Security headers added on ALL /api responses (including errors) +// +// Run: +// vix run security_cors_csrf_headers_server.cpp +// +// Terminal tests (curl): +// +// # 1) Preflight allowed (204 + CORS headers) +// curl -i -X OPTIONS http://localhost:8080/api/update \ +// -H "Origin: https://example.com" \ +// -H "Access-Control-Request-Method: POST" \ +// -H "Access-Control-Request-Headers: Content-Type, X-CSRF-Token" +// +// # 2) Preflight blocked (403) +// curl -i -X OPTIONS http://localhost:8080/api/update \ +// -H "Origin: https://evil.com" \ +// -H "Access-Control-Request-Method: POST" +// +// # 3) Get CSRF cookie (sets csrf_token=abc) +// curl -i -c cookies.txt http://localhost:8080/api/csrf \ +// -H "Origin: https://example.com" +// +// # 4) FAIL: missing CSRF header +// curl -i -b cookies.txt -X POST http://localhost:8080/api/update \ +// -H "Origin: https://example.com" \ +// -d "x=1" +// +// # 5) FAIL: wrong token +// curl -i -b cookies.txt -X POST http://localhost:8080/api/update \ +// -H "Origin: https://example.com" \ +// -H "X-CSRF-Token: wrong" \ +// -d "x=1" +// +// # 6) OK: correct token +// curl -i -b cookies.txt -X POST http://localhost:8080/api/update \ +// -H "Origin: https://example.com" \ +// -H "X-CSRF-Token: abc" \ +// -d "x=1" +// +// Browser demo: +// - Serve index.html via any static server on another port (ex: 5173) +// - Open it and click buttons. +// ============================================================================ +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // Apply on ALL /api/* + // Order matters: headers first, then CORS, then CSRF. + app.use("/api", middleware::app::security_headers_dev()); // HSTS off by default + app.use("/api", middleware::app::cors_dev({ + "http://localhost:5173", + "http://0.0.0.0:5173", + "https://example.com" // for your curl tests + })); + app.use("/api", middleware::app::csrf_dev("csrf_token", "x-csrf-token", false)); + + // Explicit OPTIONS routes (lets CORS middleware answer preflight) + app.options("/api/update", [](Request &, Response &res) + { res.status(204).send(); }); + + app.options("/api/csrf", [](Request &, Response &res) + { res.status(204).send(); }); + + // Routes + app.get("/api/csrf", [](Request &, Response &res) + { + // For cross-origin cookie in browsers: HTTPS + SameSite=None; Secure + // For local dev HTTP: SameSite=Lax is fine but cookie might not be sent cross-site. + res.header("Set-Cookie", "csrf_token=abc; Path=/; SameSite=Lax"); + res.header("X-Request-Id", "req_csrf_1"); + res.json({"csrf_token", "abc"}); }); + + app.post("/api/update", [](Request &, Response &res) + { + res.header("X-Request-Id", "req_update_1"); + res.json({"ok", true, "message", "CORS βœ… + CSRF βœ… + HEADERS βœ…"}); }); + + app.get("/", [](Request &, Response &res) + { res.send("public route"); }); + + app.run(8080); +} diff --git a/examples/headers/security_headers_server.cpp b/examples/headers/security_headers_server.cpp new file mode 100644 index 0000000..2ee773b --- /dev/null +++ b/examples/headers/security_headers_server.cpp @@ -0,0 +1,36 @@ +// ============================================================================ +// security_headers_server.cpp β€” Security headers middleware example (Vix.cpp) +// ---------------------------------------------------------------------------- +// Goal: +// - Apply security headers only on /api prefix +// - Keep / public route without forced headers (demo) +// +// Run: +// vix run security_headers_server.cpp +// +// Tests: +// curl -i http://localhost:8080/api/ping +// curl -i http://localhost:8080/ +// ============================================================================ + +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // πŸ”’ Apply security headers only on /api + app.use("/api", middleware::app::security_headers_dev()); + + app.get("/api/ping", [](Request &, Response &res) + { res.json({"ok", true, "message", "headers applied βœ…"}); }); + + // Public route (no forced headers) + app.get("/", [](Request &, Response &res) + { res.send("public route"); }); + + app.run(8080); +} diff --git a/examples/ip_filter/ip_filter_pipeline_demo.cpp b/examples/ip_filter/ip_filter_pipeline_demo.cpp new file mode 100644 index 0000000..5512502 --- /dev/null +++ b/examples/ip_filter/ip_filter_pipeline_demo.cpp @@ -0,0 +1,112 @@ +// ============================================================================ +// ip_filter_pipeline_demo.cpp β€” IP filter pipeline demo (Vix.cpp) +// ---------------------------------------------------------------------------- +// Run: +// vix run ip_filter_pipeline_demo.cpp +// ============================================================================ + +#include +#include +#include + +#include +#include +#include + +using namespace vix::middleware; + +static vix::vhttp::RawRequest make_req(std::string ip) +{ + namespace http = boost::beast::http; + vix::vhttp::RawRequest req{http::verb::get, "/x", 11}; + req.set(http::field::host, "localhost"); + req.set("X-Forwarded-For", std::move(ip)); + req.prepare_payload(); + return req; +} + +int main() +{ + namespace http = boost::beast::http; + + // ----------------------------- + // Case 1: deny list blocks + // ----------------------------- + { + auto raw = make_req("1.2.3.4"); + http::response res; + + vix::vhttp::Request req(raw, {}); + vix::vhttp::ResponseWrapper w(res); + + vix::middleware::security::IpFilterOptions opt; + opt.deny = {"1.2.3.4"}; + + HttpPipeline p; + p.use(vix::middleware::security::ip_filter(opt)); + + int final_calls = 0; + p.run(req, w, [&](Request &, Response &) + { + final_calls++; + w.ok().text("OK"); }); + + assert(final_calls == 0); + assert(res.result_int() == 403); + } + + // ----------------------------- + // Case 2: allow list lets through + // ----------------------------- + { + auto raw = make_req("9.9.9.9"); + http::response res; + + vix::vhttp::Request req(raw, {}); + vix::vhttp::ResponseWrapper w(res); + + vix::middleware::security::IpFilterOptions opt; + opt.allow = {"9.9.9.9"}; + + HttpPipeline p; + p.use(vix::middleware::security::ip_filter(opt)); + + int final_calls = 0; + p.run(req, w, [&](Request &, Response &) + { + final_calls++; + w.ok().text("OK"); }); + + assert(final_calls == 1); + assert(res.result_int() == 200); + } + + // ----------------------------- + // Case 3: allow list blocks others + // ----------------------------- + { + auto raw = make_req("2.2.2.2"); + http::response res; + + vix::vhttp::Request req(raw, {}); + vix::vhttp::ResponseWrapper w(res); + + vix::middleware::security::IpFilterOptions opt; + opt.allow = {"9.9.9.9"}; + + HttpPipeline p; + p.use(vix::middleware::security::ip_filter(opt)); + + int final_calls = 0; + p.run(req, w, [&](Request &, Response &) + { + final_calls++; + w.ok().text("OK"); }); + + assert(final_calls == 0); + assert(res.result_int() == 403); + } + + std::cout << "[OK] ip_filter pipeline demo\n"; + return 0; +} diff --git a/examples/ip_filter/ip_filter_server.cpp b/examples/ip_filter/ip_filter_server.cpp new file mode 100644 index 0000000..c84549a --- /dev/null +++ b/examples/ip_filter/ip_filter_server.cpp @@ -0,0 +1,64 @@ +// ============================================================================ +// ip_filter_server.cpp β€” IP filter middleware example (Vix.cpp) +// ---------------------------------------------------------------------------- +// Goal: +// - Protect /api/* using ip_filter() +// - Client IP extracted from X-Forwarded-For (first value) +// - Demonstrate deny + allow behavior +// +// Run: +// vix run ip_filter_server.cpp +// +// Tests: +// +// # Public route (no middleware) +// curl -i http://localhost:8080/ +// +// # Allowed IP (in allow list) +// curl -i http://localhost:8080/api/hello -H "X-Forwarded-For: 10.0.0.1" +// +// # Not allowed (not in allow list) +// curl -i http://localhost:8080/api/hello -H "X-Forwarded-For: 1.2.3.4" +// +// # Denied explicitly (deny wins if you configure both) +// curl -i http://localhost:8080/api/hello -H "X-Forwarded-For: 9.9.9.9" +// +// # X-Forwarded-For with chain: "client, proxy1, proxy2" +// curl -i http://localhost:8080/api/hello -H "X-Forwarded-For: 10.0.0.1, 127.0.0.1" +// ============================================================================ + +// ============================================================================ +// ip_filter_server.cpp β€” IP filter middleware example (Vix.cpp) +// ---------------------------------------------------------------------------- +// Run: +// vix run ip_filter_server.cpp +// ============================================================================ +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // Apply on /api/* + app.use("/api", middleware::app::ip_filter_allow_deny_dev( + "x-forwarded-for", + {"10.0.0.1", "127.0.0.1"}, // allow + {"9.9.9.9"}, // deny (priority) + true // fallback to x-real-ip, etc. + )); + + // Routes + app.get("/", [](Request &, Response &res) + { res.send("public route"); }); + + app.get("/api/hello", [](Request &req, Response &res) + { res.json({"ok", true, + "message", "Hello from /api/hello", + "x_forwarded_for", req.header("x-forwarded-for"), + "x_real_ip", req.header("x-real-ip")}); }); + + app.run(8080); +} diff --git a/examples/main.cpp b/examples/main.cpp index 349c89f..d34b497 100644 --- a/examples/main.cpp +++ b/examples/main.cpp @@ -13,6 +13,16 @@ int main() { App app; + app.get("/", [](auto &, auto &res) + { + res.send("ok"); // light + }); + + app.get_heavy("/users", [](auto &, auto &res) + { + // DB query (heavy) -> executor + res.send("users"); }); + app.get("/users/{id}", [](Request &req, Response &res) { auto id = req.param("id"); diff --git a/examples/middleware_http/http_cache_example.cpp b/examples/middleware_http/http_cache_example.cpp new file mode 100644 index 0000000..8804c19 --- /dev/null +++ b/examples/middleware_http/http_cache_example.cpp @@ -0,0 +1,27 @@ +#include +#include + +using namespace vix; + +int main() +{ + App app; + + app.use("/api", middleware::app::http_cache({.ttl_ms = 30'000, + .allow_bypass = true, + .bypass_header = "x-vix-cache", + .bypass_value = "bypass"})); + + app.get("/api/users", [](Request &req, Response &res) + { res.json({"ok", true, + "page", req.query_value("page", "1"), + "source", "origin"}); }); + + app.get("/", [](Request, Response res) + { res.send("Welcome !"); }); + + app.get("/hello", [](Request, Response res) + { res.status(200).send("Hello, World"); }); + + app.run(8080); +} diff --git a/examples/rate_limit/index.html b/examples/rate_limit/index.html new file mode 100644 index 0000000..5680a2e --- /dev/null +++ b/examples/rate_limit/index.html @@ -0,0 +1,195 @@ + + + + + + Vix Security Demo β€” CORS + IP Filter + Rate Limit + + + + + + +

Vix.cpp β€” Security Middleware Demo

+ +

+ API server: http://localhost:8080
+ This page must be served from another origin (ex: + http://0.0.0.0:5173 or http://localhost:5173) +

+ +
+ + + + + +
+ +
Ready.
+ + + + diff --git a/examples/rate_limit/rate_limit_pipeline_demo.cpp b/examples/rate_limit/rate_limit_pipeline_demo.cpp new file mode 100644 index 0000000..6d75e6e --- /dev/null +++ b/examples/rate_limit/rate_limit_pipeline_demo.cpp @@ -0,0 +1,84 @@ +// ============================================================================ +// rate_limit_pipeline_demo.cpp β€” Rate limit pipeline demo (Vix.cpp) +// ---------------------------------------------------------------------------- +// Run: +// vix run rate_limit_pipeline_demo.cpp +// ============================================================================ +#include +#include +#include + +#include + +#include +#include + +using namespace vix::middleware; + +static vix::vhttp::RawRequest make_req() +{ + namespace http = boost::beast::http; + vix::vhttp::RawRequest req{http::verb::get, "/api/x", 11}; + req.set(http::field::host, "localhost"); + req.set("x-forwarded-for", "1.2.3.4"); + req.prepare_payload(); + return req; +} + +int main() +{ + namespace http = boost::beast::http; + + vix::middleware::security::RateLimitOptions opt{}; + opt.capacity = 2.0; + opt.refill_per_sec = 0.0; + opt.add_headers = true; + + HttpPipeline p; + + auto shared = std::make_shared(); + p.services().provide(shared); + + p.use(vix::middleware::security::rate_limit(opt)); + + auto run_once = [&](http::response &res) + { + auto raw = make_req(); + vix::vhttp::Request req(raw, {}); + vix::vhttp::ResponseWrapper w(res); + + p.run(req, w, [&](Request &, Response &) + { w.ok().text("OK"); }); + }; + + // 1) OK + { + http::response res; + run_once(res); + assert(res.result_int() == 200); + assert(res.body() == "OK"); + assert(!res["X-RateLimit-Limit"].empty()); + assert(!res["X-RateLimit-Remaining"].empty()); + } + + // 2) OK + { + http::response res; + run_once(res); + assert(res.result_int() == 200); + assert(res.body() == "OK"); + } + + // 3) BLOCKED + { + http::response res; + run_once(res); + assert(res.result_int() == 429); + assert(res.body().find("rate_limited") != std::string::npos); + assert(!res["Retry-After"].empty()); + assert(res["X-RateLimit-Remaining"] == "0"); + } + + std::cout << "[OK] rate_limit pipeline demo\n"; + return 0; +} diff --git a/examples/rate_limit/rate_limit_server.cpp b/examples/rate_limit/rate_limit_server.cpp new file mode 100644 index 0000000..e2e4f30 --- /dev/null +++ b/examples/rate_limit/rate_limit_server.cpp @@ -0,0 +1,30 @@ +// ============================================================================ +// rate_limit_server.cpp β€” Rate limit server demo (Vix.cpp) +// ---------------------------------------------------------------------------- +// Run: +// vix run rate_limit_server.cpp +// +// Endpoints: +// GET / (public) +// GET /api/ping (rate limited) +// ============================================================================ +#include +#include + +using namespace vix; + +int main() +{ + App app; + + // burst=5, refill=0 => easy to trigger + app.use("/api", middleware::app::rate_limit_custom_dev(5.0, 0.0)); + + app.get("/", [](Request &, Response &res) + { res.send("public route"); }); + + app.get("/api/ping", [](Request &req, Response &res) + { res.json({"ok", true, "msg", "pong", "xff", req.header("x-forwarded-for")}); }); + + app.run(8080); +} diff --git a/examples/rate_limit/security_cors_ip_rate_server.cpp b/examples/rate_limit/security_cors_ip_rate_server.cpp new file mode 100644 index 0000000..c47904f --- /dev/null +++ b/examples/rate_limit/security_cors_ip_rate_server.cpp @@ -0,0 +1,112 @@ +// ============================================================================ +// security_cors_ip_rate_server.cpp β€” CORS + IP Filter + Rate Limit (Vix.cpp) +// ---------------------------------------------------------------------------- +// FIX: +// - Add explicit OPTIONS routes so middleware can attach CORS headers. +// - Use X-Vix-Ip consistently for browser demo. +// - Rate limit key uses x-vix-ip. +// ---------------------------------------------------------------------------- +// Run: +// vix run security_cors_ip_rate_server.cpp +// ============================================================================ +#include +#include + +using namespace vix; + +static void register_options_routes(App &app) +{ + // Without explicit OPTIONS routes, Vix core may auto-return 204 + // BEFORE middleware runs => preflight has no CORS headers. + + app.options("/api/ping", [](Request &, Response &res) + { + res.header("X-OPTIONS-HIT", "ping"); + res.status(204).send(); }); + + app.options("/api/echo", [](Request &, Response &res) + { + res.header("X-OPTIONS-HIT", "echo"); + res.status(204).send(); }); +} + +int main() +{ + App app; + + // --------------------------------------------------------------------- + // Apply all on /api prefix (ORDER MATTERS) + // --------------------------------------------------------------------- + app.use("/api", middleware::app::cors_ip_demo()); + app.use("/api", middleware::app::ip_filter_dev("x-vix-ip", {"1.2.3.4"})); + app.use("/api", middleware::app::rate_limit_custom_dev( + 5.0, // capacity (burst) + 0.0, // refill_per_sec (demo: easy to trigger) + "x-vix-ip" // key header (must match IP filter header) + )); + + // Public + app.get("/", [](Request &, Response &res) + { res.send("public route"); }); + + // Critical for browser preflight headers + register_options_routes(app); + + app.get("/api/ping", [](Request &req, Response &res) + { + res.header("X-Request-Id", "req_ping_1"); + res.json({ + "ok", true, + "msg", "pong", + "ip", req.header("x-vix-ip") + }); }); + + app.post("/api/echo", [](Request &req, Response &res) + { + res.header("X-Request-Id", "req_echo_1"); + res.json({ + "ok", true, + "msg", "echo", + "content_type", req.header("content-type") + }); }); + + app.run(8080); +} + +/* +=============================================================================== +CURL TESTS +=============================================================================== + +# 0) Run server +vix run security_cors_ip_rate_server.cpp + +# 1) Preflight allowed origin (should be 204 + Access-Control-Allow-Origin) +curl -i -X OPTIONS http://localhost:8080/api/echo \ + -H "Origin: http://localhost:5173" \ + -H "Access-Control-Request-Method: POST" \ + -H "Access-Control-Request-Headers: Content-Type" + +# 2) Preflight blocked origin (should be 403 from CORS middleware) +curl -i -X OPTIONS http://localhost:8080/api/echo \ + -H "Origin: https://evil.com" \ + -H "Access-Control-Request-Method: POST" + +# 3) IP denied (deny=1.2.3.4) => 403 ip_denied +curl -i http://localhost:8080/api/ping \ + -H "Origin: http://localhost:5173" \ + -H "X-Forwarded-For: 1.2.3.4" + +# 4) Allowed IP (not denied) => 200 OK +curl -i http://localhost:8080/api/ping \ + -H "Origin: http://localhost:5173" \ + -H "X-Forwarded-For: 9.9.9.9" + +# 5) Rate limit demo (capacity=5, refill=0): 6th request => 429 +for i in $(seq 1 6); do + echo "---- $i" + curl -i http://localhost:8080/api/ping \ + -H "Origin: http://localhost:5173" \ + -H "X-Forwarded-For: 9.9.9.9" +done +*/ diff --git a/examples/static_files/static_files_app_simple.cpp b/examples/static_files/static_files_app_simple.cpp new file mode 100644 index 0000000..0dff971 --- /dev/null +++ b/examples/static_files/static_files_app_simple.cpp @@ -0,0 +1,51 @@ +#include + +#include +#include + +#include +#include +#include + +using namespace vix; + +static std::filesystem::path source_dir() +{ + // __FILE__ = /home/softadastra/dev/tmp/static_files_app_simple.cpp + return std::filesystem::path(__FILE__).parent_path(); +} + +int main() +{ + // Dossier public Γ  cΓ΄tΓ© du .cpp (stable mΓͺme si vix run change le cwd) + std::filesystem::path root = source_dir() / "public"; + std::filesystem::create_directories(root); + + // Fichiers de test + { + std::ofstream(root / "index.html") << "

OK

"; + } + { + std::ofstream(root / "hello.txt") << "hello"; + } + + App app; + + // Adapter Pipeline middleware -> App middleware + app.use(vix::middleware::app::adapt_ctx( + vix::middleware::performance::static_files( + root, + { + .mount = "/", + .index_file = "index.html", + .add_cache_control = true, + .cache_control = "public, max-age=3600", + .fallthrough = true, + }))); + + app.get("/api/ping", [](Request &, Response &res) + { res.json({"ok", true}); }); + + std::cout << "Static root: " << root << "\n"; + app.run(8080); +} diff --git a/modules/cli b/modules/cli index 1f1f683..4d951b8 160000 --- a/modules/cli +++ b/modules/cli @@ -1 +1 @@ -Subproject commit 1f1f6835d7152cd72f0141725d7dfc0b0e61ce67 +Subproject commit 4d951b8a2bcc10048c2f23bacb58af4e57746b5e diff --git a/modules/core b/modules/core index 9529028..e1462a0 160000 --- a/modules/core +++ b/modules/core @@ -1 +1 @@ -Subproject commit 95290285a369d29d4003f2f628c22f56136c8149 +Subproject commit e1462a0dd2416df2b92a1b9c79c159692fbcd117 diff --git a/modules/middleware b/modules/middleware index 648bc61..2121a23 160000 --- a/modules/middleware +++ b/modules/middleware @@ -1 +1 @@ -Subproject commit 648bc6169147b5f68cda5974c8233b982a114569 +Subproject commit 2121a238bcf1e4047b04cea084e676aa4bc0dc2d diff --git a/modules/utils b/modules/utils index c27e8b9..f0b7656 160000 --- a/modules/utils +++ b/modules/utils @@ -1 +1 @@ -Subproject commit c27e8b94bc9ba98324ca5a38f9d41b473e78bc54 +Subproject commit f0b76563ec2bc0a3d0ecd89c74610b3694cf66ba diff --git a/modules/websocket b/modules/websocket index 4f0cabe..e85047f 160000 --- a/modules/websocket +++ b/modules/websocket @@ -1 +1 @@ -Subproject commit 4f0cabe5b17c95a3dab10c879cd23b8d9f8a3140 +Subproject commit e85047fda91162e3442c9fb3c9e500e818c1ef6d