diff --git a/.gitignore b/.gitignore index f672adb..41a2613 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,9 @@ Testing/ # IDE Folders .idea/ .vscode/ +nul + +# AI +AGENTS.md +.agents/ +.claude/ diff --git a/CMakeLists.txt b/CMakeLists.txt index b2f5093..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 @@ -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/include/xtrpg/network/TcpConnection.hpp b/include/xtrpg/network/TcpConnection.hpp index e585652..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 @@ -129,7 +130,6 @@ class TcpConnection { void appendStateChangeCallback(std::function callback) { - std::cout << "[TcpConnection] Append State Change Callback." << std::endl; this->_stateChangeCallbacks.push_back(callback); } @@ -162,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/utils/String.hpp b/include/xtrpg/utils/String.hpp index d2b1535..5efd915 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 @@ -32,4 +43,8 @@ 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/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/xml/node/TagNode.hpp b/include/xtrpg/xml/node/TagNode.hpp index 8728f0f..264c90e 100644 --- a/include/xtrpg/xml/node/TagNode.hpp +++ b/include/xtrpg/xml/node/TagNode.hpp @@ -57,6 +57,15 @@ 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. + * 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); } + /** * Appends a new TagNode with the provided tag name to this container. * The consumer function is called with the new TagNode to allow configuration @@ -92,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 88de627..8cc714e 100644 --- a/include/xtrpg/xmpp/session/ClientSession.hpp +++ b/include/xtrpg/xmpp/session/ClientSession.hpp @@ -3,17 +3,23 @@ #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" #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 +31,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(); @@ -52,17 +55,45 @@ 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); + /** Asynchronously writes an XML node to the client. */ + void send(const xml::node::INode &xmlNode); + + template + void send(const std::string &tagname, Consumer &&consumer) { + xml::node::TagNode tagNode(tagname); + consumer(tagNode); + this->send(tagNode); + } + /** Handles one token emitted by the XML stream tokenizer. */ void onXmlToken(const xml::tokenizer::XmlToken &xmlToken); /** 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; + + /** + * Returns whether the active stream handler has been defined or not. + */ + bool hasActiveStreamHandler() const { + return nullptr != this->_ptrActiveStreamHandler; + } + + void upgradeTcpConnectionToTls(); + private: /** TCP connection owned by this session. */ network::TcpConnection *_ptrTcpConnection; @@ -89,6 +120,14 @@ 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; + + /** 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/include/xtrpg/xmpp/stream/NegotiationStreamHandler.hpp b/include/xtrpg/xmpp/stream/NegotiationStreamHandler.hpp new file mode 100644 index 0000000..871cc59 --- /dev/null +++ b/include/xtrpg/xmpp/stream/NegotiationStreamHandler.hpp @@ -0,0 +1,67 @@ +#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.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 { + 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.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.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.upgradeTcpConnectionToTls(); + } +}; +} // namespace xtrpg::xmpp::stream \ No newline at end of file diff --git a/include/xtrpg/xmpp/stream/StreamHandler.hpp b/include/xtrpg/xmpp/stream/StreamHandler.hpp new file mode 100644 index 0000000..93a133d --- /dev/null +++ b/include/xtrpg/xmpp/stream/StreamHandler.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include + +#include "xtrpg/xml/node/TagNode.hpp" +#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 xml::node::TagNode &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..1b1407b --- /dev/null +++ b/include/xtrpg/xmpp/stream/UnimplementedStreamHandler.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "xtrpg/xml/node/TagNode.hpp" +#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.send("stream:error", [](xml::node::TagNode &streamError) { + streamError.append("internal-server-error", [](xml::node::TagNode &node) { + node.set("xmlns", "urn:ietf:params:xml:ns:xmpp-streams"); + }); + streamError.append("text", [](xml::node::TagNode &node) { + node.set("xmlns", "urn:ietf:params:xml:ns:xmpp-streams"); + node.set("xml:lang", "en"); + node.append("An unexpected error occurred."); + }); + }); + + session.shutdown(); + } + + void onEnd(session::ClientSession &session) const override { + session.sendRaw(""); + } + + void onStanza(session::ClientSession &session, + const xml::node::TagNode &stanza) const override { + // Not implemented + } +}; +} // namespace xtrpg::xmpp::stream diff --git a/apps/main.cpp b/src/main.cpp similarity index 78% rename from apps/main.cpp rename to src/main.cpp index 0e5bacd..0d91e89 100644 --- a/apps/main.cpp +++ b/src/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/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 " diff --git a/src/network/TcpConnection.cpp b/src/network/TcpConnection.cpp index cb9f113..505c391 100644 --- a/src/network/TcpConnection.cpp +++ b/src/network/TcpConnection.cpp @@ -13,28 +13,43 @@ void TcpConnection::dispatchCloseCallbacks() { } } -void TcpConnection::upgrade(asio::ssl::context &ssl_ctx) { +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; } - 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); }); }); @@ -43,17 +58,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 +129,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 +158,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,11 +172,15 @@ 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. 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/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 == '"') { 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(); diff --git a/src/xmpp/session/ClientSession.cpp b/src/xmpp/session/ClientSession.cpp index c768daa..c3aeae1 100644 --- a/src/xmpp/session/ClientSession.cpp +++ b/src/xmpp/session/ClientSession.cpp @@ -1,13 +1,33 @@ #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" + namespace xtrpg::xmpp::session { +ClientSession::ClientSession(network::TcpConnection *tcpConnection) + : _ptrTcpConnection(tcpConnection) { + this->_tokenizer.setObserver(this); +} + 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); + delete this->_ptrCurrentXmlNode; + this->_ptrCurrentXmlNode = nullptr; + } } void ClientSession::start() { @@ -19,33 +39,53 @@ 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; } } +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) { + 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); @@ -68,6 +108,146 @@ 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) { + 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 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; + } + + // 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->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; + } + + // 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; + } + + // 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 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) { + this->sendRaw(""); + this->setActiveStreamHandler(nullptr); + 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. + // 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( @@ -78,10 +258,60 @@ 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; } 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; +} + +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