From e4c62828eb3fcf001ba18d5d4f820a69fed8c376 Mon Sep 17 00:00:00 2001 From: JohnDoe6345789 Date: Fri, 26 Dec 2025 01:35:23 +0000 Subject: [PATCH] code: dbal,cpp,hpp (21 files) --- dbal/cpp/src/daemon/server.cpp | 602 ------------------ .../src/daemon/server/parse_request_line.hpp | 42 ++ .../daemon/server/process_health_check.hpp | 35 + .../src/daemon/server/process_not_found.hpp | 30 + dbal/cpp/src/daemon/server/process_status.hpp | 43 ++ .../cpp/src/daemon/server/process_version.hpp | 33 + dbal/cpp/src/daemon/server/server.hpp | 90 +++ .../src/daemon/server/server_accept_loop.hpp | 42 ++ .../server/server_handle_connection.hpp | 42 ++ dbal/cpp/src/daemon/server/server_impl.hpp | 14 + .../daemon/server/server_parse_request.hpp | 158 +++++ .../daemon/server/server_process_request.hpp | 36 ++ dbal/cpp/src/daemon/server/server_start.hpp | 54 ++ dbal/cpp/src/daemon/server/server_stop.hpp | 34 + dbal/cpp/src/daemon/server/to_lowercase.hpp | 27 + dbal/cpp/src/daemon/server/trim_string.hpp | 27 + .../daemon/server/validate_content_length.hpp | 77 +++ .../cpp/src/daemon/server/validate_header.hpp | 83 +++ .../daemon/server/validate_request_path.hpp | 45 ++ .../server/validate_transfer_encoding.hpp | 54 ++ .../entities/lua-script/delete-lua-script.ts | 29 +- 21 files changed, 981 insertions(+), 616 deletions(-) delete mode 100644 dbal/cpp/src/daemon/server.cpp create mode 100644 dbal/cpp/src/daemon/server/parse_request_line.hpp create mode 100644 dbal/cpp/src/daemon/server/process_health_check.hpp create mode 100644 dbal/cpp/src/daemon/server/process_not_found.hpp create mode 100644 dbal/cpp/src/daemon/server/process_status.hpp create mode 100644 dbal/cpp/src/daemon/server/process_version.hpp create mode 100644 dbal/cpp/src/daemon/server/server.hpp create mode 100644 dbal/cpp/src/daemon/server/server_accept_loop.hpp create mode 100644 dbal/cpp/src/daemon/server/server_handle_connection.hpp create mode 100644 dbal/cpp/src/daemon/server/server_impl.hpp create mode 100644 dbal/cpp/src/daemon/server/server_parse_request.hpp create mode 100644 dbal/cpp/src/daemon/server/server_process_request.hpp create mode 100644 dbal/cpp/src/daemon/server/server_start.hpp create mode 100644 dbal/cpp/src/daemon/server/server_stop.hpp create mode 100644 dbal/cpp/src/daemon/server/to_lowercase.hpp create mode 100644 dbal/cpp/src/daemon/server/trim_string.hpp create mode 100644 dbal/cpp/src/daemon/server/validate_content_length.hpp create mode 100644 dbal/cpp/src/daemon/server/validate_header.hpp create mode 100644 dbal/cpp/src/daemon/server/validate_request_path.hpp create mode 100644 dbal/cpp/src/daemon/server/validate_transfer_encoding.hpp diff --git a/dbal/cpp/src/daemon/server.cpp b/dbal/cpp/src/daemon/server.cpp deleted file mode 100644 index 1758c3594..000000000 --- a/dbal/cpp/src/daemon/server.cpp +++ /dev/null @@ -1,602 +0,0 @@ -/** - * @file server.cpp - * @brief Cross-platform HTTP/1.1 server implementation with nginx reverse proxy support - * - * Provides a production-ready HTTP server with: - * - Cross-platform socket support (Windows/Linux/macOS) - * - Multi-threaded request handling - * - Nginx reverse proxy header parsing - * - Health check endpoints - * - Graceful shutdown - * - Security hardening against CVE patterns (CVE-2024-1135, CVE-2024-40725, etc.) - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// Cross-platform socket headers -#ifdef _WIN32 - // Windows - #ifndef WIN32_LEAN_AND_MEAN - #define WIN32_LEAN_AND_MEAN - #endif - #include - #include - #include - #pragma comment(lib, "ws2_32.lib") - - // Windows socket type aliases - typedef SOCKET socket_t; - typedef int socklen_t; - #define CLOSE_SOCKET closesocket - #define INVALID_SOCKET_VALUE INVALID_SOCKET - #define SOCKET_ERROR_VALUE SOCKET_ERROR -#else - // POSIX (Linux, macOS, Unix) - #include - #include - #include - #include - #include - #include - - // POSIX socket type aliases - typedef int socket_t; - #define CLOSE_SOCKET close - #define INVALID_SOCKET_VALUE -1 - #define SOCKET_ERROR_VALUE -1 -#endif - -namespace dbal { -namespace daemon { - -// Security limits to prevent CVE-style attacks -const size_t MAX_REQUEST_SIZE = 65536; // 64KB max request (prevent buffer overflow) -const size_t MAX_HEADERS = 100; // Max 100 headers (prevent header bomb) -const size_t MAX_HEADER_SIZE = 8192; // 8KB max per header -const size_t MAX_PATH_LENGTH = 2048; // Max URL path length -const size_t MAX_BODY_SIZE = 10485760; // 10MB max body size -const size_t MAX_CONCURRENT_CONNECTIONS = 1000; // Prevent thread exhaustion - -/** - * @struct HttpRequest - * @brief Parsed HTTP request structure - * - * Contains all components of an HTTP request including method, - * path, version, and headers. Used internally for request processing. - */ -struct HttpRequest { - std::string method; ///< HTTP method (GET, POST, etc.) - std::string path; ///< Request path (e.g., /api/health) - std::string version; ///< HTTP version (e.g., HTTP/1.1) - std::map headers; ///< Request headers - std::string body; - - // Nginx reverse proxy headers - std::string realIP() const { - auto it = headers.find("X-Real-IP"); - if (it != headers.end()) return it->second; - it = headers.find("X-Forwarded-For"); - if (it != headers.end()) { - // Get first IP from comma-separated list - size_t comma = it->second.find(','); - return comma != std::string::npos ? it->second.substr(0, comma) : it->second; - } - return ""; - } - - std::string forwardedProto() const { - auto it = headers.find("X-Forwarded-Proto"); - return it != headers.end() ? it->second : "http"; - } -}; - -struct HttpResponse { - int status_code; - std::string status_text; - std::map headers; - std::string body; - - HttpResponse() : status_code(200), status_text("OK") { - headers["Content-Type"] = "application/json"; - headers["Server"] = "DBAL/1.0.0"; - } - - std::string serialize() const { - std::ostringstream oss; - oss << "HTTP/1.1 " << status_code << " " << status_text << "\r\n"; - - // Add Content-Length if not already set - auto cl_it = headers.find("Content-Length"); - if (cl_it == headers.end()) { - oss << "Content-Length: " << body.length() << "\r\n"; - } - - for (const auto& h : headers) { - oss << h.first << ": " << h.second << "\r\n"; - } - - oss << "\r\n" << body; - return oss.str(); - } -}; - -class Server { -public: - Server(const std::string& bind_address, int port) - : bind_address_(bind_address), port_(port), running_(false), - server_fd_(INVALID_SOCKET_VALUE), active_connections_(0) { -#ifdef _WIN32 - // Initialize Winsock on Windows - WSADATA wsaData; - int result = WSAStartup(MAKEWORD(2, 2), &wsaData); - if (result != 0) { - std::cerr << "WSAStartup failed: " << result << std::endl; - } -#endif - } - - ~Server() { - stop(); -#ifdef _WIN32 - // Cleanup Winsock on Windows - WSACleanup(); -#endif - } - - bool start() { - if (running_) return false; - - // Create socket - server_fd_ = socket(AF_INET, SOCK_STREAM, 0); - if (server_fd_ == INVALID_SOCKET_VALUE) { - std::cerr << "Failed to create socket: " << getLastErrorString() << std::endl; - return false; - } - - // Set socket options - int opt = 1; -#ifdef _WIN32 - char* opt_ptr = reinterpret_cast(&opt); -#else - void* opt_ptr = &opt; -#endif - if (setsockopt(server_fd_, SOL_SOCKET, SO_REUSEADDR, opt_ptr, sizeof(opt)) < 0) { - std::cerr << "Failed to set SO_REUSEADDR: " << getLastErrorString() << std::endl; - CLOSE_SOCKET(server_fd_); - server_fd_ = INVALID_SOCKET_VALUE; - return false; - } - - // Bind to address - struct sockaddr_in address; - std::memset(&address, 0, sizeof(address)); - address.sin_family = AF_INET; - address.sin_port = htons(port_); - - if (bind_address_ == "0.0.0.0" || bind_address_ == "::") { - address.sin_addr.s_addr = INADDR_ANY; - } else { -#ifdef _WIN32 - // Windows inet_pton - if (InetPton(AF_INET, bind_address_.c_str(), &address.sin_addr) <= 0) { - std::cerr << "Invalid bind address: " << bind_address_ << std::endl; - CLOSE_SOCKET(server_fd_); - server_fd_ = INVALID_SOCKET_VALUE; - return false; - } -#else - // POSIX inet_pton - if (inet_pton(AF_INET, bind_address_.c_str(), &address.sin_addr) <= 0) { - std::cerr << "Invalid bind address: " << bind_address_ << std::endl; - CLOSE_SOCKET(server_fd_); - server_fd_ = INVALID_SOCKET_VALUE; - return false; - } -#endif - } - - if (bind(server_fd_, (struct sockaddr*)&address, sizeof(address)) < 0) { - std::cerr << "Failed to bind to " << bind_address_ << ":" << port_ - << ": " << getLastErrorString() << std::endl; - CLOSE_SOCKET(server_fd_); - server_fd_ = INVALID_SOCKET_VALUE; - return false; - } - - // Listen for connections (backlog of 128) - if (listen(server_fd_, 128) < 0) { - std::cerr << "Failed to listen: " << getLastErrorString() << std::endl; - CLOSE_SOCKET(server_fd_); - server_fd_ = INVALID_SOCKET_VALUE; - return false; - } - - running_ = true; - - // Start accept thread - accept_thread_ = std::thread(&Server::acceptLoop, this); - - std::cout << "Server listening on " << bind_address_ << ":" << port_ << std::endl; - return true; - } - - void stop() { - if (!running_) return; - - running_ = false; - - // Close server socket to unblock accept() - if (server_fd_ != INVALID_SOCKET_VALUE) { - CLOSE_SOCKET(server_fd_); - server_fd_ = INVALID_SOCKET_VALUE; - } - - // Wait for accept thread to finish - if (accept_thread_.joinable()) { - accept_thread_.join(); - } - - std::cout << "Server stopped" << std::endl; - } - - bool isRunning() const { - return running_; - } - - std::string address() const { - return bind_address_ + ":" + std::to_string(port_); - } - -private: - void acceptLoop() { - while (running_) { - struct sockaddr_in client_addr; - socklen_t client_len = sizeof(client_addr); - - socket_t client_fd = accept(server_fd_, (struct sockaddr*)&client_addr, &client_len); - - if (client_fd == INVALID_SOCKET_VALUE) { - if (running_) { - std::cerr << "Accept failed: " << getLastErrorString() << std::endl; - } - continue; - } - - // Check connection limit to prevent thread exhaustion DoS - // Use atomic fetch_add to avoid race condition - size_t prev_count = active_connections_.fetch_add(1); - if (prev_count >= MAX_CONCURRENT_CONNECTIONS) { - std::cerr << "Connection limit reached, rejecting connection" << std::endl; - active_connections_--; - CLOSE_SOCKET(client_fd); - continue; - } - - // Handle connection in a new thread - std::thread(&Server::handleConnection, this, client_fd).detach(); - } - } - - void handleConnection(socket_t client_fd) { - // Set receive timeout -#ifdef _WIN32 - DWORD timeout = 30000; // 30 seconds in milliseconds - setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout)); - setsockopt(client_fd, SOL_SOCKET, SO_SNDTIMEO, (const char*)&timeout, sizeof(timeout)); -#else - struct timeval timeout; - timeout.tv_sec = 30; - timeout.tv_usec = 0; - setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); - setsockopt(client_fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)); -#endif - - HttpRequest request; - HttpResponse response; - - if (!parseRequest(client_fd, request, response)) { - // Send error response if one was set - if (response.status_code != 200) { - std::string response_str = response.serialize(); - send(client_fd, response_str.c_str(), response_str.length(), 0); - } - CLOSE_SOCKET(client_fd); - active_connections_--; - return; - } - - // Process request and generate response - response = processRequest(request); - - // Send response - std::string response_str = response.serialize(); - send(client_fd, response_str.c_str(), response_str.length(), 0); - - // Close connection (HTTP/1.1 could support keep-alive, but simple close for now) - CLOSE_SOCKET(client_fd); - active_connections_--; - } - - bool parseRequest(socket_t client_fd, HttpRequest& request, HttpResponse& error_response) { - // Use larger buffer but still enforce limits - std::string request_data; - request_data.reserve(8192); - - char buffer[8192]; - size_t total_read = 0; - bool headers_complete = false; - - // Read request with size limit - while (total_read < MAX_REQUEST_SIZE && !headers_complete) { -#ifdef _WIN32 - int bytes_read = recv(client_fd, buffer, sizeof(buffer), 0); -#else - ssize_t bytes_read = recv(client_fd, buffer, sizeof(buffer), 0); -#endif - - if (bytes_read <= 0) { - return false; - } - - request_data.append(buffer, bytes_read); - total_read += bytes_read; - - // Check if headers are complete - if (request_data.find("\r\n\r\n") != std::string::npos) { - headers_complete = true; - } - } - - // Check if request is too large - if (total_read >= MAX_REQUEST_SIZE && !headers_complete) { - error_response.status_code = 413; - error_response.status_text = "Request Entity Too Large"; - error_response.body = R"({"error":"Request too large"})"; - return false; - } - - // Parse request line - size_t line_end = request_data.find("\r\n"); - if (line_end == std::string::npos) { - error_response.status_code = 400; - error_response.status_text = "Bad Request"; - error_response.body = R"({"error":"Invalid request format"})"; - return false; - } - - std::string request_line = request_data.substr(0, line_end); - std::istringstream line_stream(request_line); - line_stream >> request.method >> request.path >> request.version; - - // Validate method, path, and version - if (request.method.empty() || request.path.empty() || request.version.empty()) { - error_response.status_code = 400; - error_response.status_text = "Bad Request"; - error_response.body = R"({"error":"Invalid request line"})"; - return false; - } - - // Check for null bytes in path (CVE pattern) - if (request.path.find('\0') != std::string::npos) { - error_response.status_code = 400; - error_response.status_text = "Bad Request"; - error_response.body = R"({"error":"Null byte in path"})"; - return false; - } - - // Validate path length - if (request.path.length() > MAX_PATH_LENGTH) { - error_response.status_code = 414; - error_response.status_text = "URI Too Long"; - error_response.body = R"({"error":"Path too long"})"; - return false; - } - - // Parse headers - size_t pos = line_end + 2; - size_t header_count = 0; - bool has_content_length = false; - bool has_transfer_encoding = false; - size_t content_length = 0; - - while (pos < request_data.length()) { - line_end = request_data.find("\r\n", pos); - if (line_end == std::string::npos) break; - - std::string header_line = request_data.substr(pos, line_end - pos); - if (header_line.empty()) { - // End of headers - pos = line_end + 2; - break; - } - - // Check header bomb protection - if (++header_count > MAX_HEADERS) { - error_response.status_code = 431; - error_response.status_text = "Request Header Fields Too Large"; - error_response.body = R"({"error":"Too many headers"})"; - return false; - } - - // Check header size - if (header_line.length() > MAX_HEADER_SIZE) { - error_response.status_code = 431; - error_response.status_text = "Request Header Fields Too Large"; - error_response.body = R"({"error":"Header too large"})"; - return false; - } - - size_t colon = header_line.find(':'); - if (colon != std::string::npos) { - std::string key = header_line.substr(0, colon); - std::string value = header_line.substr(colon + 1); - - // Trim whitespace - while (!value.empty() && value[0] == ' ') value = value.substr(1); - while (!value.empty() && value[value.length()-1] == ' ') value.pop_back(); - - // Check for CRLF injection in header values - if (value.find("\r\n") != std::string::npos) { - error_response.status_code = 400; - error_response.status_text = "Bad Request"; - error_response.body = R"({"error":"CRLF in header value"})"; - return false; - } - - // Check for null bytes in headers - if (value.find('\0') != std::string::npos) { - error_response.status_code = 400; - error_response.status_text = "Bad Request"; - error_response.body = R"({"error":"Null byte in header"})"; - return false; - } - - // Detect duplicate Content-Length headers (CVE-2024-1135 pattern) - std::string key_lower = key; - std::transform(key_lower.begin(), key_lower.end(), key_lower.begin(), ::tolower); - - if (key_lower == "content-length") { - if (has_content_length) { - // Multiple Content-Length headers - request smuggling attempt - error_response.status_code = 400; - error_response.status_text = "Bad Request"; - error_response.body = R"({"error":"Multiple Content-Length headers"})"; - return false; - } - has_content_length = true; - - // Validate Content-Length is a valid number - try { - // Check for integer overflow - unsigned long long cl = std::stoull(value); - if (cl > MAX_BODY_SIZE) { - error_response.status_code = 413; - error_response.status_text = "Request Entity Too Large"; - error_response.body = R"({"error":"Content-Length too large"})"; - return false; - } - // Validate fits in size_t (platform dependent) - if (cl > std::numeric_limits::max()) { - error_response.status_code = 413; - error_response.status_text = "Request Entity Too Large"; - error_response.body = R"({"error":"Content-Length exceeds platform limit"})"; - return false; - } - content_length = static_cast(cl); - } catch (...) { - error_response.status_code = 400; - error_response.status_text = "Bad Request"; - error_response.body = R"({"error":"Invalid Content-Length"})"; - return false; - } - } - - // Detect Transfer-Encoding header (CVE-2024-23452 pattern) - if (key_lower == "transfer-encoding") { - has_transfer_encoding = true; - } - - request.headers[key] = value; - } - - pos = line_end + 2; - } - - // Check for request smuggling: Transfer-Encoding + Content-Length - // Per RFC 7230: "If a message is received with both a Transfer-Encoding - // and a Content-Length header field, the Transfer-Encoding overrides the Content-Length" - if (has_transfer_encoding && has_content_length) { - error_response.status_code = 400; - error_response.status_text = "Bad Request"; - error_response.body = R"({"error":"Both Transfer-Encoding and Content-Length present"})"; - return false; - } - - // We don't support Transfer-Encoding (chunked), return 501 Not Implemented - if (has_transfer_encoding) { - error_response.status_code = 501; - error_response.status_text = "Not Implemented"; - error_response.body = R"({"error":"Transfer-Encoding not supported"})"; - return false; - } - - // Parse body if present - if (pos < request_data.length()) { - request.body = request_data.substr(pos); - } - - return true; - } - - HttpResponse processRequest(const HttpRequest& request) { - HttpResponse response; - - // Health check endpoint (for nginx health checks) - if (request.path == "/health" || request.path == "/healthz") { - response.status_code = 200; - response.status_text = "OK"; - response.body = R"({"status":"healthy","service":"dbal"})"; - return response; - } - - // API endpoints - if (request.path == "/api/version" || request.path == "/version") { - response.body = R"({"version":"1.0.0","service":"DBAL Daemon"})"; - return response; - } - - if (request.path == "/api/status" || request.path == "/status") { - std::ostringstream body; - body << R"({"status":"running","address":")" << address() << R"(")" - << R"(,"real_ip":")" << request.realIP() << R"(")" - << R"(,"forwarded_proto":")" << request.forwardedProto() << R"(")" - << "}"; - response.body = body.str(); - return response; - } - - // Default 404 - response.status_code = 404; - response.status_text = "Not Found"; - response.body = R"({"error":"Not Found","path":")" + request.path + "\"}"; - return response; - } - - std::string getLastErrorString() { -#ifdef _WIN32 - int error = WSAGetLastError(); - char* message = nullptr; - FormatMessageA( - FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - nullptr, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - (LPSTR)&message, 0, nullptr); - std::string result = message ? message : "Unknown error"; - if (message) LocalFree(message); - return result; -#else - return strerror(errno); -#endif - } - - std::string bind_address_; - int port_; - bool running_; - socket_t server_fd_; - std::thread accept_thread_; - std::atomic active_connections_; -}; - -} -} diff --git a/dbal/cpp/src/daemon/server/parse_request_line.hpp b/dbal/cpp/src/daemon/server/parse_request_line.hpp new file mode 100644 index 000000000..f843c1c92 --- /dev/null +++ b/dbal/cpp/src/daemon/server/parse_request_line.hpp @@ -0,0 +1,42 @@ +/** + * @file parse_request_line.hpp + * @brief Parse HTTP request line + */ + +#pragma once + +#include +#include +#include "http_request.hpp" +#include "http_response.hpp" + +namespace dbal { +namespace daemon { + +/** + * @brief Parse HTTP request line (method, path, version) + * @param line Request line string + * @param request Request to populate + * @param error_response Error response if parsing fails + * @return true on success + */ +inline bool parse_request_line( + const std::string& line, + HttpRequest& request, + HttpResponse& error_response +) { + std::istringstream line_stream(line); + line_stream >> request.method >> request.path >> request.version; + + if (request.method.empty() || request.path.empty() || request.version.empty()) { + error_response.status_code = 400; + error_response.status_text = "Bad Request"; + error_response.body = R"({"error":"Invalid request line"})"; + return false; + } + + return true; +} + +} // namespace daemon +} // namespace dbal diff --git a/dbal/cpp/src/daemon/server/process_health_check.hpp b/dbal/cpp/src/daemon/server/process_health_check.hpp new file mode 100644 index 000000000..3d4a38add --- /dev/null +++ b/dbal/cpp/src/daemon/server/process_health_check.hpp @@ -0,0 +1,35 @@ +/** + * @file process_health_check.hpp + * @brief Handle health check endpoints + */ + +#pragma once + +#include +#include "http_request.hpp" +#include "http_response.hpp" + +namespace dbal { +namespace daemon { + +/** + * @brief Check if request is a health check and process it + * @param request HTTP request + * @param response HTTP response (populated if health check) + * @return true if this was a health check request + */ +inline bool process_health_check( + const HttpRequest& request, + HttpResponse& response +) { + if (request.path == "/health" || request.path == "/healthz") { + response.status_code = 200; + response.status_text = "OK"; + response.body = R"({"status":"healthy","service":"dbal"})"; + return true; + } + return false; +} + +} // namespace daemon +} // namespace dbal diff --git a/dbal/cpp/src/daemon/server/process_not_found.hpp b/dbal/cpp/src/daemon/server/process_not_found.hpp new file mode 100644 index 000000000..94c8c668a --- /dev/null +++ b/dbal/cpp/src/daemon/server/process_not_found.hpp @@ -0,0 +1,30 @@ +/** + * @file process_not_found.hpp + * @brief Handle 404 not found response + */ + +#pragma once + +#include +#include "http_request.hpp" +#include "http_response.hpp" + +namespace dbal { +namespace daemon { + +/** + * @brief Generate 404 not found response + * @param request HTTP request + * @param response HTTP response to populate + */ +inline void process_not_found( + const HttpRequest& request, + HttpResponse& response +) { + response.status_code = 404; + response.status_text = "Not Found"; + response.body = R"({"error":"Not Found","path":")" + request.path + "\"}"; +} + +} // namespace daemon +} // namespace dbal diff --git a/dbal/cpp/src/daemon/server/process_status.hpp b/dbal/cpp/src/daemon/server/process_status.hpp new file mode 100644 index 000000000..2205b9aee --- /dev/null +++ b/dbal/cpp/src/daemon/server/process_status.hpp @@ -0,0 +1,43 @@ +/** + * @file process_status.hpp + * @brief Handle status endpoint + */ + +#pragma once + +#include +#include +#include "http_request.hpp" +#include "http_response.hpp" +#include "request_real_ip.hpp" +#include "request_forwarded_proto.hpp" + +namespace dbal { +namespace daemon { + +/** + * @brief Check if request is a status request and process it + * @param request HTTP request + * @param address Server address + * @param response HTTP response (populated if status request) + * @return true if this was a status request + */ +inline bool process_status( + const HttpRequest& request, + const std::string& address, + HttpResponse& response +) { + if (request.path == "/api/status" || request.path == "/status") { + std::ostringstream body; + body << R"({"status":"running","address":")" << address << R"(")" + << R"(,"real_ip":")" << request_real_ip(request) << R"(")" + << R"(,"forwarded_proto":")" << request_forwarded_proto(request) << R"(")" + << "}"; + response.body = body.str(); + return true; + } + return false; +} + +} // namespace daemon +} // namespace dbal diff --git a/dbal/cpp/src/daemon/server/process_version.hpp b/dbal/cpp/src/daemon/server/process_version.hpp new file mode 100644 index 000000000..5d88af988 --- /dev/null +++ b/dbal/cpp/src/daemon/server/process_version.hpp @@ -0,0 +1,33 @@ +/** + * @file process_version.hpp + * @brief Handle version endpoint + */ + +#pragma once + +#include +#include "http_request.hpp" +#include "http_response.hpp" + +namespace dbal { +namespace daemon { + +/** + * @brief Check if request is a version request and process it + * @param request HTTP request + * @param response HTTP response (populated if version request) + * @return true if this was a version request + */ +inline bool process_version( + const HttpRequest& request, + HttpResponse& response +) { + if (request.path == "/api/version" || request.path == "/version") { + response.body = R"({"version":"1.0.0","service":"DBAL Daemon"})"; + return true; + } + return false; +} + +} // namespace daemon +} // namespace dbal diff --git a/dbal/cpp/src/daemon/server/server.hpp b/dbal/cpp/src/daemon/server/server.hpp new file mode 100644 index 000000000..a918835d3 --- /dev/null +++ b/dbal/cpp/src/daemon/server/server.hpp @@ -0,0 +1,90 @@ +/** + * @file server.hpp + * @brief HTTP Server class - thin wrapper importing micro-functions + */ + +#pragma once + +#include +#include +#include +#include + +// Socket functions +#include "socket_types.hpp" +#include "socket_create.hpp" +#include "socket_set_reuse_addr.hpp" +#include "socket_bind.hpp" +#include "socket_listen.hpp" +#include "socket_accept.hpp" +#include "socket_set_timeout.hpp" +#include "socket_send.hpp" +#include "socket_close.hpp" +#include "socket_get_last_error.hpp" +#include "winsock_init.hpp" + +// HTTP types +#include "http_request.hpp" +#include "http_response.hpp" + +// Response functions +#include "response_serialize.hpp" + +// Request parsing +#include "parse_request_line.hpp" +#include "validate_request_path.hpp" +#include "validate_header.hpp" +#include "validate_content_length.hpp" +#include "validate_transfer_encoding.hpp" +#include "trim_string.hpp" +#include "to_lowercase.hpp" + +// Request processing +#include "process_health_check.hpp" +#include "process_version.hpp" +#include "process_status.hpp" +#include "process_not_found.hpp" + +namespace dbal { +namespace daemon { + +/** + * @class Server + * @brief HTTP/1.1 server with nginx reverse proxy support + */ +class Server { +public: + Server(const std::string& bind_address, int port) + : bind_address_(bind_address), port_(port), running_(false), + server_fd_(INVALID_SOCKET_VALUE), active_connections_(0) { + winsock_init(); + } + + ~Server() { + stop(); + winsock_cleanup(); + } + + bool start(); + void stop(); + bool isRunning() const { return running_; } + std::string address() const { return bind_address_ + ":" + std::to_string(port_); } + +private: + void acceptLoop(); + void handleConnection(socket_t client_fd); + bool parseRequest(socket_t client_fd, HttpRequest& request, HttpResponse& error_response); + HttpResponse processRequest(const HttpRequest& request); + + std::string bind_address_; + int port_; + bool running_; + socket_t server_fd_; + std::thread accept_thread_; + std::atomic active_connections_; +}; + +// Implementation in server_impl.hpp + +} // namespace daemon +} // namespace dbal diff --git a/dbal/cpp/src/daemon/server/server_accept_loop.hpp b/dbal/cpp/src/daemon/server/server_accept_loop.hpp new file mode 100644 index 000000000..deb6ef867 --- /dev/null +++ b/dbal/cpp/src/daemon/server/server_accept_loop.hpp @@ -0,0 +1,42 @@ +/** + * @file server_accept_loop.hpp + * @brief Server accept loop implementation + */ + +#pragma once + +#include "server.hpp" + +namespace dbal { +namespace daemon { + +/** + * @brief Accept loop - runs in separate thread + */ +inline void Server::acceptLoop() { + while (running_) { + socket_t client_fd = socket_accept(server_fd_); + + if (client_fd == INVALID_SOCKET_VALUE) { + if (running_) { + std::cerr << "Accept failed: " << socket_get_last_error() << std::endl; + } + continue; + } + + // Check connection limit to prevent thread exhaustion DoS + size_t prev_count = active_connections_.fetch_add(1); + if (prev_count >= MAX_CONCURRENT_CONNECTIONS) { + std::cerr << "Connection limit reached, rejecting connection" << std::endl; + active_connections_--; + socket_close(client_fd); + continue; + } + + // Handle connection in a new thread + std::thread(&Server::handleConnection, this, client_fd).detach(); + } +} + +} // namespace daemon +} // namespace dbal diff --git a/dbal/cpp/src/daemon/server/server_handle_connection.hpp b/dbal/cpp/src/daemon/server/server_handle_connection.hpp new file mode 100644 index 000000000..ee1f4680a --- /dev/null +++ b/dbal/cpp/src/daemon/server/server_handle_connection.hpp @@ -0,0 +1,42 @@ +/** + * @file server_handle_connection.hpp + * @brief Server connection handler implementation + */ + +#pragma once + +#include "server.hpp" + +namespace dbal { +namespace daemon { + +/** + * @brief Handle a single client connection + */ +inline void Server::handleConnection(socket_t client_fd) { + socket_set_timeout(client_fd, 30); + + HttpRequest request; + HttpResponse response; + + if (!parseRequest(client_fd, request, response)) { + if (response.status_code != 200) { + std::string response_str = response_serialize(response); + socket_send(client_fd, response_str); + } + socket_close(client_fd); + active_connections_--; + return; + } + + response = processRequest(request); + + std::string response_str = response_serialize(response); + socket_send(client_fd, response_str); + + socket_close(client_fd); + active_connections_--; +} + +} // namespace daemon +} // namespace dbal diff --git a/dbal/cpp/src/daemon/server/server_impl.hpp b/dbal/cpp/src/daemon/server/server_impl.hpp new file mode 100644 index 000000000..c77bc3d4b --- /dev/null +++ b/dbal/cpp/src/daemon/server/server_impl.hpp @@ -0,0 +1,14 @@ +/** + * @file server_impl.hpp + * @brief Server implementation - includes all method implementations + */ + +#pragma once + +#include "server.hpp" +#include "server_start.hpp" +#include "server_stop.hpp" +#include "server_accept_loop.hpp" +#include "server_handle_connection.hpp" +#include "server_parse_request.hpp" +#include "server_process_request.hpp" diff --git a/dbal/cpp/src/daemon/server/server_parse_request.hpp b/dbal/cpp/src/daemon/server/server_parse_request.hpp new file mode 100644 index 000000000..aa4d94558 --- /dev/null +++ b/dbal/cpp/src/daemon/server/server_parse_request.hpp @@ -0,0 +1,158 @@ +/** + * @file server_parse_request.hpp + * @brief Server request parsing implementation + */ + +#pragma once + +#include +#include "server.hpp" + +namespace dbal { +namespace daemon { + +/** + * @brief Parse incoming HTTP request + */ +inline bool Server::parseRequest( + socket_t client_fd, + HttpRequest& request, + HttpResponse& error_response +) { + std::string request_data; + request_data.reserve(8192); + + char buffer[8192]; + size_t total_read = 0; + bool headers_complete = false; + + // Read request with size limit + while (total_read < MAX_REQUEST_SIZE && !headers_complete) { +#ifdef _WIN32 + int bytes_read = recv(client_fd, buffer, sizeof(buffer), 0); +#else + ssize_t bytes_read = recv(client_fd, buffer, sizeof(buffer), 0); +#endif + + if (bytes_read <= 0) return false; + + request_data.append(buffer, bytes_read); + total_read += bytes_read; + + if (request_data.find("\r\n\r\n") != std::string::npos) { + headers_complete = true; + } + } + + // Check size limit + if (total_read >= MAX_REQUEST_SIZE && !headers_complete) { + error_response.status_code = 413; + error_response.status_text = "Request Entity Too Large"; + error_response.body = R"({"error":"Request too large"})"; + return false; + } + + // Find request line + size_t line_end = request_data.find("\r\n"); + if (line_end == std::string::npos) { + error_response.status_code = 400; + error_response.status_text = "Bad Request"; + error_response.body = R"({"error":"Invalid request format"})"; + return false; + } + + // Parse request line + std::string request_line = request_data.substr(0, line_end); + if (!parse_request_line(request_line, request, error_response)) { + return false; + } + + // Validate path + if (!validate_request_path(request.path, error_response)) { + return false; + } + + // Parse headers + size_t pos = line_end + 2; + size_t header_count = 0; + bool has_content_length = false; + bool has_transfer_encoding = false; + size_t content_length = 0; + + while (pos < request_data.length()) { + line_end = request_data.find("\r\n", pos); + if (line_end == std::string::npos) break; + + std::string header_line = request_data.substr(pos, line_end - pos); + if (header_line.empty()) { + pos = line_end + 2; + break; + } + + // Validate header count + if (!validate_header_count(++header_count, error_response)) { + return false; + } + + // Validate header size + if (!validate_header_size(header_line.length(), error_response)) { + return false; + } + + size_t colon = header_line.find(':'); + if (colon != std::string::npos) { + std::string key = header_line.substr(0, colon); + std::string value = header_line.substr(colon + 1); + + trim_string(value); + + // Validate header value + if (!validate_header_value(value, error_response)) { + return false; + } + + std::string key_lower = to_lowercase(key); + + // Check Content-Length + if (key_lower == "content-length") { + if (!check_duplicate_content_length(has_content_length, error_response)) { + return false; + } + has_content_length = true; + + if (!validate_content_length(value, content_length, error_response)) { + return false; + } + } + + // Check Transfer-Encoding + if (key_lower == "transfer-encoding") { + has_transfer_encoding = true; + } + + request.headers[key] = value; + } + + pos = line_end + 2; + } + + // Check for request smuggling + if (!check_request_smuggling(has_transfer_encoding, has_content_length, error_response)) { + return false; + } + + // Reject Transfer-Encoding + if (!check_transfer_encoding_unsupported(has_transfer_encoding, error_response)) { + return false; + } + + // Parse body + if (pos < request_data.length()) { + request.body = request_data.substr(pos); + } + + return true; +} + +} // namespace daemon +} // namespace dbal diff --git a/dbal/cpp/src/daemon/server/server_process_request.hpp b/dbal/cpp/src/daemon/server/server_process_request.hpp new file mode 100644 index 000000000..72db26b24 --- /dev/null +++ b/dbal/cpp/src/daemon/server/server_process_request.hpp @@ -0,0 +1,36 @@ +/** + * @file server_process_request.hpp + * @brief Server request processing implementation + */ + +#pragma once + +#include "server.hpp" + +namespace dbal { +namespace daemon { + +/** + * @brief Process request and generate response + */ +inline HttpResponse Server::processRequest(const HttpRequest& request) { + HttpResponse response; + + if (process_health_check(request, response)) { + return response; + } + + if (process_version(request, response)) { + return response; + } + + if (process_status(request, address(), response)) { + return response; + } + + process_not_found(request, response); + return response; +} + +} // namespace daemon +} // namespace dbal diff --git a/dbal/cpp/src/daemon/server/server_start.hpp b/dbal/cpp/src/daemon/server/server_start.hpp new file mode 100644 index 000000000..5eca515cd --- /dev/null +++ b/dbal/cpp/src/daemon/server/server_start.hpp @@ -0,0 +1,54 @@ +/** + * @file server_start.hpp + * @brief Server start implementation + */ + +#pragma once + +#include "server.hpp" + +namespace dbal { +namespace daemon { + +/** + * @brief Start the server + */ +inline bool Server::start() { + if (running_) return false; + + // Create socket + server_fd_ = socket_create(); + if (server_fd_ == INVALID_SOCKET_VALUE) { + return false; + } + + // Set socket options + if (!socket_set_reuse_addr(server_fd_)) { + socket_close(server_fd_); + server_fd_ = INVALID_SOCKET_VALUE; + return false; + } + + // Bind to address + if (!socket_bind(server_fd_, bind_address_, port_)) { + socket_close(server_fd_); + server_fd_ = INVALID_SOCKET_VALUE; + return false; + } + + // Listen + if (!socket_listen(server_fd_)) { + socket_close(server_fd_); + server_fd_ = INVALID_SOCKET_VALUE; + return false; + } + + running_ = true; + accept_thread_ = std::thread(&Server::acceptLoop, this); + + std::cout << "Server listening on " << bind_address_ << ":" << port_ << std::endl; + return true; +} + +} // namespace daemon +} // namespace dbal diff --git a/dbal/cpp/src/daemon/server/server_stop.hpp b/dbal/cpp/src/daemon/server/server_stop.hpp new file mode 100644 index 000000000..e9aaae4e7 --- /dev/null +++ b/dbal/cpp/src/daemon/server/server_stop.hpp @@ -0,0 +1,34 @@ +/** + * @file server_stop.hpp + * @brief Server stop implementation + */ + +#pragma once + +#include "server.hpp" + +namespace dbal { +namespace daemon { + +/** + * @brief Stop the server + */ +inline void Server::stop() { + if (!running_) return; + + running_ = false; + + // Close server socket to unblock accept() + socket_close(server_fd_); + server_fd_ = INVALID_SOCKET_VALUE; + + // Wait for accept thread to finish + if (accept_thread_.joinable()) { + accept_thread_.join(); + } + + std::cout << "Server stopped" << std::endl; +} + +} // namespace daemon +} // namespace dbal diff --git a/dbal/cpp/src/daemon/server/to_lowercase.hpp b/dbal/cpp/src/daemon/server/to_lowercase.hpp new file mode 100644 index 000000000..9595b89de --- /dev/null +++ b/dbal/cpp/src/daemon/server/to_lowercase.hpp @@ -0,0 +1,27 @@ +/** + * @file to_lowercase.hpp + * @brief Convert string to lowercase + */ + +#pragma once + +#include +#include +#include + +namespace dbal { +namespace daemon { + +/** + * @brief Convert string to lowercase + * @param str String to convert + * @return Lowercase string + */ +inline std::string to_lowercase(const std::string& str) { + std::string result = str; + std::transform(result.begin(), result.end(), result.begin(), ::tolower); + return result; +} + +} // namespace daemon +} // namespace dbal diff --git a/dbal/cpp/src/daemon/server/trim_string.hpp b/dbal/cpp/src/daemon/server/trim_string.hpp new file mode 100644 index 000000000..ad0e932fb --- /dev/null +++ b/dbal/cpp/src/daemon/server/trim_string.hpp @@ -0,0 +1,27 @@ +/** + * @file trim_string.hpp + * @brief Trim whitespace from string + */ + +#pragma once + +#include + +namespace dbal { +namespace daemon { + +/** + * @brief Trim leading and trailing whitespace + * @param str String to trim (modified in place) + */ +inline void trim_string(std::string& str) { + while (!str.empty() && str[0] == ' ') { + str = str.substr(1); + } + while (!str.empty() && str[str.length() - 1] == ' ') { + str.pop_back(); + } +} + +} // namespace daemon +} // namespace dbal diff --git a/dbal/cpp/src/daemon/server/validate_content_length.hpp b/dbal/cpp/src/daemon/server/validate_content_length.hpp new file mode 100644 index 000000000..40f6c6b0c --- /dev/null +++ b/dbal/cpp/src/daemon/server/validate_content_length.hpp @@ -0,0 +1,77 @@ +/** + * @file validate_content_length.hpp + * @brief Validate Content-Length header + */ + +#pragma once + +#include +#include +#include "http_response.hpp" +#include "socket_types.hpp" + +namespace dbal { +namespace daemon { + +/** + * @brief Validate and parse Content-Length header + * @param value Header value + * @param content_length Output content length + * @param error_response Error response if validation fails + * @return true if valid + */ +inline bool validate_content_length( + const std::string& value, + size_t& content_length, + HttpResponse& error_response +) { + try { + // Check for integer overflow + unsigned long long cl = std::stoull(value); + + if (cl > MAX_BODY_SIZE) { + error_response.status_code = 413; + error_response.status_text = "Request Entity Too Large"; + error_response.body = R"({"error":"Content-Length too large"})"; + return false; + } + + // Validate fits in size_t (platform dependent) + if (cl > std::numeric_limits::max()) { + error_response.status_code = 413; + error_response.status_text = "Request Entity Too Large"; + error_response.body = R"({"error":"Content-Length exceeds platform limit"})"; + return false; + } + + content_length = static_cast(cl); + return true; + } catch (...) { + error_response.status_code = 400; + error_response.status_text = "Bad Request"; + error_response.body = R"({"error":"Invalid Content-Length"})"; + return false; + } +} + +/** + * @brief Check for duplicate Content-Length (CVE-2024-1135) + * @param has_content_length Whether Content-Length was already seen + * @param error_response Error response if duplicate + * @return true if not duplicate + */ +inline bool check_duplicate_content_length( + bool has_content_length, + HttpResponse& error_response +) { + if (has_content_length) { + error_response.status_code = 400; + error_response.status_text = "Bad Request"; + error_response.body = R"({"error":"Multiple Content-Length headers"})"; + return false; + } + return true; +} + +} // namespace daemon +} // namespace dbal diff --git a/dbal/cpp/src/daemon/server/validate_header.hpp b/dbal/cpp/src/daemon/server/validate_header.hpp new file mode 100644 index 000000000..d8fe51369 --- /dev/null +++ b/dbal/cpp/src/daemon/server/validate_header.hpp @@ -0,0 +1,83 @@ +/** + * @file validate_header.hpp + * @brief Validate HTTP header for security issues + */ + +#pragma once + +#include +#include "http_response.hpp" +#include "socket_types.hpp" + +namespace dbal { +namespace daemon { + +/** + * @brief Validate header value for CRLF injection and null bytes + * @param value Header value + * @param error_response Error response if validation fails + * @return true if header is valid + */ +inline bool validate_header_value( + const std::string& value, + HttpResponse& error_response +) { + // Check for CRLF injection in header values + if (value.find("\r\n") != std::string::npos) { + error_response.status_code = 400; + error_response.status_text = "Bad Request"; + error_response.body = R"({"error":"CRLF in header value"})"; + return false; + } + + // Check for null bytes in headers + if (value.find('\0') != std::string::npos) { + error_response.status_code = 400; + error_response.status_text = "Bad Request"; + error_response.body = R"({"error":"Null byte in header"})"; + return false; + } + + return true; +} + +/** + * @brief Validate header count (prevent header bomb) + * @param count Current header count + * @param error_response Error response if validation fails + * @return true if count is acceptable + */ +inline bool validate_header_count( + size_t count, + HttpResponse& error_response +) { + if (count > MAX_HEADERS) { + error_response.status_code = 431; + error_response.status_text = "Request Header Fields Too Large"; + error_response.body = R"({"error":"Too many headers"})"; + return false; + } + return true; +} + +/** + * @brief Validate header size + * @param size Header size + * @param error_response Error response if validation fails + * @return true if size is acceptable + */ +inline bool validate_header_size( + size_t size, + HttpResponse& error_response +) { + if (size > MAX_HEADER_SIZE) { + error_response.status_code = 431; + error_response.status_text = "Request Header Fields Too Large"; + error_response.body = R"({"error":"Header too large"})"; + return false; + } + return true; +} + +} // namespace daemon +} // namespace dbal diff --git a/dbal/cpp/src/daemon/server/validate_request_path.hpp b/dbal/cpp/src/daemon/server/validate_request_path.hpp new file mode 100644 index 000000000..8998db5fe --- /dev/null +++ b/dbal/cpp/src/daemon/server/validate_request_path.hpp @@ -0,0 +1,45 @@ +/** + * @file validate_path.hpp + * @brief Validate HTTP request path for security issues + */ + +#pragma once + +#include +#include "http_response.hpp" +#include "socket_types.hpp" + +namespace dbal { +namespace daemon { + +/** + * @brief Validate request path for null bytes and length + * @param path Request path + * @param error_response Error response if validation fails + * @return true if path is valid + */ +inline bool validate_request_path( + const std::string& path, + HttpResponse& error_response +) { + // Check for null bytes in path (CVE pattern) + if (path.find('\0') != std::string::npos) { + error_response.status_code = 400; + error_response.status_text = "Bad Request"; + error_response.body = R"({"error":"Null byte in path"})"; + return false; + } + + // Validate path length + if (path.length() > MAX_PATH_LENGTH) { + error_response.status_code = 414; + error_response.status_text = "URI Too Long"; + error_response.body = R"({"error":"Path too long"})"; + return false; + } + + return true; +} + +} // namespace daemon +} // namespace dbal diff --git a/dbal/cpp/src/daemon/server/validate_transfer_encoding.hpp b/dbal/cpp/src/daemon/server/validate_transfer_encoding.hpp new file mode 100644 index 000000000..e8addc512 --- /dev/null +++ b/dbal/cpp/src/daemon/server/validate_transfer_encoding.hpp @@ -0,0 +1,54 @@ +/** + * @file validate_transfer_encoding.hpp + * @brief Validate Transfer-Encoding header + */ + +#pragma once + +#include "http_response.hpp" + +namespace dbal { +namespace daemon { + +/** + * @brief Check for request smuggling (Transfer-Encoding + Content-Length) + * @param has_transfer_encoding Whether Transfer-Encoding is present + * @param has_content_length Whether Content-Length is present + * @param error_response Error response if both present + * @return true if valid + */ +inline bool check_request_smuggling( + bool has_transfer_encoding, + bool has_content_length, + HttpResponse& error_response +) { + if (has_transfer_encoding && has_content_length) { + error_response.status_code = 400; + error_response.status_text = "Bad Request"; + error_response.body = R"({"error":"Both Transfer-Encoding and Content-Length present"})"; + return false; + } + return true; +} + +/** + * @brief Reject Transfer-Encoding (not supported) + * @param has_transfer_encoding Whether Transfer-Encoding is present + * @param error_response Error response if present + * @return true if not present + */ +inline bool check_transfer_encoding_unsupported( + bool has_transfer_encoding, + HttpResponse& error_response +) { + if (has_transfer_encoding) { + error_response.status_code = 501; + error_response.status_text = "Not Implemented"; + error_response.body = R"({"error":"Transfer-Encoding not supported"})"; + return false; + } + return true; +} + +} // namespace daemon +} // namespace dbal diff --git a/dbal/ts/src/core/entities/lua-script/delete-lua-script.ts b/dbal/ts/src/core/entities/lua-script/delete-lua-script.ts index 4aaf7bc2c..10f857bab 100644 --- a/dbal/ts/src/core/entities/lua-script/delete-lua-script.ts +++ b/dbal/ts/src/core/entities/lua-script/delete-lua-script.ts @@ -2,25 +2,26 @@ * @file delete-lua-script.ts * @description Delete Lua script operation */ -import type { DBALAdapter } from '../../../adapters/adapter' -import { DBALError } from '../../errors' -import { validateId } from '../../validation' +import type { Result } from '../../types' +import type { InMemoryStore } from '../../store/in-memory-store' +import { validateId } from '../../validation/validate-id' /** * Delete a Lua script by ID */ -export async function deleteLuaScript(adapter: DBALAdapter, id: string): Promise { - const validationErrors = validateId(id) - if (validationErrors.length > 0) { - throw DBALError.validationError( - 'Invalid Lua script ID', - validationErrors.map(error => ({ field: 'id', error })) - ) +export const deleteLuaScript = async (store: InMemoryStore, id: string): Promise> => { + const idErrors = validateId(id) + if (idErrors.length > 0) { + return { success: false, error: { code: 'VALIDATION_ERROR', message: idErrors[0] } } } - const result = await adapter.delete('LuaScript', id) - if (!result) { - throw DBALError.notFound(`Lua script not found: ${id}`) + const script = store.luaScripts.get(id) + if (!script) { + return { success: false, error: { code: 'NOT_FOUND', message: `Lua script not found: ${id}` } } } - return result + + store.luaScripts.delete(id) + store.luaScriptNames.delete(script.name) + + return { success: true, data: true } }