From 6113baf7e2a00f2793bea735aaecbd49b028d7f4 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Thu, 3 Sep 2026 19:05:28 +0300 Subject: [PATCH 1/3] Rename project --- brazier/.env.example | 18 + brazier/.gitignore | 9 + brazier/CMakeLists.txt | 239 +++++++ brazier/CMakePresets.json | 14 + brazier/app/Main.cpp | 46 ++ brazier/build.bat | 40 ++ brazier/build.sh | 43 ++ brazier/cmake/lightlibConfig.cmake | 15 + brazier/config_test.json | 18 + .../App/Http/Controllers/Controller.hpp | 51 ++ .../include/brazier/App/Http/Helpers/Code.hpp | 38 ++ .../brazier/App/Http/Helpers/Cookie.hpp | 40 ++ .../brazier/App/Http/Helpers/HttpClient.hpp | 94 +++ .../brazier/App/Http/Helpers/SmtpClient.hpp | 61 ++ .../brazier/App/Http/Helpers/Validator.hpp | 36 + .../App/Http/Middlewares/AuthMiddleware.hpp | 43 ++ .../App/Http/Middlewares/CorsMiddleware.hpp | 55 ++ .../App/Http/Middlewares/Middleware.hpp | 39 ++ .../Http/Middlewares/MiddlewarePipeline.hpp | 43 ++ .../brazier/App/Http/Services/Service.hpp | 26 + brazier/include/brazier/Core | 12 + brazier/include/brazier/Crypto | 4 + brazier/include/brazier/DB | 15 + brazier/include/brazier/Database/Cache.hpp | 46 ++ .../include/brazier/Database/Collection.hpp | 119 ++++ brazier/include/brazier/Database/Database.hpp | 59 ++ .../Database/Migrations/BaseMigration.hpp | 42 ++ .../Database/Migrations/MigrationManager.hpp | 138 ++++ .../Database/Migrations/Migrations.hpp | 38 ++ .../Migrations/migration_migrations_table.hpp | 32 + .../include/brazier/Database/Models/Model.hpp | 643 ++++++++++++++++++ .../Database/Models/ModelQueryBuilder.hpp | 78 +++ brazier/include/brazier/Database/Queue.hpp | 43 ++ .../brazier/Database/SQLQueryBuilder.hpp | 62 ++ .../brazier/Database/SQLSchemaBuilder.hpp | 55 ++ .../include/brazier/Database/SQLString.hpp | 32 + brazier/include/brazier/Engine.hpp | 40 ++ .../include/brazier/Filesystem/BaseDriver.hpp | 56 ++ .../include/brazier/Filesystem/FileDriver.hpp | 67 ++ .../include/brazier/Filesystem/Filesystem.hpp | 63 ++ .../include/brazier/Filesystem/S3Driver.hpp | 62 ++ .../brazier/Filesystem/StorageManager.hpp | 54 ++ brazier/include/brazier/Http | 10 + brazier/include/brazier/Router/Route.hpp | 58 ++ brazier/include/brazier/Router/Router.hpp | 48 ++ .../brazier/Router/RouterRegisterer.hpp | 124 ++++ brazier/include/brazier/Router/WSRoute.hpp | 71 ++ brazier/include/brazier/Router/WSRouter.hpp | 60 ++ brazier/include/brazier/Router/types.hpp | 37 + brazier/include/brazier/WebSocketServer.hpp | 91 +++ brazier/include/brazier/server.hpp | 85 +++ .../include/brazier/vendor/ConfigManager.hpp | 153 +++++ .../brazier/vendor/Cryptography/AES.hpp | 38 ++ .../brazier/vendor/Cryptography/RSA.hpp | 59 ++ .../include/brazier/vendor/Debug/Logger.hpp | 67 ++ .../brazier/vendor/Facades/DatabaseFacade.hpp | 28 + .../include/brazier/vendor/Facades/Hash.hpp | 58 ++ .../include/brazier/vendor/Handlers/ENV.hpp | 40 ++ .../brazier/vendor/Storage/Storage.hpp | 68 ++ .../vendor/WebSocket/WebSocketManager.hpp | 68 ++ .../vendor/WebSocket/WebSocketSession.hpp | 83 +++ brazier/shaman | Bin 0 -> 205464 bytes brazier/shaman.exe | Bin 0 -> 199168 bytes brazier/src/Code.cpp | 69 ++ brazier/src/ConfigManager.cpp | 429 ++++++++++++ brazier/src/Controller.cpp | 55 ++ brazier/src/Cookie.cpp | 48 ++ brazier/src/Cryptography/AES.cpp | 241 +++++++ brazier/src/Cryptography/RSA.cpp | 412 +++++++++++ brazier/src/Cryptography/crypt.md | 65 ++ brazier/src/Database/Cache.cpp | 149 ++++ brazier/src/Database/Database.cpp | 262 +++++++ .../Database/Migrations/MigrationManager.cpp | 69 ++ .../src/Database/Migrations/Migrations.cpp | 38 ++ .../Migrations/migration_migrations_table.cpp | 42 ++ brazier/src/Database/Queue.cpp | 119 ++++ brazier/src/Database/SQLQueryBuilder.cpp | 232 +++++++ brazier/src/Database/SQLShemaBuilder.cpp | 114 ++++ brazier/src/Database/SQLString.cpp | 43 ++ brazier/src/ENV.cpp | 68 ++ brazier/src/Engine.cpp | 28 + brazier/src/Filesystem/FileDriver.cpp | 172 +++++ brazier/src/Filesystem/Filesystem.cpp | 116 ++++ brazier/src/Filesystem/StorageManager.cpp | 67 ++ brazier/src/Hash.cpp | 118 ++++ brazier/src/HttpClient.cpp | 394 +++++++++++ brazier/src/Logger.cpp | 168 +++++ brazier/src/Middleware.cpp | 31 + brazier/src/MiddlewarePipeline.cpp | 40 ++ brazier/src/Router/Route.cpp | 97 +++ brazier/src/Router/Router.cpp | 48 ++ brazier/src/Router/RouterRegisterer.cpp | 39 ++ brazier/src/Router/WSRoute.cpp | 132 ++++ brazier/src/Router/WSRouter.cpp | 79 +++ brazier/src/Server.cpp | 233 +++++++ brazier/src/SmtpClient.cpp | 167 +++++ brazier/src/Storage.cpp | 178 +++++ brazier/src/Validator.cpp | 47 ++ brazier/src/WebSocketManager.cpp | 134 ++++ brazier/src/WebSocketServer.cpp | 272 ++++++++ brazier/src/WebSocketSession.cpp | 196 ++++++ brazier/tests/main.cpp | 105 +++ brazier/tests/main.h | 38 ++ .../tests/unit/configuration/config_test.cpp | 88 +++ .../filesystem/test_file_driver_async.cpp | 121 ++++ .../unit/filesystem/test_file_driver_sync.cpp | 134 ++++ .../tests/unit/filesystem/test_filesystem.cpp | 144 ++++ .../unit/filesystem/test_storage_manager.cpp | 119 ++++ brazier/tests/unit/filesystem/test_utils.hpp | 83 +++ brazier/tests/unit/routing/routing_test.cpp | 275 ++++++++ brazier/vcpkg-configuration.json | 7 + brazier/vcpkg.json | 24 + 112 files changed, 10323 insertions(+) create mode 100644 brazier/.env.example create mode 100644 brazier/.gitignore create mode 100644 brazier/CMakeLists.txt create mode 100644 brazier/CMakePresets.json create mode 100644 brazier/app/Main.cpp create mode 100644 brazier/build.bat create mode 100644 brazier/build.sh create mode 100644 brazier/cmake/lightlibConfig.cmake create mode 100644 brazier/config_test.json create mode 100644 brazier/include/brazier/App/Http/Controllers/Controller.hpp create mode 100644 brazier/include/brazier/App/Http/Helpers/Code.hpp create mode 100644 brazier/include/brazier/App/Http/Helpers/Cookie.hpp create mode 100644 brazier/include/brazier/App/Http/Helpers/HttpClient.hpp create mode 100644 brazier/include/brazier/App/Http/Helpers/SmtpClient.hpp create mode 100644 brazier/include/brazier/App/Http/Helpers/Validator.hpp create mode 100644 brazier/include/brazier/App/Http/Middlewares/AuthMiddleware.hpp create mode 100644 brazier/include/brazier/App/Http/Middlewares/CorsMiddleware.hpp create mode 100644 brazier/include/brazier/App/Http/Middlewares/Middleware.hpp create mode 100644 brazier/include/brazier/App/Http/Middlewares/MiddlewarePipeline.hpp create mode 100644 brazier/include/brazier/App/Http/Services/Service.hpp create mode 100644 brazier/include/brazier/Core create mode 100644 brazier/include/brazier/Crypto create mode 100644 brazier/include/brazier/DB create mode 100644 brazier/include/brazier/Database/Cache.hpp create mode 100644 brazier/include/brazier/Database/Collection.hpp create mode 100644 brazier/include/brazier/Database/Database.hpp create mode 100644 brazier/include/brazier/Database/Migrations/BaseMigration.hpp create mode 100644 brazier/include/brazier/Database/Migrations/MigrationManager.hpp create mode 100644 brazier/include/brazier/Database/Migrations/Migrations.hpp create mode 100644 brazier/include/brazier/Database/Migrations/migration_migrations_table.hpp create mode 100644 brazier/include/brazier/Database/Models/Model.hpp create mode 100644 brazier/include/brazier/Database/Models/ModelQueryBuilder.hpp create mode 100644 brazier/include/brazier/Database/Queue.hpp create mode 100644 brazier/include/brazier/Database/SQLQueryBuilder.hpp create mode 100644 brazier/include/brazier/Database/SQLSchemaBuilder.hpp create mode 100644 brazier/include/brazier/Database/SQLString.hpp create mode 100644 brazier/include/brazier/Engine.hpp create mode 100644 brazier/include/brazier/Filesystem/BaseDriver.hpp create mode 100644 brazier/include/brazier/Filesystem/FileDriver.hpp create mode 100644 brazier/include/brazier/Filesystem/Filesystem.hpp create mode 100644 brazier/include/brazier/Filesystem/S3Driver.hpp create mode 100644 brazier/include/brazier/Filesystem/StorageManager.hpp create mode 100644 brazier/include/brazier/Http create mode 100644 brazier/include/brazier/Router/Route.hpp create mode 100644 brazier/include/brazier/Router/Router.hpp create mode 100644 brazier/include/brazier/Router/RouterRegisterer.hpp create mode 100644 brazier/include/brazier/Router/WSRoute.hpp create mode 100644 brazier/include/brazier/Router/WSRouter.hpp create mode 100644 brazier/include/brazier/Router/types.hpp create mode 100644 brazier/include/brazier/WebSocketServer.hpp create mode 100644 brazier/include/brazier/server.hpp create mode 100644 brazier/include/brazier/vendor/ConfigManager.hpp create mode 100644 brazier/include/brazier/vendor/Cryptography/AES.hpp create mode 100644 brazier/include/brazier/vendor/Cryptography/RSA.hpp create mode 100644 brazier/include/brazier/vendor/Debug/Logger.hpp create mode 100644 brazier/include/brazier/vendor/Facades/DatabaseFacade.hpp create mode 100644 brazier/include/brazier/vendor/Facades/Hash.hpp create mode 100644 brazier/include/brazier/vendor/Handlers/ENV.hpp create mode 100644 brazier/include/brazier/vendor/Storage/Storage.hpp create mode 100644 brazier/include/brazier/vendor/WebSocket/WebSocketManager.hpp create mode 100644 brazier/include/brazier/vendor/WebSocket/WebSocketSession.hpp create mode 100644 brazier/shaman create mode 100644 brazier/shaman.exe create mode 100644 brazier/src/Code.cpp create mode 100644 brazier/src/ConfigManager.cpp create mode 100644 brazier/src/Controller.cpp create mode 100644 brazier/src/Cookie.cpp create mode 100644 brazier/src/Cryptography/AES.cpp create mode 100644 brazier/src/Cryptography/RSA.cpp create mode 100644 brazier/src/Cryptography/crypt.md create mode 100644 brazier/src/Database/Cache.cpp create mode 100644 brazier/src/Database/Database.cpp create mode 100644 brazier/src/Database/Migrations/MigrationManager.cpp create mode 100644 brazier/src/Database/Migrations/Migrations.cpp create mode 100644 brazier/src/Database/Migrations/migration_migrations_table.cpp create mode 100644 brazier/src/Database/Queue.cpp create mode 100644 brazier/src/Database/SQLQueryBuilder.cpp create mode 100644 brazier/src/Database/SQLShemaBuilder.cpp create mode 100644 brazier/src/Database/SQLString.cpp create mode 100644 brazier/src/ENV.cpp create mode 100644 brazier/src/Engine.cpp create mode 100644 brazier/src/Filesystem/FileDriver.cpp create mode 100644 brazier/src/Filesystem/Filesystem.cpp create mode 100644 brazier/src/Filesystem/StorageManager.cpp create mode 100644 brazier/src/Hash.cpp create mode 100644 brazier/src/HttpClient.cpp create mode 100644 brazier/src/Logger.cpp create mode 100644 brazier/src/Middleware.cpp create mode 100644 brazier/src/MiddlewarePipeline.cpp create mode 100644 brazier/src/Router/Route.cpp create mode 100644 brazier/src/Router/Router.cpp create mode 100644 brazier/src/Router/RouterRegisterer.cpp create mode 100644 brazier/src/Router/WSRoute.cpp create mode 100644 brazier/src/Router/WSRouter.cpp create mode 100644 brazier/src/Server.cpp create mode 100644 brazier/src/SmtpClient.cpp create mode 100644 brazier/src/Storage.cpp create mode 100644 brazier/src/Validator.cpp create mode 100644 brazier/src/WebSocketManager.cpp create mode 100644 brazier/src/WebSocketServer.cpp create mode 100644 brazier/src/WebSocketSession.cpp create mode 100644 brazier/tests/main.cpp create mode 100644 brazier/tests/main.h create mode 100644 brazier/tests/unit/configuration/config_test.cpp create mode 100644 brazier/tests/unit/filesystem/test_file_driver_async.cpp create mode 100644 brazier/tests/unit/filesystem/test_file_driver_sync.cpp create mode 100644 brazier/tests/unit/filesystem/test_filesystem.cpp create mode 100644 brazier/tests/unit/filesystem/test_storage_manager.cpp create mode 100644 brazier/tests/unit/filesystem/test_utils.hpp create mode 100644 brazier/tests/unit/routing/routing_test.cpp create mode 100644 brazier/vcpkg-configuration.json create mode 100644 brazier/vcpkg.json diff --git a/brazier/.env.example b/brazier/.env.example new file mode 100644 index 0000000..8c4a961 --- /dev/null +++ b/brazier/.env.example @@ -0,0 +1,18 @@ +S_HOST= +S_PORT= + +DB_HOST= +DB_PORT= +DB_USERNAME= +DB_PASSWORD= +DB_DATABASE= + +REDIS_HOST= +REDIS_PORT= + +AUTH_SECRET= + +SMTP_HOST= +SMTP_USERNAME= +SMTP_PASSWORD= +SMTP_PORT= \ No newline at end of file diff --git a/brazier/.gitignore b/brazier/.gitignore new file mode 100644 index 0000000..bbcef60 --- /dev/null +++ b/brazier/.gitignore @@ -0,0 +1,9 @@ +.env +/x64/ +/Build/ +/vcpkg_installed +/build +/.vscode +/cmake-build-debug +.clangd +config.json \ No newline at end of file diff --git a/brazier/CMakeLists.txt b/brazier/CMakeLists.txt new file mode 100644 index 0000000..d0391f3 --- /dev/null +++ b/brazier/CMakeLists.txt @@ -0,0 +1,239 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +cmake_minimum_required(VERSION 3.14) +project(brazier VERSION 0.1.5 LANGUAGES CXX) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +if(WIN32) + set(CMAKE_THREAD_PREFER_PTHREAD OFF) + set(THREADS_PREFER_PTHREAD_FLAG OFF) + set(CMAKE_USE_WIN32_THREADS_INIT ON) + set(CMAKE_THREAD_LIBS_INIT "") + set(CMAKE_HAVE_THREADS_LIBRARY 1) + set(CMAKE_USE_PTHREADS_INIT 0) + add_compile_definitions(_WIN32_WINNT=0x0601) + add_compile_definitions(WIN32_LEAN_AND_MEAN NOMINMAX) +endif() + +if(MSVC) + add_compile_options(/utf-8 /bigobj) +endif() + +find_package(Boost CONFIG REQUIRED COMPONENTS beast asio system thread filesystem) +find_package(fmt CONFIG REQUIRED) +find_package(nlohmann_json CONFIG REQUIRED) +find_package(hiredis CONFIG REQUIRED) +find_package(ZLIB REQUIRED) +find_package(PostgreSQL REQUIRED) +find_package(OpenSSL REQUIRED) + +set(LIBRARIES + Boost::beast + Boost::asio + Boost::system + Boost::thread + Boost::filesystem + fmt::fmt + nlohmann_json::nlohmann_json + hiredis::hiredis + ZLIB::ZLIB + PostgreSQL::PostgreSQL + OpenSSL::SSL + OpenSSL::Crypto +) + +file(GLOB_RECURSE PROJECT_SOURCES + "src/*.cpp" + "src/Database/*.cpp" + "src/Database/Migrations/*.cpp" + "src/Router/*.cpp" + "src/Filesystem/*.cpp" + "src/Cryptography/*.cpp" +) +list(FILTER PROJECT_SOURCES EXCLUDE REGEX "app/Main\\.cpp$") + +add_library(brazier_objects OBJECT ${PROJECT_SOURCES}) + +target_link_libraries(brazier_objects PUBLIC Boost::boost) + +target_include_directories(brazier_objects PRIVATE + ${fmt_INCLUDE_DIRS} + ${nlohmann_json_INCLUDE_DIRS} + ${hiredis_INCLUDE_DIRS} + ${ZLIB_INCLUDE_DIRS} + ${PostgreSQL_INCLUDE_DIRS} + ${OPENSSL_INCLUDE_DIR} + ${GTEST_INCLUDE_DIRS} +) + +target_include_directories(brazier_objects PUBLIC + $ + $ +) + +target_include_directories(brazier_objects PRIVATE + ${CMAKE_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/vendor + ${CMAKE_SOURCE_DIR}/Database + ${CMAKE_SOURCE_DIR}/Router + ${CMAKE_SOURCE_DIR}/App + ${CMAKE_SOURCE_DIR}/Storage +) +target_compile_features(brazier_objects PUBLIC cxx_std_20) +target_compile_definitions(brazier_objects PUBLIC + BOOST_ASIO_HAS_CO_AWAIT + BOOST_ASIO_HAS_STD_COROUTINE +) + +target_compile_features(brazier_objects PUBLIC cxx_std_20) +target_compile_definitions(brazier_objects PUBLIC + BOOST_ASIO_HAS_CO_AWAIT + BOOST_ASIO_HAS_STD_COROUTINE +) + +set_target_properties(brazier_objects PROPERTIES + UNITY_BUILD ON + UNITY_BUILD_BATCH_SIZE 8 +) + +add_library(brazier_static STATIC $) +add_library(brazier_shared SHARED $) + +if(MSVC) + set_target_properties(brazier_shared PROPERTIES + WINDOWS_EXPORT_ALL_SYMBOLS ON + ) +endif() + +target_compile_definitions(brazier_shared PRIVATE brazier_EXPORTS) +target_compile_definitions(brazier_shared PUBLIC brazier_SHARED) +target_compile_definitions(brazier_static PUBLIC) + +set_target_properties(brazier_shared PROPERTIES POSITION_INDEPENDENT_CODE ON) + +target_link_libraries(brazier_static PUBLIC ${LIBRARIES}) +target_link_libraries(brazier_shared PUBLIC ${LIBRARIES}) + +export(TARGETS brazier_static brazier_shared + NAMESPACE brazier:: + FILE "${CMAKE_CURRENT_BINARY_DIR}/brazierTargets.cmake" +) + +add_executable(brazier_app app/Main.cpp) + +target_include_directories(brazier_app PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/vendor + ${CMAKE_SOURCE_DIR}/Database + ${CMAKE_SOURCE_DIR}/Router + ${CMAKE_SOURCE_DIR}/App + ${CMAKE_SOURCE_DIR}/Storage +) + +target_compile_features(brazier_app PRIVATE cxx_std_20) + +target_compile_definitions(brazier_app PRIVATE + BOOST_ASIO_HAS_CO_AWAIT + BOOST_ASIO_HAS_STD_COROUTINE +) + +target_link_libraries(brazier_app PRIVATE + brazier_static + ${LIBRARIES} +) + +add_library(brazier INTERFACE) +target_link_libraries(brazier INTERFACE + brazier_static + brazier_shared +) + +install(TARGETS brazier_static + EXPORT brazierTargets + ARCHIVE DESTINATION lib +) + +install(TARGETS brazier_shared + EXPORT brazierTargets + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin + ARCHIVE DESTINATION lib +) + +install(TARGETS brazier + EXPORT brazierTargets +) + +install(DIRECTORY include/ DESTINATION include) + +install(EXPORT brazierTargets + NAMESPACE brazier:: + DESTINATION share/brazier + FILE brazierTargets.cmake +) + +install( + FILES "${CMAKE_CURRENT_SOURCE_DIR}/cmake/brazierConfig.cmake" + DESTINATION share/brazier +) + +option(BUILD_TESTS "Build tests" ON) + +if(BUILD_TESTS) + message(STATUS "Building tests") + + find_package(GTest CONFIG REQUIRED) + + get_target_property(GTEST_INCLUDE_DIRS GTest::gtest INTERFACE_INCLUDE_DIRECTORIES) + + file(GLOB_RECURSE TEST_SOURCES "tests/*.cpp") + + add_executable(brazier_tests ${TEST_SOURCES}) + + target_include_directories(brazier_tests PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR}/tests + ${fmt_INCLUDE_DIRS} + ${nlohmann_json_INCLUDE_DIRS} + ${hiredis_INCLUDE_DIRS} + ${ZLIB_INCLUDE_DIRS} + ${PostgreSQL_INCLUDE_DIRS} + ${OPENSSL_INCLUDE_DIR} + ${GTEST_INCLUDE_DIRS} + ) + + target_compile_definitions(brazier_tests PRIVATE + BOOST_ASIO_HAS_CO_AWAIT + BOOST_ASIO_HAS_STD_COROUTINE + ) + + target_compile_features(brazier_tests PRIVATE cxx_std_20) + + target_link_libraries(brazier_tests PRIVATE + brazier_static + GTest::gtest + GTest::gtest_main + GTest::gmock + GTest::gmock_main + ${LIBRARIES} + ) + + # include(GoogleTest) + # gtest_discover_tests(brazier_tests) + + # add_test(NAME brazier_tests COMMAND brazier_tests) + + add_custom_target(check + COMMAND $ + DEPENDS brazier_tests + COMMENT "Running tests..." + ) + + message(STATUS "Tests configured. Run 'cmake --build . --target check' to run tests") +endif() \ No newline at end of file diff --git a/brazier/CMakePresets.json b/brazier/CMakePresets.json new file mode 100644 index 0000000..581a2bb --- /dev/null +++ b/brazier/CMakePresets.json @@ -0,0 +1,14 @@ +{ + "version": 3, + "configurePresets": [ + { + "name": "brazier", + "displayName": "brazier", + "generator": "Visual Studio 18 2026", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "VCPKG_INSTALLED_DIR": "${sourceDir}/vcpkg_installed" + } + } + ] +} \ No newline at end of file diff --git a/brazier/app/Main.cpp b/brazier/app/Main.cpp new file mode 100644 index 0000000..169ec35 --- /dev/null +++ b/brazier/app/Main.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#include "../include/brazier/Core" +#include "../include/brazier/DB" +#include "../include/brazier/Http" +#include "../include/brazier/Engine.hpp" + +int main() { + try { + brazier::ConfigManager::initGlobal("config_test.json"); + brazier::global_config->setAutoSave(false); + + std::string server_host = brazier::global_config->get("server.host", "0.0.0.0"); + int server_port = brazier::global_config->get("server.port", 3502); + + brazier::Logger::log("server updated host: " + server_host, "INFO"); + brazier::Logger::log("server new host: " + std::to_string(server_port), "INFO"); + + brazier::Server server(server_host, server_port); + server.run(); + return 0; + } + catch (const std::exception& e) { + std::cerr << "Fatal error: " << e.what() << std::endl; + brazier::Logger::log(std::string(e.what()), "ERROR"); + return 1; + } +} \ No newline at end of file diff --git a/brazier/build.bat b/brazier/build.bat new file mode 100644 index 0000000..330ac09 --- /dev/null +++ b/brazier/build.bat @@ -0,0 +1,40 @@ +:: SPDX-License-Identifier: LGPL-3.0-or-later +@echo off +chcp 65001 >nul +setlocal enabledelayedexpansion + +set PROJECT_NAME=brazier +set BUILD_DIR=build +set SOURCE_DIR=%~dp0 +set CONFIG=Release +if defined VCPKG_ROOT ( + set VCPKG_TOOLCHAIN=%VCPKG_ROOT%\scripts\buildsystems\vcpkg.cmake +) else ( + set VCPKG_TOOLCHAIN=C:\Tools\vcpkg\scripts\buildsystems\vcpkg.cmake +) + +if not exist "%BUILD_DIR%" ( + mkdir "%BUILD_DIR%" +) + +cd /d "%BUILD_DIR%" + +if exist "%PROJECT_NAME%.exe" ( + ren "%PROJECT_NAME%.exe" "%PROJECT_NAME%_OLD.exe" +) + +cmake --preset brazier -A x64 -DBUILD_TESTS=ON -DCMAKE_TOOLCHAIN_FILE="%VCPKG_TOOLCHAIN%" %SOURCE_DIR% +cmake --build . --config %CONFIG% +if %errorlevel% neq 0 ( + echo CMake build failed. + exit /b %errorlevel% +) + +if exist "%CONFIG%\%PROJECT_NAME%.exe" ( + echo Running %PROJECT_NAME%.exe... + "%CONFIG%\%PROJECT_NAME%.exe" +) else ( + echo The file %PROJECT_NAME%.exe was not found. +) + +cd /d "%SOURCE_DIR%" \ No newline at end of file diff --git a/brazier/build.sh b/brazier/build.sh new file mode 100644 index 0000000..70be4e6 --- /dev/null +++ b/brazier/build.sh @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +set -e + +PROJECT_NAME="brazier" +BUILD_DIR="build" +CONFIG="Release" +SOURCE_DIR="$(pwd)" +VCPKG_TOOLCHAIN="${VCPKG_TOOLCHAIN:-$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake}" +if [ ! -f "$VCPKG_TOOLCHAIN" ]; then + echo "vcpkg toolset not found: $VCPKG_TOOLCHAIN" + exit 1 +fi + +mkdir -p "$BUILD_DIR" +cd "$BUILD_DIR" + +if [ -f "$PROJECT_NAME" ]; then + mv "./$PROJECT_NAME" "./${PROJECT_NAME}_OLD" +fi + +cmake -DCMAKE_TOOLCHAIN_FILE="$VCPKG_TOOLCHAIN" -DBUILD_TESTS=ON -DCMAKE_BUILD_TYPE=$CONFIG "$SOURCE_DIR" +cmake --build . --config $CONFIG +if [ $? -ne 0 ]; then + echo "Build failed" + exit +fi + +if [ -d "$SOURCE_DIR" ]; then + mkdir -p "$SOURCE_DIR/build/$CONFIG" + cp "$SOURCE_DIR/.env" "$SOURCE_DIR/build/$CONFIG/" + if [ $? -eq 0 ]; then + echo "Copied .env to $CONFIG." + fi +fi + +if [ -f "$PROJECT_NAME" ]; then + echo "Launch $PROJECT_NAME..." + ./"$PROJECT_NAME" +else + echo "Binary $PROJECT_NAME not found" +fi + +cd "$SOURCE_DIR" diff --git a/brazier/cmake/lightlibConfig.cmake b/brazier/cmake/lightlibConfig.cmake new file mode 100644 index 0000000..1f4f6ff --- /dev/null +++ b/brazier/cmake/lightlibConfig.cmake @@ -0,0 +1,15 @@ +include(CMakeFindDependencyMacro) + +find_dependency(Boost CONFIG REQUIRED COMPONENTS beast asio system thread filesystem) +find_dependency(fmt CONFIG REQUIRED) +find_dependency(nlohmann_json CONFIG REQUIRED) +find_dependency(hiredis CONFIG REQUIRED) +find_dependency(ZLIB REQUIRED) +find_dependency(PostgreSQL REQUIRED) +find_dependency(OpenSSL REQUIRED) + +include("${CMAKE_CURRENT_LIST_DIR}/brazierTargets.cmake") + +if(NOT TARGET brazier::brazier) + add_library(brazier::brazier ALIAS brazier) +endif() \ No newline at end of file diff --git a/brazier/config_test.json b/brazier/config_test.json new file mode 100644 index 0000000..0554a2c --- /dev/null +++ b/brazier/config_test.json @@ -0,0 +1,18 @@ +{ + "server": { + "host": "0.0.0.0", + "port": 3502 + }, + "app_name": "brazierApp", + "filesystem": { + "drivers": { + "local": { + "root": "./storage" + }, + "root": { + "root": "./" + }, + "default": "local" + } + } +} \ No newline at end of file diff --git a/brazier/include/brazier/App/Http/Controllers/Controller.hpp b/brazier/include/brazier/App/Http/Controllers/Controller.hpp new file mode 100644 index 0000000..d65d11b --- /dev/null +++ b/brazier/include/brazier/App/Http/Controllers/Controller.hpp @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once +#pragma execution_character_set("utf-8") + +#include +#include +#include + +namespace beast = boost::beast; +namespace http = beast::http; + +using Params = std::unordered_map; + +namespace brazier { + + class Controller { + public: + using Request = http::request; + using Response = http::response; + + virtual boost::asio::awaitable show(const Request& req, Response& res, const Params& params); + virtual boost::asio::awaitable store(const Request& req, Response& res, const Params& params); + virtual boost::asio::awaitable delete_(const Request& req, Response& res, const Params& params); + virtual boost::asio::awaitable update(const Request& req, Response& res, const Params& params); + + virtual void setCors(const Request& req, Response& res); + virtual void setCorsHeaders(const Request& req, Response& res); + + virtual ~Controller() = default; + }; + +} \ No newline at end of file diff --git a/brazier/include/brazier/App/Http/Helpers/Code.hpp b/brazier/include/brazier/App/Http/Helpers/Code.hpp new file mode 100644 index 0000000..e22e6c8 --- /dev/null +++ b/brazier/include/brazier/App/Http/Helpers/Code.hpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once +#include +#include +#include +#include +#include +#include + +namespace brazier { + + class Code { + public: + static std::string generateRandomCode(size_t length); + static std::string generateNumericCode(size_t length); + static std::vector generateMultipleCodes(size_t count, size_t length, bool numeric = false); + static std::string base64_encode(const std::string& bytes); + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/App/Http/Helpers/Cookie.hpp b/brazier/include/brazier/App/Http/Helpers/Cookie.hpp new file mode 100644 index 0000000..7b36191 --- /dev/null +++ b/brazier/include/brazier/App/Http/Helpers/Cookie.hpp @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once +#include +#include +#include +#include + +namespace brazier { + + namespace beast = boost::beast; + namespace http = beast::http; + + class Cookie { + public: + using Request = http::request; + using Response = http::response; + + static std::map parseCookies(const std::string& cookieHeader); + static void set(Response& res, std::map cookies); + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/App/Http/Helpers/HttpClient.hpp b/brazier/include/brazier/App/Http/Helpers/HttpClient.hpp new file mode 100644 index 0000000..5b0b9f1 --- /dev/null +++ b/brazier/include/brazier/App/Http/Helpers/HttpClient.hpp @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace brazier { + + namespace beast = boost::beast; + namespace http = beast::http; + namespace net = boost::asio; + namespace ssl = net::ssl; + using tcp = net::ip::tcp; + using Response = http::response; + using Request = http::request; + + class HttpClient { + public: + using json = nlohmann::json; + + HttpClient(); + ~HttpClient() = default; + + net::awaitable get(const std::string& url, const json& body = json{}); + net::awaitable post(const std::string& url, const json& body); + net::awaitable put(const std::string& url, const json& body); + net::awaitable del(const std::string& url, const json& body = json{}); + + void set_timeout(const std::chrono::milliseconds& timeout); + bool is_success(const Response& res) const; + + void set_verify_ssl(bool verify); + + private: + ssl::context ctx_; + std::chrono::milliseconds timeout_ = std::chrono::seconds(30); + bool verify_ssl_ = true; + + struct UrlParts { + std::string protocol; + std::string host; + std::string port; + std::string path; + std::string query; + }; + + UrlParts parse_url(const std::string& url); + + net::awaitable send_request(const std::string& url, http::verb method, const json& body); + net::awaitable send_http_request(const UrlParts& url_parts, http::verb method, const json& body); + net::awaitable send_https_request(const UrlParts& url_parts, http::verb method, const json& body); + + void setup_common_headers(Request& req, const std::string& host); + std::string json_to_query_string(const json& j); + std::string encode_url(const std::string& value); + + }; + +} \ No newline at end of file diff --git a/brazier/include/brazier/App/Http/Helpers/SmtpClient.hpp b/brazier/include/brazier/App/Http/Helpers/SmtpClient.hpp new file mode 100644 index 0000000..24ebf24 --- /dev/null +++ b/brazier/include/brazier/App/Http/Helpers/SmtpClient.hpp @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include "../../../vendor/Handlers/ENV.hpp" +#include "../../../vendor/Debug/Logger.hpp" +#include "../../../Engine.hpp" + +namespace brazier +{ + namespace beast = boost::beast; + namespace net = boost::asio; + namespace ssl = net::ssl; + using tcp = net::ip::tcp; + + class SmtpClient { + public: + explicit SmtpClient(); + ~SmtpClient(); + + net::awaitable send(const std::string& recipient, const std::string& message, const std::string& sender_name = ""); + + private: + std::string host; + std::string smtp_host; + std::string port; + std::string username; + std::string password; + + net::io_context& io_ctx_; + ssl::context ssl_ctx_; + + net::awaitable write_line(ssl::stream& socket, const std::string& line); + + net::awaitable read_response(ssl::stream& socket, int expected_code); + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/App/Http/Helpers/Validator.hpp b/brazier/include/brazier/App/Http/Helpers/Validator.hpp new file mode 100644 index 0000000..6a66565 --- /dev/null +++ b/brazier/include/brazier/App/Http/Helpers/Validator.hpp @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include +#include +#include + +namespace brazier { + + class Validator { + public: + static bool password(const std::string& password); + static bool email(const std::string& email); + static std::string passwordError(const std::string& password); + + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/App/Http/Middlewares/AuthMiddleware.hpp b/brazier/include/brazier/App/Http/Middlewares/AuthMiddleware.hpp new file mode 100644 index 0000000..869e3a1 --- /dev/null +++ b/brazier/include/brazier/App/Http/Middlewares/AuthMiddleware.hpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once +#include "Middleware.hpp" +#include "../Services/AuthService.hpp" + +namespace brazier { + + class CorsMiddleware : public Middleware { + + std::vector allowed_domains; + std::string allow_origin; + + public: + bool handle(Request& req, Response& res) { + try { + + } + catch (const std::exception& e) { + Logger::log((std::string)e.what(), "ERROR") + } + + } + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/App/Http/Middlewares/CorsMiddleware.hpp b/brazier/include/brazier/App/Http/Middlewares/CorsMiddleware.hpp new file mode 100644 index 0000000..8b23390 --- /dev/null +++ b/brazier/include/brazier/App/Http/Middlewares/CorsMiddleware.hpp @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once +#include "Middleware.hpp" + +namespace brazier { + + class CorsMiddleware : public Middleware { + + std::vector allowed_domains; + std::string allow_origin; + + public: + bool handle(Request& req, Response& res) { + std::string origin = std::string(req[http::field::origin]); + bool allowed = this->allowed_domains.empty() || + std::find(allowed_domains.begin(), allowed_domains.end(), origin) != allowed_domains.end(); + + if (!allowed) { + res.result(http::status::forbidden); + res.body() = "CORS: Origin not allowed"; + res.set(http::field::access_control_allow_origin, "null"); + res.prepare_payload(); + return false; + } + + this->setCors(req, res); + + if (req.method() == http::verb::options) { + res.result(http::status::ok); + res.body() = "CORS preflight"; + res.prepare_payload(); + return true; + } + } + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/App/Http/Middlewares/Middleware.hpp b/brazier/include/brazier/App/Http/Middlewares/Middleware.hpp new file mode 100644 index 0000000..16fc4ba --- /dev/null +++ b/brazier/include/brazier/App/Http/Middlewares/Middleware.hpp @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once +#include + +namespace brazier { + + namespace beast = boost::beast; + namespace http = beast::http; + + class Middleware { + public: + using Request = http::request; + using Response = http::response; + + virtual bool handle(Request& req, Response& res) = 0; + virtual ~Middleware() = default; + + virtual void setCors(const Request& req, Response& res); + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/App/Http/Middlewares/MiddlewarePipeline.hpp b/brazier/include/brazier/App/Http/Middlewares/MiddlewarePipeline.hpp new file mode 100644 index 0000000..d36a44b --- /dev/null +++ b/brazier/include/brazier/App/Http/Middlewares/MiddlewarePipeline.hpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once +#include "CorsMiddleware.hpp" +#include +#include + +namespace brazier { + + namespace beast = boost::beast; + namespace http = beast::http; + + class MiddlewarePipeline { + std::vector> chain; + + public: + using Request = http::request; + using Response = http::response; + + MiddlewarePipeline(std::vector> chain); + MiddlewarePipeline(); + void use(std::shared_ptr mw); + bool run(Request& req, Response& res); + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/App/Http/Services/Service.hpp b/brazier/include/brazier/App/Http/Services/Service.hpp new file mode 100644 index 0000000..10f88d1 --- /dev/null +++ b/brazier/include/brazier/App/Http/Services/Service.hpp @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +class Service { +public: + virtual ~Service() = default; +}; \ No newline at end of file diff --git a/brazier/include/brazier/Core b/brazier/include/brazier/Core new file mode 100644 index 0000000..dca66ff --- /dev/null +++ b/brazier/include/brazier/Core @@ -0,0 +1,12 @@ +#pragma once + +#include "vendor/Debug/Logger.hpp" +#include "vendor/Facades/Hash.hpp" +#include "vendor/Handlers/ENV.hpp" +#include "Router/Route.hpp" +#include "Router/Router.hpp" +#include "Router/RouterRegisterer.hpp" +#include "server.hpp" +#include "Engine.hpp" +#include "WebSocketServer.hpp" +#include "vendor/ConfigManager.hpp" \ No newline at end of file diff --git a/brazier/include/brazier/Crypto b/brazier/include/brazier/Crypto new file mode 100644 index 0000000..00f5651 --- /dev/null +++ b/brazier/include/brazier/Crypto @@ -0,0 +1,4 @@ +#pragma once + +#include "vendor/Cryptography/AES.hpp" +#include "vendor/Cryptography/RSA.hpp" \ No newline at end of file diff --git a/brazier/include/brazier/DB b/brazier/include/brazier/DB new file mode 100644 index 0000000..eaa053d --- /dev/null +++ b/brazier/include/brazier/DB @@ -0,0 +1,15 @@ +#pragma once + +#include "Database/Cache.hpp" +#include "Database/Queue.hpp" +#include "Database/Database.hpp" +#include "Database/SQLQueryBuilder.hpp" +#include "Database/SQLSchemaBuilder.hpp" +#include "Database/SQLString.hpp" +#include "Database/Models/Model.hpp" +#include "Database/Models/ModelQueryBuilder.hpp" +#include "Database/Migrations/BaseMigration.hpp" +#include "Database/Migrations/MigrationManager.hpp" +#include "Database/Migrations/Migrations.hpp" +#include "Database/Migrations/migration_migrations_table.hpp" +#include "Database/Collection.hpp" \ No newline at end of file diff --git a/brazier/include/brazier/Database/Cache.hpp b/brazier/include/brazier/Database/Cache.hpp new file mode 100644 index 0000000..3c8c9b5 --- /dev/null +++ b/brazier/include/brazier/Database/Cache.hpp @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include "../vendor/Debug/Logger.hpp" + +namespace brazier { + + class Cache { + public: + static void connect(const std::string& host = "127.0.0.1", int port = 6379); + static void disconnect(); + static void set(const std::string& key, const std::string& value, int expire_seconds = 0); + static std::string get(const std::string& key); + static void del(const std::string& key); + static void expire(const std::string& key, int expire_seconds); + static boost::asio::awaitable get_async(const std::string& key); + static boost::asio::awaitable set_async(const std::string& key, const std::string& value, int ttl); + + private: + static redisContext* context_; + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/Database/Collection.hpp b/brazier/include/brazier/Database/Collection.hpp new file mode 100644 index 0000000..9f319a7 --- /dev/null +++ b/brazier/include/brazier/Database/Collection.hpp @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include +#include +#include +#include + +namespace brazier { + + using json = nlohmann::json; + + template + class Collection : public std::vector>{ + public: + using std::vector>::vector; + + Collection() = default; + + Collection(const std::vector>& vec) + : std::vector>(vec) {} + + Collection(std::vector>&& vec) + : std::vector>(std::move(vec)) {} + + bool save() { + if (this->empty()) { + return false; + } + return ModelType::saveMany(*this); + } + + bool delete_() { + if (this->empty()) { + return false; + } + + bool success = true; + for (auto& item : *this) { + try { + item->delete_(); + } + catch (const std::exception& e) { + Logger::log("Failed to delete item: " + std::string(e.what()), "ERROR"); + success = false; + } + } + return success; + } + + json toJson() const { + json j = json::array(); + for (const auto& item : *this) { + j.push_back(item->toJson()); + } + return j; + } + + std::shared_ptr first() const { + return this->empty() ? nullptr : this->front(); + } + + std::shared_ptr last() const { + return this->empty() ? nullptr : this->back(); + } + + /* + @brief Filters the collection based on a predicate function. + @param predicate A function that takes a shared pointer to ModelType and returns a boolean. + @return A new Collection containing only the elements that satisfy the predicate. + @note This method does not modify the original collection; it returns a new filtered collection. + */ + Collection filter(std::function)> predicate) const { + Collection result; + std::copy_if(this->begin(), this->end(), std::back_inserter(result), predicate); + return result; + } + + bool all(std::function)> predicate) const { + return std::all_of(this->begin(), this->end(), predicate); + } + + bool any(std::function)> predicate) const { + return std::any_of(this->begin(), this->end(), predicate); + } + + std::shared_ptr find(std::function)> predicate) const { + auto it = std::find_if(this->begin(), this->end(), predicate); + return it != this->end() ? *it : nullptr; + } + + std::vector> toVector() const { + return *this; + } + + operator std::vector>() const { + return *this; + } + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/Database/Database.hpp b/brazier/include/brazier/Database/Database.hpp new file mode 100644 index 0000000..21e8971 --- /dev/null +++ b/brazier/include/brazier/Database/Database.hpp @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "SQLString.hpp" +#include "../vendor/Handlers/ENV.hpp" +#include "../vendor/Debug/Logger.hpp" +#include "../Engine.hpp" + +namespace brazier { + + class Database { + public: + Database(); + ~Database(); + void execute(const std::string& sql); + void transaction(const std::string name); + void rollback(const std::string name); + void commit(); + + bool isInTransaction() const { return in_transaction_; } + + std::string query(const std::string& sql); + + std::map queryMap(const std::string& sql_template, const std::vector& params); + std::vector> queryToVector(const std::string& sql_template, const std::vector& params = {}); + + PGconn* getConnection() const { return conn_; } + + private: + PGconn* conn_; + bool in_transaction_ = false; + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/Database/Migrations/BaseMigration.hpp b/brazier/include/brazier/Database/Migrations/BaseMigration.hpp new file mode 100644 index 0000000..a897f45 --- /dev/null +++ b/brazier/include/brazier/Database/Migrations/BaseMigration.hpp @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once +#include +#include "../SQLSchemaBuilder.hpp" + +namespace brazier { + + template + class BaseMigration { + + + public: + virtual ~BaseMigration() = default; + + static std::vector up() { + return Derived::up(); + } + + static std::string down() { + return Derived::down(); + } + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/Database/Migrations/MigrationManager.hpp b/brazier/include/brazier/Database/Migrations/MigrationManager.hpp new file mode 100644 index 0000000..85dbb68 --- /dev/null +++ b/brazier/include/brazier/Database/Migrations/MigrationManager.hpp @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "../Database.hpp" +#include "../SQLSchemaBuilder.hpp" +#include "../../vendor/Debug/Logger.hpp" + +namespace brazier { + + class MigrationManager { + private: + Database& db; + std::unordered_set executedMigrations; + + bool isMigrationExecuted(const std::string& name); + void markMigrationAsExecuted(const std::string& name); + void unmarkMigrationAsExecuted(const std::string& name); + void executeQueries(const std::vector& queries); + + public: + MigrationManager(Database& db); + + template + void migrate() { + std::string name = typeid(Migration).name(); + + if (isMigrationExecuted(name)) { + Logger::log("Migration already executed: " + name, "INFO"); + return; + } + + try { + db.transaction(""); + db.transaction("migration_" + name); + + auto queries = Migration::up(); + for (const auto& query : queries) { + db.execute(query); + } + + markMigrationAsExecuted(name); + db.commit(); + Logger::log("Migration completed: " + name, "INFO"); + } + catch (const std::exception& e) { + if (db.isInTransaction()) { + try { + db.rollback("migration_" + name); + } + catch (...) {} + + try { + db.rollback(""); + } + catch (...) {} + } + + Logger::log("Migration failed: " + name + " - " + e.what(), "ERROR"); + throw; + } + } + + template + void rollback() { + std::string name = typeid(Migration).name(); + + if (!isMigrationExecuted(name)) { + Logger::log("Migration not executed: " + name, "ERROR"); + return; + } + + try { + db.transaction(""); + + std::string mainQuery = Migration::down(); + db.execute(mainQuery); + + unmarkMigrationAsExecuted(name); + + db.commit(); + Logger::log("Migration rolled back: " + name, "INFO"); + } + catch (const std::exception& e) { + try { + db.rollback(""); + } + catch (const std::exception& se) { + Logger::log("Could not rollback: " + std::string(se.what()), "WARNING"); + } + Logger::log("Rollback failed: " + name + " - " + e.what(), "ERROR"); + throw; + } + } + + template + void migrateAll() { + try { + (migrate(), ...); + } + catch (const std::exception& e) { + Logger::log("Migration chain stopped due to error: " + std::string(e.what()), "ERROR"); + } + } + + template + void rollbackAll() { + (rollback(), ...); + } + + void Initialize(); + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/Database/Migrations/Migrations.hpp b/brazier/include/brazier/Database/Migrations/Migrations.hpp new file mode 100644 index 0000000..8523616 --- /dev/null +++ b/brazier/include/brazier/Database/Migrations/Migrations.hpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once +#pragma execution_character_set("utf-8") + +#include +#include +#include + +namespace brazier { + + class Migration { + using Handler = std::function; + static std::vector> migrations_; + + public: + static void addMigration(Handler handle); + static void runMigrations(); + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/Database/Migrations/migration_migrations_table.hpp b/brazier/include/brazier/Database/Migrations/migration_migrations_table.hpp new file mode 100644 index 0000000..05d8586 --- /dev/null +++ b/brazier/include/brazier/Database/Migrations/migration_migrations_table.hpp @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include "BaseMigration.hpp" + +namespace brazier { + + class MigrationMigrationsCreate : public BaseMigration { + public: + static std::vector up(); + static std::string down(); + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/Database/Models/Model.hpp b/brazier/include/brazier/Database/Models/Model.hpp new file mode 100644 index 0000000..3e3bea2 --- /dev/null +++ b/brazier/include/brazier/Database/Models/Model.hpp @@ -0,0 +1,643 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include "../Database.hpp" +#include "../../vendor/Debug/Logger.hpp" +#include "ModelQueryBuilder.hpp" +#include "../SQLQueryBuilder.hpp" +#include "../SQLString.hpp" +#include "../Collection.hpp" + +namespace brazier { + + using json = nlohmann::json; + + template + class Model { + protected: + static inline std::vector fillable; + static inline std::vector fields; + + std::map attributes; + static inline std::string primary_key = "id"; + + std::shared_ptr database; + + static std::shared_ptr getDefaultDatabase() { + static auto db = std::make_shared(); + return db; + } + + public: + Model(const std::shared_ptr& db = getDefaultDatabase()) : database(db) {} + + virtual ~Model() = default; + + void setDatabase(const std::shared_ptr& db) { + this->database = db; + } + + static ModelQueryBuilder query() { + return ModelQueryBuilder(Derived::table_name); + } + + void setAttribute(const std::string& key, const std::string& value) { + if (isField(key)) { + attributes[key] = value; + } + else { + Logger::log("Field '" + key + "' is not fillable or not a valid field.", "ERROR"); + } + } + + void debugPrintAttributes() const { + std::cout << "=== MODEL ATTRIBUTES ===\n"; + for (const auto& [key, val] : attributes) { + std::cout << key << " = '" << val << "'\n"; + } + } + + virtual bool validate() { return true; } + + virtual bool beforeSave() { return true; } + + virtual void afterSave() {} + + std::string getAttribute(const std::string& key) const { + auto it = attributes.find(key); + if (it != attributes.end()) { + return it->second; + } + throw std::invalid_argument("Attribute '" + key + "' not found."); + } + + bool save() { + if (!validate() || !beforeSave()) { + return false; + } + + if (!database) { + database = getDefaultDatabase(); + } + + if (attributes.empty()) { + Logger::log("Attributes are empty", "ERROR"); + return false; + } + + PGconn* conn = database->getConnection(); + if (!conn) { + Logger::log("Failed to get database connection", "ERROR"); + return false; + } + + bool hasId = attributes.find(Derived::primary_key) != attributes.end() && + !attributes[Derived::primary_key].empty(); + + std::map insertValues; + for (const auto& [key, value] : attributes) { + if (key == Derived::primary_key) continue; + insertValues[key] = SQLString::EscapeString(conn, value); + } + + if (insertValues.empty()) { + Logger::log("Insert values are empty. Aborting save operation", "ERROR"); + return false; + } + + SQLQueryBuilder builder(Derived::table_name); + + if (hasId) { + builder.Update(insertValues); + builder.Where(Derived::primary_key + " = " + attributes[Derived::primary_key]); + std::string query = builder.get(); + try { + database->execute(query); + afterSave(); + return true; + } + catch (const std::exception& e) { + Logger::log(e.what(), "ERROR"); + return false; + } + } + else { + builder.Insert(insertValues); + std::string query = builder.get(); + query += " RETURNING " + Derived::primary_key; + + try { + auto row = database->queryMap(query, {}); + if (!row.empty() && row.find(Derived::primary_key) != row.end()) { + attributes[Derived::primary_key] = row.at(Derived::primary_key); + } + afterSave(); + return true; + } + catch (const std::exception& e) { + Logger::log(e.what(), "ERROR"); + return false; + } + } + } + + static bool isFillable(const std::string& field) { + return std::find(Derived::fillable.begin(), Derived::fillable.end(), field) != Derived::fillable.end(); + } + + static bool isField(const std::string& field) { + return std::find(Derived::fields.begin(), Derived::fields.end(), field) != Derived::fields.end(); + } + + static std::shared_ptr create(const std::map& data, bool withFields = false, const std::shared_ptr& db = getDefaultDatabase()) { + auto model = std::make_shared(db); + for (const auto& [key, value] : data) { + if (!isField(key)) { + Logger::log("Field '" + key + "' is not a valid field (create)", "WARNING"); + continue; + } + + if (withFields || isFillable(key)) { + model->setAttribute(key, value); + } + else { + Logger::log("Field '" + key + "' is not fillable (create)", "WARNING"); + } + } + return model; + } + + static std::shared_ptr find(int id, const std::shared_ptr& db = getDefaultDatabase()) { + try { + auto model = std::make_shared(); + auto results = query().Where(Derived::primary_key + " = " + std::to_string(id)).Limit(1).get(); + if (results.empty()) { + Logger::log("No data found by id", "WARNING"); + return nullptr; + } + if (results.size() > 1) { + Logger::log("Found multiple records for single id", "ERROR"); + return nullptr; + } + + results.front()->setDatabase(db); + return results.front(); + } + catch (const std::exception& e) { + Logger::log("Error searching database (find()): " + std::string(e.what()), "ERROR"); + return nullptr; + } + } + + static void update(int id, const std::map& data, const std::shared_ptr& db = getDefaultDatabase()) { + try { + PGconn* conn = db->getConnection(); + if (!conn) { + Logger::log("Failed to get database connection", "ERROR"); + return; + } + SQLQueryBuilder builder(Derived::table_name); + std::map updateValues; + for (const auto& [key, value] : data) { + if (isField(key)) { + updateValues[key] = SQLString::EscapeString(conn, value); + } + } + if (updateValues.empty()) { + Logger::log("No valid fields provided for update", "WARNING"); + return; + } + builder.Update(updateValues).Where("id = " + SQLString::EscapeString(conn, std::to_string(id))); + db->execute(builder.get()); + } + catch (const std::exception& e) { + Logger::log("Update failed: " + std::string(e.what()), "ERROR"); + } + } + + void delete_() { + try { + if (!database) database = getDefaultDatabase(); + PGconn* conn = database->getConnection(); + if (!conn) { + Logger::log("Failed to get database connection", "ERROR"); + return; + } + SQLQueryBuilder builder(Derived::table_name); + auto id = getAttribute("id"); + if (id.empty()) { + Logger::log("ID attribute is missing", "ERROR"); + return; + } + builder.Delete().Where("id = " + SQLString::EscapeString(conn, id)); + std::string query = builder.get(); + database->execute(query); + } + catch (const std::exception& e) { + Logger::log("Delete failed: " + std::string(e.what()), "ERROR"); + } + } + + static void deleteById(int id, const std::shared_ptr& db = getDefaultDatabase()) { + try { + PGconn* conn = db->getConnection(); + if (!conn) { + Logger::log("Failed to get database connection", "ERROR"); + return; + } + SQLQueryBuilder builder(Derived::table_name); + builder.Delete().Where("id = " + SQLString::EscapeString(conn, std::to_string(id))); + std::string query = builder.get(); + db->execute(query); + } + catch (const std::exception& e) { + Logger::log("Delete by ID failed: " + std::string(e.what()), "ERROR"); + } + } + + static bool deleteWhere(const std::string& condition, const std::shared_ptr& db = getDefaultDatabase()) { + try { + PGconn* conn = db->getConnection(); + if (!conn) { + Logger::log("Failed to get database connection", "ERROR"); + return false; + } + SQLQueryBuilder builder(Derived::table_name); + builder.Delete().Where(condition); + std::string query = builder.get(); + db->execute(query); + + return true; + } + catch (const std::exception& e) { + Logger::log("Delete where failed: " + std::string(e.what()), "ERROR"); + return false; + } + } + + static Collection where(const std::string& condition, const std::shared_ptr& db = getDefaultDatabase()) { + auto items = query().Where(condition).get(); + for (const auto& item : items) { + item->setDatabase(db); + } + return Collection(items); + } + + + /* + @brief Save multiple models to the database in a single operation. + @param models A vector of shared pointers to the models to be saved. + @return True if all models were saved successfully, false otherwise. + @note This method will attempt to batch insert and update models based on their primary key. If a model has a primary key, it will be updated; otherwise, it will be inserted. + */ + static bool saveMany(const std::vector>& models, const std::shared_ptr& db = getDefaultDatabase()) { + PGconn* conn = db->getConnection(); + + if (!conn) { + Logger::log("Failed to get database connection", "ERROR"); + return false; + } + + if (models.empty()) { + Logger::log("No models to save", "WARNING"); + return false; + } + + const auto& first = models.front(); + + std::vector> toInsert; + std::vector> toUpdate; + + for (const auto& model : models) { + try { + std::string id = model->getAttribute(Derived::primary_key); + if (!id.empty()) { + toUpdate.push_back(model); + } + else { + toInsert.push_back(model); + } + } + catch (const std::exception& e) { + toInsert.push_back(model); + } + } + + if (!toUpdate.empty()) { + std::ostringstream batchQuery; + + for (const auto& model : toUpdate) { + try { + std::string id = model->getAttribute(Derived::primary_key); + + std::map updateData; + for (const auto& key : model->fields) { + if (key == Derived::primary_key) continue; + try { + std::string value = model->getAttribute(key); + if (!value.empty()) { + updateData[key] = value; + } + } + catch (const std::exception& e) {} + } + + if (updateData.empty()) continue; + + SQLQueryBuilder builder(Derived::table_name); + std::map escapedData; + for (const auto& [key, value] : updateData) { + escapedData[key] = SQLString::EscapeString(conn, value); + } + + builder.Update(escapedData).Where(Derived::primary_key + " = " + SQLString::EscapeString(conn, id)); + batchQuery << builder.get() << "; "; + } + catch (const std::exception& e) { + Logger::log("Failed to update model: " + std::string(e.what()), "ERROR"); + return false; + } + } + + std::string finalQuery = batchQuery.str(); + if (!finalQuery.empty()) { + try { + db->execute(finalQuery); + } + catch (const std::exception& e) { + Logger::log("Batch update error: " + std::string(e.what()), "ERROR"); + return false; + } + } + } + + if (!toInsert.empty()) { + std::vector columns; + for (const auto& key : first->fields) { + if (key == Derived::primary_key) continue; + try { + if (!first->getAttribute(key).empty()) { + columns.push_back(key); + } + } + catch (const std::exception& e) {} + } + + if (columns.empty()) { + Logger::log("No columns found for insert", "WARNING"); + return false; + } + + std::string valuesStr; + for (size_t i = 0; i < toInsert.size(); ++i) { + const auto& model = toInsert[i]; + std::string rowStr = "("; + + for (size_t j = 0; j < columns.size(); ++j) { + const std::string& col = columns[j]; + std::string value = model->getAttribute(col); + std::string escaped = SQLString::EscapeString(conn, value); + rowStr += escaped; + + if (j < columns.size() - 1) rowStr += ", "; + } + rowStr += ")"; + + if (i > 0) valuesStr += ", "; + valuesStr += rowStr; + } + + std::string query = "INSERT INTO " + Derived::table_name + " ("; + for (size_t i = 0; i < columns.size(); ++i) { + query += columns[i]; + if (i < columns.size() - 1) query += ", "; + } + query += ") VALUES " + valuesStr + ";"; + + try { + Logger::log("INSERT Query: " + query, "INFO"); + db->execute(query); + } + catch (const std::exception& e) { + Logger::log("Batch insert error: " + std::string(e.what()), "ERROR"); + return false; + } + } + + return true; + } + + static Collection all(const std::shared_ptr& db = getDefaultDatabase()) { + return Derived::where("1 = 1", db); + } + + static std::shared_ptr first(const std::shared_ptr& db = getDefaultDatabase()) { + auto results = query().Limit(1).get(); + if (results.empty()) return nullptr; + results.front->setDatabase(db); + return results.front(); + } + + static std::shared_ptr findOrFail(int id, const std::shared_ptr& db = getDefaultDatabase()) { + auto model = find(id, db); + if (!model) { + throw std::runtime_error("Model with id " + std::to_string(id) + " not found"); + } + return model; + } + + /* + @brief Finds the first model matching the given attributes or creates a new one if none exists. + @param attributes A map of attribute names and values to search for. + @param values A map of attribute names and values to set on the new model if it is created (default is an empty map). + @return A shared pointer to the found or newly created model. + @note This method first constructs a SQL condition based on the provided attributes and attempts to find an existing model that matches. If a matching model is found, it is returned. If no matching model exists, a new model is created with the provided attributes and values, and then returned. + */ + static std::shared_ptr firstOrCreate( + const std::map& attributes, + const std::map& values = {}, + const std::shared_ptr& db = getDefaultDatabase() + ) { + std::string condition; + for (const auto& [key, value] : attributes) { + if (!condition.empty()) condition += " AND "; + condition += key + " = '" + value + "'"; + } + + auto existing = where(condition, db); + if (!existing.empty()) return existing.front(db); + + auto allValues = attributes; + allValues.insert(values.begin(), values.end()); + return create(allValues, true, db); + } + + void fill(const std::map& data) { + for (const auto& [key, value] : data) { + if (isFillable(key)) { + setAttribute(key, value); + } + } + } + + std::map getAttributes() const { + return attributes; + } + + bool hasAttribute(const std::string& key) const { + return attributes.find(key) != attributes.end(); + } + + static int count(const std::string& condition = "1=1", const std::shared_ptr& db = getDefaultDatabase()) { + auto results = query() + .Select("COUNT(*) as total") + .Where(condition) + .get(); + // remark : Technically, models from query() doesn't need assigned `db` here unless the data is used and not just read. I assigned it for safety. + + for (const auto& row : results) row->setDatabase(db); + return results.empty() ? 0 : + std::stoi(results[0]->getAttribute("total")); + } + + static int max(const std::string& column, const std::string& condition = "1=1", const std::shared_ptr& db = getDefaultDatabase()) { + auto results = query() + .Select("MAX(" + column + ") as max") + .Where(condition) + .get(); + + for (const auto& row : results) row->setDatabase(db); + return results.empty() ? 0 : + std::stoi(results[0]->getAttribute("max")); + } + + static int sum(const std::string& column, const std::string& condition = "1=1", const std::shared_ptr& db = getDefaultDatabase()) { + auto results = query() + .Select("SUM(" + column + ") as sum") + .Where(condition) + .get(); + + for (const auto& row : results) row->setDatabase(db); + return results.empty() ? 0 : + std::stoi(results[0]->getAttribute("sum")); + } + + std::string operator[] (std::string key) { + return this->getAttribute(key); + } + + json toJson() const { + json j; + + for (const auto& [key, value] : attributes) { + j[key] = value; + } + + return j; + } + + /* + @brief Defines a belongs-to relationship between the current model and a related model. + @param foreignKey The foreign key in the current model that references the related model. + @param ownerKey The primary key in the related model that is referenced by the foreign key (default is the primary key of the related model). + @return A shared pointer to the related model that is associated with the current model, or nullptr if no related model is found. + @note This method retrieves the first instance of the related model where the owner key matches the value of the foreign key in the current model. If the foreign key value is empty, it returns nullptr. + */ + template + std::shared_ptr belongsTo( + const std::string& foreignKey, + const std::string& ownerKey = Derived::primary_key + ) { + auto foreignValue = getAttribute(foreignKey); + if (foreignValue.empty()) return nullptr; + return RelatedModel::where(ownerKey + " = " + foreignValue, database).front(); + } + + /* + @brief Defines a one-to-many relationship between the current model and a related model. + @param foreignKey The foreign key in the related model that references the current model. + @param localKey The local key in the current model that is referenced by the foreign key (default is the primary key of the current model). + @return A vector of shared pointers to the related models that are associated with the current model. + @note This method retrieves all instances of the related model where the foreign key matches the value of the local key in the current model. If the local key value is empty, it returns an empty vector. + */ + template + std::vector> hasMany( + const std::string& foreignKey, + const std::string& localKey = Derived::primary_key + ) { + auto localValue = getAttribute(localKey); + if (localValue.empty()) return {}; + return RelatedModel::where(foreignKey + " = " + localValue, database); + } + + /* + @brief Paginate the results of a query. + @param page The page number to retrieve (1-based index). + @param perPage The number of items per page. + @param condition Optional SQL condition to filter the results (default is "1=1" which means no filtering). + @return A pair containing a Collection of the paginated results and the total count of items matching the condition. + @note This method calculates the offset based on the page number and items per page, retrieves the corresponding items, and also counts the total number of items that match the given condition. + */ + static std::pair, int> paginate( + int page, + int perPage, + const std::string& condition = "1=1", + const std::shared_ptr& db = getDefaultDatabase() + ) { + int offset = (page - 1) * perPage; + auto items = query() + .Where(condition) + .Limit(perPage) + .Offset(offset) + .get(); + for (const auto& row : items) row->setDatabase(db); + + auto countQuery = query() + .Select("COUNT(*) as total") + .Where(condition) + .get(); + for (const auto& row : countQuery) row->setDatabase(db); + + int total = countQuery.empty() ? 0 : + std::stoi(countQuery[0]->getAttribute("total")); + + return { Collection(items), total }; + } + + void debugAttributes() { + for (const auto& field : fields) { + Logger::log("Field: " + field + " = " + this->getAttribute(field), "DEBUG"); + } + } + + static Collection collect(const std::vector>& items) { + return Collection(items); + } + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/Database/Models/ModelQueryBuilder.hpp b/brazier/include/brazier/Database/Models/ModelQueryBuilder.hpp new file mode 100644 index 0000000..4553e2a --- /dev/null +++ b/brazier/include/brazier/Database/Models/ModelQueryBuilder.hpp @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once +#include "../SQLQueryBuilder.hpp" +#include "../Database.hpp" +#include +#include + +namespace brazier { + + template + class ModelQueryBuilder : public SQLQueryBuilder { + public: + using SQLQueryBuilder::SQLQueryBuilder; + + ModelQueryBuilder& Select(const std::vector& columns) { + SQLQueryBuilder::Select(columns); + return *this; + } + ModelQueryBuilder& Select(const std::string column) { + SQLQueryBuilder::Select({ column }); + return *this; + } + ModelQueryBuilder& Select(const char* column) { + SQLQueryBuilder::Select({ std::string(column) }); + return *this; + } + ModelQueryBuilder& Where(const std::string& condition, const std::string& logicalOperator = "AND") { + SQLQueryBuilder::Where(condition, logicalOperator); + return *this; + } + ModelQueryBuilder& OrderBy(const std::map col_dir) { + SQLQueryBuilder::OrderBy(col_dir); + return *this; + } + ModelQueryBuilder& Limit(int value) { + SQLQueryBuilder::Limit(value); + return *this; + } + ModelQueryBuilder& Offset(int value) { + SQLQueryBuilder::Offset(value); + return *this; + } + + std::vector> get() { + auto database = std::make_shared(); + auto rows = database->queryToVector(this->getQuery()); + std::vector> result; + for (const auto& row : rows) { + auto model = Derived::create(row, true); + if (model) result.push_back(model); + } + return result; + } + private: + std::string getQuery() { + return SQLQueryBuilder::get(); + } + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/Database/Queue.hpp b/brazier/include/brazier/Database/Queue.hpp new file mode 100644 index 0000000..f361975 --- /dev/null +++ b/brazier/include/brazier/Database/Queue.hpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include "../vendor/Debug/Logger.hpp" + +namespace brazier { + + class Queue { + public: + static void connect(const std::string& host = "127.0.0.1", int port = 6379); + static void disconnect(); + static void push(const std::string& queue_name, const std::string& value); + static std::string pop(const std::string& queue_name); + static int64_t length(const std::string& queue_name); + + private: + static redisContext* context_; + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/Database/SQLQueryBuilder.hpp b/brazier/include/brazier/Database/SQLQueryBuilder.hpp new file mode 100644 index 0000000..f5609ca --- /dev/null +++ b/brazier/include/brazier/Database/SQLQueryBuilder.hpp @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace brazier { + + class SQLQueryBuilder { + private: + std::string table; + std::vector selectColumns; + std::vector whereConditions; + std::map orderByColumns; + std::vector groupByColumns; + std::string limit; + std::string offset; + std::map joins; + std::map insertValues; + std::map updateValues; + bool isDeleteQuery = false; + bool isFirstConditionInGroup = true; + + public: + explicit SQLQueryBuilder(const std::string& table); + SQLQueryBuilder& Select(const std::vector& columns); + SQLQueryBuilder& Delete(); + SQLQueryBuilder& Where(const std::string& condition, const std::string& logicalOperator = "AND"); + SQLQueryBuilder& BeginGroup(const std::string& logicalOperator = "AND"); + SQLQueryBuilder& EndGroup(); + SQLQueryBuilder& OrderBy(std::map col_dir); + SQLQueryBuilder& GroupBy(const std::vector& columns); + SQLQueryBuilder& Limit(int value); + SQLQueryBuilder& Offset(int value); + SQLQueryBuilder& Join(const std::string& table, const std::string& onCondition, const std::string& joinType = "INNER"); + SQLQueryBuilder& Insert(const std::map& values); + SQLQueryBuilder& Update(const std::map& values); + std::string get(); + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/Database/SQLSchemaBuilder.hpp b/brazier/include/brazier/Database/SQLSchemaBuilder.hpp new file mode 100644 index 0000000..03c59d6 --- /dev/null +++ b/brazier/include/brazier/Database/SQLSchemaBuilder.hpp @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include "Database.hpp" + +namespace brazier { + + class SQLSchemaBuilder { + private: + std::string table; + std::vector columns; + std::vector primaryKeys; + std::vector foreignKeys; + std::vector indexes; + std::vector uniqueConstraints; + std::vector dropColumns; + std::vector alterColumns; + + public: + explicit SQLSchemaBuilder(const std::string& table); + SQLSchemaBuilder& AddColumn(const std::string& columnDefinition); + SQLSchemaBuilder& AddPrimaryKey(const std::string& column); + SQLSchemaBuilder& AddForeignKey(const std::string& column, const std::string& referenceTable, const std::string& referenceColumn); + std::string AddIndex(const std::string& indexName, const std::vector& columns); + std::string AddUniqueConstraint(const std::string& constraintName, const std::vector& columns); + SQLSchemaBuilder& DropColumn(const std::string& column); + SQLSchemaBuilder& AlterColumn(const std::string& columnDefinition); + std::string CreateTable(); + std::string DropTable(); + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/Database/SQLString.hpp b/brazier/include/brazier/Database/SQLString.hpp new file mode 100644 index 0000000..0ad1cf4 --- /dev/null +++ b/brazier/include/brazier/Database/SQLString.hpp @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once +#include +#include + +namespace brazier { + + class SQLString { + public: + static std::string EscapeString(PGconn* conn, const std::string& input); + static std::string EscapeIdentifier(PGconn* conn, const std::string& input); + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/Engine.hpp b/brazier/include/brazier/Engine.hpp new file mode 100644 index 0000000..f126a2d --- /dev/null +++ b/brazier/include/brazier/Engine.hpp @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include +#include +#include "vendor/ConfigManager.hpp" + +namespace brazier { + class Engine { + public: + static boost::asio::io_context& get_io_context(); + static inline void init(boost::asio::io_context& io_ctx) { + Engine::io_ctx_ptr_ = &io_ctx; + } + + private: + static inline boost::asio::io_context* io_ctx_ptr_ = nullptr; + }; + + inline std::shared_ptr global_config = nullptr; +} \ No newline at end of file diff --git a/brazier/include/brazier/Filesystem/BaseDriver.hpp b/brazier/include/brazier/Filesystem/BaseDriver.hpp new file mode 100644 index 0000000..ccae9ce --- /dev/null +++ b/brazier/include/brazier/Filesystem/BaseDriver.hpp @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include +#include +#include +#include + +namespace brazier { + + class BaseDriver { + public: + virtual ~BaseDriver() = default; + virtual void initAsync(size_t threadCount = std::thread::hardware_concurrency()) = 0; + virtual void setRootPath(const std::string& path) = 0; + virtual std::string getRootPath() const = 0; + virtual void put(const std::string& path, const std::string& content) = 0; + virtual std::string get(const std::string& path) = 0; + virtual bool exists(const std::string& path) const = 0; + virtual void deleteFile(const std::string& path) = 0; + virtual void copy(const std::string& source, const std::string& destination) = 0; + virtual void move(const std::string& source, const std::string& destination) = 0; + virtual boost::asio::awaitable putAsync(const std::string& path, const std::string& content) = 0; + virtual boost::asio::awaitable getAsync(const std::string& path) = 0; + virtual boost::asio::awaitable existsAsync(const std::string& path) const = 0; + virtual boost::asio::awaitable deleteFileAsync(const std::string& path) = 0; + virtual boost::asio::awaitable copyAsync(const std::string& source, const std::string& destination) = 0; + virtual boost::asio::awaitable moveAsync(const std::string& source, const std::string& destination) = 0; + virtual std::string getDriverType() const = 0; + virtual bool isAsyncSupported() const = 0; + }; + + constexpr auto S3 = "S3"; + constexpr auto File = "Filesystem"; + constexpr auto FTP = "FTP"; + constexpr auto WebDAV = "WebDAV"; +} \ No newline at end of file diff --git a/brazier/include/brazier/Filesystem/FileDriver.hpp b/brazier/include/brazier/Filesystem/FileDriver.hpp new file mode 100644 index 0000000..988959c --- /dev/null +++ b/brazier/include/brazier/Filesystem/FileDriver.hpp @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include "BaseDriver.hpp" +#include +#include +#include +#include +#include +#include + +namespace brazier { + + namespace fs = std::filesystem; + + class FileDriver : public BaseDriver { + private: + std::string rootPath_; + std::shared_ptr ioPool_; + bool asyncInitialized_ = false; + + public: + FileDriver() = default; + ~FileDriver() override = default; + + void initAsync(size_t threadCount = std::thread::hardware_concurrency()) override; + void setRootPath(const std::string& path) override; + std::string getRootPath() const override; + + void put(const std::string& path, const std::string& content) override; + std::string get(const std::string& path) override; + bool exists(const std::string& path) const override; + void deleteFile(const std::string& path) override; + void copy(const std::string& source, const std::string& destination) override; + void move(const std::string& source, const std::string& destination) override; + + boost::asio::awaitable putAsync(const std::string& path, const std::string& content) override; + boost::asio::awaitable getAsync(const std::string& path) override; + boost::asio::awaitable existsAsync(const std::string& path) const override; + boost::asio::awaitable deleteFileAsync(const std::string& path) override; + boost::asio::awaitable copyAsync(const std::string& source, const std::string& destination) override; + boost::asio::awaitable moveAsync(const std::string& source, const std::string& destination) override; + + std::string getDriverType() const override { return brazier::File; } + bool isAsyncSupported() const override { return true; } + + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/Filesystem/Filesystem.hpp b/brazier/include/brazier/Filesystem/Filesystem.hpp new file mode 100644 index 0000000..aa62c40 --- /dev/null +++ b/brazier/include/brazier/Filesystem/Filesystem.hpp @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include "BaseDriver.hpp" +#include "FileDriver.hpp" +#include "StorageManager.hpp" +#include +#include + +namespace brazier { + + class Filesystem { + private: + std::shared_ptr driver_; + std::string driverName_; + + public: + Filesystem(); + explicit Filesystem(const std::string& name); + explicit Filesystem(std::shared_ptr driver); + + static Filesystem driver(const std::string& name); + static bool hasDriver(const std::string& name); + + std::string getDriverName() const; + std::string getDriverType() const; + std::string get(const std::string& path); + + bool exists(const std::string& path) const; + void put(const std::string& path, const std::string& content); + void deleteFile(const std::string& path); + void copy(const std::string& source, const std::string& destination); + void move(const std::string& source, const std::string& destination); + + boost::asio::awaitable putAsync(const std::string& path, const std::string& content); + boost::asio::awaitable getAsync(const std::string& path); + boost::asio::awaitable existsAsync(const std::string& path) const; + boost::asio::awaitable deleteFileAsync(const std::string& path); + boost::asio::awaitable copyAsync(const std::string& source, const std::string& destination); + boost::asio::awaitable moveAsync(const std::string& source, const std::string& destination); + + std::shared_ptr getDriver() const; + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/Filesystem/S3Driver.hpp b/brazier/include/brazier/Filesystem/S3Driver.hpp new file mode 100644 index 0000000..f3f4765 --- /dev/null +++ b/brazier/include/brazier/Filesystem/S3Driver.hpp @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include "BaseDriver.hpp" +#include +#include + +namespace brazier { + + namespace fs = std::filesystem; + + class S3Driver : public BaseDriver { + private: + std::string rootPath_; + std::shared_ptr ioPool_; + bool asyncInitialized_ = false; + + public: + S3Driver() = default; + ~S3Driver() override = default; + + void initAsync(size_t threadCount = std::thread::hardware_concurrency()) override; + void setRootPath(const std::string& path) override; + std::string getRootPath() const override; + + void put(const std::string& path, const std::string& content) override; + std::string get(const std::string& path) override; + bool exists(const std::string& path) const override; + void deleteFile(const std::string& path) override; + void copy(const std::string& source, const std::string& destination) override; + void move(const std::string& source, const std::string& destination) override; + + boost::asio::awaitable putAsync(const std::string& path, const std::string& content) override; + boost::asio::awaitable getAsync(const std::string& path) override; + boost::asio::awaitable existsAsync(const std::string& path) const override; + boost::asio::awaitable deleteFileAsync(const std::string& path) override; + boost::asio::awaitable copyAsync(const std::string& source, const std::string& destination) override; + boost::asio::awaitable moveAsync(const std::string& source, const std::string& destination) override; + + std::string getDriverType() const override { return S3; } + bool isAsyncSupported() const override { return true; } + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/Filesystem/StorageManager.hpp b/brazier/include/brazier/Filesystem/StorageManager.hpp new file mode 100644 index 0000000..4fe1ac4 --- /dev/null +++ b/brazier/include/brazier/Filesystem/StorageManager.hpp @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include "BaseDriver.hpp" +#include +#include +#include +#include "BaseDriver.hpp" + +namespace brazier { + + class StorageManager { + private: + StorageManager() = default; + ~StorageManager() = default; + + std::unordered_map> drivers_; + std::string defaultDriverName_; + mutable std::mutex mutex_; + + public: + static StorageManager& getInstance(); + + void registerDriver(const std::string& name, std::shared_ptr driver); + void unregisterDriver(const std::string& name); + void setDefaultDriver(const std::string& name); + + std::shared_ptr getDefaultDriver() const; + std::shared_ptr getDriver(const std::string& name) const; + + bool hasDriver(const std::string& name) const; + + std::vector getDriverNames() const; + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/Http b/brazier/include/brazier/Http new file mode 100644 index 0000000..369001b --- /dev/null +++ b/brazier/include/brazier/Http @@ -0,0 +1,10 @@ +#pragma once + +#include "App/Http/Controllers/Controller.hpp" +#include "App/Http/Helpers/Code.hpp" +#include "App/Http/Helpers/Cookie.hpp" +#include "App/Http/Helpers/HttpClient.hpp" +#include "App/Http/Helpers/Validator.hpp" +#include "App/Http/Middlewares/Middleware.hpp" +#include "App/Http/Middlewares/MiddlewarePipeline.hpp" +#include "App/Http/Middlewares/CorsMiddleware.hpp" \ No newline at end of file diff --git a/brazier/include/brazier/Router/Route.hpp b/brazier/include/brazier/Router/Route.hpp new file mode 100644 index 0000000..7a6c36e --- /dev/null +++ b/brazier/include/brazier/Router/Route.hpp @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +namespace beast = boost::beast; +namespace http = beast::http; + +using Request = http::request; +using Response = http::response; +using Params = std::unordered_map; + +namespace brazier { + + class Route { + std::regex pathRegex_; + std::vector paramNames_; + std::string originalPath_; + std::function(const Request&, Response&, const Params&)> handler_; + + public: + Route(const std::string& path, + std::function(const Request&, Response&, const Params&)> handler); + + bool match(const std::string& target, Params& outParams) const; + boost::asio::awaitable process(const Request& req, Response& res, const Params& params) const; + const std::string& path() const; + + private: + static std::regex convertPathToRegex(const std::string& path); + static std::vector extractParamNames(const std::string& path); + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/Router/Router.hpp b/brazier/include/brazier/Router/Router.hpp new file mode 100644 index 0000000..0c5dc75 --- /dev/null +++ b/brazier/include/brazier/Router/Router.hpp @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once +#include +#include +#include +#include +#include +#include +#include "Route.hpp" + +namespace brazier { + + namespace beast = boost::beast; + namespace http = beast::http; + + class Router { + using Request = http::request; + using Response = http::response; + using Params = std::unordered_map; + + static inline std::unordered_map> routes_; + + public: + static void add(http::verb method, const std::string& path, + std::function(const Request&, Response&, const Params&)> handler); + + static boost::asio::awaitable handle_request(const Request& req, Response& res); + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/Router/RouterRegisterer.hpp b/brazier/include/brazier/Router/RouterRegisterer.hpp new file mode 100644 index 0000000..2947a98 --- /dev/null +++ b/brazier/include/brazier/Router/RouterRegisterer.hpp @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once +#include +#include "Router.hpp" +#include "WSRouter.hpp" +#include +#include "types.hpp" + +namespace brazier { + + class RouterRegisterer { + using Request = http::request; + using Response = http::response; + using Params = std::unordered_map; + + public: + static void init(boost::asio::io_context& io); + }; + + #define R(method, path, controller, handler) \ + Router::add(method, path, [controller](const Request& req, Response& res, const Params& params) -> boost::asio::awaitable { \ + co_await controller->handler(req, res, params); \ + }) + + #define CORS(path, controller) \ + Router::add(OPTIONS, path, [controller](const Request& req, Response& res, const Params& params) -> boost::asio::awaitable { \ + controller->setCors(req, res); \ + co_return; \ + }) + + #define WS_R(path, controller, handler) \ + brazier::WebSocketRouter::instance().add_route(path, \ + [controller](std::shared_ptr session, const brazier::Params& params) { \ + controller->handler(session, params); \ + }) + + #define WS_MSG(path, controller, handler) \ + { \ + brazier::Params dummy_params; \ + auto* route = brazier::WebSocketRouter::instance().find_route(path, dummy_params); \ + if (route) { \ + route->set_message_handler([controller](const std::string& msg, std::shared_ptr session) { \ + controller->handler(msg, session); \ + }); \ + } else { \ + } \ + } + + #define WS_BIN(path, controller, handler) \ + { \ + brazier::Params dummy_params; \ + auto* route = brazier::WebSocketRouter::instance().find_route(path, dummy_params); \ + if (route) { \ + route->set_binary_handler([controller](std::vector&& data, std::shared_ptr session) { \ + controller->handler(std::move(data), session); \ + }); \ + } \ + } + + #define WS_CLOSE(path, controller, handler) \ + { \ + brazier::Params dummy_params; \ + auto* route = brazier::WebSocketRouter::instance().find_route(path, dummy_params); \ + if (route) { \ + route->set_close_handler([controller](std::shared_ptr session) { \ + controller->handler(session); \ + }); \ + } \ + } + + #define WS_ERR(path, controller, handler) \ + { \ + brazier::Params dummy_params; \ + auto* route = brazier::WebSocketRouter::instance().find_route(path, dummy_params); \ + if (route) { \ + route->set_error_handler([controller](const std::string& error, std::shared_ptr session) { \ + controller->handler(error, session); \ + }); \ + } \ + } + + #define WS_G_MSG(controller, handler) \ + brazier::WebSocketRouter::instance().set_global_message_handler( \ + [controller](const std::string& msg, std::shared_ptr session) { \ + controller->handler(msg, session); \ + }) + + #define WS_G_BIN(controller, handler) \ + brazier::WebSocketRouter::instance().set_global_binary_handler( \ + [controller](std::vector&& data, std::shared_ptr session) { \ + controller->handler(std::move(data), session); \ + }) + + #define WS_G_CLOSE(controller, handler) \ + brazier::WebSocketRouter::instance().set_global_close_handler( \ + [controller](std::shared_ptr session) { \ + controller->handler(session); \ + }) + + #define WS_G_ERR(controller, handler) \ + brazier::WebSocketRouter::instance().set_global_error_handler( \ + [controller](const std::string& error, std::shared_ptr session) { \ + controller->handler(error, session); \ + }) +} \ No newline at end of file diff --git a/brazier/include/brazier/Router/WSRoute.hpp b/brazier/include/brazier/Router/WSRoute.hpp new file mode 100644 index 0000000..374f5cd --- /dev/null +++ b/brazier/include/brazier/Router/WSRoute.hpp @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include "../vendor/WebSocket/WebSocketSession.hpp" + +namespace brazier { + + using Params = std::unordered_map; + + class WebSocketRoute { + public: + using ConnectHandler = std::function, const Params&)>; + + WebSocketRoute(const std::string& path, ConnectHandler handler); + + bool match(const std::string& target, Params& outParams) const; + void on_connect(std::shared_ptr session, const Params& params) const; + + const std::string& path() const; + + void set_message_handler(WebSocketSession::MessageHandler handler); + void set_binary_handler(WebSocketSession::BinaryHandler handler); + void set_close_handler(WebSocketSession::CloseHandler handler); + void set_error_handler(WebSocketSession::ErrorHandler handler); + + WebSocketSession::MessageHandler get_message_handler() const; + WebSocketSession::BinaryHandler get_binary_handler() const; + WebSocketSession::CloseHandler get_close_handler() const; + WebSocketSession::ErrorHandler get_error_handler() const; + + private: + std::regex convertPathToRegex(const std::string& path); + std::vector extractParamNames(const std::string& path); + + std::string originalPath_; + std::regex pathRegex_; + std::vector paramNames_; + ConnectHandler connect_handler_; + + WebSocketSession::MessageHandler message_handler_; + WebSocketSession::BinaryHandler binary_handler_; + WebSocketSession::CloseHandler close_handler_; + WebSocketSession::ErrorHandler error_handler_; + }; + +} \ No newline at end of file diff --git a/brazier/include/brazier/Router/WSRouter.hpp b/brazier/include/brazier/Router/WSRouter.hpp new file mode 100644 index 0000000..9fa2059 --- /dev/null +++ b/brazier/include/brazier/Router/WSRouter.hpp @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include "WSRoute.hpp" +#include +#include + +namespace brazier { + + class WebSocketRouter { + public: + static WebSocketRouter& instance(); + + void add_route(const std::string& path, WebSocketRoute::ConnectHandler handler); + + WebSocketRoute* find_route(const std::string& target, Params& outParams); + + void set_global_message_handler(WebSocketSession::MessageHandler handler); + void set_global_binary_handler(WebSocketSession::BinaryHandler handler); + void set_global_close_handler(WebSocketSession::CloseHandler handler); + void set_global_error_handler(WebSocketSession::ErrorHandler handler); + + WebSocketSession::MessageHandler get_global_message_handler() const; + WebSocketSession::BinaryHandler get_global_binary_handler() const; + WebSocketSession::CloseHandler get_global_close_handler() const; + WebSocketSession::ErrorHandler get_global_error_handler() const; + + void clear(); + + private: + WebSocketRouter() = default; + + std::vector routes_; + + WebSocketSession::MessageHandler global_message_handler_; + WebSocketSession::BinaryHandler global_binary_handler_; + WebSocketSession::CloseHandler global_close_handler_; + WebSocketSession::ErrorHandler global_error_handler_; + }; + +} \ No newline at end of file diff --git a/brazier/include/brazier/Router/types.hpp b/brazier/include/brazier/Router/types.hpp new file mode 100644 index 0000000..3ca3f19 --- /dev/null +++ b/brazier/include/brazier/Router/types.hpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include + +namespace brazier { + + namespace beast = boost::beast; + namespace http = beast::http; + + constexpr auto GET = http::verb::get; + constexpr auto POST = http::verb::post; + constexpr auto PUT = http::verb::put; + constexpr auto PATCH = http::verb::patch; + constexpr auto DELETE_ = http::verb::delete_; + constexpr auto OPTIONS = http::verb::options; + +} \ No newline at end of file diff --git a/brazier/include/brazier/WebSocketServer.hpp b/brazier/include/brazier/WebSocketServer.hpp new file mode 100644 index 0000000..4ed3add --- /dev/null +++ b/brazier/include/brazier/WebSocketServer.hpp @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "vendor/Handlers/ENV.hpp" +#include "vendor/Debug/Logger.hpp" +#include "vendor/WebSocket/WebSocketSession.hpp" +#include "vendor/WebSocket/WebSocketManager.hpp" +#include "Router/WSRouter.hpp" + +namespace beast = boost::beast; +namespace http = beast::http; +namespace net = boost::asio; +using tcp = net::ip::tcp; +using namespace std::chrono_literals; + +namespace brazier { + + class WebSocketServer { + private: + net::io_context io_; + tcp::acceptor acceptor_; + unsigned short port_; + std::string host_; + std::vector threads_; + std::unique_ptr> work_guard_; + std::atomic connection_count_{ 0 }; + + public: + WebSocketServer(const std::string& host, unsigned short port); + + bool initialize(); + void run(); + void stop(); + + WebSocketManager& get_websocket_manager(); + + void set_websocket_message_handler(WebSocketSession::MessageHandler handler); + void set_websocket_binary_handler(WebSocketSession::BinaryHandler handler); + void set_websocket_close_handler(WebSocketSession::CloseHandler handler); + void set_websocket_error_handler(WebSocketSession::ErrorHandler handler); + + void add_route(const std::string& path, WebSocketRoute::ConnectHandler handler); + + WebSocketRoute* set_route_message_handler(const std::string& path, WebSocketSession::MessageHandler handler); + WebSocketRoute* set_route_binary_handler(const std::string& path, WebSocketSession::BinaryHandler handler); + WebSocketRoute* set_route_close_handler(const std::string& path, WebSocketSession::CloseHandler handler); + WebSocketRoute* set_route_error_handler(const std::string& path, WebSocketSession::ErrorHandler handler); + + private: + std::string extract_path(const http::request& req); + net::awaitable handle_websocket(tcp::socket socket); + net::awaitable handle_connection(tcp::socket socket); + net::awaitable accept_loop(); + }; + +} \ No newline at end of file diff --git a/brazier/include/brazier/server.hpp b/brazier/include/brazier/server.hpp new file mode 100644 index 0000000..d9d41c3 --- /dev/null +++ b/brazier/include/brazier/server.hpp @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "vendor/Handlers/ENV.hpp" +#include "Database/Queue.hpp" +#include "Database/Cache.hpp" +#include "Database/Migrations/MigrationManager.hpp" +#include "Router/RouterRegisterer.hpp" +#include "Router/Router.hpp" +#include "Engine.hpp" +#include "Filesystem/Filesystem.hpp" +#include "vendor/ConfigManager.hpp" + +namespace beast = boost::beast; +namespace http = beast::http; +namespace net = boost::asio; +using tcp = net::ip::tcp; +using namespace std::chrono_literals; + +namespace brazier { + + class Server { + private: + net::io_context io_; + tcp::acceptor acceptor_; + unsigned short port_; + std::string host_; + std::vector threads_; + std::unique_ptr> work_guard_; + + std::atomic connection_count_{ 0 }; + std::atomic total_requests_{ 0 }; + + public: + Server(const std::string& host, unsigned short port); + + bool initialize(); + + void run(); + void stop(); + + unsigned short getPort() const; + const std::string& getHost() const; + + private: + void initializeConnections(); + + net::awaitable handle_connection(tcp::socket socket); + net::awaitable accept_loop(); + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/vendor/ConfigManager.hpp b/brazier/include/brazier/vendor/ConfigManager.hpp new file mode 100644 index 0000000..763e1c5 --- /dev/null +++ b/brazier/include/brazier/vendor/ConfigManager.hpp @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include +#include +#include "../Filesystem/Filesystem.hpp" +#include +#include +#include +#include +#include +#include "Debug/Logger.hpp" + +namespace brazier { + + using json = nlohmann::json; + + class ConfigManager { + private: + std::shared_ptr driver_; + std::string configFilePath_; + std::unordered_map config_; + mutable std::mutex mutex_; + bool autoSave_ = false; + + template + std::string toString(const T& value) { + if constexpr (std::is_same_v) { + return value; + } + else if constexpr (std::is_same_v) { + return std::string(value); + } + else if constexpr (std::is_same_v) { + return std::string(value); + } + else { + json j = value; + return j.dump(); + } + } + + template + T fromString(const std::string& str) { + if constexpr (std::is_same_v) { + return str; + } + else { + json j = json::parse(str); + return j.get(); + } + } + + void flattenJson(const std::string& prefix, const json& j, + std::unordered_map& output); + json unflattenMap(const std::unordered_map& map) const; + + public: + ConfigManager(const std::string& configFilePath = "config.json", + std::shared_ptr driver = nullptr); + + ConfigManager(const std::string& configFilePath = "config.json", + const std::string& driverName = "local"); + + ~ConfigManager() = default; + + void load(); + void save(); + + template + T get(const std::string& key, const T& defaultValue = T{}) { + std::lock_guard lock(mutex_); + auto it = config_.find(key); + if (it != config_.end()) { + try { + return fromString(it->second); + } + catch (std::exception& e) { + Logger::log(e.what(), "ERROR"); + return defaultValue; + } + } + return defaultValue; + } + + std::string get(const std::string key, const char* defaultValue) { + return get(key, std::string(defaultValue)); + } + + template + std::string get(const std::string key, const char (&defaultValue)[N]) { + return get(key, std::string(defaultValue, N - 1)); + } + + template + void set(const std::string& key, const T& value) { + std::lock_guard lock(mutex_); + config_[key] = toString(value); + if (autoSave_) { + save(); + } + } + + bool has(const std::string& key); + bool remove(const std::string& key); + std::vector getKeys() const; + json getAllAsJson() const; + void loadFromJson(const json& j); + void clear(); + void setAutoSave(bool autoSave); + void reload(); + std::unordered_map getRawData() const; + + bool hasNested(const std::string& key) const; + std::vector getKeysNested() const; + void loadFromJsonNested(const json& j); + json getNestedJson(const std::string& prefix = "") const; + + json getJson(const std::string& key) const; + + template + T getNested(const std::string& path, const T& defaultValue = T{}); + + template + void setNested(const std::string& path, const T& value); + + bool hasNestedPath(const std::string& path) const; + bool removeNested(const std::string& path); + std::vector getKeysWithPrefix(const std::string& prefix) const; + + static void initGlobal(); + static void initGlobal(std::string filepath); + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/vendor/Cryptography/AES.hpp b/brazier/include/brazier/vendor/Cryptography/AES.hpp new file mode 100644 index 0000000..765f911 --- /dev/null +++ b/brazier/include/brazier/vendor/Cryptography/AES.hpp @@ -0,0 +1,38 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +namespace brazier::crypto { + + class AES256 { + public: + static constexpr size_t KEY_SIZE = 32; + static constexpr size_t BLOCK_SIZE = 16; + static constexpr size_t SALT_SIZE = 16; + static constexpr size_t HMAC_SIZE = 32; + + static std::string generateKey(); + static std::string generateIV(); + static std::string generateSalt(); + static std::string deriveKeyFromPassword(const std::string& password, const std::string& salt, int iterations = 100000); + static std::string encrypt(const std::string& plaintext, const std::string& key, const std::string& iv); + static std::string decrypt(const std::string& ciphertext, const std::string& key, const std::string& iv); + static std::string encryptWithHmac(const std::string& plaintext, const std::string& key, const std::string& iv); + static std::string decryptWithHmac(const std::string& data, const std::string& key, const std::string& iv); + static std::string encryptMessage(const std::string& message, const std::string& key); + static std::string decryptMessage(const std::string& encryptedData, const std::string& key); + static std::string toHex(const std::string& binary); + static std::string fromHex(const std::string& hex); + static bool validateKey(const std::string& key); + static bool validateIV(const std::string& iv); + + private: + static std::string generateRandomBytes(int size); + static std::string computeHmac(const std::string& data, const std::string& key); + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/vendor/Cryptography/RSA.hpp b/brazier/include/brazier/vendor/Cryptography/RSA.hpp new file mode 100644 index 0000000..35f634c --- /dev/null +++ b/brazier/include/brazier/vendor/Cryptography/RSA.hpp @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace brazier::crypto { + + class RSA { + public: + static constexpr size_t KEY_SIZE_2048 = 2048; + static constexpr size_t KEY_SIZE_4096 = 4096; + + static std::pair generateKeyPair(int bits = KEY_SIZE_2048); + static std::string encryptWithPublic(const std::string& plaintext, const std::string& publicKey); + static std::string decryptWithPrivate(const std::string& ciphertext, const std::string& privateKey); + static std::string encryptWithPrivate(const std::string& plaintext, const std::string& privateKey); + static std::string decryptWithPublic(const std::string& ciphertext, const std::string& publicKey); + static std::string sign(const std::string& data, const std::string& privateKey); + static bool verify(const std::string& data, const std::string& signature, const std::string& publicKey); + static bool validatePublicKey(const std::string& publicKey); + static bool validatePrivateKey(const std::string& privateKey); + static std::string extractPublicKey(const std::string& privateKey); + static std::string formatPublicKey(const std::string& pem); + static std::string formatPrivateKey(const std::string& pem); + + private: + static std::string bioToString(BIO* bio); + static std::string getLastOpenSSLError(); + static EVP_PKEY* loadPublicKey(const std::string& pem); + static EVP_PKEY* loadPrivateKey(const std::string& pem); + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/vendor/Debug/Logger.hpp b/brazier/include/brazier/vendor/Debug/Logger.hpp new file mode 100644 index 0000000..b1ff8bd --- /dev/null +++ b/brazier/include/brazier/vendor/Debug/Logger.hpp @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once +#pragma warning(disable : 4996) + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace brazier { + + class Logger { + private: + static FILE* logFile; + static std::mutex logMutex; + static std::filesystem::path logFilePath; + static size_t maxLines; + static size_t currentLineCount; + static inline bool colorsEnabled = true; + + static thread_local std::array timeCache; + static thread_local std::chrono::system_clock::time_point lastTimeUpdate; + + static void signalHandler(int signal); + static void rotateLogs(); + static const char* getCurrentTimeFast(); + static const char* getColorCodeFast(const char* level); + static const char* resetColorFast(); + + public: + Logger() = delete; + Logger(const Logger&) = delete; + Logger& operator=(const Logger&) = delete; + + static void init(const std::string& filename); + static void log(const std::string& message, const char* level = "INFO"); + static void log(const char* message, const char* level = "INFO"); + static void shutdown(); + static void registerSignalHandlers(); + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/vendor/Facades/DatabaseFacade.hpp b/brazier/include/brazier/vendor/Facades/DatabaseFacade.hpp new file mode 100644 index 0000000..071713f --- /dev/null +++ b/brazier/include/brazier/vendor/Facades/DatabaseFacade.hpp @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +class DatabaseFacade { + +public: + ~DatabaseFacade() = default; + +}; \ No newline at end of file diff --git a/brazier/include/brazier/vendor/Facades/Hash.hpp b/brazier/include/brazier/vendor/Facades/Hash.hpp new file mode 100644 index 0000000..cdd6070 --- /dev/null +++ b/brazier/include/brazier/vendor/Facades/Hash.hpp @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include "../Debug/Logger.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace brazier { + + class Hash { + public: + static inline uint32_t iterations = 10; + static inline uint32_t memory_cost = 16; + static inline uint32_t parallelism = 1; + static inline uint32_t output_length = 32; + + static std::vector generateSalt(int length); + static std::pair> hash(const std::string& password, std::vector salt = {}); + static std::vector hexStringToBytes(const std::string& hex); + static std::string bytesToHexString(const std::vector& bytes); + static bool verify(const std::string& password, const std::string& stored_hash, const std::vector& salt); + static boost::asio::awaitable>> awaitableHash(const std::string& password, std::vector salt = {}); + static boost::asio::awaitable awaitableVerify(const std::string& password, const std::string& stored_hash, const std::vector& salt); + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/vendor/Handlers/ENV.hpp b/brazier/include/brazier/vendor/Handlers/ENV.hpp new file mode 100644 index 0000000..8b2f043 --- /dev/null +++ b/brazier/include/brazier/vendor/Handlers/ENV.hpp @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace brazier { + + class ENV { + public: + static inline std::unordered_map env_variables; + static bool initialized; + static const std::string env_file_path; + + static void initialize(); + static std::string get(const std::string& key); + }; +} \ No newline at end of file diff --git a/brazier/include/brazier/vendor/Storage/Storage.hpp b/brazier/include/brazier/vendor/Storage/Storage.hpp new file mode 100644 index 0000000..205fb7a --- /dev/null +++ b/brazier/include/brazier/vendor/Storage/Storage.hpp @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace brazier { + + namespace fs = std::filesystem; + + class Storage { + private: + Storage() = default; + Storage(const Storage&) = delete; + Storage& operator=(const Storage&) = delete; + + std::string rootPath_; + std::shared_ptr ioPool_; + + public: + static Storage& getInstance(); + void initAsync(size_t threadCount = std::thread::hardware_concurrency()); + void setRootPath(const std::string& path); + std::string getRootPath() const; + + void put(const std::string& path, const std::string& content); + std::string get(const std::string& path); + bool exists(const std::string& path) const; + void deleteFile(const std::string& path); + void copy(const std::string& source, const std::string& destination); + void move(const std::string& source, const std::string& destination); + boost::asio::awaitable putAsync(const std::string& path, const std::string& content); + boost::asio::awaitable getAsync(const std::string& path); + boost::asio::awaitable existsAsync(const std::string& path) const; + boost::asio::awaitable deleteFileAsync(const std::string& path); + boost::asio::awaitable copyAsync(const std::string& source, const std::string& destination); + boost::asio::awaitable moveAsync(const std::string& source, const std::string& destination); + }; + +} \ No newline at end of file diff --git a/brazier/include/brazier/vendor/WebSocket/WebSocketManager.hpp b/brazier/include/brazier/vendor/WebSocket/WebSocketManager.hpp new file mode 100644 index 0000000..b387566 --- /dev/null +++ b/brazier/include/brazier/vendor/WebSocket/WebSocketManager.hpp @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include "../include/brazier/vendor/WebSocket/WebSocketSession.hpp" +#include +#include +#include +#include + +namespace brazier { + + class WebSocketManager { + public: + static WebSocketManager& instance(); + + void add_session(std::shared_ptr session); + void remove_session(std::shared_ptr session); + + void broadcast(const std::string& message); + void broadcast_binary(const std::vector& data); + + void close_all(); + + std::shared_ptr get_session(const std::string& session_id); + + size_t get_session_count() const { return session_count_.load(std::memory_order_acquire); } + + void set_global_message_handler(WebSocketSession::MessageHandler handler); + void set_global_binary_handler(WebSocketSession::BinaryHandler handler); + void set_global_close_handler(WebSocketSession::CloseHandler handler); + void set_global_error_handler(WebSocketSession::ErrorHandler handler); + + private: + WebSocketManager() = default; + ~WebSocketManager() = default; + WebSocketManager(const WebSocketManager&) = delete; + WebSocketManager& operator=(const WebSocketManager&) = delete; + + std::vector> sessions_; + mutable std::shared_mutex mutex_; + std::atomic session_count_{ 0 }; + + WebSocketSession::MessageHandler global_message_handler_; + WebSocketSession::BinaryHandler global_binary_handler_; + WebSocketSession::CloseHandler global_close_handler_; + WebSocketSession::ErrorHandler global_error_handler_; + }; + +} \ No newline at end of file diff --git a/brazier/include/brazier/vendor/WebSocket/WebSocketSession.hpp b/brazier/include/brazier/vendor/WebSocket/WebSocketSession.hpp new file mode 100644 index 0000000..2535f15 --- /dev/null +++ b/brazier/include/brazier/vendor/WebSocket/WebSocketSession.hpp @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2026 Kirill Sergeev, Nikolay Sugonyako, Andrey Agarkov, Gleb Safyannikov + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of brazier. + * + * brazier is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * brazier is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with brazier; if not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace beast = boost::beast; +namespace websocket = beast::websocket; +namespace net = boost::asio; +namespace http = beast::http; +using tcp = net::ip::tcp; + +namespace brazier { + + class WebSocketSession : public std::enable_shared_from_this { + public: + using MessageHandler = std::function)>; + using BinaryHandler = std::function&&, std::shared_ptr)>; + using CloseHandler = std::function)>; + using ErrorHandler = std::function)>; + + explicit WebSocketSession(tcp::socket&& socket); + ~WebSocketSession(); + + net::awaitable run(); + + void send(std::string message); + void send_binary(std::vector data); + void close(websocket::close_reason reason = {}); + + bool accept_handshake(const http::request& req, beast::error_code& ec); + + void set_message_handler(MessageHandler handler); + void set_binary_handler(BinaryHandler handler); + void set_close_handler(CloseHandler handler); + void set_error_handler(ErrorHandler handler); + + const char* get_id() const; + bool is_open() const; + + private: + static uint64_t generate_session_id(); + + websocket::stream ws_; + uint64_t session_id_; + char session_id_str_[32]; + + MessageHandler message_handler_; + BinaryHandler binary_handler_; + CloseHandler close_handler_; + ErrorHandler error_handler_; + + void fast_itoa(uint64_t num, char* buf); + }; + +} \ No newline at end of file diff --git a/brazier/shaman b/brazier/shaman new file mode 100644 index 0000000000000000000000000000000000000000..04fa567073a624ecd607bfefb29779f789d8d67c GIT binary patch literal 205464 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 literal 0 HcmV?d00001 diff --git a/brazier/shaman.exe b/brazier/shaman.exe new file mode 100644 index 0000000000000000000000000000000000000000..55790450429d53bd001fb9651c781d7bd1fffd8b GIT binary patch literal 199168 zcmd?S33yc1`S?E}2^j^vgBp!n86|2ksL{Bfu{r|@+>r@H0l^9qMI%S*ixC9prZu9@V=iJ#yg3$W^J$-&Z9^>5Q z+;iUTyyt!2^PY3cf7Ia0bh%tv{KaA}*VFvUKcD*lcl>-Lubnbg>h1}XiZYk$sylkSCXC3wHeRN~b)2hDuY(4= z4(sJ|?b1*0{2F9E>e|PZzUFmr{W|gMs)U+CO#WrMD(A;Pa=zuMt|a~=<(wyS>AGrD zFIS#4;LCLFkY8`6D=$ypIR7d#U9)-KQQOOPbo^O2zf0!za%n)FmqpYY2ezB?r&M1( zyPD@?4h1OzNN{)Xh!^*|T$QIxzv_zWD_pKqR`8TMT`l|`$#2R(AJCtoi@CNb{<)2U5oqqN8H&Re&aqdcQah3)oF%ZX4vO8 z!`V}I_Hvo6*;DzEf5!O6Dq~*u)%xj=`N`oa1+L-J&1Gx$@v3O;1c-F z+(A4KTG^+|PvP=FQ)g!Rg2w8;KxFX99{N7@gpbMO+KhzcTU!67gS&9{xAIf6t-4=> z87?tH&2DaLKI6*>xR}<&{30{7!aF&-?mdl_V5a{Get8PN*3$eUDy8y%4}ILr70bQU z+soA;PepXfidU8uH=2N;)G~1$BOSX%a`T2+&TA|R_11Qv*9hCWVk>&>~|YW`L~X|z)~;Hxgx zkK#B>qmU_>?;Z!i$7+1btet*ZR@fqNl|TXlH|OQd%rvdjL$~Blb5(m0m4zn?>od&x zO^YcJ%YEt?EuodE5<1?k1n2ki&a`sODP~qMa(pSD2E+O1I8P!!7g-FCJ?CpPGTI%> zee4I+9Law01ZiM|yJewJ*3~j%4o~HM4YirA3+v@6{}999>{3NasEsEdn{Xqg zlo=VDKP{F!Mkr`?6mK#kcjnuQ2(>(dMg4rA3n_yLjdjc|(rT#0RdcQ`xkHt_L_U_M zhe3HiMPk&IJw(b*K?(PfI(5k>RWl`~HOg&9j{b(PQH*o)W*DaB_XMrM=+qBuZfwsi zEB>%{PQI)95W9T~d@7lpH>00v`4UBfK}6aog@-DJ0$nWkp`!$h&X`emAvkJX6TV-y zEIs}zsmVfQS3_s@eK36Tl+XAkfpV@!Iha4&T(GkG74xRGmL0 zh>9r+qGAes_2`+PwK}M@R^;ydTAB=4Yh$@jQ$y_#SM`x8&56j0*%Qk>j<*f%0Qraf z4Lq;|FJ-t8?*juCf>tIkQ$CC3X7WT3gQ|4Mi$rzAHc@1SIe&#ntba0MJxK)nvS#~^ zP3TIcB1AtZedtw^E-FIHIhr|Wy^lI9E8bNKGSf_Jl|B2z3`BoN(8>uQ^7OD+?pM0q z+fFtu!<-V(58ve>VtgB#ODP0Za-)>AIs%2CwPyybT|q0RRB&pM^N&bF0z?Ff)Drho ziPM1!+I^{SzRc6#=9?w0)dN)jIa{hrfnYKp(|rosrTY}9?o-pp|GdyU+QI))%NA1h z_(sGd)ak1}-(0Zkwzw`+$lNWhi5ZbZI?N+u=>{bn5707#TYZBWTi3okwcg7pRc@^a zSX&f`*HaKm)sz_nYJmV2?z*iswR~aMhP){ao#8Z;ts2s}IP8j3zIanCx13@P?R3gp z@+(UjFQx^#@+R9s?EKct=;gyHXTP>FE^09N6?qiUR165m@8dyNNmHYdC{Svjt%lyX zz@?49zAva^x{1wvdWzAIe`gt@Q3Fc3*f5-^a;B?Lj&%7jX@!V^Fe!g7gKEG5K^x9{=k9A`V zaE+q*bNMUfPp*6VOC5i)_vU}FNb0t-gYP*w*X2vEG_6Scz9 zVnhtpW=AiEXs^g$i_ujo3B(K9<(UKpl!&YjXGOxZvcKeOS5U+sVfKdTV4|# zd4GPRG&X<5B7P<}+f`3xP?GivL@O15>7ykY(`oikPg5jWAG8{uIYrbi1Q}wF^~qK4 zzn;{^hS$8<`mDW= zX?~S$o3<+5Mn(^&suN)n!9qV<-*9JX9WveOevGO#h%Yo=>#-K zZ7SczWSS0D`9rfkDjztKOZJbaCZzdXZ}DYu4aq=(

