From 4a0fe0cf9535f898af0a9d3f2a5c2811e45e019e Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sun, 23 Aug 2026 23:31:36 +1000 Subject: [PATCH 01/14] Add observable/observer interfaces Add generic Observer and Observable interfaces for event-style notifications. Observables keep weak references to registered observers, dispatch callbacks with a context object, and automatically remove expired observers while maintaining a clean notification pipeline. --- include/xtrpg/interface/Observable.hpp | 57 ++++++++++++++++++++++++++ include/xtrpg/interface/Observer.hpp | 13 ++++++ 2 files changed, 70 insertions(+) create mode 100644 include/xtrpg/interface/Observable.hpp create mode 100644 include/xtrpg/interface/Observer.hpp diff --git a/include/xtrpg/interface/Observable.hpp b/include/xtrpg/interface/Observable.hpp new file mode 100644 index 0000000..1ac61c0 --- /dev/null +++ b/include/xtrpg/interface/Observable.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include +#include + +#include "xtrpg/interface/Observer.hpp" + +namespace xtrpg::interface { +template class Observable { +public: + virtual ~Observable() = default; + + void addObserver(const std::shared_ptr> &ptr) { + if (ptr) { + this->_observers.push_back(ptr); + } + } + + void eraseObserver(const Observer *pTarget) { + if (!pTarget) { + return; + } + + std::erase_if(this->_observers, + [pTarget](const std::weak_ptr> &wp) { + auto sp = wp.lock(); + return !sp || sp.get() == pTarget; + }); + } + + void clearObservers() { this->_observers.clear(); } + +protected: + void dispatchObservation(TContext &ctx) { + std::erase_if(this->_observers, + [&ctx](const std::weak_ptr> &wp) { + // Attempt to gain temporary ownership of the current + // observer. + if (auto observer = wp.lock()) { + // Dispatch the observation. + observer->onObservation(ctx); + + // Pointer is still valid, keep it in the vector. + return false; + } + + // Pointer has expired, remove it from the observers vector. + return true; + }); + } + +private: + std::vector>> _observers; +}; +} // namespace xtrpg \ No newline at end of file diff --git a/include/xtrpg/interface/Observer.hpp b/include/xtrpg/interface/Observer.hpp new file mode 100644 index 0000000..b6e9234 --- /dev/null +++ b/include/xtrpg/interface/Observer.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace xtrpg::interface { +template class Observer { +public: + virtual ~Observer() = default; + + // Generic handler callback + virtual void onObservation(TContext &ctx) = 0; +}; +} // namespace xtrpg::interface \ No newline at end of file From 6cd648b2b13abb495b548a69897ea9ecdfbff86d Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sun, 23 Aug 2026 23:32:04 +1000 Subject: [PATCH 02/14] Add TcpConnection with optional TLS upgrade Introduce TcpConnection (header + implementation) to manage a TCP socket and optionally upgrade it to TLS. Provides upgradeToTls(ssl_ctx) to wrap the socket into an asio::ssl::stream and perform async handshake, write(string_view) that async_writes to either the SSL stream or raw socket, close() that cleanly shuts down/ closes TLS and raw connections, and operator<< stream-style writer. Tracks _isClosed and _isTlsActive and logs handshake/write/close errors. Designed as a shared_from_this-enabled connection helper for async servers. --- include/xtrpg/network/TcpConnection.hpp | 39 +++++++++++++ src/network/TcpConnection.cpp | 74 +++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 include/xtrpg/network/TcpConnection.hpp create mode 100644 src/network/TcpConnection.cpp diff --git a/include/xtrpg/network/TcpConnection.hpp b/include/xtrpg/network/TcpConnection.hpp new file mode 100644 index 0000000..48e3286 --- /dev/null +++ b/include/xtrpg/network/TcpConnection.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace xtrpg::network { + +class TcpConnection : public std::enable_shared_from_this { + +public: + explicit TcpConnection(asio::ip::tcp::socket &tcpSocket) + : _tcpSocket(std::move(tcpSocket)) {} + + void upgradeToTls(asio::ssl::context &ssl_ctx); + + void write(std::string_view data); + + void close(); + + bool isClosed() const { return this->_isClosed; } + + /** + * Stream writer to write data to the socket. + */ + TcpConnection &operator<<(std::string_view str) { + this->write(str); + return *this; + } + +private: + bool _isClosed{false}; + bool _isTlsActive{false}; + asio::ip::tcp::socket _tcpSocket; + std::optional> _sslStream; +}; +} // namespace xtrpg::network \ No newline at end of file diff --git a/src/network/TcpConnection.cpp b/src/network/TcpConnection.cpp new file mode 100644 index 0000000..334a2bb --- /dev/null +++ b/src/network/TcpConnection.cpp @@ -0,0 +1,74 @@ +#include "xtrpg/network/TcpConnection.hpp" + +namespace xtrpg::network { + +void TcpConnection::upgradeToTls(asio::ssl::context &ssl_ctx) { + if (this->_isClosed) { + // the TCP connection is closed + std::cerr << "Unable to upgrade a closed TCP connection to TLS." + << std::endl; + return; + } + + // Wrap the existing raw socket into Asio SSL stream + this->_sslStream.emplace(std::move(this->_tcpSocket), ssl_ctx); + + auto self = shared_from_this(); + this->_sslStream->async_handshake( + asio::ssl::stream_base::server, [this, self](std::error_code ec) { + if (ec) { + std::cerr << "TLS Handshake Failed: " << ec.message() << "\n"; + return; + } + + this->_isTlsActive = true; + }); +} + +void TcpConnection::write(std::string_view data) { + if (this->_isClosed) { + // the TCP connection is closed + std::cerr << "Unable to write to a closed TCP connection." << std::endl; + return; + } + + if (this->_isTlsActive && this->_sslStream) { + // Writing to the secure stream; + asio::async_write(*this->_sslStream, asio::buffer(data), + [](std::error_code, std::size_t) {}); + return; + } + + // Fallback to the raw connection + asio::async_write(this->_tcpSocket, asio::buffer(data), + [](std::error_code, std::size_t) {}); +} + +void TcpConnection::close() { + if (this->_isClosed) { + std::cerr << "TCP Connection is already closed" << std::endl; + return; + } + + this->_isClosed = true; + + if (this->_isTlsActive && this->_sslStream) { + this->_sslStream->lowest_layer().cancel(); + + auto self = shared_from_this(); + this->_sslStream->async_shutdown([this, self](const asio::error_code &ec) { + // Shut down the underlying TCP transport layer + this->_sslStream->lowest_layer().shutdown( + asio::ip::tcp::socket::shutdown_both); + + // Close the socket to free the file descriptor + this->_sslStream->lowest_layer().close(); + }); + return; + } + + this->_tcpSocket.shutdown(asio::ip::tcp::socket::shutdown_both); + this->_tcpSocket.close(); +} + +} // namespace xtrpg::network \ No newline at end of file From 6a7ce2f4ae8bd82fd363f97f0f85e0cd250e430c Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sun, 23 Aug 2026 23:43:07 +1000 Subject: [PATCH 03/14] Add SocketConnectionListener (IPv4/IPv6) Introduce SocketConnectionListener (header and implementation). The class derives from Observable> and manages ASIO IPv4 and IPv6 acceptors bound to a given port. Provides start/stop methods, runs async_accept loops for both address families, logs new connections, constructs shared TcpConnection instances and dispatches them to observers. stop() closes acceptors with std::error_code. --- .../network/SocketConnectionListener.hpp | 38 +++++++++++ src/network/SocketConnectionListener.cpp | 68 +++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 include/xtrpg/network/SocketConnectionListener.hpp create mode 100644 src/network/SocketConnectionListener.cpp diff --git a/include/xtrpg/network/SocketConnectionListener.hpp b/include/xtrpg/network/SocketConnectionListener.hpp new file mode 100644 index 0000000..2fe4d23 --- /dev/null +++ b/include/xtrpg/network/SocketConnectionListener.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "xtrpg/interface/Observable.hpp" +#include "xtrpg/network/TcpConnection.hpp" + +namespace xtrpg::network { +class SocketConnectionListener + : public interface::Observable> { +public: + /** + * Instantiates a new listener instance. + */ + SocketConnectionListener(asio::io_context &ioContext, uint16_t port) + : _ioContext(ioContext), + _ipv4Acceptor(ioContext, + asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port)), + _ipv6Acceptor(ioContext, + asio::ip::tcp::endpoint(asio::ip::tcp::v6(), port)) {}; + + void start(); + void stop(); + +private: + void acceptIPv4Connections(); + void acceptIPv6Connections(); + + asio::io_context &_ioContext; + asio::ip::tcp::acceptor _ipv4Acceptor; + asio::ip::tcp::acceptor _ipv6Acceptor; + bool _isStopped{true}; +}; +} // namespace xtrpg::network \ No newline at end of file diff --git a/src/network/SocketConnectionListener.cpp b/src/network/SocketConnectionListener.cpp new file mode 100644 index 0000000..48fb843 --- /dev/null +++ b/src/network/SocketConnectionListener.cpp @@ -0,0 +1,68 @@ +#include "xtrpg/network/SocketConnectionListener.hpp" + +namespace xtrpg::network { + +void SocketConnectionListener::start() { + if (!this->_isStopped) { + return; + } + this->_isStopped = false; + + std::cout << "[SocketConnectionListener] Listening for socket connections." + << std::endl + << " IPv4 on port " + << _ipv4Acceptor.local_endpoint().port() << "." << std::endl + << " IPv6 on port " + << _ipv6Acceptor.local_endpoint().port() << "." << std::endl; + this->acceptIPv4Connections(); + this->acceptIPv6Connections(); +} + +void SocketConnectionListener::stop() { + if (this->_isStopped) + return; + this->_isStopped = true; + + std::error_code ec; + this->_ipv4Acceptor.close(ec); + this->_ipv6Acceptor.close(ec); + std::cout << "[SocketConnectionListener] Stopped listening for socket " + "connections." + << std::endl; + ; +} + +void SocketConnectionListener::acceptIPv4Connections() { + this->_ipv4Acceptor.async_accept([this](std::error_code ec, + asio::ip::tcp::socket socket) { + if (!ec) { + std::cout << "[SocketConnectionListener] New incoming IPv4 connection." + << std::endl; + + auto tcpConnection = std::make_shared(std::move(socket)); + this->dispatchObservation(tcpConnection); + } + + if (!this->_isStopped) { + this->acceptIPv4Connections(); + } + }); +} + +void SocketConnectionListener::acceptIPv6Connections() { + this->_ipv6Acceptor.async_accept([this](std::error_code ec, + asio::ip::tcp::socket socket) { + if (!ec) { + std::cout << "[SocketConnectionListener] New incoming IPv6 connection." + << std::endl; + + auto tcpConnection = std::make_shared(std::move(socket)); + this->dispatchObservation(tcpConnection); + } + + if (!this->_isStopped) { + this->acceptIPv6Connections(); + } + }); +} +} // namespace xtrpg::network \ No newline at end of file From f3b8de6b904a1f9c57cda564bdc1e91528653e1d Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sun, 23 Aug 2026 23:44:38 +1000 Subject: [PATCH 04/14] Add xtrpg_network lib and link to executable Introduce xtrpg_network static library (src/network/SocketConnectionListener.cpp, src/network/TcpConnection.cpp). Add public includes and link against asio and OpenSSL. Apply platform-specific settings: set _WIN32_WINNT/WINVER and link ws2_32/wsock32/crypt32 on Windows; link pthread and dl on Unix. Link xtrpg_network into xtrpg_cpp_server. Removes the previously commented global target_link_libraries snippet in CMakeLists.txt. --- CMakeLists.txt | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 96e5f9b..40e3f34 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,11 +49,6 @@ find_package(UserModules QUIET) # target_include_directories(xtrpg_core PUBLIC include) # Link imported targets across platforms -# target_link_libraries(xtrpg_core PUBLIC -# asio::asio -# OpenSSL::SSL -# OpenSSL::Crypto -# ) # Platform-specific OS network primitives for raw sockets # if(WIN32) @@ -93,6 +88,31 @@ add_library(xtrpg_config STATIC ) target_include_directories(xtrpg_config PUBLIC include) +# Network LIB +add_library(xtrpg_network STATIC + src/network/SocketConnectionListener.cpp + src/network/TcpConnection.cpp +) +target_include_directories(xtrpg_network PUBLIC include) +target_link_libraries(xtrpg_network PUBLIC + asio::asio + OpenSSL::SSL + OpenSSL::Crypto +) +if(WIN32) + # Windows-specific OS network primitives for raw sockets + # Define minimum Windows version (0x0601 = Windows 7, 0x0A00 = Windows 10) + # 0x0A00 unlocks modern Windows socket features for Asio + target_compile_definitions(xtrpg_network PUBLIC + _WIN32_WINNT=0x0A00 + WINVER=0x0A00 + ) + target_link_libraries(xtrpg_network PUBLIC ws2_32 wsock32 crypt32) +elseif(UNIX AND NOT APPLE) + # Platform-specific OS network primitives for raw sockets + target_link_libraries(xtrpg_network PUBLIC pthread dl) +endif() + # XMPP LIB add_library(xtrpg_xmpp STATIC src/xmpp/Jid.cpp @@ -101,7 +121,7 @@ target_include_directories(xtrpg_xmpp PUBLIC include) # Executable Target add_executable(xtrpg_cpp_server apps/main.cpp) -target_link_libraries(xtrpg_cpp_server PRIVATE xtrpg_config xtrpg_xmpp) +target_link_libraries(xtrpg_cpp_server PRIVATE xtrpg_config xtrpg_network xtrpg_xmpp) target_include_directories(xtrpg_cpp_server PRIVATE ${CMAKE_BINARY_DIR}/generated) # If modules produce targets registered via xmpp_register_user_module From 00051cf91be87c066d00ccc41d3d508e00080ffa Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sun, 23 Aug 2026 23:45:53 +1000 Subject: [PATCH 05/14] Remove commented platform-specific linking block Clean up CMakeLists.txt by removing a large commented-out block that referenced platform-specific linking (ws2_32, wsock32, crypt32 for Windows; pthread, dl for Unix) and related notes about imported targets. This reduces clutter and keeps the file focused on active configuration; no functional changes were made. --- CMakeLists.txt | 9 --------- 1 file changed, 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 40e3f34..e34affd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,15 +48,6 @@ find_package(UserModules QUIET) # ) # target_include_directories(xtrpg_core PUBLIC include) -# Link imported targets across platforms - -# Platform-specific OS network primitives for raw sockets -# if(WIN32) -# target_link_libraries(xtrpg_core PUBLIC ws2_32 wsock32 crypt32) -# elseif(UNIX AND NOT APPLE) -# target_link_libraries(xtrpg_core PUBLIC pthread dl) -# endif() - # Conditional Storage Compilation # if(ENABLE_STORAGE_SQLITE) # find_package(SQLite3 REQUIRED) From 46d0070d34349bfbbefdb1c9000e6918884de42c Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sun, 23 Aug 2026 23:46:41 +1000 Subject: [PATCH 06/14] Handle Windows UTF-8 startup This change configures the Windows console to use UTF-8, wraps initialization in a try/catch so startup exceptions are surfaced cleanly, and preserves the existing preamble and config registration flow for TOML and CLI parsing. --- apps/main.cpp | 67 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 24 deletions(-) diff --git a/apps/main.cpp b/apps/main.cpp index 2ecab76..4dc4d44 100644 --- a/apps/main.cpp +++ b/apps/main.cpp @@ -1,35 +1,54 @@ #include #include +#ifdef _WIN32 +#include +#endif + #include "xtrpg/config/ConfigManager.hpp" int main(int argc, char *argv[]) { +#ifdef _WIN32 + // Set console codepages to UTF-8 (65001) + SetConsoleOutputCP(CP_UTF8); + SetConsoleCP(CP_UTF8); +#endif + + try { + + // Output preamble information about the application and its configuration + // system. + std::cout + << "XTRPG: A XMPP Server" << std::endl + << "Version: 0.1.0" << std::endl + << "Copyright (C) 2026 XTRPG Contributors" << std::endl + << "License: MIT" << std::endl + << " " + "" + << std::endl + << std::endl + << "This is free and open-source software. You are free to use, " + "modify, and redistribute it under the terms of the MIT License." + << std::endl + << "There is NO WARRANTY for this software." << std::endl + << std::endl; + + // Discover and register all modules' configuration schemas, then load the + // configuration file and parse command-line arguments. + xtrpg::config::ConfigManager configManager; + configManager.registerAllDiscoveredModules(); + std::ifstream configFile("./config.toml"); + if (configFile) { + configManager.loadTomlFile(configFile); + } + configManager.parseCLI(argc, argv); - // Output preamble information about the application and its configuration - // system. - std::cout << "XTRPG: A XMPP Server" << std::endl - << "Version: 0.1.0" << std::endl - << "Copyright (C) 2026 XTRPG Contributors" << std::endl - << "License: MIT" << std::endl - << " " - "" - << std::endl - << std::endl - << "This is free and open-source software. You are free to use, " - "modify, and redistribute it under the terms of the MIT License." - << std::endl - << "There is NO WARRANTY for this software." << std::endl - << std::endl; - - // Discover and register all modules' configuration schemas, then load the - // configuration file and parse command-line arguments. - xtrpg::config::ConfigManager configManager; - configManager.registerAllDiscoveredModules(); - std::ifstream configFile("./config.toml"); - if (configFile) { - configManager.loadTomlFile(configFile); + } catch (const std::exception &e) { + std::cerr << "EXCEPTION OCCURRED" << std::endl + << "Application closing die to \"" << e.what() << "\"." + << std ::endl; + return 1; } - configManager.parseCLI(argc, argv); return 0; } \ No newline at end of file From 3faa3310cc072944c6788bb8a3fa5d9402ce6678 Mon Sep 17 00:00:00 2001 From: Xeno Fox Date: Mon, 24 Aug 2026 11:53:46 +1000 Subject: [PATCH 07/14] Create ConnectionClosed.hpp --- .../network/exception/ConnectionClosed.hpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 include/xtrpg/network/exception/ConnectionClosed.hpp diff --git a/include/xtrpg/network/exception/ConnectionClosed.hpp b/include/xtrpg/network/exception/ConnectionClosed.hpp new file mode 100644 index 0000000..dc40a6a --- /dev/null +++ b/include/xtrpg/network/exception/ConnectionClosed.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace xtrpg::network::exception { + +/** + * Exception thrown when an operation is attempted on a closed connection. + */ +class ConnectionClosed : public std::runtime_error { +public: + /** + * Constructs a ConnectionClosed exception with a default error message. + */ + explicit ConnectionClosed() : std::runtime_error("Connection is closed") {} +}; +} // namespace xtrpg::network::exception \ No newline at end of file From 399d4928c155915522121891a8f117925ec742bf Mon Sep 17 00:00:00 2001 From: Xeno Fox Date: Mon, 24 Aug 2026 12:08:43 +1000 Subject: [PATCH 08/14] Cleaning up the TcpConnection class --- include/xtrpg/network/TcpConnection.hpp | 89 +++++++++++++++++++++++-- src/network/TcpConnection.cpp | 35 ++++------ 2 files changed, 98 insertions(+), 26 deletions(-) diff --git a/include/xtrpg/network/TcpConnection.hpp b/include/xtrpg/network/TcpConnection.hpp index 48e3286..e7db07c 100644 --- a/include/xtrpg/network/TcpConnection.hpp +++ b/include/xtrpg/network/TcpConnection.hpp @@ -6,21 +6,90 @@ #include #include +#include "xtrpg/network/exception/ConnectionClosed.hpp" + namespace xtrpg::network { +/** + * Represents the state of a TCP connection. + */ +enum class ConnectionState { + /** + * The connection is closed. + */ + CLOSED, + /** + * The connection is closing. + */ + CLOSING, + /** + * The connection is secure (SSL/TLS). + */ + SECURE, + /** + * The connection is insecure (TCP). + */ + INSECURE +}; + +/** + * Represents a TCP connection that can be upgraded to TLS. It provides methods + * to write data to the connection, read data from the connection and close it. + */ class TcpConnection : public std::enable_shared_from_this { public: + /** + * Constructs a TcpConnection with the given TCP socket. + */ explicit TcpConnection(asio::ip::tcp::socket &tcpSocket) : _tcpSocket(std::move(tcpSocket)) {} - void upgradeToTls(asio::ssl::context &ssl_ctx); + /** + * Upgrades the TCP connection to a TLS connection using the provided SSL + * context. + */ + void upgrade(asio::ssl::context &ssl_ctx); + /** + * Writes data to the connection. If the connection is closed, it will throw + * an exception. + */ void write(std::string_view data); + /** + * Closes the connection. If the connection is already closed, it will do + * nothing. + */ void close(); - bool isClosed() const { return this->_isClosed; } + /** + * Checks if the connection is secure (SSL/TLS). + */ + bool isSecure() const { return ConnectionState::SECURE == this->_state; } + + /** + * Checks if the connection is open. + */ + bool isOpen() const { + return ConnectionState::CLOSED != this->_state && + ConnectionState::CLOSING != this->_state; + } + + /** + * Checks if the connection is closed. + */ + bool isClosed() const { return ConnectionState::CLOSED == this->_state; } + + /** + * Asserts that the connection is open.If the connection is closed, + * it will throw an exception. + */ + void assertOpenConnection() const { + if (!this->isOpen()) { + throw exception::ConnectionClosed(); + } + } /** * Stream writer to write data to the socket. @@ -31,9 +100,21 @@ class TcpConnection : public std::enable_shared_from_this { } private: - bool _isClosed{false}; - bool _isTlsActive{false}; + /** + * The current state of the connection. + */ + ConnectionState _state{ConnectionState::INSECURE}; + + /** + * The underlying TCP socket used for the connection. + */ asio::ip::tcp::socket _tcpSocket; + + /** + * The optional SSL stream used for secure communication. It is only + * initialized when the connection is upgraded to TLS. If the connection is + * not secure, this will be std::nullopt. + */ std::optional> _sslStream; }; } // namespace xtrpg::network \ No newline at end of file diff --git a/src/network/TcpConnection.cpp b/src/network/TcpConnection.cpp index 334a2bb..523450d 100644 --- a/src/network/TcpConnection.cpp +++ b/src/network/TcpConnection.cpp @@ -2,13 +2,8 @@ namespace xtrpg::network { -void TcpConnection::upgradeToTls(asio::ssl::context &ssl_ctx) { - if (this->_isClosed) { - // the TCP connection is closed - std::cerr << "Unable to upgrade a closed TCP connection to TLS." - << std::endl; - return; - } +void TcpConnection::upgrade(asio::ssl::context &ssl_ctx) { + this->assertOpenConnection(); // Wrap the existing raw socket into Asio SSL stream this->_sslStream.emplace(std::move(this->_tcpSocket), ssl_ctx); @@ -20,19 +15,14 @@ void TcpConnection::upgradeToTls(asio::ssl::context &ssl_ctx) { std::cerr << "TLS Handshake Failed: " << ec.message() << "\n"; return; } - - this->_isTlsActive = true; + this->__state = ConnectionState::SECURE; }); } void TcpConnection::write(std::string_view data) { - if (this->_isClosed) { - // the TCP connection is closed - std::cerr << "Unable to write to a closed TCP connection." << std::endl; - return; - } + this->assertOpenConnection(); - if (this->_isTlsActive && this->_sslStream) { + if (this->isSecure() && this->_sslStream) { // Writing to the secure stream; asio::async_write(*this->_sslStream, asio::buffer(data), [](std::error_code, std::size_t) {}); @@ -45,14 +35,10 @@ void TcpConnection::write(std::string_view data) { } void TcpConnection::close() { - if (this->_isClosed) { - std::cerr << "TCP Connection is already closed" << std::endl; - return; - } - - this->_isClosed = true; + this->assertOpenConnection(); - if (this->_isTlsActive && this->_sslStream) { + if (this->isSecure() && this->_sslStream) { + this->_state = ConnectionState::CLOSING; this->_sslStream->lowest_layer().cancel(); auto self = shared_from_this(); @@ -63,12 +49,17 @@ void TcpConnection::close() { // Close the socket to free the file descriptor this->_sslStream->lowest_layer().close(); + + this->_state = ConnectionState::CLOSED; }); return; } + this->_state = ConnectionState::CLOSING; this->_tcpSocket.shutdown(asio::ip::tcp::socket::shutdown_both); this->_tcpSocket.close(); + + this->_state = ConnectionState::CLOSED; } } // namespace xtrpg::network \ No newline at end of file From 3333ba88befd1bd36eba6fe71f0d0c8acf036a4a Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Mon, 24 Aug 2026 16:25:10 +1000 Subject: [PATCH 09/14] Take tcp socket by value; fix state member Change TcpConnection constructor to accept asio::ip::tcp::socket by value and move it into the member to allow ownership transfer and avoid binding to a temporary/reference. Also fix a typo in TcpConnection::upgrade: use the correct member name `_state` instead of `__state`. These are minor API/cleanup fixes to ensure correct move semantics and proper member access. --- include/xtrpg/network/TcpConnection.hpp | 2 +- src/network/TcpConnection.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/xtrpg/network/TcpConnection.hpp b/include/xtrpg/network/TcpConnection.hpp index e7db07c..b52959a 100644 --- a/include/xtrpg/network/TcpConnection.hpp +++ b/include/xtrpg/network/TcpConnection.hpp @@ -42,7 +42,7 @@ class TcpConnection : public std::enable_shared_from_this { /** * Constructs a TcpConnection with the given TCP socket. */ - explicit TcpConnection(asio::ip::tcp::socket &tcpSocket) + explicit TcpConnection(asio::ip::tcp::socket tcpSocket) : _tcpSocket(std::move(tcpSocket)) {} /** diff --git a/src/network/TcpConnection.cpp b/src/network/TcpConnection.cpp index 523450d..3ffc623 100644 --- a/src/network/TcpConnection.cpp +++ b/src/network/TcpConnection.cpp @@ -15,7 +15,7 @@ void TcpConnection::upgrade(asio::ssl::context &ssl_ctx) { std::cerr << "TLS Handshake Failed: " << ec.message() << "\n"; return; } - this->__state = ConnectionState::SECURE; + this->_state = ConnectionState::SECURE; }); } From fdb5b240a256bc6d0ef7d481ece7d6e40171fbbe Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Mon, 24 Aug 2026 16:34:32 +1000 Subject: [PATCH 10/14] Use generated version metadata in app This updates the app to display the configured project version instead of a hardcoded 0.1.0 value, and includes the generated version header. It also fixes the generated version metadata to use the correct CMake version variables and the xtrpg namespace, while switching the fallback BUILD_VERSION to 0.0.0 so version info stays consistent across builds. --- CMakeLists.txt | 2 +- apps/main.cpp | 29 +++++++++++++++-------------- generated/version.hpp.in | 12 ++++++------ 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e34affd..70c4ecc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ endif() # If no variable is passed from the environment/command line, use a fallback if(NOT DEFINED BUILD_VERSION) - set(BUILD_VERSION "0.1.0") + set(BUILD_VERSION "0.0.0") endif() # Separate the SemVer string into major, minor, patch parts diff --git a/apps/main.cpp b/apps/main.cpp index 4dc4d44..4dfb620 100644 --- a/apps/main.cpp +++ b/apps/main.cpp @@ -5,6 +5,7 @@ #include #endif +#include "version.hpp" #include "xtrpg/config/ConfigManager.hpp" int main(int argc, char *argv[]) { @@ -18,20 +19,20 @@ int main(int argc, char *argv[]) { // Output preamble information about the application and its configuration // system. - std::cout - << "XTRPG: A XMPP Server" << std::endl - << "Version: 0.1.0" << std::endl - << "Copyright (C) 2026 XTRPG Contributors" << std::endl - << "License: MIT" << std::endl - << " " - "" - << std::endl - << std::endl - << "This is free and open-source software. You are free to use, " - "modify, and redistribute it under the terms of the MIT License." - << std::endl - << "There is NO WARRANTY for this software." << std::endl - << std::endl; + std::cout << "XTRPG: A XMPP Server" << std::endl + << "Version: " << xtrpg::VERSION << std::endl + << "Copyright (C) 2026 XTRPG Contributors" << std::endl + << "License: MIT" << std::endl + << " " + "" + << std::endl + << std::endl + << "This is free and open-source software." << std::endl + << "You are free to use, modify, and redistribute it under the " + "terms of the MIT License." + << std::endl + << "There is NO WARRANTY for this software." << std::endl + << std::endl; // Discover and register all modules' configuration schemas, then load the // configuration file and parse command-line arguments. diff --git a/generated/version.hpp.in b/generated/version.hpp.in index 0801610..1d5248a 100644 --- a/generated/version.hpp.in +++ b/generated/version.hpp.in @@ -1,10 +1,10 @@ #pragma once -#include +#include -namespace xtprg { +namespace xtrpg { constexpr std::string_view VERSION = "@PROJECT_VERSION@"; -constexpr int VERSION_MAJOR = @PROJECT_VERSION_MAJOR @; -constexpr int VERSION_MINOR = @PROJECT_VERSION_MINOR @; -constexpr int VERSION_PATCH = @PROJECT_VERSION_PATCH @; -} // namespace xtprg \ No newline at end of file +constexpr int VERSION_MAJOR = @CMAKE_PROJECT_VERSION_MAJOR@; +constexpr int VERSION_MINOR = @CMAKE_PROJECT_VERSION_MINOR@; +constexpr int VERSION_PATCH = @CMAKE_PROJECT_VERSION_PATCH@; +} // namespace xtrpg \ No newline at end of file From 8e6fe6afbda495077bac4ea2b28839956bdad602 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Mon, 24 Aug 2026 16:50:57 +1000 Subject: [PATCH 11/14] Serialize TcpConnection lifecycle access This change adds a strand to TcpConnection so socket writes, upgrades, and closes cannot race each other. It also introduces a closing-state check and guards upgrade/close/write paths against closed, closing, or already-secure connections. Handshake and I/O failures now close the underlying socket and transition the connection to CLOSED, preventing stale state and invalid async operations. --- include/xtrpg/network/TcpConnection.hpp | 15 +++- src/network/TcpConnection.cpp | 109 +++++++++++++++--------- 2 files changed, 82 insertions(+), 42 deletions(-) diff --git a/include/xtrpg/network/TcpConnection.hpp b/include/xtrpg/network/TcpConnection.hpp index b52959a..d38e618 100644 --- a/include/xtrpg/network/TcpConnection.hpp +++ b/include/xtrpg/network/TcpConnection.hpp @@ -43,7 +43,9 @@ class TcpConnection : public std::enable_shared_from_this { * Constructs a TcpConnection with the given TCP socket. */ explicit TcpConnection(asio::ip::tcp::socket tcpSocket) - : _tcpSocket(std::move(tcpSocket)) {} + : _tcpSocket(std::move(tcpSocket)) { + this->_strand.emplace(asio::make_strand(this->_tcpSocket.get_executor())); + } /** * Upgrades the TCP connection to a TLS connection using the provided SSL @@ -68,6 +70,11 @@ class TcpConnection : public std::enable_shared_from_this { */ bool isSecure() const { return ConnectionState::SECURE == this->_state; } + /** + * Checks if the connection is closing. + */ + bool isClosing() const { return ConnectionState::CLOSING == this->_state; } + /** * Checks if the connection is open. */ @@ -110,6 +117,12 @@ class TcpConnection : public std::enable_shared_from_this { */ asio::ip::tcp::socket _tcpSocket; + /** + * Serializes access to the socket and connection state so writes, upgrades, + * and closes cannot race against each other. + */ + std::optional> _strand; + /** * The optional SSL stream used for secure communication. It is only * initialized when the connection is upgraded to TLS. If the connection is diff --git a/src/network/TcpConnection.cpp b/src/network/TcpConnection.cpp index 3ffc623..f7437c3 100644 --- a/src/network/TcpConnection.cpp +++ b/src/network/TcpConnection.cpp @@ -3,63 +3,90 @@ namespace xtrpg::network { void TcpConnection::upgrade(asio::ssl::context &ssl_ctx) { - this->assertOpenConnection(); + auto self = shared_from_this(); + asio::post(*this->_strand, [this, self, &ssl_ctx]() { + if (this->isClosed() || this->isClosing() || this->isSecure()) { + return; + } - // Wrap the existing raw socket into Asio SSL stream - this->_sslStream.emplace(std::move(this->_tcpSocket), ssl_ctx); + this->_sslStream.emplace(std::move(this->_tcpSocket), ssl_ctx); - auto self = shared_from_this(); - this->_sslStream->async_handshake( - asio::ssl::stream_base::server, [this, self](std::error_code ec) { - if (ec) { - std::cerr << "TLS Handshake Failed: " << ec.message() << "\n"; - return; - } - this->_state = ConnectionState::SECURE; - }); + this->_sslStream->async_handshake( + asio::ssl::stream_base::server, [this, self](std::error_code ec) { + if (ec) { + this->_state = ConnectionState::CLOSED; + if (this->_sslStream) { + this->_sslStream->lowest_layer().close(); + } + return; + } + this->_state = ConnectionState::SECURE; + }); + }); } void TcpConnection::write(std::string_view data) { - this->assertOpenConnection(); + auto payload = std::make_shared(data); + auto self = shared_from_this(); - if (this->isSecure() && this->_sslStream) { - // Writing to the secure stream; - asio::async_write(*this->_sslStream, asio::buffer(data), - [](std::error_code, std::size_t) {}); - return; - } + asio::post(*this->_strand, [this, self, payload]() { + if (!this->isOpen()) { + throw exception::ConnectionClosed(); + } - // Fallback to the raw connection - asio::async_write(this->_tcpSocket, asio::buffer(data), - [](std::error_code, std::size_t) {}); + if (this->isSecure() && this->_sslStream) { + asio::async_write(*this->_sslStream, asio::buffer(*payload), + [this, self, payload](std::error_code ec, std::size_t) { + if (ec) { + this->_state = ConnectionState::CLOSED; + if (this->_sslStream) { + this->_sslStream->lowest_layer().close(); + } + } + }); + return; + } + + asio::async_write(this->_tcpSocket, asio::buffer(*payload), + [this, self, payload](std::error_code ec, std::size_t) { + if (ec) { + this->_state = ConnectionState::CLOSED; + this->_tcpSocket.close(); + } + }); + }); } void TcpConnection::close() { - this->assertOpenConnection(); + auto self = shared_from_this(); + asio::post(*this->_strand, [this, self]() { + if (this->isClosed() || this->isClosing()) { + return; + } - if (this->isSecure() && this->_sslStream) { this->_state = ConnectionState::CLOSING; - this->_sslStream->lowest_layer().cancel(); - - auto self = shared_from_this(); - this->_sslStream->async_shutdown([this, self](const asio::error_code &ec) { - // Shut down the underlying TCP transport layer - this->_sslStream->lowest_layer().shutdown( - asio::ip::tcp::socket::shutdown_both); - // Close the socket to free the file descriptor - this->_sslStream->lowest_layer().close(); + if (this->isSecure() && this->_sslStream) { + this->_sslStream->lowest_layer().cancel(); - this->_state = ConnectionState::CLOSED; - }); - return; - } + this->_sslStream->async_shutdown( + [this, self](const asio::error_code &ec) { + if (this->_sslStream) { + this->_sslStream->lowest_layer().shutdown( + asio::ip::tcp::socket::shutdown_both); + this->_sslStream->lowest_layer().close(); + } - this->_state = ConnectionState::CLOSING; - this->_tcpSocket.shutdown(asio::ip::tcp::socket::shutdown_both); - this->_tcpSocket.close(); + this->_state = ConnectionState::CLOSED; + }); + return; + } - this->_state = ConnectionState::CLOSED; + std::error_code ec; + this->_tcpSocket.shutdown(asio::ip::tcp::socket::shutdown_both, ec); + this->_tcpSocket.close(); + this->_state = ConnectionState::CLOSED; + }); } } // namespace xtrpg::network \ No newline at end of file From 34a76dbf279dd6a56344f9346003fa928deb96ac Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Mon, 24 Aug 2026 17:01:29 +1000 Subject: [PATCH 12/14] Enable dual-stack and optional acceptors Replace concrete IPv4/IPv6 acceptors with std::optional acceptors and add initializeAcceptors() to attempt an IPv6 dual-stack socket first, falling back to IPv4. Add robust start/stop logic that checks optionals, cancels/closes acceptors safely, and guards accept loops. Introduce isListenerShutdownError helper to suppress expected shutdown errors and improve logging for accept failures. --- .../network/SocketConnectionListener.hpp | 15 +-- src/network/SocketConnectionListener.cpp | 102 +++++++++++++++--- 2 files changed, 94 insertions(+), 23 deletions(-) diff --git a/include/xtrpg/network/SocketConnectionListener.hpp b/include/xtrpg/network/SocketConnectionListener.hpp index 2fe4d23..3b2a71d 100644 --- a/include/xtrpg/network/SocketConnectionListener.hpp +++ b/include/xtrpg/network/SocketConnectionListener.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "xtrpg/interface/Observable.hpp" @@ -17,22 +18,22 @@ class SocketConnectionListener * Instantiates a new listener instance. */ SocketConnectionListener(asio::io_context &ioContext, uint16_t port) - : _ioContext(ioContext), - _ipv4Acceptor(ioContext, - asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port)), - _ipv6Acceptor(ioContext, - asio::ip::tcp::endpoint(asio::ip::tcp::v6(), port)) {}; + : _ioContext(ioContext), _port(port), _isStopped(true) { + this->initializeAcceptors(); + } void start(); void stop(); private: + void initializeAcceptors(); void acceptIPv4Connections(); void acceptIPv6Connections(); asio::io_context &_ioContext; - asio::ip::tcp::acceptor _ipv4Acceptor; - asio::ip::tcp::acceptor _ipv6Acceptor; + uint16_t _port; + std::optional _ipv4Acceptor; + std::optional _ipv6Acceptor; bool _isStopped{true}; }; } // namespace xtrpg::network \ No newline at end of file diff --git a/src/network/SocketConnectionListener.cpp b/src/network/SocketConnectionListener.cpp index 48fb843..5f0f414 100644 --- a/src/network/SocketConnectionListener.cpp +++ b/src/network/SocketConnectionListener.cpp @@ -2,6 +2,46 @@ namespace xtrpg::network { +namespace { +bool isListenerShutdownError(const std::error_code &ec) { + return ec == asio::error::operation_aborted || + ec == asio::error::bad_descriptor; +} +} // namespace + +void SocketConnectionListener::initializeAcceptors() { + try { + asio::ip::tcp::acceptor ipv6Acceptor( + this->_ioContext, + asio::ip::tcp::endpoint(asio::ip::tcp::v6(), this->_port)); + + asio::ip::v6_only option(false); + ipv6Acceptor.set_option(option); + this->_ipv6Acceptor.emplace(std::move(ipv6Acceptor)); + + std::cout + << "[SocketConnectionListener] Enabled IPv6 dual-stack listener on " + << this->_port << std::endl; + return; + } catch (const std::exception &ex) { + std::cerr << "[SocketConnectionListener] Failed to open IPv6 dual-stack " + "socket on " + << "port " << this->_port << ": " << ex.what() << std::endl; + this->_ipv6Acceptor.reset(); + } + + try { + this->_ipv4Acceptor.emplace( + this->_ioContext, + asio::ip::tcp::endpoint(asio::ip::tcp::v4(), this->_port)); + } catch (const std::exception &ex) { + std::cerr + << "[SocketConnectionListener] Failed to open IPv4 socket on port " + << this->_port << ": " << ex.what() << std::endl; + this->_ipv4Acceptor.reset(); + } +} + void SocketConnectionListener::start() { if (!this->_isStopped) { return; @@ -9,13 +49,21 @@ void SocketConnectionListener::start() { this->_isStopped = false; std::cout << "[SocketConnectionListener] Listening for socket connections." - << std::endl - << " IPv4 on port " - << _ipv4Acceptor.local_endpoint().port() << "." << std::endl - << " IPv6 on port " - << _ipv6Acceptor.local_endpoint().port() << "." << std::endl; - this->acceptIPv4Connections(); - this->acceptIPv6Connections(); + << std::endl; + + if (this->_ipv4Acceptor) { + std::cout << " IPv4 on port " + << this->_ipv4Acceptor->local_endpoint().port() << "." + << std::endl; + this->acceptIPv4Connections(); + } + + if (this->_ipv6Acceptor) { + std::cout << " IPv6 on port " + << this->_ipv6Acceptor->local_endpoint().port() << "." + << std::endl; + this->acceptIPv6Connections(); + } } void SocketConnectionListener::stop() { @@ -24,43 +72,65 @@ void SocketConnectionListener::stop() { this->_isStopped = true; std::error_code ec; - this->_ipv4Acceptor.close(ec); - this->_ipv6Acceptor.close(ec); + + if (this->_ipv4Acceptor) { + this->_ipv4Acceptor->cancel(ec); + this->_ipv4Acceptor->close(ec); + } + + if (this->_ipv6Acceptor) { + this->_ipv6Acceptor->cancel(ec); + this->_ipv6Acceptor->close(ec); + } + std::cout << "[SocketConnectionListener] Stopped listening for socket " "connections." << std::endl; - ; } void SocketConnectionListener::acceptIPv4Connections() { - this->_ipv4Acceptor.async_accept([this](std::error_code ec, - asio::ip::tcp::socket socket) { + if (!this->_ipv4Acceptor) { + return; + } + + this->_ipv4Acceptor->async_accept([this](std::error_code ec, + asio::ip::tcp::socket socket) { if (!ec) { std::cout << "[SocketConnectionListener] New incoming IPv4 connection." << std::endl; auto tcpConnection = std::make_shared(std::move(socket)); this->dispatchObservation(tcpConnection); + } else if (!isListenerShutdownError(ec)) { + std::cerr << "[SocketConnectionListener] IPv4 accept failed: " + << ec.message() << std::endl; } - if (!this->_isStopped) { + if (!this->_isStopped && !isListenerShutdownError(ec)) { this->acceptIPv4Connections(); } }); } void SocketConnectionListener::acceptIPv6Connections() { - this->_ipv6Acceptor.async_accept([this](std::error_code ec, - asio::ip::tcp::socket socket) { + if (!this->_ipv6Acceptor) { + return; + } + + this->_ipv6Acceptor->async_accept([this](std::error_code ec, + asio::ip::tcp::socket socket) { if (!ec) { std::cout << "[SocketConnectionListener] New incoming IPv6 connection." << std::endl; auto tcpConnection = std::make_shared(std::move(socket)); this->dispatchObservation(tcpConnection); + } else if (!isListenerShutdownError(ec)) { + std::cerr << "[SocketConnectionListener] IPv6 accept failed: " + << ec.message() << std::endl; } - if (!this->_isStopped) { + if (!this->_isStopped && !isListenerShutdownError(ec)) { this->acceptIPv6Connections(); } }); From e18d53d30a12502f621a1b46fdc77edc8e582aed Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Mon, 24 Aug 2026 17:09:27 +1000 Subject: [PATCH 13/14] Stop listener on destruction Add a destructor to SocketConnectionListener that calls stop() during cleanup. This ensures acceptors and socket resources are shut down when the listener is destroyed, preventing lingering network state. --- include/xtrpg/network/SocketConnectionListener.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/xtrpg/network/SocketConnectionListener.hpp b/include/xtrpg/network/SocketConnectionListener.hpp index 3b2a71d..885e9ea 100644 --- a/include/xtrpg/network/SocketConnectionListener.hpp +++ b/include/xtrpg/network/SocketConnectionListener.hpp @@ -22,6 +22,8 @@ class SocketConnectionListener this->initializeAcceptors(); } + ~SocketConnectionListener() { this->stop(); } + void start(); void stop(); From 5f28081154522805408850a1a8fbb412c691d316 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Mon, 24 Aug 2026 17:11:29 +1000 Subject: [PATCH 14/14] Fix Windows socket include conflict This patch prevents WinSock conflicts on Windows by defining `_WINSOCKAPI_` before including . It also reorders the includes so the project headers are included consistently with the platform-specific header setup. --- apps/main.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/main.cpp b/apps/main.cpp index 4dfb620..c693556 100644 --- a/apps/main.cpp +++ b/apps/main.cpp @@ -1,13 +1,14 @@ #include #include +#include "version.hpp" +#include "xtrpg/config/ConfigManager.hpp" + #ifdef _WIN32 +#define _WINSOCKAPI_ #include #endif -#include "version.hpp" -#include "xtrpg/config/ConfigManager.hpp" - int main(int argc, char *argv[]) { #ifdef _WIN32 // Set console codepages to UTF-8 (65001)