code: dbal,cpp,hpp (21 files)

This commit is contained in:
2025-12-26 01:35:23 +00:00
parent ebfdc3023e
commit e4c62828eb
21 changed files with 981 additions and 616 deletions

View File

@@ -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 <string>
#include <thread>
#include <vector>
#include <memory>
#include <iostream>
#include <cstring>
#include <sstream>
#include <map>
#include <mutex>
#include <atomic>
#include <algorithm>
#include <cctype>
#include <limits>
// Cross-platform socket headers
#ifdef _WIN32
// Windows
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#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 <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
// 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<std::string, std::string> 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<std::string, std::string> 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<char*>(&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<size_t>::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<size_t>(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<size_t> active_connections_;
};
}
}

View File

@@ -0,0 +1,42 @@
/**
* @file parse_request_line.hpp
* @brief Parse HTTP request line
*/
#pragma once
#include <string>
#include <sstream>
#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

View File

@@ -0,0 +1,35 @@
/**
* @file process_health_check.hpp
* @brief Handle health check endpoints
*/
#pragma once
#include <string>
#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

View File

@@ -0,0 +1,30 @@
/**
* @file process_not_found.hpp
* @brief Handle 404 not found response
*/
#pragma once
#include <string>
#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

View File

@@ -0,0 +1,43 @@
/**
* @file process_status.hpp
* @brief Handle status endpoint
*/
#pragma once
#include <string>
#include <sstream>
#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

View File

@@ -0,0 +1,33 @@
/**
* @file process_version.hpp
* @brief Handle version endpoint
*/
#pragma once
#include <string>
#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

View File

@@ -0,0 +1,90 @@
/**
* @file server.hpp
* @brief HTTP Server class - thin wrapper importing micro-functions
*/
#pragma once
#include <string>
#include <thread>
#include <atomic>
#include <iostream>
// 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<size_t> active_connections_;
};
// Implementation in server_impl.hpp
} // namespace daemon
} // namespace dbal

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -0,0 +1,158 @@
/**
* @file server_parse_request.hpp
* @brief Server request parsing implementation
*/
#pragma once
#include <sstream>
#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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,27 @@
/**
* @file to_lowercase.hpp
* @brief Convert string to lowercase
*/
#pragma once
#include <string>
#include <algorithm>
#include <cctype>
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

View File

@@ -0,0 +1,27 @@
/**
* @file trim_string.hpp
* @brief Trim whitespace from string
*/
#pragma once
#include <string>
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

View File

@@ -0,0 +1,77 @@
/**
* @file validate_content_length.hpp
* @brief Validate Content-Length header
*/
#pragma once
#include <string>
#include <limits>
#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<size_t>::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<size_t>(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

View File

@@ -0,0 +1,83 @@
/**
* @file validate_header.hpp
* @brief Validate HTTP header for security issues
*/
#pragma once
#include <string>
#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

View File

@@ -0,0 +1,45 @@
/**
* @file validate_path.hpp
* @brief Validate HTTP request path for security issues
*/
#pragma once
#include <string>
#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

View File

@@ -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

View File

@@ -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<boolean> {
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<Result<boolean>> => {
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 }
}