GvshJ8{xwX~M7UPy8Ff+#?jLfcbHXePSP}fE(ipEK|lM=4G%D zQnm$4+NKXRLv!=!zsA=frALb^xR6K(pla8AmGmKTR~swP2+YK##;7y5^aOTdNkV zRIk!ss4}NFiusrZ5R@H)=~0t|UlUp}q}iOmTan;=GvYrH+kfWyFxWnKt02^dR;@V{ z3J5#BsASRHU*$psGt_vZ8QQVK44a+08tRH>@s<6XRcTD^*1t*mD*>xhIXOFnB`+Ea z=YwG&)M?B*JSYy2yX?mZ1g^2)YfE4(_<}0x#_@-#Qx6Gf-Qo{+lJ7n!(Y7@() z$@U^{mm+57l+B_xHqICQx!w%T6Gt#;8Gz>HTR!t!>+DxnX&KeuVeTo1X`0YM#(SS~ zwUt+EpDi+JHnn@v3fJlDsMo$iUF`$DCM|V`uQ=hp`u)ZC#|rKQz91_ogRj3ffiFW& z7kp3OcYH^zOyWCNUxV*W>I!_Zua!LGgvXgImi2Zu;uk5N>WD)k&=8LT1*~a`rB}vT zI;Y$!(#k=&N`JG~+Ak_VI~|5zX-?Uy5L|6n&JzSztC2$x?1QWeL-l7rOT!OG5B;j_ zx9hR>Mpgst!GXMJCXZC+1HJM8gCVUFI#Z%Qf zoS<|tT;=u~OGo6@56_d)+pC6fQ5;1W0s-sr0CM{y`U1L1Js9tiC#HHboZjBj&S**m zL533R(fnG#QmBjRil@ZL5`^0HPt7{z8;PL<7XsGqpmkDt$wxKEiY02EGt^ykxZm(( zVC6^zNl{QcT+QWHC0->RX}Y_-@I@kxy-aIkplL@oVrb&f*3&LVO29gzJQH)6*p7IP zMfQ!0q*u>#`~NBWqzqPC>-?MFC~kbtE08XT)nr*4+7GnW`mN2rlh$uWF_z{}d|@Tk-n)`l5U0^w?YU=lI%y8Qp7HCLHYy1BH^rqY2M z%KB`KK8ju%xEvO(@%L#8XZlYv-DLxBby>r*0LVXZd1;@{((v4@=t9bwCEIDV<{D{S zHLEM!vvEPg0RU7I%^gy{;O*+8{8n#p_YZ8{yu;sTxoR_DHAYXSdcT#tCn(?w?Z^qe z!GO?YEcg|0ms?+(g&k($ZnNp5On>OT9E3cxyzsq1=I4Q?4>HZnugWvueNIdQ|MS8O zae%zb+}vARPHNlyrPX%a;eM-S(~jeMo3WMyH`E_)###dwp@Nsq!q-5I5yd_LW4wF; z;f!BnSF4ZxaP6CbwcdZ+;C%n4cT0~uqr`vUN51;OCD9p_5vrF*%CZI?0?J0{A2eqc zet{I2g&)BR|K|46KC4Q@ef-u}Uu-QeX{j#uOWLpVX`k;x5!ubGa38mt*kM?dHaB(LL5$nf`%WH@_JySvS4VKd`ya z@^B{CM%~tUiFLwCio6dqX=zMW^jTh&^?9Xq;I>l!A3iGj2oKD{Z$;D#KSp|~Rs*e} z8t2;lzCeCrl`)`H+6Dgg$e6WOTGM4b3UVn{^Li?@!IHJpPmX_~fm>UBz6KqTtmR*9 z7`Q<}sDW!gu5=)Rva9WOPPQdiz%UdSz7ikvJKH z`xuW|xG`?8V+Z-MJC~m*y27#7XPry+_H%18T=-unnBn4}1&{%%hg?$y-9V)8Sz;jJ zGk`2laPBk##Kc8EdBGCL3o}=&{e$~p=-Q92V1P;a=abm0%o9w2!pd|_Qg~SLuDC%& zs60FizpR&X%P#bYSLUlf-3(7p_|Xn07-1AACWyVFc%i(>jzHXIqvwB{9l%Mg8!Fx} z7>6HK8rp2yjr-ZC`0)qrU~has7^%w8{4nmg19SosS6>_ClY6-{o8HJ4-d5P3eTvR# zdN&(@Wr*_znbXQd>tf#(?=r*M2{rn=%IpCaiai9UU~nH9Kk<^ktU0FK`ndgcGm=G|@Pk0f3ey;72k@TfMXyHCum;L4#qGPj z@o(1)fJ4-q7wubl9JJPo;1#GS)m9mY-}?iF2?KcTlYcIsX=sJf@YcbUUp0&ou2g}I z&;+{i zhzsB=-h>vmf4~%UgQ7>kx`b#_Z)HiXCEilwH%pcqb$He;!{cIrFRjP?VBC)YK&a8f z+nRUVuhWG8OcTCCq-f#&d=i}@Lq9UcL=wXDFIx<$DbIUB5aFsu0W*RZNG_Y zo-<$PlFe=>g&??q7MHxMp!OFshjI8$M^^p=2P>Mtmhq<4?Zy`f95G8R6-r_pm2 zb6cyAAwJjxe_MrLbTiH0-KqS|G5Yr|tz!wN_LMtkhLHC_PcdntTCkD^n!)x;K!Cbk0eaPIO1nI6Y8Yy5sdd zaqL>BJ08{0!`G@mGL5tU{ZaUZ(I@=c@SqBhiQX);6vN^jl8$&Cbz zbYj(!sW1#vUsNZ&l7UHP?UBjZj6wH1eeNh@sRt9zz8H63dZ3I3Ev{@cJU|Un0c$*p zH3!AYM3!0dNllsRgG}Dj2Yq|v=8eM5a_jRz$%{c_SVy@iS19a?mMLzw?5+Q`NPoIY z_&FN=y0dygu!Rn|v*sP8V9Q0pz8J7xlHnnSF4rBfCY(f0 zP*;O}XhlU@2i$>2w8s8&X$m`aytNzs`u{6GXSIJ1{Jgy_iE@jp_x}z2JoPtmem<;1 z=8l~A#7~c?Qu`#lu*7hw_(_N8GG6XGKNsvn|9m37fBq@@Z}Zbw8tP>b-3!T@{u9_S`YeDv~Q37_00cM{dM$xasIxzASt}@QIi2P zy}uTT;ik*KWop-1E3Up z=Gb@l-JZcrB^bHZe(oCV;=LF&`@*j@$IkIDraA`AUj2*z)Sht>C}X-$?3tR|_r;!h z{Yhcj?#EMD)~)|1{=GLbwd1dyeT1u~Ovi)tys>g`ngmDdQ`0(%K4BV5FY(zQH7STj zdF?~#wa-+D#2{EFKAYjHoqL3eNI5g?QFNoG*ulSpbj z$s4Cb9$wSBwg|IKG)rJ$)NFbOC-+M;^V&SqnuHm%t>$PmOelFdsRC|V0eAd8x(w55 zlP3{o`U&9#ux1uP8E$5|H9L>lfC9sejM3Agbi+U>#`H(PI>;=U;jQT}Gf3nXwAd9Z zB?+Xuk6H3B$(Fjz-Hu($%roP{WH)p0*mJEToMshiRpiG^he6|`LOAn3XzV2*W(Qgl zDG*C|;Ysv zKS+MaxH-5wi?KLgf_Q{b>hc%UfaJ)b{$0x^@IQO~GMRAbR8y=H#63;hu&q8fTH4j> z>nlH`CSG3ya~nB*oNf1S(K-m5rfMn-s;Bn((2n}%;=hUV4k6RnmGqiL8s z)R|F(JTR@WI_BZYjJ~07GinZSo>aK8EhH!|Q`r^v+$VrVA--h^fn+##XTFd`=D3@Y zVHv5&G?v5-pOWPL#Z%Al1@_F^diS%898Pcex!)vJ?&(^?Pg81m(5YceXkC^jef(3I(r@H#Lwk83wk|qJ>9qLE zSndz0L^=FAJ8Tps;fPT;mvRc4_9gP!_3ATnxlkgryQXqUCf8f!3pdz@KAMur<)QM1 z)J(4Rl#xErFD+*$W1igb6xz1{C3V7y8redw4baSAvrQC{2 zW)nfP+Yb~jG3ZqWB7>P{+#=#S)eNmN#G9NJwC3cQC7WjrkeCd8U}MxQYzmfi&N_(9 zA7a?Am?ayS1lqnL|6qw^m?T*M-x$JUy^+JX98ad2SVscj16iMf=#6+q=1AFGEPNVm6N2RA|a$y zRJ;l*LQ43tAEdnUUkOqg&p)a-C**{x^xyJGz9wmvLsGL@Ncy4#8rRq(xDkpD+h>a2 z^CT2Kqg#p|iHs|XzOy`~iMP4Y)hmL^bcs5postf}3Yy*{(|w$c|YjB9I5A2xNT5V4jF3 zGcv})Cd9w}z?1|N*ffDG6Nx=k+8wVZC@y3k9m%=?YlY@fMoc=9sy2k^^d-SaK2vqy z=FB|Rq5YbsRB?WuJFI@OmN!@V8KDSF%p~61ezb_?qO`KVP-RuA1Q{!Zj1xpS3Enej z4jBvR;fjn0F;JQIovrY6#J7$FmpN!mar z6-BoSMd{+L$u2GwrHg;5yLe|x-VF0nilR8%=@gZe9n1~;_pb;s=L!|iQptB>C!q?? zgLbi;nP&#Afq-B)kiu&&_^`T<@sy-yTWhO6eWZ9L&7M*f?mJvfhOCxyXGCb{I8h## zck_jmFJ38MFwrG40pWZilct&k5u0a1!tS!${w!4N^{`MeF@H70?ApC|n#t~67Vq9d z!KuiArl3m3(3*1px_4;1&}VXF4oU{2)gs+n_42_-E5&(>BWYK@AcK9K zDlOqT>u#kN>~pyn1+Y)r^u8tZ{jzTR{trHO`hN6z8Ig5sl6!@c zd6`Fn$f4)Q+iZ~{VfqAiCR7pXCsnMY3{-M^U~y7>m3@@rzpEU>M8c|9y1R;caew1(u|2ni1HL+TR`emgi-e=;8&9dx^Ix4)9b^Wo}|f`P2colUeB4- z!ZR5c5?gf0-o7T25rfN@BgO$U2rwGd!LhonPAQgu&2*^aaOqyS)Oma0t zep-BnJoM(*DV@OIx#ukT=`hg#^^M`> z-S(8v+UY6w&qSx#ix(vYd=Gy*)&7T8STp1VMawOd6pGoBDqc>0#S6y|Jg)BT`#)Jt zj<(-=NfIIko6G(VCtUnV6ut^)SI<8ZxXALD#hs?{_--?{JW#T6`UfNlB16h0l9)zd zr%+zzpOq;YmT3Cb?&#Ol#U!5Fe7acw%QVj!?l$Uj#G!$OL`|>CFVIU zmlP~>qaAu9!|c-}j~YKUCNvu3H{q!1GMn2ma#qVYWHpC&P^?XgZKT*HqoD0mWf!?YIyn`-H$~cr#4ML>{DZ66Bn#vB5J7q)xv738Qf@QHmT$u z$pke!p z5jjM-(vBgsSzXW*@NFfpI}j_wtvy@a$g{1Q0)u6O;09PyacgtC-y0nX)9KYl5a%`im@zn)0;t?(yUwZ;;^AsK@?GP}9lmmFw+d#F*0GZaPLcEi6@1AcXWJup@eFj9 z0ZRcPaKM_tkhseJcB#NTCc`1})nf#k_4a!_)?f=QA73QXn8WE1w`pB>mKvMa*}vjN znXwd_-{uKgGg;UB?Tq7y;=tA6RNul7i*7V6CgRT<(o#Ak!xeo+;7RzlGQbzMvKBGJ zxLvw$P-S9~m%ozLysO1`eOU%6DWz~xyUlvFa~?_zFkSf>&j=yB1V3$+^|t%FWl&;UwNTDJ15gk|(RhQn3=-+r z#_5wfamX6y31nVEZ<4*G65iBneV$?(SnAu3mx(l} zwRP+5-|`fDM%1I0h6~6lkc#SgkpSmm{Y~8Xl=oBBhh4;Iy*-*2LFCKS@?~0QNqQ$= zoTX}npD78{PS~W&`g3Q!CGomF`}6k167+o`=f#leBK|5o_IR=}x{-T&^#eTsygehKZ~K=e|r=qsZ~ zY*XvY&2afcEFo1Lz3hc5YruPf5y07UwIj5;vG2APuF{P-=o8I3(LzQE|ulEboxTFU-}@mv0oecs=s zlI-=W$OwvLU(Yfr35Q$bzDNc0_5BC)Bn4*O@2F2SVEF)vc5iu@@N0o1NAp*RSgHI> zy$C;LyxzO~ee~Eq$lsjIZshMX{ZPxF@mYqJKP$U0mq;Hn_eiSDeSUaXnLGCD??>hi z*dctJ@>?x)t9xl^zb~14pMD=QxAZkZ+9O~lW$udK?}N-~dDAuwIDpKS9x-30$Qvvq$s2)|d_UA)Q)!+fp^=l6~mjSE)5-35_}OUqb4KvFHWkeh$Ws zEoDn+YAhieQOH51VGUI7>u)1f!?tHmHtXq+VOw}dIFUSn|8F6 zZIIgQRqa~M3o*Z2Gw(r@vsLv!r}|)|yaCO;*a#XU${Wh999G?n{*^9L_Bhi0({BK;KX&glCR!0;XH@^?!Cf`%3%kA_4g058giIOt zYj$4?0K)F?OTz|K?Z5xc_oHFI+Agq;qqd$j>=Cc=E?$C>9VX3Xm|KS?ACf|0{*&G3d@MZmHM2IL|7E-p0}+iLD+Mh>UXEC*pL~t3)ND9s%-@+42nk zR`QWeVg;i}w+ULyi80ubEfPbRsMCjhsmtgc8=3u8$fc504{Q4q2byY^9yTlbd?S=` z_NF=DB|)2qd7I=y(C{YEU_D!6Auky_kO>p#*S~YRVO{L^4dBWx zS2ZPO$uLjNnSvP+PxbP{_7j4vWmJh}N2>sLJid*a8n7g2An&So+Rsjx>N_Aj8DcLP9WK_y(`|O+eR%}km#F{{vpE;GywkMKWt}0`v(n{S z!C(92?*F6=((evho9M)a8)ZYG&~n!Ey`LEtRl zOv#GBwi)?m_`G~G^yar1@&rh?02IQ&>au9&r{*KU%vGj!UVb^@>Ab}J2-O6_83Fbx zyhNd zeufQ__muNb80l(1%U{x3bAoSaMjWWr1gN{?->;_OcBA~98$8qd{1a8+5=`1hpll>$ z{M?Bdt{!T*I9|gXsbQh$vT0~Gudy^v0;Y$2b-QXZL#vqxjDwD&VQr4(X7P1H+6HL7 zq`oA1=#e@LjqahA_dl6m3v@6jWdb5Ke@-PiSRWuj6)0vVWn^1Qc3biiC6X;%=AdDk z%rCZ8lPtpT2KHOXW-@0%meg9fL3Sl-($5aP{pkVna9|T^Bql*)Ud=ePn{V9l4~(Tv z`2&olZB{Si_ifWXExn0_4|h|ED~gZC^d(;14trh+eWvc!noC;Byevf}LRrWd!N#TG zGN}DC=Dnc+3%IxN36g`ev4fzP7n^zm+upHl+A;OLdx_Fp8cHyY*>RU>nB zH4}}3vK_{tvW}Lr9RN~h3n1mJt!HVEs4suLtA1QZKvv>gWt{;vG?vqq$~*0eQd>)J zmMODBgz3I`E2XNHfeJA8uwh4>BkB*5p)FD3I8`Ep61C^_s+kPcsa^KEy2!(_Ndc&T zuT*%rQwzPK&CUL1`RRVKZQ<5%Zp@m z{>l7W0ajKjNJ_7rc^6`z5)Mnr|4hoSUaR?ECBz4z8B~CyY=bz}Vq=wU`-DlmA|=|} z?EJf3t|xNp$8Eq)ui1u-|EQ~s6WCA6I7X!-;~N|qZ|qLSHz1YU?3P~&-$lj|1-c47 zCobn3kaO+mx0Y>#)?MU$ha=~j2HPupMnw;yFCaRVOq{w-$mU9Ki`RZ-YGR_Pp zGEUIJfh8&98|>+8ze~n71h251H6i0nkRszbYPDK7GJacLH!`lP`K45&WV{;+?IGjc zljtI;Azj9G)kEY3GA^qjD_NdgAWM_g8dg~jspR~qI~_Ss9Y2{b(UVUJEiK`;{~ARt zyNqZVN1k?Q8Ml>q+jbf2=6UGsV?5H5!57B^-5f6KFvF8N)zs8#`@^$@G~r3MuP*kT zYChQo_Cd`zH&FBUrsiv;wlwkAgUKY-%HGs`i7L@^7ucJczj4)fbpZ{iq;E|9W1A61`+x@zcFzl(js%HRJI z`-E*fx{7_k%5x-tB&n%SWY=SCI8uFS{W$OUWxITY_n6S@XrUCn{%_hZ_e+i42zH%n zse8R{+g0lAzbDlH)4Fd$-AlERX1kmk>OrXgNbs}=q5das?0+WIbEJmugt}5*^lZC~ zxC4Ps>hq+%^4}%TefKWUbFmkn=$#_Z-54tWzA=6bwST_?y_yu!?gaYp@;us;K;NU@ z{(Azwl0d%}56@nW@zYcrX##yY)^4}P_!gr_fqoz3=06kY#Zp6e0)3af=vkm=hW0SV zG0gw_<@?a?q`;M5*Ch7e9Go??G+LRonM@$Y@t?bQ|?jS|GA^PtL zZ~hDWCcJm5HqwN5+08u&ZxIXqdJx_+spdZt-b|^XJ0<--M$le_cjK)|;f?F(@iGUb z60h0ym@VYleSfLi(%NeuT_ExNJ_t~MW2xI;e?UEgWV*{$M5%uud=dH08Az8sLpRGj zkR))XgG*)Z0-}aCP14{1wm`1_q$=+db=XYF!Xn#8YgI!?7w;`jI;Ef)I-OSW>rQt ztDIzCx)0gUO6@OKGR9MVD4Wp{o$%Gm{;R+^3`CffA{%(Mz4ckJ2(^%X{zVN-=J+-c z{y{sFXh#SDLsZVNS|WanA{EQi6ESi<-=q&#S*)Ta*@Nm6vrP@UFSCH5!`bhMS(1t% z3%l~hgxTBdzkV%*<=qe-+l!`CEjM1d`Q-(Q=69>J zR%D*xc+=`-PMN@g7^IRj+xGrbtSa7-A@OTi&x3Ply}`8Z%$Kl}Y#)$oM(R|nvhwX7 zM1;diybWwYCE@4l+yFao-oq&g85}F)W&gh+eCBede}qsn{S&zjjOMQpO3L&Pr`RNB z_Ot*X`BQ6`Es;-K{8KY(a@3yXQ(1AfQBU1mM+Ab2n!#h^Q#a?~hdRO|{YSo^Gl-81 zy7~AN{qe_R;vdKAQ$LP>zqj^F&Q$IHZ=(HY)2c|HpCpz5W3#_lkdPSc3d!_lSN#q> zwIcIsMY3N$3mWS3YvpzE+bZ>nomA{^21y01xKayG;LCdZeI7NG%7POKVUIBJDw&)c zQBfW~p`87O<-mXrYN;PFp>@PWR+h#}>#wO~UENgvScZGe>|pp$(hNTW3_k*;3Jfh2 zspY|9emLDoxM1*CTK^|`L2AZ$qR?h|;nUnvtWp28>7Nb!#Bv8CMZ~C?=%~qBa!Pxq zvD9Te-xyxFjki>}LkcOFUa;j!WVC4?a5^cBBYdPZFG70r`P<9`vK2}lVt0F~>P-mYSc4S#+uV!3f(rm%+Y5eq8qwi2n zs58r0aBh}Ez8`NCsBr1l*`wc;veg9cAnOzHiuxK0PRpb_G5ZF=SxbZEaj*S*5BN*D zpXzeai-9HusJ8+Ns5@1es(4)o*5pb|*4anA&&w0}SOh)&V+F#>k70Ctz8)!FqUN~jb6l=De5I!31_arf zI<#Ye`336*qcSZQ0e{#w?p~RiJ0$*E-dSb$GPheFkf}KiBw==LKc?mc1tatnb=yyz z;s{bdqwWx>6Y4zBSYROtf>X_WO}4*$BjdXQdCmzAuq^f17<#iStmVMk){d>9-X=ip zTW>~&vLRHXLNN4Yci^_Mfq|Ni>^q-)`^I3njMY z2lS7D@y>Lsei$vIl#Z3VmU)GmFWpZ&EFC0wD|GD5K z|D^44K2g;-iivD^Mog6yiy`!d(X$bE@@{2ys8=l^{DJ6cF=#vc@_N6 zucZP>g^+bxT0WlWF!@B-F|h`BUPhRE4={_Xm!>Rw^f?`CFW!KU0Jsk}Ap6Cs&sE;4y<;+jk}cT3rU4sJ?j9o&8i$My@i zi#Mm^_U4){xW&k=h~xIwwHmi6X!R7_%=u}!ox`oh?e9`P`d50LC=Cu|8t1d;sV1OF zk?PL5FEsP95N_8>vO;t(e}?QG9t!n+%+hM=^8g9)b%pR>2?$-b-+{2{i8KiR$Z#P5 zJJkO*w*sNp?yt+kha9e=M**rMVv-Q}_stF)tn4!Y6Hz09`V_9}%d%R^vKU5YQ`wWbLbAZCw}V%o6uiz6?_hs=O=p5{8n60QUGREX?LDr0P52s(*8%Bx zeI%x7l5ekbtMR&2%16hdQ_?iT@_V7CqY}x@V0rzji-y^Qi@g zNo}`*0d@~<_aOs!9Jh_=NjUPuiylwI?JOMV6uzA({GWnf7ML+fNbgF(@!FoEb!v0D$l{~BWU)AEjnk_eb z8t@n(#Vku5@y;V_Aa%rB)6#`dQP%IrWnk?TO{nt6(g=0>+%!TBx|XXR(b{p#exmiz>U6YDeX$E#!Hy(ziUdqL=#od$ z(7I<%8d`U7tI={vc~;EkVdNS!S@1uOaktz>HiD`LCuP*BpuCj8Re7aE4@TcWO>7o} z8bW3rhiv`|M%Z{Xq4}<#x?F#wx8BCz{rr8v-vKD5HT+({-$%E(T$l1ECFEOfaR z)$3=;Xo8+EN&172=2wcB#A%5F0WM(!zowOa*i6SITtHJikR{}>3aR4&8|@a>;}(MU zF^|l^TS)E~U``pX{lnie1mYilEnC8-_=jRf+82l`X4GAdZ7Ewz+FOr<-|}6bnga>h zc{-zIz@gm2HuRh-Gp1PpVaEkoLn;xSfZL8q>_V+wqmp05E9q-2Xhf5w`iEjg-i`)Q zHC?L0>+I4%*kdiVa@ zSUI0)L;Ioe_sT1hLyP_c501SKa{kS2Z19^5vFE`Hk~ zseYS+aQ_{*ML2GYaoiR$F7D92#-lSaWn0B@8=8Q&`WWT7-JXC{c_mMLLdge|yN2`~ z8ZV8zc3d_niCjMf^BhLK^s?s5j}LI(y*0og2I88By`nx?ryoX_Cfv0}+Fd*TF1l&B zY>~KYE8Fydw*>#@VZu`kc#gZaDB-SoWszIsEcA}y8IHSFwu6Cnlnkup9Z6@cY)8UZ z8)DziE}(kk@+nvj2Ka`S5d|5w^9Heupq~O9WB#FPdj zXajM?0bOT*1b6lwqa`oKG4d*md@rP7Gy#wlgr-1X6-29oH-$JgD4fU-s;tu37ma`;WxR5>^6&@<;hPN$XdsT0!>L%QR%C zD99!?^03X$Rd7vO#LZ&coq+0gU6x6>*<3|4s3V2o`7I8?t?Y*hG$KkEIS+AFU-pNV zvWFd7C*84i_U$#az0cB9Dyt3So<118^qkPc@$@g5Dyr!;c2Oq-Um>=I6(>USaeqi7 z`MJ~6NM6XTCi%lsKC0AfQiv*vRfr+9#LAYPC^V~f|vrku?rxMPLGqSJX z03+}GT^i0eO-sZ1N^UjI|2u*5(HzFh6yax+- zLQ;PzE{5uT5pgk+y)WUOt+R9X3$c}r>4;tW43H<4iFk^t&t1U=UxLva7<(z=*&;?L zdfZXmPjRa;I!($)S&Al3*>D*<$~qjJ*pNkhvQB!I_Q@913k>3usUd2(#uwQv$bN?2 zpnbANb)|f=7uBWm$yVv7;*+h{PsJyDNk7FWdxb0QlgYD$PuBgYM%Q6HPaW1Cc_E!T zCCk#Nqc(?@VQSbVnmWx7rBP?`PtvG!AGZzBpJAPfC$_#Z!PaMS;x${Ba+Si?R8MT3 zeeyI~*=MEt@$>1h&wDxzcD6>-oLx9h!=954`v=#j!Tv99HD||5`KXwTDKgqCiOk1; zjUyA~sz+ohZ`>bbj@tv7Cy45&h-rh;(lFwgu^O3kA53G!fOKR$31ogM>G`7ZL zKyA$vLg&_>CDW|j-*=r*(s}t8{qnl_%Ue=kM)b>3@t2bmFFEVOe&KLcZ{^uiuRQQ2 zUen4&`rCu!-wsT^^x0?Vm!F@hK=RqWlP`DJhwGQy^vfM~^vBE!vo`Wxm+NzLYKL*Y<~tL6+{%oof3i7c0tM~6{%0`FyCpX||6a0;`&eU$MS%UvhM zEOve^XBp;spTjYZE4?Xi2A&QXB-YV6>?~Mw4O_#%T01A-Regxv`yxIlnU=@)fR-;& zqD;~uDx@r%iEoj*D`L5yD5yGP)kF1L5b{Y<#T^yWCix|+V-;kR$cN!$BF?f%Fg#bm zgiJVl&obBFY(F*%;oH-{_DDJh`%TGazs!cS2vHwmGR<#A{mwl3cD(3PLSeqrbDmX; zNvplgM<0arnQVwTziA05$8wk6164_{@FA2ki>Jm2wj-2uhE4KL0G2h!k?hr~?`M$# zqMk&4Ucnd}9(%;sY?kba4z0^5WjpGZWym1U$=mai&fr&94Y$RHsTMXY1~ zC$hmj*#zu)_FunGb`VoDNXm5Pr5x^qpDHEDB7iCa{-nEwFn@Poi~S@GHeH=6_YX~! zHhZj^2<%`?isgF2ooQsYQJgy)XE{7$wyw06N}(X$i{yKpt@1xSvZQe^`?Hhx@r+QN z8vL;h?Qyyi%YX^bpm|}>$G=n+G|R%8X?j(`bww<$VTxZJGsd=ps=p{Hkagb9VKzFN z!nrLN<+3!(PnJWk`ixjxs58e{*eq)OURHEhTq!HM@2qWD#3W~C8*}U|xzFZiENud5#+UvynppTqu?YBug-WPZK+>OKBE z0dl?qg%_Aju`CW|uAZIP%bE=WIaTL3RB1N7$EtqfP4m<+>U_)!W^zj+*QGHZ&U`pj)YEmoN-QbVmgp_V$_dotSvb6_Ys zu+4t#DiDoi4N&~BMo?%t2dFL`;q})K_Xfi=5dd=4(obqrD)U@Xnf4j{e+~3~?g5wU zz6V{dOCEB$e#YNloWG~|eVIS@S0FkGW(*UxSJ3T?Ij}o^ZrFC|Sl;^nY8!+SyWzLeZ`cXJp2>o1*^y6RAwBC^ z!=|UbRWmk+7Y)i>QtwU9x%H_NVe8%Pe^{ny|Mu8DzUBM8dehrp*W!1(+HaS7N4lf4 zAh*7(+hi=bmOE^VvHuWqN+9J(WIM=!by~S>j%vNk+0I3F@CxAP(2eMu2xg#geXwNb ztTB)`gDUw6s5;B)Gs*!=z6&@UFfGO~sp2f#po-*PlkpAA*V#KOovIg7wMnl~|K^#T z5mwj|C}}lrua`Eh7iF{P3eCnB6Ku3K8>1M(8TK!3mOhokxs?JdhlJ$q*`VwyUrU3@ z?K>STM@IUdVOI$#Y${)&Eh^bo<9bG7VYX~+K8cMy?T-RU$<-3<$Zz-O8GF^q*Ts$M z5D+cW|1<7Y2{Fa%4l;bkk2Y>Z! z>5W;pibWIek#lvAoRMcYUA~tdxlDx%AqsR#jw8lU4wo7wr%B9mw?AMPny}EmdYHq= zWd9Wn)9N2WvykROy8M@7g>gdgsv2(S44cIc4-eyJv`VC86J2UuPc9|-xS}CiM>tOU+cvC6dzvz_Cm!skFV_c9=L*#ky{aaTJjQ|&NrXUJPD z#J+6qxAGn->28f5W8l3# zyAo|pPWkF%tf0NL)hJ&%s#a3!MKvb%VT%6(rHT@hfarLmKVDemQm@fJv-m?Ze4Wy4 zB5zgULem=Rg#}&_O*))U_ENt_v?bfXq=6~FLxk&{dn8Bm#+E8$#x{4AY24OC=cw`9@@`du!ll&N`f{pFeeNG39g>M}UpP|LJyO?g^k*|) z8LRG8CxMF<8I~T0rQ0bm6G&3>qPK(+gEy{uQpPVw^}GVeV=X}qG}w8?+U;N$VHd7J z8l{)idr)dMJi}-iRcG`cL1%IdnfU%=+eNw}{jw=eCG6ae>ZGTcZq|_4{}MYFyI9qQ zsHJQ{w#WA`!n>~DKcK(w;3Y#u+V@l&mB;ZN<;8J~WVfi2=b~3)s6!qM!LXj5WijbtU?fxSq6dHoO)pu>Ud|#e(jU zkp*F>9-f8H>t%Bi4;+_KCz5@XpsH+ENkr&kV(x+O)wJA|zCTp;rEn!FGmbt^j#pT? zn9S2tj5<_P> zcl5QVX2Of3)AvwQlB#obspcjF#hZByAoKrlgRFZ-bOTswz z@7bb^7PI>RiTIY0QK;&d?XP8vQEp5*QRicIqxRuDW+cl{7->zTY?~;l8w4}a&C>M? z_+Y9!6-8~q>$NXA6BYJdM)Or>~a5fsfNS4irKZ%x6>#@`(!aikb13NRh3icxV zz1ioGCI(Rnp6$PC%UvL9J3@#luoD{X!QW;^KH+bn@Gab1lchlXTo$mj4)x(1LMq|CBFTh15+tG2R7x>E{7i-dk$~&*t{txq>e!(hq&;kLq?8KeU?erX zC%cZrZ%5zQpM1s8Oq^7~m#)N8zDe{~d$}(E*1neig)aY_y_XkyD$}zF3I!#6fidL{ zUxbRuZWNJFV*#Bs`&?MW5LX~xIf?~0gu;laD4@P$C`mu32v&rb%V*WM3a#+mAwm>i zxAaJ~PnDud>{r#+f!VjH?-^&r-d18bf{U&WbAmovf8SHroj&`?#R@!^0xzA^>U(T* z6yOo-*bx$N zWJIKT>UHKBRISTp;vkYeuBTel=@rTTgVfdCcs)l~l&>m6(Pp2^RbtGRp+rIgDI>QQ zI;lTThNfvKq=!Z&6jG&VR>4EZQok$}B8`=q3Kl7P%n(i`_E5iE1r7s!oM1y360nOf z^yL50PAL>@hFbp#3A<=ut*imLu!a_P#rP+yHE*7ipR>w6QiPfBP z?1^*c$*NjWCHX@ITfuv%Q?=D>^cRao!t1D(DP<52D@ld$Lo22^VVHzG*%yoH9LXNm zjnJg@Pqj@Q7WIK@YtZOp{tQ=|iIIFS-dr@&^?io;Z=RrMsfQO2d z*Lf%u=g1ky>uepSealdM3s*;;g--{FrB85y`I*9v>}uH=H#Z>y&W$2w(!t>XkT&}x zpPm3v``;>m03CK&b8(|H*CPU}os_HQL1q4>%HyR#znj@Dj*E_rEVAdHjiO{0#GWk| zve#*XPWA#C()}99QzKv+Y-7RQXSoz1lI73Z^yt7-K|{gzukxBYlXyqi9HS5_fG z^kk_wL5w}%n*)4Dm?f`QdsFg5F;m#M*8W48jA!bs`qyhX$VXJp9-t|%$KgT!W$$qM zUTB$&39jlvvD|wkQ93ea8s}lw9BEp=tmk>qvP8&a^p<(D3n@}N_ZC+*(5$xaC$ENI z1+y7DKEg}YPW*rL!~)i$vsJ0vc4-(_6vTytLeZljRJ^dt``t0LX(#A;E$LkAVk5ecbW@;+|fdCqvm3)$< zSE#ANz5%-kkr-lMjVsx}R8MycPn*cgPFpDLnImR_=Bm<9@m-aVP&;%IMsEcjq2o~^ zBtD50pk|nFVtpvHwjzw5VXr7*Z0Ew+#&@;-EjhNP^%`fM*RT!0Oi(d!7jYK4Q$mkg zM0=wzQBsgZKBF5?Pclj4$K)L8pJ+!VmWx@LDq<-;UV$C`mGgCaf7uN`?xl=9UIBMk ztz3`HP?Q;Oc1M~&LWwled_GHAOnYNrf>BCp;{4G4=`5jA(F+nAv0A%5zkF`rP%QQQ za-ovy8P2-$q<-@Pee|*Odenaf@;=h@dyl;TNzd=|9AdeVh&mh7u8oPfZ_JHVdprRd+i9~7zd?+yFh=G#buv>Cnbw6y*& z{lg=hI)L*L5>=){T&<&l7Wxn6C)(3~hp*6fULMqZY+9@h;Iu7R>(fKUj=c^QUKuKy z&(-8no0c#y$mh;p6PI^4UYrq!_f^8gabocsmq*5{fY<(UvGhl+4Wmthne?}@JdJ;% z^qHD35+em9cblO(ZdY|B$Lj*ur5q*xT6g+=#RUod{+j4_judNpli65CrGzAO6ZV6I zZT7ZvR3}sM^JhEqxxEb->2ZD=b5H8M_8bFa#2wl zPlO?((6g$TeAM)2b`sWZq?QbHhVvCgmq$+oV0G2~S)z+k8VPzbzNitEy(-At$Xpf6 zT{eTSyKbZ$Qsa*B)d3v0y$70rLZh~z8PMC$nEQ(;hDPqO_cZ*=Ra2^mhx zL#S!0eLgSH+NS*veNkXPsxGo0gV4OdgvMr+fxiLQQR<9;-@iNcc}?{m>T~JzmP-Ztd-`aSAAx#Bz^Uh%vsV)bqwoJ=Bxj z{Xb=VqHBFA=03ZfU&}tsMEG>L{D6d~pXfK;$YQDVn?+~QAaji3YD-$lFi%yOPvwSWy| zL|cS)ELPH;hMZGlwq#`J43?+P1XxKJFV!x}5Y$*Pz+8gTxYwekabL9(idFG zQ+H*jW1~*8LIT$Ll1oDJ3W{E5;WVA0uvXC#A88V^i})L&X6bxhR%j3vH;dSfJQ%(W zCJooq#SzoTRy&`(CIOFd2{Yw%!?~P{F^nPF@aSX+V~|b_Q5))Ll1r-A503&gsBB## z0%gn`tlYeVl6tKh9ZX&v`BMOz3WV&C9vSA7xc3r47mx1eXA3U(=es~P

0cd8;KR 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