diff --git a/CMakeLists.txt b/CMakeLists.txt index 96e5f9b..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 @@ -48,20 +48,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) -# 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) @@ -93,6 +79,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 +112,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 diff --git a/apps/main.cpp b/apps/main.cpp index 2ecab76..c693556 100644 --- a/apps/main.cpp +++ b/apps/main.cpp @@ -1,35 +1,56 @@ #include #include +#include "version.hpp" #include "xtrpg/config/ConfigManager.hpp" +#ifdef _WIN32 +#define _WINSOCKAPI_ +#include +#endif + 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: " << 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. + 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 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 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 diff --git a/include/xtrpg/network/SocketConnectionListener.hpp b/include/xtrpg/network/SocketConnectionListener.hpp new file mode 100644 index 0000000..885e9ea --- /dev/null +++ b/include/xtrpg/network/SocketConnectionListener.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#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), _port(port), _isStopped(true) { + this->initializeAcceptors(); + } + + ~SocketConnectionListener() { this->stop(); } + + void start(); + void stop(); + +private: + void initializeAcceptors(); + void acceptIPv4Connections(); + void acceptIPv6Connections(); + + asio::io_context &_ioContext; + uint16_t _port; + std::optional _ipv4Acceptor; + std::optional _ipv6Acceptor; + bool _isStopped{true}; +}; +} // namespace xtrpg::network \ No newline at end of file diff --git a/include/xtrpg/network/TcpConnection.hpp b/include/xtrpg/network/TcpConnection.hpp new file mode 100644 index 0000000..d38e618 --- /dev/null +++ b/include/xtrpg/network/TcpConnection.hpp @@ -0,0 +1,133 @@ +#pragma once + +#include +#include +#include +#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)) { + this->_strand.emplace(asio::make_strand(this->_tcpSocket.get_executor())); + } + + /** + * 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(); + + /** + * Checks if the connection is secure (SSL/TLS). + */ + 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. + */ + 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. + */ + TcpConnection &operator<<(std::string_view str) { + this->write(str); + return *this; + } + +private: + /** + * The current state of the connection. + */ + ConnectionState _state{ConnectionState::INSECURE}; + + /** + * The underlying TCP socket used for the connection. + */ + 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 + * not secure, this will be std::nullopt. + */ + std::optional> _sslStream; +}; +} // namespace xtrpg::network \ No newline at end of file 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 diff --git a/src/network/SocketConnectionListener.cpp b/src/network/SocketConnectionListener.cpp new file mode 100644 index 0000000..5f0f414 --- /dev/null +++ b/src/network/SocketConnectionListener.cpp @@ -0,0 +1,138 @@ +#include "xtrpg/network/SocketConnectionListener.hpp" + +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; + } + this->_isStopped = false; + + std::cout << "[SocketConnectionListener] Listening for socket connections." + << 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() { + if (this->_isStopped) + return; + this->_isStopped = true; + + std::error_code 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() { + 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 && !isListenerShutdownError(ec)) { + this->acceptIPv4Connections(); + } + }); +} + +void SocketConnectionListener::acceptIPv6Connections() { + 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 && !isListenerShutdownError(ec)) { + this->acceptIPv6Connections(); + } + }); +} +} // 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..f7437c3 --- /dev/null +++ b/src/network/TcpConnection.cpp @@ -0,0 +1,92 @@ +#include "xtrpg/network/TcpConnection.hpp" + +namespace xtrpg::network { + +void TcpConnection::upgrade(asio::ssl::context &ssl_ctx) { + auto self = shared_from_this(); + asio::post(*this->_strand, [this, self, &ssl_ctx]() { + if (this->isClosed() || this->isClosing() || this->isSecure()) { + return; + } + + this->_sslStream.emplace(std::move(this->_tcpSocket), ssl_ctx); + + 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) { + auto payload = std::make_shared(data); + auto self = shared_from_this(); + + asio::post(*this->_strand, [this, self, payload]() { + if (!this->isOpen()) { + throw exception::ConnectionClosed(); + } + + 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() { + auto self = shared_from_this(); + asio::post(*this->_strand, [this, self]() { + if (this->isClosed() || this->isClosing()) { + return; + } + + this->_state = ConnectionState::CLOSING; + + if (this->isSecure() && this->_sslStream) { + this->_sslStream->lowest_layer().cancel(); + + 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::CLOSED; + }); + return; + } + + 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