From d15612da76adf0c52cfb8c77e0681d1b36903888 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sun, 6 Sep 2026 13:12:19 +1000 Subject: [PATCH 01/26] Add StreamHandler and integrate with ClientSession Introduce a StreamHandler interface and UnimplementedStreamHandler singleton for default/error behavior. ClientSession now stores a non-owning active StreamHandler pointer protected by a mutex, with setActiveStreamHandler/getActiveStreamHandler accessors. Constructor implementation moved to .cpp to set the tokenizer observer and initialize the active handler to UnimplementedStreamHandler::instance(). Added new headers under include/xtrpg/xmpp/stream and updated src/xmpp/session/ClientSession.cpp accordingly. --- include/xtrpg/xmpp/session/ClientSession.hpp | 19 +++++++-- include/xtrpg/xmpp/stream/StreamHandler.hpp | 40 +++++++++++++++++++ .../stream/UnimplementedStreamHandler.hpp | 38 ++++++++++++++++++ src/xmpp/session/ClientSession.cpp | 19 +++++++++ 4 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 include/xtrpg/xmpp/stream/StreamHandler.hpp create mode 100644 include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp diff --git a/include/xtrpg/xmpp/session/ClientSession.hpp b/include/xtrpg/xmpp/session/ClientSession.hpp index 88de627..56f4c42 100644 --- a/include/xtrpg/xmpp/session/ClientSession.hpp +++ b/include/xtrpg/xmpp/session/ClientSession.hpp @@ -14,6 +14,10 @@ #include "xtrpg/xml/tokenizer/XmlToken.hpp" #include "xtrpg/xml/tokenizer/XmlTokenListener.hpp" +namespace xtrpg::xmpp::stream { +class StreamHandler; +} + namespace xtrpg::xmpp::session { /** Coordinates XML tokenization and transport I/O for one XMPP client. */ @@ -25,10 +29,7 @@ class ClientSession : public xml::tokenizer::XmlTokenListener { * @param tcpConnection connection transferred to the new session; must not * be null */ - explicit ClientSession(network::TcpConnection *tcpConnection) - : _ptrTcpConnection(tcpConnection) { - this->_tokenizer.setObserver(this); - } + explicit ClientSession(network::TcpConnection *tcpConnection); /** Stops token processing and releases the owned connection and XML state. */ ~ClientSession(); @@ -63,6 +64,12 @@ class ClientSession : public xml::tokenizer::XmlTokenListener { /** Handles a tokenizer error reported for this client stream. */ void onTokenizationError(const xml::tokenizer::TokenizationError &error); + /** Replaces the non-owning active stream handler; nullptr clears it. */ + void setActiveStreamHandler(const stream::StreamHandler *streamHandler); + + /** Returns a snapshot of the non-owning active stream handler. */ + const stream::StreamHandler *getActiveStreamHandler() const; + private: /** TCP connection owned by this session. */ network::TcpConnection *_ptrTcpConnection; @@ -89,6 +96,10 @@ class ClientSession : public xml::tokenizer::XmlTokenListener { /** Ensures completion is reported at most once. */ std::atomic _completionNotified{false}; + /** Protects the active stream handler while it is being accessed. */ + mutable std::mutex _activeStreamHandlerMutex; + const stream::StreamHandler *_ptrActiveStreamHandler = nullptr; + /** Notifies the manager that the session has completed. */ void notifyCompletion(); }; diff --git a/include/xtrpg/xmpp/stream/StreamHandler.hpp b/include/xtrpg/xmpp/stream/StreamHandler.hpp new file mode 100644 index 0000000..3aa8205 --- /dev/null +++ b/include/xtrpg/xmpp/stream/StreamHandler.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +#include "xtrpg/xmpp/session/ClientSession.hpp" + +namespace xtrpg::xmpp::stream { + +class C2SSession; // Forward declaration + +/** + * Representation of a Stateless stream handler. + */ +class StreamHandler { +public: + virtual ~StreamHandler() = default; + + /** + * Handler function that is called when a stream receives an inbound Stanza + * for processing. + */ + virtual void onStanza(session::ClientSession &clientSession, + const int &stanza) const = 0; + + /** + * Lifecycle hook that is called immediately after a new stream is + * initialized. + */ + virtual void onStart(session::ClientSession &clientSession) const {} + + /** + * Lifecycle hook that is called immediately before a stream is + * terminated. + */ + virtual void onEnd(session::ClientSession &clientSession) const {} +}; + +} // namespace xtrpg::xmpp::stream \ No newline at end of file diff --git a/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp b/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp new file mode 100644 index 0000000..e595b92 --- /dev/null +++ b/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "xtrpg/xmpp/stream/StreamHandler.hpp" + +namespace xtrpg::xmpp::stream { + +class UnimplementedStreamHandler : public StreamHandler { +public: + static const UnimplementedStreamHandler &instance() { + static UnimplementedStreamHandler instance; + return instance; + } + + explicit UnimplementedStreamHandler() = default; + + void onStart(session::ClientSession &session) const override { + session.sendRaw( + ""); + session.sendRaw( + "An " + "unexpected error occurred."); + session.shutdown(); + } + + void onEnd(session::ClientSession &session) const override { + session.sendRaw(""); + } + + void onStanza(session::ClientSession &session, + const int &stanza) const override { + // Not implemented + } +}; +} // namespace xtrpg::xmpp::stream diff --git a/src/xmpp/session/ClientSession.cpp b/src/xmpp/session/ClientSession.cpp index c768daa..13c7156 100644 --- a/src/xmpp/session/ClientSession.cpp +++ b/src/xmpp/session/ClientSession.cpp @@ -1,7 +1,15 @@ #include "xtrpg/xmpp/session/ClientSession.hpp" +#include "xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp" + namespace xtrpg::xmpp::session { +ClientSession::ClientSession(network::TcpConnection *tcpConnection) + : _ptrTcpConnection(tcpConnection), + _ptrActiveStreamHandler(&stream::UnimplementedStreamHandler::instance()) { + this->_tokenizer.setObserver(this); +} + ClientSession::~ClientSession() { delete this->_ptrRootStreamNode; delete this->_ptrDeclarationNode; @@ -84,4 +92,15 @@ void ClientSession::onXmlToken(const xml::tokenizer::XmlToken &xmlToken) { void ClientSession::onTokenizationError( const xml::tokenizer::TokenizationError &error) {} +void ClientSession::setActiveStreamHandler( + const stream::StreamHandler *streamHandler) { + std::lock_guard lock(this->_activeStreamHandlerMutex); + this->_ptrActiveStreamHandler = streamHandler; +} + +const stream::StreamHandler *ClientSession::getActiveStreamHandler() const { + std::lock_guard lock(this->_activeStreamHandlerMutex); + return this->_ptrActiveStreamHandler; +} + } // namespace xtrpg::xmpp::session From 45ba91ec1bd55080e0dc8e91897d14bd0694d9bf Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Mon, 7 Sep 2026 12:22:29 +1000 Subject: [PATCH 02/26] Add active stream handler check Expose a convenience accessor on ClientSession for determining whether an active stream handler is currently set. This makes null checks explicit for callers and avoids repeated direct pointer comparisons. --- include/xtrpg/xmpp/session/ClientSession.hpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/include/xtrpg/xmpp/session/ClientSession.hpp b/include/xtrpg/xmpp/session/ClientSession.hpp index 56f4c42..9a0d0f7 100644 --- a/include/xtrpg/xmpp/session/ClientSession.hpp +++ b/include/xtrpg/xmpp/session/ClientSession.hpp @@ -70,6 +70,13 @@ class ClientSession : public xml::tokenizer::XmlTokenListener { /** Returns a snapshot of the non-owning active stream handler. */ const stream::StreamHandler *getActiveStreamHandler() const; + /** + * Returns whether the active stream handler has been defined or not. + */ + bool hasActiveStreamHandler() const { + return nullptr != this->_ptrActiveStreamHandler; + } + private: /** TCP connection owned by this session. */ network::TcpConnection *_ptrTcpConnection; From 6d4a110e226351f605569e6b795a2dce50c007ab Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Mon, 7 Sep 2026 12:23:21 +1000 Subject: [PATCH 03/26] Add XML stream parsing skeleton to ClientSession Remove the default active stream handler initialization in the constructor and introduce a comprehensive parsing skeleton inside onXmlToken. New logic ignores comment tokens, detects stream start (stream:stream), handles declaration/invalid-tag cases, branches for secure vs non-secure connection (placeholders for negotiation, authentication, bind phases), and scaffolds close/open/empty/text node handling and stream-close behavior. Leaves original stream:stream open handling intact. This change prepares the session code for implementing concrete stream handlers and proper error handling. --- src/xmpp/session/ClientSession.cpp | 101 ++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/src/xmpp/session/ClientSession.cpp b/src/xmpp/session/ClientSession.cpp index 13c7156..afdae9b 100644 --- a/src/xmpp/session/ClientSession.cpp +++ b/src/xmpp/session/ClientSession.cpp @@ -5,8 +5,7 @@ namespace xtrpg::xmpp::session { ClientSession::ClientSession(network::TcpConnection *tcpConnection) - : _ptrTcpConnection(tcpConnection), - _ptrActiveStreamHandler(&stream::UnimplementedStreamHandler::instance()) { + : _ptrTcpConnection(tcpConnection) { this->_tokenizer.setObserver(this); } @@ -76,6 +75,104 @@ void ClientSession::notifyCompletion() { } void ClientSession::onXmlToken(const xml::tokenizer::XmlToken &xmlToken) { + + // Ignore any comment tokens + if (xml::tokenizer::TokenType::COMMENT == xmlToken.type) { + return; + } + + // Are we waiting for the client to start a new stream? + if (nullptr == this->_ptrActiveStreamHandler) { + // ignore declaration tokens + if (xml::tokenizer::TokenType::DECLARATION == xmlToken.type) { + return; + } + + // if it's not an opening tag, then it's not the start of a stream + if (xml::tokenizer::TokenType::OPEN_TAG != xmlToken.type) { + // return a malformed xml stream error + return; + } + + // the expected tag should be a `stream:stream` tag + if ("stream:stream" != xmlToken.content) { + // return an invalid opening tag error + return; + } + + // determine which stream handler to activate + if (!this->_ptrTcpConnection->isSecure()) { + // start the negotiation phase + return; + } + + // if not authenticated + // start the authentication phase + + // start the binded phase + return; + } + + // Are we parseing the root stream:stream node? + // if (nullptr == this->_ptrCurrentNode) { + + // Are we ending the current stream + if (xml::tokenizer::TokenType::CLOSE_TAG == xmlToken.type && + "stream:stream" == xmlToken.content) { + // close the stream + // remove the handler. + return; + } + + // if (xml::tokenizer::TokenType::EMPTY_TAG != xmlToken.type) { + // process the node and dispatch to handler. + // return; + // } + + // if (xml::tokenizer::TokenType::OPEN_TAG != xmlToken.type) { malformed + // stream error close the stream. + // return; + // } + + // create the new xml node. + // return; + // } + + // From here on down we are parsing a node + + if (xml::tokenizer::TokenType::CLOSE_TAG == xmlToken.type) { + // if the current node != this close tag: + // - then return a malformed error and close stream. + // - return + + // if the current node does not have a parent node (ie parent == nullptr) + // - then dispatch the current node to the handler + // - set current node to nullptr + // - return + + // set the parent of the current node to be the new current node. + // return + } + + if (xml::tokenizer::TokenType::EMPTY_TAG == xmlToken.type) { + // append an empty node to the current node. + // return + } + + if (xml::tokenizer::TokenType::TEXT_CONTENT == xmlToken.type) { + // append text content to the current node + // return + } + + if (xml::tokenizer::TokenType::OPEN_TAG == xmlToken.type) { + // create a new node + // append the new node to the current node + // set the current node to be the new node + // return + } + + // return a malformed XML stream. + if (xml::tokenizer::TokenType::OPEN_TAG == xmlToken.type && "stream:stream" == xmlToken.content) { this->sendRaw( From d3146e31b1e797e9c8684d5f812e8ac205b336b6 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Mon, 7 Sep 2026 14:25:04 +1000 Subject: [PATCH 04/26] Handle XML stream teardown and tracking Adds a mutex-protected current XML node pointer to ClientSession and cleans it up in the destructor. The session now properly handles the closing stream:stream tag by sending the closing XML and clearing the active stream handler instead of leaving the stream in an active state. --- include/xtrpg/xmpp/session/ClientSession.hpp | 4 ++ src/xmpp/session/ClientSession.cpp | 44 +++++++++++--------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/include/xtrpg/xmpp/session/ClientSession.hpp b/include/xtrpg/xmpp/session/ClientSession.hpp index 9a0d0f7..dedbe90 100644 --- a/include/xtrpg/xmpp/session/ClientSession.hpp +++ b/include/xtrpg/xmpp/session/ClientSession.hpp @@ -107,6 +107,10 @@ class ClientSession : public xml::tokenizer::XmlTokenListener { mutable std::mutex _activeStreamHandlerMutex; const stream::StreamHandler *_ptrActiveStreamHandler = nullptr; + /** Protects the active xml node while it is being accessed. */ + mutable std::mutex _currentXmlNodeMutex; + const xml::node::TagNode *_ptrCurrentXmlNode = nullptr; + /** Notifies the manager that the session has completed. */ void notifyCompletion(); }; diff --git a/src/xmpp/session/ClientSession.cpp b/src/xmpp/session/ClientSession.cpp index afdae9b..e2b2cf1 100644 --- a/src/xmpp/session/ClientSession.cpp +++ b/src/xmpp/session/ClientSession.cpp @@ -15,6 +15,12 @@ ClientSession::~ClientSession() { this->_tokenizer.setObserver(nullptr); delete this->_ptrTcpConnection; this->_ptrTcpConnection = nullptr; + + if (nullptr != this->_ptrCurrentXmlNode) { + std::lock_guard lock(this->_currentXmlNodeMutex); + delete this->_ptrCurrentXmlNode; + this->_ptrCurrentXmlNode = nullptr; + } } void ClientSession::start() { @@ -114,29 +120,29 @@ void ClientSession::onXmlToken(const xml::tokenizer::XmlToken &xmlToken) { } // Are we parseing the root stream:stream node? - // if (nullptr == this->_ptrCurrentNode) { + if (nullptr == this->_ptrCurrentXmlNode) { - // Are we ending the current stream - if (xml::tokenizer::TokenType::CLOSE_TAG == xmlToken.type && - "stream:stream" == xmlToken.content) { - // close the stream - // remove the handler. - return; - } + // Are we ending the current stream + if (xml::tokenizer::TokenType::CLOSE_TAG == xmlToken.type && + "stream:stream" == xmlToken.content) { + this->sendRaw(""); + this->setActiveStreamHandler(nullptr); + return; + } - // if (xml::tokenizer::TokenType::EMPTY_TAG != xmlToken.type) { - // process the node and dispatch to handler. - // return; - // } + // if (xml::tokenizer::TokenType::EMPTY_TAG != xmlToken.type) { + // process the node and dispatch to handler. + // return; + // } - // if (xml::tokenizer::TokenType::OPEN_TAG != xmlToken.type) { malformed - // stream error close the stream. - // return; - // } + // if (xml::tokenizer::TokenType::OPEN_TAG != xmlToken.type) { malformed + // stream error close the stream. + // return; + // } - // create the new xml node. - // return; - // } + // create the new xml node. + // return; + } // From here on down we are parsing a node From 11bf8ea73a0ddc6fd05083c0805dc6f8bf63b6d7 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Mon, 7 Sep 2026 15:44:06 +1000 Subject: [PATCH 05/26] Make string utilities inline; add isBlank Add isBlank(const std::string&) and include . Mark ltrim, rtrim, trim, toLowerCase, and countUtf8CodePoints as inline to prevent multiple-definition/ODR issues when the header is included in multiple translation units. Minor formatting changes in include/xtrpg/utils/String.hpp. --- include/xtrpg/utils/String.hpp | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/include/xtrpg/utils/String.hpp b/include/xtrpg/utils/String.hpp index d2b1535..f07c8e3 100644 --- a/include/xtrpg/utils/String.hpp +++ b/include/xtrpg/utils/String.hpp @@ -1,27 +1,38 @@ #pragma once #include +#include #include namespace xtrpg::utils::string { +inline bool isBlank(const std::string &s) { + return std::all_of(s.begin(), s.end(), [](unsigned char character) { + return std::isspace(character) != 0; + }); +} + // Trim from start (in-place) -void ltrim(std::string &s) { s.erase(0, s.find_first_not_of(" \t\n\r\f\v")); } +inline void ltrim(std::string &s) { + s.erase(0, s.find_first_not_of(" \t\n\r\f\v")); +} // Trim from end (in-place) -void rtrim(std::string &s) { s.erase(s.find_last_not_of(" \t\n\r\f\v") + 1); } +inline void rtrim(std::string &s) { + s.erase(s.find_last_not_of(" \t\n\r\f\v") + 1); +} // Trim from both ends (in-place) -void trim(std::string &s) { +inline void trim(std::string &s) { rtrim(s); ltrim(s); } -void toLowerCase(std::string &s) { +inline void toLowerCase(std::string &s) { std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::tolower(c); }); } -size_t countUtf8CodePoints(const std::string &str) { +inline size_t countUtf8CodePoints(const std::string &str) { size_t count = 0; for (unsigned char c : str) { // If the byte is NOT a UTF-8 continuation byte, it's the start of a From 8c908f448a2d5b0558904f62ad95b53913266c89 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Mon, 7 Sep 2026 15:44:28 +1000 Subject: [PATCH 06/26] Log active client connections This change adds a diagnostic log when a client session is removed from the connection manager, printing the current active connection count. It helps track connection churn and diagnose lingering or unexpected disconnects during runtime. --- src/xmpp/ClientConnectionManager.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/xmpp/ClientConnectionManager.cpp b/src/xmpp/ClientConnectionManager.cpp index 8972623..ec5f58c 100644 --- a/src/xmpp/ClientConnectionManager.cpp +++ b/src/xmpp/ClientConnectionManager.cpp @@ -84,6 +84,9 @@ void ClientConnectionManager::onObservation(network::TcpConnection *ctx) { this->_clientSessionPtrs.erase(sessionIt); lock.unlock(); asio::post(*this->_ioContext, [session]() { delete session; }); + + std::cout << "[ClientConnectionManager] Connection Count: " + << this->countConnections() << std::endl; }); clientSession->start(); From e84a2591e124a238a40ac0f025360bacbaf92f10 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Mon, 7 Sep 2026 15:45:22 +1000 Subject: [PATCH 07/26] Combine stream start and use internal-server-error Merge two session.sendRaw calls into a single stream opening that includes the error payload. Replace the with and inline the error into the message so the handler emits a single, well-formed stream start containing the error text. --- include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp b/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp index e595b92..f3375ce 100644 --- a/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp +++ b/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp @@ -17,9 +17,8 @@ class UnimplementedStreamHandler : public StreamHandler { session.sendRaw( ""); - session.sendRaw( - "An " "unexpected error occurred."); From 6065a8c1491f0cceb07d5f387a0aead54edfcaa8 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Mon, 7 Sep 2026 15:45:52 +1000 Subject: [PATCH 08/26] Validate and log incoming XMPP stream start Add diagnostic logging for incoming XML tokens and ignore comments, declarations, and blank text. Validate that the first element is an opening stream:stream tag; on mismatch send an XMPP (bad-format) and shutdown the session. Add String.hpp for whitespace checks and include NegotiationStreamHandler (negotiation init currently commented out). Default to UnimplementedStreamHandler as a placeholder. These changes improve robustness and provide clearer diagnostics when handling stream start. --- src/xmpp/session/ClientSession.cpp | 46 ++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/src/xmpp/session/ClientSession.cpp b/src/xmpp/session/ClientSession.cpp index e2b2cf1..b8bf9a3 100644 --- a/src/xmpp/session/ClientSession.cpp +++ b/src/xmpp/session/ClientSession.cpp @@ -1,5 +1,7 @@ #include "xtrpg/xmpp/session/ClientSession.hpp" +#include "xtrpg/utils/String.hpp" +#include "xtrpg/xmpp/stream/NegotiationStreamHandler.hpp" #include "xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp" namespace xtrpg::xmpp::session { @@ -82,8 +84,12 @@ void ClientSession::notifyCompletion() { void ClientSession::onXmlToken(const xml::tokenizer::XmlToken &xmlToken) { + std::cout << "Incoming XML Token" << std::endl; + std::cout << " Content: " << xmlToken.content << std::endl; + // Ignore any comment tokens if (xml::tokenizer::TokenType::COMMENT == xmlToken.type) { + std::cout << "Ignoring Comment: " << xmlToken.content << std::endl; return; } @@ -91,35 +97,59 @@ void ClientSession::onXmlToken(const xml::tokenizer::XmlToken &xmlToken) { if (nullptr == this->_ptrActiveStreamHandler) { // ignore declaration tokens if (xml::tokenizer::TokenType::DECLARATION == xmlToken.type) { + std::cout << "Ignoring Declaration: " << xmlToken.content << std::endl; return; } - // if it's not an opening tag, then it's not the start of a stream - if (xml::tokenizer::TokenType::OPEN_TAG != xmlToken.type) { - // return a malformed xml stream error + // if the incoming token is text content and blank (only contains whitespace + // and newlines) or empty then ignore and return immediately. + if (xml::tokenizer::TokenType::TEXT_CONTENT == xmlToken.type && + xtrpg::utils::string::isBlank(xmlToken.content)) { + std::cout << "Ignoring Whitespace Text Content." << std::endl; return; } - // the expected tag should be a `stream:stream` tag - if ("stream:stream" != xmlToken.content) { - // return an invalid opening tag error + // if it's not an opening tag, then it's not the start of a stream + if (xml::tokenizer::TokenType::OPEN_TAG != xmlToken.type || + "stream:stream" != xmlToken.content) { + // return a malformed xml stream error + this->sendRaw( + "First element must be " + "an opening stream header."); + this->shutdown(); return; } // determine which stream handler to activate if (!this->_ptrTcpConnection->isSecure()) { // start the negotiation phase - return; + // { + // std::lock_guard lock(this->_activeStreamHandlerMutex); + // this->_ptrActiveStreamHandler = + // &stream::NegotiationStreamHandler::instance(); + // } + // this->_ptrActiveStreamHandler->onStart(*this); + // return; } // if not authenticated // start the authentication phase // start the binded phase + { + std::lock_guard lock(this->_activeStreamHandlerMutex); + this->_ptrActiveStreamHandler = + &stream::UnimplementedStreamHandler::instance(); + } + this->_ptrActiveStreamHandler->onStart(*this); return; } - // Are we parseing the root stream:stream node? + // Are we parsing the root stream:stream node? if (nullptr == this->_ptrCurrentXmlNode) { // Are we ending the current stream From 44acf04a71a71a83ceae9a50c9284f6439b9e433 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Mon, 7 Sep 2026 16:22:01 +1000 Subject: [PATCH 09/26] Remove debug logging from TcpConnection Remove several std::cout debug/log statements from TcpConnection (appendStateChangeCallback, upgrade, read, write, close) to reduce noisy output. No functional changes intended; existing behaviors (e.g., preserving read callback contract when closed) are preserved. --- include/xtrpg/network/TcpConnection.hpp | 1 - src/network/TcpConnection.cpp | 16 +--------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/include/xtrpg/network/TcpConnection.hpp b/include/xtrpg/network/TcpConnection.hpp index e585652..62eb3fd 100644 --- a/include/xtrpg/network/TcpConnection.hpp +++ b/include/xtrpg/network/TcpConnection.hpp @@ -129,7 +129,6 @@ class TcpConnection { void appendStateChangeCallback(std::function callback) { - std::cout << "[TcpConnection] Append State Change Callback." << std::endl; this->_stateChangeCallbacks.push_back(callback); } diff --git a/src/network/TcpConnection.cpp b/src/network/TcpConnection.cpp index cb9f113..723c5ea 100644 --- a/src/network/TcpConnection.cpp +++ b/src/network/TcpConnection.cpp @@ -15,9 +15,6 @@ void TcpConnection::dispatchCloseCallbacks() { void TcpConnection::upgrade(asio::ssl::context &ssl_ctx) { if (!this->isOpen()) { - std::cout - << "[TcpConnection] Unable to upgrade as TCP Connection is not open." - << std::endl; return; } @@ -43,17 +40,14 @@ void TcpConnection::upgrade(asio::ssl::context &ssl_ctx) { void TcpConnection::read( std::function callback) { if (!this->isOpen()) { - std::cout << "[TcpConnection] Unable to read as TCP Connection is not open." - << std::endl; // Preserve read's callback contract even when the socket closed before // the asynchronous operation could be posted. std::istringstream stream; callback(asio::error::operation_aborted, stream); return; } - std::cout << "[TcpConnection] Requesting to read." << std::endl; - auto buffer = std::make_shared>(4096); + auto buffer = std::make_shared>(4096); asio::post(*this->_strand, [this, buffer, callback]() { if (!this->isOpen()) { // The connection may close after the caller's initial state check but @@ -117,9 +111,6 @@ void TcpConnection::cancelRead() { void TcpConnection::write(std::string_view data) { if (!this->isOpen()) { - std::cout - << "[TcpConnection] Unable to write as TCP Connection is not open." - << std::endl; return; } auto payload = std::make_shared(data); @@ -149,11 +140,7 @@ void TcpConnection::write(std::string_view data) { } void TcpConnection::close(std::function callback) { - if (this->is(ConnectionState::CLOSED) || this->is(ConnectionState::CLOSING)) { - std::cout << "[TcpConnection] Connection is already closed or in the " - "process of being closed." - << std::endl; if (this->isClosed() && callback) { callback(); } else if (this->isClosing() && callback) { @@ -167,7 +154,6 @@ void TcpConnection::close(std::function callback) { } // Set the state to closing. - std::cout << "[TcpConnection] Request Close." << std::endl; this->dispatchStateChange(ConnectionState::CLOSING); // Serialize transport shutdown with reads and writes on the strand. From e07386b2a9ddbf59f3e77fe133df0c0c5c7a9b13 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 12 Sep 2026 17:02:31 +1000 Subject: [PATCH 10/26] Use TagNode for stanza parameter Replace the previous int placeholder for stanzas with a structured xml::node::TagNode reference. Added the TagNode include and updated StreamHandler::onStanza declaration and UnimplementedStreamHandler::onStanza override to accept const xml::node::TagNode&. This clarifies the API and enables passing full XML stanza data instead of an integer placeholder. --- include/xtrpg/xmpp/stream/StreamHandler.hpp | 3 ++- include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/include/xtrpg/xmpp/stream/StreamHandler.hpp b/include/xtrpg/xmpp/stream/StreamHandler.hpp index 3aa8205..93a133d 100644 --- a/include/xtrpg/xmpp/stream/StreamHandler.hpp +++ b/include/xtrpg/xmpp/stream/StreamHandler.hpp @@ -4,6 +4,7 @@ #include #include +#include "xtrpg/xml/node/TagNode.hpp" #include "xtrpg/xmpp/session/ClientSession.hpp" namespace xtrpg::xmpp::stream { @@ -22,7 +23,7 @@ class StreamHandler { * for processing. */ virtual void onStanza(session::ClientSession &clientSession, - const int &stanza) const = 0; + const xml::node::TagNode &stanza) const = 0; /** * Lifecycle hook that is called immediately after a new stream is diff --git a/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp b/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp index f3375ce..89659fe 100644 --- a/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp +++ b/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp @@ -1,5 +1,6 @@ #pragma once +#include "xtrpg/xml/node/TagNode.hpp" #include "xtrpg/xmpp/stream/StreamHandler.hpp" namespace xtrpg::xmpp::stream { @@ -30,7 +31,7 @@ class UnimplementedStreamHandler : public StreamHandler { } void onStanza(session::ClientSession &session, - const int &stanza) const override { + const xml::node::TagNode &stanza) const override { // Not implemented } }; From 684e00aab7ca0efa5b19c3d9f1e95a86ddeec7e6 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 12 Sep 2026 17:09:21 +1000 Subject: [PATCH 11/26] Fix IPv6 dual-stack listener setup Adjust the IPv6 listener initialization to set the v6_only option before binding, then bind and listen on the configured port before storing the acceptor. This fixes Windows dual-stack socket creation issues while preserving IPv6 compatibility. --- src/network/SocketConnectionListener.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/network/SocketConnectionListener.cpp b/src/network/SocketConnectionListener.cpp index 42ef434..5dec0de 100644 --- a/src/network/SocketConnectionListener.cpp +++ b/src/network/SocketConnectionListener.cpp @@ -11,18 +11,20 @@ bool isListenerShutdownError(const std::error_code &ec) { void SocketConnectionListener::initializeAcceptors() { try { - asio::ip::tcp::acceptor ipv6Acceptor( - *this->_ptrIoContext, - asio::ip::tcp::endpoint(asio::ip::tcp::v6(), this->_port)); + // Create acceptor without binding to endpoint + asio::ip::tcp::acceptor ipv6Acceptor(*this->_ptrIoContext, + asio::ip::tcp::v6()); + // Set socket option BEFORE binding (required on Windows) 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; + // Now bind to the endpoint + ipv6Acceptor.bind( + asio::ip::tcp::endpoint(asio::ip::tcp::v6(), this->_port)); + ipv6Acceptor.listen(); + + this->_ipv6Acceptor.emplace(std::move(ipv6Acceptor)); } catch (const std::exception &ex) { std::cerr << "[SocketConnectionListener] Failed to open IPv6 dual-stack " "socket on " From 2b2510dfe7635eee39b955adaf70cff6641f4ab0 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 12 Sep 2026 18:15:17 +1000 Subject: [PATCH 12/26] Add XMPP TLS negotiation stream handler Adds a new NegotiationStreamHandler for the XMPP stream handshake. It opens the initial stream, advertises STARTTLS, validates the TLS proceed request, and returns a stream error if the client attempts to continue without TLS. This establishes the required TLS negotiation phase before transitioning to the next state. --- .../xmpp/stream/NegotiationStreamHandler.hpp | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 include/xtrpg/xmpp/stream/NegotiationStreamHandler.hpp diff --git a/include/xtrpg/xmpp/stream/NegotiationStreamHandler.hpp b/include/xtrpg/xmpp/stream/NegotiationStreamHandler.hpp new file mode 100644 index 0000000..00838d8 --- /dev/null +++ b/include/xtrpg/xmpp/stream/NegotiationStreamHandler.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include "xtrpg/xml/node/TagNode.hpp" +#include "xtrpg/xmpp/stream/StreamHandler.hpp" + +namespace xtrpg::xmpp::stream { + +class NegotiationStreamHandler : public StreamHandler { +public: + static const NegotiationStreamHandler &instance() { + static NegotiationStreamHandler instance; + return instance; + } + + explicit NegotiationStreamHandler() = default; + + void onStart(session::ClientSession &session) const override { + // open the stream + session.sendRaw( + ""); + + // request encrypted + session.sendRaw(""); + } + + void onEnd(session::ClientSession &session) const override { + session.sendRaw(""); + } + + void onStanza(session::ClientSession &session, + const xml::node::TagNode &stanza) const override { + std::string_view name; // = stanza.name(); + + if ("starttls" != stanza.name()) { + // Drop unencrypted/unauthorized stanzas sent prior to TLS + session.sendRaw( + "TLS is " + "required"); + session.shutdown(); + return; + } + + // Confirm TLS proceed stanza + session.sendRaw(""); + + // Execute async SSL handshake and transition to unauthenticated phase + // session.upgrade_to_tls([&session]() { + // session.reset_parser(); + // Transition will wait for the client's post-TLS header + // session.set_state(nullptr); // Waits for next header to instantiate + // UnauthenticatedState + // }); + } +}; +} // namespace xtrpg::xmpp::stream \ No newline at end of file From 7fd452179fbbe62f530d983449d7a7eed4b66bac Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 12 Sep 2026 18:24:37 +1000 Subject: [PATCH 13/26] Send XMPP stream-start error response The unimplemented stream handler now emits the initial open tag before sending the payload. This keeps the stream error in a valid XMPP context for clients and ensures the session shuts down cleanly after reporting the unexpected failure. --- .../xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp b/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp index 89659fe..263ce5b 100644 --- a/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp +++ b/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp @@ -15,14 +15,16 @@ class UnimplementedStreamHandler : public StreamHandler { explicit UnimplementedStreamHandler() = default; void onStart(session::ClientSession &session) const override { + session.sendRaw(""); + session.sendRaw( - "An " "unexpected error occurred."); + session.shutdown(); } From d39c1e354e8217a20a0abebbeb27a9886c9faaba Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 12 Sep 2026 18:25:41 +1000 Subject: [PATCH 14/26] Send XML nodes and negotiate streams ClientSession now serializes XML nodes to text and emits them through the existing raw send path. This also activates the negotiation stream handler for insecure client connections instead of falling through to the unimplemented handler, so new XMPP sessions begin in the correct phase. --- include/xtrpg/xmpp/session/ClientSession.hpp | 5 ++++ src/xmpp/session/ClientSession.cpp | 27 +++++++++++--------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/include/xtrpg/xmpp/session/ClientSession.hpp b/include/xtrpg/xmpp/session/ClientSession.hpp index dedbe90..4ecdaae 100644 --- a/include/xtrpg/xmpp/session/ClientSession.hpp +++ b/include/xtrpg/xmpp/session/ClientSession.hpp @@ -3,11 +3,13 @@ #include #include #include +#include #include #include #include "xtrpg/network/TcpConnection.hpp" #include "xtrpg/xml/node/DeclarationNode.hpp" +#include "xtrpg/xml/node/INode.hpp" #include "xtrpg/xml/node/TagNode.hpp" #include "xtrpg/xml/tokenizer/TokenizationError.hpp" #include "xtrpg/xml/tokenizer/XmlStreamTokenizer.hpp" @@ -58,6 +60,9 @@ class ClientSession : public xml::tokenizer::XmlTokenListener { /** Asynchronously writes raw XML or other protocol data to the client. */ void sendRaw(std::string_view data); + /** Asynchronously writes an XML node to the client. */ + void send(const xml::node::INode &xmlNode); + /** Handles one token emitted by the XML stream tokenizer. */ void onXmlToken(const xml::tokenizer::XmlToken &xmlToken); diff --git a/src/xmpp/session/ClientSession.cpp b/src/xmpp/session/ClientSession.cpp index b8bf9a3..a6c3f12 100644 --- a/src/xmpp/session/ClientSession.cpp +++ b/src/xmpp/session/ClientSession.cpp @@ -53,6 +53,12 @@ void ClientSession::sendRaw(std::string_view data) { } } +void ClientSession::send(const xml::node::INode &xmlNode) { + std::ostringstream oss; + oss << xmlNode; + this->sendRaw(oss.str()); +} + void ClientSession::process() { if (this->_isStopped || this->_isShutdown) { return; @@ -127,24 +133,21 @@ void ClientSession::onXmlToken(const xml::tokenizer::XmlToken &xmlToken) { // determine which stream handler to activate if (!this->_ptrTcpConnection->isSecure()) { // start the negotiation phase - // { - // std::lock_guard lock(this->_activeStreamHandlerMutex); - // this->_ptrActiveStreamHandler = - // &stream::NegotiationStreamHandler::instance(); - // } - // this->_ptrActiveStreamHandler->onStart(*this); - // return; + std::lock_guard lock(this->_activeStreamHandlerMutex); + this->_ptrActiveStreamHandler = + &stream::NegotiationStreamHandler::instance(); + this->_ptrActiveStreamHandler->onStart(*this); + return; } // if not authenticated // start the authentication phase // start the binded phase - { - std::lock_guard lock(this->_activeStreamHandlerMutex); - this->_ptrActiveStreamHandler = - &stream::UnimplementedStreamHandler::instance(); - } + std::lock_guard lock(this->_activeStreamHandlerMutex); + this->_ptrActiveStreamHandler = + &stream::UnimplementedStreamHandler::instance(); + this->_ptrActiveStreamHandler->onStart(*this); return; } From f8c53cd5a3c848150c8d971195f83bb7f2eac4e6 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 12 Sep 2026 19:29:27 +1000 Subject: [PATCH 15/26] Expose base append; add templated send helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose NodeContainer::append in TagNode so base-class append overloads aren't hidden by the template overloads. Add a templated ClientSession::send(tagname, consumer) convenience overload that constructs a TagNode, calls the consumer to populate it, and forwards it to the existing send(const xml::node::INode&). No behavior change—API convenience only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- include/xtrpg/xml/node/TagNode.hpp | 11 +++++++++++ include/xtrpg/xmpp/session/ClientSession.hpp | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/include/xtrpg/xml/node/TagNode.hpp b/include/xtrpg/xml/node/TagNode.hpp index 8728f0f..5675c04 100644 --- a/include/xtrpg/xml/node/TagNode.hpp +++ b/include/xtrpg/xml/node/TagNode.hpp @@ -57,6 +57,17 @@ class TagNode : public ITagname, public IAttributes, public NodeContainer { */ const std::string_view name() const { return this->getTagname(); } + /** + * Appends a new TextNode containing the provided string to this container. + * Forwarding method to expose NodeContainer's string append overload. + * + * @param withText the text content for the new TextNode + * @throws std::invalid_argument if the text contains invalid XML characters + */ + void append(const std::string &withText) { + NodeContainer::append(withText); + } + /** * Appends a new TagNode with the provided tag name to this container. * The consumer function is called with the new TagNode to allow configuration diff --git a/include/xtrpg/xmpp/session/ClientSession.hpp b/include/xtrpg/xmpp/session/ClientSession.hpp index 4ecdaae..9d1252a 100644 --- a/include/xtrpg/xmpp/session/ClientSession.hpp +++ b/include/xtrpg/xmpp/session/ClientSession.hpp @@ -63,6 +63,13 @@ class ClientSession : public xml::tokenizer::XmlTokenListener { /** Asynchronously writes an XML node to the client. */ void send(const xml::node::INode &xmlNode); + template + void send(const std::string &tagname, Consumer &&consumer) { + TagNode tagNode(tagname); + consumer(tagNode); + this->send(tagNode); + } + /** Handles one token emitted by the XML stream tokenizer. */ void onXmlToken(const xml::tokenizer::XmlToken &xmlToken); From cc807706108e4e5c76021dc9589902defeefb05d Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 12 Sep 2026 19:32:17 +1000 Subject: [PATCH 16/26] Tighten TagNode::append comment & formatting Reword the append() documentation and collapse its implementation into a single-line forward to NodeContainer::append in include/xtrpg/xml/node/TagNode.hpp. This is a purely cosmetic change (comment and formatting only) with no behavioral impact. --- include/xtrpg/xml/node/TagNode.hpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/include/xtrpg/xml/node/TagNode.hpp b/include/xtrpg/xml/node/TagNode.hpp index 5675c04..c32bdb1 100644 --- a/include/xtrpg/xml/node/TagNode.hpp +++ b/include/xtrpg/xml/node/TagNode.hpp @@ -59,14 +59,12 @@ class TagNode : public ITagname, public IAttributes, public NodeContainer { /** * Appends a new TextNode containing the provided string to this container. - * Forwarding method to expose NodeContainer's string append overload. + * Forwards to NodeContainer's string append overload. * * @param withText the text content for the new TextNode * @throws std::invalid_argument if the text contains invalid XML characters */ - void append(const std::string &withText) { - NodeContainer::append(withText); - } + void append(const std::string &withText) { NodeContainer::append(withText); } /** * Appends a new TagNode with the provided tag name to this container. From dbc8860aad109f87b7a7e117ddf7056626e3c62a Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 12 Sep 2026 19:32:25 +1000 Subject: [PATCH 17/26] Update UnimplementedStreamHandler.hpp --- .../xmpp/stream/UnimplementedStreamHandler.hpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp b/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp index 263ce5b..45fd823 100644 --- a/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp +++ b/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp @@ -19,11 +19,16 @@ class UnimplementedStreamHandler : public StreamHandler { "xmlns:stream='http://etherx.jabber.org/streams' " "id='err-1' from='example.com' version='1.0'>"); - session.sendRaw( - "An " - "unexpected error occurred."); + session.send("stream:error", [](xml::node::TagNode &streamError) { + streamError.append("internal-server-error", [](xml::node::TagNode &node) { + node.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-streams"); + }); + streamError.append("text", [](xml::node::TagNode &node) { + node.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-streams"); + node.setAttribute("xml:lang", "en"); + node.append("An unexpected error occurred."); + }); + }); session.shutdown(); } From 5351695dfb54cb06a4f87745895741c5aae97a6b Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 12 Sep 2026 19:38:07 +1000 Subject: [PATCH 18/26] Add XML attribute convenience setter Adds a convenience `set()` method to `IAttributes` so attribute assignment can use the same API style as XML nodes. The XMPP stream error handler now uses this helper for `xmlns` and `xml:lang` attributes, reducing repetitive `setAttribute()` calls and keeping attribute-setting code consistent. --- include/xtrpg/xml/node/IAttributes.hpp | 7 +++++++ include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp | 6 +++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/include/xtrpg/xml/node/IAttributes.hpp b/include/xtrpg/xml/node/IAttributes.hpp index 2c183bb..35279cd 100644 --- a/include/xtrpg/xml/node/IAttributes.hpp +++ b/include/xtrpg/xml/node/IAttributes.hpp @@ -29,6 +29,13 @@ class IAttributes { */ virtual ~IAttributes() = default; + /** + * Sets an attribute key/value pair. + */ + virtual void set(std::string_view key, std::string_view value) { + this->setAttribute(key, value); + } + /** * Sets an attribute key/value pair. */ diff --git a/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp b/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp index 45fd823..1b1407b 100644 --- a/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp +++ b/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp @@ -21,11 +21,11 @@ class UnimplementedStreamHandler : public StreamHandler { session.send("stream:error", [](xml::node::TagNode &streamError) { streamError.append("internal-server-error", [](xml::node::TagNode &node) { - node.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-streams"); + node.set("xmlns", "urn:ietf:params:xml:ns:xmpp-streams"); }); streamError.append("text", [](xml::node::TagNode &node) { - node.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-streams"); - node.setAttribute("xml:lang", "en"); + node.set("xmlns", "urn:ietf:params:xml:ns:xmpp-streams"); + node.set("xml:lang", "en"); node.append("An unexpected error occurred."); }); }); From f3047c1f7e62be02cad3f7e524173e9b26b1f6d8 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 12 Sep 2026 19:55:11 +1000 Subject: [PATCH 19/26] Use TagNode builders in TLS negotiation Replace manual sendRaw XML with structured xml::node::TagNode builders in NegotiationStreamHandler (attributes and child elements are now created programmatically). Add TagNode::append(std::nullptr_t) to create empty tags without a consumer. Qualify TagNode in ClientSession::send as xml::node::TagNode. Improves safety and readability of stream/TLS negotiation by avoiding raw XML string assembly. --- include/xtrpg/xml/node/TagNode.hpp | 13 +++++++++ include/xtrpg/xmpp/session/ClientSession.hpp | 2 +- .../xmpp/stream/NegotiationStreamHandler.hpp | 28 +++++++++++++------ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/include/xtrpg/xml/node/TagNode.hpp b/include/xtrpg/xml/node/TagNode.hpp index c32bdb1..264c90e 100644 --- a/include/xtrpg/xml/node/TagNode.hpp +++ b/include/xtrpg/xml/node/TagNode.hpp @@ -101,6 +101,19 @@ class TagNode : public ITagname, public IAttributes, public NodeContainer { } } + /** + * Appends a new TagNode with the provided tag name to this container. + * This overload accepts nullptr, creating an empty TagNode without calling + * a consumer function. + * + * @param tagname the name for the new TagNode + * @param consumer nullptr (consumer not provided) + * @throws std::invalid_argument if the tag name is invalid + */ + void append(const std::string &tagname, std::nullptr_t) { + this->NodeContainer::append(new TagNode(tagname)); + } + /** * Serializes the node into an XML formatted string. */ diff --git a/include/xtrpg/xmpp/session/ClientSession.hpp b/include/xtrpg/xmpp/session/ClientSession.hpp index 9d1252a..7553695 100644 --- a/include/xtrpg/xmpp/session/ClientSession.hpp +++ b/include/xtrpg/xmpp/session/ClientSession.hpp @@ -65,7 +65,7 @@ class ClientSession : public xml::tokenizer::XmlTokenListener { template void send(const std::string &tagname, Consumer &&consumer) { - TagNode tagNode(tagname); + xml::node::TagNode tagNode(tagname); consumer(tagNode); this->send(tagNode); } diff --git a/include/xtrpg/xmpp/stream/NegotiationStreamHandler.hpp b/include/xtrpg/xmpp/stream/NegotiationStreamHandler.hpp index 00838d8..9c6f5a3 100644 --- a/include/xtrpg/xmpp/stream/NegotiationStreamHandler.hpp +++ b/include/xtrpg/xmpp/stream/NegotiationStreamHandler.hpp @@ -23,9 +23,12 @@ class NegotiationStreamHandler : public StreamHandler { "version='1.0'>"); // request encrypted - session.sendRaw(""); + session.send("stream:features", [](xml::node::TagNode &node) { + node.append("starttls", [](xml::node::TagNode &node) { + node.set("xmlns", "urn:ietf:params:xml:ns:xmpp-tls"); + node.append("required", nullptr); + }); + }); } void onEnd(session::ClientSession &session) const override { @@ -38,17 +41,24 @@ class NegotiationStreamHandler : public StreamHandler { if ("starttls" != stanza.name()) { // Drop unencrypted/unauthorized stanzas sent prior to TLS - session.sendRaw( - "TLS is " - "required"); + session.send("stream:error", [](xml::node::TagNode &node) { + node.append("policy-violation", [](xml::node::TagNode &node) { + node.set("xmlns", "urn:ietf:params:xml:ns:xmpp-streams"); + }); + node.append("text", [](xml::node::TagNode &node) { + node.set("xmlns", "urn:ietf:params:xml:ns:xmpp-streams"); + node.set("xml:lang", "en"); + node.append("TLS is required"); + }); + }); session.shutdown(); return; } // Confirm TLS proceed stanza - session.sendRaw(""); + session.send("proceed", [](xml::node::TagNode &node) { + node.set("xmlns", "urn:ietf:params:xml:ns:xmpp-tls"); + }); // Execute async SSL handshake and transition to unauthenticated phase // session.upgrade_to_tls([&session]() { From 27245d49fb25db42b61e5a0b3f17607a49b90e0d Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 12 Sep 2026 23:30:58 +1000 Subject: [PATCH 20/26] Reduce XML debug noise in ClientSession This change removes debug logging for ignored XML comments, declarations, and whitespace tokens, which was producing noisy output during normal session parsing. It also keeps a single warning when an incoming XML token cannot be processed, preserving visibility into real issues without cluttering normal traffic. --- src/xmpp/session/ClientSession.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/xmpp/session/ClientSession.cpp b/src/xmpp/session/ClientSession.cpp index a6c3f12..67e2e30 100644 --- a/src/xmpp/session/ClientSession.cpp +++ b/src/xmpp/session/ClientSession.cpp @@ -95,7 +95,6 @@ void ClientSession::onXmlToken(const xml::tokenizer::XmlToken &xmlToken) { // Ignore any comment tokens if (xml::tokenizer::TokenType::COMMENT == xmlToken.type) { - std::cout << "Ignoring Comment: " << xmlToken.content << std::endl; return; } @@ -103,7 +102,6 @@ void ClientSession::onXmlToken(const xml::tokenizer::XmlToken &xmlToken) { if (nullptr == this->_ptrActiveStreamHandler) { // ignore declaration tokens if (xml::tokenizer::TokenType::DECLARATION == xmlToken.type) { - std::cout << "Ignoring Declaration: " << xmlToken.content << std::endl; return; } @@ -111,7 +109,6 @@ void ClientSession::onXmlToken(const xml::tokenizer::XmlToken &xmlToken) { // and newlines) or empty then ignore and return immediately. if (xml::tokenizer::TokenType::TEXT_CONTENT == xmlToken.type && xtrpg::utils::string::isBlank(xmlToken.content)) { - std::cout << "Ignoring Whitespace Text Content." << std::endl; return; } @@ -147,7 +144,6 @@ void ClientSession::onXmlToken(const xml::tokenizer::XmlToken &xmlToken) { std::lock_guard lock(this->_activeStreamHandlerMutex); this->_ptrActiveStreamHandler = &stream::UnimplementedStreamHandler::instance(); - this->_ptrActiveStreamHandler->onStart(*this); return; } @@ -223,6 +219,8 @@ void ClientSession::onXmlToken(const xml::tokenizer::XmlToken &xmlToken) { "limit of 64KB exceeded."); this->shutdown(); } + + std::cout << "UNABLE TO PROCESS INCOMING XML TOKEN" << std::endl; } void ClientSession::onTokenizationError( From 3e1735daefe155b7d6c1104f270a0b6163e8ee0d Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 12 Sep 2026 23:31:44 +1000 Subject: [PATCH 21/26] Improve XML token handling in ClientSession Replace raw sendRaw error reply with structured send() builder for well-formed stream error. Ignore blank/text-only tokens at root. Add support for EMPTY_TAG tokens by constructing a TagNode (copying attributes) and dispatching it to the active stream handler under a mutex. Ensure method returns after stanza-size shutdown to stop further processing. These changes make XML parsing and error handling more robust and thread-safe. --- src/xmpp/session/ClientSession.cpp | 44 ++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/src/xmpp/session/ClientSession.cpp b/src/xmpp/session/ClientSession.cpp index 67e2e30..c563aef 100644 --- a/src/xmpp/session/ClientSession.cpp +++ b/src/xmpp/session/ClientSession.cpp @@ -116,13 +116,18 @@ void ClientSession::onXmlToken(const xml::tokenizer::XmlToken &xmlToken) { if (xml::tokenizer::TokenType::OPEN_TAG != xmlToken.type || "stream:stream" != xmlToken.content) { // return a malformed xml stream error - this->sendRaw( - "First element must be " - "an opening stream header."); + this->send("stream:stream", [](xml::node::TagNode &node) { + node.set("xmlns:stream", "http://etherx.jabber.org/streams"); + node.append("stream:error", [](xml::node::TagNode &node) { + node.append("bad-format", [](xml::node::TagNode &node) { + node.set("xmlns", "urn:ietf:params:xml:ns:xmpp-streams"); + }); + node.append("text", [](xml::node::TagNode &node) { + node.set("xmlns", "urn:ietf:params:xml:ns:xmpp-streams"); + node.append("First element must be an opening stream handler."); + }); + }); + }); this->shutdown(); return; } @@ -151,6 +156,13 @@ void ClientSession::onXmlToken(const xml::tokenizer::XmlToken &xmlToken) { // Are we parsing the root stream:stream node? if (nullptr == this->_ptrCurrentXmlNode) { + // if the incoming token is text content and blank (only contains whitespace + // and newlines) or empty then ignore and return immediately. + if (xml::tokenizer::TokenType::TEXT_CONTENT == xmlToken.type && + xtrpg::utils::string::isBlank(xmlToken.content)) { + return; + } + // Are we ending the current stream if (xml::tokenizer::TokenType::CLOSE_TAG == xmlToken.type && "stream:stream" == xmlToken.content) { @@ -159,10 +171,19 @@ void ClientSession::onXmlToken(const xml::tokenizer::XmlToken &xmlToken) { return; } - // if (xml::tokenizer::TokenType::EMPTY_TAG != xmlToken.type) { - // process the node and dispatch to handler. - // return; - // } + if (xml::tokenizer::TokenType::EMPTY_TAG == xmlToken.type) { + // create a new Tag Node with no children. + xml::node::TagNode node(xmlToken.content); + if (xmlToken.attributes.size() > 0) { + for (const auto &[key, value] : xmlToken.attributes) { + node.set(key, value); + } + } + + std::lock_guard lock(this->_activeStreamHandlerMutex); + this->_ptrActiveStreamHandler->onStanza(*this, node); + return; + } // if (xml::tokenizer::TokenType::OPEN_TAG != xmlToken.type) { malformed // stream error close the stream. @@ -218,6 +239,7 @@ void ClientSession::onXmlToken(const xml::tokenizer::XmlToken &xmlToken) { "xmlns='urn:ietf:params:xml:ns:xmpp-streams' xml:lang='en'>Stanza size " "limit of 64KB exceeded."); this->shutdown(); + return; } std::cout << "UNABLE TO PROCESS INCOMING XML TOKEN" << std::endl; From fd19258607ed91f36e12e121ab5fa7e0082a7248 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sun, 13 Sep 2026 11:21:02 +1000 Subject: [PATCH 22/26] Ignore AGENTS.md in git Add `AGENTS.md` to `.gitignore` so the local agent guidance file stays untracked. --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index f672adb..0811207 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,8 @@ Testing/ # IDE Folders .idea/ .vscode/ + +# AI +AGENTS.md +.agents/ +.claude/ From f019bce51bda24b940e4dadd9a6ee9e3a4e51c9a Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Mon, 14 Sep 2026 15:01:39 +1000 Subject: [PATCH 23/26] Ignore Windows nul artifact Add the reserved Windows `nul` filename to `.gitignore` so editor or build output does not accidentally create a tracked file that cannot exist on Windows. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0811207..41a2613 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,7 @@ Testing/ # IDE Folders .idea/ .vscode/ +nul # AI AGENTS.md From a47cc33e41ea3e06bad4f4496f58efaf45accfcd Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Wed, 16 Sep 2026 22:21:32 +1000 Subject: [PATCH 24/26] Add TLS configuration and secure upgrades Introduces shared TLS settings loaded from config and validates certificate/key files at startup. TcpConnection now keeps the SSL context alive across async handshakes and avoids shutdown races during TLS negotiation. ClientSession upgrades XMPP sessions to TLS and handles shutdown/read cleanup more safely. --- CMakeLists.txt | 1 + apps/main.cpp | 32 ++++++++- include/xtrpg/network/TcpConnection.hpp | 18 ++++- include/xtrpg/network/TlsConfig.hpp | 33 +++++++++ include/xtrpg/network/TlsConfigProvider.hpp | 26 +++++++ include/xtrpg/xmpp/session/ClientSession.hpp | 7 +- .../xmpp/stream/NegotiationStreamHandler.hpp | 7 +- src/network/TcpConnection.cpp | 29 +++++++- src/network/TlsConfig.cpp | 20 ++++++ src/xmpp/session/ClientSession.cpp | 71 ++++++++++++++++--- 10 files changed, 222 insertions(+), 22 deletions(-) create mode 100644 include/xtrpg/network/TlsConfig.hpp create mode 100644 include/xtrpg/network/TlsConfigProvider.hpp create mode 100644 src/network/TlsConfig.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b2f5093..f3478f1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,6 +70,7 @@ target_sources(xtrpg_cpp_server PRIVATE src/config/ConfigManager.cpp src/network/SocketConnectionListener.cpp src/network/TcpConnection.cpp + src/network/TlsConfig.cpp src/xml/tokenizer/XmlStreamTokenizer.cpp src/xmpp/ClientConnectionManager.cpp src/xmpp/session/ClientSession.cpp diff --git a/apps/main.cpp b/apps/main.cpp index 0e5bacd..0d91e89 100644 --- a/apps/main.cpp +++ b/apps/main.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -12,6 +13,8 @@ #include "xtrpg/config/ConfigManager.hpp" #include "xtrpg/interface/Observer.hpp" #include "xtrpg/network/SocketConnectionListener.hpp" +#include "xtrpg/network/TlsConfig.hpp" +#include "xtrpg/network/TlsConfigProvider.hpp" #include "xtrpg/xml/tokenizer/XmlStreamTokenizer.hpp" #include "xtrpg/xmpp/ClientConnectionManager.hpp" #include "xtrpg/xmpp/session/ClientSession.hpp" @@ -67,6 +70,31 @@ int main(int argc, char *argv[]) { } configManager.parseCLI(argc, argv); + // Validate TLS certificate and key files exist before starting server + auto certPath = configManager.get("tls", "cert_path") + .value_or("./server.crt"); + auto keyPath = configManager.get("tls", "key_path") + .value_or("./server.key"); + + if (!std::filesystem::exists(certPath)) { + std::cerr << "[ERROR] TLS certificate file not found: " << certPath + << std::endl; + return 1; + } + + if (!std::filesystem::exists(keyPath)) { + std::cerr << "[ERROR] TLS key file not found: " << keyPath << std::endl; + return 1; + } + + std::cout << "[INFO] TLS configuration validated:" << std::endl + << " Certificate: " << certPath << std::endl + << " Key: " << keyPath << std::endl; + + // Initialize the shared TLS settings object during startup. + xtrpg::network::initializeTlsSettings( + xtrpg::network::TlsSettings{.certPath = certPath, .keyPath = keyPath}); + // Initialize Asio IO context for async I/O operations asio::io_context ioContext; @@ -122,11 +150,11 @@ int main(int argc, char *argv[]) { } catch (const std::exception &e) { std::cerr << "EXCEPTION OCCURRED" << std::endl << "Application closing die to \"" << e.what() << "\"." - << std ::endl; + << std::endl; return 1; } catch (...) { std::cerr << "UNEXPECTED EXCEPTION OCCURRED" << std::endl - << "Application closing." << std ::endl; + << "Application closing." << std::endl; return 1; } diff --git a/include/xtrpg/network/TcpConnection.hpp b/include/xtrpg/network/TcpConnection.hpp index 62eb3fd..55fb1cb 100644 --- a/include/xtrpg/network/TcpConnection.hpp +++ b/include/xtrpg/network/TcpConnection.hpp @@ -52,9 +52,10 @@ class TcpConnection { /** * Upgrades the TCP connection to a TLS connection using the provided SSL - * context. + * context. The context is retained by the connection so that the SSL stream + * and any pending handshake keep a valid OpenSSL context alive. */ - void upgrade(asio::ssl::context &ssl_ctx); + void upgrade(asio::ssl::context ssl_ctx); /** * Async read from the underlying tcp connection, calling the provided lambda @@ -161,11 +162,24 @@ class TcpConnection { */ std::optional> _strand; + /** + * The SSL context used by the TLS stream. It must outlive the stream because + * the stream and any queued handshake still reference OpenSSL objects owned + * by the context. + */ + std::optional _sslContext; + /** * 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; + + /** Tracks whether a TLS handshake is still in flight. This gate prevents + * shutdown from issuing async_shutdown against an SSL stream that has not yet + * finished negotiating its record layer state. + */ + std::atomic _tlsHandshakeInProgress{false}; }; } // namespace xtrpg::network \ No newline at end of file diff --git a/include/xtrpg/network/TlsConfig.hpp b/include/xtrpg/network/TlsConfig.hpp new file mode 100644 index 0000000..299dcf4 --- /dev/null +++ b/include/xtrpg/network/TlsConfig.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +#include "xtrpg/config/ConfigManager.hpp" + +namespace xtrpg::network { + +struct TlsSettings { + std::string certPath = "./server.crt"; + std::string keyPath = "./server.key"; + + static std::vector configOptions() { + return {{.key = "cert_path", + .defaultValue = std::string("./server.crt"), + .description = " Path to the TLS certificate file"}, + {.key = "key_path", + .defaultValue = std::string("./server.key"), + .description = " Path to the TLS private key file"}}; + } +}; + +/** + * Shared process-level TLS configuration used for all server connections. + * Initialize it once during startup and treat it as immutable after that. + */ +extern TlsSettings g_tlsSettings; + +void initializeTlsSettings(const TlsSettings &settings); +const TlsSettings &getTlsSettings(); + +} // namespace xtrpg::network diff --git a/include/xtrpg/network/TlsConfigProvider.hpp b/include/xtrpg/network/TlsConfigProvider.hpp new file mode 100644 index 0000000..da3e871 --- /dev/null +++ b/include/xtrpg/network/TlsConfigProvider.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include "xtrpg/config/ConfigManager.hpp" +#include "xtrpg/network/TlsConfig.hpp" + +namespace xtrpg::network { + +/** + * Configuration provider for XMPP TLS settings, including certificate and key + * file paths. + */ +class TlsConfigProvider : public xtrpg::config::IModuleConfigProvider { +public: + /** Returns the TLS configuration schema. */ + xtrpg::config::ModuleConfig getConfigSchema() const override { + return {.name = "tls", + .description = "TLS/SSL configuration for XMPP server", + .options = TlsSettings::configOptions()}; + } +}; + +REGISTER_MODULE_CONFIG(TlsConfigProvider); + +} // namespace xtrpg::network diff --git a/include/xtrpg/xmpp/session/ClientSession.hpp b/include/xtrpg/xmpp/session/ClientSession.hpp index 7553695..8cc714e 100644 --- a/include/xtrpg/xmpp/session/ClientSession.hpp +++ b/include/xtrpg/xmpp/session/ClientSession.hpp @@ -55,7 +55,10 @@ class ClientSession : public xml::tokenizer::XmlTokenListener { void process(); /** Returns whether the owned connection has reached the closed state. */ - bool isClosed() const { return this->_ptrTcpConnection->isClosed(); } + bool isClosed() const { + return this->_ptrTcpConnection == nullptr || + this->_ptrTcpConnection->isClosed(); + } /** Asynchronously writes raw XML or other protocol data to the client. */ void sendRaw(std::string_view data); @@ -89,6 +92,8 @@ class ClientSession : public xml::tokenizer::XmlTokenListener { return nullptr != this->_ptrActiveStreamHandler; } + void upgradeTcpConnectionToTls(); + private: /** TCP connection owned by this session. */ network::TcpConnection *_ptrTcpConnection; diff --git a/include/xtrpg/xmpp/stream/NegotiationStreamHandler.hpp b/include/xtrpg/xmpp/stream/NegotiationStreamHandler.hpp index 9c6f5a3..871cc59 100644 --- a/include/xtrpg/xmpp/stream/NegotiationStreamHandler.hpp +++ b/include/xtrpg/xmpp/stream/NegotiationStreamHandler.hpp @@ -61,12 +61,7 @@ class NegotiationStreamHandler : public StreamHandler { }); // Execute async SSL handshake and transition to unauthenticated phase - // session.upgrade_to_tls([&session]() { - // session.reset_parser(); - // Transition will wait for the client's post-TLS header - // session.set_state(nullptr); // Waits for next header to instantiate - // UnauthenticatedState - // }); + session.upgradeTcpConnectionToTls(); } }; } // namespace xtrpg::xmpp::stream \ No newline at end of file diff --git a/src/network/TcpConnection.cpp b/src/network/TcpConnection.cpp index 723c5ea..505c391 100644 --- a/src/network/TcpConnection.cpp +++ b/src/network/TcpConnection.cpp @@ -13,25 +13,43 @@ void TcpConnection::dispatchCloseCallbacks() { } } -void TcpConnection::upgrade(asio::ssl::context &ssl_ctx) { +void TcpConnection::upgrade(asio::ssl::context ssl_ctx) { if (!this->isOpen()) { return; } - asio::post(*this->_strand, [this, &ssl_ctx]() { + asio::post(*this->_strand, [this, ssl_ctx = std::move(ssl_ctx)]() mutable { if (this->isClosed() || this->isClosing() || this->isSecure()) { return; } - this->_sslStream.emplace(std::move(this->_tcpSocket), ssl_ctx); + this->_sslContext.emplace(std::move(ssl_ctx)); + this->_sslStream.emplace(std::move(this->_tcpSocket), *this->_sslContext); + this->_tlsHandshakeInProgress = true; this->_sslStream->async_handshake( asio::ssl::stream_base::server, [this](std::error_code ec) { + this->_tlsHandshakeInProgress = false; + if (ec) { + if (this->is(ConnectionState::CLOSING)) { + this->_sslStream->lowest_layer().close(); + this->dispatchStateChange(ConnectionState::CLOSED); + this->dispatchCloseCallbacks(); + return; + } + this->close(); return; } + if (this->is(ConnectionState::CLOSING)) { + this->_sslStream->lowest_layer().close(); + this->dispatchStateChange(ConnectionState::CLOSED); + this->dispatchCloseCallbacks(); + return; + } + this->dispatchStateChange(ConnectionState::SECURE); }); }); @@ -158,6 +176,11 @@ void TcpConnection::close(std::function callback) { // Serialize transport shutdown with reads and writes on the strand. asio::post(*this->_strand, [this]() { + if (this->_tlsHandshakeInProgress && this->_sslStream) { + this->_sslStream->lowest_layer().cancel(); + return; + } + if (this->isSecure() && this->_sslStream) { this->_sslStream->lowest_layer().cancel(); diff --git a/src/network/TlsConfig.cpp b/src/network/TlsConfig.cpp new file mode 100644 index 0000000..d9dc81c --- /dev/null +++ b/src/network/TlsConfig.cpp @@ -0,0 +1,20 @@ +#include + +#include "xtrpg/network/TlsConfig.hpp" + +namespace xtrpg::network { + +TlsSettings g_tlsSettings{}; + +void initializeTlsSettings(const TlsSettings &settings) { + if (settings.certPath.empty() || settings.keyPath.empty()) { + throw std::invalid_argument( + "TLS certificate and key paths must not be empty"); + } + + g_tlsSettings = settings; +} + +const TlsSettings &getTlsSettings() { return g_tlsSettings; } + +} // namespace xtrpg::network diff --git a/src/xmpp/session/ClientSession.cpp b/src/xmpp/session/ClientSession.cpp index c563aef..c3aeae1 100644 --- a/src/xmpp/session/ClientSession.cpp +++ b/src/xmpp/session/ClientSession.cpp @@ -1,5 +1,8 @@ #include "xtrpg/xmpp/session/ClientSession.hpp" +#include + +#include "xtrpg/network/TlsConfig.hpp" #include "xtrpg/utils/String.hpp" #include "xtrpg/xmpp/stream/NegotiationStreamHandler.hpp" #include "xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp" @@ -15,8 +18,10 @@ ClientSession::~ClientSession() { delete this->_ptrRootStreamNode; delete this->_ptrDeclarationNode; this->_tokenizer.setObserver(nullptr); - delete this->_ptrTcpConnection; + + auto *tcpConnection = this->_ptrTcpConnection; this->_ptrTcpConnection = nullptr; + delete tcpConnection; if (nullptr != this->_ptrCurrentXmlNode) { std::lock_guard lock(this->_currentXmlNodeMutex); @@ -34,21 +39,31 @@ void ClientSession::start() { } void ClientSession::stop() { - if (!this->_isStopped.exchange(true)) { + if (!this->_isStopped.exchange(true) && this->_ptrTcpConnection != nullptr) { this->_ptrTcpConnection->cancelRead(); } } void ClientSession::shutdown() { - this->stop(); if (this->_isShutdown.exchange(true)) { return; } - this->_ptrTcpConnection->close([this]() { this->notifyCompletion(); }); + + this->stop(); + + auto *tcpConnection = this->_ptrTcpConnection; + if (tcpConnection == nullptr) { + this->notifyCompletion(); + return; + } + + this->_ptrTcpConnection = nullptr; + tcpConnection->close([tcpConnection]() { delete tcpConnection; }); + this->notifyCompletion(); } void ClientSession::sendRaw(std::string_view data) { - if (!this->_isShutdown) { + if (!this->_isShutdown && this->_ptrTcpConnection != nullptr) { *this->_ptrTcpConnection << data; } } @@ -60,13 +75,17 @@ void ClientSession::send(const xml::node::INode &xmlNode) { } void ClientSession::process() { - if (this->_isStopped || this->_isShutdown) { + if (this->_isStopped || this->_isShutdown || + this->_ptrTcpConnection == nullptr) { return; } this->_ptrTcpConnection->read( [this](const std::error_code &error, std::istream &stream) { - if (error || this->_isStopped || this->_isShutdown) { - this->notifyCompletion(); + if (error || this->_isStopped || this->_isShutdown || + this->_ptrTcpConnection == nullptr) { + if (!this->_isShutdown && this->_ptrTcpConnection != nullptr) { + this->shutdown(); + } return; } this->_tokenizer.process(stream); @@ -259,4 +278,40 @@ const stream::StreamHandler *ClientSession::getActiveStreamHandler() const { return this->_ptrActiveStreamHandler; } +void ClientSession::upgradeTcpConnectionToTls() { + const auto &tlsSettings = xtrpg::network::getTlsSettings(); + + if (tlsSettings.certPath.empty() || tlsSettings.keyPath.empty()) { + std::cerr << "[ERROR] TLS settings are not initialized" << std::endl; + this->shutdown(); + return; + } + + try { + // Create SSL context configured for TLS server mode. This must stay alive + // for the lifetime of the upgraded TLS stream because the async handshake + // holds OpenSSL state associated with the context. + asio::ssl::context ssl_ctx(asio::ssl::context::tls_server); + + // Set to use TLSv1.2 or higher + ssl_ctx.set_options(asio::ssl::context::default_workarounds | + asio::ssl::context::no_sslv2 | + asio::ssl::context::single_dh_use); + + // Load the server certificate + ssl_ctx.use_certificate_chain_file(tlsSettings.certPath); + + // Load the private key + ssl_ctx.use_private_key_file(tlsSettings.keyPath, asio::ssl::context::pem); + + // Upgrade the TCP connection to TLS. The connection owns the context so it + // remains valid while the async handshake is still in flight. + this->_ptrTcpConnection->upgrade(std::move(ssl_ctx)); + } catch (const std::exception &e) { + std::cerr << "[ERROR] Failed to upgrade connection to TLS: " << e.what() + << std::endl; + this->shutdown(); + } +} + } // namespace xtrpg::xmpp::session From d1e5438ad57d5e44df555f5a7ca5c725e79af35f Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Wed, 16 Sep 2026 22:28:42 +1000 Subject: [PATCH 25/26] Reuse shared whitespace helper in XML parser Move whitespace detection into `xtrpg::utils::string` and update `XmlStreamTokenizer` to use the shared helper instead of its local implementation. This centralizes character classification logic and avoids duplicating the same `isspace` check. --- include/xtrpg/utils/String.hpp | 4 ++++ src/xml/tokenizer/XmlStreamTokenizer.cpp | 29 ++++++++++++------------ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/include/xtrpg/utils/String.hpp b/include/xtrpg/utils/String.hpp index f07c8e3..5efd915 100644 --- a/include/xtrpg/utils/String.hpp +++ b/include/xtrpg/utils/String.hpp @@ -43,4 +43,8 @@ inline size_t countUtf8CodePoints(const std::string &str) { } return count; } + +inline bool isWhitespace(const char character) { + return std::isspace(static_cast(character)) != 0; +} } // namespace xtrpg::utils::string \ No newline at end of file diff --git a/src/xml/tokenizer/XmlStreamTokenizer.cpp b/src/xml/tokenizer/XmlStreamTokenizer.cpp index 1e84266..f4a4ccb 100644 --- a/src/xml/tokenizer/XmlStreamTokenizer.cpp +++ b/src/xml/tokenizer/XmlStreamTokenizer.cpp @@ -3,6 +3,8 @@ #include #include +#include "xtrpg/utils/String.hpp" + namespace { bool isNameCharacter(const char character) { return std::isalnum(static_cast(character)) != 0 || @@ -10,9 +12,6 @@ bool isNameCharacter(const char character) { character == '.'; } -bool isWhitespace(const char character) { - return std::isspace(static_cast(character)) != 0; -} } // namespace namespace xtrpg::xml::tokenizer { @@ -98,7 +97,7 @@ void XmlStreamTokenizer::process(std::istream &stream) { if (this->_buffer.size() > __TOKENIZER_MAX_BUFFER_SIZE_IN_CHARS) { bufferExceeded(); } - } else if (isWhitespace(character)) { + } else if (utils::string::isWhitespace(character)) { if (this->_buffer.empty()) { fail(TokenizationError::MALFORMED_INPUT); } else { @@ -128,7 +127,7 @@ void XmlStreamTokenizer::process(std::istream &stream) { } break; case State::START_TAG_BODY: - if (isWhitespace(character)) { + if (utils::string::isWhitespace(character)) { break; } if (character == '>') { @@ -149,7 +148,7 @@ void XmlStreamTokenizer::process(std::istream &stream) { if (this->_buffer.size() > __TOKENIZER_MAX_BUFFER_SIZE_IN_CHARS) { bufferExceeded(); } - } else if (isWhitespace(character)) { + } else if (utils::string::isWhitespace(character)) { this->_attributeName = this->_buffer; this->_buffer.clear(); this->_state = State::ATTRIBUTE_AFTER_NAME; @@ -162,7 +161,7 @@ void XmlStreamTokenizer::process(std::istream &stream) { } break; case State::ATTRIBUTE_AFTER_NAME: - if (isWhitespace(character)) { + if (utils::string::isWhitespace(character)) { break; } if (character == '=') { @@ -172,7 +171,7 @@ void XmlStreamTokenizer::process(std::istream &stream) { } break; case State::ATTRIBUTE_VALUE_START: - if (isWhitespace(character)) { + if (utils::string::isWhitespace(character)) { break; } if (character == '\'' || character == '"') { @@ -202,7 +201,7 @@ void XmlStreamTokenizer::process(std::istream &stream) { if (this->_buffer.size() > __TOKENIZER_MAX_BUFFER_SIZE_IN_CHARS) { bufferExceeded(); } - } else if (isWhitespace(character)) { + } else if (utils::string::isWhitespace(character)) { this->_currentToken.content = this->_buffer; this->_buffer.clear(); this->_state = State::END_TAG_BODY; @@ -220,7 +219,7 @@ void XmlStreamTokenizer::process(std::istream &stream) { } break; case State::END_TAG_BODY: - if (isWhitespace(character)) { + if (utils::string::isWhitespace(character)) { break; } if (character == '>') { @@ -236,7 +235,7 @@ void XmlStreamTokenizer::process(std::istream &stream) { if (this->_buffer.size() > __TOKENIZER_MAX_BUFFER_SIZE_IN_CHARS) { bufferExceeded(); } - } else if (isWhitespace(character)) { + } else if (utils::string::isWhitespace(character)) { if (this->_buffer.empty()) { fail(TokenizationError::MALFORMED_INPUT); } else { @@ -257,7 +256,7 @@ void XmlStreamTokenizer::process(std::istream &stream) { } break; case State::DECLARATION_BODY: - if (isWhitespace(character)) { + if (utils::string::isWhitespace(character)) { break; } if (character == '?') { @@ -275,7 +274,7 @@ void XmlStreamTokenizer::process(std::istream &stream) { if (this->_buffer.size() > __TOKENIZER_MAX_BUFFER_SIZE_IN_CHARS) { bufferExceeded(); } - } else if (isWhitespace(character)) { + } else if (utils::string::isWhitespace(character)) { this->_attributeName = this->_buffer; this->_buffer.clear(); this->_state = State::DECLARATION_ATTRIBUTE_AFTER_NAME; @@ -288,7 +287,7 @@ void XmlStreamTokenizer::process(std::istream &stream) { } break; case State::DECLARATION_ATTRIBUTE_AFTER_NAME: - if (isWhitespace(character)) { + if (utils::string::isWhitespace(character)) { break; } if (character == '=') { @@ -298,7 +297,7 @@ void XmlStreamTokenizer::process(std::istream &stream) { } break; case State::DECLARATION_ATTRIBUTE_VALUE_START: - if (isWhitespace(character)) { + if (utils::string::isWhitespace(character)) { break; } if (character == '\'' || character == '"') { From 7fc2a5adb50002a2b3278a9de9d7682de7b56acd Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Wed, 16 Sep 2026 23:03:57 +1000 Subject: [PATCH 26/26] Move main.cpp to src and update CMake Relocates the server entry point from `apps/main.cpp` to `src/main.cpp` and updates the executable source path in `CMakeLists.txt` so builds continue to resolve the main target correctly. --- CMakeLists.txt | 2 +- {apps => src}/main.cpp | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename {apps => src}/main.cpp (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index f3478f1..6d3b66a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,7 +59,7 @@ endif() # Single executable target add_executable(xtrpg_cpp_server - apps/main.cpp + src/main.cpp ) target_include_directories(xtrpg_cpp_server PRIVATE include diff --git a/apps/main.cpp b/src/main.cpp similarity index 100% rename from apps/main.cpp rename to src/main.cpp