From cc5670d1af9cfe3c9edb835600dfb0415803f598 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Fri, 11 Sep 2026 14:25:20 +0300 Subject: [PATCH 01/29] feat: https server logic with tests --- brazier/.gitignore | 3 +- brazier/app/Main.cpp | 22 +- brazier/config_test.json | 22 +- .../brazier/App/Http/Helpers/HttpClient.hpp | 6 + brazier/include/brazier/Core | 1 + brazier/include/brazier/HttpsServer.hpp | 128 ++++++ brazier/src/HttpsServer.cpp | 391 ++++++++++++++++ brazier/tests/main.cpp | 147 +++--- brazier/tests/main.h | 5 +- .../tests/unit/routing/https_routing_test.cpp | 381 ++++++++++++++++ brazier/tests/unit/routing/routing_test.cpp | 32 +- .../unit/security/https_security_test.cpp | 426 ++++++++++++++++++ 12 files changed, 1486 insertions(+), 78 deletions(-) create mode 100644 brazier/include/brazier/HttpsServer.hpp create mode 100644 brazier/src/HttpsServer.cpp create mode 100644 brazier/tests/unit/routing/https_routing_test.cpp create mode 100644 brazier/tests/unit/security/https_security_test.cpp diff --git a/brazier/.gitignore b/brazier/.gitignore index bbcef60..b7fb2f2 100644 --- a/brazier/.gitignore +++ b/brazier/.gitignore @@ -6,4 +6,5 @@ /.vscode /cmake-build-debug .clangd -config.json \ No newline at end of file +config.json +/certs/ \ No newline at end of file diff --git a/brazier/app/Main.cpp b/brazier/app/Main.cpp index 169ec35..273c645 100644 --- a/brazier/app/Main.cpp +++ b/brazier/app/Main.cpp @@ -26,17 +26,23 @@ int main() { try { brazier::ConfigManager::initGlobal("config_test.json"); - brazier::global_config->setAutoSave(false); + 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); + std::string https_server_host = + brazier::global_config->get("https_server.host", "0.0.0.0"); + int https_server_port = + brazier::global_config->get("https_server.port", 8443); - brazier::Logger::log("server updated host: " + server_host, "INFO"); - brazier::Logger::log("server new host: " + std::to_string(server_port), "INFO"); + brazier::Logger::log("HTTPS server: " + https_server_host + ":" + + std::to_string(https_server_port), "INFO"); - brazier::Server server(server_host, server_port); - server.run(); - return 0; + brazier::HttpsServer https_server(https_server_host, https_server_port); + + if (!https_server.initialize()) return 1; + + https_server.run(); // блокирует, гоняет io_.run() на N потоках + + return 0; } catch (const std::exception& e) { std::cerr << "Fatal error: " << e.what() << std::endl; diff --git a/brazier/config_test.json b/brazier/config_test.json index 0554a2c..e35b0a1 100644 --- a/brazier/config_test.json +++ b/brazier/config_test.json @@ -3,15 +3,25 @@ "host": "0.0.0.0", "port": 3502 }, + "https_server": { + "host": "0.0.0.0", + "port": 8443, + "tls": { + "cert_file": "app/certs/server.crt", + "key_file": "app/certs/server.key", + "ca_file": "", + "require_client_cert": false, + "verify_client_cert": false, + "handshake_timeout": 3, + "conf": [ + ] + } + }, "app_name": "brazierApp", "filesystem": { "drivers": { - "local": { - "root": "./storage" - }, - "root": { - "root": "./" - }, + "local": { "root": "./storage" }, + "root": { "root": "./" }, "default": "local" } } diff --git a/brazier/include/brazier/App/Http/Helpers/HttpClient.hpp b/brazier/include/brazier/App/Http/Helpers/HttpClient.hpp index 5b0b9f1..1a609e6 100644 --- a/brazier/include/brazier/App/Http/Helpers/HttpClient.hpp +++ b/brazier/include/brazier/App/Http/Helpers/HttpClient.hpp @@ -54,8 +54,14 @@ namespace brazier { using json = nlohmann::json; HttpClient(); + HttpClient(const HttpClient&) = delete; + HttpClient& operator=(const HttpClient&) = delete; + HttpClient(HttpClient&&) = default; + HttpClient& operator=(HttpClient&&) = default; + ~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); diff --git a/brazier/include/brazier/Core b/brazier/include/brazier/Core index dca66ff..060e226 100644 --- a/brazier/include/brazier/Core +++ b/brazier/include/brazier/Core @@ -7,6 +7,7 @@ #include "Router/Router.hpp" #include "Router/RouterRegisterer.hpp" #include "server.hpp" +#include "HttpsServer.hpp" #include "Engine.hpp" #include "WebSocketServer.hpp" #include "vendor/ConfigManager.hpp" \ No newline at end of file diff --git a/brazier/include/brazier/HttpsServer.hpp b/brazier/include/brazier/HttpsServer.hpp new file mode 100644 index 0000000..d534508 --- /dev/null +++ b/brazier/include/brazier/HttpsServer.hpp @@ -0,0 +1,128 @@ +/* + * 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 +#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; +namespace ssl = boost::asio::ssl; +using tcp = net::ip::tcp; +using namespace std::chrono_literals; + +namespace brazier { + + class HttpsServer { + public: + struct TlsConfig { + std::string cert_file; + std::string key_file; + std::string ca_file; + + std::vector> conf; + + bool require_client_cert = false; + bool verify_client_cert = false; + + std::chrono::seconds handshake_timeout{ 15 }; + }; + + private: + net::io_context io_; + ssl::context ssl_ctx_{ ssl::context::tls_server }; + 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 }; + + std::thread stats_thread_; + std::atomic shutdown_flag_{ false }; + + TlsConfig tls_; + bool tls_config_from_user_ = false; + + public: + HttpsServer(const std::string& host, unsigned short port); + HttpsServer(const std::string& host, unsigned short port, const TlsConfig& tls); + + void setTlsConfig(const TlsConfig& tls); + + bool initialize(); + + void run(); + void stop(); + + unsigned short getPort() const; + const std::string& getHost() const; + + private: + void initializeConnections(); + + void load_tls_config_from_global(); + void configure_tls(); + void apply_ssl_conf(); + + net::awaitable handle_connection(tcp::socket socket); + net::awaitable accept_loop(); + }; + +} \ No newline at end of file diff --git a/brazier/src/HttpsServer.cpp b/brazier/src/HttpsServer.cpp new file mode 100644 index 0000000..13bf3bf --- /dev/null +++ b/brazier/src/HttpsServer.cpp @@ -0,0 +1,391 @@ +/* + * 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/HttpsServer.hpp" + +brazier::HttpsServer::HttpsServer(const std::string& host, unsigned short port) + : acceptor_(io_) + , port_(port), host_(host) { + work_guard_ = std::make_unique< + net::executor_work_guard>(io_.get_executor()); +} + +brazier::HttpsServer::HttpsServer(const std::string& host, unsigned short port, + const TlsConfig& tls) + : acceptor_(io_) + , port_(port), host_(host), tls_(tls), tls_config_from_user_(true) { + work_guard_ = std::make_unique< + net::executor_work_guard>(io_.get_executor()); +} + +void brazier::HttpsServer::setTlsConfig(const TlsConfig& tls) { + tls_ = tls; + tls_config_from_user_ = true; +} + + +void brazier::HttpsServer::load_tls_config_from_global() { + if (tls_config_from_user_) return; + + tls_.cert_file = global_config->get("https_server.tls.cert_file", + std::string("server.crt")); + tls_.key_file = global_config->get("https_server.tls.key_file", + std::string("server.key")); + tls_.ca_file = global_config->get("https_server.tls.ca_file", + std::string("")); + + tls_.require_client_cert = + global_config->get("https_server.tls.require_client_cert", false); + tls_.verify_client_cert = + global_config->get("https_server.tls.verify_client_cert", false); + + tls_.handshake_timeout = std::chrono::seconds( + global_config->get("https_server.tls.handshake_timeout", 15)); + + try { + json conf = global_config->getJson("https_server.tls.conf"); + if (conf.is_array()) { + for (const auto& item : conf) { + if (!item.is_array() || item.empty() || item.size() > 2) { + throw std::runtime_error( + "https_server.tls.conf: each entry must be [command] " + "or [command, value]"); + } + std::string cmd = item[0].get(); + std::string val = item.size() > 1 ? item[1].get() : ""; + tls_.conf.emplace_back(std::move(cmd), std::move(val)); + } + } + } + catch (const std::exception& e) { + Logger::log("https_server.tls.conf not loaded: " + std::string(e.what()), + "WARNING"); + } +} + +void brazier::HttpsServer::apply_ssl_conf() { + if (tls_.conf.empty()) return; + + SSL_CONF_CTX* cctx = SSL_CONF_CTX_new(); + if (!cctx) { + throw std::runtime_error("SSL_CONF_CTX_new failed"); + } + + SSL_CONF_CTX_set_flags(cctx, SSL_CONF_FLAG_SERVER | SSL_CONF_FLAG_CERTIFICATE); + SSL_CONF_CTX_set_ssl_ctx(cctx, ssl_ctx_.native_handle()); + + for (const auto& [cmd, val] : tls_.conf) { + int rv = val.empty() + ? SSL_CONF_cmd(cctx, cmd.c_str(), nullptr) + : SSL_CONF_cmd(cctx, cmd.c_str(), val.c_str()); + + if (rv <= 0) { + SSL_CONF_CTX_free(cctx); + throw std::runtime_error( + "SSL_CONF_cmd failed for '" + cmd + + (val.empty() ? "'" : "=" + val + "'")); + } + } + + if (SSL_CONF_CTX_finish(cctx) != 1) { + SSL_CONF_CTX_free(cctx); + throw std::runtime_error("SSL_CONF_CTX_finish failed"); + } + + SSL_CONF_CTX_free(cctx); +} + +void brazier::HttpsServer::configure_tls() { + ssl_ctx_.set_options( + ssl::context::default_workarounds + | ssl::context::no_sslv2 + | ssl::context::no_sslv3 + | ssl::context::single_dh_use); + + apply_ssl_conf(); + + ssl_ctx_.use_certificate_chain_file(tls_.cert_file); + ssl_ctx_.use_private_key_file(tls_.key_file, ssl::context::pem); + + if (tls_.require_client_cert || tls_.verify_client_cert) { + if (!tls_.ca_file.empty()) { + ssl_ctx_.load_verify_file(tls_.ca_file); + } + auto mode = ssl::verify_peer; + if (tls_.require_client_cert) { + mode |= ssl::verify_fail_if_no_peer_cert; + } + ssl_ctx_.set_verify_mode(mode); + } + else { + ssl_ctx_.set_verify_mode(ssl::verify_none); + } +} + + +bool brazier::HttpsServer::initialize() { + try { + Logger::init("debug.log"); + Logger::registerSignalHandlers(); + + json drivers = global_config->getJson("filesystem.drivers"); + + for (auto& [name, cfg] : drivers.items()) { + if (name == "default") continue; + + auto driver = std::make_shared(); + driver->setRootPath(cfg.value("root", "./")); + driver->initAsync(); + + StorageManager::getInstance().registerDriver(name, driver); + } + + std::string def = global_config->getNested("filesystem.default", + "local"); + if (StorageManager::getInstance().hasDriver(def)) { + StorageManager::getInstance().setDefaultDriver(def); + } + + load_tls_config_from_global(); + configure_tls(); + + tcp::endpoint endpoint(net::ip::make_address(host_), port_); + acceptor_.open(endpoint.protocol()); + acceptor_.set_option(tcp::acceptor::reuse_address(true)); + acceptor_.bind(endpoint); + acceptor_.listen(); + + initializeConnections(); + RouterRegisterer::init(io_); + Engine::init(io_); + + Logger::log("HTTPS server initialized on " + host_ + ":" + + std::to_string(port_) + " [TLS]", "SUCCESS"); + return true; + } + catch (const std::exception& e) { + Logger::log("HTTPS initialization failed: " + std::string(e.what()), "ERROR"); + return false; + } +} + +void brazier::HttpsServer::initializeConnections() { + try { + Queue::connect(global_config->get("nosql.host", "127.0.0.1"), + global_config->get("nosql.port", 6379)); + } + catch (const std::exception& e) { + Logger::log("Connection to queue failed: " + std::string(e.what()), "ERROR"); + } + + try { + Cache::connect(global_config->get("redis.host", "127.0.0.1"), + global_config->get("redis.port", 6379)); + } + catch (const std::exception& e) { + Logger::log("Connection to NOSQL database failed: " + std::string(e.what()), + "ERROR"); + } + + try { + Database db; + (new MigrationManager(db))->Initialize(); + } + catch (const std::exception& e) { + Logger::log("Database migration failed: " + std::string(e.what()), "ERROR"); + } +} + +void brazier::HttpsServer::run() { + try { + net::co_spawn(io_, accept_loop(), net::detached); + + int threads_count = std::thread::hardware_concurrency(); + if (threads_count == 0) threads_count = 1; + + Logger::log("Starting " + std::to_string(threads_count) + + " HTTPS worker threads", "INFO"); + + for (int i = 0; i < threads_count; ++i) { + threads_.emplace_back([this] { io_.run(); }); + } + + shutdown_flag_.store(false, std::memory_order_release); + stats_thread_ = std::thread([this] { + while (!shutdown_flag_.load(std::memory_order_acquire)) { + std::this_thread::sleep_for(10s); + if (shutdown_flag_.load(std::memory_order_acquire)) break; + Logger::log( + "HTTPS STATS - Active connections: " + + std::to_string(connection_count_.load()) + + ", Total requests: " + std::to_string(total_requests_.load()), + "INFO"); + } + }); + + for (auto& t : threads_) { + if (t.joinable()) t.join(); + } + } + catch (const std::exception& e) { + Logger::log("HTTPS server run failed: " + std::string(e.what()), "ERROR"); + throw; + } +} + +void brazier::HttpsServer::stop() { + shutdown_flag_.store(true); + work_guard_.reset(); + io_.stop(); + + if (stats_thread_.joinable()) stats_thread_.join(); + Logger::log("HTTPS server stopped", "INFO"); +} + +unsigned short brazier::HttpsServer::getPort() const { return port_; } +const std::string& brazier::HttpsServer::getHost() const { return host_; } + +net::awaitable brazier::HttpsServer::accept_loop() { + for (;;) { + tcp::socket socket = co_await acceptor_.async_accept(net::use_awaitable); + net::co_spawn(io_, handle_connection(std::move(socket)), net::detached); + } +} + +net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) { + connection_count_.fetch_add(1, std::memory_order_relaxed); + + try { + socket.set_option(tcp::no_delay(true)); + socket.set_option(boost::asio::socket_base::keep_alive(true)); + + ssl::stream stream(std::move(socket), ssl_ctx_); + + beast::error_code ec; + co_await stream.async_handshake( + ssl::stream_base::server, + net::cancel_after( + tls_.handshake_timeout, + net::redirect_error(net::use_awaitable, ec))); + + if (ec) { + Logger::log("TLS handshake failed: " + ec.message(), "WARNING"); + connection_count_.fetch_sub(1, std::memory_order_relaxed); + co_return; + } + + http::request req; + http::response res; + beast::flat_buffer buffer; + bool keep_alive = true; + + const auto idle_timeout = std::chrono::seconds( + global_config->get("keep-alive-timeout", 60)); + + while (keep_alive) { + req = {}; + ec.clear(); + + co_await http::async_read( + stream, buffer, req, + net::cancel_after( + idle_timeout, + net::redirect_error(net::use_awaitable, ec))); + + if (ec == http::error::end_of_stream) break; + if (ec == net::error::timed_out) { + Logger::log("HTTPS idle timeout, closing connection", "INFO"); + break; + } + if (ec) { + if (ec == net::error::operation_aborted) break; + throw boost::system::system_error(ec); + } + + total_requests_.fetch_add(1, std::memory_order_relaxed); + keep_alive = req.keep_alive(); + + res = {}; + res.version(req.version()); + res.keep_alive(keep_alive); + res.set(http::field::connection, keep_alive ? "keep-alive" : "close"); + res.set(http::field::server, "brazier"); + res.set(http::field::strict_transport_security, "max-age=31536000"); + + co_await Router::handle_request(req, res); + + if (res.body().empty() && + res.count(http::field::content_length) == 0) { + res.content_length(0); + } + else if (!res.body().empty() && + res.count(http::field::content_length) == 0) { + res.content_length(res.body().size()); + } + res.prepare_payload(); + + ec.clear(); + co_await http::async_write( + stream, res, + net::cancel_after( + idle_timeout, + net::redirect_error(net::use_awaitable, ec))); + if (ec) break; + + buffer.consume(buffer.size()); + if (!keep_alive) break; + } + + ec.clear(); + co_await stream.async_shutdown( + net::cancel_after( + std::chrono::seconds(5), + net::redirect_error(net::use_awaitable, ec))); + + beast::error_code ignored; + auto& lowest = stream.next_layer(); + lowest.shutdown(tcp::socket::shutdown_both, ignored); + lowest.close(ignored); + } + catch (const boost::system::system_error& e) { + auto code = e.code(); + + if (code == net::error::connection_reset || + code == net::error::connection_aborted || + code == net::error::eof || + code == net::error::operation_aborted || + code == net::error::broken_pipe || + code == ssl::error::stream_truncated) { + Logger::log("HTTPS client disconnected", "DEBUG"); + } + else { + Logger::log("HTTPS connection error: " + std::string(e.what()), "ERROR"); + } + } + catch (const std::exception& e) { + Logger::log("HTTPS connection error: " + std::string(e.what()), "ERROR"); + } + catch (...) { + Logger::log("Unknown HTTPS connection error", "ERROR"); + } + + connection_count_.fetch_sub(1, std::memory_order_relaxed); + co_return; +} \ No newline at end of file diff --git a/brazier/tests/main.cpp b/brazier/tests/main.cpp index e615024..2ece486 100644 --- a/brazier/tests/main.cpp +++ b/brazier/tests/main.cpp @@ -20,31 +20,52 @@ #include "main.h" -std::shared_ptr g_test_server; + // ───────────────────────────────────────────────────────────── + // Временно: только один сервер за раз. + // Причина — оба сервера вызывают Logger::init / RouterRegisterer::init / + // Engine::init, которые не потокобезопасны. Долгосрочное решение — Application + // с общим io_context. Пока используем переключатели. + // ───────────────────────────────────────────────────────────── +constexpr bool kStartHttpServer = false; +constexpr bool kStartHttpsServer = true; + +std::shared_ptr g_test_server; +std::shared_ptr g_test_https_server; std::atomic g_server_ready{ false }; +std::atomic g_https_server_ready{ false }; std::thread g_server_thread; +std::thread g_https_server_thread; + int port_global; std::string host_global; +int https_port_global; +std::string https_host_global; + +namespace { + + std::string normalizeHost(const std::string& host) { + return (host == "0.0.0.0") ? "127.0.0.1" : host; + } + + bool WaitForServer(const std::string& host, int port, int max_attempts = 30) { + boost::asio::io_context io; + boost::asio::ip::tcp::socket socket(io); + boost::asio::ip::tcp::endpoint endpoint( + boost::asio::ip::make_address(host), port); -bool WaitForServer(int port, int max_attempts = 30) { - boost::asio::io_context io_context; - boost::asio::ip::tcp::socket socket(io_context); - boost::asio::ip::tcp::endpoint endpoint( - boost::asio::ip::make_address(host_global), - port - ); - - for (int i = 0; i < max_attempts; ++i) { - boost::system::error_code ec; - socket.connect(endpoint, ec); - if (!ec) { - socket.close(); - return true; + for (int i = 0; i < max_attempts; ++i) { + boost::system::error_code ec; + socket.connect(endpoint, ec); + if (!ec) { + socket.close(); + return true; + } + std::this_thread::sleep_for(std::chrono::milliseconds(200)); } - std::this_thread::sleep_for(std::chrono::milliseconds(200)); + return false; } - return false; -} + +} // namespace int main(int argc, char** argv) { try { @@ -52,51 +73,73 @@ int main(int argc, char** argv) { brazier::ConfigManager::initGlobal("config_test.json"); brazier::global_config->setAutoSave(false); - std::string server_host = brazier::global_config->get("server.host", "127.0.0.1"); - int server_port = brazier::global_config->get("server.port", 3502); + // ── HTTP globals (заполняем всегда — тесты используют их для скипа) ── + host_global = normalizeHost( + brazier::global_config->get("server.host", std::string("127.0.0.1"))); + port_global = brazier::global_config->get("server.port", 3502); - if (server_host == "0.0.0.0") { - server_host = "127.0.0.1"; - } + // ── HTTPS globals ── + https_host_global = normalizeHost( + brazier::global_config->get("https_server.host", std::string("127.0.0.1"))); + https_port_global = brazier::global_config->get("https_server.port", 8443); - host_global = server_host; - port_global = server_port; + // ── HTTP server (опционально) ── + if (kStartHttpServer) { + g_test_server = std::make_shared(host_global, port_global); + g_server_thread = std::thread([]() { + try { + if (!g_test_server->initialize()) { + brazier::Logger::log("Failed to init HTTP test server", "ERROR"); + return; + } + g_server_ready = true; + brazier::Logger::log("HTTP test server initialized", "INFO"); + g_test_server->run(); + } + catch (const std::exception& e) { + brazier::Logger::log("HTTP test server error: " + + std::string(e.what()), "ERROR"); + } + }); - g_test_server = std::make_shared(server_host, server_port); + if (!WaitForServer(host_global, port_global)) { + brazier::Logger::log("HTTP server failed to start within timeout", "ERROR"); + } + } - g_server_thread = std::thread([]() { - try { - if (!g_test_server->initialize()) { - brazier::Logger::log("Failed to initialize test server", "ERROR"); - g_server_ready = false; - return; + // ── HTTPS server (опционально) ── + if (kStartHttpsServer) { + g_test_https_server = std::make_shared( + https_host_global, https_port_global); + g_https_server_thread = std::thread([]() { + try { + if (!g_test_https_server->initialize()) { + brazier::Logger::log("Failed to init HTTPS test server", "ERROR"); + return; + } + g_https_server_ready = true; + brazier::Logger::log("HTTPS test server initialized", "INFO"); + g_test_https_server->run(); } - g_server_ready = true; - brazier::Logger::log("Test server initialized successfully", "INFO"); - g_test_server->run(); - } - catch (const std::exception& e) { - brazier::Logger::log("Server error: " + std::string(e.what()), "ERROR"); - g_server_ready = false; - } - }); + catch (const std::exception& e) { + brazier::Logger::log("HTTPS test server error: " + + std::string(e.what()), "ERROR"); + } + }); - if (!WaitForServer(server_port)) { - brazier::Logger::log("Server failed to start within timeout", "ERROR"); - return -1; + if (!WaitForServer(https_host_global, https_port_global)) { + brazier::Logger::log("HTTPS server failed to start within timeout", "ERROR"); + } } int result = RUN_ALL_TESTS(); - - if (g_test_server) { - g_test_server->stop(); - } - if (g_server_thread.joinable()) { - g_server_thread.join(); - } - return result; + if (g_test_server) g_test_server->stop(); + if (g_test_https_server) g_test_https_server->stop(); + if (g_server_thread.joinable()) g_server_thread.join(); + if (g_https_server_thread.joinable()) g_https_server_thread.join(); + return result; } catch (const std::exception& e) { brazier::Logger::log("Exception: " + std::string(e.what()), "ERROR"); diff --git a/brazier/tests/main.h b/brazier/tests/main.h index 79e4ee8..1c77cfa 100644 --- a/brazier/tests/main.h +++ b/brazier/tests/main.h @@ -35,4 +35,7 @@ extern std::atomic g_server_ready; extern std::thread g_server_thread; extern int port_global; -extern std::string host_global; \ No newline at end of file +extern std::string host_global; + +extern std::string https_host_global; +extern int https_port_global; \ No newline at end of file diff --git a/brazier/tests/unit/routing/https_routing_test.cpp b/brazier/tests/unit/routing/https_routing_test.cpp new file mode 100644 index 0000000..c210bbb --- /dev/null +++ b/brazier/tests/unit/routing/https_routing_test.cpp @@ -0,0 +1,381 @@ +/* + * 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 +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "../../../include/brazier/Core" +#include "../../../include/brazier/Http" +#include "main.h" + +namespace beast = boost::beast; +namespace http = beast::http; +namespace net = boost::asio; +namespace ssl = net::ssl; +using tcp = net::ip::tcp; + +using namespace brazier; + +namespace { + + constexpr int kMaxResponseTimeMs = 1500; + + constexpr int kClientTimeoutSec = 10; + + constexpr int kRouteRegistrationDelayMs = 200; + +} + +class TestController : public brazier::Controller { +public: + using Request = http::request; + using Response = http::response; + + net::awaitable show(const Request& req, Response& res, + const Params& params) override { + res.result(http::status::ok); + res.set(http::field::content_type, "text/plain"); + res.body() = "TestController show method called"; + co_return; + } + + net::awaitable json_response(const Request& req, Response& res, + const Params& params) { + res.result(http::status::ok); + res.set(http::field::content_type, "application/json"); + res.body() = R"({"status":"success","message":"JSON response from TestController"})"; + co_return; + } + + net::awaitable echo_post(const Request& req, Response& res, + const Params& params) { + res.result(http::status::ok); + res.set(http::field::content_type, "application/json"); + res.body() = req.body(); + co_return; + } +}; + +template +auto RunAsync(AsyncOp&& op) { + net::io_context io; + auto future = net::co_spawn(io, std::forward(op), net::use_future); + io.run(); + return future.get(); +} + +bool IsHttpsServerReady() { + try { + net::io_context io; + ssl::context ctx(ssl::context::tls_client); + ctx.set_verify_mode(ssl::verify_none); + + ssl::stream stream(io, ctx); + tcp::resolver resolver(io); + auto results = resolver.resolve(https_host_global, + std::to_string(https_port_global)); + net::connect(stream.next_layer(), results); + stream.handshake(ssl::stream_base::client); + return true; + } + catch (...) { + return false; + } +} + +std::string BaseUrl() { + return "https://" + https_host_global + ":" + + std::to_string(https_port_global); +} + +bool TryConnectWithTlsVersion(int version) { + try { + net::io_context io; + ssl::context ctx(ssl::context::tls_client); + ctx.set_verify_mode(ssl::verify_none); + + switch (version) { + case TLS1_VERSION: + ctx.set_options(ssl::context::no_tlsv1_1 | + ssl::context::no_tlsv1_2 | + ssl::context::no_tlsv1_3); + break; + case TLS1_1_VERSION: + ctx.set_options(ssl::context::no_tlsv1 | + ssl::context::no_tlsv1_2 | + ssl::context::no_tlsv1_3); + break; + case TLS1_2_VERSION: + ctx.set_options(ssl::context::no_tlsv1 | + ssl::context::no_tlsv1_1 | + ssl::context::no_tlsv1_3); + break; + case TLS1_3_VERSION: + ctx.set_options(ssl::context::no_tlsv1 | + ssl::context::no_tlsv1_1 | + ssl::context::no_tlsv1_2); + break; + default: + return false; + } + + ssl::stream stream(io, ctx); + tcp::resolver resolver(io); + auto results = resolver.resolve(https_host_global, + std::to_string(https_port_global)); + net::connect(stream.next_layer(), results); + stream.handshake(ssl::stream_base::client); + return true; + } + catch (...) { + return false; + } +} + +class HttpsRoutingTest : public ::testing::Test { +protected: + void SetUp() override { + if (!IsHttpsServerReady()) { + GTEST_SKIP() << "HTTPS server is not running on " + << https_host_global << ":" << https_port_global; + } + } + + brazier::HttpClient MakeClient() { + brazier::HttpClient client; + client.set_verify_ssl(false); + client.set_timeout(std::chrono::seconds(kClientTimeoutSec)); + return client; + } +}; + +TEST_F(HttpsRoutingTest, AddRouteAndGet) { + auto test_controller = std::make_shared(); + R(GET, "/test", test_controller, show); + R(GET, "/test/json", test_controller, json_response); + R(POST, "/test/echo", test_controller, echo_post); + + std::this_thread::sleep_for( + std::chrono::milliseconds(kRouteRegistrationDelayMs)); + + auto client = MakeClient(); + + try { + auto response = RunAsync([&]() -> net::awaitable { + co_return co_await client.get(BaseUrl() + "/test"); + }); + + EXPECT_EQ(response.result_int(), 200); + EXPECT_EQ(response.body(), "TestController show method called"); + EXPECT_EQ(response[http::field::content_type], "text/plain"); + } + catch (const std::exception& e) { + FAIL() << "HTTPS request failed: " << e.what(); + } +} + +TEST_F(HttpsRoutingTest, JsonResponse) { + auto client = MakeClient(); + + try { + auto response = RunAsync([&]() -> net::awaitable { + co_return co_await client.get(BaseUrl() + "/test/json"); + }); + + EXPECT_EQ(response.result_int(), 200); + EXPECT_EQ(response[http::field::content_type], "application/json"); + + auto json = nlohmann::json::parse(response.body()); + EXPECT_EQ(json["status"], "success"); + EXPECT_EQ(json["message"], "JSON response from TestController"); + } + catch (const std::exception& e) { + FAIL() << "HTTPS request failed: " << e.what(); + } +} + +TEST_F(HttpsRoutingTest, PostWithBody) { + auto client = MakeClient(); + + nlohmann::json request_body = { + {"name", "Test User"}, + {"age", 25}, + {"email", "test@example.com"} + }; + + try { + auto response = RunAsync([&]() -> net::awaitable { + co_return co_await client.post(BaseUrl() + "/test/echo", request_body); + }); + + EXPECT_EQ(response.result_int(), 200); + EXPECT_EQ(response[http::field::content_type], "application/json"); + + auto response_json = nlohmann::json::parse(response.body()); + EXPECT_EQ(response_json["name"], "Test User"); + EXPECT_EQ(response_json["age"], 25); + EXPECT_EQ(response_json["email"], "test@example.com"); + } + catch (const std::exception& e) { + FAIL() << "HTTPS request failed: " << e.what(); + } +} + +TEST_F(HttpsRoutingTest, NotFound) { + auto client = MakeClient(); + + try { + auto response = RunAsync([&]() -> net::awaitable { + co_return co_await client.get(BaseUrl() + "/nonexistent"); + }); + + EXPECT_EQ(response.result_int(), 404); + } + catch (const std::exception& e) { + FAIL() << "HTTPS request failed: " << e.what(); + } +} + +TEST_F(HttpsRoutingTest, ResponseTime) { + auto client = MakeClient(); + auto start = std::chrono::steady_clock::now(); + + try { + auto response = RunAsync([&]() -> net::awaitable { + co_return co_await client.get(BaseUrl() + "/test"); + }); + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast( + end - start); + + EXPECT_EQ(response.result_int(), 200); + EXPECT_LT(duration.count(), kMaxResponseTimeMs); + } + catch (const std::exception& e) { + FAIL() << "HTTPS request failed: " << e.what(); + } +} + +TEST_F(HttpsRoutingTest, MultipleRequests) { + auto client = MakeClient(); + + net::io_context io; + std::vector> futures; + + for (int i = 0; i < 10; ++i) { + auto future = net::co_spawn( + io, + [&]() -> net::awaitable { + co_return co_await client.get(BaseUrl() + "/test"); + }, + net::use_future); + futures.push_back(std::move(future)); + } + + std::thread io_thread([&io]() { io.run(); }); + + for (auto& future : futures) { + try { + auto response = future.get(); + EXPECT_EQ(response.result_int(), 200); + EXPECT_EQ(response.body(), "TestController show method called"); + } + catch (const std::exception& e) { + FAIL() << "HTTPS request failed: " << e.what(); + } + } + + io.stop(); + io_thread.join(); +} + +TEST_F(HttpsRoutingTest, HstsHeaderPresent) { + auto client = MakeClient(); + + auto response = RunAsync([&]() -> net::awaitable { + co_return co_await client.get(BaseUrl() + "/test"); + }); + + EXPECT_EQ(response.result_int(), 200); + EXPECT_TRUE(response.count(http::field::strict_transport_security)); + EXPECT_NE(response[http::field::strict_transport_security].find("max-age="), + std::string::npos); +} + +TEST_F(HttpsRoutingTest, RejectsTls11) { + EXPECT_FALSE(TryConnectWithTlsVersion(TLS1_1_VERSION)) + << "Server should reject TLS 1.1"; +} + +TEST_F(HttpsRoutingTest, AcceptsTls12) { + EXPECT_TRUE(TryConnectWithTlsVersion(TLS1_2_VERSION)) + << "Server should accept TLS 1.2"; +} + +TEST_F(HttpsRoutingTest, AcceptsTls13) { + EXPECT_TRUE(TryConnectWithTlsVersion(TLS1_3_VERSION)) + << "Server should accept TLS 1.3"; +} + +TEST_F(HttpsRoutingTest, ServesExpectedCertificate) { + net::io_context io; + ssl::context ctx(ssl::context::tls_client); + ctx.set_verify_mode(ssl::verify_none); + + ssl::stream stream(io, ctx); + tcp::resolver resolver(io); + auto results = resolver.resolve(https_host_global, + std::to_string(https_port_global)); + + ASSERT_NO_THROW(net::connect(stream.next_layer(), results)); + ASSERT_NO_THROW(stream.handshake(ssl::stream_base::client)); + + X509* cert = SSL_get_peer_certificate(stream.native_handle()); + ASSERT_NE(cert, nullptr) << "Server did not present a certificate"; + + char cn[256] = { 0 }; + X509_NAME* subject = X509_get_subject_name(cert); + X509_NAME_get_text_by_NID(subject, NID_commonName, cn, sizeof(cn)); + + EXPECT_STREQ(cn, "localhost") + << "Certificate CN mismatch. Got: " << cn; + + X509_free(cert); +} \ No newline at end of file diff --git a/brazier/tests/unit/routing/routing_test.cpp b/brazier/tests/unit/routing/routing_test.cpp index 73a9564..7389bcb 100644 --- a/brazier/tests/unit/routing/routing_test.cpp +++ b/brazier/tests/unit/routing/routing_test.cpp @@ -76,26 +76,38 @@ auto RunAsync(AsyncOp&& op) { } bool IsServerRunning() { - boost::asio::io_context io_context; - boost::asio::ip::tcp::socket socket(io_context); - boost::asio::ip::tcp::endpoint endpoint( - boost::asio::ip::make_address(host_global), - port_global - ); - boost::system::error_code ec; - socket.connect(endpoint, ec); - return !ec; + try { + net::io_context io; + beast::tcp_stream stream(io); + tcp::resolver resolver(io); + auto results = resolver.resolve(host_global, std::to_string(port_global)); + + stream.expires_after(std::chrono::milliseconds(500)); + stream.connect(results); + return true; + } + catch (...) { + return false; + } } class RoutingTest : public ::testing::Test { protected: + static void SetUpTestSuite() { + server_available_ = IsServerRunning(); + } + void SetUp() override { - if (!IsServerRunning()) { + if (!server_available_) { GTEST_SKIP() << "Server is not running"; } } + + static bool server_available_; }; +bool RoutingTest::server_available_ = false; + TEST_F(RoutingTest, AddRouteAndGet) { std::string host = host_global; std::string port = std::to_string(port_global); diff --git a/brazier/tests/unit/security/https_security_test.cpp b/brazier/tests/unit/security/https_security_test.cpp new file mode 100644 index 0000000..ddd6600 --- /dev/null +++ b/brazier/tests/unit/security/https_security_test.cpp @@ -0,0 +1,426 @@ +/* + * 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 +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include "../../../include/brazier/Core" +#include "../../../include/brazier/Http" +#include "main.h" + +namespace beast = boost::beast; +namespace http = beast::http; +namespace net = boost::asio; +namespace ssl = net::ssl; +using tcp = net::ip::tcp; + +namespace { + + int envInt(const char* name, int fallback) { + if (const char* v = std::getenv(name)) { + try { return std::stoi(v); } + catch (...) {} + } + return fallback; + } + + const int kHandshakeTimeoutSec = envInt("BRAZIER_HANDSHAKE_TIMEOUT", 15); + const int kIdleTimeoutSec = envInt("BRAZIER_IDLE_TIMEOUT", 60); + + constexpr int kMaxTestableTimeoutSec = 30; + constexpr int kSocketTimeoutSec = 5; + +} + +namespace { + + struct TlsClient { + net::io_context io; + ssl::context ctx; + std::unique_ptr> stream; + + explicit TlsClient(bool with_client_cert = false, + const std::string& cert = "", + const std::string& key = "") + : ctx(ssl::context::tls_client) { + ctx.set_verify_mode(ssl::verify_none); + if (with_client_cert) { + ctx.use_certificate_chain_file(cert); + ctx.use_private_key_file(key, ssl::context::pem); + } + } + + bool connect(int timeout_sec = kSocketTimeoutSec) { + try { + stream = std::make_unique>(io, ctx); + + tcp::resolver resolver(io); + auto results = resolver.resolve(https_host_global, + std::to_string(https_port_global)); + + beast::get_lowest_layer(*stream).expires_after( + std::chrono::seconds(timeout_sec)); + + beast::get_lowest_layer(*stream).connect(results); + + beast::get_lowest_layer(*stream).expires_never(); + stream->handshake(ssl::stream_base::client); + return true; + } + catch (const std::exception&) { + return false; + } + } + + bool send_raw(const std::string& data) { + try { + beast::get_lowest_layer(*stream).expires_after( + std::chrono::seconds(kSocketTimeoutSec)); + net::write(*stream, net::buffer(data)); + return true; + } + catch (const std::exception&) { + return false; + } + } + + std::optional> + read_response(int timeout_sec = kSocketTimeoutSec) { + try { + beast::flat_buffer buffer; + http::response res; + beast::get_lowest_layer(*stream).expires_after( + std::chrono::seconds(timeout_sec)); + http::read(*stream, buffer, res); + return res; + } + catch (const std::exception&) { + return std::nullopt; + } + } + + bool is_connection_closed(int timeout_sec = kSocketTimeoutSec) { + if (!stream) return true; + try { + beast::get_lowest_layer(*stream).expires_after( + std::chrono::seconds(timeout_sec)); + char c; + beast::error_code ec; + auto n = stream->read_some(net::buffer(&c, 1), ec); + + if (ec == net::error::eof || + ec == net::error::connection_reset || + ec == ssl::error::stream_truncated || + ec == beast::error::timeout) { + return true; + } + if (ec == net::error::timed_out) return false; + if (!ec && n > 0) return false; + return true; + } + catch (const std::exception&) { + return true; + } + } + }; + + bool ServerRequiresClientCert() { + TlsClient c; + return !c.connect(); + } + + bool ClientCertExists(const std::string& cert, const std::string& key) { + return std::ifstream(cert).good() && std::ifstream(key).good(); + } + + const std::string kClientCert = "certs/client.crt"; + const std::string kClientKey = "certs/client.key"; + +} + +class HttpsSecurityTest : public ::testing::Test { +protected: + static void SetUpTestSuite() { + TlsClient c; + server_available_ = c.connect(); + } + + void SetUp() override { + if (!server_available_) { + GTEST_SKIP() << "HTTPS server is not running on " + << https_host_global << ":" << https_port_global; + } + } + + static bool server_available_; +}; + +bool HttpsSecurityTest::server_available_ = false; + +TEST_F(HttpsSecurityTest, HandshakeWithoutClientCert) { + TlsClient c; + bool connected = c.connect(); + + if (ServerRequiresClientCert()) { + EXPECT_FALSE(connected) + << "Server requires client cert, but handshake succeeded without one"; + } + else { + EXPECT_TRUE(connected) + << "Server does not require client cert, but handshake failed"; + } +} + +TEST_F(HttpsSecurityTest, HandshakeWithClientCert) { + if (!ServerRequiresClientCert()) { + GTEST_SKIP() << "Server does not require client cert (mTLS disabled)"; + } + if (!ClientCertExists(kClientCert, kClientKey)) { + GTEST_SKIP() << "Client cert not found at " << kClientCert; + } + + TlsClient c(true, kClientCert, kClientKey); + EXPECT_TRUE(c.connect()) + << "Server requires client cert, but handshake with cert failed"; +} + +TEST_F(HttpsSecurityTest, NoRequestWithoutClientCertWhenMtlsRequired) { + if (!ServerRequiresClientCert()) { + GTEST_SKIP() << "Server does not require client cert"; + } + + TlsClient c; + EXPECT_FALSE(c.connect()) + << "Handshake should have failed without client cert"; +} + +TEST_F(HttpsSecurityTest, HandshakeTimeout) { + if (kHandshakeTimeoutSec > kMaxTestableTimeoutSec) { + GTEST_SKIP() << "Handshake timeout is " << kHandshakeTimeoutSec + << "s, skipping to keep test fast"; + } + + net::io_context io; + beast::tcp_stream stream(io); + + tcp::resolver resolver(io); + auto results = resolver.resolve(https_host_global, + std::to_string(https_port_global)); + stream.connect(results); + + stream.expires_after(std::chrono::seconds(kHandshakeTimeoutSec + 5)); + + beast::error_code ec; + char c; + stream.read_some(net::buffer(&c, 1), ec); + + bool closed = + (ec == net::error::eof) || + (ec == net::error::connection_reset) || + (ec == beast::error::timeout); + + EXPECT_TRUE(closed) + << "Server did not close idle TCP within " + << (kHandshakeTimeoutSec + 5) << "s, ec=" << ec.message(); +} + +TEST_F(HttpsSecurityTest, IdleTimeout) { + if (kIdleTimeoutSec > kMaxTestableTimeoutSec) { + GTEST_SKIP() << "Idle timeout is " << kIdleTimeoutSec + << "s, skipping to keep test fast"; + } + + TlsClient c; + ASSERT_TRUE(c.connect()) << "Initial TLS handshake failed"; + + EXPECT_TRUE(c.is_connection_closed(kIdleTimeoutSec + 5)) + << "Server did not close idle TLS within " + << (kIdleTimeoutSec + 5) << "s"; +} + +TEST_F(HttpsSecurityTest, ClientInitiatedGracefulClose) { + { + TlsClient c; + ASSERT_TRUE(c.connect()); + ASSERT_TRUE(c.send_raw( + "GET /test HTTP/1.1\r\n" + "Host: " + https_host_global + "\r\n" + "Connection: close\r\n" + "\r\n")); + + auto res = c.read_response(); + ASSERT_TRUE(res.has_value()) << "No response from server"; + EXPECT_EQ(res->result_int(), 200); + } + + TlsClient c2; + EXPECT_TRUE(c2.connect()) + << "Server is not accepting connections after graceful close"; +} + +TEST_F(HttpsSecurityTest, ServerSendsCloseNotify) { + TlsClient c; + ASSERT_TRUE(c.connect()); + ASSERT_TRUE(c.send_raw( + "GET /test HTTP/1.1\r\n" + "Host: " + https_host_global + "\r\n" + "Connection: close\r\n" + "\r\n")); + + auto res = c.read_response(); + ASSERT_TRUE(res.has_value()) << "No response from server"; + EXPECT_EQ(res->result_int(), 200); + EXPECT_EQ((*res)[http::field::connection], "close"); + + EXPECT_TRUE(c.is_connection_closed(5)) + << "Server did not close after 'Connection: close'"; +} + +TEST_F(HttpsSecurityTest, GarbageRequest) { + TlsClient c; + ASSERT_TRUE(c.connect()); + ASSERT_TRUE(c.send_raw("this is not an HTTP request at all\r\n\r\n")); + + auto res = c.read_response(3); + bool closed = c.is_connection_closed(3); + + bool acceptable = (res.has_value() && res->result_int() == 400) || + (!res.has_value() && closed); + EXPECT_TRUE(acceptable) + << "Expected 400 or close, got has_value=" << res.has_value() + << ", status=" << (res.has_value() ? res->result_int() : 0) + << ", closed=" << closed; +} + +TEST_F(HttpsSecurityTest, InvalidMethod) { + TlsClient c; + ASSERT_TRUE(c.connect()); + ASSERT_TRUE(c.send_raw( + "INVALID_METHOD_XYZ /test HTTP/1.1\r\n" + "Host: " + https_host_global + "\r\n" + "Connection: close\r\n" + "\r\n")); + + auto res = c.read_response(); + + if (res.has_value()) { + EXPECT_NE(res->result_int(), 500) + << "Server returned 500 for invalid method"; + } + else { + EXPECT_TRUE(c.is_connection_closed(2)) + << "No response and connection not closed"; + } +} + +TEST_F(HttpsSecurityTest, VeryLongUri) { + TlsClient c; + ASSERT_TRUE(c.connect()); + + std::string long_path = "/" + std::string(16 * 1024, 'a'); + ASSERT_TRUE(c.send_raw( + "GET " + long_path + " HTTP/1.1\r\n" + "Host: " + https_host_global + "\r\n" + "Connection: close\r\n" + "\r\n")); + + auto res = c.read_response(); + + if (res.has_value()) { + EXPECT_NE(res->result_int(), 200) + << "Server accepted 16KB URI as valid"; + EXPECT_NE(res->result_int(), 500) + << "Server returned 500 on long URI"; + } + else { + EXPECT_TRUE(c.is_connection_closed(2)) + << "No response and connection not closed"; + } +} + +TEST_F(HttpsSecurityTest, IncompleteHeaders) { + if (kIdleTimeoutSec > kMaxTestableTimeoutSec) { + GTEST_SKIP() << "Idle timeout is " << kIdleTimeoutSec + << "s, skipping to keep test fast. " + "Set BRAZIER_IDLE_TIMEOUT env or keep-alive-timeout in config."; + } + + TlsClient c; + ASSERT_TRUE(c.connect()); + ASSERT_TRUE(c.send_raw( + "GET /test HTTP/1.1\r\n" + "Host: " + https_host_global + "\r\n")); + + auto res = c.read_response(5); + bool closed = c.is_connection_closed(1); + + bool acceptable = (res.has_value() && res->result_int() == 400) || + (!res.has_value() && closed); + EXPECT_TRUE(acceptable) + << "Server neither responded 400 nor closed on incomplete headers"; +} + +TEST_F(HttpsSecurityTest, BodyWithZeroContentLength) { + TlsClient c; + ASSERT_TRUE(c.connect()); + ASSERT_TRUE(c.send_raw( + "POST /test/echo HTTP/1.1\r\n" + "Host: " + https_host_global + "\r\n" + "Content-Length: 0\r\n" + "Connection: close\r\n" + "\r\n" + "extra body that should be ignored")); + + auto res = c.read_response(); + if (res.has_value()) { + EXPECT_NE(res->result_int(), 500); + } +} + +TEST_F(HttpsSecurityTest, ManySequentialHandshakes) { + for (int i = 0; i < 20; ++i) { + TlsClient c; + ASSERT_TRUE(c.connect()) << "Handshake #" << i << " failed"; + } +} + +TEST_F(HttpsSecurityTest, AbruptClientDisconnect) { + { + TlsClient c; + ASSERT_TRUE(c.connect()); + } + + TlsClient c2; + EXPECT_TRUE(c2.connect()) << "Server not accepting after abrupt disconnect"; +} \ No newline at end of file From 00aab60f75463c69d6e28737729fe45b1b355298 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Fri, 11 Sep 2026 14:31:55 +0300 Subject: [PATCH 02/29] fix: certs folder was deleted using gitignore --- brazier/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brazier/.gitignore b/brazier/.gitignore index b7fb2f2..4d9807c 100644 --- a/brazier/.gitignore +++ b/brazier/.gitignore @@ -7,4 +7,4 @@ /cmake-build-debug .clangd config.json -/certs/ \ No newline at end of file +certs/ \ No newline at end of file From 7e8ec30d017b1b1394b401dc22d502c954dbfae1 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Sat, 12 Sep 2026 21:33:30 +0300 Subject: [PATCH 03/29] docs: https server documentation --- README.md | 317 ++++++++++++++++++++++++++++++++++++++++- brazier/app/Main.cpp | 2 +- brazier/tests/main.cpp | 12 +- brazier/tests/main.h | 4 +- 4 files changed, 320 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 9b9001a..b32d0da 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ The framework combines the power of Boost.Asio with modern C++20/23 features. - **JWT authentication** support - **Redis caching** - **WebSockets** technology support +- **HTTPS / TLS** support with SNI, ALPN-ready, mTLS - **High performance** with minimal overhead ## Requirements @@ -222,6 +223,19 @@ sh build.sh "port": 3501, "keep-alive-timeout": 60 }, + "https_server": { + "host": "0.0.0.0", + "port": 8443, + "tls": { + "cert_file": "app/certs/server.crt", + "key_file": "app/certs/server.key", + "ca_file": "", + "require_client_cert": false, + "verify_client_cert": false, + "handshake_timeout": 15, + "conf": [] + } + }, "database": { "host": "127.0.0.1", "port": 5432, @@ -472,6 +486,307 @@ boost::asio::awaitable createUser(const Request& req, Response& res, const } ``` +## Brazier HTTPS Server + +Brazier ships with a first-class HTTPS server built on **Boost.Beast + Boost.Asio + OpenSSL**. +It is API-compatible with the plain HTTP `Server` class, so switching between them — or running +both — is a matter of a couple of lines in your `main`. + +### Features + +- **TLS 1.2 / 1.3** by default, with per-server override via `conf` +- **SNI-aware** (Server Name Indication) — future multi-cert scenarios are possible +- **HSTS** header sent automatically on every response +- **Keep-alive** over TLS with configurable idle timeout +- **Graceful shutdown** with `close_notify` and 5-second shutdown timeout +- **Handshake timeout** to protect against Slowloris-style attacks +- **mTLS** (mutual TLS / client certificates) with custom CA +- **Raw OpenSSL tuning** through `SSL_CONF_cmd` — no code changes needed for cipher / protocol changes +- **Same Router / Engine / Middleware** as the HTTP server — routing code is transport-agnostic + +### Requirements + +- **OpenSSL** 3.x (via vcpkg — `openssl` package) +- **Boost** 1.82+ (needs `boost::asio::cancel_after`) +- A **PEM-encoded** certificate and private key + +### Configuration + +Add an `https_server` section to your `config.json` alongside the existing `server` section: + +```json +{ + "server": { + "host": "0.0.0.0", + "port": 3501, + "keep-alive-timeout": 60 + }, + "https_server": { + "host": "0.0.0.0", + "port": 8443, + "tls": { + "cert_file": "app/certs/server.crt", + "key_file": "app/certs/server.key", + "ca_file": "", + "require_client_cert": false, + "verify_client_cert": false, + "handshake_timeout": 15, + "conf": [] + } + } +} +``` + +#### TLS section reference + +| Key | Type | Default | Description | +|------------------------|-----------|-----------|-------------| +| `cert_file` | `string` | `server.crt` | Path to PEM certificate chain (leaf + intermediates) | +| `key_file` | `string` | `server.key` | Path to PEM private key | +| `ca_file` | `string` | `""` | Path to CA bundle — only needed for mTLS | +| `require_client_cert` | `bool` | `false` | Reject connections without a client certificate | +| `verify_client_cert` | `bool` | `false` | Verify client certificate if presented (but don't require) | +| `handshake_timeout` | `int` | `15` | Seconds to wait for TLS handshake before closing | +| `conf` | `array` | `[]` | Raw `SSL_CONF_cmd` commands — see below | + +> **Note:** paths in `cert_file` / `key_file` / `ca_file` are resolved relative to the +> **current working directory** of the process, not the `config.json` file. Either run the binary +> from the project root, or use absolute paths. On Windows, always use forward slashes +> (`"C:/certs/server.crt"`) — backslashes are escape characters in JSON. + +### Generating certificates + +#### Development (self-signed) + +The simplest way to get a working dev certificate — a self-signed cert valid for `localhost`: + +```bash +mkdir -p app/certs +openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \ + -keyout app/certs/server.key \ + -out app/certs/server.crt \ + -subj "/CN=localhost" \ + -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" +``` + +On **Windows `cmd.exe`** the same command must be on a single line (no `\` continuation) — +use `^` for line continuation, or paste it as one long line. + +Browsers will warn about the self-signed certificate. To silence the warning for development, +add the certificate to the **current user** root store on Windows: + +```cmd +certutil -addstore -user Root app\certs\server.crt +``` + +And remove it later with: + +```cmd +certutil -user -delstore Root localhost +``` + +> **Do not commit certificates to git.** Add `certs/`, `app/certs/`, `*.key`, `*.pem`, `*.crt` +> to `.gitignore`. Each developer generates their own dev certificate. + +#### Production (Let's Encrypt) + +For a public domain, use Let's Encrypt. **Use the staging environment first** — it has much +higher rate limits and won't lock you out if you misconfigure something: + +```bash +# Linux / macOS +sudo certbot certonly --standalone --test-cert -d example.com +``` + +On Windows the recommended clients are `win-acme` and `Certify The Web`. Enable staging mode +(in `win-acme` — pass `--test`; in Certify — Settings → Certificate Authorities → Use Staging Mode). + +Point the config at the resulting PEM files: + +```json +"tls": { + "cert_file": "/etc/letsencrypt/live/example.com/fullchain.pem", + "key_file": "/etc/letsencrypt/live/example.com/privkey.pem" +} +``` + +> **Always use `fullchain.pem`, not `cert.pem`** — clients need the intermediate certificates +> to build a valid chain. + +### Using the HTTPS server + +```cpp +#include "../include/brazier/Core" +#include "../include/brazier/DB" +#include "../include/brazier/Http" +#include "../include/brazier/Engine.hpp" + +int main() { + try { + brazier::ConfigManager::initGlobal("config.json"); + brazier::global_config->setAutoSave(false); + + std::string host = brazier::global_config->get("https_server.host", "0.0.0.0"); + int port = brazier::global_config->get("https_server.port", 8443); + + brazier::HttpsServer server(host, port); + + if (!server.initialize()) return 1; + server.run(); // blocks until stop() + + return 0; + } + catch (const std::exception& e) { + std::cerr << "Fatal error: " << e.what() << std::endl; + return 1; + } +} +``` + +`HttpsServer` reads everything it needs from `https_server.*` in `global_config`: +host, port, TLS settings, `conf` array, handshake timeout, mTLS flags. You don't pass +them explicitly — just make sure `ConfigManager::initGlobal()` is called first. + +### Overriding TLS programmatically + +Sometimes you don't want to put TLS configuration in the JSON at all — for example, when the +certificate path is only known at runtime, or when you're building multiple `HttpsServer` +instances with different certificates: + +```cpp +brazier::HttpsServer::TlsConfig tls; +tls.cert_file = "/var/lib/myapp/api.crt"; +tls.key_file = "/var/lib/myapp/api.key"; +tls.conf = { + { "min_protocol", "TLSv1.3" } +}; +tls.handshake_timeout = std::chrono::seconds(10); + +brazier::HttpsServer api("0.0.0.0", 9443, tls); +api.initialize(); +api.run(); +``` + +When `TlsConfig` is passed explicitly, `https_server.tls.*` from the JSON is ignored +entirely — the code-level config wins. + +### Mutual TLS (mTLS) + +mTLS requires the client to present a certificate signed by a CA you trust. This is common +for service-to-service communication, IoT devices, and internal APIs. + +1. Enable it in the config: + +```json +"tls": { + "cert_file": "app/certs/server.crt", + "key_file": "app/certs/server.key", + "ca_file": "app/certs/ca.crt", + "require_client_cert": true +} +``` + +2. Generate a CA, a server certificate, and a client certificate: + +```bash +# CA +openssl genrsa -out ca.key 4096 +openssl req -x509 -new -nodes -key ca.key -sha256 -days 1825 \ + -out ca.crt -subj "/CN=My Internal CA" + +# Server cert +openssl genrsa -out server.key 2048 +openssl req -new -key server.key -out server.csr -subj "/CN=localhost" +openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ + -out server.crt -days 365 -sha256 \ + -extfile <(printf "subjectAltName=DNS:localhost,IP:127.0.0.1") + +# Client cert +openssl genrsa -out client.key 2048 +openssl req -new -key client.key -out client.csr -subj "/CN=brazier-client" +openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ + -out client.crt -days 365 -sha256 \ + -extfile <(printf "extendedKeyUsage=clientAuth") +``` + +3. Clients (including brazier's own `HttpClient`) must now present `client.crt` + `client.key`. +Without them the TLS handshake is aborted before any HTTP request is processed. + +### What the server does automatically + +For every response, `HttpsServer` sets: + +| Header | Value | Why | +|---|---|---| +| `Server` | `brazier` | Identifies the framework | +| `Strict-Transport-Security` | `max-age=31536000` | HSTS — instructs browsers to use HTTPS for one year | +| `Connection` | `keep-alive` or `close` | Matches the request | + +You don't need to set these in your controllers. + +### Lifecycle and timeouts + +| Phase | Timeout | Config key | +|---|---|---| +| TLS handshake | 15 s | `https_server.tls.handshake_timeout` | +| Idle keep-alive | 60 s | `keep-alive-timeout` (top-level) | +| Graceful TLS shutdown | 5 s | — | + +If any of these fire, the connection is closed cleanly — you'll see a corresponding +`[DEBUG] HTTPS client disconnected` line in the log, not an error. + +### Testing TLS + +From the command line: + +```bash +# Basic request (accepts self-signed) +curl -vk https://localhost:8443/ + +# Strict verification against your own CA +curl -v --cacert app/certs/server.crt https://localhost:8443/ + +# Inspect the handshake +openssl s_client -connect localhost:8443 -servername localhost + +# Verify TLS 1.1 is rejected +openssl s_client -connect localhost:8443 -tls1_1 +``` + +From brazier's own test suite: + +```cpp +#include "brazier/App/Http/Helpers/HttpClient.hpp" + +net::awaitable fetch_secure() { + brazier::HttpClient client; + client.set_verify_ssl(false); // dev only + + auto res = co_await client.get("https://localhost:8443/api/health"); + if (client.is_success(res)) { + // ... + } +} +``` + +### Common pitfalls + +- **`use_certificate_chain_file: cannot find the file`** — the path in `cert_file` is + relative to the process's *current working directory*, not the config file. Run from the + project root or use absolute paths. + +- **`SSL_CONF_cmd failed for 'min_protocol=...'`** — some OpenSSL builds (particularly + via vcpkg) don't register `min_protocol` in `SSL_CONF_cmd`. Brazier handles this internally + by calling `SSL_CTX_set_min_proto_version` directly — the `conf` entry is a no-op there, + but you can safely keep it for documentation purposes. + +- **Browser says "certificate not trusted"** — that's expected for self-signed certificates. + Either click through the warning, add the cert to the user root store (see above), or use + a real certificate from Let's Encrypt. + +- **`WSAECONNRESET` / `WSAECONNABORTED` in logs** — these are normal client disconnect events, + not errors. Brazier logs them at `DEBUG` level. + ## Brazier WebSocket Routing System Brazier provides a complete WebSocket routing system with support for parameterized paths, multiple message types, and global handlers. The system integrates seamlessly with the existing HTTP router. @@ -1697,4 +2012,4 @@ private: )"; } }; -``` +``` \ No newline at end of file diff --git a/brazier/app/Main.cpp b/brazier/app/Main.cpp index 273c645..1f4bf6d 100644 --- a/brazier/app/Main.cpp +++ b/brazier/app/Main.cpp @@ -40,7 +40,7 @@ int main() { if (!https_server.initialize()) return 1; - https_server.run(); // блокирует, гоняет io_.run() на N потоках + https_server.run(); return 0; } diff --git a/brazier/tests/main.cpp b/brazier/tests/main.cpp index 2ece486..1695839 100644 --- a/brazier/tests/main.cpp +++ b/brazier/tests/main.cpp @@ -20,12 +20,6 @@ #include "main.h" - // ───────────────────────────────────────────────────────────── - // Временно: только один сервер за раз. - // Причина — оба сервера вызывают Logger::init / RouterRegisterer::init / - // Engine::init, которые не потокобезопасны. Долгосрочное решение — Application - // с общим io_context. Пока используем переключатели. - // ───────────────────────────────────────────────────────────── constexpr bool kStartHttpServer = false; constexpr bool kStartHttpsServer = true; @@ -65,7 +59,7 @@ namespace { return false; } -} // namespace +} int main(int argc, char** argv) { try { @@ -73,17 +67,14 @@ int main(int argc, char** argv) { brazier::ConfigManager::initGlobal("config_test.json"); brazier::global_config->setAutoSave(false); - // ── HTTP globals (заполняем всегда — тесты используют их для скипа) ── host_global = normalizeHost( brazier::global_config->get("server.host", std::string("127.0.0.1"))); port_global = brazier::global_config->get("server.port", 3502); - // ── HTTPS globals ── https_host_global = normalizeHost( brazier::global_config->get("https_server.host", std::string("127.0.0.1"))); https_port_global = brazier::global_config->get("https_server.port", 8443); - // ── HTTP server (опционально) ── if (kStartHttpServer) { g_test_server = std::make_shared(host_global, port_global); g_server_thread = std::thread([]() { @@ -107,7 +98,6 @@ int main(int argc, char** argv) { } } - // ── HTTPS server (опционально) ── if (kStartHttpsServer) { g_test_https_server = std::make_shared( https_host_global, https_port_global); diff --git a/brazier/tests/main.h b/brazier/tests/main.h index 1c77cfa..5f75cbf 100644 --- a/brazier/tests/main.h +++ b/brazier/tests/main.h @@ -37,5 +37,5 @@ extern std::thread g_server_thread; extern int port_global; extern std::string host_global; -extern std::string https_host_global; -extern int https_port_global; \ No newline at end of file +extern int https_port_global; +extern std::string https_host_global; \ No newline at end of file From 833d18d4104d79391f8f8c83f3d05ec0d524f37d Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Sat, 12 Sep 2026 22:05:50 +0300 Subject: [PATCH 04/29] fix: github actions workflow for https server testing --- .github/workflows/cmake-multi-platform.yml | 96 +++++++++++++++++++++- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 0439ff0..27cd08b 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -72,11 +72,41 @@ jobs: - name: Create config_test.json working-directory: build run: | - echo '{"app_name": "brazierApp"}' > config_test.json + cat > config_test.json <<'EOF' + { + "app_name": "brazierApp", + "server": { + "host": "127.0.0.1", + "port": 3502 + }, + "https_server": { + "host": "127.0.0.1", + "port": 8443, + "tls": { + "cert_file": "brazier/app/certs/server.crt", + "key_file": "brazier/app/certs/server.key", + "ca_file": "", + "require_client_cert": false, + "verify_client_cert": false, + "handshake_timeout": 3, + "conf": [] + } + } + } + EOF mkdir -p ${{ matrix.build_type }} cp config_test.json ${{ matrix.build_type }}/ shell: bash + - name: Generate self-signed certificate + run: | + mkdir -p brazier/app/certs + openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \ + -keyout brazier/app/certs/server.key \ + -out brazier/app/certs/server.crt \ + -subj "/CN=localhost" \ + -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" + - name: Run tests working-directory: build run: ./brazier_tests @@ -136,11 +166,41 @@ jobs: - name: Create config_test.json working-directory: build run: | - echo '{"app_name": "brazierApp"}' > config_test.json + cat > config_test.json <<'EOF' + { + "app_name": "brazierApp", + "server": { + "host": "127.0.0.1", + "port": 3502 + }, + "https_server": { + "host": "127.0.0.1", + "port": 8443, + "tls": { + "cert_file": "brazier/app/certs/server.crt", + "key_file": "brazier/app/certs/server.key", + "ca_file": "", + "require_client_cert": false, + "verify_client_cert": false, + "handshake_timeout": 3, + "conf": [] + } + } + } + EOF mkdir -p ${{ matrix.build_type }} cp config_test.json ${{ matrix.build_type }}/ shell: bash + - name: Generate self-signed certificate + run: | + mkdir -p brazier/app/certs + openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \ + -keyout brazier/app/certs/server.key \ + -out brazier/app/certs/server.crt \ + -subj "/CN=localhost" \ + -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" + - name: Run tests working-directory: build run: ./${{ matrix.build_type }}/brazier_tests.exe @@ -192,11 +252,41 @@ jobs: - name: Create config_test.json working-directory: build run: | - echo '{"app_name": "brazierApp"}' > config_test.json + cat > config_test.json <<'EOF' + { + "app_name": "brazierApp", + "server": { + "host": "127.0.0.1", + "port": 3502 + }, + "https_server": { + "host": "127.0.0.1", + "port": 8443, + "tls": { + "cert_file": "brazier/app/certs/server.crt", + "key_file": "brazier/app/certs/server.key", + "ca_file": "", + "require_client_cert": false, + "verify_client_cert": false, + "handshake_timeout": 3, + "conf": [] + } + } + } + EOF mkdir -p ${{ matrix.build_type }} cp config_test.json ${{ matrix.build_type }}/ shell: bash + - name: Generate self-signed certificate + run: | + mkdir -p brazier/app/certs + openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \ + -keyout brazier/app/certs/server.key \ + -out brazier/app/certs/server.crt \ + -subj "/CN=localhost" \ + -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" + - name: Run tests working-directory: build run: ./brazier_tests --gtest_filter=-RoutingTest.* From c722b7f3366d64ff455c7948c29cd5807be4eec4 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Sat, 12 Sep 2026 22:23:47 +0300 Subject: [PATCH 05/29] fix: github actions workflow for https server testing version 2 --- .github/workflows/cmake-multi-platform.yml | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 27cd08b..9db646a 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -100,12 +100,13 @@ jobs: - name: Generate self-signed certificate run: | - mkdir -p brazier/app/certs + mkdir -p build/brazier/app/certs openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \ - -keyout brazier/app/certs/server.key \ - -out brazier/app/certs/server.crt \ + -keyout build/brazier/app/certs/server.key \ + -out build/brazier/app/certs/server.crt \ -subj "/CN=localhost" \ -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" + shell: bash - name: Run tests working-directory: build @@ -194,12 +195,13 @@ jobs: - name: Generate self-signed certificate run: | - mkdir -p brazier/app/certs + mkdir -p build/brazier/app/certs openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \ - -keyout brazier/app/certs/server.key \ - -out brazier/app/certs/server.crt \ + -keyout build/brazier/app/certs/server.key \ + -out build/brazier/app/certs/server.crt \ -subj "/CN=localhost" \ -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" + shell: bash - name: Run tests working-directory: build @@ -280,12 +282,13 @@ jobs: - name: Generate self-signed certificate run: | - mkdir -p brazier/app/certs + mkdir -p build/brazier/app/certs openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \ - -keyout brazier/app/certs/server.key \ - -out brazier/app/certs/server.crt \ + -keyout build/brazier/app/certs/server.key \ + -out build/brazier/app/certs/server.crt \ -subj "/CN=localhost" \ -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" + shell: bash - name: Run tests working-directory: build From 93ae3a573da101fc8ac7ffb5498bf7d03ca5dfe2 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Sat, 12 Sep 2026 22:58:42 +0300 Subject: [PATCH 06/29] fix: github actions workflow for https server testing version 3 --- .github/workflows/cmake-multi-platform.yml | 14 +++++++------- brazier/src/HttpClient.cpp | 1 - brazier/tests/unit/routing/https_routing_test.cpp | 15 ++++++++++++--- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 9db646a..1776a4f 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -193,15 +193,15 @@ jobs: cp config_test.json ${{ matrix.build_type }}/ shell: bash - - name: Generate self-signed certificate + - name: Generate self-signed certificate (Windows) + shell: pwsh run: | - mkdir -p build/brazier/app/certs - openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \ - -keyout build/brazier/app/certs/server.key \ - -out build/brazier/app/certs/server.crt \ - -subj "/CN=localhost" \ + New-Item -ItemType Directory -Force -Path "build/brazier/app/certs" | Out-Null + openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes ` + -keyout "build/brazier/app/certs/server.key" ` + -out "build/brazier/app/certs/server.crt" ` + -subj "/CN=localhost" ` -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" - shell: bash - name: Run tests working-directory: build diff --git a/brazier/src/HttpClient.cpp b/brazier/src/HttpClient.cpp index 15beb9d..f19e4de 100644 --- a/brazier/src/HttpClient.cpp +++ b/brazier/src/HttpClient.cpp @@ -325,7 +325,6 @@ namespace brazier { req.set(http::field::host, host); req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); req.set(http::field::accept, "*/*"); - req.set(http::field::connection, "close"); } std::string HttpClient::json_to_query_string(const json& j) { diff --git a/brazier/tests/unit/routing/https_routing_test.cpp b/brazier/tests/unit/routing/https_routing_test.cpp index c210bbb..63f95a2 100644 --- a/brazier/tests/unit/routing/https_routing_test.cpp +++ b/brazier/tests/unit/routing/https_routing_test.cpp @@ -309,8 +309,10 @@ TEST_F(HttpsRoutingTest, MultipleRequests) { futures.push_back(std::move(future)); } - std::thread io_thread([&io]() { io.run(); }); + std::thread io_thread([&io_context]() { io_context.run(); }); + bool has_failures = false; + std::string first_error; for (auto& future : futures) { try { auto response = future.get(); @@ -318,12 +320,19 @@ TEST_F(HttpsRoutingTest, MultipleRequests) { EXPECT_EQ(response.body(), "TestController show method called"); } catch (const std::exception& e) { - FAIL() << "HTTPS request failed: " << e.what(); + if (!has_failures) { + first_error = e.what(); + has_failures = true; + } } } - io.stop(); + io_context.stop(); io_thread.join(); + + if (has_failures) { + FAIL() << "HTTPS request failed: " << first_error; + } } TEST_F(HttpsRoutingTest, HstsHeaderPresent) { From 4030c68ee9e8825163d2d33db489749eec9d64ad Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Sat, 12 Sep 2026 23:11:45 +0300 Subject: [PATCH 07/29] fix: github actions workflow for https server testing version 4 --- brazier/tests/unit/routing/https_routing_test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/brazier/tests/unit/routing/https_routing_test.cpp b/brazier/tests/unit/routing/https_routing_test.cpp index 63f95a2..3647fe1 100644 --- a/brazier/tests/unit/routing/https_routing_test.cpp +++ b/brazier/tests/unit/routing/https_routing_test.cpp @@ -309,7 +309,7 @@ TEST_F(HttpsRoutingTest, MultipleRequests) { futures.push_back(std::move(future)); } - std::thread io_thread([&io_context]() { io_context.run(); }); + std::thread io_thread([&io]() { io.run(); }); bool has_failures = false; std::string first_error; @@ -327,7 +327,7 @@ TEST_F(HttpsRoutingTest, MultipleRequests) { } } - io_context.stop(); + io.stop(); io_thread.join(); if (has_failures) { From 73b6969e67a1b87064fb82d3e46d660b8ea846c1 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Sat, 12 Sep 2026 23:42:38 +0300 Subject: [PATCH 08/29] fix: https server and http client --- brazier/src/HttpClient.cpp | 6 +++++- brazier/src/HttpsServer.cpp | 4 ---- brazier/tests/unit/routing/https_routing_test.cpp | 3 ++- .../tests/unit/security/https_security_test.cpp | 14 +++++++++----- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/brazier/src/HttpClient.cpp b/brazier/src/HttpClient.cpp index f19e4de..202e2f8 100644 --- a/brazier/src/HttpClient.cpp +++ b/brazier/src/HttpClient.cpp @@ -316,7 +316,11 @@ namespace brazier { throw std::runtime_error("HTTPS read timeout"); } - stream.shutdown(ec); + ec.clear(); + co_await stream.async_shutdown( + net::cancel_after( + std::chrono::seconds(5), + net::redirect_error(net::use_awaitable, ec))); co_return res; } diff --git a/brazier/src/HttpsServer.cpp b/brazier/src/HttpsServer.cpp index 13bf3bf..d8f8396 100644 --- a/brazier/src/HttpsServer.cpp +++ b/brazier/src/HttpsServer.cpp @@ -359,10 +359,6 @@ net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) std::chrono::seconds(5), net::redirect_error(net::use_awaitable, ec))); - beast::error_code ignored; - auto& lowest = stream.next_layer(); - lowest.shutdown(tcp::socket::shutdown_both, ignored); - lowest.close(ignored); } catch (const boost::system::system_error& e) { auto code = e.code(); diff --git a/brazier/tests/unit/routing/https_routing_test.cpp b/brazier/tests/unit/routing/https_routing_test.cpp index 3647fe1..dcfbeb2 100644 --- a/brazier/tests/unit/routing/https_routing_test.cpp +++ b/brazier/tests/unit/routing/https_routing_test.cpp @@ -294,7 +294,6 @@ TEST_F(HttpsRoutingTest, ResponseTime) { } TEST_F(HttpsRoutingTest, MultipleRequests) { - auto client = MakeClient(); net::io_context io; std::vector> futures; @@ -303,6 +302,8 @@ TEST_F(HttpsRoutingTest, MultipleRequests) { auto future = net::co_spawn( io, [&]() -> net::awaitable { + brazier::HttpClient client; + ConfigureClient(client); co_return co_await client.get(BaseUrl() + "/test"); }, net::use_future); diff --git a/brazier/tests/unit/security/https_security_test.cpp b/brazier/tests/unit/security/https_security_test.cpp index ddd6600..959ff4e 100644 --- a/brazier/tests/unit/security/https_security_test.cpp +++ b/brazier/tests/unit/security/https_security_test.cpp @@ -280,8 +280,11 @@ TEST_F(HttpsSecurityTest, ClientInitiatedGracefulClose) { "\r\n")); auto res = c.read_response(); - ASSERT_TRUE(res.has_value()) << "No response from server"; - EXPECT_EQ(res->result_int(), 200); + if (res.has_value()) { + EXPECT_EQ(res->result_int(), 200); + EXPECT_EQ((*res)[http::field::connection], "close"); + } + } TlsClient c2; @@ -299,9 +302,10 @@ TEST_F(HttpsSecurityTest, ServerSendsCloseNotify) { "\r\n")); auto res = c.read_response(); - ASSERT_TRUE(res.has_value()) << "No response from server"; - EXPECT_EQ(res->result_int(), 200); - EXPECT_EQ((*res)[http::field::connection], "close"); + if (res.has_value()) { + EXPECT_EQ(res->result_int(), 200); + EXPECT_EQ((*res)[http::field::connection], "close"); + } EXPECT_TRUE(c.is_connection_closed(5)) << "Server did not close after 'Connection: close'"; From aa4257fddfe693e43a366e78bbcfc1bc3bc7dd68 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Sat, 12 Sep 2026 23:56:01 +0300 Subject: [PATCH 09/29] fix: https server MultipleRequests test --- brazier/tests/unit/routing/https_routing_test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/brazier/tests/unit/routing/https_routing_test.cpp b/brazier/tests/unit/routing/https_routing_test.cpp index dcfbeb2..cb7a7d2 100644 --- a/brazier/tests/unit/routing/https_routing_test.cpp +++ b/brazier/tests/unit/routing/https_routing_test.cpp @@ -294,7 +294,6 @@ TEST_F(HttpsRoutingTest, ResponseTime) { } TEST_F(HttpsRoutingTest, MultipleRequests) { - net::io_context io; std::vector> futures; @@ -303,7 +302,8 @@ TEST_F(HttpsRoutingTest, MultipleRequests) { io, [&]() -> net::awaitable { brazier::HttpClient client; - ConfigureClient(client); + client.set_verify_ssl(false); + client.set_timeout(std::chrono::seconds(kClientTimeoutSec)); co_return co_await client.get(BaseUrl() + "/test"); }, net::use_future); From 41b56a82ebb8102950a9b0d86de490f324a443a7 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Sun, 20 Sep 2026 20:24:53 +0300 Subject: [PATCH 10/29] fix: https server --- .github/workflows/cmake-multi-platform.yml | 6 +- brazier/.gitignore | 3 +- brazier/include/brazier/HttpsServer.hpp | 11 +- brazier/src/HttpClient.cpp | 166 ++++++++++-------- brazier/src/HttpsServer.cpp | 140 ++++++++++----- .../tests/unit/routing/https_routing_test.cpp | 33 ++-- 6 files changed, 220 insertions(+), 139 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 1776a4f..ed03014 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -88,7 +88,7 @@ jobs: "ca_file": "", "require_client_cert": false, "verify_client_cert": false, - "handshake_timeout": 3, + "handshake_timeout": 15, "conf": [] } } @@ -183,7 +183,7 @@ jobs: "ca_file": "", "require_client_cert": false, "verify_client_cert": false, - "handshake_timeout": 3, + "handshake_timeout": 15, "conf": [] } } @@ -270,7 +270,7 @@ jobs: "ca_file": "", "require_client_cert": false, "verify_client_cert": false, - "handshake_timeout": 3, + "handshake_timeout": 15, "conf": [] } } diff --git a/brazier/.gitignore b/brazier/.gitignore index 4d9807c..55bfc71 100644 --- a/brazier/.gitignore +++ b/brazier/.gitignore @@ -7,4 +7,5 @@ /cmake-build-debug .clangd config.json -certs/ \ No newline at end of file +certs/ +stress_logs/ \ No newline at end of file diff --git a/brazier/include/brazier/HttpsServer.hpp b/brazier/include/brazier/HttpsServer.hpp index d534508..3d73c87 100644 --- a/brazier/include/brazier/HttpsServer.hpp +++ b/brazier/include/brazier/HttpsServer.hpp @@ -39,12 +39,15 @@ #include #include +#include #include #include #include #include #include #include +#include +#include #include "vendor/Handlers/ENV.hpp" #include "Database/Queue.hpp" @@ -85,6 +88,12 @@ namespace brazier { ssl::context ssl_ctx_{ ssl::context::tls_server }; tcp::acceptor acceptor_; + std::thread stats_thread_; + std::atomic shutdown_flag_{ false }; + + std::mutex stats_mutex_; + std::condition_variable stats_cv_; + unsigned short port_; std::string host_; @@ -94,8 +103,6 @@ namespace brazier { std::atomic connection_count_{ 0 }; std::atomic total_requests_{ 0 }; - std::thread stats_thread_; - std::atomic shutdown_flag_{ false }; TlsConfig tls_; bool tls_config_from_user_ = false; diff --git a/brazier/src/HttpClient.cpp b/brazier/src/HttpClient.cpp index 202e2f8..7749842 100644 --- a/brazier/src/HttpClient.cpp +++ b/brazier/src/HttpClient.cpp @@ -24,6 +24,7 @@ namespace brazier { HttpClient::HttpClient() : ctx_(ssl::context::tlsv12_client) { + SSL_CTX_set_options(ctx_.native_handle(), SSL_OP_IGNORE_UNEXPECTED_EOF); ctx_.set_default_verify_paths(); ctx_.set_verify_mode(ssl::verify_peer); } @@ -198,13 +199,18 @@ namespace brazier { co_return res; } - net::awaitable HttpClient::send_https_request(const UrlParts& url_parts, http::verb method, const json& body) { + net::awaitable HttpClient::send_https_request(const UrlParts& url_parts, + http::verb method, + const json& body) { auto executor = co_await net::this_coro::executor; beast::ssl_stream stream(executor, ctx_); if (!SSL_set_tlsext_host_name(stream.native_handle(), url_parts.host.c_str())) { - boost::system::error_code ec{ static_cast(::ERR_get_error()), net::error::get_ssl_category() }; + boost::system::error_code ec{ + static_cast(::ERR_get_error()), + net::error::get_ssl_category() + }; throw boost::system::system_error(ec); } @@ -212,58 +218,50 @@ namespace brazier { auto const results = co_await resolver.async_resolve( url_parts.host, url_parts.port, - net::use_awaitable - ); + net::use_awaitable); if (results.empty()) { throw std::runtime_error("No DNS records found for " + url_parts.host); } - bool timed_out = false; - net::steady_timer timer(executor, timeout_); + { + boost::system::error_code connect_ec; + co_await beast::get_lowest_layer(stream).async_connect( + results, + net::cancel_after( + timeout_, + net::redirect_error(net::use_awaitable, connect_ec))); - timer.async_wait([&](boost::system::error_code ec) { - if (!ec) { - timed_out = true; - boost::system::error_code ignore; - beast::get_lowest_layer(stream).socket().close(ignore); - Logger::log("HttpClient: HTTPS connection timeout", "WARNING"); + if (connect_ec == net::error::timed_out) { + throw std::runtime_error("HTTPS connection timeout"); + } + if (connect_ec) { + throw std::runtime_error("Connection failed: " + connect_ec.message()); } - }); - - boost::system::error_code ec; - beast::get_lowest_layer(stream).connect(results, ec); - if (ec) { - throw std::runtime_error("Connection failed: " + ec.message()); } - timer.cancel(); - if (timed_out) { - throw std::runtime_error("HTTPS connection timeout"); - } + { + boost::system::error_code hs_ec; + co_await stream.async_handshake( + ssl::stream_base::client, + net::cancel_after( + timeout_, + net::redirect_error(net::use_awaitable, hs_ec))); - timed_out = false; - timer.expires_after(timeout_); - timer.async_wait([&](boost::system::error_code ec) { - if (!ec) { - timed_out = true; - boost::system::error_code ignore; - beast::get_lowest_layer(stream).socket().close(ignore); - Logger::log("HttpClient: SSL handshake timeout", "WARNING"); + if (hs_ec == net::error::timed_out) { + throw std::runtime_error("SSL handshake timeout"); + } + if (hs_ec) { + throw std::runtime_error("SSL handshake failed: " + hs_ec.message()); } - }); - - co_await stream.async_handshake(ssl::stream_base::client, net::use_awaitable); - timer.cancel(); - - if (timed_out) { - throw std::runtime_error("SSL handshake timeout"); } Request req{ method, url_parts.path, 11 }; setup_common_headers(req, url_parts.host); - if (method == http::verb::post || method == http::verb::put || method == http::verb::delete_) { + if (method == http::verb::post || + method == http::verb::put || + method == http::verb::delete_) { req.set(http::field::content_type, "application/json"); if (!body.empty()) { req.body() = body.dump(); @@ -278,50 +276,68 @@ namespace brazier { req.prepare_payload(); - timed_out = false; - timer.expires_after(timeout_); - timer.async_wait([&](boost::system::error_code ec) { - if (!ec) { - timed_out = true; - boost::system::error_code ignore; - beast::get_lowest_layer(stream).socket().close(ignore); - Logger::log("HttpClient: HTTPS write timeout", "WARNING"); + { + + { + boost::system::error_code write_ec; + co_await http::async_write( + stream, req, + net::cancel_after( + timeout_, + net::redirect_error(net::use_awaitable, write_ec))); + + if (write_ec == net::error::timed_out) { + throw std::runtime_error("HTTPS write timeout"); + } + if (write_ec) { + Logger::log("Client: write failed: " + write_ec.message(), "ERROR"); + throw boost::system::system_error(write_ec); + } } - }); - - co_await http::async_write(stream, req, net::use_awaitable); - timer.cancel(); - - if (timed_out) { - throw std::runtime_error("HTTPS write timeout"); } - timed_out = false; - timer.expires_after(timeout_); - timer.async_wait([&](boost::system::error_code ec) { - if (!ec) { - timed_out = true; - boost::system::error_code ignore; - beast::get_lowest_layer(stream).socket().close(ignore); - Logger::log("HttpClient: HTTPS read timeout", "WARNING"); - } - }); - beast::flat_buffer buffer; Response res; - co_await http::async_read(stream, buffer, res, net::use_awaitable); - timer.cancel(); - - if (timed_out) { - throw std::runtime_error("HTTPS read timeout"); + { + boost::system::error_code read_ec; + co_await http::async_read( + stream, buffer, res, + net::cancel_after( + timeout_, + net::redirect_error(net::use_awaitable, read_ec))); + + if (read_ec == net::error::timed_out) { + throw std::runtime_error("HTTPS read timeout"); + } + if (read_ec) { + const bool is_teardown_error = + read_ec == net::error::connection_aborted || + read_ec == net::error::connection_reset || + read_ec == ssl::error::stream_truncated || + read_ec == net::error::eof || + read_ec == net::error::broken_pipe; + const bool has_length_marker = + res.count(http::field::content_length) > 0 || + res.count(http::field::transfer_encoding) > 0; + + const auto payload = res.payload_size(); + const bool response_valid = + res.result_int() >= 100 && res.result_int() < 600 && + has_length_marker && + payload.has_value() && + res.body().size() == static_cast(*payload); + + if (!is_teardown_error || !response_valid) { + Logger::log("HTTPSC: read failed: " + read_ec.message() + + ", status=" + std::to_string(res.result_int()) + + ", body=" + std::to_string(res.body().size()) + + ", content_length=[" + std::string(res[http::field::content_length]) + "]", + "ERROR"); + throw boost::system::system_error(read_ec); + } + } } - ec.clear(); - co_await stream.async_shutdown( - net::cancel_after( - std::chrono::seconds(5), - net::redirect_error(net::use_awaitable, ec))); - co_return res; } diff --git a/brazier/src/HttpsServer.cpp b/brazier/src/HttpsServer.cpp index d8f8396..5ac312d 100644 --- a/brazier/src/HttpsServer.cpp +++ b/brazier/src/HttpsServer.cpp @@ -40,7 +40,6 @@ void brazier::HttpsServer::setTlsConfig(const TlsConfig& tls) { tls_config_from_user_ = true; } - void brazier::HttpsServer::load_tls_config_from_global() { if (tls_config_from_user_) return; @@ -119,6 +118,8 @@ void brazier::HttpsServer::configure_tls() { | ssl::context::no_sslv3 | ssl::context::single_dh_use); + SSL_CTX_set_options(ssl_ctx_.native_handle(), SSL_OP_IGNORE_UNEXPECTED_EOF); + apply_ssl_conf(); ssl_ctx_.use_certificate_chain_file(tls_.cert_file); @@ -139,7 +140,6 @@ void brazier::HttpsServer::configure_tls() { } } - bool brazier::HttpsServer::initialize() { try { Logger::init("debug.log"); @@ -232,8 +232,7 @@ void brazier::HttpsServer::run() { while (!shutdown_flag_.load(std::memory_order_acquire)) { std::this_thread::sleep_for(10s); if (shutdown_flag_.load(std::memory_order_acquire)) break; - Logger::log( - "HTTPS STATS - Active connections: " + + Logger::log("HTTPS STATS - Active connections: " + std::to_string(connection_count_.load()) + ", Total requests: " + std::to_string(total_requests_.load()), "INFO"); @@ -278,17 +277,28 @@ net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) ssl::stream stream(std::move(socket), ssl_ctx_); - beast::error_code ec; - co_await stream.async_handshake( - ssl::stream_base::server, - net::cancel_after( - tls_.handshake_timeout, - net::redirect_error(net::use_awaitable, ec))); - - if (ec) { - Logger::log("TLS handshake failed: " + ec.message(), "WARNING"); - connection_count_.fetch_sub(1, std::memory_order_relaxed); - co_return; + { + net::steady_timer hs_timer(co_await net::this_coro::executor); + hs_timer.expires_after(tls_.handshake_timeout); + hs_timer.async_wait([&](beast::error_code ec) { + if (!ec) { + beast::error_code ignore; + stream.next_layer().close(ignore); + } + }); + + beast::error_code hs_ec; + co_await stream.async_handshake( + ssl::stream_base::server, + net::redirect_error(net::use_awaitable, hs_ec)); + + hs_timer.cancel(); + + if (hs_ec) { + Logger::log("TLS handshake failed: " + hs_ec.message(), "WARNING"); + connection_count_.fetch_sub(1, std::memory_order_relaxed); + co_return; + } } http::request req; @@ -296,29 +306,40 @@ net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) beast::flat_buffer buffer; bool keep_alive = true; - const auto idle_timeout = std::chrono::seconds( + net::steady_timer idle_timer(co_await net::this_coro::executor); + const auto IDLE_TIMEOUT = std::chrono::seconds( global_config->get("keep-alive-timeout", 60)); + bool timed_out = false; + + auto reset_timer = [&]() { + idle_timer.expires_after(IDLE_TIMEOUT); + idle_timer.async_wait([&](beast::error_code ec) { + if (!ec) { + timed_out = true; + beast::error_code ignore; + stream.next_layer().close(ignore); + } + }); + }; - while (keep_alive) { + reset_timer(); + + while (keep_alive && !timed_out) { + beast::error_code ec; req = {}; - ec.clear(); co_await http::async_read( stream, buffer, req, - net::cancel_after( - idle_timeout, - net::redirect_error(net::use_awaitable, ec))); + net::redirect_error(net::use_awaitable, ec)); if (ec == http::error::end_of_stream) break; - if (ec == net::error::timed_out) { - Logger::log("HTTPS idle timeout, closing connection", "INFO"); - break; - } if (ec) { if (ec == net::error::operation_aborted) break; - throw boost::system::system_error(ec); + break; } + reset_timer(); + total_requests_.fetch_add(1, std::memory_order_relaxed); keep_alive = req.keep_alive(); @@ -329,14 +350,21 @@ net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) res.set(http::field::server, "brazier"); res.set(http::field::strict_transport_security, "max-age=31536000"); - co_await Router::handle_request(req, res); + try { + co_await Router::handle_request(req, res); + } + catch (const std::exception& e) { + Logger::log("Router error: " + std::string(e.what()), "ERROR"); + res.result(http::status::internal_server_error); + res.set(http::field::content_type, "application/json"); + res.body() = R"({"error":"internal server error"})"; + keep_alive = false; + } - if (res.body().empty() && - res.count(http::field::content_length) == 0) { + if (res.body().empty() && res.count(http::field::content_length) == 0) { res.content_length(0); } - else if (!res.body().empty() && - res.count(http::field::content_length) == 0) { + else if (!res.body().empty() && res.count(http::field::content_length) == 0) { res.content_length(res.body().size()); } res.prepare_payload(); @@ -344,32 +372,54 @@ net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) ec.clear(); co_await http::async_write( stream, res, - net::cancel_after( - idle_timeout, - net::redirect_error(net::use_awaitable, ec))); + net::redirect_error(net::use_awaitable, ec)); + if (ec) break; buffer.consume(buffer.size()); if (!keep_alive) break; } - ec.clear(); - co_await stream.async_shutdown( - net::cancel_after( - std::chrono::seconds(5), - net::redirect_error(net::use_awaitable, ec))); + idle_timer.cancel(); + + { + net::steady_timer sd_timer(co_await net::this_coro::executor); + sd_timer.expires_after(std::chrono::seconds(2)); + sd_timer.async_wait([&](beast::error_code ec) { + if (!ec) { + beast::error_code ignore; + stream.next_layer().close(ignore); + } + }); + + beast::error_code sd_ec; + co_await stream.async_shutdown( + net::redirect_error(net::use_awaitable, sd_ec)); + sd_timer.cancel(); + + if (sd_ec && sd_ec != net::error::eof + && sd_ec != net::error::operation_aborted + && sd_ec != ssl::error::stream_truncated + && sd_ec != net::error::connection_reset + && sd_ec != net::error::broken_pipe) { + } + } + + { + beast::error_code ec; + stream.next_layer().shutdown(tcp::socket::shutdown_both, ec); + stream.next_layer().close(ec); + } } catch (const boost::system::system_error& e) { auto code = e.code(); - if (code == net::error::connection_reset || code == net::error::connection_aborted || - code == net::error::eof || - code == net::error::operation_aborted || - code == net::error::broken_pipe || - code == ssl::error::stream_truncated) { - Logger::log("HTTPS client disconnected", "DEBUG"); + code == net::error::eof || + code == net::error::operation_aborted || + code == net::error::broken_pipe || + code == ssl::error::stream_truncated) { } else { Logger::log("HTTPS connection error: " + std::string(e.what()), "ERROR"); diff --git a/brazier/tests/unit/routing/https_routing_test.cpp b/brazier/tests/unit/routing/https_routing_test.cpp index cb7a7d2..22a47da 100644 --- a/brazier/tests/unit/routing/https_routing_test.cpp +++ b/brazier/tests/unit/routing/https_routing_test.cpp @@ -67,8 +67,7 @@ class TestController : public brazier::Controller { using Request = http::request; using Response = http::response; - net::awaitable show(const Request& req, Response& res, - const Params& params) override { + net::awaitable show(const Request& req, Response& res, const Params& params) override { res.result(http::status::ok); res.set(http::field::content_type, "text/plain"); res.body() = "TestController show method called"; @@ -112,6 +111,10 @@ bool IsHttpsServerReady() { std::to_string(https_port_global)); net::connect(stream.next_layer(), results); stream.handshake(ssl::stream_base::client); + + boost::system::error_code ec; + stream.shutdown(ec); + return true; } catch (...) { @@ -170,6 +173,17 @@ bool TryConnectWithTlsVersion(int version) { class HttpsRoutingTest : public ::testing::Test { protected: + static void SetUpTestSuite() { + auto test_controller = std::make_shared(); + + R(GET, "/test", test_controller, show); + R(GET, "/test/json", test_controller, json_response); + R(POST, "/test/echo", test_controller, echo_post); + + std::this_thread::sleep_for( + std::chrono::milliseconds(kRouteRegistrationDelayMs)); + } + void SetUp() override { if (!IsHttpsServerReady()) { GTEST_SKIP() << "HTTPS server is not running on " @@ -186,14 +200,6 @@ class HttpsRoutingTest : public ::testing::Test { }; TEST_F(HttpsRoutingTest, AddRouteAndGet) { - auto test_controller = std::make_shared(); - R(GET, "/test", test_controller, show); - R(GET, "/test/json", test_controller, json_response); - R(POST, "/test/echo", test_controller, echo_post); - - std::this_thread::sleep_for( - std::chrono::milliseconds(kRouteRegistrationDelayMs)); - auto client = MakeClient(); try { @@ -282,8 +288,7 @@ TEST_F(HttpsRoutingTest, ResponseTime) { }); auto end = std::chrono::steady_clock::now(); - auto duration = std::chrono::duration_cast( - end - start); + auto duration = std::chrono::duration_cast(end - start); EXPECT_EQ(response.result_int(), 200); EXPECT_LT(duration.count(), kMaxResponseTimeMs); @@ -294,10 +299,12 @@ TEST_F(HttpsRoutingTest, ResponseTime) { } TEST_F(HttpsRoutingTest, MultipleRequests) { + constexpr int kParallel = 10; + net::io_context io; std::vector> futures; - for (int i = 0; i < 10; ++i) { + for (int i = 0; i < kParallel; ++i) { auto future = net::co_spawn( io, [&]() -> net::awaitable { From 99e2738c2722c41d71cb999cbd2d7eb92dac341e Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Sun, 20 Sep 2026 22:39:46 +0300 Subject: [PATCH 11/29] feat: limits to headers, body and connections count --- .github/workflows/cmake-multi-platform.yml | 51 +-- brazier/config_test.json | 19 +- brazier/include/brazier/HttpsServer.hpp | 60 +++- brazier/src/HttpsServer.cpp | 309 ++++++++++++++++-- brazier/tests/main.h | 2 + .../tests/unit/routing/https_routing_test.cpp | 141 ++++++++ .../tests/unit/routing/tls_test_client.hpp | 118 +++++++ 7 files changed, 626 insertions(+), 74 deletions(-) create mode 100644 brazier/tests/unit/routing/tls_test_client.hpp diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index ed03014..634b349 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -75,22 +75,23 @@ jobs: cat > config_test.json <<'EOF' { "app_name": "brazierApp", - "server": { - "host": "127.0.0.1", - "port": 3502 - }, "https_server": { - "host": "127.0.0.1", + "host": "0.0.0.0", "port": 8443, "tls": { - "cert_file": "brazier/app/certs/server.crt", - "key_file": "brazier/app/certs/server.key", + "cert_file": "app/certs/server.crt", + "key_file": "app/certs/server.key", "ca_file": "", "require_client_cert": false, "verify_client_cert": false, - "handshake_timeout": 15, + "handshake_timeout": 3, "conf": [] } + }, + "http": { + "max_connections_testing": 20, + "max_body_size_testing": 1048576, + "max_header_size_testing": 8192 } } EOF @@ -170,22 +171,23 @@ jobs: cat > config_test.json <<'EOF' { "app_name": "brazierApp", - "server": { - "host": "127.0.0.1", - "port": 3502 - }, "https_server": { - "host": "127.0.0.1", + "host": "0.0.0.0", "port": 8443, "tls": { - "cert_file": "brazier/app/certs/server.crt", - "key_file": "brazier/app/certs/server.key", + "cert_file": "app/certs/server.crt", + "key_file": "app/certs/server.key", "ca_file": "", "require_client_cert": false, "verify_client_cert": false, - "handshake_timeout": 15, + "handshake_timeout": 3, "conf": [] } + }, + "http": { + "max_connections_testing": 20, + "max_body_size_testing": 1048576, + "max_header_size_testing": 8192 } } EOF @@ -257,22 +259,23 @@ jobs: cat > config_test.json <<'EOF' { "app_name": "brazierApp", - "server": { - "host": "127.0.0.1", - "port": 3502 - }, "https_server": { - "host": "127.0.0.1", + "host": "0.0.0.0", "port": 8443, "tls": { - "cert_file": "brazier/app/certs/server.crt", - "key_file": "brazier/app/certs/server.key", + "cert_file": "app/certs/server.crt", + "key_file": "app/certs/server.key", "ca_file": "", "require_client_cert": false, "verify_client_cert": false, - "handshake_timeout": 15, + "handshake_timeout": 3, "conf": [] } + }, + "http": { + "max_connections_testing": 20, + "max_body_size_testing": 1048576, + "max_header_size_testing": 8192 } } EOF diff --git a/brazier/config_test.json b/brazier/config_test.json index e35b0a1..993eb50 100644 --- a/brazier/config_test.json +++ b/brazier/config_test.json @@ -1,8 +1,5 @@ { - "server": { - "host": "0.0.0.0", - "port": 3502 - }, + "app_name": "brazierApp", "https_server": { "host": "0.0.0.0", "port": 8443, @@ -13,16 +10,12 @@ "require_client_cert": false, "verify_client_cert": false, "handshake_timeout": 3, - "conf": [ - ] + "conf": [] } }, - "app_name": "brazierApp", - "filesystem": { - "drivers": { - "local": { "root": "./storage" }, - "root": { "root": "./" }, - "default": "local" - } + "http": { + "max_connections_testing": 20, + "max_body_size_testing": 1048576, + "max_header_size_testing": 8192 } } \ No newline at end of file diff --git a/brazier/include/brazier/HttpsServer.hpp b/brazier/include/brazier/HttpsServer.hpp index 3d73c87..99a0d89 100644 --- a/brazier/include/brazier/HttpsServer.hpp +++ b/brazier/include/brazier/HttpsServer.hpp @@ -32,7 +32,7 @@ #include #include #include -#include +#include #include #include @@ -41,13 +41,14 @@ #include #include #include +#include +#include #include +#include #include #include -#include +#include #include -#include -#include #include "vendor/Handlers/ENV.hpp" #include "Database/Queue.hpp" @@ -71,9 +72,9 @@ namespace brazier { class HttpsServer { public: struct TlsConfig { - std::string cert_file; - std::string key_file; - std::string ca_file; + std::string cert_file; + std::string key_file; + std::string ca_file; std::vector> conf; @@ -84,32 +85,53 @@ namespace brazier { }; private: + std::chrono::seconds keep_alive_timeout_{ 60 }; + std::chrono::milliseconds handshake_timeout_ms_{ 15000 }; + + std::size_t max_body_size_ = 1024 * 1024; + std::size_t max_header_size_ = 8 * 1024; + int max_connections_ = 10000; + net::io_context io_; ssl::context ssl_ctx_{ ssl::context::tls_server }; tcp::acceptor acceptor_; - std::thread stats_thread_; - std::atomic shutdown_flag_{ false }; + std::thread stats_thread_; + std::atomic shutdown_flag_{ false }; std::mutex stats_mutex_; std::condition_variable stats_cv_; + std::atomic shutting_down_{ false }; + std::mutex shutdown_mutex_; + std::condition_variable shutdown_cv_; + unsigned short port_; std::string host_; std::vector threads_; - std::unique_ptr> work_guard_; + std::unique_ptr< + net::executor_work_guard> work_guard_; std::atomic connection_count_{ 0 }; std::atomic total_requests_{ 0 }; - TlsConfig tls_; bool tls_config_from_user_ = false; + struct ConnectionGuard { + HttpsServer& srv; + explicit ConnectionGuard(HttpsServer& s) noexcept : srv(s) {} + ~ConnectionGuard() { srv.release_connection(); } + + ConnectionGuard(const ConnectionGuard&) = delete; + ConnectionGuard& operator=(const ConnectionGuard&) = delete; + }; + public: HttpsServer(const std::string& host, unsigned short port); - HttpsServer(const std::string& host, unsigned short port, const TlsConfig& tls); + HttpsServer(const std::string& host, unsigned short port, + const TlsConfig& tls); void setTlsConfig(const TlsConfig& tls); @@ -121,13 +143,27 @@ namespace brazier { unsigned short getPort() const; const std::string& getHost() const; + int getMaxConnections() const { return max_connections_; } + std::size_t getMaxBodySize() const { return max_body_size_; } + std::size_t getMaxHeaderSize() const { return max_header_size_; } + private: void initializeConnections(); void load_tls_config_from_global(); + void load_limits_from_config(); + void configure_tls(); void apply_ssl_conf(); + void release_connection(); + + static std::size_t get_fd_limit(); + static std::size_t get_system_memory_mb(); + + static std::size_t compute_max_body_size(std::size_t ram_mb, int max_conn); + static std::size_t compute_max_header_size(std::size_t ram_mb, int max_conn); + net::awaitable handle_connection(tcp::socket socket); net::awaitable accept_loop(); }; diff --git a/brazier/src/HttpsServer.cpp b/brazier/src/HttpsServer.cpp index 5ac312d..263d9d4 100644 --- a/brazier/src/HttpsServer.cpp +++ b/brazier/src/HttpsServer.cpp @@ -20,6 +20,20 @@ #include "../include/brazier/HttpsServer.hpp" +#include + +#ifdef _WIN32 +# include +#else +# include +# ifdef __APPLE__ +# include +# include +# else +# include +# endif +#endif + brazier::HttpsServer::HttpsServer(const std::string& host, unsigned short port) : acceptor_(io_) , port_(port), host_(host) { @@ -40,9 +54,96 @@ void brazier::HttpsServer::setTlsConfig(const TlsConfig& tls) { tls_config_from_user_ = true; } +std::size_t brazier::HttpsServer::get_fd_limit() { +#ifdef _WIN32 + return 16384; +#else + struct rlimit rl; + if (::getrlimit(RLIMIT_NOFILE, &rl) != 0) { + return 1024; + } + if (rl.rlim_cur == RLIM_INFINITY) { + if (rl.rlim_max == RLIM_INFINITY) { + return 65536; + } + return static_cast(rl.rlim_max); + } + return static_cast(rl.rlim_cur); +#endif +} + +std::size_t brazier::HttpsServer::get_system_memory_mb() { +#ifdef _WIN32 + MEMORYSTATUSEX ms{}; + ms.dwLength = sizeof(ms); + if (::GlobalMemoryStatusEx(&ms)) { + return static_cast(ms.ullTotalPhys / (1024 * 1024)); + } + return 1024; +#elif defined(__APPLE__) + int mib[2] = { CTL_HW, HW_MEMSIZE }; + uint64_t memsize = 0; + size_t len = sizeof(memsize); + if (::sysctl(mib, 2, &memsize, &len, nullptr, 0) == 0) { + return static_cast(memsize / (1024 * 1024)); + } + return 1024; +#else + struct sysinfo si; + if (::sysinfo(&si) == 0) { + return static_cast( + (static_cast(si.totalram) * si.mem_unit) / + (1024 * 1024)); + } + return 1024; +#endif +} + +std::size_t brazier::HttpsServer::compute_max_body_size( + std::size_t ram_mb, int max_conn) { + constexpr double kBodyRamBudget = 0.25; + constexpr std::size_t kMinBody = 64 * 1024; + constexpr std::size_t kMaxBodyCap = 16 * 1024 * 1024; + + if (max_conn <= 0) max_conn = 1; + + const std::size_t ram_bytes = ram_mb * 1024 * 1024; + const std::size_t budget = static_cast(ram_bytes * kBodyRamBudget); + const std::size_t per_conn = budget / static_cast(max_conn); + + std::size_t result = per_conn; + if (result < kMinBody) result = kMinBody; + if (result > kMaxBodyCap) result = kMaxBodyCap; + return result; +} + +std::size_t brazier::HttpsServer::compute_max_header_size( + std::size_t ram_mb, int max_conn) { + constexpr double kHeaderRamBudget = 0.01; + constexpr std::size_t kMinHeader = 4 * 1024; + constexpr std::size_t kMaxHeaderCap = 32 * 1024; + + if (max_conn <= 0) max_conn = 1; + + const std::size_t ram_bytes = ram_mb * 1024 * 1024; + const std::size_t budget = static_cast(ram_bytes * kHeaderRamBudget); + const std::size_t per_conn = budget / static_cast(max_conn); + + std::size_t result = per_conn; + if (result < kMinHeader) result = kMinHeader; + if (result > kMaxHeaderCap) result = kMaxHeaderCap; + return result; +} + void brazier::HttpsServer::load_tls_config_from_global() { if (tls_config_from_user_) return; + keep_alive_timeout_ = std::chrono::seconds( + global_config->get("keep-alive-timeout", 60)); + + handshake_timeout_ms_ = std::chrono::milliseconds( + global_config->get("https_server.tls.handshake_timeout_ms", 15000)); + tls_.cert_file = global_config->get("https_server.tls.cert_file", std::string("server.crt")); tls_.key_file = global_config->get("https_server.tls.key_file", @@ -79,6 +180,57 @@ void brazier::HttpsServer::load_tls_config_from_global() { } } +void brazier::HttpsServer::load_limits_from_config() { + const std::size_t ram_mb = get_system_memory_mb(); + + const int testing_conn = global_config->get("http.max_connections_testing", 0); + const int testing_body = global_config->get("http.max_body_size_testing", 0); + const int testing_hdr = global_config->get("http.max_header_size_testing", 0); + + const bool testing_mode = + testing_conn > 0 || testing_body > 0 || testing_hdr > 0; + + if (testing_conn > 0) { + max_connections_ = testing_conn; + } + else if (int v = global_config->get("http.max_connections", 0); v > 0) { + max_connections_ = v; + } + else { + const int fd_limit = static_cast(get_fd_limit()); + max_connections_ = static_cast(fd_limit * 0.8); + } + + if (testing_body > 0) { + max_body_size_ = static_cast(testing_body); + } + else if (int v = global_config->get("http.max_body_size", 0); v > 0) { + max_body_size_ = static_cast(v); + } + else { + max_body_size_ = compute_max_body_size(ram_mb, max_connections_); + } + + if (testing_hdr > 0) { + max_header_size_ = static_cast(testing_hdr); + } + else if (int v = global_config->get("http.max_header_size", 0); v > 0) { + max_header_size_ = static_cast(v); + } + else { + max_header_size_ = compute_max_header_size(ram_mb, max_connections_); + } + + Logger::log( + std::string(testing_mode ? "[TESTING] " : "") + + "Final HTTP limits: max_connections=" + + std::to_string(max_connections_) + + ", max_body=" + std::to_string(max_body_size_ / 1024) + "KB" + + ", max_header=" + std::to_string(max_header_size_ / 1024) + "KB" + + " (RAM=" + std::to_string(ram_mb) + "MB)", + testing_mode ? "WARNING" : "INFO"); +} + void brazier::HttpsServer::apply_ssl_conf() { if (tls_.conf.empty()) return; @@ -164,6 +316,7 @@ bool brazier::HttpsServer::initialize() { } load_tls_config_from_global(); + load_limits_from_config(); configure_tls(); tcp::endpoint endpoint(net::ip::make_address(host_), port_); @@ -228,20 +381,35 @@ void brazier::HttpsServer::run() { } shutdown_flag_.store(false, std::memory_order_release); + stats_thread_ = std::thread([this] { + std::unique_lock lock(stats_mutex_); while (!shutdown_flag_.load(std::memory_order_acquire)) { - std::this_thread::sleep_for(10s); - if (shutdown_flag_.load(std::memory_order_acquire)) break; + const bool woke = stats_cv_.wait_for( + lock, + std::chrono::seconds(10), + [this] { + return shutdown_flag_.load(std::memory_order_acquire); + }); + + if (woke) break; + + lock.unlock(); Logger::log("HTTPS STATS - Active connections: " + std::to_string(connection_count_.load()) + ", Total requests: " + std::to_string(total_requests_.load()), "INFO"); + lock.lock(); } }); for (auto& t : threads_) { if (t.joinable()) t.join(); } + + if (stats_thread_.joinable()) stats_thread_.join(); + + Logger::log("HTTPS server stopped", "INFO"); } catch (const std::exception& e) { Logger::log("HTTPS server run failed: " + std::string(e.what()), "ERROR"); @@ -250,26 +418,90 @@ void brazier::HttpsServer::run() { } void brazier::HttpsServer::stop() { - shutdown_flag_.store(true); + Logger::log("HTTPS server stopping (graceful)...", "INFO"); + + shutdown_flag_.store(true, std::memory_order_release); + shutting_down_.store(true, std::memory_order_release); + + { + std::lock_guard lock(stats_mutex_); + stats_cv_.notify_all(); + } + + { + boost::system::error_code ignore; + acceptor_.close(ignore); + } + work_guard_.reset(); + + { + std::unique_lock lock(shutdown_mutex_); + const bool drained = shutdown_cv_.wait_for( + lock, + std::chrono::seconds(10), + [this] { + return connection_count_.load(std::memory_order_acquire) == 0; + }); + + if (!drained) { + Logger::log("Graceful shutdown timeout: " + + std::to_string(connection_count_.load()) + + " connections still active, forcing stop", "WARNING"); + } + } + io_.stop(); - if (stats_thread_.joinable()) stats_thread_.join(); - Logger::log("HTTPS server stopped", "INFO"); + Logger::log("HTTPS server stop() signaled", "INFO"); } unsigned short brazier::HttpsServer::getPort() const { return port_; } const std::string& brazier::HttpsServer::getHost() const { return host_; } +void brazier::HttpsServer::release_connection() { + if (connection_count_.fetch_sub(1, std::memory_order_acq_rel) == 1) { + std::lock_guard lock(shutdown_mutex_); + shutdown_cv_.notify_all(); + } +} + net::awaitable brazier::HttpsServer::accept_loop() { for (;;) { - tcp::socket socket = co_await acceptor_.async_accept(net::use_awaitable); + if (shutting_down_.load(std::memory_order_acquire)) { + co_return; + } + + beast::error_code ec; + tcp::socket socket = co_await acceptor_.async_accept( + net::redirect_error(net::use_awaitable, ec)); + + if (ec) { + if (ec == net::error::operation_aborted || + shutting_down_.load(std::memory_order_acquire)) { + co_return; + } + Logger::log("Accept error: " + ec.message(), "ERROR"); + continue; + } + + const int prev = connection_count_.fetch_add(1, std::memory_order_acq_rel); + if (prev >= max_connections_) { + connection_count_.fetch_sub(1, std::memory_order_acq_rel); + Logger::log("Connection limit reached (" + + std::to_string(prev) + "/" + + std::to_string(max_connections_) + "), rejecting", "WARNING"); + boost::system::error_code ignore; + socket.close(ignore); + continue; + } + net::co_spawn(io_, handle_connection(std::move(socket)), net::detached); } } net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) { - connection_count_.fetch_add(1, std::memory_order_relaxed); + ConnectionGuard guard(*this); try { socket.set_option(tcp::no_delay(true)); @@ -296,19 +528,18 @@ net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) if (hs_ec) { Logger::log("TLS handshake failed: " + hs_ec.message(), "WARNING"); - connection_count_.fetch_sub(1, std::memory_order_relaxed); co_return; } } - http::request req; + std::optional> parser; + http::response res; beast::flat_buffer buffer; bool keep_alive = true; net::steady_timer idle_timer(co_await net::this_coro::executor); - const auto IDLE_TIMEOUT = std::chrono::seconds( - global_config->get("keep-alive-timeout", 60)); + const auto IDLE_TIMEOUT = keep_alive_timeout_; bool timed_out = false; auto reset_timer = [&]() { @@ -325,21 +556,52 @@ net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) reset_timer(); while (keep_alive && !timed_out) { + parser.emplace(); + parser->body_limit(max_body_size_); + parser->header_limit(static_cast(max_header_size_)); + beast::error_code ec; - req = {}; co_await http::async_read( - stream, buffer, req, + stream, buffer, *parser, net::redirect_error(net::use_awaitable, ec)); - if (ec == http::error::end_of_stream) break; - if (ec) { - if (ec == net::error::operation_aborted) break; + if (ec == http::error::body_limit) { + http::response err{ + http::status::payload_too_large, 11 }; + err.set(http::field::content_type, "text/plain"); + err.set(http::field::connection, "close"); + err.body() = "Payload too large"; + err.prepare_payload(); + + beast::error_code write_ec; + co_await http::async_write( + stream, err, + net::redirect_error(net::use_awaitable, write_ec)); + break; + } + + if (ec == http::error::header_limit) { + http::response err{ + http::status::request_header_fields_too_large, 11 }; + err.set(http::field::content_type, "text/plain"); + err.set(http::field::connection, "close"); + err.body() = "Header too large"; + err.prepare_payload(); + + beast::error_code write_ec; + co_await http::async_write( + stream, err, + net::redirect_error(net::use_awaitable, write_ec)); break; } + if (ec == http::error::end_of_stream) break; + if (ec) break; + reset_timer(); + http::request req = parser->release(); total_requests_.fetch_add(1, std::memory_order_relaxed); keep_alive = req.keep_alive(); @@ -414,14 +676,12 @@ net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) } catch (const boost::system::system_error& e) { auto code = e.code(); - if (code == net::error::connection_reset || - code == net::error::connection_aborted || - code == net::error::eof || - code == net::error::operation_aborted || - code == net::error::broken_pipe || - code == ssl::error::stream_truncated) { - } - else { + if (code != net::error::connection_reset && + code != net::error::connection_aborted && + code != net::error::eof && + code != net::error::operation_aborted && + code != net::error::broken_pipe && + code != ssl::error::stream_truncated) { Logger::log("HTTPS connection error: " + std::string(e.what()), "ERROR"); } } @@ -432,6 +692,5 @@ net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) Logger::log("Unknown HTTPS connection error", "ERROR"); } - connection_count_.fetch_sub(1, std::memory_order_relaxed); co_return; } \ No newline at end of file diff --git a/brazier/tests/main.h b/brazier/tests/main.h index 5f75cbf..0b9594a 100644 --- a/brazier/tests/main.h +++ b/brazier/tests/main.h @@ -34,6 +34,8 @@ extern std::shared_ptr g_test_server; extern std::atomic g_server_ready; extern std::thread g_server_thread; +extern std::shared_ptr g_test_https_server; + extern int port_global; extern std::string host_global; diff --git a/brazier/tests/unit/routing/https_routing_test.cpp b/brazier/tests/unit/routing/https_routing_test.cpp index 22a47da..dac9186 100644 --- a/brazier/tests/unit/routing/https_routing_test.cpp +++ b/brazier/tests/unit/routing/https_routing_test.cpp @@ -43,6 +43,7 @@ #include "../../../include/brazier/Core" #include "../../../include/brazier/Http" #include "main.h" +#include "tls_test_client.hpp" namespace beast = boost::beast; namespace http = beast::http; @@ -395,4 +396,144 @@ TEST_F(HttpsRoutingTest, ServesExpectedCertificate) { << "Certificate CN mismatch. Got: " << cn; X509_free(cert); +} + +TEST_F(HttpsRoutingTest, PayloadTooLarge) { + ASSERT_NE(g_test_https_server, nullptr); + const std::size_t limit = g_test_https_server->getMaxBodySize(); + const std::size_t body_size = limit * 4; + + tls_test::TlsClient c(https_host_global, https_port_global); + ASSERT_TRUE(c.connect()); + + std::string headers = + "POST /test/echo HTTP/1.1\r\n" + "Host: " + https_host_global + "\r\n" + "Content-Type: application/json\r\n" + "Content-Length: " + std::to_string(body_size) + "\r\n" + "Connection: close\r\n" + "\r\n"; + + ASSERT_TRUE(c.send_raw(headers)); + auto res = c.read_response(5); + + if (res.has_value()) { + EXPECT_EQ(res->result_int(), 413); + } + else { + EXPECT_TRUE(c.is_connection_closed(2)) + << "No response and connection not closed"; + } +} + +TEST_F(HttpsRoutingTest, HeaderTooLarge) { + ASSERT_NE(g_test_https_server, nullptr); + const std::size_t limit = g_test_https_server->getMaxHeaderSize(); + const std::size_t header_size = limit * 4; + + tls_test::TlsClient c(https_host_global, https_port_global); + ASSERT_TRUE(c.connect()); + + std::string huge(header_size, 'x'); + std::string req = + "GET /test HTTP/1.1\r\n" + "Host: " + https_host_global + "\r\n" + "X-Huge: " + huge + "\r\n" + "Connection: close\r\n" + "\r\n"; + + ASSERT_TRUE(c.send_raw(req)); + + auto res = c.read_response(5); + bool closed = c.is_connection_closed(3); + + bool acceptable = (res.has_value() && + res->result_int() >= 400 && + res->result_int() < 500) + || (!res.has_value() && closed); + + EXPECT_TRUE(acceptable) + << "Expected 4xx or close, got has_value=" << res.has_value() + << ", status=" << (res.has_value() ? res->result_int() : 0); +} + +TEST_F(HttpsRoutingTest, ConnectionLimit) { + ASSERT_NE(g_test_https_server, nullptr); + const int limit = g_test_https_server->getMaxConnections(); + + if (limit <= 0 || limit > 100) { + GTEST_SKIP() << "max_connections=" << limit + << " is not suitable. Set http.max_connections_testing to 10..100."; + } + + std::vector> held; + held.reserve(limit); + + for (int i = 0; i < limit; ++i) { + auto c = std::make_unique( + https_host_global, https_port_global); + ASSERT_TRUE(c->connect()) + << "Handshake #" << i << " failed within limit " << limit; + held.push_back(std::move(c)); + } + + tls_test::TlsClient extra(https_host_global, https_port_global); + const bool connected = extra.connect(); + + if (connected) { + EXPECT_TRUE(extra.is_connection_closed(3)) + << "Server accepted connection beyond limit " << limit + << " and did not close it"; + } +} + +TEST_F(HttpsRoutingTest, ConnectionsReleasedAfterClose) { + ASSERT_NE(g_test_https_server, nullptr); + const int limit = g_test_https_server->getMaxConnections(); + + if (limit <= 0 || limit > 100) { + GTEST_SKIP() << "max_connections=" << limit + << " is not suitable. Set http.max_connections_testing to 10..100."; + } + + const int iterations = limit + 5; + + for (int i = 0; i < iterations; ++i) { + tls_test::TlsClient c(https_host_global, https_port_global); + ASSERT_TRUE(c.connect()) + << "Connection #" << i << " rejected — " + "ConnectionGuard likely not releasing the counter"; + + ASSERT_TRUE(c.send_raw( + "GET /test HTTP/1.1\r\n" + "Host: " + https_host_global + "\r\n" + "Connection: close\r\n" + "\r\n")); + + auto res = c.read_response(); + ASSERT_TRUE(res.has_value()) << "No response at iteration " << i; + EXPECT_EQ(res->result_int(), 200); + } +} + +TEST_F(HttpsRoutingTest, ConnectionsReleasedAfterProtocolError) { + ASSERT_NE(g_test_https_server, nullptr); + const int limit = g_test_https_server->getMaxConnections(); + + if (limit <= 0 || limit > 100) { + GTEST_SKIP() << "max_connections=" << limit + << " is not suitable. Set http.max_connections_testing to 10..100."; + } + + const int iterations = limit + 5; + + for (int i = 0; i < iterations; ++i) { + tls_test::TlsClient c(https_host_global, https_port_global); + ASSERT_TRUE(c.connect()) + << "Connection #" << i << " rejected after protocol errors — " + "ConnectionGuard likely not releasing on exceptions"; + + c.send_raw("this is not an HTTP request\r\n\r\n"); + c.is_connection_closed(2); + } } \ No newline at end of file diff --git a/brazier/tests/unit/routing/tls_test_client.hpp b/brazier/tests/unit/routing/tls_test_client.hpp new file mode 100644 index 0000000..69c6cf0 --- /dev/null +++ b/brazier/tests/unit/routing/tls_test_client.hpp @@ -0,0 +1,118 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace beast = boost::beast; +namespace http = beast::http; +namespace net = boost::asio; +namespace ssl = net::ssl; +using tcp = net::ip::tcp; + +namespace tls_test { + + inline constexpr int kSocketTimeoutSec = 5; + + struct TlsClient { + net::io_context io; + ssl::context ctx; + std::unique_ptr> stream; + + TlsClient(const std::string& host, unsigned short port, + bool with_client_cert = false, + const std::string& cert = "", + const std::string& key = "") + : host_(host), port_(port) + , ctx(ssl::context::tls_client) { + ctx.set_verify_mode(ssl::verify_none); + if (with_client_cert) { + ctx.use_certificate_chain_file(cert); + ctx.use_private_key_file(key, ssl::context::pem); + } + } + + bool connect(int timeout_sec = kSocketTimeoutSec) { + try { + stream = std::make_unique>(io, ctx); + tcp::resolver resolver(io); + auto results = resolver.resolve(host_, std::to_string(port_)); + + beast::get_lowest_layer(*stream).expires_after( + std::chrono::seconds(timeout_sec)); + beast::get_lowest_layer(*stream).connect(results); + beast::get_lowest_layer(*stream).expires_never(); + stream->handshake(ssl::stream_base::client); + return true; + } + catch (const std::exception&) { + return false; + } + } + + bool send_raw(const std::string& data) { + try { + beast::get_lowest_layer(*stream).expires_after( + std::chrono::seconds(kSocketTimeoutSec)); + net::write(*stream, net::buffer(data)); + return true; + } + catch (const std::exception&) { + return false; + } + } + + std::optional> + read_response(int timeout_sec = kSocketTimeoutSec) { + try { + beast::flat_buffer buffer; + http::response res; + beast::get_lowest_layer(*stream).expires_after( + std::chrono::seconds(timeout_sec)); + http::read(*stream, buffer, res); + return res; + } + catch (const std::exception&) { + return std::nullopt; + } + } + + bool is_connection_closed(int timeout_sec = kSocketTimeoutSec) { + if (!stream) return true; + try { + beast::get_lowest_layer(*stream).expires_after( + std::chrono::seconds(timeout_sec)); + char c; + beast::error_code ec; + auto n = stream->read_some(net::buffer(&c, 1), ec); + + if (ec == net::error::eof || + ec == net::error::connection_reset || + ec == ssl::error::stream_truncated || + ec == beast::error::timeout) { + return true; + } + if (ec == net::error::timed_out) return false; + if (!ec && n > 0) return false; + return true; + } + catch (const std::exception&) { + return true; + } + } + + private: + std::string host_; + unsigned short port_; + }; + +} \ No newline at end of file From be3d837d321a0d63cd796d064e2b58460bfa700f Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Sun, 20 Sep 2026 23:28:26 +0300 Subject: [PATCH 12/29] feat: multi-context engine --- brazier/include/brazier/Engine.hpp | 26 +---- brazier/include/brazier/HttpsServer.hpp | 29 +++--- brazier/src/Engine.cpp | 16 ++- brazier/src/HttpsServer.cpp | 133 ++++++++++++++++-------- 4 files changed, 119 insertions(+), 85 deletions(-) diff --git a/brazier/include/brazier/Engine.hpp b/brazier/include/brazier/Engine.hpp index f126a2d..9fc7176 100644 --- a/brazier/include/brazier/Engine.hpp +++ b/brazier/include/brazier/Engine.hpp @@ -1,23 +1,3 @@ -/* - * 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 @@ -28,12 +8,10 @@ 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; - } + static void init(boost::asio::io_context& io_ctx); private: - static inline boost::asio::io_context* io_ctx_ptr_ = nullptr; + static boost::asio::io_context*& io_ctx_ptr(); }; inline std::shared_ptr global_config = nullptr; diff --git a/brazier/include/brazier/HttpsServer.hpp b/brazier/include/brazier/HttpsServer.hpp index 99a0d89..468db79 100644 --- a/brazier/include/brazier/HttpsServer.hpp +++ b/brazier/include/brazier/HttpsServer.hpp @@ -39,10 +39,10 @@ #include #include -#include #include #include #include +#include #include #include #include @@ -89,12 +89,15 @@ namespace brazier { std::chrono::milliseconds handshake_timeout_ms_{ 15000 }; std::size_t max_body_size_ = 1024 * 1024; - std::size_t max_header_size_ = 8 * 1024; + std::uint32_t max_header_size_ = 8 * 1024; int max_connections_ = 10000; - net::io_context io_; - ssl::context ssl_ctx_{ ssl::context::tls_server }; - tcp::acceptor acceptor_; + std::vector> io_contexts_; + std::vector> acceptors_; + std::vector>> work_guards_; + + ssl::context ssl_ctx_{ ssl::context::tls_server }; std::thread stats_thread_; std::atomic shutdown_flag_{ false }; @@ -110,8 +113,6 @@ namespace brazier { std::string host_; std::vector threads_; - std::unique_ptr< - net::executor_work_guard> work_guard_; std::atomic connection_count_{ 0 }; std::atomic total_requests_{ 0 }; @@ -143,9 +144,10 @@ namespace brazier { unsigned short getPort() const; const std::string& getHost() const; - int getMaxConnections() const { return max_connections_; } - std::size_t getMaxBodySize() const { return max_body_size_; } - std::size_t getMaxHeaderSize() const { return max_header_size_; } + int getMaxConnections() const { return max_connections_; } + std::size_t getMaxBodySize() const { return max_body_size_; } + std::uint32_t getMaxHeaderSize() const { return max_header_size_; } + std::size_t getIoContextCount() const { return io_contexts_.size(); } private: void initializeConnections(); @@ -160,12 +162,13 @@ namespace brazier { static std::size_t get_fd_limit(); static std::size_t get_system_memory_mb(); + static int get_worker_count(); - static std::size_t compute_max_body_size(std::size_t ram_mb, int max_conn); - static std::size_t compute_max_header_size(std::size_t ram_mb, int max_conn); + static std::size_t compute_max_body_size(std::size_t ram_mb, int max_conn); + static std::uint32_t compute_max_header_size(std::size_t ram_mb, int max_conn); net::awaitable handle_connection(tcp::socket socket); - net::awaitable accept_loop(); + net::awaitable accept_loop(tcp::acceptor& acceptor); }; } \ No newline at end of file diff --git a/brazier/src/Engine.cpp b/brazier/src/Engine.cpp index 4f72e23..6df223f 100644 --- a/brazier/src/Engine.cpp +++ b/brazier/src/Engine.cpp @@ -20,9 +20,19 @@ #include "../include/brazier/Engine.hpp" +boost::asio::io_context*& brazier::Engine::io_ctx_ptr() { + static thread_local boost::asio::io_context* ptr = nullptr; + return ptr; +} + +void brazier::Engine::init(boost::asio::io_context& io_ctx) { + io_ctx_ptr() = &io_ctx; +} + boost::asio::io_context& brazier::Engine::get_io_context() { - if (!brazier::Engine::io_ctx_ptr_) { - throw std::runtime_error("IO context not initialized"); + auto* ptr = io_ctx_ptr(); + if (!ptr) { + throw std::runtime_error("IO context not initialized for this thread"); } - return *brazier::Engine::io_ctx_ptr_; + return *ptr; } \ No newline at end of file diff --git a/brazier/src/HttpsServer.cpp b/brazier/src/HttpsServer.cpp index 263d9d4..4d45979 100644 --- a/brazier/src/HttpsServer.cpp +++ b/brazier/src/HttpsServer.cpp @@ -26,6 +26,7 @@ # include #else # include +# include # ifdef __APPLE__ # include # include @@ -35,19 +36,11 @@ #endif brazier::HttpsServer::HttpsServer(const std::string& host, unsigned short port) - : acceptor_(io_) - , port_(port), host_(host) { - work_guard_ = std::make_unique< - net::executor_work_guard>(io_.get_executor()); -} + : port_(port), host_(host) {} brazier::HttpsServer::HttpsServer(const std::string& host, unsigned short port, const TlsConfig& tls) - : acceptor_(io_) - , port_(port), host_(host), tls_(tls), tls_config_from_user_(true) { - work_guard_ = std::make_unique< - net::executor_work_guard>(io_.get_executor()); -} + : port_(port), host_(host), tls_(tls), tls_config_from_user_(true) {} void brazier::HttpsServer::setTlsConfig(const TlsConfig& tls) { tls_ = tls; @@ -99,11 +92,20 @@ std::size_t brazier::HttpsServer::get_system_memory_mb() { #endif } +int brazier::HttpsServer::get_worker_count() { +#if defined(SO_REUSEPORT) + int n = static_cast(std::thread::hardware_concurrency()); + return (n > 0) ? n : 1; +#else + return 1; +#endif +} + std::size_t brazier::HttpsServer::compute_max_body_size( std::size_t ram_mb, int max_conn) { constexpr double kBodyRamBudget = 0.25; - constexpr std::size_t kMinBody = 64 * 1024; - constexpr std::size_t kMaxBodyCap = 16 * 1024 * 1024; + constexpr std::size_t kMinBody = 64 * 1024; + constexpr std::size_t kMaxBodyCap = 16 * 1024 * 1024; if (max_conn <= 0) max_conn = 1; @@ -117,11 +119,11 @@ std::size_t brazier::HttpsServer::compute_max_body_size( return result; } -std::size_t brazier::HttpsServer::compute_max_header_size( +std::uint32_t brazier::HttpsServer::compute_max_header_size( std::size_t ram_mb, int max_conn) { - constexpr double kHeaderRamBudget = 0.01; - constexpr std::size_t kMinHeader = 4 * 1024; - constexpr std::size_t kMaxHeaderCap = 32 * 1024; + constexpr double kHeaderRamBudget = 0.01; + constexpr std::uint32_t kMinHeader = 4 * 1024; + constexpr std::uint32_t kMaxHeaderCap = 32 * 1024; if (max_conn <= 0) max_conn = 1; @@ -132,7 +134,7 @@ std::size_t brazier::HttpsServer::compute_max_header_size( std::size_t result = per_conn; if (result < kMinHeader) result = kMinHeader; if (result > kMaxHeaderCap) result = kMaxHeaderCap; - return result; + return static_cast(result); } void brazier::HttpsServer::load_tls_config_from_global() { @@ -212,10 +214,10 @@ void brazier::HttpsServer::load_limits_from_config() { } if (testing_hdr > 0) { - max_header_size_ = static_cast(testing_hdr); + max_header_size_ = static_cast(testing_hdr); } else if (int v = global_config->get("http.max_header_size", 0); v > 0) { - max_header_size_ = static_cast(v); + max_header_size_ = static_cast(v); } else { max_header_size_ = compute_max_header_size(ram_mb, max_connections_); @@ -319,18 +321,52 @@ bool brazier::HttpsServer::initialize() { load_limits_from_config(); configure_tls(); - tcp::endpoint endpoint(net::ip::make_address(host_), port_); - acceptor_.open(endpoint.protocol()); - acceptor_.set_option(tcp::acceptor::reuse_address(true)); - acceptor_.bind(endpoint); - acceptor_.listen(); + const tcp::endpoint endpoint(net::ip::make_address(host_), port_); + const int worker_count = get_worker_count(); - initializeConnections(); - RouterRegisterer::init(io_); - Engine::init(io_); + io_contexts_.reserve(worker_count); + acceptors_.reserve(worker_count); + work_guards_.reserve(worker_count); + + for (int i = 0; i < worker_count; ++i) { + auto io = std::make_unique(); + auto acc = std::make_unique(*io); + + acc->open(endpoint.protocol()); + acc->set_option(tcp::acceptor::reuse_address(true)); + +#if defined(SO_REUSEPORT) + int one = 1; + if (::setsockopt(acc->native_handle(), SOL_SOCKET, SO_REUSEPORT, + reinterpret_cast(&one), + sizeof(one)) != 0) { + Logger::log("SO_REUSEPORT setsockopt failed on worker " + + std::to_string(i), "WARNING"); + } +#endif + + acc->bind(endpoint); + acc->listen(boost::asio::socket_base::max_listen_connections); + + work_guards_.push_back(std::make_unique< + net::executor_work_guard>( + io->get_executor())); + io_contexts_.push_back(std::move(io)); + acceptors_.push_back(std::move(acc)); + } + + initializeConnections(); + RouterRegisterer::init(*io_contexts_[0]); Logger::log("HTTPS server initialized on " + host_ + ":" + - std::to_string(port_) + " [TLS]", "SUCCESS"); + std::to_string(port_) + " [TLS, workers=" + + std::to_string(worker_count) + ", SO_REUSEPORT=" + +#if defined(SO_REUSEPORT) + "yes" +#else + "no" +#endif + + "]", "SUCCESS"); return true; } catch (const std::exception& e) { @@ -368,16 +404,21 @@ void brazier::HttpsServer::initializeConnections() { void brazier::HttpsServer::run() { try { - net::co_spawn(io_, accept_loop(), net::detached); - - int threads_count = std::thread::hardware_concurrency(); - if (threads_count == 0) threads_count = 1; + for (size_t i = 0; i < io_contexts_.size(); ++i) { + net::co_spawn(*io_contexts_[i], + accept_loop(*acceptors_[i]), + net::detached); + } - Logger::log("Starting " + std::to_string(threads_count) + - " HTTPS worker threads", "INFO"); + Logger::log("Starting " + std::to_string(io_contexts_.size()) + + " HTTPS worker thread(s)", "INFO"); - for (int i = 0; i < threads_count; ++i) { - threads_.emplace_back([this] { io_.run(); }); + for (auto& io : io_contexts_) { + auto* io_ptr = io.get(); + threads_.emplace_back([io_ptr] { + brazier::Engine::init(*io_ptr); + io_ptr->run(); + }); } shutdown_flag_.store(false, std::memory_order_release); @@ -392,7 +433,7 @@ void brazier::HttpsServer::run() { return shutdown_flag_.load(std::memory_order_acquire); }); - if (woke) break; + if (woke) break; lock.unlock(); Logger::log("HTTPS STATS - Active connections: " + @@ -428,12 +469,12 @@ void brazier::HttpsServer::stop() { stats_cv_.notify_all(); } - { + for (auto& acc : acceptors_) { boost::system::error_code ignore; - acceptor_.close(ignore); + acc->close(ignore); } - work_guard_.reset(); + for (auto& wg : work_guards_) wg->reset(); { std::unique_lock lock(shutdown_mutex_); @@ -451,7 +492,7 @@ void brazier::HttpsServer::stop() { } } - io_.stop(); + for (auto& io : io_contexts_) io->stop(); Logger::log("HTTPS server stop() signaled", "INFO"); } @@ -466,14 +507,14 @@ void brazier::HttpsServer::release_connection() { } } -net::awaitable brazier::HttpsServer::accept_loop() { +net::awaitable brazier::HttpsServer::accept_loop(tcp::acceptor& acceptor) { for (;;) { if (shutting_down_.load(std::memory_order_acquire)) { co_return; } beast::error_code ec; - tcp::socket socket = co_await acceptor_.async_accept( + tcp::socket socket = co_await acceptor.async_accept( net::redirect_error(net::use_awaitable, ec)); if (ec) { @@ -496,7 +537,9 @@ net::awaitable brazier::HttpsServer::accept_loop() { continue; } - net::co_spawn(io_, handle_connection(std::move(socket)), net::detached); + net::co_spawn(acceptor.get_executor(), + handle_connection(std::move(socket)), + net::detached); } } @@ -558,7 +601,7 @@ net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) while (keep_alive && !timed_out) { parser.emplace(); parser->body_limit(max_body_size_); - parser->header_limit(static_cast(max_header_size_)); + parser->header_limit(max_header_size_); beast::error_code ec; From cea9b5816dabbdda2d5b3a463f5486cc270ddf68 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Sun, 20 Sep 2026 23:38:04 +0300 Subject: [PATCH 13/29] fix: gtihub ci config --- .github/workflows/cmake-multi-platform.yml | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 634b349..068421e 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -79,8 +79,8 @@ jobs: "host": "0.0.0.0", "port": 8443, "tls": { - "cert_file": "app/certs/server.crt", - "key_file": "app/certs/server.key", + "cert_file": "brazier/app/certs/server.crt", + "key_file": "brazier/app/certs/server.key", "ca_file": "", "require_client_cert": false, "verify_client_cert": false, @@ -103,8 +103,8 @@ jobs: run: | mkdir -p build/brazier/app/certs openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \ - -keyout build/brazier/app/certs/server.key \ - -out build/brazier/app/certs/server.crt \ + -keyout build/brazier/brazier/app/certs/server.key \ + -out build/brazier/brazier/app/certs/server.crt \ -subj "/CN=localhost" \ -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" shell: bash @@ -175,8 +175,8 @@ jobs: "host": "0.0.0.0", "port": 8443, "tls": { - "cert_file": "app/certs/server.crt", - "key_file": "app/certs/server.key", + "cert_file": "brazier/app/certs/server.crt", + "key_file": "brazier/app/certs/server.key", "ca_file": "", "require_client_cert": false, "verify_client_cert": false, @@ -200,8 +200,8 @@ jobs: run: | New-Item -ItemType Directory -Force -Path "build/brazier/app/certs" | Out-Null openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes ` - -keyout "build/brazier/app/certs/server.key" ` - -out "build/brazier/app/certs/server.crt" ` + -keyout "build/brazier/brazier/app/certs/server.key" ` + -out "build/brazier/brazier/app/certs/server.crt" ` -subj "/CN=localhost" ` -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" @@ -263,8 +263,8 @@ jobs: "host": "0.0.0.0", "port": 8443, "tls": { - "cert_file": "app/certs/server.crt", - "key_file": "app/certs/server.key", + "cert_file": "brazier/app/certs/server.crt", + "key_file": "brazier/app/certs/server.key", "ca_file": "", "require_client_cert": false, "verify_client_cert": false, @@ -287,8 +287,8 @@ jobs: run: | mkdir -p build/brazier/app/certs openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \ - -keyout build/brazier/app/certs/server.key \ - -out build/brazier/app/certs/server.crt \ + -keyout build/brazier/brazier/app/certs/server.key \ + -out build/brazier/brazier/app/certs/server.crt \ -subj "/CN=localhost" \ -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" shell: bash From 4ed2efdcce6c93ff3694852658edcf47465a6878 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Sun, 20 Sep 2026 23:49:02 +0300 Subject: [PATCH 14/29] fix: gtihub ci config 2 --- .github/workflows/cmake-multi-platform.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 068421e..65cfe13 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -103,8 +103,8 @@ jobs: run: | mkdir -p build/brazier/app/certs openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \ - -keyout build/brazier/brazier/app/certs/server.key \ - -out build/brazier/brazier/app/certs/server.crt \ + -keyout build/brazier/app/certs/server.key \ + -out build/brazier/app/certs/server.crt \ -subj "/CN=localhost" \ -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" shell: bash @@ -200,8 +200,8 @@ jobs: run: | New-Item -ItemType Directory -Force -Path "build/brazier/app/certs" | Out-Null openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes ` - -keyout "build/brazier/brazier/app/certs/server.key" ` - -out "build/brazier/brazier/app/certs/server.crt" ` + -keyout "build/brazier/app/certs/server.key" ` + -out "build/brazier/app/certs/server.crt" ` -subj "/CN=localhost" ` -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" @@ -287,8 +287,8 @@ jobs: run: | mkdir -p build/brazier/app/certs openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \ - -keyout build/brazier/brazier/app/certs/server.key \ - -out build/brazier/brazier/app/certs/server.crt \ + -keyout build/brazier/app/certs/server.key \ + -out build/brazier/app/certs/server.crt \ -subj "/CN=localhost" \ -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" shell: bash From 4b20e6c30c8f5c0db76ca7d26a4a46cef48e4515 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Mon, 21 Sep 2026 12:31:56 +0300 Subject: [PATCH 15/29] feat: sessions resumption --- brazier/include/brazier/HttpsServer.hpp | 7 + .../include/brazier/TLS/TicketKeyStore.hpp | 70 ++++ brazier/src/HttpsServer.cpp | 13 + brazier/src/TLS/TicketKeyStore.cpp | 223 +++++++++++ .../unit/security/https_security_test.cpp | 140 +++++++ .../tests/unit/tls/ticket_key_store_test.cpp | 358 ++++++++++++++++++ 6 files changed, 811 insertions(+) create mode 100644 brazier/include/brazier/TLS/TicketKeyStore.hpp create mode 100644 brazier/src/TLS/TicketKeyStore.cpp create mode 100644 brazier/tests/unit/tls/ticket_key_store_test.cpp diff --git a/brazier/include/brazier/HttpsServer.hpp b/brazier/include/brazier/HttpsServer.hpp index 468db79..36bd0a1 100644 --- a/brazier/include/brazier/HttpsServer.hpp +++ b/brazier/include/brazier/HttpsServer.hpp @@ -49,6 +49,9 @@ #include #include #include +#include +#include +#include #include "vendor/Handlers/ENV.hpp" #include "Database/Queue.hpp" @@ -60,6 +63,8 @@ #include "Filesystem/Filesystem.hpp" #include "vendor/ConfigManager.hpp" +#include "../include/brazier/TLS/TicketKeyStore.hpp" + namespace beast = boost::beast; namespace http = beast::http; namespace net = boost::asio; @@ -129,6 +134,8 @@ namespace brazier { ConnectionGuard& operator=(const ConnectionGuard&) = delete; }; + brazier::TicketKeyStore ticket_store_; + public: HttpsServer(const std::string& host, unsigned short port); HttpsServer(const std::string& host, unsigned short port, diff --git a/brazier/include/brazier/TLS/TicketKeyStore.hpp b/brazier/include/brazier/TLS/TicketKeyStore.hpp new file mode 100644 index 0000000..7c655f7 --- /dev/null +++ b/brazier/include/brazier/TLS/TicketKeyStore.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include + +#if OPENSSL_VERSION_NUMBER < 0x30000000L +# include +#endif + +#include +#include +#include +#include +#include +#include + +namespace brazier { + + class TicketKeyStore { + public: + using Clock = std::chrono::steady_clock; + using TimePoint = Clock::time_point; + + static constexpr auto kRotationInterval = std::chrono::hours(12); + static constexpr auto kKeyLifetime = std::chrono::hours(48); + + static constexpr std::size_t kNameSize = 16; + static constexpr std::size_t kAesKeySize = 32; + static constexpr std::size_t kHmacKeySize = 32; + + struct Key { + unsigned char name[kNameSize]; + unsigned char aes_key[kAesKeySize]; + unsigned char hmac_key[kHmacKeySize]; + TimePoint created_at; + }; + + TicketKeyStore(); + explicit TicketKeyStore(std::function now_provider); + + TicketKeyStore(const TicketKeyStore&) = delete; + TicketKeyStore& operator=(const TicketKeyStore&) = delete; + + int handle(unsigned char key_name[kNameSize], + unsigned char iv[EVP_MAX_IV_LENGTH], + EVP_CIPHER_CTX* cipher_ctx, + void* mac_ctx, + int enc); + + void ensure_initialized(); + void rotate_now(); + + void attach_to(SSL_CTX* ctx); + + std::size_t key_count() const; + std::vector snapshot() const; + + private: + static Key generate_key(TimePoint now); + void maybe_rotate_locked(TimePoint now); + + mutable std::mutex mtx_; + std::deque keys_; + std::function now_provider_; + }; + + int ticket_store_ex_index(); + +} \ No newline at end of file diff --git a/brazier/src/HttpsServer.cpp b/brazier/src/HttpsServer.cpp index 4d45979..5b310ec 100644 --- a/brazier/src/HttpsServer.cpp +++ b/brazier/src/HttpsServer.cpp @@ -274,6 +274,19 @@ void brazier::HttpsServer::configure_tls() { SSL_CTX_set_options(ssl_ctx_.native_handle(), SSL_OP_IGNORE_UNEXPECTED_EOF); + SSL_CTX_set_session_cache_mode( + ssl_ctx_.native_handle(), + SSL_SESS_CACHE_SERVER); + + static const unsigned char sid_ctx[] = "brazier-https"; + SSL_CTX_set_session_id_context( + ssl_ctx_.native_handle(), + sid_ctx, + sizeof(sid_ctx) - 1); + + ticket_store_.ensure_initialized(); + ticket_store_.attach_to(ssl_ctx_.native_handle()); + apply_ssl_conf(); ssl_ctx_.use_certificate_chain_file(tls_.cert_file); diff --git a/brazier/src/TLS/TicketKeyStore.cpp b/brazier/src/TLS/TicketKeyStore.cpp new file mode 100644 index 0000000..002a33f --- /dev/null +++ b/brazier/src/TLS/TicketKeyStore.cpp @@ -0,0 +1,223 @@ +#include "../../include/brazier/Tls/TicketKeyStore.hpp" + +#include + +#if OPENSSL_VERSION_NUMBER >= 0x30000000L +# include +# include +#endif + +#include +#include +#include + +namespace { + + std::once_flag g_ex_index_flag; + int g_ex_index = -1; + +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + + bool init_hmac(EVP_MAC_CTX* hctx, const unsigned char* key, std::size_t keylen) { + char digest_name[] = "SHA256"; + OSSL_PARAM params[] = { + OSSL_PARAM_construct_utf8_string(OSSL_MAC_PARAM_DIGEST, + digest_name, 0), + OSSL_PARAM_construct_end() + }; + return EVP_MAC_init(hctx, key, keylen, params) == 1; + } + + int ticket_key_cb(SSL* ssl, + unsigned char key_name[16], + unsigned char iv[EVP_MAX_IV_LENGTH], + EVP_CIPHER_CTX* cipher_ctx, + EVP_MAC_CTX* mac_ctx, + int enc) { + SSL_CTX* ssl_ctx = SSL_get_SSL_CTX(ssl); + if (!ssl_ctx) return 0; + + auto* store = static_cast( + SSL_CTX_get_ex_data(ssl_ctx, brazier::ticket_store_ex_index())); + if (!store) return 0; + + return store->handle(key_name, iv, cipher_ctx, mac_ctx, enc); + } + +#else + + bool init_hmac(HMAC_CTX* hctx, const unsigned char* key, int keylen) { + return HMAC_Init_ex(hctx, key, keylen, EVP_sha256(), nullptr) == 1; + } + + int ticket_key_cb(SSL* ssl, + unsigned char key_name[16], + unsigned char iv[EVP_MAX_IV_LENGTH], + EVP_CIPHER_CTX* cipher_ctx, + HMAC_CTX* mac_ctx, + int enc) { + SSL_CTX* ssl_ctx = SSL_get_SSL_CTX(ssl); + if (!ssl_ctx) return 0; + + auto* store = static_cast( + SSL_CTX_get_ex_data(ssl_ctx, brazier::ticket_store_ex_index())); + if (!store) return 0; + + return store->handle(key_name, iv, cipher_ctx, mac_ctx, enc); + } + +#endif + +} + +namespace brazier { + + TicketKeyStore::TicketKeyStore() + : now_provider_([] { return Clock::now(); }) {} + + TicketKeyStore::TicketKeyStore(std::function now_provider) + : now_provider_(std::move(now_provider)) { + if (!now_provider_) { + throw std::invalid_argument("TicketKeyStore: now_provider is null"); + } + } + + TicketKeyStore::Key TicketKeyStore::generate_key(TimePoint now) { + Key k{}; + if (RAND_bytes(k.name, kNameSize) != 1 || + RAND_bytes(k.aes_key, kAesKeySize) != 1 || + RAND_bytes(k.hmac_key, kHmacKeySize) != 1) { + throw std::runtime_error("TicketKeyStore: RAND_bytes failed"); + } + k.created_at = now; + return k; + } + + void TicketKeyStore::ensure_initialized() { + std::lock_guard lk(mtx_); + if (keys_.empty()) { + keys_.push_back(generate_key(now_provider_())); + } + } + + void TicketKeyStore::rotate_now() { + std::lock_guard lk(mtx_); + const auto now = now_provider_(); + keys_.push_front(generate_key(now)); + while (!keys_.empty() && + now - keys_.back().created_at > kKeyLifetime) { + keys_.pop_back(); + } + } + + void TicketKeyStore::maybe_rotate_locked(TimePoint now) { + if (keys_.empty()) { + keys_.push_back(generate_key(now)); + return; + } + if (now - keys_.front().created_at < kRotationInterval) { + return; + } + keys_.push_front(generate_key(now)); + while (!keys_.empty() && + now - keys_.back().created_at > kKeyLifetime) { + keys_.pop_back(); + } + } + + int TicketKeyStore::handle(unsigned char key_name[kNameSize], + unsigned char iv[EVP_MAX_IV_LENGTH], + EVP_CIPHER_CTX* cipher_ctx, + void* mac_ctx, + int enc) { +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + auto* hctx = static_cast(mac_ctx); +#else + auto* hctx = static_cast(mac_ctx); +#endif + + std::lock_guard lk(mtx_); + + if (keys_.empty()) { + keys_.push_back(generate_key(now_provider_())); + } + + if (enc == 1) { + maybe_rotate_locked(now_provider_()); + + const auto& cur = keys_.front(); + std::memcpy(key_name, cur.name, kNameSize); + RAND_bytes(iv, EVP_MAX_IV_LENGTH); + + if (EVP_EncryptInit_ex(cipher_ctx, EVP_aes_256_cbc(), nullptr, + cur.aes_key, iv) != 1) { + return -1; + } + if (!init_hmac(hctx, cur.hmac_key, kHmacKeySize)) { + return -1; + } + return 1; + } + + for (std::size_t i = 0; i < keys_.size(); ++i) { + if (std::memcmp(key_name, keys_[i].name, kNameSize) != 0) { + continue; + } + const auto& k = keys_[i]; + if (EVP_DecryptInit_ex(cipher_ctx, EVP_aes_256_cbc(), nullptr, + k.aes_key, iv) != 1) { + return -1; + } + if (!init_hmac(hctx, k.hmac_key, kHmacKeySize)) { + return -1; + } + return (i == 0) ? 1 : 2; + } + return 0; + } + + void TicketKeyStore::attach_to(SSL_CTX* ctx) { + if (!ctx) { + throw std::invalid_argument("TicketKeyStore::attach_to: ctx is null"); + } + + const int idx = ticket_store_ex_index(); + if (idx < 0) { + throw std::runtime_error( + "TicketKeyStore::attach_to: ex_data index not available"); + } + + void* existing = SSL_CTX_get_ex_data(ctx, idx); + if (existing != nullptr && existing != this) { + throw std::runtime_error( + "TicketKeyStore::attach_to: SSL_CTX already has a different store"); + } + + SSL_CTX_set_ex_data(ctx, idx, this); + +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + SSL_CTX_set_tlsext_ticket_key_evp_cb(ctx, ticket_key_cb); +#else + SSL_CTX_set_tlsext_ticket_key_cb(ctx, ticket_key_cb); +#endif + } + + std::size_t TicketKeyStore::key_count() const { + std::lock_guard lk(mtx_); + return keys_.size(); + } + + std::vector TicketKeyStore::snapshot() const { + std::lock_guard lk(mtx_); + return { keys_.begin(), keys_.end() }; + } + + int ticket_store_ex_index() { + std::call_once(g_ex_index_flag, [] { + g_ex_index = SSL_CTX_get_ex_new_index( + 0, nullptr, nullptr, nullptr, nullptr); + }); + return g_ex_index; + } + +} \ No newline at end of file diff --git a/brazier/tests/unit/security/https_security_test.cpp b/brazier/tests/unit/security/https_security_test.cpp index 959ff4e..c61f458 100644 --- a/brazier/tests/unit/security/https_security_test.cpp +++ b/brazier/tests/unit/security/https_security_test.cpp @@ -427,4 +427,144 @@ TEST_F(HttpsSecurityTest, AbruptClientDisconnect) { TlsClient c2; EXPECT_TRUE(c2.connect()) << "Server not accepting after abrupt disconnect"; +} + +TEST_F(HttpsSecurityTest, SessionResumptionTls12) { + net::io_context io; + ssl::context ctx(ssl::context::tls_client); + ctx.set_verify_mode(ssl::verify_none); + ctx.set_options(ssl::context::no_tlsv1 + | ssl::context::no_tlsv1_1 + | ssl::context::no_tlsv1_3); + + std::unique_ptr + session(nullptr, &SSL_SESSION_free); + + { + ssl::stream stream(io, ctx); + tcp::resolver resolver(io); + auto results = resolver.resolve(https_host_global, + std::to_string(https_port_global)); + + beast::get_lowest_layer(stream).expires_after( + std::chrono::seconds(kSocketTimeoutSec)); + beast::get_lowest_layer(stream).connect(results); + beast::get_lowest_layer(stream).expires_never(); + stream.handshake(ssl::stream_base::client); + + EXPECT_FALSE(SSL_session_reused(stream.native_handle())) + << "First handshake must be full"; + + std::string req = "GET /test HTTP/1.1\r\n" + "Host: " + https_host_global + "\r\n" + "Connection: close\r\n\r\n"; + net::write(stream, net::buffer(req)); + + beast::flat_buffer buf; + http::response res; + beast::get_lowest_layer(stream).expires_after( + std::chrono::seconds(kSocketTimeoutSec)); + http::read(stream, buf, res); + EXPECT_EQ(res.result_int(), 200); + + session.reset(SSL_get1_session(stream.native_handle())); + ASSERT_NE(session.get(), nullptr) + << "Server did not issue a session"; + + beast::error_code ec; + stream.shutdown(ec); + } + + { + ssl::stream stream(io, ctx); + tcp::resolver resolver(io); + auto results = resolver.resolve(https_host_global, + std::to_string(https_port_global)); + + beast::get_lowest_layer(stream).expires_after( + std::chrono::seconds(kSocketTimeoutSec)); + beast::get_lowest_layer(stream).connect(results); + beast::get_lowest_layer(stream).expires_never(); + + ASSERT_EQ(SSL_set_session(stream.native_handle(), session.get()), 1) + << "SSL_set_session failed"; + + stream.handshake(ssl::stream_base::client); + + EXPECT_TRUE(SSL_session_reused(stream.native_handle())) + << "Second handshake should be abbreviated (TLS 1.2)"; + + beast::error_code ec; + stream.shutdown(ec); + } +} + +TEST_F(HttpsSecurityTest, SessionResumptionTls13) { + net::io_context io; + ssl::context ctx(ssl::context::tls_client); + ctx.set_verify_mode(ssl::verify_none); + ctx.set_options(ssl::context::no_tlsv1 + | ssl::context::no_tlsv1_1 + | ssl::context::no_tlsv1_2); + + std::unique_ptr + session(nullptr, &SSL_SESSION_free); + + { + ssl::stream stream(io, ctx); + tcp::resolver resolver(io); + auto results = resolver.resolve(https_host_global, + std::to_string(https_port_global)); + + beast::get_lowest_layer(stream).expires_after( + std::chrono::seconds(kSocketTimeoutSec)); + beast::get_lowest_layer(stream).connect(results); + beast::get_lowest_layer(stream).expires_never(); + stream.handshake(ssl::stream_base::client); + + EXPECT_FALSE(SSL_session_reused(stream.native_handle())) + << "First handshake must be full"; + + std::string req = "GET /test HTTP/1.1\r\n" + "Host: " + https_host_global + "\r\n" + "Connection: close\r\n\r\n"; + net::write(stream, net::buffer(req)); + + beast::flat_buffer buf; + http::response res; + beast::get_lowest_layer(stream).expires_after( + std::chrono::seconds(kSocketTimeoutSec)); + http::read(stream, buf, res); + EXPECT_EQ(res.result_int(), 200); + + session.reset(SSL_get1_session(stream.native_handle())); + ASSERT_NE(session.get(), nullptr) + << "Server did not issue a session (ticket)"; + + beast::error_code ec; + stream.shutdown(ec); + } + + { + ssl::stream stream(io, ctx); + tcp::resolver resolver(io); + auto results = resolver.resolve(https_host_global, + std::to_string(https_port_global)); + + beast::get_lowest_layer(stream).expires_after( + std::chrono::seconds(kSocketTimeoutSec)); + beast::get_lowest_layer(stream).connect(results); + beast::get_lowest_layer(stream).expires_never(); + + ASSERT_EQ(SSL_set_session(stream.native_handle(), session.get()), 1) + << "SSL_set_session failed"; + + stream.handshake(ssl::stream_base::client); + + EXPECT_TRUE(SSL_session_reused(stream.native_handle())) + << "Second handshake should be abbreviated (TLS 1.3)"; + + beast::error_code ec; + stream.shutdown(ec); + } } \ No newline at end of file diff --git a/brazier/tests/unit/tls/ticket_key_store_test.cpp b/brazier/tests/unit/tls/ticket_key_store_test.cpp new file mode 100644 index 0000000..c1cc061 --- /dev/null +++ b/brazier/tests/unit/tls/ticket_key_store_test.cpp @@ -0,0 +1,358 @@ +#include + +#include "../../../include/brazier/Tls/TicketKeyStore.hpp" + +#include +#include + +#if OPENSSL_VERSION_NUMBER < 0x30000000L +# include +#endif + +#include +#include + +using brazier::TicketKeyStore; +using Clock = TicketKeyStore::Clock; + +namespace { + + struct FakeTime { + Clock::time_point t = Clock::time_point{}; + Clock::time_point operator()() const { return t; } + void advance(std::chrono::seconds s) { t += s; } + void advance(std::chrono::hours h) { t += h; } + }; + + struct CipherPair { + EVP_CIPHER_CTX* cipher = EVP_CIPHER_CTX_new(); + +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + EVP_MAC* mac_alg = EVP_MAC_fetch(nullptr, "HMAC", nullptr); + EVP_MAC_CTX* mac = mac_alg ? EVP_MAC_CTX_new(mac_alg) : nullptr; + + ~CipherPair() { + EVP_CIPHER_CTX_free(cipher); + EVP_MAC_CTX_free(mac); + EVP_MAC_free(mac_alg); + } +#else + HMAC_CTX* mac = HMAC_CTX_new(); + + ~CipherPair() { + EVP_CIPHER_CTX_free(cipher); + HMAC_CTX_free(mac); + } +#endif + + CipherPair() = default; + CipherPair(const CipherPair&) = delete; + CipherPair& operator=(const CipherPair&) = delete; + }; + +} + +TEST(TicketKeyStore, InitiallyEmpty) { + TicketKeyStore store; + EXPECT_EQ(store.key_count(), 0u); + EXPECT_TRUE(store.snapshot().empty()); +} + +TEST(TicketKeyStore, EnsureInitializedCreatesOneKey) { + TicketKeyStore store; + store.ensure_initialized(); + ASSERT_EQ(store.key_count(), 1u); + store.ensure_initialized(); + EXPECT_EQ(store.key_count(), 1u); +} + +TEST(TicketKeyStore, EnsureInitializedSetsTimestamp) { + FakeTime ft; + TicketKeyStore store([&] { return ft(); }); + + ft.advance(std::chrono::hours(100)); + store.ensure_initialized(); + + const auto snap = store.snapshot(); + ASSERT_EQ(snap.size(), 1u); + EXPECT_EQ(snap[0].created_at, ft.t); +} + +TEST(TicketKeyStore, EncryptReturnsOne) { + TicketKeyStore store; + unsigned char name[16] = {}; + unsigned char iv[EVP_MAX_IV_LENGTH] = {}; + CipherPair cp; + + EXPECT_EQ(store.handle(name, iv, cp.cipher, cp.mac, 1), 1); +} + +TEST(TicketKeyStore, EncryptFillsNameAndIv) { + TicketKeyStore store; + unsigned char name[16] = {}; + unsigned char iv[EVP_MAX_IV_LENGTH] = {}; + CipherPair cp; + + ASSERT_EQ(store.handle(name, iv, cp.cipher, cp.mac, 1), 1); + + const auto snap = store.snapshot(); + ASSERT_EQ(snap.size(), 1u); + EXPECT_EQ(std::memcmp(name, snap[0].name, 16), 0); + + bool all_zero = true; + for (auto b : iv) if (b != 0) { all_zero = false; break; } + EXPECT_FALSE(all_zero); +} + +TEST(TicketKeyStore, DecryptWithCurrentKeyReturnsOne) { + TicketKeyStore store; + unsigned char name[16] = {}; + unsigned char iv[EVP_MAX_IV_LENGTH] = {}; + CipherPair cp1; + + ASSERT_EQ(store.handle(name, iv, cp1.cipher, cp1.mac, 1), 1); + + CipherPair cp2; + EXPECT_EQ(store.handle(name, iv, cp2.cipher, cp2.mac, 0), 1); +} + +TEST(TicketKeyStore, DecryptWithUnknownKeyReturnsZero) { + TicketKeyStore store; + store.ensure_initialized(); + + unsigned char name[16]; + std::memset(name, 0xFF, sizeof(name)); + unsigned char iv[EVP_MAX_IV_LENGTH] = {}; + CipherPair cp; + + EXPECT_EQ(store.handle(name, iv, cp.cipher, cp.mac, 0), 0); +} + +TEST(TicketKeyStore, NoRotationBeforeInterval) { + FakeTime ft; + TicketKeyStore store([&] { return ft(); }); + store.ensure_initialized(); + + unsigned char name[16] = {}; + unsigned char iv[EVP_MAX_IV_LENGTH] = {}; + + ft.advance(std::chrono::hours(11)); + CipherPair cp; + ASSERT_EQ(store.handle(name, iv, cp.cipher, cp.mac, 1), 1); + EXPECT_EQ(store.key_count(), 1u); +} + +TEST(TicketKeyStore, RotationAfterInterval) { + FakeTime ft; + TicketKeyStore store([&] { return ft(); }); + store.ensure_initialized(); + + unsigned char first_raw[16]; + std::memcpy(first_raw, store.snapshot()[0].name, 16); + + ft.advance(std::chrono::hours(13)); + + unsigned char name[16] = {}; + unsigned char iv[EVP_MAX_IV_LENGTH] = {}; + CipherPair cp; + ASSERT_EQ(store.handle(name, iv, cp.cipher, cp.mac, 1), 1); + + ASSERT_EQ(store.key_count(), 2u); + const auto snap = store.snapshot(); + EXPECT_EQ(std::memcmp(name, snap[0].name, 16), 0); + EXPECT_EQ(std::memcmp(first_raw, snap[1].name, 16), 0); +} + +TEST(TicketKeyStore, RotateNowForcesRotation) { + TicketKeyStore store; + store.ensure_initialized(); + ASSERT_EQ(store.key_count(), 1u); + + unsigned char old_name[16]; + std::memcpy(old_name, store.snapshot()[0].name, 16); + + store.rotate_now(); + + ASSERT_EQ(store.key_count(), 2u); + const auto snap = store.snapshot(); + EXPECT_NE(std::memcmp(old_name, snap[0].name, 16), 0); + EXPECT_EQ(std::memcmp(old_name, snap[1].name, 16), 0); +} + +TEST(TicketKeyStore, OldKeyReturnsTwo) { + FakeTime ft; + TicketKeyStore store([&] { return ft(); }); + store.ensure_initialized(); + + unsigned char old_name[16]; + std::memcpy(old_name, store.snapshot()[0].name, 16); + + ft.advance(std::chrono::hours(13)); + store.rotate_now(); + ASSERT_EQ(store.key_count(), 2u); + + unsigned char iv[EVP_MAX_IV_LENGTH] = {}; + CipherPair cp; + EXPECT_EQ(store.handle(old_name, iv, cp.cipher, cp.mac, 0), 2); +} + +TEST(TicketKeyStore, CurrentKeyStillReturnsOneAfterRotation) { + FakeTime ft; + TicketKeyStore store([&] { return ft(); }); + store.ensure_initialized(); + + ft.advance(std::chrono::hours(13)); + store.rotate_now(); + + unsigned char current_name[16]; + std::memcpy(current_name, store.snapshot()[0].name, 16); + + unsigned char iv[EVP_MAX_IV_LENGTH] = {}; + CipherPair cp; + EXPECT_EQ(store.handle(current_name, iv, cp.cipher, cp.mac, 0), 1); +} + +TEST(TicketKeyStore, KeysOlderThanLifetimeAreRemoved) { + FakeTime ft; + TicketKeyStore store([&] { return ft(); }); + store.ensure_initialized(); + + for (int i = 0; i < 5; ++i) { + ft.advance(std::chrono::hours(13)); + store.rotate_now(); + } + + const auto snap = store.snapshot(); + EXPECT_LE(snap.size(), 4u); + EXPECT_GE(snap.size(), 2u); + + for (const auto& k : snap) { + EXPECT_LT(ft.t - k.created_at, TicketKeyStore::kKeyLifetime); + } +} + +TEST(TicketKeyStore, OldestKeyTimestampOrder) { + FakeTime ft; + TicketKeyStore store([&] { return ft(); }); + store.ensure_initialized(); + + ft.advance(std::chrono::hours(13)); + store.rotate_now(); + ft.advance(std::chrono::hours(13)); + store.rotate_now(); + + const auto snap = store.snapshot(); + ASSERT_GE(snap.size(), 3u); + + for (std::size_t i = 1; i < snap.size(); ++i) { + EXPECT_GE(snap[i - 1].created_at, snap[i].created_at); + } +} + +TEST(TicketKeyStore, TwoStoresHaveDifferentKeys) { + TicketKeyStore a; + TicketKeyStore b; + a.ensure_initialized(); + b.ensure_initialized(); + + const auto sa = a.snapshot(); + const auto sb = b.snapshot(); + ASSERT_EQ(sa.size(), 1u); + ASSERT_EQ(sb.size(), 1u); + + EXPECT_NE(std::memcmp(sa[0].name, sb[0].name, 16), 0); +} + +TEST(TicketKeyStore, KeyFromOneStoreNotAcceptedByOther) { + TicketKeyStore a; + TicketKeyStore b; + a.ensure_initialized(); + b.ensure_initialized(); + + unsigned char name[16] = {}; + unsigned char iv[EVP_MAX_IV_LENGTH] = {}; + CipherPair cp1; + ASSERT_EQ(a.handle(name, iv, cp1.cipher, cp1.mac, 1), 1); + + CipherPair cp2; + EXPECT_EQ(b.handle(name, iv, cp2.cipher, cp2.mac, 0), 0); +} + +TEST(TicketKeyStore, AttachRegistersStoreInExData) { + SSL_CTX* ctx = SSL_CTX_new(TLS_server_method()); + ASSERT_NE(ctx, nullptr); + + TicketKeyStore store; + store.ensure_initialized(); + + store.attach_to(ctx); + + const int idx = brazier::ticket_store_ex_index(); + ASSERT_GE(idx, 0); + + void* raw = SSL_CTX_get_ex_data(ctx, idx); + EXPECT_EQ(static_cast(raw), &store); + + SSL_CTX_free(ctx); +} + +TEST(TicketKeyStore, AttachIsIdempotentForSameStore) { + SSL_CTX* ctx = SSL_CTX_new(TLS_server_method()); + ASSERT_NE(ctx, nullptr); + + TicketKeyStore store; + store.ensure_initialized(); + + store.attach_to(ctx); + EXPECT_NO_THROW(store.attach_to(ctx)); + + SSL_CTX_free(ctx); +} + +TEST(TicketKeyStore, AttachThrowsIfDifferentStoreAlreadyAttached) { + SSL_CTX* ctx = SSL_CTX_new(TLS_server_method()); + ASSERT_NE(ctx, nullptr); + + TicketKeyStore a; + TicketKeyStore b; + a.ensure_initialized(); + b.ensure_initialized(); + + a.attach_to(ctx); + EXPECT_THROW(b.attach_to(ctx), std::runtime_error); + + SSL_CTX_free(ctx); +} + +TEST(TicketKeyStore, AttachThrowsOnNullCtx) { + TicketKeyStore store; + EXPECT_THROW(store.attach_to(nullptr), std::invalid_argument); +} + +TEST(TicketKeyStore, TwoContextsUseSeparateStores) { + SSL_CTX* ctx_a = SSL_CTX_new(TLS_server_method()); + SSL_CTX* ctx_b = SSL_CTX_new(TLS_server_method()); + ASSERT_NE(ctx_a, nullptr); + ASSERT_NE(ctx_b, nullptr); + + TicketKeyStore a; + TicketKeyStore b; + a.ensure_initialized(); + b.ensure_initialized(); + + a.attach_to(ctx_a); + b.attach_to(ctx_b); + + const int idx = brazier::ticket_store_ex_index(); + EXPECT_EQ(SSL_CTX_get_ex_data(ctx_a, idx), &a); + EXPECT_EQ(SSL_CTX_get_ex_data(ctx_b, idx), &b); + + SSL_CTX_free(ctx_a); + SSL_CTX_free(ctx_b); +} + +TEST(TicketKeyStore, NullNowProviderThrows) { + EXPECT_THROW( + TicketKeyStore(std::function{}), + std::invalid_argument); +} \ No newline at end of file From 3c8e03f9fe525ebb06385f77e6f1cdeff11ba774 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Mon, 21 Sep 2026 13:07:53 +0300 Subject: [PATCH 16/29] fix: include dirs for ticket key store --- brazier/src/TLS/TicketKeyStore.cpp | 2 +- brazier/tests/unit/tls/ticket_key_store_test.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/brazier/src/TLS/TicketKeyStore.cpp b/brazier/src/TLS/TicketKeyStore.cpp index 002a33f..832aaf6 100644 --- a/brazier/src/TLS/TicketKeyStore.cpp +++ b/brazier/src/TLS/TicketKeyStore.cpp @@ -1,4 +1,4 @@ -#include "../../include/brazier/Tls/TicketKeyStore.hpp" +#include "../../include/brazier/TLS/TicketKeyStore.hpp" #include diff --git a/brazier/tests/unit/tls/ticket_key_store_test.cpp b/brazier/tests/unit/tls/ticket_key_store_test.cpp index c1cc061..c7fccb3 100644 --- a/brazier/tests/unit/tls/ticket_key_store_test.cpp +++ b/brazier/tests/unit/tls/ticket_key_store_test.cpp @@ -1,6 +1,6 @@ #include -#include "../../../include/brazier/Tls/TicketKeyStore.hpp" +#include "../../../include/brazier/TLS/TicketKeyStore.hpp" #include #include From 73297275a8652079c87475b9455f4e4b39e47d14 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Mon, 21 Sep 2026 16:25:36 +0300 Subject: [PATCH 17/29] feat: tcp_defer_accept --- brazier/src/HttpsServer.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/brazier/src/HttpsServer.cpp b/brazier/src/HttpsServer.cpp index 5b310ec..d484932 100644 --- a/brazier/src/HttpsServer.cpp +++ b/brazier/src/HttpsServer.cpp @@ -32,6 +32,7 @@ # include # else # include +# include # endif #endif @@ -358,6 +359,16 @@ bool brazier::HttpsServer::initialize() { } #endif +#if defined(TCP_DEFER_ACCEPT) + int defer_secs = 1; + if (::setsockopt(acc->native_handle(), IPPROTO_TCP, TCP_DEFER_ACCEPT, + reinterpret_cast(&defer_secs), + sizeof(defer_secs)) != 0) { + Logger::log("TCP_DEFER_ACCEPT setsockopt failed on worker " + + std::to_string(i), "WARNING"); + } +#endif + acc->bind(endpoint); acc->listen(boost::asio::socket_base::max_listen_connections); @@ -371,6 +382,7 @@ bool brazier::HttpsServer::initialize() { initializeConnections(); RouterRegisterer::init(*io_contexts_[0]); + Logger::log("HTTPS server initialized on " + host_ + ":" + std::to_string(port_) + " [TLS, workers=" + std::to_string(worker_count) + ", SO_REUSEPORT=" + @@ -378,6 +390,12 @@ bool brazier::HttpsServer::initialize() { "yes" #else "no" +#endif + + ", TCP_DEFER_ACCEPT=" + +#if defined(TCP_DEFER_ACCEPT) + "yes" +#else + "no" #endif + "]", "SUCCESS"); return true; From 6fff220d450722d7073ceab93809d60156105cef Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Mon, 21 Sep 2026 16:27:26 +0300 Subject: [PATCH 18/29] fix: include dir for linux tcp --- brazier/src/HttpsServer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brazier/src/HttpsServer.cpp b/brazier/src/HttpsServer.cpp index d484932..ad14fb5 100644 --- a/brazier/src/HttpsServer.cpp +++ b/brazier/src/HttpsServer.cpp @@ -32,7 +32,7 @@ # include # else # include -# include +# include # endif #endif From b8f03d96539dc1a91bcdc9b8b841faa8cd492d62 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Mon, 21 Sep 2026 19:40:02 +0300 Subject: [PATCH 19/29] fix: http server --- brazier/app/Main.cpp | 38 +++++++++++++++++++++++++++++--------- brazier/src/Server.cpp | 4 ++-- brazier/tests/main.cpp | 4 ++-- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/brazier/app/Main.cpp b/brazier/app/Main.cpp index 1f4bf6d..a60a0ee 100644 --- a/brazier/app/Main.cpp +++ b/brazier/app/Main.cpp @@ -25,22 +25,42 @@ int main() { try { + //brazier::ConfigManager::initGlobal("config_test.json"); + //brazier::global_config->setAutoSave(false); + + //std::string https_server_host = + // brazier::global_config->get("https_server.host", "0.0.0.0"); + //int https_server_port = + // brazier::global_config->get("https_server.port", 8443); + + //brazier::Logger::log("HTTPS server: " + https_server_host + ":" + + // std::to_string(https_server_port), "INFO"); + + //brazier::HttpsServer https_server(https_server_host, https_server_port); + + //if (!https_server.initialize()) return 1; + + //https_server.run(); + brazier::ConfigManager::initGlobal("config_test.json"); brazier::global_config->setAutoSave(false); - std::string https_server_host = - brazier::global_config->get("https_server.host", "0.0.0.0"); - int https_server_port = - brazier::global_config->get("https_server.port", 8443); + 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("HTTPS server: " + https_server_host + ":" + - std::to_string(https_server_port), "INFO"); + brazier::Logger::log("HTTP server: " + server_host + ":" + + std::to_string(server_port), "INFO"); - brazier::HttpsServer https_server(https_server_host, https_server_port); + brazier::Server server(server_host, + static_cast(server_port)); - if (!https_server.initialize()) return 1; + if (!server.initialize()) return 1; - https_server.run(); + server.run(); + + return 0; return 0; } diff --git a/brazier/src/Server.cpp b/brazier/src/Server.cpp index 1f0ff5f..f710ae3 100644 --- a/brazier/src/Server.cpp +++ b/brazier/src/Server.cpp @@ -51,7 +51,6 @@ bool brazier::Server::initialize() { initializeConnections(); RouterRegisterer::init(io_); - Engine::init(io_); Logger::log("Server initialized on " + host_ + ":" + std::to_string(port_), "SUCCESS"); return true; @@ -73,8 +72,9 @@ void brazier::Server::run() { for (int i = 0; i < threads_count; ++i) { threads_.emplace_back([this] { + brazier::Engine::init(io_); io_.run(); - }); + }); } shutdown_flag_.store(false, std::memory_order_release); diff --git a/brazier/tests/main.cpp b/brazier/tests/main.cpp index 1695839..a79314d 100644 --- a/brazier/tests/main.cpp +++ b/brazier/tests/main.cpp @@ -20,8 +20,8 @@ #include "main.h" -constexpr bool kStartHttpServer = false; -constexpr bool kStartHttpsServer = true; +constexpr bool kStartHttpServer = true; +constexpr bool kStartHttpsServer = false; std::shared_ptr g_test_server; std::shared_ptr g_test_https_server; From 06831b945854464ebdc547c86974aa2e0688dc71 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Mon, 21 Sep 2026 20:54:34 +0300 Subject: [PATCH 20/29] fix: server optimization --- brazier/app/Main.cpp | 50 ++++++++++++------------- brazier/include/brazier/HttpsServer.hpp | 2 +- brazier/src/HttpsServer.cpp | 38 +++++++++---------- brazier/tests/main.cpp | 4 +- 4 files changed, 45 insertions(+), 49 deletions(-) diff --git a/brazier/app/Main.cpp b/brazier/app/Main.cpp index a60a0ee..b7c40ca 100644 --- a/brazier/app/Main.cpp +++ b/brazier/app/Main.cpp @@ -25,44 +25,42 @@ int main() { try { - //brazier::ConfigManager::initGlobal("config_test.json"); - //brazier::global_config->setAutoSave(false); + brazier::ConfigManager::initGlobal("config_test.json"); + brazier::global_config->setAutoSave(false); - //std::string https_server_host = - // brazier::global_config->get("https_server.host", "0.0.0.0"); - //int https_server_port = - // brazier::global_config->get("https_server.port", 8443); + std::string https_server_host = + brazier::global_config->get("https_server.host", "0.0.0.0"); + int https_server_port = + brazier::global_config->get("https_server.port", 8443); - //brazier::Logger::log("HTTPS server: " + https_server_host + ":" + - // std::to_string(https_server_port), "INFO"); + brazier::Logger::log("HTTPS server: " + https_server_host + ":" + + std::to_string(https_server_port), "INFO"); - //brazier::HttpsServer https_server(https_server_host, https_server_port); + brazier::HttpsServer https_server(https_server_host, https_server_port); - //if (!https_server.initialize()) return 1; + if (!https_server.initialize()) return 1; - //https_server.run(); + https_server.run(); - 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::ConfigManager::initGlobal("config_test.json"); + //brazier::global_config->setAutoSave(false); - brazier::Logger::log("HTTP server: " + server_host + ":" + - std::to_string(server_port), "INFO"); + //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::Server server(server_host, - static_cast(server_port)); + //brazier::Logger::log("HTTP server: " + server_host + ":" + + // std::to_string(server_port), "INFO"); - if (!server.initialize()) return 1; + //brazier::Server server(server_host, + // static_cast(server_port)); - server.run(); + //if (!server.initialize()) return 1; - return 0; + //server.run(); - return 0; + //return 0; } catch (const std::exception& e) { std::cerr << "Fatal error: " << e.what() << std::endl; diff --git a/brazier/include/brazier/HttpsServer.hpp b/brazier/include/brazier/HttpsServer.hpp index 36bd0a1..d3a4dc7 100644 --- a/brazier/include/brazier/HttpsServer.hpp +++ b/brazier/include/brazier/HttpsServer.hpp @@ -91,7 +91,6 @@ namespace brazier { private: std::chrono::seconds keep_alive_timeout_{ 60 }; - std::chrono::milliseconds handshake_timeout_ms_{ 15000 }; std::size_t max_body_size_ = 1024 * 1024; std::uint32_t max_header_size_ = 8 * 1024; @@ -160,6 +159,7 @@ namespace brazier { void initializeConnections(); void load_tls_config_from_global(); + void load_common_config_from_global(); void load_limits_from_config(); void configure_tls(); diff --git a/brazier/src/HttpsServer.cpp b/brazier/src/HttpsServer.cpp index ad14fb5..32ce397 100644 --- a/brazier/src/HttpsServer.cpp +++ b/brazier/src/HttpsServer.cpp @@ -141,12 +141,6 @@ std::uint32_t brazier::HttpsServer::compute_max_header_size( void brazier::HttpsServer::load_tls_config_from_global() { if (tls_config_from_user_) return; - keep_alive_timeout_ = std::chrono::seconds( - global_config->get("keep-alive-timeout", 60)); - - handshake_timeout_ms_ = std::chrono::milliseconds( - global_config->get("https_server.tls.handshake_timeout_ms", 15000)); - tls_.cert_file = global_config->get("https_server.tls.cert_file", std::string("server.crt")); tls_.key_file = global_config->get("https_server.tls.key_file", @@ -183,6 +177,11 @@ void brazier::HttpsServer::load_tls_config_from_global() { } } +void brazier::HttpsServer::load_common_config_from_global() { + keep_alive_timeout_ = std::chrono::seconds( + global_config->get("keep-alive-timeout", 60)); +} + void brazier::HttpsServer::load_limits_from_config() { const std::size_t ram_mb = get_system_memory_mb(); @@ -271,13 +270,24 @@ void brazier::HttpsServer::configure_tls() { ssl::context::default_workarounds | ssl::context::no_sslv2 | ssl::context::no_sslv3 + | ssl::context::no_tlsv1 + | ssl::context::no_tlsv1_1 | ssl::context::single_dh_use); SSL_CTX_set_options(ssl_ctx_.native_handle(), SSL_OP_IGNORE_UNEXPECTED_EOF); + SSL_CTX_set_cipher_list( + ssl_ctx_.native_handle(), + "ECDHE-ECDSA-AES128-GCM-SHA256:" + "ECDHE-RSA-AES128-GCM-SHA256:" + "ECDHE-ECDSA-AES256-GCM-SHA384:" + "ECDHE-RSA-AES256-GCM-SHA384:" + "ECDHE-ECDSA-CHACHA20-POLY1305:" + "ECDHE-RSA-CHACHA20-POLY1305"); + SSL_CTX_set_session_cache_mode( ssl_ctx_.native_handle(), - SSL_SESS_CACHE_SERVER); + SSL_SESS_CACHE_OFF); static const unsigned char sid_ctx[] = "brazier-https"; SSL_CTX_set_session_id_context( @@ -331,6 +341,7 @@ bool brazier::HttpsServer::initialize() { StorageManager::getInstance().setDefaultDriver(def); } + load_common_config_from_global(); load_tls_config_from_global(); load_limits_from_config(); configure_tls(); @@ -697,12 +708,6 @@ net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) keep_alive = false; } - if (res.body().empty() && res.count(http::field::content_length) == 0) { - res.content_length(0); - } - else if (!res.body().empty() && res.count(http::field::content_length) == 0) { - res.content_length(res.body().size()); - } res.prepare_payload(); ec.clear(); @@ -733,13 +738,6 @@ net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) net::redirect_error(net::use_awaitable, sd_ec)); sd_timer.cancel(); - - if (sd_ec && sd_ec != net::error::eof - && sd_ec != net::error::operation_aborted - && sd_ec != ssl::error::stream_truncated - && sd_ec != net::error::connection_reset - && sd_ec != net::error::broken_pipe) { - } } { diff --git a/brazier/tests/main.cpp b/brazier/tests/main.cpp index a79314d..1695839 100644 --- a/brazier/tests/main.cpp +++ b/brazier/tests/main.cpp @@ -20,8 +20,8 @@ #include "main.h" -constexpr bool kStartHttpServer = true; -constexpr bool kStartHttpsServer = false; +constexpr bool kStartHttpServer = false; +constexpr bool kStartHttpsServer = true; std::shared_ptr g_test_server; std::shared_ptr g_test_https_server; From 4233cea38e3510497705b32e8a6e1840a2e9ceb8 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Mon, 21 Sep 2026 23:16:08 +0300 Subject: [PATCH 21/29] fix: multiplatform preprocessor defs --- brazier/CMakeLists.txt | 60 ++++-- brazier/include/brazier/Engine.hpp | 19 ++ brazier/include/brazier/HttpsServer.hpp | 40 ++-- .../include/brazier/Platform/NativeSocket.hpp | 9 + .../brazier/Platform/SocketOptions.hpp | 13 ++ .../include/brazier/Platform/SystemInfo.hpp | 9 + brazier/src/HttpsServer.cpp | 199 +++++------------- brazier/src/Platform/Linux/SocketOptions.cpp | 23 ++ brazier/src/Platform/Linux/SystemInfo.cpp | 37 ++++ brazier/src/Platform/MacOS/SocketOptions.cpp | 20 ++ brazier/src/Platform/MacOS/SystemInfo.cpp | 38 ++++ .../src/Platform/Windows/SocketOptions.cpp | 13 ++ brazier/src/Platform/Windows/SystemInfo.cpp | 24 +++ 13 files changed, 316 insertions(+), 188 deletions(-) create mode 100644 brazier/include/brazier/Platform/NativeSocket.hpp create mode 100644 brazier/include/brazier/Platform/SocketOptions.hpp create mode 100644 brazier/include/brazier/Platform/SystemInfo.hpp create mode 100644 brazier/src/Platform/Linux/SocketOptions.cpp create mode 100644 brazier/src/Platform/Linux/SystemInfo.cpp create mode 100644 brazier/src/Platform/MacOS/SocketOptions.cpp create mode 100644 brazier/src/Platform/MacOS/SystemInfo.cpp create mode 100644 brazier/src/Platform/Windows/SocketOptions.cpp create mode 100644 brazier/src/Platform/Windows/SystemInfo.cpp diff --git a/brazier/CMakeLists.txt b/brazier/CMakeLists.txt index e2126e3..f1a0d84 100644 --- a/brazier/CMakeLists.txt +++ b/brazier/CMakeLists.txt @@ -46,18 +46,48 @@ set(LIBRARIES OpenSSL::Crypto ) -file(GLOB_RECURSE PROJECT_SOURCES +if(WIN32) + set(BRAZIER_PLATFORM_DIR "Windows") +elseif(APPLE) + set(BRAZIER_PLATFORM_DIR "MacOS") +else() + set(BRAZIER_PLATFORM_DIR "Linux") +endif() + +message(STATUS "brazier: platform = ${BRAZIER_PLATFORM_DIR}") + +file(GLOB_RECURSE PROJECT_SOURCES CONFIGURE_DEPENDS "src/*.cpp" - "src/Database/*.cpp" - "src/Database/Migrations/*.cpp" - "src/Router/*.cpp" - "src/Filesystem/*.cpp" - "src/Cryptography/*.cpp" ) + +list(FILTER PROJECT_SOURCES EXCLUDE REGEX "^src/Platform/") + +file(GLOB BRAZIER_PLATFORM_SOURCES CONFIGURE_DEPENDS + "src/Platform/${BRAZIER_PLATFORM_DIR}/*.cpp" +) + +if(NOT BRAZIER_PLATFORM_SOURCES) + message(FATAL_ERROR + "No platform sources found in src/Platform/${BRAZIER_PLATFORM_DIR}/") +endif() + +list(APPEND PROJECT_SOURCES ${BRAZIER_PLATFORM_SOURCES}) + list(FILTER PROJECT_SOURCES EXCLUDE REGEX "app/Main\\.cpp$") add_library(brazier_objects OBJECT ${PROJECT_SOURCES}) +foreach(other_platform Linux MacOS Windows) + if(NOT other_platform STREQUAL BRAZIER_PLATFORM_DIR) + file(GLOB OTHER_SOURCES "src/Platform/${other_platform}/*.cpp") + if(OTHER_SOURCES) + set_source_files_properties(${OTHER_SOURCES} + PROPERTIES HEADER_FILE_ONLY TRUE + ) + endif() + endif() +endforeach() + target_link_libraries(brazier_objects PUBLIC Boost::boost) target_include_directories(brazier_objects PUBLIC @@ -134,13 +164,13 @@ target_compile_definitions(brazier_app PRIVATE BOOST_ASIO_HAS_STD_COROUTINE ) -target_link_libraries(brazier_app PRIVATE +target_link_libraries(brazier_app PRIVATE brazier_static ${LIBRARIES} ) add_library(brazier INTERFACE) -target_link_libraries(brazier INTERFACE +target_link_libraries(brazier INTERFACE brazier_static brazier_shared ) @@ -166,7 +196,7 @@ install(DIRECTORY include/ DESTINATION include) install(EXPORT brazierTargets NAMESPACE brazier:: DESTINATION share/brazier - FILE brazierTargets.cmake + FILE brazierTargets.cmake ) install( @@ -178,12 +208,14 @@ 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") + file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS + "tests/*.cpp" + ) add_executable(brazier_tests ${TEST_SOURCES}) @@ -199,12 +231,12 @@ if(BUILD_TESTS) ${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 @@ -226,6 +258,6 @@ if(BUILD_TESTS) 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/include/brazier/Engine.hpp b/brazier/include/brazier/Engine.hpp index 9fc7176..73969fe 100644 --- a/brazier/include/brazier/Engine.hpp +++ b/brazier/include/brazier/Engine.hpp @@ -1,3 +1,22 @@ +/* + * 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 diff --git a/brazier/include/brazier/HttpsServer.hpp b/brazier/include/brazier/HttpsServer.hpp index d3a4dc7..dd18bea 100644 --- a/brazier/include/brazier/HttpsServer.hpp +++ b/brazier/include/brazier/HttpsServer.hpp @@ -41,18 +41,14 @@ #include #include #include -#include -#include #include #include #include #include #include #include -#include -#include -#include +#include "TLS/TicketKeyStore.hpp" #include "vendor/Handlers/ENV.hpp" #include "Database/Queue.hpp" #include "Database/Cache.hpp" @@ -63,8 +59,6 @@ #include "Filesystem/Filesystem.hpp" #include "vendor/ConfigManager.hpp" -#include "../include/brazier/TLS/TicketKeyStore.hpp" - namespace beast = boost::beast; namespace http = beast::http; namespace net = boost::asio; @@ -90,11 +84,11 @@ namespace brazier { }; private: - std::chrono::seconds keep_alive_timeout_{ 60 }; + std::chrono::seconds keep_alive_timeout_{ 60 }; - std::size_t max_body_size_ = 1024 * 1024; - std::uint32_t max_header_size_ = 8 * 1024; - int max_connections_ = 10000; + int max_body_size_ = 1024 * 1024; + int max_header_size_ = 8 * 1024; + int max_connections_ = 10000; std::vector> io_contexts_; std::vector> acceptors_; @@ -121,8 +115,9 @@ namespace brazier { std::atomic connection_count_{ 0 }; std::atomic total_requests_{ 0 }; - TlsConfig tls_; - bool tls_config_from_user_ = false; + TicketKeyStore ticket_store_; + TlsConfig tls_; + bool tls_config_from_user_ = false; struct ConnectionGuard { HttpsServer& srv; @@ -133,8 +128,6 @@ namespace brazier { ConnectionGuard& operator=(const ConnectionGuard&) = delete; }; - brazier::TicketKeyStore ticket_store_; - public: HttpsServer(const std::string& host, unsigned short port); HttpsServer(const std::string& host, unsigned short port, @@ -150,16 +143,15 @@ namespace brazier { unsigned short getPort() const; const std::string& getHost() const; - int getMaxConnections() const { return max_connections_; } - std::size_t getMaxBodySize() const { return max_body_size_; } - std::uint32_t getMaxHeaderSize() const { return max_header_size_; } - std::size_t getIoContextCount() const { return io_contexts_.size(); } + int getMaxConnections() const { return max_connections_; } + int getMaxBodySize() const { return max_body_size_; } + int getMaxHeaderSize() const { return max_header_size_; } private: void initializeConnections(); - void load_tls_config_from_global(); void load_common_config_from_global(); + void load_tls_config_from_global(); void load_limits_from_config(); void configure_tls(); @@ -167,12 +159,8 @@ namespace brazier { void release_connection(); - static std::size_t get_fd_limit(); - static std::size_t get_system_memory_mb(); - static int get_worker_count(); - - static std::size_t compute_max_body_size(std::size_t ram_mb, int max_conn); - static std::uint32_t compute_max_header_size(std::size_t ram_mb, int max_conn); + static int compute_max_body_size(int ram_mb, int max_conn); + static int compute_max_header_size(int ram_mb, int max_conn); net::awaitable handle_connection(tcp::socket socket); net::awaitable accept_loop(tcp::acceptor& acceptor); diff --git a/brazier/include/brazier/Platform/NativeSocket.hpp b/brazier/include/brazier/Platform/NativeSocket.hpp new file mode 100644 index 0000000..33bb51d --- /dev/null +++ b/brazier/include/brazier/Platform/NativeSocket.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace brazier::platform { + + using NativeSocket = std::intptr_t; + +} \ No newline at end of file diff --git a/brazier/include/brazier/Platform/SocketOptions.hpp b/brazier/include/brazier/Platform/SocketOptions.hpp new file mode 100644 index 0000000..b2efe14 --- /dev/null +++ b/brazier/include/brazier/Platform/SocketOptions.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "NativeSocket.hpp" + +namespace brazier::platform { + + bool set_reuse_port(NativeSocket fd) noexcept; + bool set_defer_accept(NativeSocket fd, int seconds) noexcept; + + bool has_reuse_port() noexcept; + bool has_defer_accept() noexcept; + +} \ No newline at end of file diff --git a/brazier/include/brazier/Platform/SystemInfo.hpp b/brazier/include/brazier/Platform/SystemInfo.hpp new file mode 100644 index 0000000..a789159 --- /dev/null +++ b/brazier/include/brazier/Platform/SystemInfo.hpp @@ -0,0 +1,9 @@ +#pragma once + +namespace brazier::platform { + + int get_fd_limit() noexcept; + int get_system_memory_mb() noexcept; + int get_worker_count() noexcept; + +} \ No newline at end of file diff --git a/brazier/src/HttpsServer.cpp b/brazier/src/HttpsServer.cpp index 32ce397..823e6cc 100644 --- a/brazier/src/HttpsServer.cpp +++ b/brazier/src/HttpsServer.cpp @@ -19,23 +19,11 @@ */ #include "../include/brazier/HttpsServer.hpp" +#include "brazier/Platform/SocketOptions.hpp" +#include "brazier/Platform/SystemInfo.hpp" #include -#ifdef _WIN32 -# include -#else -# include -# include -# ifdef __APPLE__ -# include -# include -# else -# include -# include -# endif -#endif - brazier::HttpsServer::HttpsServer(const std::string& host, unsigned short port) : port_(port), host_(host) {} @@ -48,94 +36,42 @@ void brazier::HttpsServer::setTlsConfig(const TlsConfig& tls) { tls_config_from_user_ = true; } -std::size_t brazier::HttpsServer::get_fd_limit() { -#ifdef _WIN32 - return 16384; -#else - struct rlimit rl; - if (::getrlimit(RLIMIT_NOFILE, &rl) != 0) { - return 1024; - } - if (rl.rlim_cur == RLIM_INFINITY) { - if (rl.rlim_max == RLIM_INFINITY) { - return 65536; - } - return static_cast(rl.rlim_max); - } - return static_cast(rl.rlim_cur); -#endif -} - -std::size_t brazier::HttpsServer::get_system_memory_mb() { -#ifdef _WIN32 - MEMORYSTATUSEX ms{}; - ms.dwLength = sizeof(ms); - if (::GlobalMemoryStatusEx(&ms)) { - return static_cast(ms.ullTotalPhys / (1024 * 1024)); - } - return 1024; -#elif defined(__APPLE__) - int mib[2] = { CTL_HW, HW_MEMSIZE }; - uint64_t memsize = 0; - size_t len = sizeof(memsize); - if (::sysctl(mib, 2, &memsize, &len, nullptr, 0) == 0) { - return static_cast(memsize / (1024 * 1024)); - } - return 1024; -#else - struct sysinfo si; - if (::sysinfo(&si) == 0) { - return static_cast( - (static_cast(si.totalram) * si.mem_unit) / - (1024 * 1024)); - } - return 1024; -#endif -} - -int brazier::HttpsServer::get_worker_count() { -#if defined(SO_REUSEPORT) - int n = static_cast(std::thread::hardware_concurrency()); - return (n > 0) ? n : 1; -#else - return 1; -#endif -} - -std::size_t brazier::HttpsServer::compute_max_body_size( - std::size_t ram_mb, int max_conn) { - constexpr double kBodyRamBudget = 0.25; - constexpr std::size_t kMinBody = 64 * 1024; - constexpr std::size_t kMaxBodyCap = 16 * 1024 * 1024; +int brazier::HttpsServer::compute_max_body_size(int ram_mb, int max_conn) { + constexpr int kBodyRamBudgetPct = 25; + constexpr int kMinBody = 64 * 1024; + constexpr int kMaxBodyCap = 16 * 1024 * 1024; if (max_conn <= 0) max_conn = 1; - const std::size_t ram_bytes = ram_mb * 1024 * 1024; - const std::size_t budget = static_cast(ram_bytes * kBodyRamBudget); - const std::size_t per_conn = budget / static_cast(max_conn); + const std::int64_t ram_bytes = (std::int64_t)(ram_mb) * 1024 * 1024; + const std::int64_t budget = ram_bytes * kBodyRamBudgetPct / 100; + std::int64_t per_conn = budget / (std::int64_t)(max_conn); - std::size_t result = per_conn; - if (result < kMinBody) result = kMinBody; - if (result > kMaxBodyCap) result = kMaxBodyCap; - return result; + if (per_conn < kMinBody) per_conn = kMinBody; + if (per_conn > kMaxBodyCap) per_conn = kMaxBodyCap; + return static_cast(per_conn); } -std::uint32_t brazier::HttpsServer::compute_max_header_size( - std::size_t ram_mb, int max_conn) { - constexpr double kHeaderRamBudget = 0.01; - constexpr std::uint32_t kMinHeader = 4 * 1024; - constexpr std::uint32_t kMaxHeaderCap = 32 * 1024; +int brazier::HttpsServer::compute_max_header_size(int ram_mb, int max_conn) { + constexpr int kHeaderRamBudgetPct = 1; + constexpr int kMinHeader = 4 * 1024; + constexpr int kMaxHeaderCap = 32 * 1024; if (max_conn <= 0) max_conn = 1; - const std::size_t ram_bytes = ram_mb * 1024 * 1024; - const std::size_t budget = static_cast(ram_bytes * kHeaderRamBudget); - const std::size_t per_conn = budget / static_cast(max_conn); + const std::int64_t ram_bytes = (std::int64_t)(ram_mb) * 1024 * 1024; + const std::int64_t budget = ram_bytes * kHeaderRamBudgetPct / 100; + std::int64_t per_conn = budget / (std::int64_t)(max_conn); + + if (per_conn < kMinHeader) per_conn = kMinHeader; + if (per_conn > kMaxHeaderCap) per_conn = kMaxHeaderCap; + return static_cast(per_conn); +} - std::size_t result = per_conn; - if (result < kMinHeader) result = kMinHeader; - if (result > kMaxHeaderCap) result = kMaxHeaderCap; - return static_cast(result); +void brazier::HttpsServer::load_common_config_from_global() { + keep_alive_timeout_ = std::chrono::seconds( + global_config->get("http.keep_alive_timeout", + global_config->get("keep-alive-timeout", 60))); } void brazier::HttpsServer::load_tls_config_from_global() { @@ -177,13 +113,8 @@ void brazier::HttpsServer::load_tls_config_from_global() { } } -void brazier::HttpsServer::load_common_config_from_global() { - keep_alive_timeout_ = std::chrono::seconds( - global_config->get("keep-alive-timeout", 60)); -} - void brazier::HttpsServer::load_limits_from_config() { - const std::size_t ram_mb = get_system_memory_mb(); + const int ram_mb = platform::get_system_memory_mb(); const int testing_conn = global_config->get("http.max_connections_testing", 0); const int testing_body = global_config->get("http.max_body_size_testing", 0); @@ -199,25 +130,25 @@ void brazier::HttpsServer::load_limits_from_config() { max_connections_ = v; } else { - const int fd_limit = static_cast(get_fd_limit()); - max_connections_ = static_cast(fd_limit * 0.8); + const int fd_limit = platform::get_fd_limit(); + max_connections_ = fd_limit * 8 / 10; } if (testing_body > 0) { - max_body_size_ = static_cast(testing_body); + max_body_size_ = testing_body; } else if (int v = global_config->get("http.max_body_size", 0); v > 0) { - max_body_size_ = static_cast(v); + max_body_size_ = v; } else { max_body_size_ = compute_max_body_size(ram_mb, max_connections_); } if (testing_hdr > 0) { - max_header_size_ = static_cast(testing_hdr); + max_header_size_ = testing_hdr; } else if (int v = global_config->get("http.max_header_size", 0); v > 0) { - max_header_size_ = static_cast(v); + max_header_size_ = v; } else { max_header_size_ = compute_max_header_size(ram_mb, max_connections_); @@ -276,25 +207,10 @@ void brazier::HttpsServer::configure_tls() { SSL_CTX_set_options(ssl_ctx_.native_handle(), SSL_OP_IGNORE_UNEXPECTED_EOF); - SSL_CTX_set_cipher_list( - ssl_ctx_.native_handle(), - "ECDHE-ECDSA-AES128-GCM-SHA256:" - "ECDHE-RSA-AES128-GCM-SHA256:" - "ECDHE-ECDSA-AES256-GCM-SHA384:" - "ECDHE-RSA-AES256-GCM-SHA384:" - "ECDHE-ECDSA-CHACHA20-POLY1305:" - "ECDHE-RSA-CHACHA20-POLY1305"); - SSL_CTX_set_session_cache_mode( ssl_ctx_.native_handle(), SSL_SESS_CACHE_OFF); - static const unsigned char sid_ctx[] = "brazier-https"; - SSL_CTX_set_session_id_context( - ssl_ctx_.native_handle(), - sid_ctx, - sizeof(sid_ctx) - 1); - ticket_store_.ensure_initialized(); ticket_store_.attach_to(ssl_ctx_.native_handle()); @@ -347,7 +263,7 @@ bool brazier::HttpsServer::initialize() { configure_tls(); const tcp::endpoint endpoint(net::ip::make_address(host_), port_); - const int worker_count = get_worker_count(); + const int worker_count = platform::get_worker_count(); io_contexts_.reserve(worker_count); acceptors_.reserve(worker_count); @@ -360,25 +276,19 @@ bool brazier::HttpsServer::initialize() { acc->open(endpoint.protocol()); acc->set_option(tcp::acceptor::reuse_address(true)); -#if defined(SO_REUSEPORT) - int one = 1; - if (::setsockopt(acc->native_handle(), SOL_SOCKET, SO_REUSEPORT, - reinterpret_cast(&one), - sizeof(one)) != 0) { + const auto native = static_cast( + acc->native_handle()); + + if (!platform::set_reuse_port(native) && platform::has_reuse_port()) { Logger::log("SO_REUSEPORT setsockopt failed on worker " + std::to_string(i), "WARNING"); } -#endif -#if defined(TCP_DEFER_ACCEPT) - int defer_secs = 1; - if (::setsockopt(acc->native_handle(), IPPROTO_TCP, TCP_DEFER_ACCEPT, - reinterpret_cast(&defer_secs), - sizeof(defer_secs)) != 0) { + if (!platform::set_defer_accept(native, 1) + && platform::has_defer_accept()) { Logger::log("TCP_DEFER_ACCEPT setsockopt failed on worker " + std::to_string(i), "WARNING"); } -#endif acc->bind(endpoint); acc->listen(boost::asio::socket_base::max_listen_connections); @@ -397,18 +307,10 @@ bool brazier::HttpsServer::initialize() { Logger::log("HTTPS server initialized on " + host_ + ":" + std::to_string(port_) + " [TLS, workers=" + std::to_string(worker_count) + ", SO_REUSEPORT=" + -#if defined(SO_REUSEPORT) - "yes" -#else - "no" -#endif - + ", TCP_DEFER_ACCEPT=" + -#if defined(TCP_DEFER_ACCEPT) - "yes" -#else - "no" -#endif - + "]", "SUCCESS"); + (platform::has_reuse_port() ? "yes" : "no") + + ", TCP_DEFER_ACCEPT=" + + (platform::has_defer_accept() ? "yes" : "no") + + "]", "SUCCESS"); return true; } catch (const std::exception& e) { @@ -437,7 +339,8 @@ void brazier::HttpsServer::initializeConnections() { try { Database db; - (new MigrationManager(db))->Initialize(); + auto migrator = std::make_unique(db); + migrator->Initialize(); } catch (const std::exception& e) { Logger::log("Database migration failed: " + std::string(e.what()), "ERROR"); @@ -446,7 +349,7 @@ void brazier::HttpsServer::initializeConnections() { void brazier::HttpsServer::run() { try { - for (size_t i = 0; i < io_contexts_.size(); ++i) { + for (int i = 0; i < static_cast(io_contexts_.size()); ++i) { net::co_spawn(*io_contexts_[i], accept_loop(*acceptors_[i]), net::detached); @@ -642,8 +545,8 @@ net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) while (keep_alive && !timed_out) { parser.emplace(); - parser->body_limit(max_body_size_); - parser->header_limit(max_header_size_); + parser->body_limit(static_cast(max_body_size_)); + parser->header_limit(static_cast(max_header_size_)); beast::error_code ec; diff --git a/brazier/src/Platform/Linux/SocketOptions.cpp b/brazier/src/Platform/Linux/SocketOptions.cpp new file mode 100644 index 0000000..0a5db19 --- /dev/null +++ b/brazier/src/Platform/Linux/SocketOptions.cpp @@ -0,0 +1,23 @@ +#include "brazier/Platform/SocketOptions.hpp" + +#include +#include +#include + +namespace brazier::platform { + + bool set_reuse_port(NativeSocket fd) noexcept { + int one = 1; + return ::setsockopt(static_cast(fd), SOL_SOCKET, SO_REUSEPORT, + &one, sizeof(one)) == 0; + } + + bool set_defer_accept(NativeSocket fd, int seconds) noexcept { + return ::setsockopt(static_cast(fd), IPPROTO_TCP, TCP_DEFER_ACCEPT, + &seconds, sizeof(seconds)) == 0; + } + + bool has_reuse_port() noexcept { return true; } + bool has_defer_accept() noexcept { return true; } + +} \ No newline at end of file diff --git a/brazier/src/Platform/Linux/SystemInfo.cpp b/brazier/src/Platform/Linux/SystemInfo.cpp new file mode 100644 index 0000000..9550245 --- /dev/null +++ b/brazier/src/Platform/Linux/SystemInfo.cpp @@ -0,0 +1,37 @@ +#include "brazier/Platform/SystemInfo.hpp" + +#include +#include +#include + +namespace brazier::platform { + + int get_fd_limit() noexcept { + struct rlimit rl; + if (::getrlimit(RLIMIT_NOFILE, &rl) != 0) { + return 1024; + } + if (rl.rlim_cur == RLIM_INFINITY) { + return (rl.rlim_max == RLIM_INFINITY) + ? 65536 + : static_cast(rl.rlim_max); + } + return static_cast(rl.rlim_cur); + } + + int get_system_memory_mb() noexcept { + struct sysinfo si; + if (::sysinfo(&si) == 0) { + return static_cast( + (static_cast(si.totalram) * si.mem_unit) / + (1024 * 1024)); + } + return 1024; + } + + int get_worker_count() noexcept { + const int n = static_cast(std::thread::hardware_concurrency()); + return (n > 0) ? n : 1; + } + +} \ No newline at end of file diff --git a/brazier/src/Platform/MacOS/SocketOptions.cpp b/brazier/src/Platform/MacOS/SocketOptions.cpp new file mode 100644 index 0000000..094987e --- /dev/null +++ b/brazier/src/Platform/MacOS/SocketOptions.cpp @@ -0,0 +1,20 @@ +#include "brazier/Platform/SocketOptions.hpp" + +#include + +namespace brazier::platform { + + bool set_reuse_port(NativeSocket fd) noexcept { + int one = 1; + return ::setsockopt(static_cast(fd), SOL_SOCKET, SO_REUSEPORT, + &one, sizeof(one)) == 0; + } + + bool set_defer_accept(NativeSocket /*fd*/, int /*seconds*/) noexcept { + return false; + } + + bool has_reuse_port() noexcept { return true; } + bool has_defer_accept() noexcept { return false; } + +} \ No newline at end of file diff --git a/brazier/src/Platform/MacOS/SystemInfo.cpp b/brazier/src/Platform/MacOS/SystemInfo.cpp new file mode 100644 index 0000000..2ee4cb5 --- /dev/null +++ b/brazier/src/Platform/MacOS/SystemInfo.cpp @@ -0,0 +1,38 @@ +#include "brazier/Platform/SystemInfo.hpp" + +#include +#include +#include +#include + +namespace brazier::platform { + + int get_fd_limit() noexcept { + struct rlimit rl; + if (::getrlimit(RLIMIT_NOFILE, &rl) != 0) { + return 1024; + } + if (rl.rlim_cur == RLIM_INFINITY) { + return (rl.rlim_max == RLIM_INFINITY) + ? 65536 + : static_cast(rl.rlim_max); + } + return static_cast(rl.rlim_cur); + } + + int get_system_memory_mb() noexcept { + int mib[2] = { CTL_HW, HW_MEMSIZE }; + uint64_t memsize = 0; + size_t len = sizeof(memsize); + if (::sysctl(mib, 2, &memsize, &len, nullptr, 0) == 0) { + return static_cast(memsize / (1024 * 1024)); + } + return 1024; + } + + int get_worker_count() noexcept { + const int n = static_cast(std::thread::hardware_concurrency()); + return (n > 0) ? n : 1; + } + +} \ No newline at end of file diff --git a/brazier/src/Platform/Windows/SocketOptions.cpp b/brazier/src/Platform/Windows/SocketOptions.cpp new file mode 100644 index 0000000..b1cf792 --- /dev/null +++ b/brazier/src/Platform/Windows/SocketOptions.cpp @@ -0,0 +1,13 @@ +#include "brazier/Platform/SocketOptions.hpp" + +#include + +namespace brazier::platform { + + bool set_reuse_port(NativeSocket /*fd*/) noexcept { return false; } + bool set_defer_accept(NativeSocket /*fd*/, int /*seconds*/) noexcept { return false; } + + bool has_reuse_port() noexcept { return false; } + bool has_defer_accept() noexcept { return false; } + +} \ No newline at end of file diff --git a/brazier/src/Platform/Windows/SystemInfo.cpp b/brazier/src/Platform/Windows/SystemInfo.cpp new file mode 100644 index 0000000..d1d1f63 --- /dev/null +++ b/brazier/src/Platform/Windows/SystemInfo.cpp @@ -0,0 +1,24 @@ +#include "brazier/Platform/SystemInfo.hpp" + +#include + +namespace brazier::platform { + + int get_fd_limit() noexcept { + return 16384; + } + + int get_system_memory_mb() noexcept { + MEMORYSTATUSEX ms{}; + ms.dwLength = sizeof(ms); + if (::GlobalMemoryStatusEx(&ms)) { + return static_cast(ms.ullTotalPhys / (1024 * 1024)); + } + return 1024; + } + + int get_worker_count() noexcept { + return 1; + } + +} \ No newline at end of file From 68b24db32ac7b4e63872d39bfc10bf350db467af Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Tue, 22 Sep 2026 14:22:28 +0300 Subject: [PATCH 22/29] perf: server optimization --- brazier/CMakeLists.txt | 3 ++ brazier/app/BenchmarkController.cpp | 14 ++++++ brazier/app/Main.cpp | 13 +++-- brazier/config_test.json | 2 +- .../include/brazier/Platform/SystemInfo.hpp | 4 ++ brazier/src/HttpsServer.cpp | 47 ++++++++++++++----- brazier/src/Platform/Linux/SystemInfo.cpp | 5 ++ brazier/src/Platform/MacOS/SystemInfo.cpp | 4 ++ brazier/src/Platform/Windows/SystemInfo.cpp | 5 ++ 9 files changed, 81 insertions(+), 16 deletions(-) create mode 100644 brazier/app/BenchmarkController.cpp diff --git a/brazier/CMakeLists.txt b/brazier/CMakeLists.txt index f1a0d84..1697a51 100644 --- a/brazier/CMakeLists.txt +++ b/brazier/CMakeLists.txt @@ -21,6 +21,9 @@ endif() if(MSVC) add_compile_options(/utf-8 /bigobj) + + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /Zi") + set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /DEBUG /OPT:REF /OPT:ICF") endif() find_package(Boost CONFIG REQUIRED COMPONENTS beast asio system thread filesystem) diff --git a/brazier/app/BenchmarkController.cpp b/brazier/app/BenchmarkController.cpp new file mode 100644 index 0000000..8061749 --- /dev/null +++ b/brazier/app/BenchmarkController.cpp @@ -0,0 +1,14 @@ +#include "../include/brazier/Http" + +class BenchmarkController : public brazier::Controller { +public: + using Request = http::request; + using Response = http::response; + + boost::asio::awaitable test(const Request& req, Response& res, const Params& params) { + res.result(http::status::ok); + res.set(http::field::content_type, "text/plain"); + res.body() = ""; + co_return; + } +}; \ No newline at end of file diff --git a/brazier/app/Main.cpp b/brazier/app/Main.cpp index b7c40ca..0e380a1 100644 --- a/brazier/app/Main.cpp +++ b/brazier/app/Main.cpp @@ -22,26 +22,31 @@ #include "../include/brazier/DB" #include "../include/brazier/Http" #include "../include/brazier/Engine.hpp" +#include "BenchmarkController.cpp" + +using namespace brazier; int main() { try { + auto benchmark_controller = std::make_shared(); + + R(GET, "/test", benchmark_controller, test); + brazier::ConfigManager::initGlobal("config_test.json"); brazier::global_config->setAutoSave(false); - std::string https_server_host = brazier::global_config->get("https_server.host", "0.0.0.0"); int https_server_port = brazier::global_config->get("https_server.port", 8443); - brazier::Logger::log("HTTPS server: " + https_server_host + ":" + std::to_string(https_server_port), "INFO"); brazier::HttpsServer https_server(https_server_host, https_server_port); if (!https_server.initialize()) return 1; + https_server.run(); - https_server.run(); - + return 0; //brazier::ConfigManager::initGlobal("config_test.json"); //brazier::global_config->setAutoSave(false); diff --git a/brazier/config_test.json b/brazier/config_test.json index 993eb50..aec896e 100644 --- a/brazier/config_test.json +++ b/brazier/config_test.json @@ -14,7 +14,7 @@ } }, "http": { - "max_connections_testing": 20, + "max_connections_testing": 100, "max_body_size_testing": 1048576, "max_header_size_testing": 8192 } diff --git a/brazier/include/brazier/Platform/SystemInfo.hpp b/brazier/include/brazier/Platform/SystemInfo.hpp index a789159..6d3d7ee 100644 --- a/brazier/include/brazier/Platform/SystemInfo.hpp +++ b/brazier/include/brazier/Platform/SystemInfo.hpp @@ -3,7 +3,11 @@ namespace brazier::platform { int get_fd_limit() noexcept; + int get_system_memory_mb() noexcept; + int get_worker_count() noexcept; + int get_thread_count() noexcept; + } \ No newline at end of file diff --git a/brazier/src/HttpsServer.cpp b/brazier/src/HttpsServer.cpp index 823e6cc..e171a15 100644 --- a/brazier/src/HttpsServer.cpp +++ b/brazier/src/HttpsServer.cpp @@ -355,11 +355,16 @@ void brazier::HttpsServer::run() { net::detached); } - Logger::log("Starting " + std::to_string(io_contexts_.size()) + - " HTTPS worker thread(s)", "INFO"); + const int thread_count = platform::get_thread_count(); + const int ctx_count = static_cast(io_contexts_.size()); - for (auto& io : io_contexts_) { - auto* io_ptr = io.get(); + Logger::log("Starting " + std::to_string(thread_count) + + " HTTPS worker thread(s) over " + + std::to_string(ctx_count) + " io_context(s)", + "INFO"); + + for (int i = 0; i < thread_count; ++i) { + auto* io_ptr = io_contexts_[i % ctx_count].get(); threads_.emplace_back([io_ptr] { brazier::Engine::init(*io_ptr); io_ptr->run(); @@ -593,12 +598,9 @@ net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) total_requests_.fetch_add(1, std::memory_order_relaxed); keep_alive = req.keep_alive(); - res = {}; + res.clear(); res.version(req.version()); res.keep_alive(keep_alive); - res.set(http::field::connection, keep_alive ? "keep-alive" : "close"); - res.set(http::field::server, "brazier"); - res.set(http::field::strict_transport_security, "max-age=31536000"); try { co_await Router::handle_request(req, res); @@ -613,12 +615,35 @@ net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) res.prepare_payload(); + std::string flat; + flat.reserve(512 + res.body().size()); + + flat += "HTTP/1.1 "; + flat += std::to_string(res.result_int()); + flat += ' '; + flat += res.reason(); + flat += "\r\n"; + + flat += "Server: brazier\r\n"; + flat += "Strict-Transport-Security: max-age=31536000\r\n"; + flat += "Connection: "; + flat += keep_alive ? "keep-alive\r\n" : "close\r\n"; + + for (const auto& field : res.base()) { + flat += field.name_string(); + flat += ": "; + flat += field.value(); + flat += "\r\n"; + } + flat += "\r\n"; + flat += res.body(); + ec.clear(); - co_await http::async_write( - stream, res, + co_await net::async_write( + stream, net::buffer(flat), net::redirect_error(net::use_awaitable, ec)); - if (ec) break; + if (ec) break;; buffer.consume(buffer.size()); if (!keep_alive) break; diff --git a/brazier/src/Platform/Linux/SystemInfo.cpp b/brazier/src/Platform/Linux/SystemInfo.cpp index 9550245..2cfce0c 100644 --- a/brazier/src/Platform/Linux/SystemInfo.cpp +++ b/brazier/src/Platform/Linux/SystemInfo.cpp @@ -34,4 +34,9 @@ namespace brazier::platform { return (n > 0) ? n : 1; } + int get_thread_count() noexcept { + const int n = static_cast(std::thread::hardware_concurrency()); + return (n > 0) ? n : 1; + } + } \ No newline at end of file diff --git a/brazier/src/Platform/MacOS/SystemInfo.cpp b/brazier/src/Platform/MacOS/SystemInfo.cpp index 2ee4cb5..f7eee90 100644 --- a/brazier/src/Platform/MacOS/SystemInfo.cpp +++ b/brazier/src/Platform/MacOS/SystemInfo.cpp @@ -35,4 +35,8 @@ namespace brazier::platform { return (n > 0) ? n : 1; } + int get_thread_count() noexcept { + const int n = static_cast(std::thread::hardware_concurrency()); + return (n > 0) ? n : 1; + } } \ No newline at end of file diff --git a/brazier/src/Platform/Windows/SystemInfo.cpp b/brazier/src/Platform/Windows/SystemInfo.cpp index d1d1f63..6d31972 100644 --- a/brazier/src/Platform/Windows/SystemInfo.cpp +++ b/brazier/src/Platform/Windows/SystemInfo.cpp @@ -21,4 +21,9 @@ namespace brazier::platform { return 1; } + int get_thread_count() noexcept { + const int n = static_cast(std::thread::hardware_concurrency()); + return (n > 0) ? n : 1; + } + } \ No newline at end of file From 898603b281fa469717d80a698d35dac4d0a6e56e Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Tue, 22 Sep 2026 15:05:16 +0300 Subject: [PATCH 23/29] perf: threads and contexts count --- brazier/include/brazier/HttpsServer.hpp | 3 +- .../include/brazier/Platform/SystemInfo.hpp | 4 + brazier/src/HttpsServer.cpp | 132 +++++++++++++----- 3 files changed, 103 insertions(+), 36 deletions(-) diff --git a/brazier/include/brazier/HttpsServer.hpp b/brazier/include/brazier/HttpsServer.hpp index dd18bea..d70613f 100644 --- a/brazier/include/brazier/HttpsServer.hpp +++ b/brazier/include/brazier/HttpsServer.hpp @@ -93,7 +93,7 @@ namespace brazier { std::vector> io_contexts_; std::vector> acceptors_; std::vector>> work_guards_; + net::executor_work_guard>> work_guards_; ssl::context ssl_ctx_{ ssl::context::tls_server }; @@ -164,6 +164,7 @@ namespace brazier { net::awaitable handle_connection(tcp::socket socket); net::awaitable accept_loop(tcp::acceptor& acceptor); + net::awaitable accept_and_dispatch(tcp::acceptor& acceptor, int worker_begin); }; } \ No newline at end of file diff --git a/brazier/include/brazier/Platform/SystemInfo.hpp b/brazier/include/brazier/Platform/SystemInfo.hpp index 6d3d7ee..2dc0db7 100644 --- a/brazier/include/brazier/Platform/SystemInfo.hpp +++ b/brazier/include/brazier/Platform/SystemInfo.hpp @@ -10,4 +10,8 @@ namespace brazier::platform { int get_thread_count() noexcept; + bool has_reuse_port() noexcept; + + int get_io_context_count() noexcept; + } \ No newline at end of file diff --git a/brazier/src/HttpsServer.cpp b/brazier/src/HttpsServer.cpp index e171a15..b2d1dfe 100644 --- a/brazier/src/HttpsServer.cpp +++ b/brazier/src/HttpsServer.cpp @@ -263,50 +263,61 @@ bool brazier::HttpsServer::initialize() { configure_tls(); const tcp::endpoint endpoint(net::ip::make_address(host_), port_); - const int worker_count = platform::get_worker_count(); - io_contexts_.reserve(worker_count); - acceptors_.reserve(worker_count); - work_guards_.reserve(worker_count); + const int n = platform::get_thread_count(); + const bool split_accept = !platform::has_reuse_port(); + const int io_count = n; - for (int i = 0; i < worker_count; ++i) { + io_contexts_.reserve(io_count); + work_guards_.reserve(io_count); + acceptors_.reserve(split_accept ? 1 : io_count); + + for (int i = 0; i < io_count; ++i) { auto io = std::make_unique(); - auto acc = std::make_unique(*io); - acc->open(endpoint.protocol()); - acc->set_option(tcp::acceptor::reuse_address(true)); + const bool needs_acceptor = !split_accept || (i == 0); - const auto native = static_cast( - acc->native_handle()); + if (needs_acceptor) { + auto acc = std::make_unique(*io); + acc->open(endpoint.protocol()); + acc->set_option(tcp::acceptor::reuse_address(true)); - if (!platform::set_reuse_port(native) && platform::has_reuse_port()) { - Logger::log("SO_REUSEPORT setsockopt failed on worker " + - std::to_string(i), "WARNING"); - } + const auto native = static_cast( + acc->native_handle()); - if (!platform::set_defer_accept(native, 1) - && platform::has_defer_accept()) { - Logger::log("TCP_DEFER_ACCEPT setsockopt failed on worker " + - std::to_string(i), "WARNING"); - } + if (!platform::set_reuse_port(native) + && platform::has_reuse_port()) { + Logger::log("SO_REUSEPORT setsockopt failed on worker " + + std::to_string(i), "WARNING"); + } + + if (!platform::set_defer_accept(native, 1) + && platform::has_defer_accept()) { + Logger::log("TCP_DEFER_ACCEPT setsockopt failed on worker " + + std::to_string(i), "WARNING"); + } - acc->bind(endpoint); - acc->listen(boost::asio::socket_base::max_listen_connections); + acc->bind(endpoint); + acc->listen(boost::asio::socket_base::max_listen_connections); + acceptors_.push_back(std::move(acc)); + } work_guards_.push_back(std::make_unique< net::executor_work_guard>( io->get_executor())); io_contexts_.push_back(std::move(io)); - acceptors_.push_back(std::move(acc)); } initializeConnections(); RouterRegisterer::init(*io_contexts_[0]); Logger::log("HTTPS server initialized on " + host_ + ":" + - std::to_string(port_) + " [TLS, workers=" + - std::to_string(worker_count) + ", SO_REUSEPORT=" + + std::to_string(port_) + " [TLS, io_contexts=" + + std::to_string(io_count) + ", acceptors=" + + std::to_string(acceptors_.size()) + ", dispatch=" + + (split_accept ? "round-robin" : "SO_REUSEPORT") + + ", SO_REUSEPORT=" + (platform::has_reuse_port() ? "yes" : "no") + ", TCP_DEFER_ACCEPT=" + (platform::has_defer_accept() ? "yes" : "no") + @@ -349,22 +360,30 @@ void brazier::HttpsServer::initializeConnections() { void brazier::HttpsServer::run() { try { - for (int i = 0; i < static_cast(io_contexts_.size()); ++i) { - net::co_spawn(*io_contexts_[i], - accept_loop(*acceptors_[i]), + const int io_count = static_cast(io_contexts_.size()); + const bool split_accept = !platform::has_reuse_port(); + + if (split_accept) { + net::co_spawn(*io_contexts_[0], + accept_and_dispatch(*acceptors_[0], 0), net::detached); } + else { + for (int i = 0; i < io_count; ++i) { + net::co_spawn(*io_contexts_[i], + accept_loop(*acceptors_[i]), + net::detached); + } + } - const int thread_count = platform::get_thread_count(); - const int ctx_count = static_cast(io_contexts_.size()); - - Logger::log("Starting " + std::to_string(thread_count) + - " HTTPS worker thread(s) over " + - std::to_string(ctx_count) + " io_context(s)", + Logger::log("Starting " + std::to_string(io_count) + + " io_context(s) over " + std::to_string(io_count) + + " thread(s), dispatch=" + + (split_accept ? "round-robin" : "SO_REUSEPORT"), "INFO"); - for (int i = 0; i < thread_count; ++i) { - auto* io_ptr = io_contexts_[i % ctx_count].get(); + for (auto& io : io_contexts_) { + auto* io_ptr = io.get(); threads_.emplace_back([io_ptr] { brazier::Engine::init(*io_ptr); io_ptr->run(); @@ -408,6 +427,49 @@ void brazier::HttpsServer::run() { } } +net::awaitable brazier::HttpsServer::accept_and_dispatch( + tcp::acceptor& acceptor, int worker_begin) +{ + const int worker_count = static_cast(io_contexts_.size()) - worker_begin; + int next = 0; + + for (;;) { + if (shutting_down_.load(std::memory_order_acquire)) { + co_return; + } + + beast::error_code ec; + tcp::socket socket = co_await acceptor.async_accept( + net::redirect_error(net::use_awaitable, ec)); + + if (ec) { + if (ec == net::error::operation_aborted || + shutting_down_.load(std::memory_order_acquire)) { + co_return; + } + Logger::log("Accept error: " + ec.message(), "ERROR"); + continue; + } + + const int prev = connection_count_.fetch_add(1, std::memory_order_acq_rel); + if (prev >= max_connections_) { + connection_count_.fetch_sub(1, std::memory_order_acq_rel); + Logger::log("Connection limit reached (" + + std::to_string(prev) + "/" + + std::to_string(max_connections_) + "), rejecting", "WARNING"); + boost::system::error_code ignore; + socket.close(ignore); + continue; + } + + const int idx = worker_begin + (next++ % worker_count); + + net::co_spawn(*io_contexts_[idx], + handle_connection(std::move(socket)), + net::detached); + } +} + void brazier::HttpsServer::stop() { Logger::log("HTTPS server stopping (graceful)...", "INFO"); From c2b605deaa04fa51fef233b2147fb6104ebaccb6 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Thu, 24 Sep 2026 15:50:01 +0300 Subject: [PATCH 24/29] fix: pem-string certs and mTLS testing --- .github/workflows/cmake-multi-platform.yml | 94 ++++++- README.md | 237 ++++++++++++++++- brazier/app/Main.cpp | 17 +- brazier/include/brazier/HttpsServer.hpp | 21 +- brazier/src/HttpsServer.cpp | 227 +++++++++++++--- .../tests/unit/routing/https_routing_test.cpp | 2 +- .../tests/unit/security/https_mtls_test.cpp | 250 ++++++++++++++++++ .../unit/security/https_security_test.cpp | 23 -- 8 files changed, 794 insertions(+), 77 deletions(-) create mode 100644 brazier/tests/unit/security/https_mtls_test.cpp diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 65cfe13..e8cfebf 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -99,7 +99,7 @@ jobs: cp config_test.json ${{ matrix.build_type }}/ shell: bash - - name: Generate self-signed certificate + - name: Generate self-signed certificate (main server) run: | mkdir -p build/brazier/app/certs openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \ @@ -109,6 +109,34 @@ jobs: -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" shell: bash + - name: Generate mTLS certificates (CA + client) + run: | + set -e + mkdir -p build/certs + cd build/certs + + openssl genrsa -out ca.key 4096 + openssl req -x509 -new -nodes -key ca.key -sha256 -days 1825 \ + -out ca.crt -subj "/CN=Test CA" + + openssl genrsa -out server.key 2048 + openssl req -new -key server.key -out server.csr -subj "/CN=localhost" + echo "subjectAltName=DNS:localhost,IP:127.0.0.1" > server_ext.cnf + openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \ + -CAcreateserial -out server.crt -days 365 -sha256 \ + -extfile server_ext.cnf + + openssl genrsa -out client.key 2048 + openssl req -new -key client.key -out client.csr -subj "/CN=brazier-client" + echo "extendedKeyUsage=clientAuth" > client_ext.cnf + openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key \ + -CAserial ca.srl -out client.crt -days 365 -sha256 \ + -extfile client_ext.cnf + + openssl verify -CAfile ca.crt server.crt + openssl verify -CAfile ca.crt client.crt + shell: bash + - name: Run tests working-directory: build run: ./brazier_tests @@ -195,7 +223,7 @@ jobs: cp config_test.json ${{ matrix.build_type }}/ shell: bash - - name: Generate self-signed certificate (Windows) + - name: Generate self-signed certificate (main server) shell: pwsh run: | New-Item -ItemType Directory -Force -Path "build/brazier/app/certs" | Out-Null @@ -205,6 +233,30 @@ jobs: -subj "/CN=localhost" ` -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" + - name: Generate mTLS certificates (CA + client) + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path "build/certs" | Out-Null + Push-Location build/certs + + openssl genrsa -out ca.key 4096 + openssl req -x509 -new -nodes -key ca.key -sha256 -days 1825 -out ca.crt -subj "/CN=Test CA" + + openssl genrsa -out server.key 2048 + openssl req -new -key server.key -out server.csr -subj "/CN=localhost" + Set-Content -Path server_ext.cnf -Value "subjectAltName=DNS:localhost,IP:127.0.0.1" + openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365 -sha256 -extfile server_ext.cnf + + openssl genrsa -out client.key 2048 + openssl req -new -key client.key -out client.csr -subj "/CN=brazier-client" + Set-Content -Path client_ext.cnf -Value "extendedKeyUsage=clientAuth" + openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAserial ca.srl -out client.crt -days 365 -sha256 -extfile client_ext.cnf + + openssl verify -CAfile ca.crt server.crt + openssl verify -CAfile ca.crt client.crt + + Pop-Location + - name: Run tests working-directory: build run: ./${{ matrix.build_type }}/brazier_tests.exe @@ -283,14 +335,44 @@ jobs: cp config_test.json ${{ matrix.build_type }}/ shell: bash - - name: Generate self-signed certificate + - name: Generate self-signed certificate (main server) run: | mkdir -p build/brazier/app/certs + cd build/brazier/app/certs + echo "subjectAltName=DNS:localhost,IP:127.0.0.1" > server_ext.cnf openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \ - -keyout build/brazier/app/certs/server.key \ - -out build/brazier/app/certs/server.crt \ + -keyout server.key -out server.crt \ -subj "/CN=localhost" \ - -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" + -extensions v3_req \ + -config <(cat /etc/ssl/openssl.cnf; echo "[v3_req]"; echo "subjectAltName=DNS:localhost,IP:127.0.0.1") + shell: bash + + - name: Generate mTLS certificates (CA + client) + run: | + set -e + mkdir -p build/certs + cd build/certs + + openssl genrsa -out ca.key 4096 + openssl req -x509 -new -nodes -key ca.key -sha256 -days 1825 \ + -out ca.crt -subj "/CN=Test CA" + + openssl genrsa -out server.key 2048 + openssl req -new -key server.key -out server.csr -subj "/CN=localhost" + echo "subjectAltName=DNS:localhost,IP:127.0.0.1" > server_ext.cnf + openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \ + -CAcreateserial -out server.crt -days 365 -sha256 \ + -extfile server_ext.cnf + + openssl genrsa -out client.key 2048 + openssl req -new -key client.key -out client.csr -subj "/CN=brazier-client" + echo "extendedKeyUsage=clientAuth" > client_ext.cnf + openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key \ + -CAserial ca.srl -out client.crt -days 365 -sha256 \ + -extfile client_ext.cnf + + openssl verify -CAfile ca.crt server.crt + openssl verify -CAfile ca.crt client.crt shell: bash - name: Run tests diff --git a/README.md b/README.md index 1db4ac0..4c7607d 100644 --- a/README.md +++ b/README.md @@ -495,14 +495,21 @@ both — is a matter of a couple of lines in your `main`. ### Features - **TLS 1.2 / 1.3** by default, with per-server override via `conf` +- **Session resumption** via RFC 5077 tickets with automatic key rotation +- **Hot-reload** of certificates without restarting the server - **SNI-aware** (Server Name Indication) — future multi-cert scenarios are possible - **HSTS** header sent automatically on every response - **Keep-alive** over TLS with configurable idle timeout - **Graceful shutdown** with `close_notify` and 5-second shutdown timeout - **Handshake timeout** to protect against Slowloris-style attacks - **mTLS** (mutual TLS / client certificates) with custom CA -- **Raw OpenSSL tuning** through `SSL_CONF_cmd` — no code changes needed for cipher / protocol changes -- **Same Router / Engine / Middleware** as the HTTP server — routing code is transport-agnostic +- **Multi-io_context** — N worker threads, one per CPU core +- **`SO_REUSEPORT`** on Linux/macOS, round-robin dispatch on Windows +- **`TCP_DEFER_ACCEPT`** on Linux — less wake-ups, more throughput +- **Dynamic limits** — body size, header size, connection count auto-tuned to hardware +- **Raw OpenSSL tuning** through `SSL_CONF_cmd` — no code changes for cipher / protocol changes +- **Certificate from file OR from memory (PEM string)** — for Vault, K8s secrets, etc. +- **Same Router / Engine / Middleware** as the HTTP server — routing is transport-agnostic ### Requirements @@ -533,6 +540,12 @@ Add an `https_server` section to your `config.json` alongside the existing `serv "handshake_timeout": 15, "conf": [] } + }, + "http": { + "keep_alive_timeout": 60, + "max_connections": 50000, + "max_body_size": 1048576, + "max_header_size": 8192 } } ``` @@ -543,6 +556,8 @@ Add an `https_server` section to your `config.json` alongside the existing `serv |------------------------|-----------|-----------|-------------| | `cert_file` | `string` | `server.crt` | Path to PEM certificate chain (leaf + intermediates) | | `key_file` | `string` | `server.key` | Path to PEM private key | +| `cert_pem` | `string` | `""` | Certificate as in-memory PEM string (overrides `cert_file`) | +| `key_pem` | `string` | `""` | Private key as in-memory PEM string (overrides `key_file`) | | `ca_file` | `string` | `""` | Path to CA bundle — only needed for mTLS | | `require_client_cert` | `bool` | `false` | Reject connections without a client certificate | | `verify_client_cert` | `bool` | `false` | Verify client certificate if presented (but don't require) | @@ -554,6 +569,29 @@ Add an `https_server` section to your `config.json` alongside the existing `serv > from the project root, or use absolute paths. On Windows, always use forward slashes > (`"C:/certs/server.crt"`) — backslashes are escape characters in JSON. +#### HTTP section reference + +Global HTTP limits used by both HTTP and HTTPS servers. + +| Key | Type | Default | Description | +|------------------------|-----------|--------------|-------------| +| `keep_alive_timeout` | `int` | `60` | Seconds to keep an idle connection open (also accepts legacy `keep-alive-timeout` at top level) | +| `max_connections` | `int` | `ulimit×0.8` | Max simultaneous connections; if not set, derived from `RLIMIT_NOFILE` | +| `max_body_size` | `int` | auto | Max request body in bytes; auto-derived from RAM and `max_connections` if not set | +| `max_header_size` | `int` | auto | Max total request header size; auto-derived from RAM and `max_connections` | +| `max_connections_testing` | `int` | `0` | Test-only override for `max_connections` | +| `max_body_size_testing` | `int` | `0` | Test-only override for `max_body_size` | +| `max_header_size_testing` | `int` | `0` | Test-only override for `max_header_size` | + +**Auto-derived limits** work like this: + +- `max_connections` = `RLIMIT_NOFILE × 0.8` (POSIX) or `16384 × 0.8` (Windows) +- `max_body_size` = `(RAM × 25%) / max_connections`, clamped to `[64 KB, 16 MB]` +- `max_header_size` = `(RAM × 1%) / max_connections`, clamped to `[4 KB, 32 KB]` + +Any explicit value in the config overrides the auto-derivation. The `_testing` keys take +highest priority and are only meant for the test suite. + ### Generating certificates #### Development (self-signed) @@ -569,6 +607,16 @@ openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \ -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" ``` +For an ECDSA P-256 certificate (smaller, faster handshake — recommended for internal services): + +```bash +openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -days 365 -nodes \ + -keyout app/certs/server.key \ + -out app/certs/server.crt \ + -subj "/CN=localhost" \ + -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" +``` + On **Windows `cmd.exe`** the same command must be on a single line (no `\` continuation) — use `^` for line continuation, or paste it as one long line. @@ -657,9 +705,6 @@ instances with different certificates: brazier::HttpsServer::TlsConfig tls; tls.cert_file = "/var/lib/myapp/api.crt"; tls.key_file = "/var/lib/myapp/api.key"; -tls.conf = { - { "min_protocol", "TLSv1.3" } -}; tls.handshake_timeout = std::chrono::seconds(10); brazier::HttpsServer api("0.0.0.0", 9443, tls); @@ -670,6 +715,145 @@ api.run(); When `TlsConfig` is passed explicitly, `https_server.tls.*` from the JSON is ignored entirely — the code-level config wins. +### Loading certificates from memory (PEM strings) + +If your certificate comes from a secret manager, an environment variable, or any source +other than a local file, populate `cert_pem` / `key_pem` instead of `cert_file` / `key_file`: + +```cpp +brazier::HttpsServer::TlsConfig tls; +tls.cert_pem = std::getenv("TLS_CERT_PEM"); // full PEM chain +tls.key_pem = std::getenv("TLS_KEY_PEM"); // PEM private key + +brazier::HttpsServer server("0.0.0.0", 8443, tls); +server.initialize(); +``` + +`cert_pem` may contain **the full chain** (leaf + intermediates) — Brazier parses and +registers each certificate automatically. `key_pem` must be the matching private key; +the pair is validated with `SSL_CTX_check_private_key` at load time. + +> **`cert_pem` takes priority over `cert_file`.** If both are set, the memory copy wins. +> This lets you supply a file path for hot-reload **and** a cached PEM for the initial +> load — but note that `reloadTls()` needs `cert_file`/`key_file` to be set to know where +> to re-read from. + +### Hot-reload of TLS certificates + +Reload certificates without restarting the server or dropping existing connections: + +```cpp +// Re-read files (cert_file / key_file must be set) +server.reloadTls(); + +// Or explicitly supply a new config — useful for Vault / K8s / DB sources +brazier::HttpsServer::TlsConfig new_tls = server.getTlsConfig(); +new_tls.cert_pem = fetchPemFromVault("tls/server.crt"); +new_tls.key_pem = fetchPemFromVault("tls/server.key"); +server.reloadTls(new_tls); +``` + +**Guarantees:** + +- Existing connections continue on the **old** `SSL_CTX` and finish normally. +- New handshakes use the **new** `SSL_CTX`. +- If the new config is invalid, the operation fails and the **old** `SSL_CTX` stays + active — the server is never left in a broken state. +- Session ticket keys (`TicketKeyStore`) are shared across contexts, so resumption + continues to work across reloads. + +**Common patterns:** + +```cpp +// 1. Console command +std::thread([&server] { + std::string line; + while (std::getline(std::cin, line)) { + if (line == "reload") server.reloadTls(); + if (line == "quit") server.stop(); + } +}).detach(); + +// 2. SIGHUP (Linux / macOS) +std::signal(SIGHUP, [](int) { + g_reload_requested.store(true, std::memory_order_release); +}); +// ... in a background thread ... +if (g_reload_requested.exchange(false)) server.reloadTls(); + +// 3. File watcher +std::thread([&server] { + fs::file_time_type last{}; + while (server.running()) { + auto now = fs::last_write_time("app/certs/server.crt"); + if (last != fs::file_time_type{} && now != last) { + server.reloadTls(); + } + last = now; + std::this_thread::sleep_for(std::chrono::seconds(5)); + } +}).detach(); +``` + +### Session resumption + +Brazier uses **stateless session tickets** (RFC 5077) — no per-session state is kept on +the server. This is the only mechanism that works in TLS 1.3, and it's the recommended +mechanism for TLS 1.2 too. + +Tickets are encrypted with rotating keys managed by `TicketKeyStore`: + +- **Rotation interval** — a new key every 12 hours. +- **Key lifetime** — old keys kept for 48 hours, so clients with valid tickets can still + resume. +- **Thread-safe** — one store per `HttpsServer`; multiple servers in the same process + have independent stores. + +Resumption typically costs **0.3–0.8 ms** vs **2–3 ms** for a full handshake — roughly a +10× CPU reduction on resumed sessions. This matters most for connection-churn workloads +(REST APIs behind a proxy, health checks). + + +### Threading model + +On startup, `HttpsServer` creates **N io_context instances, one per CPU core**: + +| Platform | io_contexts | Acceptors | Dispatch | +|---|---|---|---| +| Linux / macOS | `hardware_concurrency()` | Same as io_contexts | `SO_REUSEPORT` — kernel hashes 4-tuples | +| Windows | `hardware_concurrency()` | 1 | round-robin `co_spawn` from a single acceptor | + +Each io_context has its own thread. Each thread has its own IOCP (Windows) or epoll +instance (Linux). There is **no cross-thread signalling** on the hot path — completions +for a connection always run on the same thread that owns its io_context. + +The startup log tells you exactly what happened: + +``` +[SUCCESS] HTTPS server initialized on 0.0.0.0:8443 [TLS, io_contexts=12, acceptors=1, dispatch=round-robin, SO_REUSEPORT=no, TCP_DEFER_ACCEPT=no] +[INFO] Starting 12 io_context(s) over 12 thread(s), dispatch=round-robin +``` + +On Linux expect `io_contexts=N, acceptors=N, dispatch=SO_REUSEPORT`. On Windows expect +`io_contexts=N, acceptors=1, dispatch=round-robin`. + +### Dynamic limits + +Body size, header size, and connection count are all auto-tuned to the host hardware at +startup, unless overridden in `config.json`: + +``` +[WARNING] [TESTING] Final HTTP limits: max_connections=20, max_body=1024KB, max_header=8KB (RAM=15611MB) +[INFO] Final HTTP limits: max_connections=50000, max_body=2097152KB, max_header=16KB (RAM=16384MB) +``` + +Auto-derivation uses `RLIMIT_NOFILE` for connection count and total RAM for body/header +caps. If you set any of `http.max_connections`, `http.max_body_size`, or +`http.max_header_size` explicitly, that value wins. + +The `_testing` variants take absolute priority — they exist so the test suite can enforce +small limits without touching the prod config. + ### Mutual TLS (mTLS) mTLS requires the client to present a certificate signed by a CA you trust. This is common @@ -729,12 +913,31 @@ You don't need to set these in your controllers. | Phase | Timeout | Config key | |---|---|---| | TLS handshake | 15 s | `https_server.tls.handshake_timeout` | -| Idle keep-alive | 60 s | `keep-alive-timeout` (top-level) | +| Idle keep-alive | 60 s | `http.keep_alive_timeout` (or legacy `keep-alive-timeout`) | | Graceful TLS shutdown | 5 s | — | +| Graceful server shutdown | 10 s | — | If any of these fire, the connection is closed cleanly — you'll see a corresponding `[DEBUG] HTTPS client disconnected` line in the log, not an error. +### Shutting down + +```cpp +server.stop(); // returns after all in-flight connections complete (up to 10 s) +``` + +`stop()` performs a graceful shutdown: + +1. Acceptors closed — no new connections. +2. `work_guard` released — `io_context::run()` will exit when idle. +3. Waits up to **10 seconds** for `connection_count` to reach zero. +4. `io_context::stop()` — force-cancels anything still running. +5. Worker threads joined. + +If `run()` is executing on a different thread, join that thread **after** `stop()` returns. +Do **not** call `stop()` from inside a request handler — it will deadlock waiting for +the calling connection to close. + ### Testing TLS From the command line: @@ -753,6 +956,11 @@ openssl s_client -connect localhost:8443 -servername localhost openssl s_client -connect localhost:8443 -tls1_1 ``` +> **Windows `curl.exe` uses Schannel and does not support ECDSA server certificates.** +> If your cert is ECDSA P-256, use `Git for Windows`'s curl (which links against OpenSSL), +> or stick to `openssl s_client` for testing. `ab` (ApacheBench) uses its own OpenSSL and +> works with both cert types. + From brazier's own test suite: ```cpp @@ -775,11 +983,20 @@ net::awaitable fetch_secure() { relative to the process's *current working directory*, not the config file. Run from the project root or use absolute paths. +- **`Certificate/private key mismatch`** — the cert and key in `cert_file` / `key_file` + don't belong to the same pair, or the key was regenerated without regenerating the cert. + Brazier rejects this at startup with a clear error. + - **`SSL_CONF_cmd failed for 'min_protocol=...'`** — some OpenSSL builds (particularly via vcpkg) don't register `min_protocol` in `SSL_CONF_cmd`. Brazier handles this internally by calling `SSL_CTX_set_min_proto_version` directly — the `conf` entry is a no-op there, but you can safely keep it for documentation purposes. +- **`no shared cipher` / `alert 40` on handshake** — the client and server can't agree on + a cipher suite. Most often: the client is offering only RSA suites while your cert is ECDSA + (or vice versa). Check the certificate type with + `openssl x509 -in cert.crt -noout -text | findstr "Public Key Algorithm"`. + - **Browser says "certificate not trusted"** — that's expected for self-signed certificates. Either click through the warning, add the cert to the user root store (see above), or use a real certificate from Let's Encrypt. @@ -787,6 +1004,14 @@ net::awaitable fetch_secure() { - **`WSAECONNRESET` / `WSAECONNABORTED` in logs** — these are normal client disconnect events, not errors. Brazier logs them at `DEBUG` level. +- **`TLS shutdown error` on every connection** — this is `APPLICATION_DATA_AFTER_CLOSE_NOTIFY`, + a benign artefact of clients that send data after the shutdown alert. It's logged at + `DEBUG` and can be ignored. + +- **Hot-reload says `reloadTls: source is PEM, nothing to reload`** — you supplied the + certificate only as `cert_pem` / `key_pem`, with no `cert_file` / `key_file`. Set the file + paths too, or call `reloadTls(new_tls)` with fresh PEM strings from your secret source. + ## Brazier WebSocket Routing System Brazier provides a complete WebSocket routing system with support for parameterized paths, multiple message types, and global handlers. The system integrates seamlessly with the existing HTTP router. diff --git a/brazier/app/Main.cpp b/brazier/app/Main.cpp index 0e380a1..88fb99c 100644 --- a/brazier/app/Main.cpp +++ b/brazier/app/Main.cpp @@ -41,9 +41,24 @@ int main() { brazier::Logger::log("HTTPS server: " + https_server_host + ":" + std::to_string(https_server_port), "INFO"); - brazier::HttpsServer https_server(https_server_host, https_server_port); + brazier::HttpsServer::TlsConfig tls; + tls.cert_file = "app/certs/server.crt"; + tls.key_file = "app/certs/server.key"; + brazier::HttpsServer https_server(https_server_host, https_server_port, tls); if (!https_server.initialize()) return 1; + std::thread([&https_server] { + std::string line; + while (std::getline(std::cin, line)) { + if (line == "reload") { + https_server.reloadTls(); + } + else if (line == "quit") { + https_server.stop(); + break; + } + } + }).detach(); https_server.run(); return 0; diff --git a/brazier/include/brazier/HttpsServer.hpp b/brazier/include/brazier/HttpsServer.hpp index d70613f..c6300a6 100644 --- a/brazier/include/brazier/HttpsServer.hpp +++ b/brazier/include/brazier/HttpsServer.hpp @@ -37,16 +37,19 @@ #include #include +#include #include #include #include #include #include +#include #include #include #include #include +#include #include "TLS/TicketKeyStore.hpp" #include "vendor/Handlers/ENV.hpp" @@ -74,6 +77,8 @@ namespace brazier { std::string cert_file; std::string key_file; std::string ca_file; + std::string cert_pem; + std::string key_pem; std::vector> conf; @@ -95,7 +100,8 @@ namespace brazier { std::vector>> work_guards_; - ssl::context ssl_ctx_{ ssl::context::tls_server }; + std::shared_ptr ssl_ctx_; + std::shared_mutex ssl_ctx_mutex_; std::thread stats_thread_; std::atomic shutdown_flag_{ false }; @@ -135,6 +141,9 @@ namespace brazier { void setTlsConfig(const TlsConfig& tls); + bool reloadTls(); + bool reloadTls(const TlsConfig& new_tls); + bool initialize(); void run(); @@ -154,8 +163,14 @@ namespace brazier { void load_tls_config_from_global(); void load_limits_from_config(); - void configure_tls(); - void apply_ssl_conf(); + void configure_tls(); + void configure_ssl_ctx(ssl::context& ctx); + void apply_ssl_conf(ssl::context& ctx); + void load_cert_from_memory(ssl::context& ctx, + const std::string& cert_pem, + const std::string& key_pem); + + std::shared_ptr get_ssl_ctx(); void release_connection(); diff --git a/brazier/src/HttpsServer.cpp b/brazier/src/HttpsServer.cpp index b2d1dfe..37c9346 100644 --- a/brazier/src/HttpsServer.cpp +++ b/brazier/src/HttpsServer.cpp @@ -77,6 +77,11 @@ void brazier::HttpsServer::load_common_config_from_global() { void brazier::HttpsServer::load_tls_config_from_global() { if (tls_config_from_user_) return; + tls_.cert_pem = global_config->get("https_server.tls.cert_pem", + std::string("")); + tls_.key_pem = global_config->get("https_server.tls.key_pem", + std::string("")); + tls_.cert_file = global_config->get("https_server.tls.cert_file", std::string("server.crt")); tls_.key_file = global_config->get("https_server.tls.key_file", @@ -113,6 +118,182 @@ void brazier::HttpsServer::load_tls_config_from_global() { } } +void brazier::HttpsServer::load_cert_from_memory( + ssl::context& ctx, + const std::string& cert_pem, const std::string& key_pem) +{ + if (cert_pem.empty() || key_pem.empty()) { + throw std::runtime_error("load_cert_from_memory: empty PEM"); + } + + BIO* cert_bio = BIO_new_mem_buf(cert_pem.data(), + static_cast(cert_pem.size())); + if (!cert_bio) { + throw std::runtime_error("BIO_new_mem_buf (cert) failed"); + } + + X509* leaf = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr); + if (!leaf) { + BIO_free(cert_bio); + throw std::runtime_error("PEM_read_bio_X509 failed: " + + std::string(ERR_error_string(ERR_get_error(), nullptr))); + } + + if (SSL_CTX_use_certificate(ctx.native_handle(), leaf) != 1) { + X509_free(leaf); + BIO_free(cert_bio); + throw std::runtime_error("SSL_CTX_use_certificate failed"); + } + X509_free(leaf); + + X509* chain_cert = nullptr; + while ((chain_cert = PEM_read_bio_X509(cert_bio, nullptr, + nullptr, nullptr)) != nullptr) { + if (SSL_CTX_add_extra_chain_cert(ctx.native_handle(), chain_cert) != 1) { + X509_free(chain_cert); + BIO_free(cert_bio); + throw std::runtime_error("SSL_CTX_add_extra_chain_cert failed"); + } + } + BIO_free(cert_bio); + + BIO* key_bio = BIO_new_mem_buf(key_pem.data(), + static_cast(key_pem.size())); + if (!key_bio) { + throw std::runtime_error("BIO_new_mem_buf (key) failed"); + } + + EVP_PKEY* pkey = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr, nullptr); + BIO_free(key_bio); + + if (!pkey) { + throw std::runtime_error("PEM_read_bio_PrivateKey failed: " + + std::string(ERR_error_string(ERR_get_error(), nullptr))); + } + + if (SSL_CTX_use_PrivateKey(ctx.native_handle(), pkey) != 1) { + EVP_PKEY_free(pkey); + throw std::runtime_error("SSL_CTX_use_PrivateKey failed"); + } + EVP_PKEY_free(pkey); + + if (SSL_CTX_check_private_key(ctx.native_handle()) != 1) { + throw std::runtime_error("Certificate and private key do not match"); + } + + Logger::log("TLS certificate loaded from PEM (in-memory, with chain)", + "INFO"); +} + +void brazier::HttpsServer::configure_ssl_ctx(ssl::context& ctx) { + ctx.set_options( + ssl::context::default_workarounds + | ssl::context::no_sslv2 + | ssl::context::no_sslv3 + | ssl::context::no_tlsv1 + | ssl::context::no_tlsv1_1 + | ssl::context::single_dh_use); + + SSL_CTX_set_options(ctx.native_handle(), SSL_OP_IGNORE_UNEXPECTED_EOF); + + SSL_CTX_set_session_cache_mode( + ctx.native_handle(), + SSL_SESS_CACHE_OFF); + + ticket_store_.ensure_initialized(); + ticket_store_.attach_to(ctx.native_handle()); + + apply_ssl_conf(ctx); + + if (!tls_.cert_pem.empty() && !tls_.key_pem.empty()) { + load_cert_from_memory(ctx, tls_.cert_pem, tls_.key_pem); + } + else { + ctx.use_certificate_chain_file(tls_.cert_file); + ctx.use_private_key_file(tls_.key_file, ssl::context::pem); + + if (SSL_CTX_check_private_key(ctx.native_handle()) != 1) { + throw std::runtime_error( + "Certificate/private key mismatch: " + + tls_.cert_file + " / " + tls_.key_file); + } + } + + if (tls_.require_client_cert || tls_.verify_client_cert) { + if (!tls_.ca_file.empty()) { + ctx.load_verify_file(tls_.ca_file); + } + auto mode = ssl::verify_peer; + if (tls_.require_client_cert) { + mode |= ssl::verify_fail_if_no_peer_cert; + } + ctx.set_verify_mode(mode); + } + else { + ctx.set_verify_mode(ssl::verify_none); + } +} + +std::shared_ptr brazier::HttpsServer::get_ssl_ctx() { + std::shared_lock lock(ssl_ctx_mutex_); + return ssl_ctx_; +} + +bool brazier::HttpsServer::reloadTls() { + if (!tls_.cert_file.empty() && !tls_.key_file.empty()) { + std::ifstream cf(tls_.cert_file, std::ios::binary); + std::ifstream kf(tls_.key_file, std::ios::binary); + + if (!cf || !kf) { + Logger::log("reloadTls: cannot open files: " + + tls_.cert_file + " / " + tls_.key_file, "ERROR"); + return false; + } + + TlsConfig new_tls = tls_; + new_tls.cert_pem = std::string( + std::istreambuf_iterator(cf), {}); + new_tls.key_pem = std::string( + std::istreambuf_iterator(kf), {}); + + return reloadTls(new_tls); + } + + Logger::log("reloadTls: no file paths configured, " + "use reloadTls(new_tls) with fresh PEM", "WARNING"); + return false; +} + +bool brazier::HttpsServer::reloadTls(const TlsConfig& new_tls) { + try { + auto new_ctx = std::make_shared(ssl::context::tls_server); + + TlsConfig saved = tls_; + tls_ = new_tls; + + try { + configure_ssl_ctx(*new_ctx); + } + catch (...) { + tls_ = saved; + throw; + } + + { + std::unique_lock lock(ssl_ctx_mutex_); + ssl_ctx_ = new_ctx; + } + + Logger::log("TLS reloaded successfully (new SSL_CTX active)", "SUCCESS"); + return true; + } + catch (const std::exception& e) { + Logger::log("TLS reload failed: " + std::string(e.what()) + + " (keeping old SSL_CTX)", "ERROR"); + return false; + } +} + void brazier::HttpsServer::load_limits_from_config() { const int ram_mb = platform::get_system_memory_mb(); @@ -164,7 +345,7 @@ void brazier::HttpsServer::load_limits_from_config() { testing_mode ? "WARNING" : "INFO"); } -void brazier::HttpsServer::apply_ssl_conf() { +void brazier::HttpsServer::apply_ssl_conf(ssl::context& ctx) { if (tls_.conf.empty()) return; SSL_CONF_CTX* cctx = SSL_CONF_CTX_new(); @@ -173,7 +354,7 @@ void brazier::HttpsServer::apply_ssl_conf() { } SSL_CONF_CTX_set_flags(cctx, SSL_CONF_FLAG_SERVER | SSL_CONF_FLAG_CERTIFICATE); - SSL_CONF_CTX_set_ssl_ctx(cctx, ssl_ctx_.native_handle()); + SSL_CONF_CTX_set_ssl_ctx(cctx, ctx.native_handle()); for (const auto& [cmd, val] : tls_.conf) { int rv = val.empty() @@ -197,41 +378,10 @@ void brazier::HttpsServer::apply_ssl_conf() { } void brazier::HttpsServer::configure_tls() { - ssl_ctx_.set_options( - ssl::context::default_workarounds - | ssl::context::no_sslv2 - | ssl::context::no_sslv3 - | ssl::context::no_tlsv1 - | ssl::context::no_tlsv1_1 - | ssl::context::single_dh_use); - - SSL_CTX_set_options(ssl_ctx_.native_handle(), SSL_OP_IGNORE_UNEXPECTED_EOF); - - SSL_CTX_set_session_cache_mode( - ssl_ctx_.native_handle(), - SSL_SESS_CACHE_OFF); - - ticket_store_.ensure_initialized(); - ticket_store_.attach_to(ssl_ctx_.native_handle()); - - apply_ssl_conf(); - - ssl_ctx_.use_certificate_chain_file(tls_.cert_file); - ssl_ctx_.use_private_key_file(tls_.key_file, ssl::context::pem); - - if (tls_.require_client_cert || tls_.verify_client_cert) { - if (!tls_.ca_file.empty()) { - ssl_ctx_.load_verify_file(tls_.ca_file); - } - auto mode = ssl::verify_peer; - if (tls_.require_client_cert) { - mode |= ssl::verify_fail_if_no_peer_cert; - } - ssl_ctx_.set_verify_mode(mode); - } - else { - ssl_ctx_.set_verify_mode(ssl::verify_none); + if (!ssl_ctx_) { + throw std::runtime_error("configure_tls: ssl_ctx_ not initialized"); } + configure_ssl_ctx(*ssl_ctx_); } bool brazier::HttpsServer::initialize() { @@ -260,6 +410,8 @@ bool brazier::HttpsServer::initialize() { load_common_config_from_global(); load_tls_config_from_global(); load_limits_from_config(); + + ssl_ctx_ = std::make_shared(ssl::context::tls_server); configure_tls(); const tcp::endpoint endpoint(net::ip::make_address(host_), port_); @@ -562,7 +714,8 @@ net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) socket.set_option(tcp::no_delay(true)); socket.set_option(boost::asio::socket_base::keep_alive(true)); - ssl::stream stream(std::move(socket), ssl_ctx_); + auto ctx = get_ssl_ctx(); + ssl::stream stream(std::move(socket), *ctx); { net::steady_timer hs_timer(co_await net::this_coro::executor); diff --git a/brazier/tests/unit/routing/https_routing_test.cpp b/brazier/tests/unit/routing/https_routing_test.cpp index dac9186..3bce8cf 100644 --- a/brazier/tests/unit/routing/https_routing_test.cpp +++ b/brazier/tests/unit/routing/https_routing_test.cpp @@ -61,7 +61,7 @@ namespace { constexpr int kRouteRegistrationDelayMs = 200; -} +} class TestController : public brazier::Controller { public: diff --git a/brazier/tests/unit/security/https_mtls_test.cpp b/brazier/tests/unit/security/https_mtls_test.cpp new file mode 100644 index 0000000..73e2bd4 --- /dev/null +++ b/brazier/tests/unit/security/https_mtls_test.cpp @@ -0,0 +1,250 @@ +/* + * 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 +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include "../../../include/brazier/Core" +#include "../../../include/brazier/Http" +#include "main.h" + +namespace beast = boost::beast; +namespace http = beast::http; +namespace net = boost::asio; +namespace ssl = net::ssl; +using tcp = net::ip::tcp; + +namespace { + + constexpr unsigned short kMtlsPort = 9443; + constexpr int kSocketTimeoutSec = 5; + + const std::string kCaCert = "certs/ca.crt"; + const std::string kServerCert = "certs/server.crt"; + const std::string kServerKey = "certs/server.key"; + const std::string kClientCert = "certs/client.crt"; + const std::string kClientKey = "certs/client.key"; + + bool FileExists(const std::string& path) { + std::ifstream f(path); + return f.good(); + } + + bool MtlsCertsPresent() { + return FileExists(kCaCert) && FileExists(kServerCert) && + FileExists(kServerKey) && FileExists(kClientCert) && + FileExists(kClientKey); + } + + struct MtlsClient { + net::io_context io; + ssl::context ctx; + std::unique_ptr> stream; + + explicit MtlsClient(const std::string& cert = "", + const std::string& key = "") + : ctx(ssl::context::tls_client) { + ctx.set_verify_mode(ssl::verify_none); + if (!cert.empty() && !key.empty()) { + ctx.use_certificate_chain_file(cert); + ctx.use_private_key_file(key, ssl::context::pem); + } + } + + bool connect(int timeout_sec = kSocketTimeoutSec) { + try { + stream = std::make_unique< + ssl::stream>(io, ctx); + + tcp::resolver resolver(io); + auto results = resolver.resolve( + "127.0.0.1", std::to_string(kMtlsPort)); + + beast::get_lowest_layer(*stream).expires_after( + std::chrono::seconds(timeout_sec)); + beast::get_lowest_layer(*stream).connect(results); + beast::get_lowest_layer(*stream).expires_never(); + + stream->handshake(ssl::stream_base::client); + return true; + } + catch (const std::exception&) { + return false; + } + } + + bool send_request(const std::string& target = "/test") { + try { + std::string req = + "GET " + target + " HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: close\r\n" + "\r\n"; + beast::get_lowest_layer(*stream).expires_after( + std::chrono::seconds(kSocketTimeoutSec)); + net::write(*stream, net::buffer(req)); + return true; + } + catch (const std::exception&) { + return false; + } + } + + std::optional> read_response( + int timeout_sec = kSocketTimeoutSec) { + try { + beast::flat_buffer buffer; + http::response res; + beast::get_lowest_layer(*stream).expires_after( + std::chrono::seconds(timeout_sec)); + http::read(*stream, buffer, res); + return res; + } + catch (const std::exception&) { + return std::nullopt; + } + } + }; + +} + +class HttpsMtlsTest : public ::testing::Test { +protected: + static std::unique_ptr server_; + static std::thread thread_; + + static void SetUpTestSuite() { + if (!MtlsCertsPresent()) { + return; + } + + brazier::HttpsServer::TlsConfig tls; + tls.cert_file = kServerCert; + tls.key_file = kServerKey; + tls.ca_file = kCaCert; + tls.require_client_cert = true; + tls.verify_client_cert = true; + tls.handshake_timeout = std::chrono::seconds(3); + + server_ = std::make_unique( + "127.0.0.1", kMtlsPort, tls); + + if (!server_->initialize()) { + server_.reset(); + return; + } + + thread_ = std::thread([] { server_->run(); }); + + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + } + + static void TearDownTestSuite() { + if (server_) { + server_->stop(); + if (thread_.joinable()) thread_.join(); + } + } + + void SetUp() override { + if (!server_) { + GTEST_SKIP() << "mTLS test server not started " + "(certs missing in certs/)"; + } + } +}; + +std::unique_ptr HttpsMtlsTest::server_; +std::thread HttpsMtlsTest::thread_; + +TEST_F(HttpsMtlsTest, RejectsClientWithoutCert) { + MtlsClient c; + + if (c.connect()) { + c.send_request("/test"); + auto res = c.read_response(3); + EXPECT_FALSE(res.has_value()) + << "Server responded to request from client without certificate"; + } +} + +TEST_F(HttpsMtlsTest, AcceptsClientWithValidCert) { + MtlsClient c(kClientCert, kClientKey); + + ASSERT_TRUE(c.connect()) + << "Server must accept handshake with valid client certificate"; + + X509* peer = SSL_get1_peer_certificate(c.stream->native_handle()); + ASSERT_NE(peer, nullptr) + << "Server did not present its own certificate"; + + char cn[256] = { 0 }; + X509_NAME* subj = X509_get_subject_name(peer); + X509_NAME_get_text_by_NID(subj, NID_commonName, cn, sizeof(cn)); + EXPECT_STREQ(cn, "localhost") + << "Unexpected server CN: " << cn; + + X509_free(peer); +} + +TEST_F(HttpsMtlsTest, RequestAfterMtlsHandshake) { + MtlsClient c(kClientCert, kClientKey); + ASSERT_TRUE(c.connect()); + ASSERT_TRUE(c.send_request("/test")); + + auto res = c.read_response(); + ASSERT_TRUE(res.has_value()) << "No response after mTLS handshake"; + EXPECT_EQ(res->result_int(), 200); +} + +TEST_F(HttpsMtlsTest, ManySequentialMtlsHandshakes) { + for (int i = 0; i < 10; ++i) { + MtlsClient c(kClientCert, kClientKey); + ASSERT_TRUE(c.connect()) << "Handshake #" << i << " failed"; + + ASSERT_TRUE(c.send_request("/test")); + auto res = c.read_response(); + ASSERT_TRUE(res.has_value()) << "No response #" << i; + EXPECT_EQ(res->result_int(), 200); + } +} + +TEST_F(HttpsMtlsTest, NoRequestWithoutClientCert) { + MtlsClient c; + + if (c.connect()) { + c.send_request("/test"); + auto res = c.read_response(3); + EXPECT_FALSE(res.has_value()); + } +} \ No newline at end of file diff --git a/brazier/tests/unit/security/https_security_test.cpp b/brazier/tests/unit/security/https_security_test.cpp index c61f458..7bb895b 100644 --- a/brazier/tests/unit/security/https_security_test.cpp +++ b/brazier/tests/unit/security/https_security_test.cpp @@ -202,29 +202,6 @@ TEST_F(HttpsSecurityTest, HandshakeWithoutClientCert) { } } -TEST_F(HttpsSecurityTest, HandshakeWithClientCert) { - if (!ServerRequiresClientCert()) { - GTEST_SKIP() << "Server does not require client cert (mTLS disabled)"; - } - if (!ClientCertExists(kClientCert, kClientKey)) { - GTEST_SKIP() << "Client cert not found at " << kClientCert; - } - - TlsClient c(true, kClientCert, kClientKey); - EXPECT_TRUE(c.connect()) - << "Server requires client cert, but handshake with cert failed"; -} - -TEST_F(HttpsSecurityTest, NoRequestWithoutClientCertWhenMtlsRequired) { - if (!ServerRequiresClientCert()) { - GTEST_SKIP() << "Server does not require client cert"; - } - - TlsClient c; - EXPECT_FALSE(c.connect()) - << "Handshake should have failed without client cert"; -} - TEST_F(HttpsSecurityTest, HandshakeTimeout) { if (kHandshakeTimeoutSec > kMaxTestableTimeoutSec) { GTEST_SKIP() << "Handshake timeout is " << kHandshakeTimeoutSec From 2d91a01e9bc57bb6db2b30f5fd00214a4989d816 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Thu, 24 Sep 2026 16:04:30 +0300 Subject: [PATCH 25/29] fix: github actions file --- .github/workflows/cmake-multi-platform.yml | 4 ++-- brazier/.gitignore | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index e8cfebf..2788750 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -223,7 +223,7 @@ jobs: cp config_test.json ${{ matrix.build_type }}/ shell: bash - - name: Generate self-signed certificate (main server) + - name: Generate self-signed certificate (main server) shell: pwsh run: | New-Item -ItemType Directory -Force -Path "build/brazier/app/certs" | Out-Null @@ -335,7 +335,7 @@ jobs: cp config_test.json ${{ matrix.build_type }}/ shell: bash - - name: Generate self-signed certificate (main server) + - name: Generate self-signed certificate (main server) run: | mkdir -p build/brazier/app/certs cd build/brazier/app/certs diff --git a/brazier/.gitignore b/brazier/.gitignore index 55bfc71..12e6d29 100644 --- a/brazier/.gitignore +++ b/brazier/.gitignore @@ -8,4 +8,5 @@ .clangd config.json certs/ -stress_logs/ \ No newline at end of file +stress_logs/ +*.pem \ No newline at end of file From 8f54738ee1006af6580a2c18934a9cf014b09cf8 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Thu, 24 Sep 2026 17:18:31 +0300 Subject: [PATCH 26/29] fix: static headers --- brazier/app/BenchmarkController.cpp | 14 -------------- brazier/app/BenchmarkController.hpp | 20 ++++++++++++++++++++ brazier/app/Main.cpp | 2 +- brazier/include/brazier/HttpsServer.hpp | 6 ++++++ brazier/src/HttpsServer.cpp | 22 +++++++++++++++++----- 5 files changed, 44 insertions(+), 20 deletions(-) delete mode 100644 brazier/app/BenchmarkController.cpp create mode 100644 brazier/app/BenchmarkController.hpp diff --git a/brazier/app/BenchmarkController.cpp b/brazier/app/BenchmarkController.cpp deleted file mode 100644 index 8061749..0000000 --- a/brazier/app/BenchmarkController.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "../include/brazier/Http" - -class BenchmarkController : public brazier::Controller { -public: - using Request = http::request; - using Response = http::response; - - boost::asio::awaitable test(const Request& req, Response& res, const Params& params) { - res.result(http::status::ok); - res.set(http::field::content_type, "text/plain"); - res.body() = ""; - co_return; - } -}; \ No newline at end of file diff --git a/brazier/app/BenchmarkController.hpp b/brazier/app/BenchmarkController.hpp new file mode 100644 index 0000000..724b5b1 --- /dev/null +++ b/brazier/app/BenchmarkController.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include "../include/brazier/Core" + +class BenchmarkController : public brazier::Controller { +public: + using Request = boost::beast::http::request; + using Response = boost::beast::http::response; + + boost::asio::awaitable test(const Request& req, + Response& res, + const brazier::Params& params) { + res.result(boost::beast::http::status::ok); + res.set(boost::beast::http::field::content_type, "text/plain"); + res.body() = ""; + co_return; + } +}; \ No newline at end of file diff --git a/brazier/app/Main.cpp b/brazier/app/Main.cpp index 88fb99c..89da2c1 100644 --- a/brazier/app/Main.cpp +++ b/brazier/app/Main.cpp @@ -22,7 +22,7 @@ #include "../include/brazier/DB" #include "../include/brazier/Http" #include "../include/brazier/Engine.hpp" -#include "BenchmarkController.cpp" +#include "BenchmarkController.hpp" using namespace brazier; diff --git a/brazier/include/brazier/HttpsServer.hpp b/brazier/include/brazier/HttpsServer.hpp index c6300a6..2a8072b 100644 --- a/brazier/include/brazier/HttpsServer.hpp +++ b/brazier/include/brazier/HttpsServer.hpp @@ -134,6 +134,12 @@ namespace brazier { ConnectionGuard& operator=(const ConnectionGuard&) = delete; }; + std::string server_name_ = "brazier"; + std::string hsts_header_ = "max-age=31536000"; + bool hsts_enabled_ = true; + + std::string static_headers_; + public: HttpsServer(const std::string& host, unsigned short port); HttpsServer(const std::string& host, unsigned short port, diff --git a/brazier/src/HttpsServer.cpp b/brazier/src/HttpsServer.cpp index 37c9346..109cca0 100644 --- a/brazier/src/HttpsServer.cpp +++ b/brazier/src/HttpsServer.cpp @@ -72,6 +72,18 @@ void brazier::HttpsServer::load_common_config_from_global() { keep_alive_timeout_ = std::chrono::seconds( global_config->get("http.keep_alive_timeout", global_config->get("keep-alive-timeout", 60))); + + server_name_ = global_config->get("http.server_name", + std::string("brazier")); + hsts_enabled_ = global_config->get("http.hsts_enabled", true); + hsts_header_ = global_config->get("http.hsts_header", + std::string("max-age=31536000")); + + static_headers_.clear(); + static_headers_ += "Server: " + server_name_ + "\r\n"; + if (hsts_enabled_) { + static_headers_ += "Strict-Transport-Security: " + hsts_header_ + "\r\n"; + } } void brazier::HttpsServer::load_tls_config_from_global() { @@ -813,7 +825,7 @@ net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) total_requests_.fetch_add(1, std::memory_order_relaxed); keep_alive = req.keep_alive(); - res.clear(); + res.clear(); res.version(req.version()); res.keep_alive(keep_alive); @@ -831,7 +843,7 @@ net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) res.prepare_payload(); std::string flat; - flat.reserve(512 + res.body().size()); + flat.reserve(256 + static_headers_.size() + res.body().size()); flat += "HTTP/1.1 "; flat += std::to_string(res.result_int()); @@ -839,8 +851,8 @@ net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) flat += res.reason(); flat += "\r\n"; - flat += "Server: brazier\r\n"; - flat += "Strict-Transport-Security: max-age=31536000\r\n"; + flat += static_headers_; + flat += "Connection: "; flat += keep_alive ? "keep-alive\r\n" : "close\r\n"; @@ -858,7 +870,7 @@ net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) stream, net::buffer(flat), net::redirect_error(net::use_awaitable, ec)); - if (ec) break;; + if (ec) break; buffer.consume(buffer.size()); if (!keep_alive) break; From 8a54876deaffdca3036dd37dbf43b20b292d9d00 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Thu, 24 Sep 2026 17:37:37 +0300 Subject: [PATCH 27/29] refactor: module structure of https server --- brazier/include/brazier/HttpsServer.hpp | 19 +- brazier/src/HttpsServer.cpp | 684 +----------------------- brazier/src/HttpsServerConfig.cpp | 201 +++++++ brazier/src/HttpsServerConnection.cpp | 319 +++++++++++ brazier/src/HttpsServerTLS.cpp | 244 +++++++++ 5 files changed, 781 insertions(+), 686 deletions(-) create mode 100644 brazier/src/HttpsServerConfig.cpp create mode 100644 brazier/src/HttpsServerConnection.cpp create mode 100644 brazier/src/HttpsServerTLS.cpp diff --git a/brazier/include/brazier/HttpsServer.hpp b/brazier/include/brazier/HttpsServer.hpp index 2a8072b..b26d772 100644 --- a/brazier/include/brazier/HttpsServer.hpp +++ b/brazier/include/brazier/HttpsServer.hpp @@ -51,6 +51,9 @@ #include #include +#include "Platform/SocketOptions.hpp" +#include "Platform/SystemInfo.hpp" + #include "TLS/TicketKeyStore.hpp" #include "vendor/Handlers/ENV.hpp" #include "Database/Queue.hpp" @@ -77,7 +80,7 @@ namespace brazier { std::string cert_file; std::string key_file; std::string ca_file; - std::string cert_pem; + std::string cert_pem; std::string key_pem; std::vector> conf; @@ -98,10 +101,10 @@ namespace brazier { std::vector> io_contexts_; std::vector> acceptors_; std::vector>> work_guards_; + net::executor_work_guard>> work_guards_; std::shared_ptr ssl_ctx_; - std::shared_mutex ssl_ctx_mutex_; + std::shared_mutex ssl_ctx_mutex_; std::thread stats_thread_; std::atomic shutdown_flag_{ false }; @@ -169,12 +172,12 @@ namespace brazier { void load_tls_config_from_global(); void load_limits_from_config(); - void configure_tls(); - void configure_ssl_ctx(ssl::context& ctx); - void apply_ssl_conf(ssl::context& ctx); + void configure_tls(); + void configure_ssl_ctx(ssl::context& ctx); + void apply_ssl_conf(ssl::context& ctx); void load_cert_from_memory(ssl::context& ctx, - const std::string& cert_pem, - const std::string& key_pem); + const std::string& cert_pem, + const std::string& key_pem); std::shared_ptr get_ssl_ctx(); diff --git a/brazier/src/HttpsServer.cpp b/brazier/src/HttpsServer.cpp index 109cca0..da7ce55 100644 --- a/brazier/src/HttpsServer.cpp +++ b/brazier/src/HttpsServer.cpp @@ -19,10 +19,6 @@ */ #include "../include/brazier/HttpsServer.hpp" -#include "brazier/Platform/SocketOptions.hpp" -#include "brazier/Platform/SystemInfo.hpp" - -#include brazier::HttpsServer::HttpsServer(const std::string& host, unsigned short port) : port_(port), host_(host) {} @@ -36,366 +32,6 @@ void brazier::HttpsServer::setTlsConfig(const TlsConfig& tls) { tls_config_from_user_ = true; } -int brazier::HttpsServer::compute_max_body_size(int ram_mb, int max_conn) { - constexpr int kBodyRamBudgetPct = 25; - constexpr int kMinBody = 64 * 1024; - constexpr int kMaxBodyCap = 16 * 1024 * 1024; - - if (max_conn <= 0) max_conn = 1; - - const std::int64_t ram_bytes = (std::int64_t)(ram_mb) * 1024 * 1024; - const std::int64_t budget = ram_bytes * kBodyRamBudgetPct / 100; - std::int64_t per_conn = budget / (std::int64_t)(max_conn); - - if (per_conn < kMinBody) per_conn = kMinBody; - if (per_conn > kMaxBodyCap) per_conn = kMaxBodyCap; - return static_cast(per_conn); -} - -int brazier::HttpsServer::compute_max_header_size(int ram_mb, int max_conn) { - constexpr int kHeaderRamBudgetPct = 1; - constexpr int kMinHeader = 4 * 1024; - constexpr int kMaxHeaderCap = 32 * 1024; - - if (max_conn <= 0) max_conn = 1; - - const std::int64_t ram_bytes = (std::int64_t)(ram_mb) * 1024 * 1024; - const std::int64_t budget = ram_bytes * kHeaderRamBudgetPct / 100; - std::int64_t per_conn = budget / (std::int64_t)(max_conn); - - if (per_conn < kMinHeader) per_conn = kMinHeader; - if (per_conn > kMaxHeaderCap) per_conn = kMaxHeaderCap; - return static_cast(per_conn); -} - -void brazier::HttpsServer::load_common_config_from_global() { - keep_alive_timeout_ = std::chrono::seconds( - global_config->get("http.keep_alive_timeout", - global_config->get("keep-alive-timeout", 60))); - - server_name_ = global_config->get("http.server_name", - std::string("brazier")); - hsts_enabled_ = global_config->get("http.hsts_enabled", true); - hsts_header_ = global_config->get("http.hsts_header", - std::string("max-age=31536000")); - - static_headers_.clear(); - static_headers_ += "Server: " + server_name_ + "\r\n"; - if (hsts_enabled_) { - static_headers_ += "Strict-Transport-Security: " + hsts_header_ + "\r\n"; - } -} - -void brazier::HttpsServer::load_tls_config_from_global() { - if (tls_config_from_user_) return; - - tls_.cert_pem = global_config->get("https_server.tls.cert_pem", - std::string("")); - tls_.key_pem = global_config->get("https_server.tls.key_pem", - std::string("")); - - tls_.cert_file = global_config->get("https_server.tls.cert_file", - std::string("server.crt")); - tls_.key_file = global_config->get("https_server.tls.key_file", - std::string("server.key")); - tls_.ca_file = global_config->get("https_server.tls.ca_file", - std::string("")); - - tls_.require_client_cert = - global_config->get("https_server.tls.require_client_cert", false); - tls_.verify_client_cert = - global_config->get("https_server.tls.verify_client_cert", false); - - tls_.handshake_timeout = std::chrono::seconds( - global_config->get("https_server.tls.handshake_timeout", 15)); - - try { - json conf = global_config->getJson("https_server.tls.conf"); - if (conf.is_array()) { - for (const auto& item : conf) { - if (!item.is_array() || item.empty() || item.size() > 2) { - throw std::runtime_error( - "https_server.tls.conf: each entry must be [command] " - "or [command, value]"); - } - std::string cmd = item[0].get(); - std::string val = item.size() > 1 ? item[1].get() : ""; - tls_.conf.emplace_back(std::move(cmd), std::move(val)); - } - } - } - catch (const std::exception& e) { - Logger::log("https_server.tls.conf not loaded: " + std::string(e.what()), - "WARNING"); - } -} - -void brazier::HttpsServer::load_cert_from_memory( - ssl::context& ctx, - const std::string& cert_pem, const std::string& key_pem) -{ - if (cert_pem.empty() || key_pem.empty()) { - throw std::runtime_error("load_cert_from_memory: empty PEM"); - } - - BIO* cert_bio = BIO_new_mem_buf(cert_pem.data(), - static_cast(cert_pem.size())); - if (!cert_bio) { - throw std::runtime_error("BIO_new_mem_buf (cert) failed"); - } - - X509* leaf = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr); - if (!leaf) { - BIO_free(cert_bio); - throw std::runtime_error("PEM_read_bio_X509 failed: " + - std::string(ERR_error_string(ERR_get_error(), nullptr))); - } - - if (SSL_CTX_use_certificate(ctx.native_handle(), leaf) != 1) { - X509_free(leaf); - BIO_free(cert_bio); - throw std::runtime_error("SSL_CTX_use_certificate failed"); - } - X509_free(leaf); - - X509* chain_cert = nullptr; - while ((chain_cert = PEM_read_bio_X509(cert_bio, nullptr, - nullptr, nullptr)) != nullptr) { - if (SSL_CTX_add_extra_chain_cert(ctx.native_handle(), chain_cert) != 1) { - X509_free(chain_cert); - BIO_free(cert_bio); - throw std::runtime_error("SSL_CTX_add_extra_chain_cert failed"); - } - } - BIO_free(cert_bio); - - BIO* key_bio = BIO_new_mem_buf(key_pem.data(), - static_cast(key_pem.size())); - if (!key_bio) { - throw std::runtime_error("BIO_new_mem_buf (key) failed"); - } - - EVP_PKEY* pkey = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr, nullptr); - BIO_free(key_bio); - - if (!pkey) { - throw std::runtime_error("PEM_read_bio_PrivateKey failed: " + - std::string(ERR_error_string(ERR_get_error(), nullptr))); - } - - if (SSL_CTX_use_PrivateKey(ctx.native_handle(), pkey) != 1) { - EVP_PKEY_free(pkey); - throw std::runtime_error("SSL_CTX_use_PrivateKey failed"); - } - EVP_PKEY_free(pkey); - - if (SSL_CTX_check_private_key(ctx.native_handle()) != 1) { - throw std::runtime_error("Certificate and private key do not match"); - } - - Logger::log("TLS certificate loaded from PEM (in-memory, with chain)", - "INFO"); -} - -void brazier::HttpsServer::configure_ssl_ctx(ssl::context& ctx) { - ctx.set_options( - ssl::context::default_workarounds - | ssl::context::no_sslv2 - | ssl::context::no_sslv3 - | ssl::context::no_tlsv1 - | ssl::context::no_tlsv1_1 - | ssl::context::single_dh_use); - - SSL_CTX_set_options(ctx.native_handle(), SSL_OP_IGNORE_UNEXPECTED_EOF); - - SSL_CTX_set_session_cache_mode( - ctx.native_handle(), - SSL_SESS_CACHE_OFF); - - ticket_store_.ensure_initialized(); - ticket_store_.attach_to(ctx.native_handle()); - - apply_ssl_conf(ctx); - - if (!tls_.cert_pem.empty() && !tls_.key_pem.empty()) { - load_cert_from_memory(ctx, tls_.cert_pem, tls_.key_pem); - } - else { - ctx.use_certificate_chain_file(tls_.cert_file); - ctx.use_private_key_file(tls_.key_file, ssl::context::pem); - - if (SSL_CTX_check_private_key(ctx.native_handle()) != 1) { - throw std::runtime_error( - "Certificate/private key mismatch: " + - tls_.cert_file + " / " + tls_.key_file); - } - } - - if (tls_.require_client_cert || tls_.verify_client_cert) { - if (!tls_.ca_file.empty()) { - ctx.load_verify_file(tls_.ca_file); - } - auto mode = ssl::verify_peer; - if (tls_.require_client_cert) { - mode |= ssl::verify_fail_if_no_peer_cert; - } - ctx.set_verify_mode(mode); - } - else { - ctx.set_verify_mode(ssl::verify_none); - } -} - -std::shared_ptr brazier::HttpsServer::get_ssl_ctx() { - std::shared_lock lock(ssl_ctx_mutex_); - return ssl_ctx_; -} - -bool brazier::HttpsServer::reloadTls() { - if (!tls_.cert_file.empty() && !tls_.key_file.empty()) { - std::ifstream cf(tls_.cert_file, std::ios::binary); - std::ifstream kf(tls_.key_file, std::ios::binary); - - if (!cf || !kf) { - Logger::log("reloadTls: cannot open files: " + - tls_.cert_file + " / " + tls_.key_file, "ERROR"); - return false; - } - - TlsConfig new_tls = tls_; - new_tls.cert_pem = std::string( - std::istreambuf_iterator(cf), {}); - new_tls.key_pem = std::string( - std::istreambuf_iterator(kf), {}); - - return reloadTls(new_tls); - } - - Logger::log("reloadTls: no file paths configured, " - "use reloadTls(new_tls) with fresh PEM", "WARNING"); - return false; -} - -bool brazier::HttpsServer::reloadTls(const TlsConfig& new_tls) { - try { - auto new_ctx = std::make_shared(ssl::context::tls_server); - - TlsConfig saved = tls_; - tls_ = new_tls; - - try { - configure_ssl_ctx(*new_ctx); - } - catch (...) { - tls_ = saved; - throw; - } - - { - std::unique_lock lock(ssl_ctx_mutex_); - ssl_ctx_ = new_ctx; - } - - Logger::log("TLS reloaded successfully (new SSL_CTX active)", "SUCCESS"); - return true; - } - catch (const std::exception& e) { - Logger::log("TLS reload failed: " + std::string(e.what()) + - " (keeping old SSL_CTX)", "ERROR"); - return false; - } -} - -void brazier::HttpsServer::load_limits_from_config() { - const int ram_mb = platform::get_system_memory_mb(); - - const int testing_conn = global_config->get("http.max_connections_testing", 0); - const int testing_body = global_config->get("http.max_body_size_testing", 0); - const int testing_hdr = global_config->get("http.max_header_size_testing", 0); - - const bool testing_mode = - testing_conn > 0 || testing_body > 0 || testing_hdr > 0; - - if (testing_conn > 0) { - max_connections_ = testing_conn; - } - else if (int v = global_config->get("http.max_connections", 0); v > 0) { - max_connections_ = v; - } - else { - const int fd_limit = platform::get_fd_limit(); - max_connections_ = fd_limit * 8 / 10; - } - - if (testing_body > 0) { - max_body_size_ = testing_body; - } - else if (int v = global_config->get("http.max_body_size", 0); v > 0) { - max_body_size_ = v; - } - else { - max_body_size_ = compute_max_body_size(ram_mb, max_connections_); - } - - if (testing_hdr > 0) { - max_header_size_ = testing_hdr; - } - else if (int v = global_config->get("http.max_header_size", 0); v > 0) { - max_header_size_ = v; - } - else { - max_header_size_ = compute_max_header_size(ram_mb, max_connections_); - } - - Logger::log( - std::string(testing_mode ? "[TESTING] " : "") + - "Final HTTP limits: max_connections=" + - std::to_string(max_connections_) + - ", max_body=" + std::to_string(max_body_size_ / 1024) + "KB" + - ", max_header=" + std::to_string(max_header_size_ / 1024) + "KB" + - " (RAM=" + std::to_string(ram_mb) + "MB)", - testing_mode ? "WARNING" : "INFO"); -} - -void brazier::HttpsServer::apply_ssl_conf(ssl::context& ctx) { - if (tls_.conf.empty()) return; - - SSL_CONF_CTX* cctx = SSL_CONF_CTX_new(); - if (!cctx) { - throw std::runtime_error("SSL_CONF_CTX_new failed"); - } - - SSL_CONF_CTX_set_flags(cctx, SSL_CONF_FLAG_SERVER | SSL_CONF_FLAG_CERTIFICATE); - SSL_CONF_CTX_set_ssl_ctx(cctx, ctx.native_handle()); - - for (const auto& [cmd, val] : tls_.conf) { - int rv = val.empty() - ? SSL_CONF_cmd(cctx, cmd.c_str(), nullptr) - : SSL_CONF_cmd(cctx, cmd.c_str(), val.c_str()); - - if (rv <= 0) { - SSL_CONF_CTX_free(cctx); - throw std::runtime_error( - "SSL_CONF_cmd failed for '" + cmd + - (val.empty() ? "'" : "=" + val + "'")); - } - } - - if (SSL_CONF_CTX_finish(cctx) != 1) { - SSL_CONF_CTX_free(cctx); - throw std::runtime_error("SSL_CONF_CTX_finish failed"); - } - - SSL_CONF_CTX_free(cctx); -} - -void brazier::HttpsServer::configure_tls() { - if (!ssl_ctx_) { - throw std::runtime_error("configure_tls: ssl_ctx_ not initialized"); - } - configure_ssl_ctx(*ssl_ctx_); -} - bool brazier::HttpsServer::initialize() { try { Logger::init("debug.log"); @@ -413,8 +49,8 @@ bool brazier::HttpsServer::initialize() { StorageManager::getInstance().registerDriver(name, driver); } - std::string def = global_config->getNested("filesystem.default", - "local"); + std::string def = global_config->getNested( + "filesystem.default", "local"); if (StorageManager::getInstance().hasDriver(def)) { StorageManager::getInstance().setDefaultDriver(def); } @@ -489,36 +125,9 @@ bool brazier::HttpsServer::initialize() { return true; } catch (const std::exception& e) { - Logger::log("HTTPS initialization failed: " + std::string(e.what()), "ERROR"); - return false; - } -} - -void brazier::HttpsServer::initializeConnections() { - try { - Queue::connect(global_config->get("nosql.host", "127.0.0.1"), - global_config->get("nosql.port", 6379)); - } - catch (const std::exception& e) { - Logger::log("Connection to queue failed: " + std::string(e.what()), "ERROR"); - } - - try { - Cache::connect(global_config->get("redis.host", "127.0.0.1"), - global_config->get("redis.port", 6379)); - } - catch (const std::exception& e) { - Logger::log("Connection to NOSQL database failed: " + std::string(e.what()), + Logger::log("HTTPS initialization failed: " + std::string(e.what()), "ERROR"); - } - - try { - Database db; - auto migrator = std::make_unique(db); - migrator->Initialize(); - } - catch (const std::exception& e) { - Logger::log("Database migration failed: " + std::string(e.what()), "ERROR"); + return false; } } @@ -571,7 +180,8 @@ void brazier::HttpsServer::run() { lock.unlock(); Logger::log("HTTPS STATS - Active connections: " + std::to_string(connection_count_.load()) + - ", Total requests: " + std::to_string(total_requests_.load()), + ", Total requests: " + + std::to_string(total_requests_.load()), "INFO"); lock.lock(); } @@ -591,49 +201,6 @@ void brazier::HttpsServer::run() { } } -net::awaitable brazier::HttpsServer::accept_and_dispatch( - tcp::acceptor& acceptor, int worker_begin) -{ - const int worker_count = static_cast(io_contexts_.size()) - worker_begin; - int next = 0; - - for (;;) { - if (shutting_down_.load(std::memory_order_acquire)) { - co_return; - } - - beast::error_code ec; - tcp::socket socket = co_await acceptor.async_accept( - net::redirect_error(net::use_awaitable, ec)); - - if (ec) { - if (ec == net::error::operation_aborted || - shutting_down_.load(std::memory_order_acquire)) { - co_return; - } - Logger::log("Accept error: " + ec.message(), "ERROR"); - continue; - } - - const int prev = connection_count_.fetch_add(1, std::memory_order_acq_rel); - if (prev >= max_connections_) { - connection_count_.fetch_sub(1, std::memory_order_acq_rel); - Logger::log("Connection limit reached (" + - std::to_string(prev) + "/" + - std::to_string(max_connections_) + "), rejecting", "WARNING"); - boost::system::error_code ignore; - socket.close(ignore); - continue; - } - - const int idx = worker_begin + (next++ % worker_count); - - net::co_spawn(*io_contexts_[idx], - handle_connection(std::move(socket)), - net::detached); - } -} - void brazier::HttpsServer::stop() { Logger::log("HTTPS server stopping (graceful)...", "INFO"); @@ -681,243 +248,4 @@ void brazier::HttpsServer::release_connection() { std::lock_guard lock(shutdown_mutex_); shutdown_cv_.notify_all(); } -} - -net::awaitable brazier::HttpsServer::accept_loop(tcp::acceptor& acceptor) { - for (;;) { - if (shutting_down_.load(std::memory_order_acquire)) { - co_return; - } - - beast::error_code ec; - tcp::socket socket = co_await acceptor.async_accept( - net::redirect_error(net::use_awaitable, ec)); - - if (ec) { - if (ec == net::error::operation_aborted || - shutting_down_.load(std::memory_order_acquire)) { - co_return; - } - Logger::log("Accept error: " + ec.message(), "ERROR"); - continue; - } - - const int prev = connection_count_.fetch_add(1, std::memory_order_acq_rel); - if (prev >= max_connections_) { - connection_count_.fetch_sub(1, std::memory_order_acq_rel); - Logger::log("Connection limit reached (" + - std::to_string(prev) + "/" + - std::to_string(max_connections_) + "), rejecting", "WARNING"); - boost::system::error_code ignore; - socket.close(ignore); - continue; - } - - net::co_spawn(acceptor.get_executor(), - handle_connection(std::move(socket)), - net::detached); - } -} - -net::awaitable brazier::HttpsServer::handle_connection(tcp::socket socket) { - ConnectionGuard guard(*this); - - try { - socket.set_option(tcp::no_delay(true)); - socket.set_option(boost::asio::socket_base::keep_alive(true)); - - auto ctx = get_ssl_ctx(); - ssl::stream stream(std::move(socket), *ctx); - - { - net::steady_timer hs_timer(co_await net::this_coro::executor); - hs_timer.expires_after(tls_.handshake_timeout); - hs_timer.async_wait([&](beast::error_code ec) { - if (!ec) { - beast::error_code ignore; - stream.next_layer().close(ignore); - } - }); - - beast::error_code hs_ec; - co_await stream.async_handshake( - ssl::stream_base::server, - net::redirect_error(net::use_awaitable, hs_ec)); - - hs_timer.cancel(); - - if (hs_ec) { - Logger::log("TLS handshake failed: " + hs_ec.message(), "WARNING"); - co_return; - } - } - - std::optional> parser; - - http::response res; - beast::flat_buffer buffer; - bool keep_alive = true; - - net::steady_timer idle_timer(co_await net::this_coro::executor); - const auto IDLE_TIMEOUT = keep_alive_timeout_; - bool timed_out = false; - - auto reset_timer = [&]() { - idle_timer.expires_after(IDLE_TIMEOUT); - idle_timer.async_wait([&](beast::error_code ec) { - if (!ec) { - timed_out = true; - beast::error_code ignore; - stream.next_layer().close(ignore); - } - }); - }; - - reset_timer(); - - while (keep_alive && !timed_out) { - parser.emplace(); - parser->body_limit(static_cast(max_body_size_)); - parser->header_limit(static_cast(max_header_size_)); - - beast::error_code ec; - - co_await http::async_read( - stream, buffer, *parser, - net::redirect_error(net::use_awaitable, ec)); - - if (ec == http::error::body_limit) { - http::response err{ - http::status::payload_too_large, 11 }; - err.set(http::field::content_type, "text/plain"); - err.set(http::field::connection, "close"); - err.body() = "Payload too large"; - err.prepare_payload(); - - beast::error_code write_ec; - co_await http::async_write( - stream, err, - net::redirect_error(net::use_awaitable, write_ec)); - break; - } - - if (ec == http::error::header_limit) { - http::response err{ - http::status::request_header_fields_too_large, 11 }; - err.set(http::field::content_type, "text/plain"); - err.set(http::field::connection, "close"); - err.body() = "Header too large"; - err.prepare_payload(); - - beast::error_code write_ec; - co_await http::async_write( - stream, err, - net::redirect_error(net::use_awaitable, write_ec)); - break; - } - - if (ec == http::error::end_of_stream) break; - if (ec) break; - - reset_timer(); - - http::request req = parser->release(); - total_requests_.fetch_add(1, std::memory_order_relaxed); - keep_alive = req.keep_alive(); - - res.clear(); - res.version(req.version()); - res.keep_alive(keep_alive); - - try { - co_await Router::handle_request(req, res); - } - catch (const std::exception& e) { - Logger::log("Router error: " + std::string(e.what()), "ERROR"); - res.result(http::status::internal_server_error); - res.set(http::field::content_type, "application/json"); - res.body() = R"({"error":"internal server error"})"; - keep_alive = false; - } - - res.prepare_payload(); - - std::string flat; - flat.reserve(256 + static_headers_.size() + res.body().size()); - - flat += "HTTP/1.1 "; - flat += std::to_string(res.result_int()); - flat += ' '; - flat += res.reason(); - flat += "\r\n"; - - flat += static_headers_; - - flat += "Connection: "; - flat += keep_alive ? "keep-alive\r\n" : "close\r\n"; - - for (const auto& field : res.base()) { - flat += field.name_string(); - flat += ": "; - flat += field.value(); - flat += "\r\n"; - } - flat += "\r\n"; - flat += res.body(); - - ec.clear(); - co_await net::async_write( - stream, net::buffer(flat), - net::redirect_error(net::use_awaitable, ec)); - - if (ec) break; - - buffer.consume(buffer.size()); - if (!keep_alive) break; - } - - idle_timer.cancel(); - - { - net::steady_timer sd_timer(co_await net::this_coro::executor); - sd_timer.expires_after(std::chrono::seconds(2)); - sd_timer.async_wait([&](beast::error_code ec) { - if (!ec) { - beast::error_code ignore; - stream.next_layer().close(ignore); - } - }); - - beast::error_code sd_ec; - co_await stream.async_shutdown( - net::redirect_error(net::use_awaitable, sd_ec)); - - sd_timer.cancel(); - } - - { - beast::error_code ec; - stream.next_layer().shutdown(tcp::socket::shutdown_both, ec); - stream.next_layer().close(ec); - } - } - catch (const boost::system::system_error& e) { - auto code = e.code(); - if (code != net::error::connection_reset && - code != net::error::connection_aborted && - code != net::error::eof && - code != net::error::operation_aborted && - code != net::error::broken_pipe && - code != ssl::error::stream_truncated) { - Logger::log("HTTPS connection error: " + std::string(e.what()), "ERROR"); - } - } - catch (const std::exception& e) { - Logger::log("HTTPS connection error: " + std::string(e.what()), "ERROR"); - } - catch (...) { - Logger::log("Unknown HTTPS connection error", "ERROR"); - } - - co_return; } \ No newline at end of file diff --git a/brazier/src/HttpsServerConfig.cpp b/brazier/src/HttpsServerConfig.cpp new file mode 100644 index 0000000..1d0b7cf --- /dev/null +++ b/brazier/src/HttpsServerConfig.cpp @@ -0,0 +1,201 @@ +/* + * 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/HttpsServer.hpp" + +int brazier::HttpsServer::compute_max_body_size(int ram_mb, int max_conn) { + constexpr int kBodyRamBudgetPct = 25; + constexpr int kMinBody = 64 * 1024; + constexpr int kMaxBodyCap = 16 * 1024 * 1024; + + if (max_conn <= 0) max_conn = 1; + + const std::int64_t ram_bytes = (std::int64_t)(ram_mb) * 1024 * 1024; + const std::int64_t budget = ram_bytes * kBodyRamBudgetPct / 100; + std::int64_t per_conn = budget / (std::int64_t)(max_conn); + + if (per_conn < kMinBody) per_conn = kMinBody; + if (per_conn > kMaxBodyCap) per_conn = kMaxBodyCap; + return static_cast(per_conn); +} + +int brazier::HttpsServer::compute_max_header_size(int ram_mb, int max_conn) { + constexpr int kHeaderRamBudgetPct = 1; + constexpr int kMinHeader = 4 * 1024; + constexpr int kMaxHeaderCap = 32 * 1024; + + if (max_conn <= 0) max_conn = 1; + + const std::int64_t ram_bytes = (std::int64_t)(ram_mb) * 1024 * 1024; + const std::int64_t budget = ram_bytes * kHeaderRamBudgetPct / 100; + std::int64_t per_conn = budget / (std::int64_t)(max_conn); + + if (per_conn < kMinHeader) per_conn = kMinHeader; + if (per_conn > kMaxHeaderCap) per_conn = kMaxHeaderCap; + return static_cast(per_conn); +} + +void brazier::HttpsServer::load_common_config_from_global() { + keep_alive_timeout_ = std::chrono::seconds( + global_config->get("http.keep_alive_timeout", + global_config->get("keep-alive-timeout", 60))); + + server_name_ = global_config->get("http.server_name", + std::string("brazier")); + hsts_enabled_ = global_config->get("http.hsts_enabled", true); + hsts_header_ = global_config->get("http.hsts_header", + std::string("max-age=31536000")); + + static_headers_.clear(); + static_headers_ += "Server: " + server_name_ + "\r\n"; + if (hsts_enabled_) { + static_headers_ += "Strict-Transport-Security: " + + hsts_header_ + "\r\n"; + } +} + +void brazier::HttpsServer::load_tls_config_from_global() { + if (tls_config_from_user_) return; + + tls_.cert_pem = global_config->get("https_server.tls.cert_pem", + std::string("")); + tls_.key_pem = global_config->get("https_server.tls.key_pem", + std::string("")); + + tls_.cert_file = global_config->get("https_server.tls.cert_file", + std::string("server.crt")); + tls_.key_file = global_config->get("https_server.tls.key_file", + std::string("server.key")); + tls_.ca_file = global_config->get("https_server.tls.ca_file", + std::string("")); + + tls_.require_client_cert = + global_config->get("https_server.tls.require_client_cert", false); + tls_.verify_client_cert = + global_config->get("https_server.tls.verify_client_cert", false); + + tls_.handshake_timeout = std::chrono::seconds( + global_config->get("https_server.tls.handshake_timeout", 15)); + + try { + json conf = global_config->getJson("https_server.tls.conf"); + if (conf.is_array()) { + for (const auto& item : conf) { + if (!item.is_array() || item.empty() || item.size() > 2) { + throw std::runtime_error( + "https_server.tls.conf: each entry must be [command] " + "or [command, value]"); + } + std::string cmd = item[0].get(); + std::string val = item.size() > 1 + ? item[1].get() : ""; + tls_.conf.emplace_back(std::move(cmd), std::move(val)); + } + } + } + catch (const std::exception& e) { + Logger::log("https_server.tls.conf not loaded: " + + std::string(e.what()), "WARNING"); + } +} + +void brazier::HttpsServer::load_limits_from_config() { + const int ram_mb = platform::get_system_memory_mb(); + + const int testing_conn = + global_config->get("http.max_connections_testing", 0); + const int testing_body = + global_config->get("http.max_body_size_testing", 0); + const int testing_hdr = + global_config->get("http.max_header_size_testing", 0); + + const bool testing_mode = + testing_conn > 0 || testing_body > 0 || testing_hdr > 0; + + if (testing_conn > 0) { + max_connections_ = testing_conn; + } + else if (int v = global_config->get("http.max_connections", 0); v > 0) { + max_connections_ = v; + } + else { + const int fd_limit = platform::get_fd_limit(); + max_connections_ = fd_limit * 8 / 10; + } + + if (testing_body > 0) { + max_body_size_ = testing_body; + } + else if (int v = global_config->get("http.max_body_size", 0); v > 0) { + max_body_size_ = v; + } + else { + max_body_size_ = compute_max_body_size(ram_mb, max_connections_); + } + + if (testing_hdr > 0) { + max_header_size_ = testing_hdr; + } + else if (int v = global_config->get("http.max_header_size", 0); v > 0) { + max_header_size_ = v; + } + else { + max_header_size_ = compute_max_header_size(ram_mb, max_connections_); + } + + Logger::log( + std::string(testing_mode ? "[TESTING] " : "") + + "Final HTTP limits: max_connections=" + + std::to_string(max_connections_) + + ", max_body=" + std::to_string(max_body_size_ / 1024) + "KB" + + ", max_header=" + std::to_string(max_header_size_ / 1024) + "KB" + + " (RAM=" + std::to_string(ram_mb) + "MB)", + testing_mode ? "WARNING" : "INFO"); +} + +void brazier::HttpsServer::initializeConnections() { + try { + Queue::connect(global_config->get("nosql.host", "127.0.0.1"), + global_config->get("nosql.port", 6379)); + } + catch (const std::exception& e) { + Logger::log("Connection to queue failed: " + std::string(e.what()), + "ERROR"); + } + + try { + Cache::connect(global_config->get("redis.host", "127.0.0.1"), + global_config->get("redis.port", 6379)); + } + catch (const std::exception& e) { + Logger::log("Connection to NOSQL database failed: " + + std::string(e.what()), "ERROR"); + } + + try { + Database db; + auto migrator = std::make_unique(db); + migrator->Initialize(); + } + catch (const std::exception& e) { + Logger::log("Database migration failed: " + std::string(e.what()), + "ERROR"); + } +} \ No newline at end of file diff --git a/brazier/src/HttpsServerConnection.cpp b/brazier/src/HttpsServerConnection.cpp new file mode 100644 index 0000000..8c68a00 --- /dev/null +++ b/brazier/src/HttpsServerConnection.cpp @@ -0,0 +1,319 @@ +/* + * 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/HttpsServer.hpp" + +net::awaitable brazier::HttpsServer::accept_and_dispatch( + tcp::acceptor& acceptor, int worker_begin) +{ + const int worker_count = + static_cast(io_contexts_.size()) - worker_begin; + int next = 0; + + for (;;) { + if (shutting_down_.load(std::memory_order_acquire)) { + co_return; + } + + beast::error_code ec; + tcp::socket socket = co_await acceptor.async_accept( + net::redirect_error(net::use_awaitable, ec)); + + if (ec) { + if (ec == net::error::operation_aborted || + shutting_down_.load(std::memory_order_acquire)) { + co_return; + } + Logger::log("Accept error: " + ec.message(), "ERROR"); + continue; + } + + const int prev = connection_count_.fetch_add(1, + std::memory_order_acq_rel); + if (prev >= max_connections_) { + connection_count_.fetch_sub(1, std::memory_order_acq_rel); + Logger::log("Connection limit reached (" + + std::to_string(prev) + "/" + + std::to_string(max_connections_) + "), rejecting", + "WARNING"); + boost::system::error_code ignore; + socket.close(ignore); + continue; + } + + const int idx = worker_begin + (next++ % worker_count); + + net::co_spawn(*io_contexts_[idx], + handle_connection(std::move(socket)), + net::detached); + } +} + +net::awaitable brazier::HttpsServer::accept_loop( + tcp::acceptor& acceptor) +{ + for (;;) { + if (shutting_down_.load(std::memory_order_acquire)) { + co_return; + } + + beast::error_code ec; + tcp::socket socket = co_await acceptor.async_accept( + net::redirect_error(net::use_awaitable, ec)); + + if (ec) { + if (ec == net::error::operation_aborted || + shutting_down_.load(std::memory_order_acquire)) { + co_return; + } + Logger::log("Accept error: " + ec.message(), "ERROR"); + continue; + } + + const int prev = connection_count_.fetch_add(1, + std::memory_order_acq_rel); + if (prev >= max_connections_) { + connection_count_.fetch_sub(1, std::memory_order_acq_rel); + Logger::log("Connection limit reached (" + + std::to_string(prev) + "/" + + std::to_string(max_connections_) + "), rejecting", + "WARNING"); + boost::system::error_code ignore; + socket.close(ignore); + continue; + } + + net::co_spawn(acceptor.get_executor(), + handle_connection(std::move(socket)), + net::detached); + } +} + +net::awaitable brazier::HttpsServer::handle_connection( + tcp::socket socket) +{ + ConnectionGuard guard(*this); + + try { + socket.set_option(tcp::no_delay(true)); + socket.set_option(boost::asio::socket_base::keep_alive(true)); + + auto ctx = get_ssl_ctx(); + ssl::stream stream(std::move(socket), *ctx); + + { + net::steady_timer hs_timer(co_await net::this_coro::executor); + hs_timer.expires_after(tls_.handshake_timeout); + hs_timer.async_wait([&](beast::error_code ec) { + if (!ec) { + beast::error_code ignore; + stream.next_layer().close(ignore); + } + }); + + beast::error_code hs_ec; + co_await stream.async_handshake( + ssl::stream_base::server, + net::redirect_error(net::use_awaitable, hs_ec)); + + hs_timer.cancel(); + + if (hs_ec) { + Logger::log("TLS handshake failed: " + hs_ec.message(), + "WARNING"); + co_return; + } + } + + std::optional> parser; + + http::response res; + beast::flat_buffer buffer; + bool keep_alive = true; + + net::steady_timer idle_timer(co_await net::this_coro::executor); + const auto IDLE_TIMEOUT = keep_alive_timeout_; + bool timed_out = false; + + auto reset_timer = [&]() { + idle_timer.expires_after(IDLE_TIMEOUT); + idle_timer.async_wait([&](beast::error_code ec) { + if (!ec) { + timed_out = true; + beast::error_code ignore; + stream.next_layer().close(ignore); + } + }); + }; + + reset_timer(); + + while (keep_alive && !timed_out) { + parser.emplace(); + parser->body_limit( + static_cast(max_body_size_)); + parser->header_limit( + static_cast(max_header_size_)); + + beast::error_code ec; + + co_await http::async_read( + stream, buffer, *parser, + net::redirect_error(net::use_awaitable, ec)); + + if (ec == http::error::body_limit) { + http::response err{ + http::status::payload_too_large, 11 }; + err.set(http::field::content_type, "text/plain"); + err.set(http::field::connection, "close"); + err.body() = "Payload too large"; + err.prepare_payload(); + + beast::error_code write_ec; + co_await http::async_write( + stream, err, + net::redirect_error(net::use_awaitable, write_ec)); + break; + } + + if (ec == http::error::header_limit) { + http::response err{ + http::status::request_header_fields_too_large, 11 }; + err.set(http::field::content_type, "text/plain"); + err.set(http::field::connection, "close"); + err.body() = "Header too large"; + err.prepare_payload(); + + beast::error_code write_ec; + co_await http::async_write( + stream, err, + net::redirect_error(net::use_awaitable, write_ec)); + break; + } + + if (ec == http::error::end_of_stream) break; + if (ec) break; + + reset_timer(); + + http::request req = parser->release(); + total_requests_.fetch_add(1, std::memory_order_relaxed); + keep_alive = req.keep_alive(); + + res.clear(); + res.version(req.version()); + res.keep_alive(keep_alive); + + try { + co_await Router::handle_request(req, res); + } + catch (const std::exception& e) { + Logger::log("Router error: " + std::string(e.what()), + "ERROR"); + res.result(http::status::internal_server_error); + res.set(http::field::content_type, "application/json"); + res.body() = R"({"error":"internal server error"})"; + keep_alive = false; + } + + res.prepare_payload(); + + std::string flat; + flat.reserve(256 + static_headers_.size() + + res.body().size()); + + flat += "HTTP/1.1 "; + flat += std::to_string(res.result_int()); + flat += ' '; + flat += res.reason(); + flat += "\r\n"; + + flat += static_headers_; + + flat += "Connection: "; + flat += keep_alive ? "keep-alive\r\n" : "close\r\n"; + + for (const auto& field : res.base()) { + flat += field.name_string(); + flat += ": "; + flat += field.value(); + flat += "\r\n"; + } + flat += "\r\n"; + flat += res.body(); + + ec.clear(); + co_await net::async_write( + stream, net::buffer(flat), + net::redirect_error(net::use_awaitable, ec)); + + if (ec) break; + + buffer.consume(buffer.size()); + if (!keep_alive) break; + } + + idle_timer.cancel(); + + { + net::steady_timer sd_timer(co_await net::this_coro::executor); + sd_timer.expires_after(std::chrono::seconds(2)); + sd_timer.async_wait([&](beast::error_code ec) { + if (!ec) { + beast::error_code ignore; + stream.next_layer().close(ignore); + } + }); + + beast::error_code sd_ec; + co_await stream.async_shutdown( + net::redirect_error(net::use_awaitable, sd_ec)); + + sd_timer.cancel(); + } + + { + beast::error_code ec; + stream.next_layer().shutdown(tcp::socket::shutdown_both, ec); + stream.next_layer().close(ec); + } + } + catch (const boost::system::system_error& e) { + auto code = e.code(); + if (code != net::error::connection_reset && + code != net::error::connection_aborted && + code != net::error::eof && + code != net::error::operation_aborted && + code != net::error::broken_pipe && + code != ssl::error::stream_truncated) { + Logger::log("HTTPS connection error: " + + std::string(e.what()), "ERROR"); + } + } + catch (const std::exception& e) { + Logger::log("HTTPS connection error: " + std::string(e.what()), + "ERROR"); + } + catch (...) { + Logger::log("Unknown HTTPS connection error", "ERROR"); + } + + co_return; +} \ No newline at end of file diff --git a/brazier/src/HttpsServerTLS.cpp b/brazier/src/HttpsServerTLS.cpp new file mode 100644 index 0000000..a05bbba --- /dev/null +++ b/brazier/src/HttpsServerTLS.cpp @@ -0,0 +1,244 @@ +/* + * 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/HttpsServer.hpp" + +#include + +void brazier::HttpsServer::apply_ssl_conf(ssl::context& ctx) { + if (tls_.conf.empty()) return; + + SSL_CONF_CTX* cctx = SSL_CONF_CTX_new(); + if (!cctx) { + throw std::runtime_error("SSL_CONF_CTX_new failed"); + } + + SSL_CONF_CTX_set_flags(cctx, + SSL_CONF_FLAG_SERVER | SSL_CONF_FLAG_CERTIFICATE); + SSL_CONF_CTX_set_ssl_ctx(cctx, ctx.native_handle()); + + for (const auto& [cmd, val] : tls_.conf) { + int rv = val.empty() + ? SSL_CONF_cmd(cctx, cmd.c_str(), nullptr) + : SSL_CONF_cmd(cctx, cmd.c_str(), val.c_str()); + + if (rv <= 0) { + SSL_CONF_CTX_free(cctx); + throw std::runtime_error( + "SSL_CONF_cmd failed for '" + cmd + + (val.empty() ? "'" : "=" + val + "'")); + } + } + + if (SSL_CONF_CTX_finish(cctx) != 1) { + SSL_CONF_CTX_free(cctx); + throw std::runtime_error("SSL_CONF_CTX_finish failed"); + } + + SSL_CONF_CTX_free(cctx); +} + +void brazier::HttpsServer::configure_ssl_ctx(ssl::context& ctx) { + ctx.set_options( + ssl::context::default_workarounds + | ssl::context::no_sslv2 + | ssl::context::no_sslv3 + | ssl::context::no_tlsv1 + | ssl::context::no_tlsv1_1 + | ssl::context::single_dh_use); + + SSL_CTX_set_options(ctx.native_handle(), + SSL_OP_IGNORE_UNEXPECTED_EOF); + + SSL_CTX_set_session_cache_mode(ctx.native_handle(), SSL_SESS_CACHE_OFF); + + ticket_store_.ensure_initialized(); + ticket_store_.attach_to(ctx.native_handle()); + + apply_ssl_conf(ctx); + + if (!tls_.cert_pem.empty() && !tls_.key_pem.empty()) { + load_cert_from_memory(ctx, tls_.cert_pem, tls_.key_pem); + } + else { + ctx.use_certificate_chain_file(tls_.cert_file); + ctx.use_private_key_file(tls_.key_file, ssl::context::pem); + + if (SSL_CTX_check_private_key(ctx.native_handle()) != 1) { + throw std::runtime_error( + "Certificate/private key mismatch: " + + tls_.cert_file + " / " + tls_.key_file); + } + } + + if (tls_.require_client_cert || tls_.verify_client_cert) { + if (!tls_.ca_file.empty()) { + ctx.load_verify_file(tls_.ca_file); + } + auto mode = ssl::verify_peer; + if (tls_.require_client_cert) { + mode |= ssl::verify_fail_if_no_peer_cert; + } + ctx.set_verify_mode(mode); + } + else { + ctx.set_verify_mode(ssl::verify_none); + } +} + +void brazier::HttpsServer::configure_tls() { + if (!ssl_ctx_) { + throw std::runtime_error("configure_tls: ssl_ctx_ not initialized"); + } + configure_ssl_ctx(*ssl_ctx_); +} + +void brazier::HttpsServer::load_cert_from_memory( + ssl::context& ctx, + const std::string& cert_pem, const std::string& key_pem) +{ + if (cert_pem.empty() || key_pem.empty()) { + throw std::runtime_error("load_cert_from_memory: empty PEM"); + } + + BIO* cert_bio = BIO_new_mem_buf(cert_pem.data(), + static_cast(cert_pem.size())); + if (!cert_bio) { + throw std::runtime_error("BIO_new_mem_buf (cert) failed"); + } + + X509* leaf = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr); + if (!leaf) { + BIO_free(cert_bio); + throw std::runtime_error("PEM_read_bio_X509 failed: " + + std::string(ERR_error_string(ERR_get_error(), nullptr))); + } + + if (SSL_CTX_use_certificate(ctx.native_handle(), leaf) != 1) { + X509_free(leaf); + BIO_free(cert_bio); + throw std::runtime_error("SSL_CTX_use_certificate failed"); + } + X509_free(leaf); + + X509* chain_cert = nullptr; + while ((chain_cert = PEM_read_bio_X509(cert_bio, nullptr, + nullptr, nullptr)) != nullptr) { + if (SSL_CTX_add_extra_chain_cert(ctx.native_handle(), + chain_cert) != 1) { + X509_free(chain_cert); + BIO_free(cert_bio); + throw std::runtime_error( + "SSL_CTX_add_extra_chain_cert failed"); + } + } + BIO_free(cert_bio); + + BIO* key_bio = BIO_new_mem_buf(key_pem.data(), + static_cast(key_pem.size())); + if (!key_bio) { + throw std::runtime_error("BIO_new_mem_buf (key) failed"); + } + + EVP_PKEY* pkey = PEM_read_bio_PrivateKey(key_bio, nullptr, + nullptr, nullptr); + BIO_free(key_bio); + + if (!pkey) { + throw std::runtime_error("PEM_read_bio_PrivateKey failed: " + + std::string(ERR_error_string(ERR_get_error(), nullptr))); + } + + if (SSL_CTX_use_PrivateKey(ctx.native_handle(), pkey) != 1) { + EVP_PKEY_free(pkey); + throw std::runtime_error("SSL_CTX_use_PrivateKey failed"); + } + EVP_PKEY_free(pkey); + + if (SSL_CTX_check_private_key(ctx.native_handle()) != 1) { + throw std::runtime_error( + "Certificate and private key do not match"); + } + + Logger::log("TLS certificate loaded from PEM (in-memory, with chain)", + "INFO"); +} + +std::shared_ptr brazier::HttpsServer::get_ssl_ctx() { + std::shared_lock lock(ssl_ctx_mutex_); + return ssl_ctx_; +} + +bool brazier::HttpsServer::reloadTls() { + if (!tls_.cert_file.empty() && !tls_.key_file.empty()) { + std::ifstream cf(tls_.cert_file, std::ios::binary); + std::ifstream kf(tls_.key_file, std::ios::binary); + + if (!cf || !kf) { + Logger::log("reloadTls: cannot open files: " + + tls_.cert_file + " / " + tls_.key_file, "ERROR"); + return false; + } + + TlsConfig new_tls = tls_; + new_tls.cert_pem = std::string( + std::istreambuf_iterator(cf), {}); + new_tls.key_pem = std::string( + std::istreambuf_iterator(kf), {}); + + return reloadTls(new_tls); + } + + Logger::log("reloadTls: no file paths configured, " + "use reloadTls(new_tls) with fresh PEM", "WARNING"); + return false; +} + +bool brazier::HttpsServer::reloadTls(const TlsConfig& new_tls) { + try { + auto new_ctx = std::make_shared( + ssl::context::tls_server); + + TlsConfig saved = tls_; + tls_ = new_tls; + + try { + configure_ssl_ctx(*new_ctx); + } + catch (...) { + tls_ = saved; + throw; + } + + { + std::unique_lock lock(ssl_ctx_mutex_); + ssl_ctx_ = new_ctx; + } + + Logger::log("TLS reloaded successfully (new SSL_CTX active)", + "SUCCESS"); + return true; + } + catch (const std::exception& e) { + Logger::log("TLS reload failed: " + std::string(e.what()) + + " (keeping old SSL_CTX)", "ERROR"); + return false; + } +} \ No newline at end of file From a345c023cb3ca3c92a0ca2dfa273e6603f3b72a6 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Thu, 24 Sep 2026 18:05:05 +0300 Subject: [PATCH 28/29] docs: https server --- README.md | 494 +++++++++++------------- brazier/include/brazier/HttpsServer.hpp | 6 +- 2 files changed, 218 insertions(+), 282 deletions(-) diff --git a/README.md b/README.md index 4c7607d..8bd561b 100644 --- a/README.md +++ b/README.md @@ -489,37 +489,38 @@ boost::asio::awaitable createUser(const Request& req, Response& res, const ## Brazier HTTPS Server Brazier ships with a first-class HTTPS server built on **Boost.Beast + Boost.Asio + OpenSSL**. -It is API-compatible with the plain HTTP `Server` class, so switching between them — or running -both — is a matter of a couple of lines in your `main`. +It is API-compatible with the plain HTTP `Server` class — switching between them, or running +both side by side, is a couple of lines in your `main`. ### Features - **TLS 1.2 / 1.3** by default, with per-server override via `conf` - **Session resumption** via RFC 5077 tickets with automatic key rotation - **Hot-reload** of certificates without restarting the server -- **SNI-aware** (Server Name Indication) — future multi-cert scenarios are possible +- **Certificate from file OR from memory (PEM string)** - **HSTS** header sent automatically on every response - **Keep-alive** over TLS with configurable idle timeout -- **Graceful shutdown** with `close_notify` and 5-second shutdown timeout +- **Graceful shutdown** with `close_notify` and a bounded drain timeout - **Handshake timeout** to protect against Slowloris-style attacks - **mTLS** (mutual TLS / client certificates) with custom CA - **Multi-io_context** — N worker threads, one per CPU core - **`SO_REUSEPORT`** on Linux/macOS, round-robin dispatch on Windows - **`TCP_DEFER_ACCEPT`** on Linux — less wake-ups, more throughput -- **Dynamic limits** — body size, header size, connection count auto-tuned to hardware -- **Raw OpenSSL tuning** through `SSL_CONF_cmd` — no code changes for cipher / protocol changes -- **Certificate from file OR from memory (PEM string)** — for Vault, K8s secrets, etc. +- **Dynamic limits** — body size, header size, and connection count + auto-tuned to the host hardware at startup +- **Raw OpenSSL tuning** through `SSL_CONF_cmd` — no code changes for + cipher / protocol tweaks - **Same Router / Engine / Middleware** as the HTTP server — routing is transport-agnostic ### Requirements -- **OpenSSL** 3.x (via vcpkg — `openssl` package) -- **Boost** 1.82+ (needs `boost::asio::cancel_after`) -- A **PEM-encoded** certificate and private key +- **OpenSSL** 3.x (installed via vcpkg — the `openssl` package) +- **Boost** 1.82+ (`boost::asio::cancel_after` is used internally) +- A **PEM-encoded** certificate chain and matching private key ### Configuration -Add an `https_server` section to your `config.json` alongside the existing `server` section: +Add an `https_server` section to your `config.json` alongside the existing `server`: ```json { @@ -552,114 +553,63 @@ Add an `https_server` section to your `config.json` alongside the existing `serv #### TLS section reference -| Key | Type | Default | Description | -|------------------------|-----------|-----------|-------------| -| `cert_file` | `string` | `server.crt` | Path to PEM certificate chain (leaf + intermediates) | -| `key_file` | `string` | `server.key` | Path to PEM private key | -| `cert_pem` | `string` | `""` | Certificate as in-memory PEM string (overrides `cert_file`) | -| `key_pem` | `string` | `""` | Private key as in-memory PEM string (overrides `key_file`) | -| `ca_file` | `string` | `""` | Path to CA bundle — only needed for mTLS | -| `require_client_cert` | `bool` | `false` | Reject connections without a client certificate | -| `verify_client_cert` | `bool` | `false` | Verify client certificate if presented (but don't require) | -| `handshake_timeout` | `int` | `15` | Seconds to wait for TLS handshake before closing | -| `conf` | `array` | `[]` | Raw `SSL_CONF_cmd` commands — see below | - -> **Note:** paths in `cert_file` / `key_file` / `ca_file` are resolved relative to the -> **current working directory** of the process, not the `config.json` file. Either run the binary -> from the project root, or use absolute paths. On Windows, always use forward slashes -> (`"C:/certs/server.crt"`) — backslashes are escape characters in JSON. +| Key | Type | Default | Description | +|------------------------|-----------|---------------|-------------| +| `cert_file` | `string` | `server.crt` | Path to PEM certificate chain (leaf + intermediates) | +| `key_file` | `string` | `server.key` | Path to PEM private key | +| `cert_pem` | `string` | `""` | Certificate as an in-memory PEM string (overrides `cert_file`) | +| `key_pem` | `string` | `""` | Private key as an in-memory PEM string (overrides `key_file`) | +| `ca_file` | `string` | `""` | Path to a CA bundle — required for mTLS | +| `require_client_cert` | `bool` | `false` | Reject connections without a client certificate | +| `verify_client_cert` | `bool` | `false` | Verify client certificate if presented (but don't require) | +| `handshake_timeout` | `int` | `15` | Seconds to wait for TLS handshake before closing | +| `conf` | `array` | `[]` | Raw `SSL_CONF_cmd` commands — see below | + +> **Paths are resolved relative to the process's current working +> directory**, not to the `config.json` file. Either run the binary from +> the project root, or use absolute paths. On Windows, always use forward +> slashes (`"C:/certs/server.crt"`) — backslashes are escape characters in JSON. + +#### `conf` array — raw OpenSSL commands + +The `conf` array passes commands directly to `SSL_CONF_cmd`. Each entry is +a two-element array `["command", "value"]`, or a one-element array for +commands without arguments. + +```json +"conf": [ + ["CipherString", "ECDHE+AESGCM:ECDHE+CHACHA20"], + ["Options", "-SessionTicket"], + ["MinProtocol", "TLSv1.3"] +] +``` + +These are passed verbatim to OpenSSL. See the OpenSSL documentation for +`SSL_CONF_cmd` for the full list. Brazier does not validate them — if +OpenSSL rejects a command, `initialize()` fails with a clear error. #### HTTP section reference -Global HTTP limits used by both HTTP and HTTPS servers. +Global HTTP limits shared by both `Server` and `HttpsServer`. -| Key | Type | Default | Description | -|------------------------|-----------|--------------|-------------| -| `keep_alive_timeout` | `int` | `60` | Seconds to keep an idle connection open (also accepts legacy `keep-alive-timeout` at top level) | -| `max_connections` | `int` | `ulimit×0.8` | Max simultaneous connections; if not set, derived from `RLIMIT_NOFILE` | -| `max_body_size` | `int` | auto | Max request body in bytes; auto-derived from RAM and `max_connections` if not set | -| `max_header_size` | `int` | auto | Max total request header size; auto-derived from RAM and `max_connections` | -| `max_connections_testing` | `int` | `0` | Test-only override for `max_connections` | -| `max_body_size_testing` | `int` | `0` | Test-only override for `max_body_size` | -| `max_header_size_testing` | `int` | `0` | Test-only override for `max_header_size` | +| Key | Type | Default | Description | +|---------------------------|-------|---------------|-------------| +| `keep_alive_timeout` | `int` | `60` | Seconds to keep an idle connection open (legacy `keep-alive-timeout` at top level is also accepted) | +| `max_connections` | `int` | `ulimit × 0.8` | Max simultaneous connections; if unset, derived from `RLIMIT_NOFILE` | +| `max_body_size` | `int` | auto | Max request body in bytes; auto-derived from RAM and `max_connections` | +| `max_header_size` | `int` | auto | Max total request header size; auto-derived from RAM and `max_connections` | +| `max_connections_testing` | `int` | `0` | Test-only override for `max_connections` | +| `max_body_size_testing` | `int` | `0` | Test-only override for `max_body_size` | +| `max_header_size_testing` | `int` | `0` | Test-only override for `max_header_size` | -**Auto-derived limits** work like this: +**Auto-derivation rules:** - `max_connections` = `RLIMIT_NOFILE × 0.8` (POSIX) or `16384 × 0.8` (Windows) - `max_body_size` = `(RAM × 25%) / max_connections`, clamped to `[64 KB, 16 MB]` - `max_header_size` = `(RAM × 1%) / max_connections`, clamped to `[4 KB, 32 KB]` -Any explicit value in the config overrides the auto-derivation. The `_testing` keys take -highest priority and are only meant for the test suite. - -### Generating certificates - -#### Development (self-signed) - -The simplest way to get a working dev certificate — a self-signed cert valid for `localhost`: - -```bash -mkdir -p app/certs -openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \ - -keyout app/certs/server.key \ - -out app/certs/server.crt \ - -subj "/CN=localhost" \ - -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" -``` - -For an ECDSA P-256 certificate (smaller, faster handshake — recommended for internal services): - -```bash -openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -days 365 -nodes \ - -keyout app/certs/server.key \ - -out app/certs/server.crt \ - -subj "/CN=localhost" \ - -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" -``` - -On **Windows `cmd.exe`** the same command must be on a single line (no `\` continuation) — -use `^` for line continuation, or paste it as one long line. - -Browsers will warn about the self-signed certificate. To silence the warning for development, -add the certificate to the **current user** root store on Windows: - -```cmd -certutil -addstore -user Root app\certs\server.crt -``` - -And remove it later with: - -```cmd -certutil -user -delstore Root localhost -``` - -> **Do not commit certificates to git.** Add `certs/`, `app/certs/`, `*.key`, `*.pem`, `*.crt` -> to `.gitignore`. Each developer generates their own dev certificate. - -#### Production (Let's Encrypt) - -For a public domain, use Let's Encrypt. **Use the staging environment first** — it has much -higher rate limits and won't lock you out if you misconfigure something: - -```bash -# Linux / macOS -sudo certbot certonly --standalone --test-cert -d example.com -``` - -On Windows the recommended clients are `win-acme` and `Certify The Web`. Enable staging mode -(in `win-acme` — pass `--test`; in Certify — Settings → Certificate Authorities → Use Staging Mode). - -Point the config at the resulting PEM files: - -```json -"tls": { - "cert_file": "/etc/letsencrypt/live/example.com/fullchain.pem", - "key_file": "/etc/letsencrypt/live/example.com/privkey.pem" -} -``` - -> **Always use `fullchain.pem`, not `cert.pem`** — clients need the intermediate certificates -> to build a valid chain. +Any explicit value in the config wins over auto-derivation. The `_testing` +keys take priority over everything else and are intended for the test suite. ### Using the HTTPS server @@ -674,8 +624,10 @@ int main() { brazier::ConfigManager::initGlobal("config.json"); brazier::global_config->setAutoSave(false); - std::string host = brazier::global_config->get("https_server.host", "0.0.0.0"); - int port = brazier::global_config->get("https_server.port", 8443); + std::string host = brazier::global_config->get( + "https_server.host", "0.0.0.0"); + int port = brazier::global_config->get( + "https_server.port", 8443); brazier::HttpsServer server(host, port); @@ -691,20 +643,24 @@ int main() { } ``` -`HttpsServer` reads everything it needs from `https_server.*` in `global_config`: -host, port, TLS settings, `conf` array, handshake timeout, mTLS flags. You don't pass -them explicitly — just make sure `ConfigManager::initGlobal()` is called first. +`HttpsServer` reads everything it needs from `https_server.*` in +`global_config`: host, port, TLS settings, the `conf` array, handshake +timeout, and mTLS flags. You don't pass them explicitly — just make sure +`ConfigManager::initGlobal()` runs first. ### Overriding TLS programmatically -Sometimes you don't want to put TLS configuration in the JSON at all — for example, when the -certificate path is only known at runtime, or when you're building multiple `HttpsServer` -instances with different certificates: +When the certificate path is only known at runtime, or you're running +multiple `HttpsServer` instances with different certificates, pass a +`TlsConfig` directly: ```cpp brazier::HttpsServer::TlsConfig tls; tls.cert_file = "/var/lib/myapp/api.crt"; tls.key_file = "/var/lib/myapp/api.key"; +tls.conf = { + { "min_protocol", "TLSv1.3" } +}; tls.handshake_timeout = std::chrono::seconds(10); brazier::HttpsServer api("0.0.0.0", 9443, tls); @@ -712,41 +668,65 @@ api.initialize(); api.run(); ``` -When `TlsConfig` is passed explicitly, `https_server.tls.*` from the JSON is ignored -entirely — the code-level config wins. +When `TlsConfig` is passed explicitly, `https_server.tls.*` from the JSON +is ignored entirely — the code-level config wins. ### Loading certificates from memory (PEM strings) -If your certificate comes from a secret manager, an environment variable, or any source -other than a local file, populate `cert_pem` / `key_pem` instead of `cert_file` / `key_file`: +If the certificate comes from a secret manager, a mounted Kubernetes secret, +or any source other than a plain file path, put the PEM content directly +into `config.json` using `cert_pem` / `key_pem` instead of `cert_file` / +`key_file`: + +```json +"https_server": { + "tls": { + "cert_pem": "-----BEGIN CERTIFICATE-----\nMIIF...\n-----END CERTIFICATE-----\n", + "key_pem": "-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----\n" + } +} +``` + +Note the `\n` escapes — PEM is multi-line, and JSON strings must escape +newlines. The `nlohmann::json` parser converts them to real newlines before +OpenSSL sees them. + +`cert_pem` may contain the full chain (leaf + intermediates) — Brazier parses +and registers each certificate automatically. `key_pem` must be the matching +private key; the pair is validated with `SSL_CTX_check_private_key` at load +time. If the key does not match the certificate, `initialize()` fails with +`"Certificate/private key mismatch"`. + +`cert_pem` takes priority over `cert_file`. If both are set, the in-memory +copy is used. This lets you supply a file path for hot-reload and a cached +PEM for the initial load — but note that `reloadTls()` (no args) needs +`cert_file` / `key_file` to know where to re-read from. + +For programmatic sources (Vault API, custom secret fetch, database), skip +the config entirely and pass `TlsConfig` directly: ```cpp brazier::HttpsServer::TlsConfig tls; -tls.cert_pem = std::getenv("TLS_CERT_PEM"); // full PEM chain -tls.key_pem = std::getenv("TLS_KEY_PEM"); // PEM private key +tls.cert_pem = fetchPemFromVault("tls/server.crt"); +tls.key_pem = fetchPemFromVault("tls/server.key"); brazier::HttpsServer server("0.0.0.0", 8443, tls); server.initialize(); ``` -`cert_pem` may contain **the full chain** (leaf + intermediates) — Brazier parses and -registers each certificate automatically. `key_pem` must be the matching private key; -the pair is validated with `SSL_CTX_check_private_key` at load time. - -> **`cert_pem` takes priority over `cert_file`.** If both are set, the memory copy wins. -> This lets you supply a file path for hot-reload **and** a cached PEM for the initial -> load — but note that `reloadTls()` needs `cert_file`/`key_file` to be set to know where -> to re-read from. +When `TlsConfig` is passed explicitly, `https_server.tls.*` from `config.json` +is ignored entirely — the code-level config wins. ### Hot-reload of TLS certificates -Reload certificates without restarting the server or dropping existing connections: +Reload certificates without restarting the server or dropping existing +connections: ```cpp -// Re-read files (cert_file / key_file must be set) +// Re-read from files (cert_file / key_file must be set) server.reloadTls(); -// Or explicitly supply a new config — useful for Vault / K8s / DB sources +// Or supply a new config explicitly — useful for Vault / K8s / DB sources brazier::HttpsServer::TlsConfig new_tls = server.getTlsConfig(); new_tls.cert_pem = fetchPemFromVault("tls/server.crt"); new_tls.key_pem = fetchPemFromVault("tls/server.key"); @@ -755,14 +735,18 @@ server.reloadTls(new_tls); **Guarantees:** -- Existing connections continue on the **old** `SSL_CTX` and finish normally. -- New handshakes use the **new** `SSL_CTX`. -- If the new config is invalid, the operation fails and the **old** `SSL_CTX` stays - active — the server is never left in a broken state. -- Session ticket keys (`TicketKeyStore`) are shared across contexts, so resumption - continues to work across reloads. +- Existing connections continue on the old `SSL_CTX` and finish normally. +- New handshakes use the new `SSL_CTX`. +- If the new config is invalid, the operation fails and the old `SSL_CTX` + stays active — the server is never left in a broken state. +- Session ticket keys are shared across contexts, so resumption keeps + working across reloads. + +The reload is implemented with `std::shared_ptr` plus a +`std::shared_mutex`. The cost per connection is one atomic refcount and one +shared-lock acquire — invoked once per connection, not per request. -**Common patterns:** +**Common trigger patterns:** ```cpp // 1. Console command @@ -797,35 +781,35 @@ std::thread([&server] { ### Session resumption -Brazier uses **stateless session tickets** (RFC 5077) — no per-session state is kept on -the server. This is the only mechanism that works in TLS 1.3, and it's the recommended -mechanism for TLS 1.2 too. +Brazier uses **stateless session tickets** (RFC 5077) — no per-session state +is kept on the server. This is the only resumption mechanism in TLS 1.3, +and it is the recommended mechanism for TLS 1.2 as well. -Tickets are encrypted with rotating keys managed by `TicketKeyStore`: +Tickets are encrypted with rotating keys: - **Rotation interval** — a new key every 12 hours. -- **Key lifetime** — old keys kept for 48 hours, so clients with valid tickets can still - resume. -- **Thread-safe** — one store per `HttpsServer`; multiple servers in the same process - have independent stores. - -Resumption typically costs **0.3–0.8 ms** vs **2–3 ms** for a full handshake — roughly a -10× CPU reduction on resumed sessions. This matters most for connection-churn workloads -(REST APIs behind a proxy, health checks). +- **Key lifetime** — old keys are kept for 48 hours, so clients with valid + tickets can still resume. +- **Per-server isolation** — each `HttpsServer` has its own key store. + Multiple servers in the same process do not share tickets. +Resumption typically costs **0.3–0.8 ms** versus **2–3 ms** for a full +handshake — roughly a 10× CPU reduction on resumed sessions. This matters +most for connection-churn workloads. ### Threading model On startup, `HttpsServer` creates **N io_context instances, one per CPU core**: -| Platform | io_contexts | Acceptors | Dispatch | -|---|---|---|---| -| Linux / macOS | `hardware_concurrency()` | Same as io_contexts | `SO_REUSEPORT` — kernel hashes 4-tuples | -| Windows | `hardware_concurrency()` | 1 | round-robin `co_spawn` from a single acceptor | +| Platform | io_contexts | Acceptors | Dispatch | +|---------------|------------------------|--------------------|----------| +| Linux / macOS | `hardware_concurrency()` | same as io_contexts | `SO_REUSEPORT` — kernel hashes 4-tuples | +| Windows | `hardware_concurrency()` | 1 | round-robin `co_spawn` from a single acceptor | -Each io_context has its own thread. Each thread has its own IOCP (Windows) or epoll -instance (Linux). There is **no cross-thread signalling** on the hot path — completions -for a connection always run on the same thread that owns its io_context. +Each io_context owns its own thread, and each thread owns its own IOCP +(Windows) or epoll instance (Linux). There is no cross-thread signalling on +the hot path — completions for a connection always run on the thread that +owns its io_context. The startup log tells you exactly what happened: @@ -834,32 +818,13 @@ The startup log tells you exactly what happened: [INFO] Starting 12 io_context(s) over 12 thread(s), dispatch=round-robin ``` -On Linux expect `io_contexts=N, acceptors=N, dispatch=SO_REUSEPORT`. On Windows expect -`io_contexts=N, acceptors=1, dispatch=round-robin`. - -### Dynamic limits - -Body size, header size, and connection count are all auto-tuned to the host hardware at -startup, unless overridden in `config.json`: - -``` -[WARNING] [TESTING] Final HTTP limits: max_connections=20, max_body=1024KB, max_header=8KB (RAM=15611MB) -[INFO] Final HTTP limits: max_connections=50000, max_body=2097152KB, max_header=16KB (RAM=16384MB) -``` - -Auto-derivation uses `RLIMIT_NOFILE` for connection count and total RAM for body/header -caps. If you set any of `http.max_connections`, `http.max_body_size`, or -`http.max_header_size` explicitly, that value wins. - -The `_testing` variants take absolute priority — they exist so the test suite can enforce -small limits without touching the prod config. - ### Mutual TLS (mTLS) -mTLS requires the client to present a certificate signed by a CA you trust. This is common -for service-to-service communication, IoT devices, and internal APIs. +mTLS requires each client to present a certificate signed by a CA you trust. +This is common for service-to-service communication, IoT devices, and +internal APIs. -1. Enable it in the config: +Enable it in the config: ```json "tls": { @@ -870,55 +835,43 @@ for service-to-service communication, IoT devices, and internal APIs. } ``` -2. Generate a CA, a server certificate, and a client certificate: +With `require_client_cert: true`, the server sends a `CertificateRequest` +during the handshake. Clients that cannot present a valid certificate are +rejected **before** any HTTP request is processed — the TLS handshake +simply fails. -```bash -# CA -openssl genrsa -out ca.key 4096 -openssl req -x509 -new -nodes -key ca.key -sha256 -days 1825 \ - -out ca.crt -subj "/CN=My Internal CA" +Set `verify_client_cert: true` (without `require_client_cert`) to ask for a +client certificate but accept clients that don't present one. Useful when +client certs are optional. -# Server cert -openssl genrsa -out server.key 2048 -openssl req -new -key server.key -out server.csr -subj "/CN=localhost" -openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ - -out server.crt -days 365 -sha256 \ - -extfile <(printf "subjectAltName=DNS:localhost,IP:127.0.0.1") +> **`ca_file` is required when `require_client_cert: true`.** Without a CA +> bundle the server has no way to validate client certificates. -# Client cert -openssl genrsa -out client.key 2048 -openssl req -new -key client.key -out client.csr -subj "/CN=brazier-client" -openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ - -out client.crt -days 365 -sha256 \ - -extfile <(printf "extendedKeyUsage=clientAuth") -``` - -3. Clients (including brazier's own `HttpClient`) must now present `client.crt` + `client.key`. -Without them the TLS handshake is aborted before any HTTP request is processed. - -### What the server does automatically +### Response headers set automatically For every response, `HttpsServer` sets: -| Header | Value | Why | -|---|---|---| -| `Server` | `brazier` | Identifies the framework | -| `Strict-Transport-Security` | `max-age=31536000` | HSTS — instructs browsers to use HTTPS for one year | -| `Connection` | `keep-alive` or `close` | Matches the request | +| Header | Value | Why | +|----------------------------|--------------------------------|-----| +| `Server` | `brazier` (configurable) | Identifies the framework | +| `Strict-Transport-Security`| `max-age=31536000` (configurable) | HSTS — tells browsers to use HTTPS for a year | +| `Connection` | `keep-alive` or `close` | Matches the request | +Configure via `http.server_name`, `http.hsts_enabled`, and `http.hsts_header`. You don't need to set these in your controllers. ### Lifecycle and timeouts -| Phase | Timeout | Config key | -|---|---|---| -| TLS handshake | 15 s | `https_server.tls.handshake_timeout` | -| Idle keep-alive | 60 s | `http.keep_alive_timeout` (or legacy `keep-alive-timeout`) | -| Graceful TLS shutdown | 5 s | — | -| Graceful server shutdown | 10 s | — | +| Phase | Timeout | Config key | +|--------------------------|---------|------------| +| TLS handshake | 15 s | `https_server.tls.handshake_timeout` | +| Idle keep-alive | 60 s | `http.keep_alive_timeout` (or legacy `keep-alive-timeout`) | +| Graceful TLS shutdown | 5 s | — | +| Graceful server shutdown | 10 s | — | -If any of these fire, the connection is closed cleanly — you'll see a corresponding -`[DEBUG] HTTPS client disconnected` line in the log, not an error. +If any of these fire, the connection is closed cleanly — you'll see a +corresponding `[DEBUG] HTTPS client disconnected` line in the log, not an +error. ### Shutting down @@ -928,89 +881,72 @@ server.stop(); // returns after all in-flight connections complete (up to 10 s `stop()` performs a graceful shutdown: -1. Acceptors closed — no new connections. -2. `work_guard` released — `io_context::run()` will exit when idle. -3. Waits up to **10 seconds** for `connection_count` to reach zero. +1. Acceptors are closed — no new connections. +2. The work guard is released — `io_context::run()` exits when idle. +3. Waits up to **10 seconds** for the active connection count to reach zero. 4. `io_context::stop()` — force-cancels anything still running. -5. Worker threads joined. +5. Worker threads are joined. -If `run()` is executing on a different thread, join that thread **after** `stop()` returns. -Do **not** call `stop()` from inside a request handler — it will deadlock waiting for -the calling connection to close. +If `run()` is executing on a different thread, join that thread **after** +`stop()` returns. Do **not** call `stop()` from inside a request handler — +it will deadlock waiting for the calling connection to close. -### Testing TLS - -From the command line: +### Verifying a running server ```bash -# Basic request (accepts self-signed) +// Basic request (accepts self-signed certificates) curl -vk https://localhost:8443/ -# Strict verification against your own CA +// Strict verification against your own CA curl -v --cacert app/certs/server.crt https://localhost:8443/ -# Inspect the handshake +// Inspect the handshake openssl s_client -connect localhost:8443 -servername localhost -# Verify TLS 1.1 is rejected +// Verify that TLS 1.1 is rejected openssl s_client -connect localhost:8443 -tls1_1 ``` -> **Windows `curl.exe` uses Schannel and does not support ECDSA server certificates.** -> If your cert is ECDSA P-256, use `Git for Windows`'s curl (which links against OpenSSL), -> or stick to `openssl s_client` for testing. `ab` (ApacheBench) uses its own OpenSSL and -> works with both cert types. - -From brazier's own test suite: - -```cpp -#include "brazier/App/Http/Helpers/HttpClient.hpp" - -net::awaitable fetch_secure() { - brazier::HttpClient client; - client.set_verify_ssl(false); // dev only - - auto res = co_await client.get("https://localhost:8443/api/health"); - if (client.is_success(res)) { - // ... - } -} -``` +> **Windows `curl.exe` uses Schannel, which does not support ECDSA server +> certificates.** If your cert is ECDSA P-256, use curl from `Git for Windows` +> (which links against OpenSSL), or stick with `openssl s_client`. `ab` +> (ApacheBench) uses its own OpenSSL and works with both cert types. ### Common pitfalls -- **`use_certificate_chain_file: cannot find the file`** — the path in `cert_file` is - relative to the process's *current working directory*, not the config file. Run from the - project root or use absolute paths. +- **`use_certificate_chain_file: cannot find the file`** — the path in + `cert_file` is relative to the process's *current working directory*, not + the config file. Run from the project root or use absolute paths. -- **`Certificate/private key mismatch`** — the cert and key in `cert_file` / `key_file` - don't belong to the same pair, or the key was regenerated without regenerating the cert. - Brazier rejects this at startup with a clear error. +- **`Certificate/private key mismatch`** — the cert and key in `cert_file` / + `key_file` don't belong to the same pair. Brazier rejects this at startup + with a clear error instead of failing later at handshake time. -- **`SSL_CONF_cmd failed for 'min_protocol=...'`** — some OpenSSL builds (particularly - via vcpkg) don't register `min_protocol` in `SSL_CONF_cmd`. Brazier handles this internally - by calling `SSL_CTX_set_min_proto_version` directly — the `conf` entry is a no-op there, - but you can safely keep it for documentation purposes. +- **`SSL_CONF_cmd failed for 'min_protocol=...'`** — some OpenSSL builds + (notably via vcpkg) don't register `min_protocol` in `SSL_CONF_cmd`. + Use TLS options in code instead, or check the OpenSSL documentation for + the correct command name in your build. -- **`no shared cipher` / `alert 40` on handshake** — the client and server can't agree on - a cipher suite. Most often: the client is offering only RSA suites while your cert is ECDSA - (or vice versa). Check the certificate type with - `openssl x509 -in cert.crt -noout -text | findstr "Public Key Algorithm"`. +- **`no shared cipher` / `alert 40` on handshake** — the client and server + could not agree on a cipher suite. The usual cause is a mismatch between + the certificate key type and the client's cipher list: an ECDSA certificate + cannot satisfy an RSA-only client, and vice versa. -- **Browser says "certificate not trusted"** — that's expected for self-signed certificates. - Either click through the warning, add the cert to the user root store (see above), or use - a real certificate from Let's Encrypt. +- **Browser says "certificate not trusted"** — expected for self-signed + certificates. Either click through the warning, add the cert to the user + root store, or use a certificate from a public CA. -- **`WSAECONNRESET` / `WSAECONNABORTED` in logs** — these are normal client disconnect events, - not errors. Brazier logs them at `DEBUG` level. +- **`WSAECONNRESET` / `WSAECONNABORTED` in logs** — these are normal client + disconnect events, not errors. Brazier logs them at `DEBUG` level. -- **`TLS shutdown error` on every connection** — this is `APPLICATION_DATA_AFTER_CLOSE_NOTIFY`, - a benign artefact of clients that send data after the shutdown alert. It's logged at - `DEBUG` and can be ignored. +- **`TLS shutdown error` on every connection** — this is + `APPLICATION_DATA_AFTER_CLOSE_NOTIFY`, a benign artefact of clients that + send data after the shutdown alert. Logged at `DEBUG` and safe to ignore. -- **Hot-reload says `reloadTls: source is PEM, nothing to reload`** — you supplied the - certificate only as `cert_pem` / `key_pem`, with no `cert_file` / `key_file`. Set the file - paths too, or call `reloadTls(new_tls)` with fresh PEM strings from your secret source. +- **Hot-reload says `reloadTls: source is PEM, nothing to reload`** — you + supplied the certificate only as `cert_pem` / `key_pem`, with no + `cert_file` / `key_file`. Set the file paths as well, or call + `reloadTls(new_tls)` with fresh PEM strings from your secret source. ## Brazier WebSocket Routing System diff --git a/brazier/include/brazier/HttpsServer.hpp b/brazier/include/brazier/HttpsServer.hpp index b26d772..32f7912 100644 --- a/brazier/include/brazier/HttpsServer.hpp +++ b/brazier/include/brazier/HttpsServer.hpp @@ -44,18 +44,17 @@ #include #include #include +#include #include #include #include #include #include -#include #include "Platform/SocketOptions.hpp" #include "Platform/SystemInfo.hpp" #include "TLS/TicketKeyStore.hpp" -#include "vendor/Handlers/ENV.hpp" #include "Database/Queue.hpp" #include "Database/Cache.hpp" #include "Database/Migrations/MigrationManager.hpp" @@ -188,7 +187,8 @@ namespace brazier { net::awaitable handle_connection(tcp::socket socket); net::awaitable accept_loop(tcp::acceptor& acceptor); - net::awaitable accept_and_dispatch(tcp::acceptor& acceptor, int worker_begin); + net::awaitable accept_and_dispatch(tcp::acceptor& acceptor, + int worker_begin); }; } \ No newline at end of file From ded9fa229cdaef7a06878f2902d682ebfd32d687 Mon Sep 17 00:00:00 2001 From: CodeGonshik Date: Thu, 24 Sep 2026 22:23:30 +0300 Subject: [PATCH 29/29] fix: cmakelists and config --- brazier/CMakeLists.txt | 40 ++++++++++++++----------------- brazier/config_test.json | 19 ++++++++++++++- brazier/src/HttpsServerConfig.cpp | 22 ++++++++--------- 3 files changed, 47 insertions(+), 34 deletions(-) diff --git a/brazier/CMakeLists.txt b/brazier/CMakeLists.txt index 1697a51..f2f293d 100644 --- a/brazier/CMakeLists.txt +++ b/brazier/CMakeLists.txt @@ -59,15 +59,12 @@ endif() message(STATUS "brazier: platform = ${BRAZIER_PLATFORM_DIR}") -file(GLOB_RECURSE PROJECT_SOURCES CONFIGURE_DEPENDS - "src/*.cpp" -) +file(GLOB_RECURSE PROJECT_SOURCES CONFIGURE_DEPENDS "src/*.cpp") list(FILTER PROJECT_SOURCES EXCLUDE REGEX "^src/Platform/") -file(GLOB BRAZIER_PLATFORM_SOURCES CONFIGURE_DEPENDS - "src/Platform/${BRAZIER_PLATFORM_DIR}/*.cpp" -) +file(GLOB_RECURSE BRAZIER_PLATFORM_SOURCES CONFIGURE_DEPENDS + "src/Platform/${BRAZIER_PLATFORM_DIR}/*.cpp") if(NOT BRAZIER_PLATFORM_SOURCES) message(FATAL_ERROR @@ -78,18 +75,22 @@ list(APPEND PROJECT_SOURCES ${BRAZIER_PLATFORM_SOURCES}) list(FILTER PROJECT_SOURCES EXCLUDE REGEX "app/Main\\.cpp$") -add_library(brazier_objects OBJECT ${PROJECT_SOURCES}) - -foreach(other_platform Linux MacOS Windows) - if(NOT other_platform STREQUAL BRAZIER_PLATFORM_DIR) - file(GLOB OTHER_SOURCES "src/Platform/${other_platform}/*.cpp") - if(OTHER_SOURCES) - set_source_files_properties(${OTHER_SOURCES} - PROPERTIES HEADER_FILE_ONLY TRUE - ) - endif() +function(brazier_hide_platform platform) + if(platform STREQUAL BRAZIER_PLATFORM_DIR) + return() + endif() + file(GLOB_RECURSE OTHER "src/Platform/${platform}/*.cpp") + if(OTHER) + set_source_files_properties(${OTHER} + PROPERTIES HEADER_FILE_ONLY TRUE) endif() -endforeach() +endfunction() + +brazier_hide_platform(Linux) +brazier_hide_platform(MacOS) +brazier_hide_platform(Windows) + +add_library(brazier_objects OBJECT ${PROJECT_SOURCES}) target_link_libraries(brazier_objects PUBLIC Boost::boost) @@ -251,11 +252,6 @@ if(BUILD_TESTS) ${LIBRARIES} ) - # include(GoogleTest) - # gtest_discover_tests(brazier_tests) - - # add_test(NAME brazier_tests COMMAND brazier_tests) - add_custom_target(check COMMAND $ DEPENDS brazier_tests diff --git a/brazier/config_test.json b/brazier/config_test.json index aec896e..9acf9a6 100644 --- a/brazier/config_test.json +++ b/brazier/config_test.json @@ -1,5 +1,9 @@ { "app_name": "brazierApp", + "server": { + "host": "0.0.0.0", + "port": 3502 + }, "https_server": { "host": "0.0.0.0", "port": 8443, @@ -11,11 +15,24 @@ "verify_client_cert": false, "handshake_timeout": 3, "conf": [] + }, + "hsts": { + "enabled": true, + "header": "max-age=31536000" } }, - "http": { + "protocol": { + "keep_alive_timeout": 60, + "max_connections_testing": 100, "max_body_size_testing": 1048576, "max_header_size_testing": 8192 + }, + "filesystem": { + "drivers": { + "local": { "root": "./storage" }, + "root": { "root": "./" }, + "default": "local" + } } } \ No newline at end of file diff --git a/brazier/src/HttpsServerConfig.cpp b/brazier/src/HttpsServerConfig.cpp index 1d0b7cf..b2d7bff 100644 --- a/brazier/src/HttpsServerConfig.cpp +++ b/brazier/src/HttpsServerConfig.cpp @@ -54,13 +54,13 @@ int brazier::HttpsServer::compute_max_header_size(int ram_mb, int max_conn) { void brazier::HttpsServer::load_common_config_from_global() { keep_alive_timeout_ = std::chrono::seconds( - global_config->get("http.keep_alive_timeout", - global_config->get("keep-alive-timeout", 60))); + global_config->get("protocol.keep_alive_timeout", 60)); - server_name_ = global_config->get("http.server_name", + server_name_ = global_config->get("protocol.server_name", std::string("brazier")); - hsts_enabled_ = global_config->get("http.hsts_enabled", true); - hsts_header_ = global_config->get("http.hsts_header", + + hsts_enabled_ = global_config->get("https_server.hsts.enabled", true); + hsts_header_ = global_config->get("https_server.hsts.header", std::string("max-age=31536000")); static_headers_.clear(); @@ -120,11 +120,11 @@ void brazier::HttpsServer::load_limits_from_config() { const int ram_mb = platform::get_system_memory_mb(); const int testing_conn = - global_config->get("http.max_connections_testing", 0); + global_config->get("protocol.max_connections_testing", 0); const int testing_body = - global_config->get("http.max_body_size_testing", 0); + global_config->get("protocol.max_body_size_testing", 0); const int testing_hdr = - global_config->get("http.max_header_size_testing", 0); + global_config->get("protocol.max_header_size_testing", 0); const bool testing_mode = testing_conn > 0 || testing_body > 0 || testing_hdr > 0; @@ -132,7 +132,7 @@ void brazier::HttpsServer::load_limits_from_config() { if (testing_conn > 0) { max_connections_ = testing_conn; } - else if (int v = global_config->get("http.max_connections", 0); v > 0) { + else if (int v = global_config->get("protocol.max_connections", 0); v > 0) { max_connections_ = v; } else { @@ -143,7 +143,7 @@ void brazier::HttpsServer::load_limits_from_config() { if (testing_body > 0) { max_body_size_ = testing_body; } - else if (int v = global_config->get("http.max_body_size", 0); v > 0) { + else if (int v = global_config->get("protocol.max_body_size", 0); v > 0) { max_body_size_ = v; } else { @@ -153,7 +153,7 @@ void brazier::HttpsServer::load_limits_from_config() { if (testing_hdr > 0) { max_header_size_ = testing_hdr; } - else if (int v = global_config->get("http.max_header_size", 0); v > 0) { + else if (int v = global_config->get("protocol.max_header_size", 0); v > 0) { max_header_size_ = v; } else {