Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d15612d
Add StreamHandler and integrate with ClientSession
XenoSnowFox Sep 6, 2026
45ba91e
Add active stream handler check
XenoSnowFox Sep 7, 2026
6d4a110
Add XML stream parsing skeleton to ClientSession
XenoSnowFox Sep 7, 2026
d3146e3
Handle XML stream teardown and tracking
XenoSnowFox Sep 7, 2026
11bf8ea
Make string utilities inline; add isBlank
XenoSnowFox Sep 7, 2026
8c908f4
Log active client connections
XenoSnowFox Sep 7, 2026
e84a259
Combine stream start and use internal-server-error
XenoSnowFox Sep 7, 2026
6065a8c
Validate and log incoming XMPP stream start
XenoSnowFox Sep 7, 2026
44acf04
Remove debug logging from TcpConnection
XenoSnowFox Sep 7, 2026
e07386b
Use TagNode for stanza parameter
XenoSnowFox Sep 12, 2026
684e00a
Fix IPv6 dual-stack listener setup
XenoSnowFox Sep 12, 2026
2b2510d
Add XMPP TLS negotiation stream handler
XenoSnowFox Sep 12, 2026
7fd4521
Send XMPP stream-start error response
XenoSnowFox Sep 12, 2026
d39c1e3
Send XML nodes and negotiate streams
XenoSnowFox Sep 12, 2026
b7e4ce6
Merge branch 'main' into feature/negotiation-stream-handler
XenoSnowFox Sep 12, 2026
f8c53cd
Expose base append; add templated send helper
XenoSnowFox Sep 12, 2026
cc80770
Tighten TagNode::append comment & formatting
XenoSnowFox Sep 12, 2026
dbc8860
Update UnimplementedStreamHandler.hpp
XenoSnowFox Sep 12, 2026
5351695
Add XML attribute convenience setter
XenoSnowFox Sep 12, 2026
f3047c1
Use TagNode builders in TLS negotiation
XenoSnowFox Sep 12, 2026
27245d4
Reduce XML debug noise in ClientSession
XenoSnowFox Sep 12, 2026
3e1735d
Improve XML token handling in ClientSession
XenoSnowFox Sep 12, 2026
fd19258
Ignore AGENTS.md in git
XenoSnowFox Sep 13, 2026
f019bce
Ignore Windows nul artifact
XenoSnowFox Sep 14, 2026
a47cc33
Add TLS configuration and secure upgrades
XenoSnowFox Sep 16, 2026
d1e5438
Reuse shared whitespace helper in XML parser
XenoSnowFox Sep 16, 2026
7fc2a5a
Move main.cpp to src and update CMake
XenoSnowFox Sep 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,9 @@ Testing/
# IDE Folders
.idea/
.vscode/
nul

# AI
AGENTS.md
.agents/
.claude/
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
19 changes: 16 additions & 3 deletions include/xtrpg/network/TcpConnection.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -129,7 +130,6 @@ class TcpConnection {

void
appendStateChangeCallback(std::function<void(ConnectionState)> callback) {
std::cout << "[TcpConnection] Append State Change Callback." << std::endl;
this->_stateChangeCallbacks.push_back(callback);
}

Expand Down Expand Up @@ -162,11 +162,24 @@ class TcpConnection {
*/
std::optional<asio::strand<asio::any_io_executor>> _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<asio::ssl::context> _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<asio::ssl::stream<asio::ip::tcp::socket>> _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<bool> _tlsHandshakeInProgress{false};
};
} // namespace xtrpg::network
33 changes: 33 additions & 0 deletions include/xtrpg/network/TlsConfig.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#pragma once

#include <string>
#include <vector>

#include "xtrpg/config/ConfigManager.hpp"

namespace xtrpg::network {

struct TlsSettings {
std::string certPath = "./server.crt";
std::string keyPath = "./server.key";

static std::vector<xtrpg::config::ConfigOption> configOptions() {
return {{.key = "cert_path",
.defaultValue = std::string("./server.crt"),
.description = "<path> Path to the TLS certificate file"},
{.key = "key_path",
.defaultValue = std::string("./server.key"),
.description = "<path> 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
26 changes: 26 additions & 0 deletions include/xtrpg/network/TlsConfigProvider.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#pragma once

#include <string>

#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
25 changes: 20 additions & 5 deletions include/xtrpg/utils/String.hpp
Original file line number Diff line number Diff line change
@@ -1,27 +1,38 @@
#pragma once

#include <algorithm>
#include <cctype>
#include <string>

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
Expand All @@ -32,4 +43,8 @@ size_t countUtf8CodePoints(const std::string &str) {
}
return count;
}

inline bool isWhitespace(const char character) {
return std::isspace(static_cast<unsigned char>(character)) != 0;
}
} // namespace xtrpg::utils::string
7 changes: 7 additions & 0 deletions include/xtrpg/xml/node/IAttributes.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
22 changes: 22 additions & 0 deletions include/xtrpg/xml/node/TagNode.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
*/
Expand Down
49 changes: 44 additions & 5 deletions include/xtrpg/xmpp/session/ClientSession.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,23 @@
#include <atomic>
#include <functional>
#include <mutex>
#include <sstream>
#include <string_view>
#include <utility>

#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. */
Expand All @@ -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();
Expand All @@ -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 <typename Consumer>
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;
Expand All @@ -89,6 +120,14 @@ class ClientSession : public xml::tokenizer::XmlTokenListener {
/** Ensures completion is reported at most once. */
std::atomic<bool> _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();
};
Expand Down
Loading
Loading