From 46edb7cd26133edc4271b65d4586bc5798abcd7d Mon Sep 17 00:00:00 2001 From: Andrew Noyes Date: Wed, 3 Sep 2025 16:09:16 -0400 Subject: [PATCH] Allow listening on multiple interfaces --- config.toml | 6 +- src/config.cpp | 77 ++++++++++++++++++------- src/config.hpp | 33 +++++++++-- src/main.cpp | 146 ++++++++++++++++++++++++----------------------- src/server.cpp | 8 ++- test_config.toml | 8 ++- 6 files changed, 172 insertions(+), 106 deletions(-) diff --git a/config.toml b/config.toml index 0f4148e..e51875f 100644 --- a/config.toml +++ b/config.toml @@ -1,8 +1,10 @@ # WeaselDB Configuration File [server] -bind_address = "127.0.0.1" -port = 8080 +# Network interfaces to listen on - production config with just TCP +interfaces = [ + { type = "tcp", address = "127.0.0.1", port = 8080 } +] # Maximum request size in bytes (for 413 Content Too Large responses) max_request_size_bytes = 1048576 # 1MB # Number of I/O threads for handling connections and network events diff --git a/src/config.cpp b/src/config.cpp index 044931b..58ae998 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -79,9 +79,31 @@ void ConfigParser::parse_section(const auto &toml_data, void ConfigParser::parse_server_config(const auto &toml_data, ServerConfig &config) { parse_section(toml_data, "server", [&](const auto &srv) { - parse_field(srv, "bind_address", config.bind_address); - parse_field(srv, "port", config.port); - parse_field(srv, "unix_socket_path", config.unix_socket_path); + // Parse interfaces array + if (srv.contains("interfaces")) { + auto interfaces = srv.at("interfaces"); + if (interfaces.is_array()) { + for (const auto &iface : interfaces.as_array()) { + if (iface.contains("type")) { + std::string type = iface.at("type").as_string(); + if (type == "tcp") { + std::string address = iface.at("address").as_string(); + int port = iface.at("port").as_integer(); + config.interfaces.push_back(ListenInterface::tcp(address, port)); + } else if (type == "unix") { + std::string path = iface.at("path").as_string(); + config.interfaces.push_back(ListenInterface::unix_socket(path)); + } + } + } + } + } + + // If no interfaces configured, use default TCP interface + if (config.interfaces.empty()) { + config.interfaces.push_back(ListenInterface::tcp("127.0.0.1", 8080)); + } + parse_field(srv, "max_request_size_bytes", config.max_request_size_bytes); parse_field(srv, "io_threads", config.io_threads); @@ -127,24 +149,37 @@ void ConfigParser::parse_subscription_config(const auto &toml_data, bool ConfigParser::validate_config(const Config &config) { bool valid = true; - // Validate server configuration - if (config.server.unix_socket_path.empty()) { - // TCP mode validation - if (config.server.port <= 0 || config.server.port > 65535) { - std::cerr << "Configuration error: server.port must be between 1 and " - "65535, got " - << config.server.port << std::endl; - valid = false; - } - } else { - // Unix socket mode validation - if (config.server.unix_socket_path.length() > - 107) { // UNIX_PATH_MAX is typically 108 - std::cerr << "Configuration error: unix_socket_path too long (max 107 " - "chars), got " - << config.server.unix_socket_path.length() << " chars" - << std::endl; - valid = false; + // Validate server interfaces + if (config.server.interfaces.empty()) { + std::cerr << "Configuration error: no interfaces configured" << std::endl; + valid = false; + } + + for (const auto &iface : config.server.interfaces) { + if (iface.type == ListenInterface::Type::TCP) { + if (iface.port <= 0 || iface.port > 65535) { + std::cerr << "Configuration error: TCP port must be between 1 and " + "65535, got " + << iface.port << std::endl; + valid = false; + } + if (iface.address.empty()) { + std::cerr << "Configuration error: TCP address cannot be empty" + << std::endl; + valid = false; + } + } else { // Unix socket + if (iface.path.empty()) { + std::cerr << "Configuration error: Unix socket path cannot be empty" + << std::endl; + valid = false; + } + if (iface.path.length() > 107) { // UNIX_PATH_MAX is typically 108 + std::cerr << "Configuration error: Unix socket path too long (max 107 " + "chars), got " + << iface.path.length() << " chars" << std::endl; + valid = false; + } } } diff --git a/src/config.hpp b/src/config.hpp index b22e7d2..7a2854d 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -3,19 +3,40 @@ #include #include #include +#include namespace weaseldb { +/** + * @brief Configuration for a single network interface to listen on. + */ +struct ListenInterface { + enum class Type { TCP, Unix }; + + Type type; + /// For TCP: IP address to bind to (e.g., "127.0.0.1", "0.0.0.0") + std::string address; + /// For TCP: port number + int port = 0; + /// For Unix: socket file path + std::string path; + + // Factory methods for cleaner config creation + static ListenInterface tcp(const std::string &addr, int port_num) { + return {Type::TCP, addr, port_num, ""}; + } + + static ListenInterface unix_socket(const std::string &socket_path) { + return {Type::Unix, "", 0, socket_path}; + } +}; + /** * @brief Configuration settings for the WeaselDB server component. */ struct ServerConfig { - /// IP address to bind the server to (default: localhost) - std::string bind_address = "127.0.0.1"; - /// TCP port number for the server to listen on - int port = 8080; - /// Unix socket path (if specified, takes precedence over TCP) - std::string unix_socket_path; + /// Network interfaces to listen on (TCP and/or Unix sockets) + std::vector interfaces; /// Maximum size in bytes for incoming HTTP requests (default: 1MB) int64_t max_request_size_bytes = 1024 * 1024; /// Number of I/O threads for handling connections and network events diff --git a/src/main.cpp b/src/main.cpp index 83d0385..aad7b3a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -29,48 +29,41 @@ void signal_handler(int sig) { } } -std::vector create_listen_sockets(const weaseldb::Config &config) { - std::vector listen_fds; - - // Check if unix socket path is specified - if (!config.server.unix_socket_path.empty()) { - // Create unix socket - int sfd = socket(AF_UNIX, SOCK_STREAM, 0); - if (sfd == -1) { - perror("socket"); - std::abort(); - } - - // Remove existing socket file if it exists - unlink(config.server.unix_socket_path.c_str()); - - struct sockaddr_un addr; - std::memset(&addr, 0, sizeof(addr)); - addr.sun_family = AF_UNIX; - - if (config.server.unix_socket_path.length() >= sizeof(addr.sun_path)) { - std::fprintf(stderr, "Unix socket path too long\n"); - std::abort(); - } - - std::strncpy(addr.sun_path, config.server.unix_socket_path.c_str(), - sizeof(addr.sun_path) - 1); - - if (bind(sfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) { - perror("bind"); - std::abort(); - } - - if (listen(sfd, SOMAXCONN) == -1) { - perror("listen"); - std::abort(); - } - - listen_fds.push_back(sfd); - return listen_fds; +int create_unix_socket(const std::string &path) { + int sfd = socket(AF_UNIX, SOCK_STREAM, 0); + if (sfd == -1) { + perror("socket"); + std::abort(); } - // TCP socket creation + // Remove existing socket file if it exists + unlink(path.c_str()); + + struct sockaddr_un addr; + std::memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + + if (path.length() >= sizeof(addr.sun_path)) { + std::fprintf(stderr, "Unix socket path too long: %s\n", path.c_str()); + std::abort(); + } + + std::strncpy(addr.sun_path, path.c_str(), sizeof(addr.sun_path) - 1); + + if (bind(sfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) { + perror("bind"); + std::abort(); + } + + if (listen(sfd, SOMAXCONN) == -1) { + perror("listen"); + std::abort(); + } + + return sfd; +} + +int create_tcp_socket(const std::string &address, int port) { struct addrinfo hints; struct addrinfo *result, *rp; int s; @@ -84,8 +77,8 @@ std::vector create_listen_sockets(const weaseldb::Config &config) { hints.ai_addr = nullptr; hints.ai_next = nullptr; - s = getaddrinfo(config.server.bind_address.c_str(), - std::to_string(config.server.port).c_str(), &hints, &result); + s = getaddrinfo(address.c_str(), std::to_string(port).c_str(), &hints, + &result); if (s != 0) { std::fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s)); std::abort(); @@ -94,18 +87,13 @@ std::vector create_listen_sockets(const weaseldb::Config &config) { int sfd = -1; for (rp = result; rp != nullptr; rp = rp->ai_next) { sfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); - if (sfd == -1) { + if (sfd == -1) continue; - } int val = 1; if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) == -1) { perror("setsockopt SO_REUSEADDR"); - int e = close(sfd); - if (e == -1 && errno != EINTR) { - perror("close sfd (SO_REUSEADDR failed)"); - std::abort(); - } + close(sfd); continue; } @@ -113,40 +101,56 @@ std::vector create_listen_sockets(const weaseldb::Config &config) { if (rp->ai_family == AF_INET || rp->ai_family == AF_INET6) { if (setsockopt(sfd, IPPROTO_TCP, TCP_NODELAY, &val, sizeof(val)) == -1) { perror("setsockopt TCP_NODELAY"); - int e = close(sfd); - if (e == -1 && errno != EINTR) { - perror("close sfd (TCP_NODELAY failed)"); - std::abort(); - } + close(sfd); continue; } } if (bind(sfd, rp->ai_addr, rp->ai_addrlen) == 0) { + if (listen(sfd, SOMAXCONN) == -1) { + perror("listen"); + close(sfd); + freeaddrinfo(result); + std::abort(); + } break; /* Success */ } - int e = close(sfd); - if (e == -1 && errno != EINTR) { - perror("close sfd (bind failed)"); - std::abort(); - } + close(sfd); sfd = -1; } freeaddrinfo(result); - if (rp == nullptr || sfd == -1) { - std::fprintf(stderr, "Could not bind to any address\n"); + if (sfd == -1) { + std::fprintf(stderr, "Could not bind to %s:%d\n", address.c_str(), port); std::abort(); } - if (listen(sfd, SOMAXCONN) == -1) { - perror("listen"); + return sfd; +} + +std::vector create_listen_sockets(const weaseldb::Config &config) { + std::vector listen_fds; + + for (const auto &iface : config.server.interfaces) { + int fd; + if (iface.type == weaseldb::ListenInterface::Type::TCP) { + fd = create_tcp_socket(iface.address, iface.port); + std::cout << "Listening on TCP " << iface.address << ":" << iface.port + << std::endl; + } else { + fd = create_unix_socket(iface.path); + std::cout << "Listening on Unix socket " << iface.path << std::endl; + } + listen_fds.push_back(fd); + } + + if (listen_fds.empty()) { + std::fprintf(stderr, "No interfaces configured\n"); std::abort(); } - listen_fds.push_back(sfd); return listen_fds; } @@ -218,13 +222,13 @@ int main(int argc, char *argv[]) { } std::cout << "Configuration loaded successfully:" << std::endl; - if (!config->server.unix_socket_path.empty()) { - std::cout << "Unix socket path: " << config->server.unix_socket_path - << std::endl; - } else { - std::cout << "Server bind address: " << config->server.bind_address - << std::endl; - std::cout << "Server port: " << config->server.port << std::endl; + std::cout << "Interfaces: " << config->server.interfaces.size() << std::endl; + for (const auto &iface : config->server.interfaces) { + if (iface.type == weaseldb::ListenInterface::Type::TCP) { + std::cout << " TCP: " << iface.address << ":" << iface.port << std::endl; + } else { + std::cout << " Unix socket: " << iface.path << std::endl; + } } std::cout << "Max request size: " << config->server.max_request_size_bytes << " bytes" << std::endl; diff --git a/src/server.cpp b/src/server.cpp index 77fde1f..ee384d5 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -98,9 +98,11 @@ Server::~Server() { } } - // Clean up unix socket file if it exists - if (!config_.server.unix_socket_path.empty()) { - unlink(config_.server.unix_socket_path.c_str()); + // Clean up unix socket files if they exist + for (const auto &iface : config_.server.interfaces) { + if (iface.type == weaseldb::ListenInterface::Type::Unix) { + unlink(iface.path.c_str()); + } } } diff --git a/test_config.toml b/test_config.toml index 2915869..d77b0db 100644 --- a/test_config.toml +++ b/test_config.toml @@ -1,9 +1,11 @@ # WeaselDB Configuration File [server] -unix_socket_path = "weaseldb.sock" -bind_address = "127.0.0.1" -port = 8080 +# Network interfaces to listen on - both TCP for external access and Unix socket for high-performance local testing +interfaces = [ + { type = "tcp", address = "127.0.0.1", port = 8080 }, + { type = "unix", path = "weaseldb.sock" } +] # Maximum request size in bytes (for 413 Content Too Large responses) max_request_size_bytes = 1048576 # 1MB # Number of I/O threads for handling connections and network events