From 6ad9bfff3fa391c03aad94c31171627a8d5f0392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sun, 5 Jul 2026 21:24:28 +0200 Subject: [PATCH 01/41] Working POC for networking --- src/client/CMakeLists.txt | 4 + src/client/Main.qml | 221 ++++++++++++++++++++++++++ src/client/client.cpp | 7 + src/client/client.h | 2 + src/client/client_network_manager.cpp | 145 +++++++++++++++++ src/client/client_network_manager.h | 48 ++++++ src/server/CMakeLists.txt | 18 ++- src/server/client_session.cpp | 70 ++++++++ src/server/client_session.h | 33 ++++ src/server/server.cpp | 84 +++++++++- src/server/server.h | 27 +++- src/shared/CMakeLists.txt | 16 ++ src/shared/message_envelope.cpp | 27 ++++ src/shared/message_envelope.h | 36 +++++ 14 files changed, 727 insertions(+), 11 deletions(-) create mode 100644 src/client/client_network_manager.cpp create mode 100644 src/client/client_network_manager.h create mode 100644 src/server/client_session.cpp create mode 100644 src/server/client_session.h create mode 100644 src/shared/message_envelope.cpp create mode 100644 src/shared/message_envelope.h diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index 8775839..b534054 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -5,7 +5,10 @@ qt_standard_project_setup(REQUIRES 6.5) qt_add_executable(Messenger-Client WIN32 MACOSX_BUNDLE + client_network_manager.cpp + client_network_manager.h client.cpp + client.h ) set_target_properties(Messenger-Client PROPERTIES @@ -25,4 +28,5 @@ target_link_libraries(Messenger-Client Qt6::Quick Qt6::Qml Qt6::Network + Messenger-Shared ) diff --git a/src/client/Main.qml b/src/client/Main.qml index f71ebff..45a196d 100644 --- a/src/client/Main.qml +++ b/src/client/Main.qml @@ -1,4 +1,5 @@ import QtQuick +import QtQml Window { width: 960 @@ -7,4 +8,224 @@ Window { title: qsTr("Messenger Client") color: "#f7f8fa" + + property string receivedMessages: "" + + Column { + anchors { + fill: parent + margins: 32 + } + spacing: 16 + + Text { + text: qsTr("Messenger Client") + color: "#1f2933" + font.pixelSize: 36 + } + + Text { + text: networkManager.statusText + color: networkManager.connected ? "#127a3a" : "#7a2830" + font.pixelSize: 16 + } + + Row { + spacing: 12 + + Rectangle { + width: 240 + height: 40 + radius: 6 + color: "#ffffff" + border.color: "#c8d0d9" + + TextInput { + id: hostInput + anchors.fill: parent + anchors.margins: 10 + text: "127.0.0.1" + color: "#1f2933" + font.pixelSize: 15 + verticalAlignment: TextInput.AlignVCenter + selectByMouse: true + } + } + + Rectangle { + width: 96 + height: 40 + radius: 6 + color: "#ffffff" + border.color: "#c8d0d9" + + TextInput { + id: portInput + anchors.fill: parent + anchors.margins: 10 + text: String(networkManager.defaultPort) + color: "#1f2933" + font.pixelSize: 15 + verticalAlignment: TextInput.AlignVCenter + validator: IntValidator { bottom: 1; top: 65535 } + selectByMouse: true + } + } + + Rectangle { + width: 120 + height: 40 + radius: 6 + color: networkManager.connected ? "#d7dde5" : "#205493" + + Text { + anchors.centerIn: parent + text: networkManager.connected ? qsTr("Connected") : qsTr("Connect") + color: networkManager.connected ? "#43515f" : "#ffffff" + font.pixelSize: 15 + } + + MouseArea { + anchors.fill: parent + enabled: !networkManager.connected + onClicked: networkManager.connectToServer(hostInput.text, Number(portInput.text)) + } + } + + Rectangle { + width: 120 + height: 40 + radius: 6 + color: networkManager.connected ? "#8b2635" : "#d7dde5" + + Text { + anchors.centerIn: parent + text: qsTr("Disconnect") + color: networkManager.connected ? "#ffffff" : "#43515f" + font.pixelSize: 15 + } + + MouseArea { + anchors.fill: parent + enabled: networkManager.connected + onClicked: networkManager.disconnectFromServer() + } + } + } + + Row { + spacing: 12 + + Rectangle { + width: 180 + height: 40 + radius: 6 + color: "#ffffff" + border.color: "#c8d0d9" + + TextInput { + id: senderInput + anchors.fill: parent + anchors.margins: 10 + text: "client" + color: "#1f2933" + font.pixelSize: 15 + verticalAlignment: TextInput.AlignVCenter + selectByMouse: true + } + } + + Rectangle { + width: 180 + height: 40 + radius: 6 + color: "#ffffff" + border.color: "#c8d0d9" + + TextInput { + id: recipientInput + anchors.fill: parent + anchors.margins: 10 + text: "server" + color: "#1f2933" + font.pixelSize: 15 + verticalAlignment: TextInput.AlignVCenter + selectByMouse: true + } + } + } + + Rectangle { + width: parent.width + height: 44 + radius: 6 + color: "#ffffff" + border.color: "#c8d0d9" + + TextInput { + id: messageInput + anchors.fill: parent + anchors.margins: 10 + color: "#1f2933" + font.pixelSize: 15 + verticalAlignment: TextInput.AlignVCenter + selectByMouse: true + } + } + + Rectangle { + width: 140 + height: 40 + radius: 6 + color: networkManager.connected ? "#205493" : "#d7dde5" + + Text { + anchors.centerIn: parent + text: qsTr("Send") + color: networkManager.connected ? "#ffffff" : "#43515f" + font.pixelSize: 15 + } + + MouseArea { + anchors.fill: parent + enabled: networkManager.connected + onClicked: { + networkManager.sendChatMessage(senderInput.text, recipientInput.text, messageInput.text) + messageInput.text = "" + } + } + } + + Rectangle { + width: parent.width + height: Math.max(180, parent.height - y) + radius: 6 + color: "#ffffff" + border.color: "#c8d0d9" + clip: true + + Text { + anchors { + fill: parent + margins: 12 + } + text: receivedMessages.length > 0 ? receivedMessages : qsTr("No messages received.") + color: "#1f2933" + font.pixelSize: 15 + wrapMode: Text.Wrap + } + } + } + + Connections { + target: networkManager + + function onMessageReceived(sender, recipient, text) { + receivedMessages += sender + " -> " + recipient + ": " + text + "\n" + } + + function onConnectionError(message) { + receivedMessages += "Error: " + message + "\n" + } + } } diff --git a/src/client/client.cpp b/src/client/client.cpp index c9ff8d3..400cfdb 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -1,7 +1,10 @@ #include "client.h" +#include "client_network_manager.h" + #include #include +#include client& client::getInstance() { static client instance; @@ -13,16 +16,20 @@ client::client() = default; client::~client() = default; int client::run(QGuiApplication& app) { + networkManager_ = std::make_unique(); engine_ = std::make_unique(); + engine_->rootContext()->setContextProperty("networkManager", networkManager_.get()); engine_->loadFromModule("Messenger.Client", "Main"); if (engine_->rootObjects().isEmpty()) { engine_.reset(); + networkManager_.reset(); return -1; } const auto exitCode = app.exec(); engine_.reset(); + networkManager_.reset(); return exitCode; } diff --git a/src/client/client.h b/src/client/client.h index 6eec7b3..80b1fd2 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -3,6 +3,7 @@ #include +class ClientNetworkManager; class QGuiApplication; class QQmlApplicationEngine; @@ -22,6 +23,7 @@ class client { client(); std::unique_ptr engine_; + std::unique_ptr networkManager_; }; #endif // MESSENGER_CLIENT_H diff --git a/src/client/client_network_manager.cpp b/src/client/client_network_manager.cpp new file mode 100644 index 0000000..e9cbb18 --- /dev/null +++ b/src/client/client_network_manager.cpp @@ -0,0 +1,145 @@ +#include "client_network_manager.h" + +#include +#include + +using messenger::protocol::CurrentProtocolVersion; +using messenger::protocol::DataStreamVersion; +using messenger::protocol::DefaultPort; +using messenger::protocol::MessageEnvelope; +using messenger::protocol::MessageType; +using messenger::protocol::ReadBufferSize; + +ClientNetworkManager::ClientNetworkManager(QObject* parent) + : QObject(parent), stream_(&socket_), statusText_(tr("Disconnected")) { + socket_.setReadBufferSize(ReadBufferSize); + stream_.setVersion(DataStreamVersion); + + connect(&socket_, &QTcpSocket::connected, this, &ClientNetworkManager::handleConnected); + connect(&socket_, &QTcpSocket::disconnected, this, &ClientNetworkManager::handleDisconnected); + connect(&socket_, &QTcpSocket::errorOccurred, this, &ClientNetworkManager::handleError); + connect(&socket_, &QTcpSocket::readyRead, this, &ClientNetworkManager::readAvailable); +} + +bool ClientNetworkManager::connected() const { + return socket_.state() == QAbstractSocket::ConnectedState; +} + +QString ClientNetworkManager::statusText() const { + return statusText_; +} + +int ClientNetworkManager::defaultPort() const { + return DefaultPort; +} + +void ClientNetworkManager::connectToServer(const QString& host, quint16 port) { + const auto trimmedHost = host.trimmed(); + if (trimmedHost.isEmpty()) { + const auto message = tr("Host must not be empty."); + setStatusText(message); + emit connectionError(message); + return; + } + + const auto previousConnected = connected(); + if (socket_.state() != QAbstractSocket::UnconnectedState) { + socket_.abort(); + } + + setStatusText(tr("Connecting to %1:%2 ...").arg(trimmedHost).arg(port)); + emitConnectedIfChanged(previousConnected); + socket_.connectToHost(trimmedHost, port); +} + +void ClientNetworkManager::disconnectFromServer() { + const auto previousConnected = connected(); + + if (socket_.state() == QAbstractSocket::UnconnectedState) { + setStatusText(tr("Disconnected")); + emitConnectedIfChanged(previousConnected); + return; + } + + setStatusText(tr("Disconnecting ...")); + socket_.disconnectFromHost(); +} + +void ClientNetworkManager::sendChatMessage(const QString& sender, const QString& recipient, const QString& text) { + if (!connected()) { + const auto message = tr("Not connected to a server."); + setStatusText(message); + emit connectionError(message); + return; + } + + MessageEnvelope message; + message.messageType = static_cast(MessageType::ChatMessage); + message.sender = sender; + message.recipient = recipient; + message.text = text; + message.timestamp = QDateTime::currentDateTimeUtc(); + + QDataStream out(&socket_); + out.setVersion(DataStreamVersion); + out << message; + socket_.flush(); +} + +void ClientNetworkManager::handleConnected() { + setStatusText(tr("Connected")); + emit connectedChanged(); +} + +void ClientNetworkManager::handleDisconnected() { + setStatusText(tr("Disconnected")); + emit connectedChanged(); +} + +void ClientNetworkManager::handleError() { + if (socket_.error() == QAbstractSocket::RemoteHostClosedError) { + return; + } + + const auto message = socket_.errorString(); + setStatusText(message); + emit connectionError(message); +} + +void ClientNetworkManager::readAvailable() { + while (socket_.bytesAvailable() > 0) { + stream_.startTransaction(); + + MessageEnvelope message; + stream_ >> message; + + if (!stream_.commitTransaction()) { + return; + } + + if (message.protocolVersion != CurrentProtocolVersion) { + const auto errorMessage = tr("Unsupported protocol version: %1").arg(message.protocolVersion); + setStatusText(errorMessage); + emit connectionError(errorMessage); + socket_.disconnectFromHost(); + return; + } + + emit messageReceived(message.sender, message.recipient, message.text); + } +} + +void ClientNetworkManager::setStatusText(const QString& statusText) { + if (statusText_ == statusText) { + return; + } + + statusText_ = statusText; + emit statusTextChanged(); +} + +void ClientNetworkManager::emitConnectedIfChanged(bool previousConnected) { + if (previousConnected != connected()) { + emit connectedChanged(); + } +} diff --git a/src/client/client_network_manager.h b/src/client/client_network_manager.h new file mode 100644 index 0000000..0a2e9ab --- /dev/null +++ b/src/client/client_network_manager.h @@ -0,0 +1,48 @@ +#ifndef MESSENGER_CLIENT_NETWORK_MANAGER_H +#define MESSENGER_CLIENT_NETWORK_MANAGER_H + +#include "message_envelope.h" + +#include +#include +#include + +class ClientNetworkManager final : public QObject { + Q_OBJECT + Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged) + Q_PROPERTY(QString statusText READ statusText NOTIFY statusTextChanged) + Q_PROPERTY(int defaultPort READ defaultPort CONSTANT) + + public: + explicit ClientNetworkManager(QObject* parent = nullptr); + + [[nodiscard]] bool connected() const; + [[nodiscard]] QString statusText() const; + [[nodiscard]] int defaultPort() const; + + Q_INVOKABLE void connectToServer(const QString& host, quint16 port); + Q_INVOKABLE void disconnectFromServer(); + Q_INVOKABLE void sendChatMessage(const QString& sender, const QString& recipient, const QString& text); + + signals: + void connectedChanged(); + void statusTextChanged(); + void messageReceived(const QString& sender, const QString& recipient, const QString& text); + void connectionError(const QString& message); + + private slots: + void handleConnected(); + void handleDisconnected(); + void handleError(); + void readAvailable(); + + private: + void setStatusText(const QString& statusText); + void emitConnectedIfChanged(bool previousConnected); + + QTcpSocket socket_; + QDataStream stream_; + QString statusText_; +}; + +#endif // MESSENGER_CLIENT_NETWORK_MANAGER_H diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt index ba9761d..d85c986 100644 --- a/src/server/CMakeLists.txt +++ b/src/server/CMakeLists.txt @@ -1,5 +1,17 @@ -find_package(Qt6 REQUIRED COMPONENTS Network) +find_package(Qt6 REQUIRED COMPONENTS Core Network) -add_executable(Messenger-Server server.cpp) +set(CMAKE_AUTOMOC ON) -target_link_libraries(Messenger-Server PRIVATE Qt6::Network) +add_executable(Messenger-Server + client_session.cpp + client_session.h + server.cpp + server.h +) + +target_link_libraries(Messenger-Server + PRIVATE + Qt6::Core + Qt6::Network + Messenger-Shared +) diff --git a/src/server/client_session.cpp b/src/server/client_session.cpp new file mode 100644 index 0000000..9c77669 --- /dev/null +++ b/src/server/client_session.cpp @@ -0,0 +1,70 @@ +#include "client_session.h" + +#include +#include +#include +#include + +using messenger::protocol::CurrentProtocolVersion; +using messenger::protocol::DataStreamVersion; +using messenger::protocol::MessageEnvelope; +using messenger::protocol::ReadBufferSize; + +ClientSession::ClientSession(QTcpSocket* socket, QObject* parent) + : QObject(parent), socket_(socket), stream_(socket) { + socket_->setParent(this); + socket_->setReadBufferSize(ReadBufferSize); + stream_.setVersion(DataStreamVersion); + + connect(socket_, &QTcpSocket::readyRead, this, &ClientSession::readAvailable); + connect(socket_, &QTcpSocket::disconnected, this, &ClientSession::handleDisconnected); + connect(socket_, &QTcpSocket::errorOccurred, this, &ClientSession::handleError); + + qInfo() << "Client connected from" << socket_->peerAddress().toString() << socket_->peerPort(); +} + +void ClientSession::sendMessage(const MessageEnvelope& message) { + if (socket_->state() != QAbstractSocket::ConnectedState) { + return; + } + + QDataStream out(socket_); + out.setVersion(DataStreamVersion); + out << message; + socket_->flush(); +} + +void ClientSession::readAvailable() { + while (socket_->bytesAvailable() > 0) { + stream_.startTransaction(); + + MessageEnvelope message; + stream_ >> message; + + if (!stream_.commitTransaction()) { + return; + } + + if (message.protocolVersion != CurrentProtocolVersion) { + qWarning() << "Unsupported protocol version" << message.protocolVersion + << "from" << socket_->peerAddress().toString(); + socket_->disconnectFromHost(); + return; + } + + emit messageReceived(message, this); + } +} + +void ClientSession::handleDisconnected() { + qInfo() << "Client disconnected from" << socket_->peerAddress().toString() << socket_->peerPort(); + emit disconnected(this); +} + +void ClientSession::handleError() { + if (socket_->error() == QAbstractSocket::RemoteHostClosedError) { + return; + } + + qWarning() << "Client socket error:" << socket_->errorString(); +} diff --git a/src/server/client_session.h b/src/server/client_session.h new file mode 100644 index 0000000..d4217c5 --- /dev/null +++ b/src/server/client_session.h @@ -0,0 +1,33 @@ +#ifndef MESSENGER_CLIENT_SESSION_H +#define MESSENGER_CLIENT_SESSION_H + +#include "message_envelope.h" + +#include +#include + +class QTcpSocket; + +class ClientSession final : public QObject { + Q_OBJECT + + public: + explicit ClientSession(QTcpSocket* socket, QObject* parent = nullptr); + + void sendMessage(const messenger::protocol::MessageEnvelope& message); + + signals: + void messageReceived(const messenger::protocol::MessageEnvelope& message, ClientSession* session); + void disconnected(ClientSession* session); + + private slots: + void readAvailable(); + void handleDisconnected(); + void handleError(); + + private: + QTcpSocket* socket_; + QDataStream stream_; +}; + +#endif // MESSENGER_CLIENT_SESSION_H diff --git a/src/server/server.cpp b/src/server/server.cpp index 7a1b330..4ed8178 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -1,19 +1,91 @@ #include "server.h" -#include +#include "client_session.h" + +#include +#include +#include +#include +#include + +#include + +using messenger::protocol::CurrentProtocolVersion; +using messenger::protocol::DefaultPort; +using messenger::protocol::MessageEnvelope; +using messenger::protocol::MessageType; server& server::getInstance() { static server instance; return instance; } -server::server() { std::cout << "Starting Messenger Server ...\n"; } +server::server() { + connect(&tcpServer_, &QTcpServer::newConnection, this, &server::handleNewConnection); + connect(&tcpServer_, &QTcpServer::acceptError, this, &server::handleAcceptError); +} + +server::~server() { + tcpServer_.close(); + qDeleteAll(sessions_); + sessions_.clear(); +} + +bool server::listen(const QHostAddress& address, quint16 port) { + if (tcpServer_.listen(address, port)) { + qInfo() << "Messenger server listening on" << tcpServer_.serverAddress().toString() + << tcpServer_.serverPort(); + return true; + } + + qCritical() << "Could not start Messenger server:" << tcpServer_.errorString(); + return false; +} + +void server::handleNewConnection() { + while (tcpServer_.hasPendingConnections()) { + auto* socket = tcpServer_.nextPendingConnection(); + auto* session = new ClientSession(socket, this); + sessions_.insert(session); + + connect(session, &ClientSession::messageReceived, this, &server::handleMessageReceived); + connect(session, &ClientSession::disconnected, this, &server::handleSessionDisconnected); + } +} + +void server::handleAcceptError(QAbstractSocket::SocketError socketError) { + qWarning() << "Server accept error:" << socketError << tcpServer_.errorString(); +} + +void server::handleMessageReceived(const MessageEnvelope& message, ClientSession* session) { + if (message.protocolVersion != CurrentProtocolVersion) { + qWarning() << "Ignoring message with unsupported protocol version" << message.protocolVersion; + return; + } + + qInfo() << "Message from" << message.sender << "to" << message.recipient << ":" << message.text; + + if (message.messageType == static_cast(MessageType::ChatMessage)) { + for (auto* connectedSession : std::as_const(sessions_)) { + connectedSession->sendMessage(message); + } + } + + Q_UNUSED(session) +} + +void server::handleSessionDisconnected(ClientSession* session) { + sessions_.remove(session); + session->deleteLater(); +} -server::~server() { std::cout << "Stopping Messenger Server ...\n"; } +int main(int argc, char* argv[]) { + QCoreApplication app(argc, argv); -int main() { auto& serverInstance = server::getInstance(); - (void)serverInstance; + if (!serverInstance.listen(QHostAddress::Any, DefaultPort)) { + return 1; + } - return 0; + return app.exec(); } diff --git a/src/server/server.h b/src/server/server.h index 2825dfe..91920c9 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -1,18 +1,41 @@ #ifndef MESSENGER_SERVER_H #define MESSENGER_SERVER_H -class server { +#include "message_envelope.h" + +#include +#include +#include +#include + +class ClientSession; +class QHostAddress; + +class server final : public QObject { + Q_OBJECT + public: static server& getInstance(); - ~server(); + ~server() override; + + bool listen(const QHostAddress& address, quint16 port); server(const server&) = delete; server& operator=(const server&) = delete; server(server&&) = delete; server& operator=(server&&) = delete; + private slots: + void handleNewConnection(); + void handleAcceptError(QAbstractSocket::SocketError socketError); + void handleMessageReceived(const messenger::protocol::MessageEnvelope& message, ClientSession* session); + void handleSessionDisconnected(ClientSession* session); + private: server(); + + QTcpServer tcpServer_; + QSet sessions_; }; #endif // MESSENGER_SERVER_H diff --git a/src/shared/CMakeLists.txt b/src/shared/CMakeLists.txt index e69de29..a0b6127 100644 --- a/src/shared/CMakeLists.txt +++ b/src/shared/CMakeLists.txt @@ -0,0 +1,16 @@ +find_package(Qt6 REQUIRED COMPONENTS Core) + +add_library(Messenger-Shared STATIC + message_envelope.cpp + message_envelope.h +) + +target_link_libraries(Messenger-Shared + PUBLIC + Qt6::Core +) + +target_include_directories(Messenger-Shared + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) diff --git a/src/shared/message_envelope.cpp b/src/shared/message_envelope.cpp new file mode 100644 index 0000000..953eca1 --- /dev/null +++ b/src/shared/message_envelope.cpp @@ -0,0 +1,27 @@ +#include "message_envelope.h" + +namespace messenger::protocol { + +QDataStream& operator<<(QDataStream& out, const MessageEnvelope& message) { + out << message.protocolVersion; + out << message.messageType; + out << message.sender; + out << message.recipient; + out << message.text; + out << message.timestamp; + + return out; +} + +QDataStream& operator>>(QDataStream& in, MessageEnvelope& message) { + in >> message.protocolVersion; + in >> message.messageType; + in >> message.sender; + in >> message.recipient; + in >> message.text; + in >> message.timestamp; + + return in; +} + +} // namespace messenger::protocol diff --git a/src/shared/message_envelope.h b/src/shared/message_envelope.h new file mode 100644 index 0000000..14faed1 --- /dev/null +++ b/src/shared/message_envelope.h @@ -0,0 +1,36 @@ +#ifndef MESSENGER_MESSAGE_ENVELOPE_H +#define MESSENGER_MESSAGE_ENVELOPE_H + +#include +#include +#include +#include + +namespace messenger::protocol { + +inline constexpr quint32 CurrentProtocolVersion = 1; +inline constexpr quint16 DefaultPort = 4242; +inline constexpr qint64 ReadBufferSize = 1024 * 1024; +inline constexpr auto DataStreamVersion = QDataStream::Qt_6_5; + +enum class MessageType : quint32 { + ChatMessage = 1, + SystemMessage = 2, + ErrorMessage = 3, +}; + +struct MessageEnvelope { + quint32 protocolVersion = CurrentProtocolVersion; + quint32 messageType = static_cast(MessageType::ChatMessage); + QString sender; + QString recipient; + QString text; + QDateTime timestamp = QDateTime::currentDateTimeUtc(); +}; + +QDataStream& operator<<(QDataStream& out, const MessageEnvelope& message); +QDataStream& operator>>(QDataStream& in, MessageEnvelope& message); + +} // namespace messenger::protocol + +#endif // MESSENGER_MESSAGE_ENVELOPE_H From f7e14607be9ee48d0bab773f8189fefd7b717c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sun, 5 Jul 2026 21:32:48 +0200 Subject: [PATCH 02/41] Rename network files --- src/client/CMakeLists.txt | 4 +- src/client/client.cpp | 4 +- src/client/client.h | 4 +- src/client/client_network_manager.cpp | 145 -------------------------- src/client/client_network_manager.h | 48 --------- src/server/CMakeLists.txt | 4 +- src/server/client_session.cpp | 70 ------------- src/server/client_session.h | 33 ------ src/server/server.cpp | 14 +-- src/server/server.h | 10 +- src/shared/CMakeLists.txt | 4 +- src/shared/message_envelope.cpp | 27 ----- src/shared/message_envelope.h | 36 ------- 13 files changed, 21 insertions(+), 382 deletions(-) delete mode 100644 src/client/client_network_manager.cpp delete mode 100644 src/client/client_network_manager.h delete mode 100644 src/server/client_session.cpp delete mode 100644 src/server/client_session.h delete mode 100644 src/shared/message_envelope.cpp delete mode 100644 src/shared/message_envelope.h diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index b534054..55f2460 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -5,10 +5,8 @@ qt_standard_project_setup(REQUIRES 6.5) qt_add_executable(Messenger-Client WIN32 MACOSX_BUNDLE - client_network_manager.cpp - client_network_manager.h + network_manager.cpp client.cpp - client.h ) set_target_properties(Messenger-Client PROPERTIES diff --git a/src/client/client.cpp b/src/client/client.cpp index 400cfdb..2d541c9 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -1,6 +1,6 @@ #include "client.h" -#include "client_network_manager.h" +#include "network_manager.h" #include #include @@ -16,7 +16,7 @@ client::client() = default; client::~client() = default; int client::run(QGuiApplication& app) { - networkManager_ = std::make_unique(); + networkManager_ = std::make_unique(); engine_ = std::make_unique(); engine_->rootContext()->setContextProperty("networkManager", networkManager_.get()); engine_->loadFromModule("Messenger.Client", "Main"); diff --git a/src/client/client.h b/src/client/client.h index 80b1fd2..23bfdf1 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -3,7 +3,7 @@ #include -class ClientNetworkManager; +class NetworkManager; class QGuiApplication; class QQmlApplicationEngine; @@ -23,7 +23,7 @@ class client { client(); std::unique_ptr engine_; - std::unique_ptr networkManager_; + std::unique_ptr networkManager_; }; #endif // MESSENGER_CLIENT_H diff --git a/src/client/client_network_manager.cpp b/src/client/client_network_manager.cpp deleted file mode 100644 index e9cbb18..0000000 --- a/src/client/client_network_manager.cpp +++ /dev/null @@ -1,145 +0,0 @@ -#include "client_network_manager.h" - -#include -#include - -using messenger::protocol::CurrentProtocolVersion; -using messenger::protocol::DataStreamVersion; -using messenger::protocol::DefaultPort; -using messenger::protocol::MessageEnvelope; -using messenger::protocol::MessageType; -using messenger::protocol::ReadBufferSize; - -ClientNetworkManager::ClientNetworkManager(QObject* parent) - : QObject(parent), stream_(&socket_), statusText_(tr("Disconnected")) { - socket_.setReadBufferSize(ReadBufferSize); - stream_.setVersion(DataStreamVersion); - - connect(&socket_, &QTcpSocket::connected, this, &ClientNetworkManager::handleConnected); - connect(&socket_, &QTcpSocket::disconnected, this, &ClientNetworkManager::handleDisconnected); - connect(&socket_, &QTcpSocket::errorOccurred, this, &ClientNetworkManager::handleError); - connect(&socket_, &QTcpSocket::readyRead, this, &ClientNetworkManager::readAvailable); -} - -bool ClientNetworkManager::connected() const { - return socket_.state() == QAbstractSocket::ConnectedState; -} - -QString ClientNetworkManager::statusText() const { - return statusText_; -} - -int ClientNetworkManager::defaultPort() const { - return DefaultPort; -} - -void ClientNetworkManager::connectToServer(const QString& host, quint16 port) { - const auto trimmedHost = host.trimmed(); - if (trimmedHost.isEmpty()) { - const auto message = tr("Host must not be empty."); - setStatusText(message); - emit connectionError(message); - return; - } - - const auto previousConnected = connected(); - if (socket_.state() != QAbstractSocket::UnconnectedState) { - socket_.abort(); - } - - setStatusText(tr("Connecting to %1:%2 ...").arg(trimmedHost).arg(port)); - emitConnectedIfChanged(previousConnected); - socket_.connectToHost(trimmedHost, port); -} - -void ClientNetworkManager::disconnectFromServer() { - const auto previousConnected = connected(); - - if (socket_.state() == QAbstractSocket::UnconnectedState) { - setStatusText(tr("Disconnected")); - emitConnectedIfChanged(previousConnected); - return; - } - - setStatusText(tr("Disconnecting ...")); - socket_.disconnectFromHost(); -} - -void ClientNetworkManager::sendChatMessage(const QString& sender, const QString& recipient, const QString& text) { - if (!connected()) { - const auto message = tr("Not connected to a server."); - setStatusText(message); - emit connectionError(message); - return; - } - - MessageEnvelope message; - message.messageType = static_cast(MessageType::ChatMessage); - message.sender = sender; - message.recipient = recipient; - message.text = text; - message.timestamp = QDateTime::currentDateTimeUtc(); - - QDataStream out(&socket_); - out.setVersion(DataStreamVersion); - out << message; - socket_.flush(); -} - -void ClientNetworkManager::handleConnected() { - setStatusText(tr("Connected")); - emit connectedChanged(); -} - -void ClientNetworkManager::handleDisconnected() { - setStatusText(tr("Disconnected")); - emit connectedChanged(); -} - -void ClientNetworkManager::handleError() { - if (socket_.error() == QAbstractSocket::RemoteHostClosedError) { - return; - } - - const auto message = socket_.errorString(); - setStatusText(message); - emit connectionError(message); -} - -void ClientNetworkManager::readAvailable() { - while (socket_.bytesAvailable() > 0) { - stream_.startTransaction(); - - MessageEnvelope message; - stream_ >> message; - - if (!stream_.commitTransaction()) { - return; - } - - if (message.protocolVersion != CurrentProtocolVersion) { - const auto errorMessage = tr("Unsupported protocol version: %1").arg(message.protocolVersion); - setStatusText(errorMessage); - emit connectionError(errorMessage); - socket_.disconnectFromHost(); - return; - } - - emit messageReceived(message.sender, message.recipient, message.text); - } -} - -void ClientNetworkManager::setStatusText(const QString& statusText) { - if (statusText_ == statusText) { - return; - } - - statusText_ = statusText; - emit statusTextChanged(); -} - -void ClientNetworkManager::emitConnectedIfChanged(bool previousConnected) { - if (previousConnected != connected()) { - emit connectedChanged(); - } -} diff --git a/src/client/client_network_manager.h b/src/client/client_network_manager.h deleted file mode 100644 index 0a2e9ab..0000000 --- a/src/client/client_network_manager.h +++ /dev/null @@ -1,48 +0,0 @@ -#ifndef MESSENGER_CLIENT_NETWORK_MANAGER_H -#define MESSENGER_CLIENT_NETWORK_MANAGER_H - -#include "message_envelope.h" - -#include -#include -#include - -class ClientNetworkManager final : public QObject { - Q_OBJECT - Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged) - Q_PROPERTY(QString statusText READ statusText NOTIFY statusTextChanged) - Q_PROPERTY(int defaultPort READ defaultPort CONSTANT) - - public: - explicit ClientNetworkManager(QObject* parent = nullptr); - - [[nodiscard]] bool connected() const; - [[nodiscard]] QString statusText() const; - [[nodiscard]] int defaultPort() const; - - Q_INVOKABLE void connectToServer(const QString& host, quint16 port); - Q_INVOKABLE void disconnectFromServer(); - Q_INVOKABLE void sendChatMessage(const QString& sender, const QString& recipient, const QString& text); - - signals: - void connectedChanged(); - void statusTextChanged(); - void messageReceived(const QString& sender, const QString& recipient, const QString& text); - void connectionError(const QString& message); - - private slots: - void handleConnected(); - void handleDisconnected(); - void handleError(); - void readAvailable(); - - private: - void setStatusText(const QString& statusText); - void emitConnectedIfChanged(bool previousConnected); - - QTcpSocket socket_; - QDataStream stream_; - QString statusText_; -}; - -#endif // MESSENGER_CLIENT_NETWORK_MANAGER_H diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt index d85c986..4ea7af9 100644 --- a/src/server/CMakeLists.txt +++ b/src/server/CMakeLists.txt @@ -3,8 +3,8 @@ find_package(Qt6 REQUIRED COMPONENTS Core Network) set(CMAKE_AUTOMOC ON) add_executable(Messenger-Server - client_session.cpp - client_session.h + session.cpp + session.h server.cpp server.h ) diff --git a/src/server/client_session.cpp b/src/server/client_session.cpp deleted file mode 100644 index 9c77669..0000000 --- a/src/server/client_session.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include "client_session.h" - -#include -#include -#include -#include - -using messenger::protocol::CurrentProtocolVersion; -using messenger::protocol::DataStreamVersion; -using messenger::protocol::MessageEnvelope; -using messenger::protocol::ReadBufferSize; - -ClientSession::ClientSession(QTcpSocket* socket, QObject* parent) - : QObject(parent), socket_(socket), stream_(socket) { - socket_->setParent(this); - socket_->setReadBufferSize(ReadBufferSize); - stream_.setVersion(DataStreamVersion); - - connect(socket_, &QTcpSocket::readyRead, this, &ClientSession::readAvailable); - connect(socket_, &QTcpSocket::disconnected, this, &ClientSession::handleDisconnected); - connect(socket_, &QTcpSocket::errorOccurred, this, &ClientSession::handleError); - - qInfo() << "Client connected from" << socket_->peerAddress().toString() << socket_->peerPort(); -} - -void ClientSession::sendMessage(const MessageEnvelope& message) { - if (socket_->state() != QAbstractSocket::ConnectedState) { - return; - } - - QDataStream out(socket_); - out.setVersion(DataStreamVersion); - out << message; - socket_->flush(); -} - -void ClientSession::readAvailable() { - while (socket_->bytesAvailable() > 0) { - stream_.startTransaction(); - - MessageEnvelope message; - stream_ >> message; - - if (!stream_.commitTransaction()) { - return; - } - - if (message.protocolVersion != CurrentProtocolVersion) { - qWarning() << "Unsupported protocol version" << message.protocolVersion - << "from" << socket_->peerAddress().toString(); - socket_->disconnectFromHost(); - return; - } - - emit messageReceived(message, this); - } -} - -void ClientSession::handleDisconnected() { - qInfo() << "Client disconnected from" << socket_->peerAddress().toString() << socket_->peerPort(); - emit disconnected(this); -} - -void ClientSession::handleError() { - if (socket_->error() == QAbstractSocket::RemoteHostClosedError) { - return; - } - - qWarning() << "Client socket error:" << socket_->errorString(); -} diff --git a/src/server/client_session.h b/src/server/client_session.h deleted file mode 100644 index d4217c5..0000000 --- a/src/server/client_session.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef MESSENGER_CLIENT_SESSION_H -#define MESSENGER_CLIENT_SESSION_H - -#include "message_envelope.h" - -#include -#include - -class QTcpSocket; - -class ClientSession final : public QObject { - Q_OBJECT - - public: - explicit ClientSession(QTcpSocket* socket, QObject* parent = nullptr); - - void sendMessage(const messenger::protocol::MessageEnvelope& message); - - signals: - void messageReceived(const messenger::protocol::MessageEnvelope& message, ClientSession* session); - void disconnected(ClientSession* session); - - private slots: - void readAvailable(); - void handleDisconnected(); - void handleError(); - - private: - QTcpSocket* socket_; - QDataStream stream_; -}; - -#endif // MESSENGER_CLIENT_SESSION_H diff --git a/src/server/server.cpp b/src/server/server.cpp index 4ed8178..e85a696 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -1,6 +1,6 @@ #include "server.h" -#include "client_session.h" +#include "session.h" #include #include @@ -12,7 +12,7 @@ using messenger::protocol::CurrentProtocolVersion; using messenger::protocol::DefaultPort; -using messenger::protocol::MessageEnvelope; +using messenger::protocol::Message; using messenger::protocol::MessageType; server& server::getInstance() { @@ -45,11 +45,11 @@ bool server::listen(const QHostAddress& address, quint16 port) { void server::handleNewConnection() { while (tcpServer_.hasPendingConnections()) { auto* socket = tcpServer_.nextPendingConnection(); - auto* session = new ClientSession(socket, this); + auto* session = new Session(socket, this); sessions_.insert(session); - connect(session, &ClientSession::messageReceived, this, &server::handleMessageReceived); - connect(session, &ClientSession::disconnected, this, &server::handleSessionDisconnected); + connect(session, &Session::messageReceived, this, &server::handleMessageReceived); + connect(session, &Session::disconnected, this, &server::handleSessionDisconnected); } } @@ -57,7 +57,7 @@ void server::handleAcceptError(QAbstractSocket::SocketError socketError) { qWarning() << "Server accept error:" << socketError << tcpServer_.errorString(); } -void server::handleMessageReceived(const MessageEnvelope& message, ClientSession* session) { +void server::handleMessageReceived(const Message& message, Session* session) { if (message.protocolVersion != CurrentProtocolVersion) { qWarning() << "Ignoring message with unsupported protocol version" << message.protocolVersion; return; @@ -74,7 +74,7 @@ void server::handleMessageReceived(const MessageEnvelope& message, ClientSession Q_UNUSED(session) } -void server::handleSessionDisconnected(ClientSession* session) { +void server::handleSessionDisconnected(Session* session) { sessions_.remove(session); session->deleteLater(); } diff --git a/src/server/server.h b/src/server/server.h index 91920c9..f2989a9 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -1,14 +1,14 @@ #ifndef MESSENGER_SERVER_H #define MESSENGER_SERVER_H -#include "message_envelope.h" +#include "message.h" #include #include #include #include -class ClientSession; +class Session; class QHostAddress; class server final : public QObject { @@ -28,14 +28,14 @@ class server final : public QObject { private slots: void handleNewConnection(); void handleAcceptError(QAbstractSocket::SocketError socketError); - void handleMessageReceived(const messenger::protocol::MessageEnvelope& message, ClientSession* session); - void handleSessionDisconnected(ClientSession* session); + void handleMessageReceived(const messenger::protocol::Message& message, Session* session); + void handleSessionDisconnected(Session* session); private: server(); QTcpServer tcpServer_; - QSet sessions_; + QSet sessions_; }; #endif // MESSENGER_SERVER_H diff --git a/src/shared/CMakeLists.txt b/src/shared/CMakeLists.txt index a0b6127..0d6dcb0 100644 --- a/src/shared/CMakeLists.txt +++ b/src/shared/CMakeLists.txt @@ -1,8 +1,8 @@ find_package(Qt6 REQUIRED COMPONENTS Core) add_library(Messenger-Shared STATIC - message_envelope.cpp - message_envelope.h + message.cpp + message.h ) target_link_libraries(Messenger-Shared diff --git a/src/shared/message_envelope.cpp b/src/shared/message_envelope.cpp deleted file mode 100644 index 953eca1..0000000 --- a/src/shared/message_envelope.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "message_envelope.h" - -namespace messenger::protocol { - -QDataStream& operator<<(QDataStream& out, const MessageEnvelope& message) { - out << message.protocolVersion; - out << message.messageType; - out << message.sender; - out << message.recipient; - out << message.text; - out << message.timestamp; - - return out; -} - -QDataStream& operator>>(QDataStream& in, MessageEnvelope& message) { - in >> message.protocolVersion; - in >> message.messageType; - in >> message.sender; - in >> message.recipient; - in >> message.text; - in >> message.timestamp; - - return in; -} - -} // namespace messenger::protocol diff --git a/src/shared/message_envelope.h b/src/shared/message_envelope.h deleted file mode 100644 index 14faed1..0000000 --- a/src/shared/message_envelope.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef MESSENGER_MESSAGE_ENVELOPE_H -#define MESSENGER_MESSAGE_ENVELOPE_H - -#include -#include -#include -#include - -namespace messenger::protocol { - -inline constexpr quint32 CurrentProtocolVersion = 1; -inline constexpr quint16 DefaultPort = 4242; -inline constexpr qint64 ReadBufferSize = 1024 * 1024; -inline constexpr auto DataStreamVersion = QDataStream::Qt_6_5; - -enum class MessageType : quint32 { - ChatMessage = 1, - SystemMessage = 2, - ErrorMessage = 3, -}; - -struct MessageEnvelope { - quint32 protocolVersion = CurrentProtocolVersion; - quint32 messageType = static_cast(MessageType::ChatMessage); - QString sender; - QString recipient; - QString text; - QDateTime timestamp = QDateTime::currentDateTimeUtc(); -}; - -QDataStream& operator<<(QDataStream& out, const MessageEnvelope& message); -QDataStream& operator>>(QDataStream& in, MessageEnvelope& message); - -} // namespace messenger::protocol - -#endif // MESSENGER_MESSAGE_ENVELOPE_H From 1a64d60dfdba7b0e8dd49019c31449d37cac1253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sun, 5 Jul 2026 21:33:08 +0200 Subject: [PATCH 03/41] Add renamed files --- src/client/network_manager.cpp | 145 +++++++++++++++++++++++++++++++++ src/client/network_manager.h | 48 +++++++++++ src/server/session.cpp | 70 ++++++++++++++++ src/server/session.h | 33 ++++++++ src/shared/message.cpp | 27 ++++++ src/shared/message.h | 36 ++++++++ 6 files changed, 359 insertions(+) create mode 100644 src/client/network_manager.cpp create mode 100644 src/client/network_manager.h create mode 100644 src/server/session.cpp create mode 100644 src/server/session.h create mode 100644 src/shared/message.cpp create mode 100644 src/shared/message.h diff --git a/src/client/network_manager.cpp b/src/client/network_manager.cpp new file mode 100644 index 0000000..4e680de --- /dev/null +++ b/src/client/network_manager.cpp @@ -0,0 +1,145 @@ +#include "network_manager.h" + +#include +#include + +using messenger::protocol::CurrentProtocolVersion; +using messenger::protocol::DataStreamVersion; +using messenger::protocol::DefaultPort; +using messenger::protocol::Message; +using messenger::protocol::MessageType; +using messenger::protocol::ReadBufferSize; + +NetworkManager::NetworkManager(QObject* parent) + : QObject(parent), stream_(&socket_), statusText_(tr("Disconnected")) { + socket_.setReadBufferSize(ReadBufferSize); + stream_.setVersion(DataStreamVersion); + + connect(&socket_, &QTcpSocket::connected, this, &NetworkManager::handleConnected); + connect(&socket_, &QTcpSocket::disconnected, this, &NetworkManager::handleDisconnected); + connect(&socket_, &QTcpSocket::errorOccurred, this, &NetworkManager::handleError); + connect(&socket_, &QTcpSocket::readyRead, this, &NetworkManager::readAvailable); +} + +bool NetworkManager::connected() const { + return socket_.state() == QAbstractSocket::ConnectedState; +} + +QString NetworkManager::statusText() const { + return statusText_; +} + +int NetworkManager::defaultPort() const { + return DefaultPort; +} + +void NetworkManager::connectToServer(const QString& host, quint16 port) { + const auto trimmedHost = host.trimmed(); + if (trimmedHost.isEmpty()) { + const auto message = tr("Host must not be empty."); + setStatusText(message); + emit connectionError(message); + return; + } + + const auto previousConnected = connected(); + if (socket_.state() != QAbstractSocket::UnconnectedState) { + socket_.abort(); + } + + setStatusText(tr("Connecting to %1:%2 ...").arg(trimmedHost).arg(port)); + emitConnectedIfChanged(previousConnected); + socket_.connectToHost(trimmedHost, port); +} + +void NetworkManager::disconnectFromServer() { + const auto previousConnected = connected(); + + if (socket_.state() == QAbstractSocket::UnconnectedState) { + setStatusText(tr("Disconnected")); + emitConnectedIfChanged(previousConnected); + return; + } + + setStatusText(tr("Disconnecting ...")); + socket_.disconnectFromHost(); +} + +void NetworkManager::sendChatMessage(const QString& sender, const QString& recipient, const QString& text) { + if (!connected()) { + const auto message = tr("Not connected to a server."); + setStatusText(message); + emit connectionError(message); + return; + } + + Message message; + message.messageType = static_cast(MessageType::ChatMessage); + message.sender = sender; + message.recipient = recipient; + message.text = text; + message.timestamp = QDateTime::currentDateTimeUtc(); + + QDataStream out(&socket_); + out.setVersion(DataStreamVersion); + out << message; + socket_.flush(); +} + +void NetworkManager::handleConnected() { + setStatusText(tr("Connected")); + emit connectedChanged(); +} + +void NetworkManager::handleDisconnected() { + setStatusText(tr("Disconnected")); + emit connectedChanged(); +} + +void NetworkManager::handleError() { + if (socket_.error() == QAbstractSocket::RemoteHostClosedError) { + return; + } + + const auto message = socket_.errorString(); + setStatusText(message); + emit connectionError(message); +} + +void NetworkManager::readAvailable() { + while (socket_.bytesAvailable() > 0) { + stream_.startTransaction(); + + Message message; + stream_ >> message; + + if (!stream_.commitTransaction()) { + return; + } + + if (message.protocolVersion != CurrentProtocolVersion) { + const auto errorMessage = tr("Unsupported protocol version: %1").arg(message.protocolVersion); + setStatusText(errorMessage); + emit connectionError(errorMessage); + socket_.disconnectFromHost(); + return; + } + + emit messageReceived(message.sender, message.recipient, message.text); + } +} + +void NetworkManager::setStatusText(const QString& statusText) { + if (statusText_ == statusText) { + return; + } + + statusText_ = statusText; + emit statusTextChanged(); +} + +void NetworkManager::emitConnectedIfChanged(bool previousConnected) { + if (previousConnected != connected()) { + emit connectedChanged(); + } +} diff --git a/src/client/network_manager.h b/src/client/network_manager.h new file mode 100644 index 0000000..2178d5e --- /dev/null +++ b/src/client/network_manager.h @@ -0,0 +1,48 @@ +#ifndef MESSENGER_NETWORK_MANAGER_H +#define MESSENGER_NETWORK_MANAGER_H + +#include "message.h" + +#include +#include +#include + +class NetworkManager final : public QObject { + Q_OBJECT + Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged) + Q_PROPERTY(QString statusText READ statusText NOTIFY statusTextChanged) + Q_PROPERTY(int defaultPort READ defaultPort CONSTANT) + + public: + explicit NetworkManager(QObject* parent = nullptr); + + [[nodiscard]] bool connected() const; + [[nodiscard]] QString statusText() const; + [[nodiscard]] int defaultPort() const; + + Q_INVOKABLE void connectToServer(const QString& host, quint16 port); + Q_INVOKABLE void disconnectFromServer(); + Q_INVOKABLE void sendChatMessage(const QString& sender, const QString& recipient, const QString& text); + + signals: + void connectedChanged(); + void statusTextChanged(); + void messageReceived(const QString& sender, const QString& recipient, const QString& text); + void connectionError(const QString& message); + + private slots: + void handleConnected(); + void handleDisconnected(); + void handleError(); + void readAvailable(); + + private: + void setStatusText(const QString& statusText); + void emitConnectedIfChanged(bool previousConnected); + + QTcpSocket socket_; + QDataStream stream_; + QString statusText_; +}; + +#endif // MESSENGER_NETWORK_MANAGER_H diff --git a/src/server/session.cpp b/src/server/session.cpp new file mode 100644 index 0000000..30aa8b1 --- /dev/null +++ b/src/server/session.cpp @@ -0,0 +1,70 @@ +#include "session.h" + +#include +#include +#include +#include + +using messenger::protocol::CurrentProtocolVersion; +using messenger::protocol::DataStreamVersion; +using messenger::protocol::Message; +using messenger::protocol::ReadBufferSize; + +Session::Session(QTcpSocket* socket, QObject* parent) + : QObject(parent), socket_(socket), stream_(socket) { + socket_->setParent(this); + socket_->setReadBufferSize(ReadBufferSize); + stream_.setVersion(DataStreamVersion); + + connect(socket_, &QTcpSocket::readyRead, this, &Session::readAvailable); + connect(socket_, &QTcpSocket::disconnected, this, &Session::handleDisconnected); + connect(socket_, &QTcpSocket::errorOccurred, this, &Session::handleError); + + qInfo() << "Client connected from" << socket_->peerAddress().toString() << socket_->peerPort(); +} + +void Session::sendMessage(const Message& message) { + if (socket_->state() != QAbstractSocket::ConnectedState) { + return; + } + + QDataStream out(socket_); + out.setVersion(DataStreamVersion); + out << message; + socket_->flush(); +} + +void Session::readAvailable() { + while (socket_->bytesAvailable() > 0) { + stream_.startTransaction(); + + Message message; + stream_ >> message; + + if (!stream_.commitTransaction()) { + return; + } + + if (message.protocolVersion != CurrentProtocolVersion) { + qWarning() << "Unsupported protocol version" << message.protocolVersion + << "from" << socket_->peerAddress().toString(); + socket_->disconnectFromHost(); + return; + } + + emit messageReceived(message, this); + } +} + +void Session::handleDisconnected() { + qInfo() << "Client disconnected from" << socket_->peerAddress().toString() << socket_->peerPort(); + emit disconnected(this); +} + +void Session::handleError() { + if (socket_->error() == QAbstractSocket::RemoteHostClosedError) { + return; + } + + qWarning() << "Client socket error:" << socket_->errorString(); +} diff --git a/src/server/session.h b/src/server/session.h new file mode 100644 index 0000000..eb1a6cb --- /dev/null +++ b/src/server/session.h @@ -0,0 +1,33 @@ +#ifndef MESSENGER_SESSION_H +#define MESSENGER_SESSION_H + +#include "message.h" + +#include +#include + +class QTcpSocket; + +class Session final : public QObject { + Q_OBJECT + + public: + explicit Session(QTcpSocket* socket, QObject* parent = nullptr); + + void sendMessage(const messenger::protocol::Message& message); + + signals: + void messageReceived(const messenger::protocol::Message& message, Session* session); + void disconnected(Session* session); + + private slots: + void readAvailable(); + void handleDisconnected(); + void handleError(); + + private: + QTcpSocket* socket_; + QDataStream stream_; +}; + +#endif // MESSENGER_SESSION_H diff --git a/src/shared/message.cpp b/src/shared/message.cpp new file mode 100644 index 0000000..92b77ec --- /dev/null +++ b/src/shared/message.cpp @@ -0,0 +1,27 @@ +#include "message.h" + +namespace messenger::protocol { + +QDataStream& operator<<(QDataStream& out, const Message& message) { + out << message.protocolVersion; + out << message.messageType; + out << message.sender; + out << message.recipient; + out << message.text; + out << message.timestamp; + + return out; +} + +QDataStream& operator>>(QDataStream& in, Message& message) { + in >> message.protocolVersion; + in >> message.messageType; + in >> message.sender; + in >> message.recipient; + in >> message.text; + in >> message.timestamp; + + return in; +} + +} // namespace messenger::protocol diff --git a/src/shared/message.h b/src/shared/message.h new file mode 100644 index 0000000..4023ff6 --- /dev/null +++ b/src/shared/message.h @@ -0,0 +1,36 @@ +#ifndef MESSENGER_MESSAGE_H +#define MESSENGER_MESSAGE_H + +#include +#include +#include +#include + +namespace messenger::protocol { + +inline constexpr quint32 CurrentProtocolVersion = 1; +inline constexpr quint16 DefaultPort = 4242; +inline constexpr qint64 ReadBufferSize = 1024 * 1024; +inline constexpr auto DataStreamVersion = QDataStream::Qt_6_5; + +enum class MessageType : quint32 { + ChatMessage = 1, + SystemMessage = 2, + ErrorMessage = 3, +}; + +struct Message { + quint32 protocolVersion = CurrentProtocolVersion; + quint32 messageType = static_cast(MessageType::ChatMessage); + QString sender; + QString recipient; + QString text; + QDateTime timestamp = QDateTime::currentDateTimeUtc(); +}; + +QDataStream& operator<<(QDataStream& out, const Message& message); +QDataStream& operator>>(QDataStream& in, Message& message); + +} // namespace messenger::protocol + +#endif // MESSENGER_MESSAGE_H From e6fb87c488d070ead89f7e9eb5626fbcf69edd15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Tue, 7 Jul 2026 07:00:08 +0200 Subject: [PATCH 04/41] Define functionality in README --- README.md | 13 +++++++++++++ src/client/network_manager.h | 1 + 2 files changed, 14 insertions(+) diff --git a/README.md b/README.md index 03314ab..20aee06 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,19 @@ Messenger is a cross-platform client-server messenger application based on the RSA algorithm. The project is still in an early stage and currently provides the foundation for the client and server. +## Functionality + +The project currently implements the core flow for a TCP-based group chat: + +- **Client connection:** enter a server host, connect automatically to the server, disconnect again when terminating the program, and see the current connection status in the UI. +- **Group chat login:** each server represents one group chat; users join that group chat by signing in with a name and password. +- **Message sending:** compose text messages and send them to the connected server. +- **Message receiving:** display incoming messages in the client window as they arrive from the server. +- **Group chat forwarding:** the server keeps track of the clients connected to its group chat and broadcasts chat messages to every active session. +- **Chat history:** the server stores the group chat history and sends all previous messages to newly connected clients. +- **Protocol validation:** client and server exchange structured messages and reject unsupported protocol versions. +- **Error handling:** connection errors and invalid client-side input are surfaced through the status text and message log. + ## Architecture The repository contains two applications: `Messenger-Client` and `Messenger-Server`. The client is built with Qt/QML; the server is a terminal program. diff --git a/src/client/network_manager.h b/src/client/network_manager.h index 2178d5e..c076be4 100644 --- a/src/client/network_manager.h +++ b/src/client/network_manager.h @@ -25,6 +25,7 @@ class NetworkManager final : public QObject { Q_INVOKABLE void sendChatMessage(const QString& sender, const QString& recipient, const QString& text); signals: + // These methods are implemented in qt void connectedChanged(); void statusTextChanged(); void messageReceived(const QString& sender, const QString& recipient, const QString& text); From 2e70bb399be4640e2800d039f4310f7c6b65902a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Tue, 7 Jul 2026 18:43:11 +0200 Subject: [PATCH 05/41] Change authentication concept in README --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 20aee06..647ae9e 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,8 @@ Messenger is a cross-platform client-server messenger application based on the R ## Functionality -The project currently implements the core flow for a TCP-based group chat: - - **Client connection:** enter a server host, connect automatically to the server, disconnect again when terminating the program, and see the current connection status in the UI. -- **Group chat login:** each server represents one group chat; users join that group chat by signing in with a name and password. +- **Group chat authentication:** each server represents one group chat; user accounts are created on the server with a name and a public key, without passwords. Clients authenticate through RSA-based verification using the public key stored for their account. - **Message sending:** compose text messages and send them to the connected server. - **Message receiving:** display incoming messages in the client window as they arrive from the server. - **Group chat forwarding:** the server keeps track of the clients connected to its group chat and broadcasts chat messages to every active session. From c1a2f69d3c76532225615415044c1fb53b8e0d35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:30:33 +0200 Subject: [PATCH 06/41] Add connection UI page --- src/client/CMakeLists.txt | 3 +- src/client/Main.qml | 378 +++++++++++++++++++++++------------ src/client/network_manager.h | 1 + 3 files changed, 252 insertions(+), 130 deletions(-) diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index 55f2460..d0f08b8 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -1,4 +1,4 @@ -find_package(Qt6 COMPONENTS Quick Qml Network REQUIRED) +find_package(Qt6 COMPONENTS Quick Qml QuickControls2 Network REQUIRED) qt_standard_project_setup(REQUIRES 6.5) @@ -25,6 +25,7 @@ target_link_libraries(Messenger-Client PRIVATE Qt6::Quick Qt6::Qml + Qt6::QuickControls2 Qt6::Network Messenger-Shared ) diff --git a/src/client/Main.qml b/src/client/Main.qml index 45a196d..f9670e9 100644 --- a/src/client/Main.qml +++ b/src/client/Main.qml @@ -1,9 +1,12 @@ import QtQuick +import QtQuick.Controls import QtQml Window { width: 960 height: 640 + minimumWidth: 720 + minimumHeight: 520 visible: true title: qsTr("Messenger Client") @@ -11,210 +14,321 @@ Window { property string receivedMessages: "" - Column { - anchors { - fill: parent - margins: 32 + function connectWithInput() { + if (hostInput.text.trim().length === 0 || !portInput.acceptableInput) { + return } - spacing: 16 - Text { - text: qsTr("Messenger Client") + networkManager.connectToServer(hostInput.text, Number(portInput.text)) + } + + component FieldLabel: Text { + color: "#43515f" + font.pixelSize: 14 + } + + component TextInputBox: Rectangle { + id: inputBox + + property alias text: input.text + property alias validator: input.validator + property alias acceptableInput: input.acceptableInput + property bool passwordMode: false + signal accepted() + + height: 44 + radius: 6 + color: "#ffffff" + border.color: input.activeFocus ? "#205493" : "#c8d0d9" + + TextInput { + id: input + anchors.fill: parent + anchors.margins: 11 color: "#1f2933" - font.pixelSize: 36 + font.pixelSize: 15 + verticalAlignment: TextInput.AlignVCenter + selectByMouse: true + echoMode: passwordMode ? TextInput.Password : TextInput.Normal + onAccepted: inputBox.accepted() } + } + + component ActionButton: Rectangle { + property alias text: label.text + property bool enabledState: true + property color activeColor: "#205493" + signal clicked() + + height: 44 + radius: 6 + color: enabledState ? activeColor : "#d7dde5" Text { - text: networkManager.statusText - color: networkManager.connected ? "#127a3a" : "#7a2830" - font.pixelSize: 16 + id: label + anchors.centerIn: parent + color: enabledState ? "#ffffff" : "#43515f" + font.pixelSize: 15 } - Row { - spacing: 12 + MouseArea { + anchors.fill: parent + enabled: enabledState + cursorShape: enabledState ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: parent.clicked() + } + } - Rectangle { - width: 240 - height: 40 - radius: 6 - color: "#ffffff" - border.color: "#c8d0d9" + Item { + id: connectionPage + anchors.fill: parent + visible: !networkManager.connected + + Column { + anchors.centerIn: parent + width: Math.min(420, parent.width - 64) + spacing: 18 + + Text { + width: parent.width + text: qsTr("Messenger Client") + color: "#1f2933" + font.pixelSize: 36 + horizontalAlignment: Text.AlignHCenter + } + + Text { + width: parent.width + text: networkManager.statusText + color: "#7a2830" + font.pixelSize: 16 + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + } - TextInput { + Column { + width: parent.width + spacing: 8 + + FieldLabel { + text: qsTr("Server IP") + } + + TextInputBox { id: hostInput - anchors.fill: parent - anchors.margins: 10 + width: parent.width text: "127.0.0.1" - color: "#1f2933" - font.pixelSize: 15 - verticalAlignment: TextInput.AlignVCenter - selectByMouse: true + onAccepted: connectWithInput() } } - Rectangle { - width: 96 - height: 40 - radius: 6 - color: "#ffffff" - border.color: "#c8d0d9" + Column { + width: parent.width + spacing: 8 - TextInput { + FieldLabel { + text: qsTr("Port") + } + + TextInputBox { id: portInput - anchors.fill: parent - anchors.margins: 10 + width: parent.width text: String(networkManager.defaultPort) - color: "#1f2933" - font.pixelSize: 15 - verticalAlignment: TextInput.AlignVCenter - validator: IntValidator { bottom: 1; top: 65535 } - selectByMouse: true + onAccepted: connectWithInput() + validator: IntValidator { + bottom: 1 + top: 65535 + } } } - Rectangle { - width: 120 - height: 40 - radius: 6 - color: networkManager.connected ? "#d7dde5" : "#205493" + ActionButton { + width: parent.width + text: qsTr("Verbinden") + enabledState: hostInput.text.trim().length > 0 && portInput.acceptableInput + onClicked: connectWithInput() + } + } + } + + Column { + id: chatPage + anchors { + fill: parent + margins: 32 + } + spacing: 16 + visible: networkManager.connected + + Row { + width: parent.width + spacing: 12 + + Column { + width: Math.max(180, parent.width - disconnectButton.width - parent.spacing) + spacing: 4 Text { - anchors.centerIn: parent - text: networkManager.connected ? qsTr("Connected") : qsTr("Connect") - color: networkManager.connected ? "#43515f" : "#ffffff" - font.pixelSize: 15 + text: qsTr("Messenger Client") + color: "#1f2933" + font.pixelSize: 32 } - MouseArea { - anchors.fill: parent - enabled: !networkManager.connected - onClicked: networkManager.connectToServer(hostInput.text, Number(portInput.text)) + Text { + text: networkManager.statusText + color: "#127a3a" + font.pixelSize: 16 } } Rectangle { - width: 120 - height: 40 + id: disconnectButton + width: 44 + height: 44 radius: 6 - color: networkManager.connected ? "#8b2635" : "#d7dde5" + color: disconnectMouseArea.containsMouse ? "#c8d0d9" : "#d7dde5" + border.color: "#b7c0ca" - Text { + Canvas { anchors.centerIn: parent - text: qsTr("Disconnect") - color: networkManager.connected ? "#ffffff" : "#43515f" - font.pixelSize: 15 + width: 24 + height: 24 + + onPaint: { + const context = getContext("2d") + context.clearRect(0, 0, width, height) + context.strokeStyle = "#43515f" + context.lineWidth = 2 + context.lineCap = "round" + context.lineJoin = "round" + + context.beginPath() + context.moveTo(10, 5) + context.lineTo(5, 5) + context.lineTo(5, 19) + context.lineTo(10, 19) + context.stroke() + + context.beginPath() + context.moveTo(12, 12) + context.lineTo(21, 12) + context.stroke() + + context.beginPath() + context.moveTo(17, 8) + context.lineTo(21, 12) + context.lineTo(17, 16) + context.stroke() + } } MouseArea { + id: disconnectMouseArea anchors.fill: parent - enabled: networkManager.connected + hoverEnabled: true + cursorShape: Qt.PointingHandCursor onClicked: networkManager.disconnectFromServer() } } } Row { + width: parent.width spacing: 12 - Rectangle { - width: 180 - height: 40 - radius: 6 - color: "#ffffff" - border.color: "#c8d0d9" + Column { + width: Math.max(120, (parent.width - parent.spacing) / 2) + spacing: 8 + + FieldLabel { + text: qsTr("Absender") + } - TextInput { + TextInputBox { id: senderInput - anchors.fill: parent - anchors.margins: 10 + width: parent.width text: "client" - color: "#1f2933" - font.pixelSize: 15 - verticalAlignment: TextInput.AlignVCenter - selectByMouse: true } } - Rectangle { - width: 180 - height: 40 - radius: 6 - color: "#ffffff" - border.color: "#c8d0d9" + Column { + width: Math.max(120, (parent.width - parent.spacing) / 2) + spacing: 8 + + FieldLabel { + text: qsTr("Empfaenger") + } - TextInput { + TextInputBox { id: recipientInput - anchors.fill: parent - anchors.margins: 10 + width: parent.width text: "server" - color: "#1f2933" - font.pixelSize: 15 - verticalAlignment: TextInput.AlignVCenter - selectByMouse: true } } } Rectangle { width: parent.width - height: 44 + height: Math.max(120, parent.height - y - composeRow.height - parent.spacing) radius: 6 color: "#ffffff" border.color: "#c8d0d9" + clip: true - TextInput { - id: messageInput - anchors.fill: parent - anchors.margins: 10 - color: "#1f2933" - font.pixelSize: 15 - verticalAlignment: TextInput.AlignVCenter - selectByMouse: true + Flickable { + id: messageFlickable + anchors { + fill: parent + margins: 12 + } + contentWidth: width + contentHeight: Math.max(height, messageText.paintedHeight) + boundsBehavior: Flickable.StopAtBounds + clip: true + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + } + + Text { + id: messageText + width: messageFlickable.width + y: Math.max(0, messageFlickable.height - paintedHeight) + text: receivedMessages.length > 0 ? receivedMessages : qsTr("Keine Nachrichten empfangen.") + color: "#1f2933" + font.pixelSize: 15 + wrapMode: Text.Wrap + } } } - Rectangle { - width: 140 - height: 40 - radius: 6 - color: networkManager.connected ? "#205493" : "#d7dde5" + Row { + id: composeRow + width: parent.width + spacing: 12 - Text { - anchors.centerIn: parent - text: qsTr("Send") - color: networkManager.connected ? "#ffffff" : "#43515f" - font.pixelSize: 15 + TextInputBox { + id: messageInput + width: Math.max(120, composeRow.width - sendButton.width - composeRow.spacing) + onAccepted: { + if (networkManager.connected && messageInput.text.length > 0) { + networkManager.sendChatMessage(senderInput.text, recipientInput.text, messageInput.text) + messageInput.text = "" + } + } } - MouseArea { - anchors.fill: parent - enabled: networkManager.connected + ActionButton { + id: sendButton + width: 140 + text: qsTr("Senden") + enabledState: networkManager.connected && messageInput.text.length > 0 onClicked: { networkManager.sendChatMessage(senderInput.text, recipientInput.text, messageInput.text) messageInput.text = "" } } } - - Rectangle { - width: parent.width - height: Math.max(180, parent.height - y) - radius: 6 - color: "#ffffff" - border.color: "#c8d0d9" - clip: true - - Text { - anchors { - fill: parent - margins: 12 - } - text: receivedMessages.length > 0 ? receivedMessages : qsTr("No messages received.") - color: "#1f2933" - font.pixelSize: 15 - wrapMode: Text.Wrap - } - } } Connections { @@ -222,10 +336,16 @@ Window { function onMessageReceived(sender, recipient, text) { receivedMessages += sender + " -> " + recipient + ": " + text + "\n" + Qt.callLater(function() { + messageFlickable.contentY = Math.max(0, messageFlickable.contentHeight - messageFlickable.height) + }) } function onConnectionError(message) { receivedMessages += "Error: " + message + "\n" + Qt.callLater(function() { + messageFlickable.contentY = Math.max(0, messageFlickable.contentHeight - messageFlickable.height) + }) } } } diff --git a/src/client/network_manager.h b/src/client/network_manager.h index c076be4..22ddbb0 100644 --- a/src/client/network_manager.h +++ b/src/client/network_manager.h @@ -20,6 +20,7 @@ class NetworkManager final : public QObject { [[nodiscard]] QString statusText() const; [[nodiscard]] int defaultPort() const; + // Methode that can be called from QML Q_INVOKABLE void connectToServer(const QString& host, quint16 port); Q_INVOKABLE void disconnectFromServer(); Q_INVOKABLE void sendChatMessage(const QString& sender, const QString& recipient, const QString& text); From 7bcded5cb254811e8f6801944c9e95d50a7dd973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:42:31 +0200 Subject: [PATCH 07/41] Remove sender and receiver from protocol --- src/client/Main.qml | 49 +++++++--------------------------- src/client/network_manager.cpp | 6 ++--- src/client/network_manager.h | 4 +-- src/server/server.cpp | 2 +- src/shared/message.cpp | 4 --- src/shared/message.h | 2 -- 6 files changed, 15 insertions(+), 52 deletions(-) diff --git a/src/client/Main.qml b/src/client/Main.qml index f9670e9..4e9e953 100644 --- a/src/client/Main.qml +++ b/src/client/Main.qml @@ -232,41 +232,6 @@ Window { } } - Row { - width: parent.width - spacing: 12 - - Column { - width: Math.max(120, (parent.width - parent.spacing) / 2) - spacing: 8 - - FieldLabel { - text: qsTr("Absender") - } - - TextInputBox { - id: senderInput - width: parent.width - text: "client" - } - } - - Column { - width: Math.max(120, (parent.width - parent.spacing) / 2) - spacing: 8 - - FieldLabel { - text: qsTr("Empfaenger") - } - - TextInputBox { - id: recipientInput - width: parent.width - text: "server" - } - } - } - Rectangle { width: parent.width height: Math.max(120, parent.height - y - composeRow.height - parent.spacing) @@ -312,7 +277,7 @@ Window { width: Math.max(120, composeRow.width - sendButton.width - composeRow.spacing) onAccepted: { if (networkManager.connected && messageInput.text.length > 0) { - networkManager.sendChatMessage(senderInput.text, recipientInput.text, messageInput.text) + networkManager.sendChatMessage(messageInput.text) messageInput.text = "" } } @@ -324,7 +289,7 @@ Window { text: qsTr("Senden") enabledState: networkManager.connected && messageInput.text.length > 0 onClicked: { - networkManager.sendChatMessage(senderInput.text, recipientInput.text, messageInput.text) + networkManager.sendChatMessage(messageInput.text) messageInput.text = "" } } @@ -334,8 +299,14 @@ Window { Connections { target: networkManager - function onMessageReceived(sender, recipient, text) { - receivedMessages += sender + " -> " + recipient + ": " + text + "\n" + function onConnectedChanged() { + if (!networkManager.connected) { + receivedMessages = "" + } + } + + function onMessageReceived(text) { + receivedMessages += text + "\n" Qt.callLater(function() { messageFlickable.contentY = Math.max(0, messageFlickable.contentHeight - messageFlickable.height) }) diff --git a/src/client/network_manager.cpp b/src/client/network_manager.cpp index 4e680de..cddbebc 100644 --- a/src/client/network_manager.cpp +++ b/src/client/network_manager.cpp @@ -65,7 +65,7 @@ void NetworkManager::disconnectFromServer() { socket_.disconnectFromHost(); } -void NetworkManager::sendChatMessage(const QString& sender, const QString& recipient, const QString& text) { +void NetworkManager::sendChatMessage(const QString& text) { if (!connected()) { const auto message = tr("Not connected to a server."); setStatusText(message); @@ -75,8 +75,6 @@ void NetworkManager::sendChatMessage(const QString& sender, const QString& recip Message message; message.messageType = static_cast(MessageType::ChatMessage); - message.sender = sender; - message.recipient = recipient; message.text = text; message.timestamp = QDateTime::currentDateTimeUtc(); @@ -125,7 +123,7 @@ void NetworkManager::readAvailable() { return; } - emit messageReceived(message.sender, message.recipient, message.text); + emit messageReceived(message.text); } } diff --git a/src/client/network_manager.h b/src/client/network_manager.h index 22ddbb0..e48df36 100644 --- a/src/client/network_manager.h +++ b/src/client/network_manager.h @@ -23,13 +23,13 @@ class NetworkManager final : public QObject { // Methode that can be called from QML Q_INVOKABLE void connectToServer(const QString& host, quint16 port); Q_INVOKABLE void disconnectFromServer(); - Q_INVOKABLE void sendChatMessage(const QString& sender, const QString& recipient, const QString& text); + Q_INVOKABLE void sendChatMessage(const QString& text); signals: // These methods are implemented in qt void connectedChanged(); void statusTextChanged(); - void messageReceived(const QString& sender, const QString& recipient, const QString& text); + void messageReceived(const QString& text); void connectionError(const QString& message); private slots: diff --git a/src/server/server.cpp b/src/server/server.cpp index e85a696..be9e6de 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -63,7 +63,7 @@ void server::handleMessageReceived(const Message& message, Session* session) { return; } - qInfo() << "Message from" << message.sender << "to" << message.recipient << ":" << message.text; + qInfo() << "Message:" << message.text; if (message.messageType == static_cast(MessageType::ChatMessage)) { for (auto* connectedSession : std::as_const(sessions_)) { diff --git a/src/shared/message.cpp b/src/shared/message.cpp index 92b77ec..d04cbef 100644 --- a/src/shared/message.cpp +++ b/src/shared/message.cpp @@ -5,8 +5,6 @@ namespace messenger::protocol { QDataStream& operator<<(QDataStream& out, const Message& message) { out << message.protocolVersion; out << message.messageType; - out << message.sender; - out << message.recipient; out << message.text; out << message.timestamp; @@ -16,8 +14,6 @@ QDataStream& operator<<(QDataStream& out, const Message& message) { QDataStream& operator>>(QDataStream& in, Message& message) { in >> message.protocolVersion; in >> message.messageType; - in >> message.sender; - in >> message.recipient; in >> message.text; in >> message.timestamp; diff --git a/src/shared/message.h b/src/shared/message.h index 4023ff6..0b6b0bf 100644 --- a/src/shared/message.h +++ b/src/shared/message.h @@ -22,8 +22,6 @@ enum class MessageType : quint32 { struct Message { quint32 protocolVersion = CurrentProtocolVersion; quint32 messageType = static_cast(MessageType::ChatMessage); - QString sender; - QString recipient; QString text; QDateTime timestamp = QDateTime::currentDateTimeUtc(); }; From 1e14e8be9cca85d29eec9c7037a314bb0d5d7f37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Wed, 8 Jul 2026 19:58:00 +0200 Subject: [PATCH 08/41] Update Readme description --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 647ae9e..c39fa62 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Messenger -Messenger is a cross-platform client-server messenger application based on the RSA algorithm. The project is still in an early stage and currently provides the foundation for the client and server. +Messenger is a simple chat program with a client and a server. Users can connect to a server, send messages, receive messages, and see the previous chat history. The messenger is intentionally kept simple: there is only one chat on each server, because the project is mainly a learning project focused on networking and encryption. ## Functionality From f638e1feea17043e181fdbfccadead5002ca4f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Wed, 8 Jul 2026 20:17:02 +0200 Subject: [PATCH 09/41] Add SQLite to store messages and implement logic --- .gitignore | 7 ++++++- src/server/CMakeLists.txt | 11 ++++++++++- src/server/server.cpp | 26 ++++++++++++++++++++++++++ src/server/server.h | 3 +++ 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 5d343cc..135d8ea 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,9 @@ /build /Testing /cmake-build-debug -.vscode \ No newline at end of file +.vscode +*.db +*.sqlite +*.sqlite3 +*.sqlite-wal +*.sqlite-shm diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt index 4ea7af9..041048b 100644 --- a/src/server/CMakeLists.txt +++ b/src/server/CMakeLists.txt @@ -1,17 +1,26 @@ -find_package(Qt6 REQUIRED COMPONENTS Core Network) +find_package(Qt6 REQUIRED COMPONENTS Core Network Sql) set(CMAKE_AUTOMOC ON) add_executable(Messenger-Server + Storage/message_store.cpp + Storage/message_store.h session.cpp session.h server.cpp server.h ) +add_custom_command(TARGET Messenger-Server POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_CURRENT_SOURCE_DIR}/Migrations" + "$/Migrations" +) + target_link_libraries(Messenger-Server PRIVATE Qt6::Core Qt6::Network + Qt6::Sql Messenger-Shared ) diff --git a/src/server/server.cpp b/src/server/server.cpp index be9e6de..dd3f637 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -26,12 +26,24 @@ server::server() { } server::~server() { + shutdown(); +} + +void server::shutdown() { tcpServer_.close(); qDeleteAll(sessions_); sessions_.clear(); + if (QCoreApplication::instance() != nullptr) { + messageStore_.close(); + } } bool server::listen(const QHostAddress& address, quint16 port) { + if (!messageStore_.initialize()) { + qCritical() << "Could not initialize message storage"; + return false; + } + if (tcpServer_.listen(address, port)) { qInfo() << "Messenger server listening on" << tcpServer_.serverAddress().toString() << tcpServer_.serverPort(); @@ -50,6 +62,11 @@ void server::handleNewConnection() { connect(session, &Session::messageReceived, this, &server::handleMessageReceived); connect(session, &Session::disconnected, this, &server::handleSessionDisconnected); + + const auto previousMessages = messageStore_.loadMessages(); + for (const auto& message : previousMessages) { + session->sendMessage(message); + } } } @@ -66,6 +83,10 @@ void server::handleMessageReceived(const Message& message, Session* session) { qInfo() << "Message:" << message.text; if (message.messageType == static_cast(MessageType::ChatMessage)) { + if (!messageStore_.saveMessage(message)) { + qWarning() << "Message was not persisted"; + } + for (auto* connectedSession : std::as_const(sessions_)) { connectedSession->sendMessage(message); } @@ -81,11 +102,16 @@ void server::handleSessionDisconnected(Session* session) { int main(int argc, char* argv[]) { QCoreApplication app(argc, argv); + QCoreApplication::setApplicationName(QStringLiteral("Messenger")); + QCoreApplication::setOrganizationName(QStringLiteral("Messenger")); auto& serverInstance = server::getInstance(); if (!serverInstance.listen(QHostAddress::Any, DefaultPort)) { + serverInstance.shutdown(); return 1; } + QObject::connect(&app, &QCoreApplication::aboutToQuit, &serverInstance, &server::shutdown); + return app.exec(); } diff --git a/src/server/server.h b/src/server/server.h index f2989a9..84e3686 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -2,6 +2,7 @@ #define MESSENGER_SERVER_H #include "message.h" +#include "Storage/message_store.h" #include #include @@ -19,6 +20,7 @@ class server final : public QObject { ~server() override; bool listen(const QHostAddress& address, quint16 port); + void shutdown(); server(const server&) = delete; server& operator=(const server&) = delete; @@ -36,6 +38,7 @@ class server final : public QObject { QTcpServer tcpServer_; QSet sessions_; + MessageStore messageStore_; }; #endif // MESSENGER_SERVER_H From 398aef98f9488bce982a172ce92b9c46725d9cda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Wed, 8 Jul 2026 20:17:22 +0200 Subject: [PATCH 10/41] Add migrations --- src/server/Migrations/001_create_messages.sql | 27 ++ src/server/Storage/message_store.cpp | 339 ++++++++++++++++++ src/server/Storage/message_store.h | 34 ++ 3 files changed, 400 insertions(+) create mode 100644 src/server/Migrations/001_create_messages.sql create mode 100644 src/server/Storage/message_store.cpp create mode 100644 src/server/Storage/message_store.h diff --git a/src/server/Migrations/001_create_messages.sql b/src/server/Migrations/001_create_messages.sql new file mode 100644 index 0000000..497665f --- /dev/null +++ b/src/server/Migrations/001_create_messages.sql @@ -0,0 +1,27 @@ +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + display_name TEXT NOT NULL, + public_key TEXT NOT NULL, + created_at TEXT NOT NULL +); + +/* TODO Remove this later, when user exist */ +INSERT OR IGNORE INTO users (id, username, display_name, public_key, created_at) +VALUES (1, 'anonymous', 'Anonymous', '', strftime('%Y-%m-%dT%H:%M:%fZ', 'now')); + +CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + message_type INTEGER NOT NULL, + body TEXT NOT NULL, + client_timestamp TEXT NOT NULL, + stored_at TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) +); + +CREATE INDEX IF NOT EXISTS idx_messages_stored_at +ON messages(stored_at, id); + +CREATE INDEX IF NOT EXISTS idx_messages_user_id +ON messages(user_id); diff --git a/src/server/Storage/message_store.cpp b/src/server/Storage/message_store.cpp new file mode 100644 index 0000000..d358488 --- /dev/null +++ b/src/server/Storage/message_store.cpp @@ -0,0 +1,339 @@ +#include "message_store.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using messenger::protocol::CurrentProtocolVersion; +using messenger::protocol::Message; + +namespace { + +constexpr auto DefaultUserId = 1; + +QString lastErrorText(const QSqlQuery& query) { + return query.lastError().text(); +} + +QList splitSqlStatements(const QString& script) { + QList statements; + QString current; + bool inSingleQuote = false; + bool inDoubleQuote = false; + bool inLineComment = false; + + for (qsizetype i = 0; i < script.size(); ++i) { + const auto currentChar = script.at(i); + const auto nextChar = i + 1 < script.size() ? script.at(i + 1) : QChar(); + + if (inLineComment) { + if (currentChar == QLatin1Char('\n')) { + inLineComment = false; + } + continue; + } + + if (!inSingleQuote && !inDoubleQuote && currentChar == QLatin1Char('-') && nextChar == QLatin1Char('-')) { + inLineComment = true; + ++i; + continue; + } + + if (currentChar == QLatin1Char('\'') && !inDoubleQuote) { + inSingleQuote = !inSingleQuote; + } else if (currentChar == QLatin1Char('"') && !inSingleQuote) { + inDoubleQuote = !inDoubleQuote; + } + + if (currentChar == QLatin1Char(';') && !inSingleQuote && !inDoubleQuote) { + const auto statement = current.trimmed(); + if (!statement.isEmpty()) { + statements.append(statement); + } + current.clear(); + continue; + } + + current.append(currentChar); + } + + const auto statement = current.trimmed(); + if (!statement.isEmpty()) { + statements.append(statement); + } + + return statements; +} + +} // namespace + +MessageStore::MessageStore() + : connectionName_(QStringLiteral("messenger_server_storage")) {} + +MessageStore::~MessageStore() { + if (QCoreApplication::instance() == nullptr) { + return; + } + + close(); +} + +void MessageStore::close() { + if (!QSqlDatabase::contains(connectionName_)) { + return; + } + + { + auto database = QSqlDatabase::database(connectionName_, false); + if (database.isValid()) { + database.close(); + } + } + + QSqlDatabase::removeDatabase(connectionName_); + initialized_ = false; +} + +bool MessageStore::initialize() { + if (initialized_) { + return true; + } + + if (!openDatabase()) { + return false; + } + + if (!runMigrations()) { + return false; + } + + initialized_ = true; + return true; +} + +bool MessageStore::saveMessage(const Message& message) { + if (!initialized_) { + qWarning() << "Cannot save message before message store initialization"; + return false; + } + + auto database = QSqlDatabase::database(connectionName_); + QSqlQuery query(database); + query.prepare(R"( + INSERT INTO messages ( + user_id, + message_type, + body, + client_timestamp, + stored_at + ) VALUES ( + :user_id, + :message_type, + :body, + :client_timestamp, + :stored_at + ) + )"); + query.bindValue(QStringLiteral(":user_id"), DefaultUserId); + query.bindValue(QStringLiteral(":message_type"), message.messageType); + query.bindValue(QStringLiteral(":body"), message.text); + query.bindValue(QStringLiteral(":client_timestamp"), + message.timestamp.toUTC().toString(Qt::ISODateWithMs)); + query.bindValue(QStringLiteral(":stored_at"), + QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs)); + + if (!query.exec()) { + qWarning() << "Could not save chat message:" << lastErrorText(query); + return false; + } + + return true; +} + +QList MessageStore::loadMessages() const { + QList messages; + + if (!initialized_) { + qWarning() << "Cannot load messages before message store initialization"; + return messages; + } + + auto database = QSqlDatabase::database(connectionName_); + QSqlQuery query(database); + query.prepare(R"( + SELECT message_type, body, client_timestamp + FROM messages + ORDER BY stored_at ASC, id ASC + )"); + + if (!query.exec()) { + qWarning() << "Could not load chat messages:" << lastErrorText(query); + return messages; + } + + while (query.next()) { + Message message; + message.protocolVersion = CurrentProtocolVersion; + message.messageType = query.value(QStringLiteral("message_type")).toUInt(); + message.text = query.value(QStringLiteral("body")).toString(); + message.timestamp = QDateTime::fromString(query.value(QStringLiteral("client_timestamp")).toString(), + Qt::ISODateWithMs); + if (!message.timestamp.isValid()) { + message.timestamp = QDateTime::currentDateTimeUtc(); + } + messages.append(message); + } + + return messages; +} + +bool MessageStore::openDatabase() { + if (QSqlDatabase::contains(connectionName_)) { + return QSqlDatabase::database(connectionName_).isOpen(); + } + + auto database = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), connectionName_); + database.setDatabaseName(databasePath()); + + const auto databaseDirectory = QFileInfo(database.databaseName()).absoluteDir(); + if (!databaseDirectory.exists() && !QDir().mkpath(databaseDirectory.absolutePath())) { + qWarning() << "Could not create database directory:" << databaseDirectory.absolutePath(); + return false; + } + + if (!database.open()) { + qWarning() << "Could not open SQLite database:" << database.lastError().text(); + return false; + } + + QSqlQuery query(database); + if (!query.exec(QStringLiteral("PRAGMA foreign_keys = ON"))) { + qWarning() << "Could not enable SQLite foreign keys:" << lastErrorText(query); + return false; + } + + if (!query.exec(QStringLiteral("PRAGMA journal_mode = WAL"))) { + qWarning() << "Could not enable SQLite WAL mode:" << lastErrorText(query); + return false; + } + + qInfo() << "Using SQLite database" << database.databaseName(); + return true; +} + +bool MessageStore::runMigrations() { + auto database = QSqlDatabase::database(connectionName_); + QSqlQuery query(database); + + if (!query.exec(R"( + CREATE TABLE IF NOT EXISTS schema_migrations ( + version TEXT PRIMARY KEY, + applied_at TEXT NOT NULL + ) + )")) { + qWarning() << "Could not create schema_migrations table:" << lastErrorText(query); + return false; + } + + const QDir migrationsDirectory(QCoreApplication::applicationDirPath() + QStringLiteral("/Migrations")); + const auto migrationFiles = migrationsDirectory.entryList({QStringLiteral("*.sql")}, QDir::Files, QDir::Name); + + if (migrationFiles.isEmpty()) { + qWarning() << "No database migrations found in" << migrationsDirectory.absolutePath(); + return false; + } + + for (const auto& migrationFile : migrationFiles) { + const auto version = migrationFile.section(QLatin1Char('_'), 0, 0); + + QSqlQuery appliedQuery(database); + appliedQuery.prepare(QStringLiteral("SELECT COUNT(*) FROM schema_migrations WHERE version = :version")); + appliedQuery.bindValue(QStringLiteral(":version"), version); + if (!appliedQuery.exec() || !appliedQuery.next()) { + qWarning() << "Could not check migration state for" << migrationFile << lastErrorText(appliedQuery); + return false; + } + + if (appliedQuery.value(0).toInt() > 0) { + continue; + } + + QFile file(migrationsDirectory.filePath(migrationFile)); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + qWarning() << "Could not open migration" << file.fileName() << file.errorString(); + return false; + } + + const auto script = QString::fromUtf8(file.readAll()); + if (!database.transaction()) { + qWarning() << "Could not start migration transaction:" << database.lastError().text(); + return false; + } + + if (!executeSqlScript(script)) { + database.rollback(); + return false; + } + + QSqlQuery insertQuery(database); + insertQuery.prepare(R"( + INSERT INTO schema_migrations (version, applied_at) + VALUES (:version, :applied_at) + )"); + insertQuery.bindValue(QStringLiteral(":version"), version); + insertQuery.bindValue(QStringLiteral(":applied_at"), + QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs)); + + if (!insertQuery.exec()) { + qWarning() << "Could not record migration" << migrationFile << lastErrorText(insertQuery); + database.rollback(); + return false; + } + + if (!database.commit()) { + qWarning() << "Could not commit migration" << migrationFile << database.lastError().text(); + database.rollback(); + return false; + } + + qInfo() << "Applied database migration" << migrationFile; + } + + return true; +} + +bool MessageStore::executeSqlScript(const QString& script) const { + auto database = QSqlDatabase::database(connectionName_); + + for (const auto& statement : splitSqlStatements(script)) { + QSqlQuery query(database); + if (!query.exec(statement)) { + qWarning() << "Could not execute SQL migration statement:" << lastErrorText(query); + qWarning() << statement; + return false; + } + } + + return true; +} + +QString MessageStore::databasePath() const { + const auto configuredPath = qEnvironmentVariable("MESSENGER_DB_PATH"); + if (!configuredPath.isEmpty()) { + return configuredPath; + } + + auto appDataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + if (appDataPath.isEmpty()) { + appDataPath = QDir::homePath() + QStringLiteral("/.messenger"); + } + + return QDir(appDataPath).filePath(QStringLiteral("messenger.sqlite3")); +} diff --git a/src/server/Storage/message_store.h b/src/server/Storage/message_store.h new file mode 100644 index 0000000..92f4c4b --- /dev/null +++ b/src/server/Storage/message_store.h @@ -0,0 +1,34 @@ +#ifndef MESSENGER_MESSAGE_STORE_H +#define MESSENGER_MESSAGE_STORE_H + +#include "message.h" + +#include +#include + +class MessageStore { + public: + MessageStore(); + ~MessageStore(); + + MessageStore(const MessageStore&) = delete; + MessageStore& operator=(const MessageStore&) = delete; + MessageStore(MessageStore&&) = delete; + MessageStore& operator=(MessageStore&&) = delete; + + bool initialize(); + void close(); + bool saveMessage(const messenger::protocol::Message& message); + QList loadMessages() const; + + private: + bool openDatabase(); + bool runMigrations(); + bool executeSqlScript(const QString& script) const; + QString databasePath() const; + + QString connectionName_; + bool initialized_ = false; +}; + +#endif // MESSENGER_MESSAGE_STORE_H From 0b934c3f62d2102257ef9d3468601bf340033de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Wed, 8 Jul 2026 20:38:45 +0200 Subject: [PATCH 11/41] Add senderName to protocol --- src/shared/message.cpp | 2 ++ src/shared/message.h | 1 + 2 files changed, 3 insertions(+) diff --git a/src/shared/message.cpp b/src/shared/message.cpp index d04cbef..ff9aceb 100644 --- a/src/shared/message.cpp +++ b/src/shared/message.cpp @@ -5,6 +5,7 @@ namespace messenger::protocol { QDataStream& operator<<(QDataStream& out, const Message& message) { out << message.protocolVersion; out << message.messageType; + out << message.senderName; out << message.text; out << message.timestamp; @@ -14,6 +15,7 @@ QDataStream& operator<<(QDataStream& out, const Message& message) { QDataStream& operator>>(QDataStream& in, Message& message) { in >> message.protocolVersion; in >> message.messageType; + in >> message.senderName; in >> message.text; in >> message.timestamp; diff --git a/src/shared/message.h b/src/shared/message.h index 0b6b0bf..541ef65 100644 --- a/src/shared/message.h +++ b/src/shared/message.h @@ -22,6 +22,7 @@ enum class MessageType : quint32 { struct Message { quint32 protocolVersion = CurrentProtocolVersion; quint32 messageType = static_cast(MessageType::ChatMessage); + QString senderName; QString text; QDateTime timestamp = QDateTime::currentDateTimeUtc(); }; From c7d13e0fbf5d47cb17aedc01f6c678080d0bf66e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Wed, 8 Jul 2026 20:46:28 +0200 Subject: [PATCH 12/41] Add username logic to client --- src/client/Main.qml | 30 ++++++++++++++++++++++++++---- src/client/network_manager.cpp | 33 +++++++++++++++++++++++++++++++-- src/client/network_manager.h | 7 ++++++- 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/client/Main.qml b/src/client/Main.qml index 4e9e953..36048e0 100644 --- a/src/client/Main.qml +++ b/src/client/Main.qml @@ -15,10 +15,13 @@ Window { property string receivedMessages: "" function connectWithInput() { - if (hostInput.text.trim().length === 0 || !portInput.acceptableInput) { + if (userNameInput.text.trim().length === 0 + || hostInput.text.trim().length === 0 + || !portInput.acceptableInput) { return } + networkManager.userName = userNameInput.text networkManager.connectToServer(hostInput.text, Number(portInput.text)) } @@ -106,6 +109,23 @@ Window { wrapMode: Text.Wrap } + Column { + width: parent.width + spacing: 8 + + FieldLabel { + text: qsTr("Name") + } + + TextInputBox { + id: userNameInput + width: parent.width + text: networkManager.userName + onAccepted: connectWithInput() + onTextChanged: networkManager.userName = text + } + } + Column { width: parent.width spacing: 8 @@ -145,7 +165,9 @@ Window { ActionButton { width: parent.width text: qsTr("Verbinden") - enabledState: hostInput.text.trim().length > 0 && portInput.acceptableInput + enabledState: userNameInput.text.trim().length > 0 + && hostInput.text.trim().length > 0 + && portInput.acceptableInput onClicked: connectWithInput() } } @@ -305,8 +327,8 @@ Window { } } - function onMessageReceived(text) { - receivedMessages += text + "\n" + function onMessageReceived(senderName, text, sentAt) { + receivedMessages += "[" + sentAt + "] " + senderName + ": " + text + "\n" Qt.callLater(function() { messageFlickable.contentY = Math.max(0, messageFlickable.contentHeight - messageFlickable.height) }) diff --git a/src/client/network_manager.cpp b/src/client/network_manager.cpp index cddbebc..cf723f1 100644 --- a/src/client/network_manager.cpp +++ b/src/client/network_manager.cpp @@ -2,6 +2,7 @@ #include #include +#include using messenger::protocol::CurrentProtocolVersion; using messenger::protocol::DataStreamVersion; @@ -11,7 +12,10 @@ using messenger::protocol::MessageType; using messenger::protocol::ReadBufferSize; NetworkManager::NetworkManager(QObject* parent) - : QObject(parent), stream_(&socket_), statusText_(tr("Disconnected")) { + : QObject(parent), + stream_(&socket_), + statusText_(tr("Disconnected")), + userName_(QStringLiteral("anonymous")) { socket_.setReadBufferSize(ReadBufferSize); stream_.setVersion(DataStreamVersion); @@ -29,10 +33,24 @@ QString NetworkManager::statusText() const { return statusText_; } +QString NetworkManager::userName() const { + return userName_; +} + int NetworkManager::defaultPort() const { return DefaultPort; } +void NetworkManager::setUserName(const QString& userName) { + const auto trimmedUserName = userName.trimmed(); + if (userName_ == trimmedUserName) { + return; + } + + userName_ = trimmedUserName; + emit userNameChanged(); +} + void NetworkManager::connectToServer(const QString& host, quint16 port) { const auto trimmedHost = host.trimmed(); if (trimmedHost.isEmpty()) { @@ -73,8 +91,17 @@ void NetworkManager::sendChatMessage(const QString& text) { return; } + const auto senderName = userName_.trimmed(); + if (senderName.isEmpty()) { + const auto message = tr("User name must not be empty."); + setStatusText(message); + emit connectionError(message); + return; + } + Message message; message.messageType = static_cast(MessageType::ChatMessage); + message.senderName = senderName; message.text = text; message.timestamp = QDateTime::currentDateTimeUtc(); @@ -123,7 +150,9 @@ void NetworkManager::readAvailable() { return; } - emit messageReceived(message.text); + emit messageReceived(message.senderName, + message.text, + message.timestamp.toLocalTime().toString(QStringLiteral("HH:mm"))); } } diff --git a/src/client/network_manager.h b/src/client/network_manager.h index e48df36..110e787 100644 --- a/src/client/network_manager.h +++ b/src/client/network_manager.h @@ -11,6 +11,7 @@ class NetworkManager final : public QObject { Q_OBJECT Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged) Q_PROPERTY(QString statusText READ statusText NOTIFY statusTextChanged) + Q_PROPERTY(QString userName READ userName WRITE setUserName NOTIFY userNameChanged) Q_PROPERTY(int defaultPort READ defaultPort CONSTANT) public: @@ -18,7 +19,9 @@ class NetworkManager final : public QObject { [[nodiscard]] bool connected() const; [[nodiscard]] QString statusText() const; + [[nodiscard]] QString userName() const; [[nodiscard]] int defaultPort() const; + void setUserName(const QString& userName); // Methode that can be called from QML Q_INVOKABLE void connectToServer(const QString& host, quint16 port); @@ -29,7 +32,8 @@ class NetworkManager final : public QObject { // These methods are implemented in qt void connectedChanged(); void statusTextChanged(); - void messageReceived(const QString& text); + void userNameChanged(); + void messageReceived(const QString& senderName, const QString& text, const QString& sentAt); void connectionError(const QString& message); private slots: @@ -45,6 +49,7 @@ class NetworkManager final : public QObject { QTcpSocket socket_; QDataStream stream_; QString statusText_; + QString userName_; }; #endif // MESSENGER_NETWORK_MANAGER_H From a855d6ce257e95d798c8a3951698c7c6a1c929e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:01:25 +0200 Subject: [PATCH 13/41] Implement userName on server --- src/server/Storage/message_store.cpp | 42 ++++++++++++++++++++++--- src/server/Storage/message_store.h | 2 ++ src/server/server.cpp | 47 ++++++++++++++++++++++++---- src/server/session.cpp | 12 +++++++ src/server/session.h | 4 +++ 5 files changed, 97 insertions(+), 10 deletions(-) diff --git a/src/server/Storage/message_store.cpp b/src/server/Storage/message_store.cpp index d358488..1216b99 100644 --- a/src/server/Storage/message_store.cpp +++ b/src/server/Storage/message_store.cpp @@ -16,7 +16,7 @@ using messenger::protocol::Message; namespace { -constexpr auto DefaultUserId = 1; +constexpr auto InvalidUserId = -1; QString lastErrorText(const QSqlQuery& query) { return query.lastError().text(); @@ -124,6 +124,12 @@ bool MessageStore::saveMessage(const Message& message) { return false; } + const auto userId = userIdForUserName(message.senderName); + if (userId == InvalidUserId) { + qWarning() << "Cannot save message for unknown user:" << message.senderName; + return false; + } + auto database = QSqlDatabase::database(connectionName_); QSqlQuery query(database); query.prepare(R"( @@ -141,7 +147,7 @@ bool MessageStore::saveMessage(const Message& message) { :stored_at ) )"); - query.bindValue(QStringLiteral(":user_id"), DefaultUserId); + query.bindValue(QStringLiteral(":user_id"), userId); query.bindValue(QStringLiteral(":message_type"), message.messageType); query.bindValue(QStringLiteral(":body"), message.text); query.bindValue(QStringLiteral(":client_timestamp"), @@ -168,9 +174,10 @@ QList MessageStore::loadMessages() const { auto database = QSqlDatabase::database(connectionName_); QSqlQuery query(database); query.prepare(R"( - SELECT message_type, body, client_timestamp + SELECT users.username, messages.message_type, messages.body, messages.client_timestamp FROM messages - ORDER BY stored_at ASC, id ASC + INNER JOIN users ON users.id = messages.user_id + ORDER BY messages.stored_at ASC, messages.id ASC )"); if (!query.exec()) { @@ -181,6 +188,7 @@ QList MessageStore::loadMessages() const { while (query.next()) { Message message; message.protocolVersion = CurrentProtocolVersion; + message.senderName = query.value(QStringLiteral("username")).toString(); message.messageType = query.value(QStringLiteral("message_type")).toUInt(); message.text = query.value(QStringLiteral("body")).toString(); message.timestamp = QDateTime::fromString(query.value(QStringLiteral("client_timestamp")).toString(), @@ -194,6 +202,32 @@ QList MessageStore::loadMessages() const { return messages; } +bool MessageStore::hasUser(const QString& userName) const { + return userIdForUserName(userName) != InvalidUserId; +} + +int MessageStore::userIdForUserName(const QString& userName) const { + if (!initialized_) { + qWarning() << "Cannot look up user before message store initialization"; + return InvalidUserId; + } + + QSqlQuery query(QSqlDatabase::database(connectionName_)); + query.prepare(QStringLiteral("SELECT id FROM users WHERE username = :username")); + query.bindValue(QStringLiteral(":username"), userName.trimmed()); + + if (!query.exec()) { + qWarning() << "Could not look up user:" << lastErrorText(query); + return InvalidUserId; + } + + if (!query.next()) { + return InvalidUserId; + } + + return query.value(QStringLiteral("id")).toInt(); +} + bool MessageStore::openDatabase() { if (QSqlDatabase::contains(connectionName_)) { return QSqlDatabase::database(connectionName_).isOpen(); diff --git a/src/server/Storage/message_store.h b/src/server/Storage/message_store.h index 92f4c4b..8850b85 100644 --- a/src/server/Storage/message_store.h +++ b/src/server/Storage/message_store.h @@ -18,10 +18,12 @@ class MessageStore { bool initialize(); void close(); + bool hasUser(const QString& userName) const; bool saveMessage(const messenger::protocol::Message& message); QList loadMessages() const; private: + int userIdForUserName(const QString& userName) const; bool openDatabase(); bool runMigrations(); bool executeSqlScript(const QString& script) const; diff --git a/src/server/server.cpp b/src/server/server.cpp index dd3f637..a730d99 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -15,6 +15,17 @@ using messenger::protocol::DefaultPort; using messenger::protocol::Message; using messenger::protocol::MessageType; +namespace { + +bool isValidChatMessage(const Message& message) { + return message.messageType == static_cast(MessageType::ChatMessage) + && !message.senderName.trimmed().isEmpty() + && !message.text.trimmed().isEmpty() + && message.timestamp.isValid(); +} + +} // namespace + server& server::getInstance() { static server instance; return instance; @@ -80,19 +91,43 @@ void server::handleMessageReceived(const Message& message, Session* session) { return; } - qInfo() << "Message:" << message.text; - if (message.messageType == static_cast(MessageType::ChatMessage)) { - if (!messageStore_.saveMessage(message)) { + if (!isValidChatMessage(message)) { + qWarning() << "Ignoring invalid chat message from session"; + return; + } + + const auto senderName = message.senderName.trimmed(); + + if (!messageStore_.hasUser(senderName)) { + qWarning() << "Ignoring chat message from unknown user:" << senderName; + return; + } + + // TODO: Replace this self-declared session binding with login and authentication + // once user creation/authentication exist. + if (!session->hasUserName()) { + session->setUserName(senderName); + } else if (session->userName() != senderName) { + qWarning() << "Ignoring chat message with sender mismatch. Session user:" + << session->userName() << "message sender:" << senderName; + return; + } + + Message verifiedMessage = message; + verifiedMessage.senderName = senderName; + + qInfo() << "Message from" << verifiedMessage.senderName << ":" << verifiedMessage.text; + + if (!messageStore_.saveMessage(verifiedMessage)) { qWarning() << "Message was not persisted"; + return; } for (auto* connectedSession : std::as_const(sessions_)) { - connectedSession->sendMessage(message); + connectedSession->sendMessage(verifiedMessage); } } - - Q_UNUSED(session) } void server::handleSessionDisconnected(Session* session) { diff --git a/src/server/session.cpp b/src/server/session.cpp index 30aa8b1..858a890 100644 --- a/src/server/session.cpp +++ b/src/server/session.cpp @@ -34,6 +34,18 @@ void Session::sendMessage(const Message& message) { socket_->flush(); } +QString Session::userName() const { + return userName_; +} + +bool Session::hasUserName() const { + return !userName_.isEmpty(); +} + +void Session::setUserName(const QString& userName) { + userName_ = userName.trimmed(); +} + void Session::readAvailable() { while (socket_->bytesAvailable() > 0) { stream_.startTransaction(); diff --git a/src/server/session.h b/src/server/session.h index eb1a6cb..ddeff11 100644 --- a/src/server/session.h +++ b/src/server/session.h @@ -15,6 +15,9 @@ class Session final : public QObject { explicit Session(QTcpSocket* socket, QObject* parent = nullptr); void sendMessage(const messenger::protocol::Message& message); + [[nodiscard]] QString userName() const; + [[nodiscard]] bool hasUserName() const; + void setUserName(const QString& userName); signals: void messageReceived(const messenger::protocol::Message& message, Session* session); @@ -28,6 +31,7 @@ class Session final : public QObject { private: QTcpSocket* socket_; QDataStream stream_; + QString userName_; }; #endif // MESSENGER_SESSION_H From dd224448ae10855dba6891aa924d1c1acb1bff40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Wed, 8 Jul 2026 22:00:06 +0200 Subject: [PATCH 14/41] Link RSA --- src/client/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index d0f08b8..3f30f44 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -5,6 +5,7 @@ qt_standard_project_setup(REQUIRES 6.5) qt_add_executable(Messenger-Client WIN32 MACOSX_BUNDLE + client_connection_store.cpp network_manager.cpp client.cpp ) @@ -28,4 +29,5 @@ target_link_libraries(Messenger-Client Qt6::QuickControls2 Qt6::Network Messenger-Shared + RSA ) From 59e45eb17ac27d725d31e00796de5b44709cfb65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Wed, 8 Jul 2026 22:01:53 +0200 Subject: [PATCH 15/41] Add UI elements and popups for RSA key handling --- src/client/Main.qml | 588 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 580 insertions(+), 8 deletions(-) diff --git a/src/client/Main.qml b/src/client/Main.qml index 36048e0..4ce9472 100644 --- a/src/client/Main.qml +++ b/src/client/Main.qml @@ -13,18 +13,41 @@ Window { color: "#f7f8fa" property string receivedMessages: "" + property string pendingDeleteKeyName: "" + property bool connectionInputValid: userNameInput.text.trim().length > 0 + && hostInput.text.trim().length > 0 + && portInput.acceptableInput + && connectionStore.selectedKeyName.trim().length > 0 function connectWithInput() { if (userNameInput.text.trim().length === 0 || hostInput.text.trim().length === 0 - || !portInput.acceptableInput) { + || !portInput.acceptableInput + || connectionStore.selectedKeyName.trim().length === 0) { return } networkManager.userName = userNameInput.text + if (!connectionStore.saveLastConnection(hostInput.text, + Number(portInput.text), + userNameInput.text, + connectionStore.selectedKeyName)) { + return + } + networkManager.connectToServer(hostInput.text, Number(portInput.text)) } + function syncKeyComboBox() { + if (!keyComboBox) { + return + } + + keyComboBox.currentIndex = connectionStore.selectedKeyName.length > 0 + ? keyComboBox.find(connectionStore.selectedKeyName) + : -1 + } + component FieldLabel: Text { color: "#43515f" font.pixelSize: 14 @@ -82,6 +105,73 @@ Window { } } + component IconButton: Rectangle { + id: iconButton + + property bool enabledState: true + property string iconName: "edit" + property color activeColor: "#205493" + property color iconColor: enabledState ? "#ffffff" : "#43515f" + signal clicked() + + width: 44 + height: 44 + radius: 6 + color: enabledState ? activeColor : "#d7dde5" + + Canvas { + id: iconCanvas + anchors.centerIn: parent + width: 24 + height: 24 + + onPaint: { + const context = getContext("2d") + context.clearRect(0, 0, width, height) + context.strokeStyle = iconButton.iconColor + context.fillStyle = iconButton.iconColor + context.lineWidth = 2 + context.lineCap = "round" + context.lineJoin = "round" + + if (iconButton.iconName === "close") { + context.beginPath() + context.moveTo(7, 7) + context.lineTo(17, 17) + context.moveTo(17, 7) + context.lineTo(7, 17) + context.stroke() + } else { + context.beginPath() + context.moveTo(5, 19) + context.lineTo(9, 18) + context.lineTo(18, 9) + context.lineTo(15, 6) + context.lineTo(6, 15) + context.closePath() + context.stroke() + + context.beginPath() + context.moveTo(14, 7) + context.lineTo(17, 10) + context.stroke() + } + } + } + + MouseArea { + id: iconMouseArea + anchors.fill: parent + enabled: iconButton.enabledState + hoverEnabled: true + cursorShape: iconButton.enabledState ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: iconButton.clicked() + } + + onIconNameChanged: iconCanvas.requestPaint() + onIconColorChanged: iconCanvas.requestPaint() + } + Item { id: connectionPage anchors.fill: parent @@ -120,9 +210,63 @@ Window { TextInputBox { id: userNameInput width: parent.width - text: networkManager.userName + text: connectionStore.userName onAccepted: connectWithInput() - onTextChanged: networkManager.userName = text + onTextChanged: { + networkManager.userName = text + connectionStore.userName = text + } + } + } + + Column { + width: parent.width + spacing: 8 + + FieldLabel { + text: qsTr("RSA-Schlüssel") + } + + Row { + id: keySelectionRow + width: parent.width + spacing: 12 + + ComboBox { + id: keyComboBox + width: Math.max(120, keySelectionRow.width - manageKeysButton.width - keySelectionRow.spacing) + height: 44 + model: connectionStore.availableKeyNames + currentIndex: -1 + + background: Rectangle { + radius: 6 + color: "#ffffff" + border.color: keyComboBox.activeFocus ? "#205493" : "#c8d0d9" + } + + contentItem: Text { + leftPadding: 11 + rightPadding: 28 + text: keyComboBox.displayText + color: "#1f2933" + font.pixelSize: 15 + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + onActivated: function(index) { + connectionStore.selectedKeyName = textAt(index) + } + + Component.onCompleted: syncKeyComboBox() + } + + IconButton { + id: manageKeysButton + iconName: "edit" + onClicked: keyManagementPopup.open() + } } } @@ -137,8 +281,9 @@ Window { TextInputBox { id: hostInput width: parent.width - text: "127.0.0.1" + text: connectionStore.host onAccepted: connectWithInput() + onTextChanged: connectionStore.host = text } } @@ -153,8 +298,13 @@ Window { TextInputBox { id: portInput width: parent.width - text: String(networkManager.defaultPort) + text: String(connectionStore.port) onAccepted: connectWithInput() + onTextChanged: { + if (acceptableInput) { + connectionStore.port = Number(text) + } + } validator: IntValidator { bottom: 1 top: 65535 @@ -165,14 +315,424 @@ Window { ActionButton { width: parent.width text: qsTr("Verbinden") - enabledState: userNameInput.text.trim().length > 0 - && hostInput.text.trim().length > 0 - && portInput.acceptableInput + enabledState: connectionInputValid onClicked: connectWithInput() } } } + Popup { + id: keyManagementPopup + width: Math.min(476, parent.width - 64) + height: Math.min(446, parent.height - 64) + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + modal: true + focus: true + padding: 8 + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + transformOrigin: Item.Center + + enter: Transition { + NumberAnimation { + property: "opacity" + from: 0 + to: 1 + duration: 140 + easing.type: Easing.OutCubic + } + + NumberAnimation { + property: "scale" + from: 0.98 + to: 1 + duration: 140 + easing.type: Easing.OutCubic + } + } + + exit: Transition { + NumberAnimation { + property: "opacity" + from: 1 + to: 0 + duration: 100 + easing.type: Easing.InCubic + } + + NumberAnimation { + property: "scale" + from: 1 + to: 0.98 + duration: 100 + easing.type: Easing.InCubic + } + } + + function submit() { + if (newKeyNameInput.text.trim().length === 0) { + return + } + + if (connectionStore.createKeyPair(newKeyNameInput.text)) { + newKeyNameInput.text = "" + } + } + + onOpened: { + connectionStore.clearErrorText() + pendingDeleteKeyName = "" + newKeyNameInput.text = "" + newKeyNameInput.forceActiveFocus() + } + + background: Rectangle { + anchors { + fill: parent + margins: 8 + } + radius: 8 + color: "#ffffff" + border.color: "#c8d0d9" + } + + Overlay.modal: Rectangle { + color: "#1f2933" + opacity: keyManagementPopup.visible ? 0.28 : 0 + + Behavior on opacity { + NumberAnimation { + duration: 140 + easing.type: Easing.OutCubic + } + } + } + + contentItem: Item { + anchors.fill: parent + + Column { + anchors { + fill: parent + margins: 20 + } + spacing: 16 + + Row { + width: parent.width + height: 32 + spacing: 12 + + Text { + width: Math.max(120, parent.width - closeKeyManagementButton.width - parent.spacing) + text: qsTr("RSA-Schlüssel") + color: "#1f2933" + font.pixelSize: 22 + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + IconButton { + id: closeKeyManagementButton + width: 32 + height: 32 + iconName: "close" + activeColor: "#d7dde5" + iconColor: "#43515f" + onClicked: keyManagementPopup.close() + } + } + + Rectangle { + width: parent.width + height: 1 + color: "#e3e8ef" + } + + Item { + width: parent.width + height: Math.max(92, parent.height - 32 - 1 - createKeySection.height + - errorMessage.height - parent.spacing * 4) + + Text { + anchors.centerIn: parent + width: parent.width + text: qsTr("Noch keine RSA-Schlüssel vorhanden.") + color: "#607080" + font.pixelSize: 14 + horizontalAlignment: Text.AlignHCenter + visible: connectionStore.availableKeyNames.length === 0 + } + + ListView { + id: keysListView + anchors.fill: parent + visible: connectionStore.availableKeyNames.length > 0 + clip: true + spacing: 8 + model: connectionStore.availableKeyNames + + delegate: Rectangle { + required property string modelData + + width: keysListView.width + height: 48 + radius: 6 + color: modelData === connectionStore.selectedKeyName ? "#eef4fb" : "#f7f8fa" + border.color: modelData === connectionStore.selectedKeyName ? "#8bb4dc" : "#d7dde5" + + Text { + anchors { + left: parent.left + right: deleteKeyButton.left + verticalCenter: parent.verticalCenter + leftMargin: 12 + rightMargin: 12 + } + text: modelData + color: "#1f2933" + font.pixelSize: 15 + elide: Text.ElideRight + } + + Rectangle { + id: deleteKeyButton + anchors { + right: parent.right + verticalCenter: parent.verticalCenter + rightMargin: 8 + } + width: 76 + height: 32 + radius: 6 + color: deleteKeyMouseArea.containsMouse ? "#ead8da" : "#f3e7e8" + border.color: "#ddb9bd" + + Text { + anchors.centerIn: parent + text: qsTr("Löschen") + color: "#7a2830" + font.pixelSize: 13 + } + + MouseArea { + id: deleteKeyMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + connectionStore.clearErrorText() + pendingDeleteKeyName = modelData + deleteKeyConfirmationPopup.open() + } + } + } + + MouseArea { + anchors { + left: parent.left + right: deleteKeyButton.left + top: parent.top + bottom: parent.bottom + } + cursorShape: Qt.PointingHandCursor + onClicked: connectionStore.selectedKeyName = modelData + } + } + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + } + } + } + + Column { + id: createKeySection + width: parent.width + spacing: 8 + + FieldLabel { + text: qsTr("Neuer Schlüssel") + } + + Row { + width: parent.width + spacing: 12 + + TextInputBox { + id: newKeyNameInput + width: Math.max(120, parent.width - createManagedKeyButton.width - parent.spacing) + onAccepted: keyManagementPopup.submit() + } + + ActionButton { + id: createManagedKeyButton + width: 112 + text: qsTr("Erstellen") + enabledState: newKeyNameInput.text.trim().length > 0 + onClicked: keyManagementPopup.submit() + } + } + } + + Text { + id: errorMessage + width: parent.width + height: visible ? paintedHeight : 0 + text: connectionStore.errorText + color: "#7a2830" + font.pixelSize: 13 + wrapMode: Text.Wrap + visible: text.length > 0 + } + } + } + } + + Popup { + id: deleteKeyConfirmationPopup + width: Math.min(396, parent.width - 64) + height: Math.min(276, parent.height - 64) + x: (parent.width - width) / 2 + y: (parent.height - height) / 2 + modal: true + focus: true + padding: 8 + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + transformOrigin: Item.Center + + enter: Transition { + NumberAnimation { + property: "opacity" + from: 0 + to: 1 + duration: 120 + easing.type: Easing.OutCubic + } + + NumberAnimation { + property: "scale" + from: 0.98 + to: 1 + duration: 120 + easing.type: Easing.OutCubic + } + } + + exit: Transition { + NumberAnimation { + property: "opacity" + from: 1 + to: 0 + duration: 90 + easing.type: Easing.InCubic + } + + NumberAnimation { + property: "scale" + from: 1 + to: 0.98 + duration: 90 + easing.type: Easing.InCubic + } + } + + background: Rectangle { + anchors { + fill: parent + margins: 8 + } + radius: 8 + color: "#ffffff" + border.color: "#c8d0d9" + } + + Overlay.modal: Rectangle { + color: "#1f2933" + opacity: deleteKeyConfirmationPopup.visible ? 0.34 : 0 + + Behavior on opacity { + NumberAnimation { + duration: 120 + easing.type: Easing.OutCubic + } + } + } + + onClosed: { + if (!visible) { + pendingDeleteKeyName = "" + } + } + + contentItem: Item { + anchors.fill: parent + + Column { + anchors { + fill: parent + margins: 20 + } + spacing: 14 + + Text { + width: parent.width + text: qsTr("Schlüssel löschen") + color: "#1f2933" + font.pixelSize: 22 + elide: Text.ElideRight + } + + Text { + width: parent.width + text: qsTr("Der RSA-Schlüssel \"%1\" kann nicht wiederhergestellt werden. Eine Verbindung zum Server unter diesem Benutzer ist anschließend nicht mehr möglich.").arg(pendingDeleteKeyName) + color: "#43515f" + font.pixelSize: 14 + wrapMode: Text.Wrap + } + + Text { + width: parent.width + height: visible ? paintedHeight : 0 + text: connectionStore.errorText + color: "#7a2830" + font.pixelSize: 13 + wrapMode: Text.Wrap + visible: text.length > 0 + } + + Item { + width: parent.width + height: Math.max(0, parent.height - 22 - 56 - 44 - parent.spacing * 3) + } + + Row { + width: parent.width + spacing: 12 + + ActionButton { + width: Math.max(120, (parent.width - parent.spacing) / 2) + text: qsTr("Abbrechen") + activeColor: "#607080" + onClicked: deleteKeyConfirmationPopup.close() + } + + ActionButton { + width: Math.max(120, (parent.width - parent.spacing) / 2) + text: qsTr("Löschen") + activeColor: "#9f2f3a" + enabledState: pendingDeleteKeyName.length > 0 + onClicked: { + if (connectionStore.deleteKeyPair(pendingDeleteKeyName)) { + deleteKeyConfirmationPopup.close() + } + } + } + } + } + } + } + Column { id: chatPage anchors { @@ -341,4 +901,16 @@ Window { }) } } + + Connections { + target: connectionStore + + function onAvailableKeyNamesChanged() { + syncKeyComboBox() + } + + function onSelectedKeyNameChanged() { + syncKeyComboBox() + } + } } From cfbe3328c78a7c1e315c1aa5dda6bcdc0a2900b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Wed, 8 Jul 2026 22:06:54 +0200 Subject: [PATCH 16/41] Implement client local key and connection store --- src/client/CMakeLists.txt | 2 +- src/client/client.cpp | 11 + src/client/client.h | 2 + src/client/connection_store.cpp | 351 ++++++++++++++++++++++++++++++++ src/client/connection_store.h | 71 +++++++ 5 files changed, 436 insertions(+), 1 deletion(-) create mode 100644 src/client/connection_store.cpp create mode 100644 src/client/connection_store.h diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index 3f30f44..112df00 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -5,7 +5,7 @@ qt_standard_project_setup(REQUIRES 6.5) qt_add_executable(Messenger-Client WIN32 MACOSX_BUNDLE - client_connection_store.cpp + connection_store.cpp network_manager.cpp client.cpp ) diff --git a/src/client/client.cpp b/src/client/client.cpp index 2d541c9..72597a3 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -1,7 +1,9 @@ #include "client.h" +#include "connection_store.h" #include "network_manager.h" +#include #include #include #include @@ -16,19 +18,28 @@ client::client() = default; client::~client() = default; int client::run(QGuiApplication& app) { + QCoreApplication::setOrganizationName(QStringLiteral("ParallelEngineering")); + QCoreApplication::setApplicationName(QStringLiteral("Messenger")); + networkManager_ = std::make_unique(); + connectionStore_ = std::make_unique(); + networkManager_->setUserName(connectionStore_->userName()); + engine_ = std::make_unique(); engine_->rootContext()->setContextProperty("networkManager", networkManager_.get()); + engine_->rootContext()->setContextProperty("connectionStore", connectionStore_.get()); engine_->loadFromModule("Messenger.Client", "Main"); if (engine_->rootObjects().isEmpty()) { engine_.reset(); + connectionStore_.reset(); networkManager_.reset(); return -1; } const auto exitCode = app.exec(); engine_.reset(); + connectionStore_.reset(); networkManager_.reset(); return exitCode; diff --git a/src/client/client.h b/src/client/client.h index 23bfdf1..011f8f2 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -3,6 +3,7 @@ #include +class ConnectionStore; class NetworkManager; class QGuiApplication; class QQmlApplicationEngine; @@ -24,6 +25,7 @@ class client { std::unique_ptr engine_; std::unique_ptr networkManager_; + std::unique_ptr connectionStore_; }; #endif // MESSENGER_CLIENT_H diff --git a/src/client/connection_store.cpp b/src/client/connection_store.cpp new file mode 100644 index 0000000..c395547 --- /dev/null +++ b/src/client/connection_store.cpp @@ -0,0 +1,351 @@ +#include "connection_store.h" + +#include "keyPair.h" +#include "message.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +constexpr auto KeyFolderName = "rsa-keys"; +constexpr auto SettingsFileName = "client-connection.json"; +constexpr auto PublicKeySuffix = ".public.rsa"; +constexpr auto PrivateKeySuffix = ".private.rsa"; + +QByteArray toByteArray(const std::vector& bytes) { + return QByteArray(reinterpret_cast(bytes.data()), static_cast(bytes.size())); +} + +} // namespace + +ConnectionStore::ConnectionStore(QObject* parent) + : QObject(parent), + host_(QStringLiteral("127.0.0.1")), + port_(messenger::protocol::DefaultPort), + userName_(QStringLiteral("anonymous")) { + refreshAvailableKeyNames(); + loadLastConnection(); + clearInvalidSelectedKey(); +} + +QString ConnectionStore::host() const { + return host_; +} + +int ConnectionStore::port() const { + return port_; +} + +QString ConnectionStore::userName() const { + return userName_; +} + +QString ConnectionStore::selectedKeyName() const { + return selectedKeyName_; +} + +QStringList ConnectionStore::availableKeyNames() const { + return availableKeyNames_; +} + +QString ConnectionStore::errorText() const { + return errorText_; +} + +void ConnectionStore::setHost(const QString& host) { + const auto trimmedHost = host.trimmed(); + if (host_ == trimmedHost) { + return; + } + + host_ = trimmedHost; + emit hostChanged(); +} + +void ConnectionStore::setPort(int port) { + if (port_ == port) { + return; + } + + port_ = port; + emit portChanged(); +} + +void ConnectionStore::setUserName(const QString& userName) { + const auto trimmedUserName = userName.trimmed(); + if (userName_ == trimmedUserName) { + return; + } + + userName_ = trimmedUserName; + emit userNameChanged(); +} + +void ConnectionStore::setSelectedKeyName(const QString& selectedKeyName) { + const auto trimmedKeyName = selectedKeyName.trimmed(); + if (selectedKeyName_ == trimmedKeyName) { + return; + } + + selectedKeyName_ = trimmedKeyName; + emit selectedKeyNameChanged(); +} + +bool ConnectionStore::createKeyPair(const QString& name) { + const auto keyName = name.trimmed(); + setErrorText({}); + + if (!keyNameIsValid(keyName)) { + setErrorText(tr("Der Schlüsselname ist ungültig.")); + return false; + } + + if (!ensureKeyDirectory()) { + setErrorText(tr("Der Schlüsselordner konnte nicht erstellt werden.")); + return false; + } + + if (keyExists(keyName)) { + setErrorText(tr("Ein Schlüssel mit diesem Namen existiert bereits.")); + return false; + } + + keyPair generatedKeyPair; + const auto publicKeyBytes = toByteArray(generatedKeyPair.getPublicKey().serialize()); + const auto privateKeyBytes = toByteArray(generatedKeyPair.getPrivateKey().serialize()); + + QFile publicKeyFile(publicKeyPath(keyName)); + if (!publicKeyFile.open(QIODevice::WriteOnly | QIODevice::NewOnly)) { + setErrorText(tr("Der öffentliche Schlüssel konnte nicht gespeichert werden.")); + return false; + } + + if (publicKeyFile.write(publicKeyBytes) != publicKeyBytes.size()) { + publicKeyFile.remove(); + setErrorText(tr("Der öffentliche Schlüssel konnte nicht vollständig gespeichert werden.")); + return false; + } + publicKeyFile.close(); + + QFile privateKeyFile(privateKeyPath(keyName)); + if (!privateKeyFile.open(QIODevice::WriteOnly | QIODevice::NewOnly)) { + QFile::remove(publicKeyPath(keyName)); + setErrorText(tr("Der private Schlüssel konnte nicht gespeichert werden.")); + return false; + } + + if (privateKeyFile.write(privateKeyBytes) != privateKeyBytes.size()) { + privateKeyFile.remove(); + QFile::remove(publicKeyPath(keyName)); + setErrorText(tr("Der private Schlüssel konnte nicht vollständig gespeichert werden.")); + return false; + } + privateKeyFile.close(); + + refreshAvailableKeyNames(); + setSelectedKeyName(keyName); + return true; +} + +bool ConnectionStore::deleteKeyPair(const QString& name) { + const auto keyName = name.trimmed(); + setErrorText({}); + + if (!availableKeyNames_.contains(keyName)) { + setErrorText(tr("Der Schlüssel wurde nicht gefunden.")); + return false; + } + + const auto publicRemoved = QFile::remove(publicKeyPath(keyName)); + const auto privateRemoved = QFile::remove(privateKeyPath(keyName)); + if (!publicRemoved || !privateRemoved) { + setErrorText(tr("Der Schlüssel konnte nicht vollständig gelöscht werden.")); + refreshAvailableKeyNames(); + clearInvalidSelectedKey(); + return false; + } + + refreshAvailableKeyNames(); + if (selectedKeyName_ == keyName) { + setSelectedKeyName(availableKeyNames_.isEmpty() ? QString() : availableKeyNames_.first()); + } + + return true; +} + +void ConnectionStore::clearErrorText() { + setErrorText({}); +} + +bool ConnectionStore::saveLastConnection(const QString& host, + int port, + const QString& userName, + const QString& selectedKeyName) { + const auto trimmedHost = host.trimmed(); + const auto trimmedUserName = userName.trimmed(); + const auto trimmedKeyName = selectedKeyName.trimmed(); + setErrorText({}); + + if (trimmedHost.isEmpty() || trimmedUserName.isEmpty() || port < 1 || port > 65535 || + !availableKeyNames_.contains(trimmedKeyName)) { + setErrorText(tr("Die Verbindungsdaten sind unvollständig.")); + return false; + } + + const auto settingsFileInfo = QFileInfo(settingsFilePath()); + if (!settingsFileInfo.absoluteDir().exists() && !QDir().mkpath(settingsFileInfo.absolutePath())) { + setErrorText(tr("Der Speicherordner konnte nicht erstellt werden.")); + return false; + } + + QJsonObject root; + root.insert(QStringLiteral("host"), trimmedHost); + root.insert(QStringLiteral("port"), port); + root.insert(QStringLiteral("userName"), trimmedUserName); + root.insert(QStringLiteral("selectedKeyName"), trimmedKeyName); + + QSaveFile settingsFile(settingsFilePath()); + if (!settingsFile.open(QIODevice::WriteOnly)) { + setErrorText(tr("Die letzten Verbindungsdaten konnten nicht gespeichert werden.")); + return false; + } + + settingsFile.write(QJsonDocument(root).toJson(QJsonDocument::Indented)); + if (!settingsFile.commit()) { + setErrorText(tr("Die letzten Verbindungsdaten konnten nicht geschrieben werden.")); + return false; + } + + setHost(trimmedHost); + setPort(port); + setUserName(trimmedUserName); + setSelectedKeyName(trimmedKeyName); + return true; +} + +QString ConnectionStore::appDataPath() const { + auto path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + if (path.isEmpty()) { + path = QDir::homePath() + QStringLiteral("/.messenger"); + } + + return path; +} + +QString ConnectionStore::keyDirectoryPath() const { + return QDir(appDataPath()).filePath(QString::fromLatin1(KeyFolderName)); +} + +QString ConnectionStore::settingsFilePath() const { + return QDir(appDataPath()).filePath(QString::fromLatin1(SettingsFileName)); +} + +bool ConnectionStore::ensureKeyDirectory() { + const QDir keyDirectory(keyDirectoryPath()); + if (keyDirectory.exists()) { + return true; + } + + return QDir().mkpath(keyDirectory.absolutePath()); +} + +bool ConnectionStore::keyNameIsValid(const QString& keyName) const { + static const QRegularExpression invalidCharacters(QStringLiteral(R"([\\/:*?"<>|\x00-\x1F])")); + + return !keyName.isEmpty() && keyName != QStringLiteral(".") && keyName != QStringLiteral("..") && + !keyName.contains(invalidCharacters); +} + +bool ConnectionStore::keyExists(const QString& keyName) const { + return QFileInfo::exists(publicKeyPath(keyName)) || QFileInfo::exists(privateKeyPath(keyName)); +} + +QString ConnectionStore::publicKeyPath(const QString& keyName) const { + return QDir(keyDirectoryPath()).filePath(keyName + QString::fromLatin1(PublicKeySuffix)); +} + +QString ConnectionStore::privateKeyPath(const QString& keyName) const { + return QDir(keyDirectoryPath()).filePath(keyName + QString::fromLatin1(PrivateKeySuffix)); +} + +void ConnectionStore::refreshAvailableKeyNames() { + const QDir keyDirectory(keyDirectoryPath()); + const auto publicKeyFiles = keyDirectory.entryList({QStringLiteral("*") + QString::fromLatin1(PublicKeySuffix)}, + QDir::Files, + QDir::Name | QDir::IgnoreCase); + + QStringList keyNames; + for (const auto& publicKeyFile : publicKeyFiles) { + auto keyName = publicKeyFile; + keyName.chop(QString::fromLatin1(PublicKeySuffix).size()); + if (QFileInfo::exists(privateKeyPath(keyName))) { + keyNames.append(keyName); + } + } + + if (availableKeyNames_ == keyNames) { + return; + } + + availableKeyNames_ = keyNames; + emit availableKeyNamesChanged(); +} + +void ConnectionStore::loadLastConnection() { + QFile settingsFile(settingsFilePath()); + if (!settingsFile.open(QIODevice::ReadOnly)) { + return; + } + + const auto document = QJsonDocument::fromJson(settingsFile.readAll()); + if (!document.isObject()) { + return; + } + + const auto root = document.object(); + if (root.value(QStringLiteral("host")).isString()) { + setHost(root.value(QStringLiteral("host")).toString()); + } + + if (root.value(QStringLiteral("port")).isDouble()) { + const auto loadedPort = root.value(QStringLiteral("port")).toInt(); + if (loadedPort >= 1 && loadedPort <= 65535) { + setPort(loadedPort); + } + } + + if (root.value(QStringLiteral("userName")).isString()) { + setUserName(root.value(QStringLiteral("userName")).toString()); + } + + if (root.value(QStringLiteral("selectedKeyName")).isString()) { + setSelectedKeyName(root.value(QStringLiteral("selectedKeyName")).toString()); + } +} + +void ConnectionStore::clearInvalidSelectedKey() { + if (selectedKeyName_.isEmpty() || availableKeyNames_.contains(selectedKeyName_)) { + return; + } + + selectedKeyName_.clear(); + emit selectedKeyNameChanged(); +} + +void ConnectionStore::setErrorText(const QString& errorText) { + if (errorText_ == errorText) { + return; + } + + errorText_ = errorText; + emit errorTextChanged(); +} diff --git a/src/client/connection_store.h b/src/client/connection_store.h new file mode 100644 index 0000000..d3ce713 --- /dev/null +++ b/src/client/connection_store.h @@ -0,0 +1,71 @@ +#ifndef MESSENGER_CONNECTION_STORE_H +#define MESSENGER_CONNECTION_STORE_H + +#include +#include +#include + +class ConnectionStore final : public QObject { + Q_OBJECT + Q_PROPERTY(QString host READ host WRITE setHost NOTIFY hostChanged) + Q_PROPERTY(int port READ port WRITE setPort NOTIFY portChanged) + Q_PROPERTY(QString userName READ userName WRITE setUserName NOTIFY userNameChanged) + Q_PROPERTY(QString selectedKeyName READ selectedKeyName WRITE setSelectedKeyName NOTIFY selectedKeyNameChanged) + Q_PROPERTY(QStringList availableKeyNames READ availableKeyNames NOTIFY availableKeyNamesChanged) + Q_PROPERTY(QString errorText READ errorText NOTIFY errorTextChanged) + + public: + explicit ConnectionStore(QObject* parent = nullptr); + + [[nodiscard]] QString host() const; + [[nodiscard]] int port() const; + [[nodiscard]] QString userName() const; + [[nodiscard]] QString selectedKeyName() const; + [[nodiscard]] QStringList availableKeyNames() const; + [[nodiscard]] QString errorText() const; + + void setHost(const QString& host); + void setPort(int port); + void setUserName(const QString& userName); + void setSelectedKeyName(const QString& selectedKeyName); + + Q_INVOKABLE bool createKeyPair(const QString& name); + Q_INVOKABLE bool deleteKeyPair(const QString& name); + Q_INVOKABLE void clearErrorText(); + Q_INVOKABLE bool saveLastConnection(const QString& host, + int port, + const QString& userName, + const QString& selectedKeyName); + + signals: + void hostChanged(); + void portChanged(); + void userNameChanged(); + void selectedKeyNameChanged(); + void availableKeyNamesChanged(); + void errorTextChanged(); + + private: + [[nodiscard]] QString appDataPath() const; + [[nodiscard]] QString keyDirectoryPath() const; + [[nodiscard]] QString settingsFilePath() const; + [[nodiscard]] bool ensureKeyDirectory(); + [[nodiscard]] bool keyNameIsValid(const QString& keyName) const; + [[nodiscard]] bool keyExists(const QString& keyName) const; + [[nodiscard]] QString publicKeyPath(const QString& keyName) const; + [[nodiscard]] QString privateKeyPath(const QString& keyName) const; + + void refreshAvailableKeyNames(); + void loadLastConnection(); + void clearInvalidSelectedKey(); + void setErrorText(const QString& errorText); + + QString host_; + int port_; + QString userName_; + QString selectedKeyName_; + QStringList availableKeyNames_; + QString errorText_; +}; + +#endif // MESSENGER_CONNECTION_STORE_H From a52d28e34046d5e0830fa2f8e5df3c3e6db8fabe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Wed, 8 Jul 2026 22:16:01 +0200 Subject: [PATCH 17/41] Fix dropdown and delete menu design --- src/client/Main.qml | 134 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 115 insertions(+), 19 deletions(-) diff --git a/src/client/Main.qml b/src/client/Main.qml index 4ce9472..3a4c894 100644 --- a/src/client/Main.qml +++ b/src/client/Main.qml @@ -105,6 +105,40 @@ Window { } } + component QuietButton: Rectangle { + id: quietButton + + property alias text: label.text + property bool enabledState: true + property color normalColor: "#ffffff" + property color hoverColor: "#f7f8fa" + property color disabledColor: "#d7dde5" + property color borderColor: "#c8d0d9" + property color textColor: "#1f2933" + signal clicked() + + height: 44 + radius: 6 + color: enabledState ? (quietButtonMouseArea.containsMouse ? hoverColor : normalColor) : disabledColor + border.color: enabledState ? borderColor : "#c8d0d9" + + Text { + id: label + anchors.centerIn: parent + color: enabledState ? quietButton.textColor : "#43515f" + font.pixelSize: 15 + } + + MouseArea { + id: quietButtonMouseArea + anchors.fill: parent + enabled: quietButton.enabledState + hoverEnabled: true + cursorShape: quietButton.enabledState ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: quietButton.clicked() + } + } + component IconButton: Rectangle { id: iconButton @@ -240,14 +274,14 @@ Window { currentIndex: -1 background: Rectangle { - radius: 6 + radius: 8 color: "#ffffff" border.color: keyComboBox.activeFocus ? "#205493" : "#c8d0d9" } contentItem: Text { leftPadding: 11 - rightPadding: 28 + rightPadding: 36 text: keyComboBox.displayText color: "#1f2933" font.pixelSize: 15 @@ -255,6 +289,73 @@ Window { elide: Text.ElideRight } + indicator: Canvas { + x: keyComboBox.width - width - 12 + y: (keyComboBox.height - height) / 2 + width: 16 + height: 16 + + onPaint: { + const context = getContext("2d") + context.clearRect(0, 0, width, height) + context.strokeStyle = "#43515f" + context.lineWidth = 2 + context.lineCap = "round" + context.lineJoin = "round" + context.beginPath() + context.moveTo(4, 6) + context.lineTo(8, 10) + context.lineTo(12, 6) + context.stroke() + } + } + + delegate: ItemDelegate { + required property string modelData + required property int index + + width: keyComboBox.width - 8 + height: 40 + highlighted: keyComboBox.highlightedIndex === index + + contentItem: Text { + text: modelData + color: "#1f2933" + font.pixelSize: 15 + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + background: Rectangle { + radius: 6 + color: parent.highlighted ? "#eef4fb" : "#ffffff" + } + } + + popup: Popup { + y: keyComboBox.height + 4 + width: keyComboBox.width + implicitHeight: Math.min(contentItem.implicitHeight + 8, 220) + padding: 4 + + background: Rectangle { + radius: 8 + color: "#ffffff" + border.color: "#c8d0d9" + } + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: keyComboBox.popup.visible ? keyComboBox.delegateModel : null + currentIndex: keyComboBox.highlightedIndex + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + } + } + } + onActivated: function(index) { connectionStore.selectedKeyName = textAt(index) } @@ -478,8 +579,8 @@ Window { width: keysListView.width height: 48 radius: 6 - color: modelData === connectionStore.selectedKeyName ? "#eef4fb" : "#f7f8fa" - border.color: modelData === connectionStore.selectedKeyName ? "#8bb4dc" : "#d7dde5" + color: "#f7f8fa" + border.color: "#d7dde5" Text { anchors { @@ -527,17 +628,6 @@ Window { } } } - - MouseArea { - anchors { - left: parent.left - right: deleteKeyButton.left - top: parent.top - bottom: parent.bottom - } - cursorShape: Qt.PointingHandCursor - onClicked: connectionStore.selectedKeyName = modelData - } } ScrollBar.vertical: ScrollBar { @@ -710,17 +800,23 @@ Window { width: parent.width spacing: 12 - ActionButton { + QuietButton { width: Math.max(120, (parent.width - parent.spacing) / 2) text: qsTr("Abbrechen") - activeColor: "#607080" + normalColor: "#ffffff" + hoverColor: "#f7f8fa" + borderColor: "#c8d0d9" + textColor: "#43515f" onClicked: deleteKeyConfirmationPopup.close() } - ActionButton { + QuietButton { width: Math.max(120, (parent.width - parent.spacing) / 2) text: qsTr("Löschen") - activeColor: "#9f2f3a" + normalColor: "#f3e7e8" + hoverColor: "#ead8da" + borderColor: "#ddb9bd" + textColor: "#7a2830" enabledState: pendingDeleteKeyName.length > 0 onClicked: { if (connectionStore.deleteKeyPair(pendingDeleteKeyName)) { From 9849d8e1b225a27b5b36e93ca6e0138c930f677c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Thu, 9 Jul 2026 09:42:31 +0200 Subject: [PATCH 18/41] Update Readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c39fa62..1ccccc7 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ Messenger is a simple chat program with a client and a server. Users can connect ## Functionality -- **Client connection:** enter a server host, connect automatically to the server, disconnect again when terminating the program, and see the current connection status in the UI. +- **Client connection:** enter a server host, connect to the server, disconnect again when terminating the program, and see the current connection status in the UI. - **Group chat authentication:** each server represents one group chat; user accounts are created on the server with a name and a public key, without passwords. Clients authenticate through RSA-based verification using the public key stored for their account. - **Message sending:** compose text messages and send them to the connected server. -- **Message receiving:** display incoming messages in the client window as they arrive from the server. +- **Message receiving:** display incoming messages in the client window as they arrive from the server with timestamp and username. - **Group chat forwarding:** the server keeps track of the clients connected to its group chat and broadcasts chat messages to every active session. - **Chat history:** the server stores the group chat history and sends all previous messages to newly connected clients. - **Protocol validation:** client and server exchange structured messages and reject unsupported protocol versions. From 818f2d5d482562da7f3a82102e54bc30030e710f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Fri, 10 Jul 2026 08:05:44 +0200 Subject: [PATCH 19/41] Update org name on the server --- src/server/server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/server.cpp b/src/server/server.cpp index a730d99..570205b 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -138,7 +138,7 @@ void server::handleSessionDisconnected(Session* session) { int main(int argc, char* argv[]) { QCoreApplication app(argc, argv); QCoreApplication::setApplicationName(QStringLiteral("Messenger")); - QCoreApplication::setOrganizationName(QStringLiteral("Messenger")); + QCoreApplication::setOrganizationName(QStringLiteral("ParallelEngineering")); auto& serverInstance = server::getInstance(); if (!serverInstance.listen(QHostAddress::Any, DefaultPort)) { From ea8e3a7a2316cfa18f419d75d3c2b5a13bce5b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sat, 11 Jul 2026 14:12:41 +0200 Subject: [PATCH 20/41] Update RSA submodule --- lib/RSA | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/RSA b/lib/RSA index 480dfb0..ae48056 160000 --- a/lib/RSA +++ b/lib/RSA @@ -1 +1 @@ -Subproject commit 480dfb017249f79769978e23fe9bd5167e5a6b4d +Subproject commit ae48056506456eff9a2488a6d0cd4431b7b8df7d From 1aad8013bd1d4358700441d540ce61f852e6a63d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sat, 11 Jul 2026 15:51:46 +0200 Subject: [PATCH 21/41] Implement key loading --- src/client/connection_store.cpp | 80 ++++++++++++++++++++++++++++++--- src/client/connection_store.h | 7 +++ 2 files changed, 81 insertions(+), 6 deletions(-) diff --git a/src/client/connection_store.cpp b/src/client/connection_store.cpp index c395547..5c4b738 100644 --- a/src/client/connection_store.cpp +++ b/src/client/connection_store.cpp @@ -12,6 +12,8 @@ #include #include +#include +#include #include namespace { @@ -25,6 +27,11 @@ QByteArray toByteArray(const std::vector& bytes) { return QByteArray(reinterpret_cast(bytes.data()), static_cast(bytes.size())); } +std::vector toByteVector(const QByteArray& bytes) { + const auto* begin = reinterpret_cast(bytes.constData()); + return {begin, begin + bytes.size()}; +} + } // namespace ConnectionStore::ConnectionStore(QObject* parent) @@ -53,6 +60,10 @@ QString ConnectionStore::selectedKeyName() const { return selectedKeyName_; } +const keyPair* ConnectionStore::currentKeyPair() const { + return currentKeyPair_ ? &*currentKeyPair_ : nullptr; +} + QStringList ConnectionStore::availableKeyNames() const { return availableKeyNames_; } @@ -92,12 +103,35 @@ void ConnectionStore::setUserName(const QString& userName) { void ConnectionStore::setSelectedKeyName(const QString& selectedKeyName) { const auto trimmedKeyName = selectedKeyName.trimmed(); - if (selectedKeyName_ == trimmedKeyName) { + + if (trimmedKeyName.isEmpty()) { + const auto selectionChanged = !selectedKeyName_.isEmpty(); + selectedKeyName_.clear(); + currentKeyPair_.reset(); + if (selectionChanged) { + emit selectedKeyNameChanged(); + } return; } + if (selectedKeyName_ == trimmedKeyName && currentKeyPair_) { + return; + } + + auto loadedKeyPair = loadKeyPair(trimmedKeyName); + if (!loadedKeyPair) { + // Re-emit the current value so QML controls return to the still-active selection. + emit selectedKeyNameChanged(); + return; + } + + const auto selectionChanged = selectedKeyName_ != trimmedKeyName; selectedKeyName_ = trimmedKeyName; - emit selectedKeyNameChanged(); + currentKeyPair_ = std::move(*loadedKeyPair); + setErrorText({}); + if (selectionChanged) { + emit selectedKeyNameChanged(); + } } bool ConnectionStore::createKeyPair(const QString& name) { @@ -153,7 +187,7 @@ bool ConnectionStore::createKeyPair(const QString& name) { refreshAvailableKeyNames(); setSelectedKeyName(keyName); - return true; + return selectedKeyName_ == keyName && currentKeyPair_.has_value(); } bool ConnectionStore::deleteKeyPair(const QString& name) { @@ -201,6 +235,13 @@ bool ConnectionStore::saveLastConnection(const QString& host, return false; } + if (selectedKeyName_ != trimmedKeyName || !currentKeyPair_) { + setSelectedKeyName(trimmedKeyName); + if (selectedKeyName_ != trimmedKeyName || !currentKeyPair_) { + return false; + } + } + const auto settingsFileInfo = QFileInfo(settingsFilePath()); if (!settingsFileInfo.absoluteDir().exists() && !QDir().mkpath(settingsFileInfo.absolutePath())) { setErrorText(tr("Der Speicherordner konnte nicht erstellt werden.")); @@ -277,6 +318,29 @@ QString ConnectionStore::privateKeyPath(const QString& keyName) const { return QDir(keyDirectoryPath()).filePath(keyName + QString::fromLatin1(PrivateKeySuffix)); } +std::optional ConnectionStore::loadKeyPair(const QString& keyName) { + QFile publicKeyFile(publicKeyPath(keyName)); + if (!publicKeyFile.open(QIODevice::ReadOnly)) { + setErrorText(tr("Der öffentliche Schlüssel konnte nicht eingelesen werden.")); + return std::nullopt; + } + const auto publicKeyBytes = toByteVector(publicKeyFile.readAll()); + + QFile privateKeyFile(privateKeyPath(keyName)); + if (!privateKeyFile.open(QIODevice::ReadOnly)) { + setErrorText(tr("Der private Schlüssel konnte nicht eingelesen werden.")); + return std::nullopt; + } + const auto privateKeyBytes = toByteVector(privateKeyFile.readAll()); + + try { + return keyPair::create(publicKeyBytes, privateKeyBytes); + } catch (const std::exception&) { + setErrorText(tr("Das Schlüsselpaar konnte nicht deserialisiert werden.")); + return std::nullopt; + } +} + void ConnectionStore::refreshAvailableKeyNames() { const QDir keyDirectory(keyDirectoryPath()); const auto publicKeyFiles = keyDirectory.entryList({QStringLiteral("*") + QString::fromLatin1(PublicKeySuffix)}, @@ -333,12 +397,16 @@ void ConnectionStore::loadLastConnection() { } void ConnectionStore::clearInvalidSelectedKey() { - if (selectedKeyName_.isEmpty() || availableKeyNames_.contains(selectedKeyName_)) { + if (selectedKeyName_.isEmpty()) { + currentKeyPair_.reset(); + return; + } + + if (availableKeyNames_.contains(selectedKeyName_) && currentKeyPair_) { return; } - selectedKeyName_.clear(); - emit selectedKeyNameChanged(); + setSelectedKeyName({}); } void ConnectionStore::setErrorText(const QString& errorText) { diff --git a/src/client/connection_store.h b/src/client/connection_store.h index d3ce713..3100cc9 100644 --- a/src/client/connection_store.h +++ b/src/client/connection_store.h @@ -1,10 +1,14 @@ #ifndef MESSENGER_CONNECTION_STORE_H #define MESSENGER_CONNECTION_STORE_H +#include "keyPair.h" + #include #include #include +#include + class ConnectionStore final : public QObject { Q_OBJECT Q_PROPERTY(QString host READ host WRITE setHost NOTIFY hostChanged) @@ -21,6 +25,7 @@ class ConnectionStore final : public QObject { [[nodiscard]] int port() const; [[nodiscard]] QString userName() const; [[nodiscard]] QString selectedKeyName() const; + [[nodiscard]] const keyPair* currentKeyPair() const; [[nodiscard]] QStringList availableKeyNames() const; [[nodiscard]] QString errorText() const; @@ -54,6 +59,7 @@ class ConnectionStore final : public QObject { [[nodiscard]] bool keyExists(const QString& keyName) const; [[nodiscard]] QString publicKeyPath(const QString& keyName) const; [[nodiscard]] QString privateKeyPath(const QString& keyName) const; + [[nodiscard]] std::optional loadKeyPair(const QString& keyName); void refreshAvailableKeyNames(); void loadLastConnection(); @@ -64,6 +70,7 @@ class ConnectionStore final : public QObject { int port_; QString userName_; QString selectedKeyName_; + std::optional currentKeyPair_; QStringList availableKeyNames_; QString errorText_; }; From db4a3ebaa349c8d6e014dcce26ef5eaa8586e431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sat, 11 Jul 2026 16:42:52 +0200 Subject: [PATCH 22/41] Add loading animation for key creation --- src/client/Main.qml | 84 ++++++++++++++++++++-- src/client/connection_store.cpp | 121 ++++++++++++++++++++++++-------- src/client/connection_store.h | 18 ++++- 3 files changed, 187 insertions(+), 36 deletions(-) diff --git a/src/client/Main.qml b/src/client/Main.qml index 3a4c894..c9da64e 100644 --- a/src/client/Main.qml +++ b/src/client/Main.qml @@ -431,7 +431,9 @@ Window { modal: true focus: true padding: 8 - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + closePolicy: connectionStore.keyGenerationInProgress + ? Popup.NoAutoClose + : Popup.CloseOnEscape | Popup.CloseOnPressOutside transformOrigin: Item.Center enter: Transition { @@ -475,7 +477,7 @@ Window { return } - if (connectionStore.createKeyPair(newKeyNameInput.text)) { + if (connectionStore.startKeyPairCreation(newKeyNameInput.text)) { newKeyNameInput.text = "" } } @@ -538,6 +540,7 @@ Window { width: 32 height: 32 iconName: "close" + enabledState: !connectionStore.keyGenerationInProgress activeColor: "#d7dde5" iconColor: "#43515f" onClicked: keyManagementPopup.close() @@ -552,7 +555,9 @@ Window { Item { width: parent.width - height: Math.max(92, parent.height - 32 - 1 - createKeySection.height + height: Math.max(92, parent.height - 32 - 1 + - (connectionStore.keyGenerationInProgress + ? keyGenerationSection.height : createKeySection.height) - errorMessage.height - parent.spacing * 4) Text { @@ -620,7 +625,8 @@ Window { id: deleteKeyMouseArea anchors.fill: parent hoverEnabled: true - cursorShape: Qt.PointingHandCursor + enabled: !connectionStore.keyGenerationInProgress + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor onClicked: { connectionStore.clearErrorText() pendingDeleteKeyName = modelData @@ -640,6 +646,7 @@ Window { id: createKeySection width: parent.width spacing: 8 + visible: !connectionStore.keyGenerationInProgress FieldLabel { text: qsTr("Neuer Schlüssel") @@ -665,6 +672,75 @@ Window { } } + Column { + id: keyGenerationSection + width: parent.width + spacing: 10 + visible: connectionStore.keyGenerationInProgress + + FieldLabel { + width: parent.width + text: qsTr("Schlüsselerzeugung...") + elide: Text.ElideRight + } + + Canvas { + id: progressTrack + width: parent.width + height: 12 + antialiasing: true + property real barX: -width * 0.32 + + onBarXChanged: requestPaint() + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + + onPaint: { + const context = getContext("2d") + const radius = height / 2 + const barWidth = width * 0.32 + context.reset() + + context.beginPath() + context.roundedRect(0, 0, width, height, radius, radius) + context.fillStyle = "#e3e8ef" + context.fill() + context.clip() + + context.beginPath() + context.roundedRect(barX, 0, barWidth, height, radius, radius) + context.fillStyle = "#205493" + context.fill() + } + + NumberAnimation on barX { + running: connectionStore.keyGenerationInProgress + loops: Animation.Infinite + from: -progressTrack.width * 0.32 + to: progressTrack.width + duration: 1100 + easing.type: Easing.InOutCubic + } + } + + Item { + width: parent.width + height: 44 + + QuietButton { + id: cancelKeyGenerationButton + anchors.right: parent.right + width: 112 + text: qsTr("Abbrechen") + normalColor: "#ffffff" + hoverColor: "#f7f8fa" + borderColor: "#c8d0d9" + textColor: "#43515f" + onClicked: connectionStore.cancelKeyPairCreation() + } + } + } + Text { id: errorMessage width: parent.width diff --git a/src/client/connection_store.cpp b/src/client/connection_store.cpp index 5c4b738..c9b7fdb 100644 --- a/src/client/connection_store.cpp +++ b/src/client/connection_store.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include @@ -44,6 +46,10 @@ ConnectionStore::ConnectionStore(QObject* parent) clearInvalidSelectedKey(); } +ConnectionStore::~ConnectionStore() { + cancelKeyPairCreation(); +} + QString ConnectionStore::host() const { return host_; } @@ -72,6 +78,8 @@ QString ConnectionStore::errorText() const { return errorText_; } +bool ConnectionStore::keyGenerationInProgress() const { return keyGenerationInProgress_; } + void ConnectionStore::setHost(const QString& host) { const auto trimmedHost = host.trimmed(); if (host_ == trimmedHost) { @@ -134,7 +142,7 @@ void ConnectionStore::setSelectedKeyName(const QString& selectedKeyName) { } } -bool ConnectionStore::createKeyPair(const QString& name) { +bool ConnectionStore::startKeyPairCreation(const QString& name) { const auto keyName = name.trimmed(); setErrorText({}); @@ -153,47 +161,98 @@ bool ConnectionStore::createKeyPair(const QString& name) { return false; } - keyPair generatedKeyPair; - const auto publicKeyBytes = toByteArray(generatedKeyPair.getPublicKey().serialize()); - const auto privateKeyBytes = toByteArray(generatedKeyPair.getPrivateKey().serialize()); - - QFile publicKeyFile(publicKeyPath(keyName)); - if (!publicKeyFile.open(QIODevice::WriteOnly | QIODevice::NewOnly)) { - setErrorText(tr("Der öffentliche Schlüssel konnte nicht gespeichert werden.")); - return false; - } + if (discardGeneratedKey_) { + discardGeneratedKey_->store(true, std::memory_order_relaxed); + } + discardGeneratedKey_ = std::make_shared(false); + keyGenerationInProgress_ = true; + emit keyGenerationInProgressChanged(); + + const QPointer guardedThis(this); + const auto discardGeneratedKey = discardGeneratedKey_; + keyGenerationThreads_.emplace_back([guardedThis, discardGeneratedKey, keyName]() { + QByteArray publicBytes; + QByteArray privateBytes; + QString error; + try { + keyPair generated; + if (!discardGeneratedKey->load(std::memory_order_relaxed)) { + publicBytes = toByteArray(generated.getPublicKey().serialize()); + privateBytes = toByteArray(generated.getPrivateKey().serialize()); + } + } catch (const std::exception& exception) { + error = QString::fromUtf8(exception.what()); + } + if (!guardedThis) return; + const auto discarded = discardGeneratedKey->load(std::memory_order_relaxed); + QMetaObject::invokeMethod(guardedThis, [guardedThis, keyName, publicBytes, privateBytes, error, + discarded, discardGeneratedKey]() { + if (guardedThis) { + guardedThis->finishKeyPairCreation(keyName, publicBytes, privateBytes, error, + discarded, discardGeneratedKey); + } + }, Qt::QueuedConnection); + }); + return true; +} - if (publicKeyFile.write(publicKeyBytes) != publicKeyBytes.size()) { - publicKeyFile.remove(); - setErrorText(tr("Der öffentliche Schlüssel konnte nicht vollständig gespeichert werden.")); - return false; - } - publicKeyFile.close(); +void ConnectionStore::cancelKeyPairCreation() { + if (!keyGenerationInProgress_) return; + discardGeneratedKey_->store(true, std::memory_order_relaxed); + keyGenerationInProgress_ = false; + emit keyGenerationInProgressChanged(); +} - QFile privateKeyFile(privateKeyPath(keyName)); - if (!privateKeyFile.open(QIODevice::WriteOnly | QIODevice::NewOnly)) { - QFile::remove(publicKeyPath(keyName)); - setErrorText(tr("Der private Schlüssel konnte nicht gespeichert werden.")); - return false; +void ConnectionStore::finishKeyPairCreation(const QString& keyName, const QByteArray& publicKeyBytes, + const QByteArray& privateKeyBytes, const QString& workerError, + bool discarded, + const std::shared_ptr& discardGeneratedKey) { + discarded = discarded || discardGeneratedKey->load(std::memory_order_relaxed); + + if (!discarded && workerError.isEmpty()) { + if (keyExists(keyName)) { + setErrorText(tr("Ein Schlüssel mit diesem Namen existiert bereits.")); + } else { + QFile publicKeyFile(publicKeyPath(keyName)); + if (!publicKeyFile.open(QIODevice::WriteOnly | QIODevice::NewOnly) || + publicKeyFile.write(publicKeyBytes) != publicKeyBytes.size()) { + publicKeyFile.remove(); + setErrorText(tr("Der öffentliche Schlüssel konnte nicht gespeichert werden.")); + } else { + publicKeyFile.close(); + QFile privateKeyFile(privateKeyPath(keyName)); + if (!privateKeyFile.open(QIODevice::WriteOnly | QIODevice::NewOnly) || + privateKeyFile.write(privateKeyBytes) != privateKeyBytes.size()) { + privateKeyFile.remove(); + QFile::remove(publicKeyPath(keyName)); + setErrorText(tr("Der private Schlüssel konnte nicht gespeichert werden.")); + } else { + privateKeyFile.close(); + refreshAvailableKeyNames(); + setSelectedKeyName(keyName); + } + } + } + } else if (!discarded) { + setErrorText(tr("Das Schlüsselpaar konnte nicht erstellt werden: %1").arg(workerError)); } - if (privateKeyFile.write(privateKeyBytes) != privateKeyBytes.size()) { - privateKeyFile.remove(); - QFile::remove(publicKeyPath(keyName)); - setErrorText(tr("Der private Schlüssel konnte nicht vollständig gespeichert werden.")); - return false; + if (discardGeneratedKey_ == discardGeneratedKey) { + discardGeneratedKey_.reset(); + keyGenerationInProgress_ = false; + emit keyGenerationInProgressChanged(); } - privateKeyFile.close(); - - refreshAvailableKeyNames(); - setSelectedKeyName(keyName); - return selectedKeyName_ == keyName && currentKeyPair_.has_value(); } bool ConnectionStore::deleteKeyPair(const QString& name) { const auto keyName = name.trimmed(); setErrorText({}); + if (keyGenerationInProgress_) { + setErrorText(tr("Während der Schlüsselerstellung kann kein Schlüssel gelöscht werden.")); + return false; + } + if (!availableKeyNames_.contains(keyName)) { setErrorText(tr("Der Schlüssel wurde nicht gefunden.")); return false; diff --git a/src/client/connection_store.h b/src/client/connection_store.h index 3100cc9..dc00b23 100644 --- a/src/client/connection_store.h +++ b/src/client/connection_store.h @@ -8,6 +8,10 @@ #include #include +#include +#include +#include +#include class ConnectionStore final : public QObject { Q_OBJECT @@ -17,9 +21,11 @@ class ConnectionStore final : public QObject { Q_PROPERTY(QString selectedKeyName READ selectedKeyName WRITE setSelectedKeyName NOTIFY selectedKeyNameChanged) Q_PROPERTY(QStringList availableKeyNames READ availableKeyNames NOTIFY availableKeyNamesChanged) Q_PROPERTY(QString errorText READ errorText NOTIFY errorTextChanged) + Q_PROPERTY(bool keyGenerationInProgress READ keyGenerationInProgress NOTIFY keyGenerationInProgressChanged) public: explicit ConnectionStore(QObject* parent = nullptr); + ~ConnectionStore() override; [[nodiscard]] QString host() const; [[nodiscard]] int port() const; @@ -28,13 +34,15 @@ class ConnectionStore final : public QObject { [[nodiscard]] const keyPair* currentKeyPair() const; [[nodiscard]] QStringList availableKeyNames() const; [[nodiscard]] QString errorText() const; + [[nodiscard]] bool keyGenerationInProgress() const; void setHost(const QString& host); void setPort(int port); void setUserName(const QString& userName); void setSelectedKeyName(const QString& selectedKeyName); - Q_INVOKABLE bool createKeyPair(const QString& name); + Q_INVOKABLE bool startKeyPairCreation(const QString& name); + Q_INVOKABLE void cancelKeyPairCreation(); Q_INVOKABLE bool deleteKeyPair(const QString& name); Q_INVOKABLE void clearErrorText(); Q_INVOKABLE bool saveLastConnection(const QString& host, @@ -49,6 +57,7 @@ class ConnectionStore final : public QObject { void selectedKeyNameChanged(); void availableKeyNamesChanged(); void errorTextChanged(); + void keyGenerationInProgressChanged(); private: [[nodiscard]] QString appDataPath() const; @@ -65,6 +74,10 @@ class ConnectionStore final : public QObject { void loadLastConnection(); void clearInvalidSelectedKey(); void setErrorText(const QString& errorText); + void finishKeyPairCreation(const QString& keyName, const QByteArray& publicKeyBytes, + const QByteArray& privateKeyBytes, const QString& workerError, + bool discarded, + const std::shared_ptr& discardGeneratedKey); QString host_; int port_; @@ -73,6 +86,9 @@ class ConnectionStore final : public QObject { std::optional currentKeyPair_; QStringList availableKeyNames_; QString errorText_; + bool keyGenerationInProgress_ = false; + std::shared_ptr discardGeneratedKey_; + std::vector keyGenerationThreads_; }; #endif // MESSENGER_CONNECTION_STORE_H From 92f51ea1904ad672481067e852b365a05af6bd92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sat, 11 Jul 2026 19:07:17 +0200 Subject: [PATCH 23/41] Set UI style --- src/client/client.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/client/client.cpp b/src/client/client.cpp index 72597a3..ecad322 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -7,6 +7,7 @@ #include #include #include +#include client& client::getInstance() { static client instance; @@ -46,6 +47,7 @@ int client::run(QGuiApplication& app) { } int main(int argc, char* argv[]) { + QQuickStyle::setStyle(QStringLiteral("Material")); QGuiApplication app(argc, argv); auto& clientInstance = client::getInstance(); From 6a7fbff65659641f54c1097d0aca8b460f39eb62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sat, 11 Jul 2026 19:15:16 +0200 Subject: [PATCH 24/41] Add admin user --- src/server/CMakeLists.txt | 1 + src/server/Migrations/001_create_messages.sql | 7 +- src/server/Storage/message_store.cpp | 97 +++++++++++++++++++ src/server/Storage/message_store.h | 2 + 4 files changed, 101 insertions(+), 6 deletions(-) diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt index 041048b..7ac98c7 100644 --- a/src/server/CMakeLists.txt +++ b/src/server/CMakeLists.txt @@ -23,4 +23,5 @@ target_link_libraries(Messenger-Server Qt6::Network Qt6::Sql Messenger-Shared + RSA ) diff --git a/src/server/Migrations/001_create_messages.sql b/src/server/Migrations/001_create_messages.sql index 497665f..4bd34cf 100644 --- a/src/server/Migrations/001_create_messages.sql +++ b/src/server/Migrations/001_create_messages.sql @@ -1,15 +1,10 @@ CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, - display_name TEXT NOT NULL, - public_key TEXT NOT NULL, + public_key BLOB NOT NULL, created_at TEXT NOT NULL ); -/* TODO Remove this later, when user exist */ -INSERT OR IGNORE INTO users (id, username, display_name, public_key, created_at) -VALUES (1, 'anonymous', 'Anonymous', '', strftime('%Y-%m-%dT%H:%M:%fZ', 'now')); - CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, diff --git a/src/server/Storage/message_store.cpp b/src/server/Storage/message_store.cpp index 1216b99..e2e89a0 100644 --- a/src/server/Storage/message_store.cpp +++ b/src/server/Storage/message_store.cpp @@ -1,5 +1,7 @@ #include "message_store.h" +#include "keyPair.h" + #include #include #include @@ -11,17 +13,70 @@ #include #include +#include +#include +#include +#include + using messenger::protocol::CurrentProtocolVersion; using messenger::protocol::Message; namespace { constexpr auto InvalidUserId = -1; +constexpr auto AdminPublicKeyFileName = "admin.public.rsa"; QString lastErrorText(const QSqlQuery& query) { return query.lastError().text(); } +std::optional loadAdminPublicKey(const QString& databasePath) { + const auto publicKeyPath = QFileInfo(databasePath).absoluteDir().filePath( + QString::fromLatin1(AdminPublicKeyFileName)); + const QFileInfo publicKeyInfo(publicKeyPath); + if (!publicKeyInfo.exists()) { + qCritical() << "Admin public key file does not exist:" << publicKeyInfo.absoluteFilePath(); + return std::nullopt; + } + if (!publicKeyInfo.isFile()) { + qCritical() << "Admin public key path is not a file:" << publicKeyInfo.absoluteFilePath(); + return std::nullopt; + } + + QFile publicKeyFile(publicKeyPath); + if (!publicKeyFile.open(QIODevice::ReadOnly)) { + qCritical() << "Could not read admin public key file:" << publicKeyInfo.absoluteFilePath() + << publicKeyFile.errorString(); + return std::nullopt; + } + + const auto publicKeyData = publicKeyFile.readAll(); + if (publicKeyData.isEmpty()) { + qCritical() << "Admin public key file is empty:" << publicKeyInfo.absoluteFilePath(); + return std::nullopt; + } + + const std::vector serializedKey(publicKeyData.cbegin(), publicKeyData.cend()); + try { + PublicKey publicKey; + const operations::BigInt one(1); + if (!keyPair::s_deserialize(serializedKey, publicKey.n, publicKey.e) + || publicKey.n <= one + || publicKey.e <= one + || publicKey.serialize() != serializedKey) { + qCritical() << "Admin public key file contains an invalid RSA public key:" + << publicKeyInfo.absoluteFilePath(); + return std::nullopt; + } + } catch (const std::exception& exception) { + qCritical() << "Could not parse admin public key file:" << publicKeyInfo.absoluteFilePath() + << exception.what(); + return std::nullopt; + } + + return publicKeyData; +} + QList splitSqlStatements(const QString& script) { QList statements; QString current; @@ -106,6 +161,11 @@ bool MessageStore::initialize() { return true; } + const auto adminPublicKey = loadAdminPublicKey(databasePath()); + if (!adminPublicKey) { + return false; + } + if (!openDatabase()) { return false; } @@ -114,6 +174,10 @@ bool MessageStore::initialize() { return false; } + if (!ensureAdminUser(*adminPublicKey)) { + return false; + } + initialized_ = true; return true; } @@ -343,6 +407,39 @@ bool MessageStore::runMigrations() { return true; } +bool MessageStore::ensureAdminUser(const QByteArray& publicKey) const { + auto database = QSqlDatabase::database(connectionName_); + if (!database.transaction()) { + qWarning() << "Could not start admin user transaction:" << database.lastError().text(); + return false; + } + + QSqlQuery query(database); + query.prepare(R"( + INSERT INTO users (username, public_key, created_at) + VALUES (:username, :public_key, :created_at) + ON CONFLICT(username) DO UPDATE SET public_key = excluded.public_key + )"); + query.bindValue(QStringLiteral(":username"), QStringLiteral("admin")); + query.bindValue(QStringLiteral(":public_key"), publicKey); + query.bindValue(QStringLiteral(":created_at"), + QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs)); + + if (!query.exec()) { + qWarning() << "Could not create or update admin user:" << lastErrorText(query); + database.rollback(); + return false; + } + + if (!database.commit()) { + qWarning() << "Could not commit admin user transaction:" << database.lastError().text(); + database.rollback(); + return false; + } + + return true; +} + bool MessageStore::executeSqlScript(const QString& script) const { auto database = QSqlDatabase::database(connectionName_); diff --git a/src/server/Storage/message_store.h b/src/server/Storage/message_store.h index 8850b85..f33f70e 100644 --- a/src/server/Storage/message_store.h +++ b/src/server/Storage/message_store.h @@ -3,6 +3,7 @@ #include "message.h" +#include #include #include @@ -26,6 +27,7 @@ class MessageStore { int userIdForUserName(const QString& userName) const; bool openDatabase(); bool runMigrations(); + bool ensureAdminUser(const QByteArray& publicKey) const; bool executeSqlScript(const QString& script) const; QString databasePath() const; From 789ed1ee86ceca256babe49e001d02f4076cbbe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sat, 11 Jul 2026 19:36:48 +0200 Subject: [PATCH 25/41] Add connection establishment concept --- docs/connection-establishment.md | 504 +++++++++++++++++++++++++++++++ 1 file changed, 504 insertions(+) create mode 100644 docs/connection-establishment.md diff --git a/docs/connection-establishment.md b/docs/connection-establishment.md new file mode 100644 index 0000000..1920026 --- /dev/null +++ b/docs/connection-establishment.md @@ -0,0 +1,504 @@ +# Connection Establishment and Client Authentication + +## Purpose + +The Messenger server must authenticate every client immediately after the TCP connection is established. A client is authenticated by proving that it owns the private RSA key belonging to a public key stored for its user account in the server database. + +The private key never leaves the client. The server stores only public keys. Initially, the database contains only the `admin` user and the admin public key loaded during server initialization. + +The authentication flow is inspired by SSH public-key authentication and deliberately kept small enough for this learning project. + +## Security goals + +The connection establishment must ensure that: + +- a TCP connection alone does not grant access to server functionality; +- a client must prove possession of the matching private RSA key; +- an authentication response cannot be reused for a later connection; +- the authenticated user identity is bound to the server-side session; +- the client cannot select or change its sender identity after authentication; +- chat history and other application data are sent only after authentication; +- failed, malformed, or timed-out authentication attempts are disconnected; +- private keys are never transmitted to or stored on the server. + +This design authenticates the client to the server. It does not, by itself, authenticate the server to the client or encrypt chat traffic. TLS or an SSH-like pinned server host key can be added separately for those properties. + +## Existing project state + +The project already provides the main building blocks: + +- `users` contains a unique username and a serialized RSA public key; +- the server imports the initial admin public key from `admin.public.rsa`; +- `MessageStore::hasUser()` can determine whether a username exists; +- `Session` represents one TCP connection; +- client and server exchange versioned messages through `QDataStream`; +- the RSA library provides key serialization, `BigInt`, and modular exponentiation; +- Qt provides SHA-256 through `QCryptographicHash`. + +The current server behavior is not yet authenticated: + +- chat history is sent as soon as a TCP client connects; +- the first chat message binds its self-declared sender name to the session; +- the client considers an established TCP socket to be fully connected; +- the existing RSA API supports encryption and decryption, but no signature API. + +These behaviors must be replaced by the flow below. + +## Protocol overview + +```mermaid +sequenceDiagram + participant C as Client + participant S as Server + participant DB as Database + + C->>S: Establish TCP connection + Note over C,S: Session state: AwaitingHello + + C->>S: AuthHello(username, clientNonce) + S->>DB: Load user ID and public key + DB-->>S: User ID and public key + S->>S: Generate authId and serverNonce + S-->>C: AuthChallenge(authId, serverNonce) + Note over C,S: Session state: AwaitingProof + + C->>C: Hash canonical authentication transcript + C->>C: Sign digest with private RSA key + C->>S: AuthProof(authId, signature) + S->>S: Verify signature with stored public key + + alt Signature is valid + S->>S: Bind user ID and username to session + S->>S: Session state: Authenticated + S-->>C: AuthSuccess + S-->>C: Chat history + else Signature is invalid + S-->>C: AuthFailure + S->>S: Session state: Rejected + S->>S: Close connection + end +``` + +## Authentication messages + +Authentication messages are protocol messages, but they should have dedicated payload structures instead of overloading the chat `Message` fields. + +```cpp +enum class MessageType : quint32 { + AuthHello = 1, + AuthChallenge = 2, + AuthProof = 3, + AuthSuccess = 4, + AuthFailure = 5, + + ChatMessage = 100, + SystemMessage = 101, + ErrorMessage = 102, +}; +``` + +Suggested payloads: + +```cpp +struct AuthHello { + QString username; + QByteArray clientNonce; // Exactly 32 bytes +}; + +struct AuthChallenge { + QByteArray authId; // Exactly 16 bytes + QByteArray serverNonce; // Exactly 32 bytes +}; + +struct AuthProof { + QByteArray authId; + QByteArray signature; +}; + +struct AuthResult { + bool success; + QString reason; // Generic, non-sensitive error text +}; +``` + +Every serialized protocol frame must continue to include `protocolVersion` and `messageType`. Payload lengths must be validated before allocating or processing data. + +## Authentication transcript + +The signature must cover the full connection-specific context, not only the server nonce. Client and server independently create the same canonical byte sequence: + +```text +domainSeparator = "MessengerAuth/v1" +protocolVersion = CurrentProtocolVersion +username = normalized username from AuthHello +authId = random authentication attempt ID +clientNonce = random client nonce +serverNonce = random server nonce +``` + +Conceptually: + +```text +authData = Encode( + domainSeparator, + protocolVersion, + username, + authId, + clientNonce, + serverNonce +) + +digest = SHA-256(authData) +``` + +`Encode` must be deterministic. It should use fixed field order, explicit length prefixes, UTF-8 for the username, and fixed-size integer encoding. Client and server must use the same `QDataStream` version and byte order. Concatenating ambiguous strings without lengths is not permitted. + +The domain separator prevents a signature produced for a different feature or protocol from being accepted as a Messenger login proof. + +## Using the existing RSA library + +### What can be reused + +The current RSA library can remain the basis of the implementation. The following parts are reusable: + +- `PublicKey` and `PrivateKey`; +- key serialization and deserialization; +- `operations::BigInt`; +- `operations::math::modPow()`; +- the existing 4096-bit key generation; +- operating-system-backed random generation already used during key generation. + +Qt's `QCryptographicHash` can provide SHA-256, so another library is not required for the initial learning implementation. + +### What must not be used unchanged + +The existing `core::encryptor::encrypt()` must not be used to encrypt the authentication challenge. It encrypts every plaintext byte independently and deterministically. An observer can encrypt all 256 possible byte values with the public key and use the resulting lookup table to recover the challenge without possessing the private key. + +Consequently, the following flow is not acceptable: + +```text +server encrypts a string challenge with encrypt() +client decrypts it with decrypt() +client returns the plaintext challenge +``` + +### Minimal signature extension + +For the initial learning version, add a small digest-signing API to the RSA library: + +```cpp +namespace core::signature { + +std::vector signDigest( + const PrivateKey& privateKey, + const std::vector& digest); + +bool verifyDigest( + const PublicKey& publicKey, + const std::vector& digest, + const std::vector& signature); + +} // namespace core::signature +``` + +The minimal implementation uses the existing modular exponentiation: + +```text +digestInteger = OS2IP(SHA-256(authData)) +signature = digestInteger^d mod n +verifiedValue = signature^e mod n +valid = verifiedValue == digestInteger +``` + +`OS2IP` converts the 32-byte digest to one unsigned integer using a documented byte order. Signatures should be serialized to the fixed byte length of the RSA modulus so that leading zero bytes are preserved. Verification must reject signatures that are empty, incorrectly sized, or greater than or equal to the modulus. + +This direct hash-and-RSA construction demonstrates private-key possession and is suitable only for the project's first educational implementation. It is not equivalent to a standardized SSH signature scheme. + +### Planned security upgrade + +The signature API should later implement RSA-PSS with SHA-256 and MGF1: + +```text +signature = RSA-PSS-Sign(privateKey, SHA-256(authData)) +valid = RSA-PSS-Verify(publicKey, SHA-256(authData), signature) +``` + +The network handshake and message structures do not need to change when the internal implementation moves from the learning scheme to RSA-PSS. A future protocol version or signature-algorithm field should identify the selected algorithm if both variants are supported at the same time. + +## Server-side session state + +`Session` must explicitly track authentication state: + +```cpp +enum class AuthenticationState { + AwaitingHello, + AwaitingProof, + Authenticated, + Rejected, +}; +``` + +Suggested session data: + +```cpp +AuthenticationState authenticationState_; +int authenticatedUserId_; +QString authenticatedUserName_; +PublicKey authenticationPublicKey_; +QByteArray authId_; +QByteArray clientNonce_; +QByteArray serverNonce_; +QDeadlineTimer authenticationDeadline_; +``` + +Only one message type is valid in each unauthenticated state: + +| Session state | Accepted client message | All other messages | +|---|---|---| +| `AwaitingHello` | `AuthHello` | Reject and disconnect | +| `AwaitingProof` | `AuthProof` | Reject and disconnect | +| `Authenticated` | Authorized application messages | Handle normally | +| `Rejected` | None | Disconnect | + +An authentication attempt should expire after a short fixed interval, for example 10 seconds. Each challenge is valid only for its originating `Session`, only for its matching `authId`, and only once. Challenge data must be cleared after success, failure, timeout, or disconnect. + +## Detailed server flow + +### 1. Accept TCP connection + +Create a `Session` in `AwaitingHello`. Do not send chat history and do not expose other application functionality. + +### 2. Process `AuthHello` + +The server: + +1. verifies the protocol version and message shape; +2. normalizes and validates the username; +3. verifies that `clientNonce` is exactly 32 bytes; +4. loads the user ID, canonical username, and serialized public key from the database; +5. validates and deserializes the public key; +6. creates a cryptographically random 16-byte `authId`; +7. creates a cryptographically random 32-byte `serverNonce`; +8. stores all authentication context in the `Session`; +9. changes the state to `AwaitingProof`; +10. sends `AuthChallenge`. + +Unknown users should receive the same generic failure behavior as invalid signatures so that authentication errors reveal as little account information as practical. + +### 3. Process `AuthProof` + +The server: + +1. verifies that the session is in `AwaitingProof`; +2. verifies that the attempt has not expired; +3. compares the received `authId` with the session value; +4. reconstructs the canonical authentication transcript; +5. calculates its SHA-256 digest; +6. verifies the signature using the public key loaded for this session; +7. invalidates the challenge regardless of the result. + +On success, the server stores the database user ID and canonical username in the session, changes its state to `Authenticated`, sends `AuthSuccess`, and then sends the chat history. + +On failure, the server sends a generic `AuthFailure`, marks the session as `Rejected`, and closes the socket. It must not allow a second proof attempt for the same challenge. + +## Authorization after authentication + +Authentication establishes the session identity. Every server action must still begin with an authorization check: + +```cpp +if (!session->isAuthenticated()) { + session->disconnectFromHost(); + return; +} +``` + +For incoming chat messages, the server must ignore any sender name supplied by the client and set it from the session: + +```cpp +Message verifiedMessage = incomingMessage; +verifiedMessage.senderName = session->authenticatedUserName(); +verifiedMessage.timestamp = QDateTime::currentDateTimeUtc(); +``` + +Preferably, client chat payloads should no longer contain `senderName` at all. The server is the authority for both sender identity and the timestamp used for ordering and storage. + +Broadcasting must also target only authenticated sessions. An unauthenticated TCP connection must not receive chat messages, history, user data, or administrative responses. + +## Client state + +The client must distinguish TCP connectivity from successful authentication: + +```cpp +enum class ConnectionState { + Disconnected, + Connecting, + Authenticating, + Authenticated, + Disconnecting, +}; +``` + +`QTcpSocket::connected` changes the state to `Authenticating`, not `Authenticated`. Chat controls remain disabled until `AuthSuccess` is received. Only the authenticated state should be exposed to the existing UI as ready for messaging. + +The client flow is: + +1. load and validate its private key before connecting; +2. establish the TCP connection; +3. generate a fresh 32-byte `clientNonce`; +4. send `AuthHello`; +5. validate `AuthChallenge` and its field sizes; +6. construct the canonical transcript and calculate SHA-256; +7. sign the digest with the private key; +8. send `AuthProof`; +9. clear temporary authentication data; +10. enable messaging only after `AuthSuccess`. + +Failure to load the private key must prevent connection authentication and produce a clear local error. The private key must never be logged or included in an error message. + +## Database access + +The existing schema is sufficient for the initial implementation: + +```sql +CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + public_key BLOB NOT NULL, + created_at TEXT NOT NULL +); +``` + +`MessageStore` needs a lookup that returns the user record and public key rather than only a Boolean: + +```cpp +struct UserAuthenticationRecord { + int userId; + QString username; + QByteArray publicKey; +}; + +std::optional +findUserForAuthentication(const QString& username) const; +``` + +The admin bootstrap remains unchanged: the server validates `admin.public.rsa` and stores it for the initial `admin` account. Future user-management functionality can allow an authenticated and authorized admin to add more usernames and public keys. + +## Randomness and replay protection + +Both nonces and `authId` must come from a cryptographically secure operating-system random source. `std::rand()`, timestamps, counters, and regular pseudo-random generators are not sufficient. + +Replay protection is provided by: + +- a new `clientNonce` for every client attempt; +- a new `serverNonce` and `authId` for every server session; +- binding all values to the signature; +- accepting each challenge only once; +- applying a short authentication deadline; +- keeping challenge state inside the originating session. + +A previously recorded `AuthProof` therefore cannot authenticate a new connection because its transcript will contain different nonces and a different `authId`. + +## Failure handling + +The server should close the connection for: + +- an unsupported protocol version; +- an unexpected message type for the current state; +- malformed or oversized fields; +- an empty or invalid username; +- an unknown user; +- a missing or invalid public key; +- an incorrect `authId`; +- an expired challenge; +- an invalid signature; +- more than one authentication attempt on the same session. + +External errors should stay generic, for example `Authentication failed`. Detailed reasons may be logged on the server, but logs must not include private material, full authentication payloads, or reusable secrets. Repeated failures should eventually be rate-limited by source address and username. + +## Required code changes + +### Shared protocol + +- add authentication message types and payload serialization; +- define strict size limits and canonical transcript encoding; +- increment `CurrentProtocolVersion` because the connection behavior changes incompatibly. + +### RSA library + +- add `signDigest(PrivateKey, digest)`; +- add `verifyDigest(PublicKey, digest, signature)`; +- add fixed-width signature serialization and strict input validation; +- add unit tests for valid, invalid, modified, and wrong-key signatures; +- later replace the minimal implementation with RSA-PSS. + +### Server storage + +- add `findUserForAuthentication()`; +- return the database user ID, canonical username, and public key; +- keep the existing admin public-key bootstrap. + +### Server session + +- add the authentication state and temporary challenge data; +- add the authentication deadline; +- expose authenticated user ID and username only after success; +- reject unexpected pre-authentication traffic. + +### Server + +- stop sending history from `handleNewConnection()`; +- handle `AuthHello` and `AuthProof` before application messages; +- send history only after successful authentication; +- remove the self-declared username binding from the first chat message; +- derive all message identities from the authenticated session; +- broadcast only to authenticated sessions. + +### Client + +- load the private key from a configured local file; +- implement connection and authentication states; +- send `AuthHello` after the socket connects; +- sign `AuthChallenge` and send `AuthProof`; +- keep chat input disabled until `AuthSuccess`; +- clear temporary authentication values after completion. + +## Verification plan + +At minimum, automated tests should cover: + +1. the admin authenticates with the matching private key; +2. authentication fails with a different private key; +3. authentication fails for an unknown username; +4. changing the username, nonce, `authId`, or signature causes failure; +5. replaying a proof in a new connection causes failure; +6. replaying a proof twice in one connection causes disconnection; +7. an expired challenge causes failure; +8. chat messages before authentication cause disconnection; +9. unauthenticated sessions receive neither history nor broadcasts; +10. the server replaces or ignores a client-provided sender name; +11. malformed and oversized authentication messages are rejected; +12. the client UI enables messaging only after `AuthSuccess`; +13. signature serialization preserves leading zero bytes; +14. signing with one key and verification with another key fails. + +## Implementation order + +1. Add and test digest signing and verification in the RSA library. +2. Add authentication protocol payloads and canonical transcript encoding. +3. Add the public-key lookup to `MessageStore`. +4. Add the authentication state machine to `Session`. +5. Implement the server-side handshake and move history delivery after authentication. +6. Implement private-key loading and the client-side handshake. +7. Remove trust in client-provided sender names. +8. Add integration tests for success, failure, replay, timeout, and pre-authentication access. +9. Upgrade the RSA signature implementation to RSA-PSS before treating the protocol as production security. + +## Final connection rule + +The central rule is: + +```text +TCP connected != authenticated +``` + +Until the RSA proof has been verified, the connection may process only the authentication handshake. After successful verification, the server binds the database user to the session and permits authorized application messages. Any deviation from the expected handshake closes the connection. From b3900b2f96a12bfd5960c5e0e461cb6dd682d102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sun, 12 Jul 2026 14:46:40 +0200 Subject: [PATCH 26/41] Improve chat messages UI --- src/client/Main.qml | 124 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 100 insertions(+), 24 deletions(-) diff --git a/src/client/Main.qml b/src/client/Main.qml index c9da64e..6f8a557 100644 --- a/src/client/Main.qml +++ b/src/client/Main.qml @@ -12,13 +12,16 @@ Window { color: "#f7f8fa" - property string receivedMessages: "" property string pendingDeleteKeyName: "" property bool connectionInputValid: userNameInput.text.trim().length > 0 && hostInput.text.trim().length > 0 && portInput.acceptableInput && connectionStore.selectedKeyName.trim().length > 0 + ListModel { + id: chatMessages + } + function connectWithInput() { if (userNameInput.text.trim().length === 0 || hostInput.text.trim().length === 0 @@ -989,34 +992,95 @@ Window { Rectangle { width: parent.width height: Math.max(120, parent.height - y - composeRow.height - parent.spacing) - radius: 6 - color: "#ffffff" - border.color: "#c8d0d9" + radius: 12 + color: "#f0f3f7" + border.color: "#d7dde5" clip: true - Flickable { - id: messageFlickable + Text { + anchors.centerIn: parent + text: qsTr("Noch keine Nachrichten") + color: "#7b8794" + font.pixelSize: 15 + visible: chatMessages.count === 0 + } + + ListView { + id: messageList anchors { - fill: parent - margins: 12 + left: parent.left + right: parent.right + bottom: parent.bottom + margins: 16 } - contentWidth: width - contentHeight: Math.max(height, messageText.paintedHeight) - boundsBehavior: Flickable.StopAtBounds + height: Math.min(parent.height - 32, contentHeight) clip: true + spacing: 4 + model: chatMessages + boundsBehavior: Flickable.StopAtBounds ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } - Text { - id: messageText - width: messageFlickable.width - y: Math.max(0, messageFlickable.height - paintedHeight) - text: receivedMessages.length > 0 ? receivedMessages : qsTr("Keine Nachrichten empfangen.") - color: "#1f2933" - font.pixelSize: 15 - wrapMode: Text.Wrap + delegate: Item { + required property string senderName + required property string messageText + required property string sentAt + required property bool ownMessage + required property bool systemMessage + + width: messageList.width + height: messageBubble.height + 8 + + Rectangle { + id: messageBubble + anchors { + right: ownMessage ? parent.right : undefined + left: ownMessage ? undefined : parent.left + } + width: Math.min(parent.width * 0.72, + Math.max(messageBody.implicitWidth, + ownMessage ? 0 : senderLabel.implicitWidth) + 28) + height: messageContent.implicitHeight + 20 + radius: 14 + color: systemMessage ? "#fff4d6" + : ownMessage ? "#205493" : "#ffffff" + border.color: systemMessage ? "#ead59a" + : ownMessage ? "#205493" : "#d7dde5" + + Column { + id: messageContent + anchors { + fill: parent + margins: 10 + leftMargin: 14 + rightMargin: 14 + } + spacing: 4 + + Text { + id: senderLabel + width: parent.width + text: senderName + color: systemMessage ? "#806000" + : ownMessage ? "#dcecff" : "#607080" + font.pixelSize: 12 + font.weight: Font.DemiBold + visible: !ownMessage + wrapMode: Text.Wrap + } + + Text { + id: messageBody + width: parent.width + text: messageText + color: ownMessage ? "#ffffff" : "#1f2933" + font.pixelSize: 15 + wrapMode: Text.Wrap + } + } + } } } } @@ -1055,21 +1119,33 @@ Window { function onConnectedChanged() { if (!networkManager.connected) { - receivedMessages = "" + chatMessages.clear() } } function onMessageReceived(senderName, text, sentAt) { - receivedMessages += "[" + sentAt + "] " + senderName + ": " + text + "\n" + chatMessages.append({ + "senderName": senderName, + "messageText": text, + "sentAt": sentAt, + "ownMessage": senderName === networkManager.userName, + "systemMessage": false + }) Qt.callLater(function() { - messageFlickable.contentY = Math.max(0, messageFlickable.contentHeight - messageFlickable.height) + messageList.positionViewAtEnd() }) } function onConnectionError(message) { - receivedMessages += "Error: " + message + "\n" + chatMessages.append({ + "senderName": qsTr("System"), + "messageText": message, + "sentAt": "", + "ownMessage": false, + "systemMessage": true + }) Qt.callLater(function() { - messageFlickable.contentY = Math.max(0, messageFlickable.contentHeight - messageFlickable.height) + messageList.positionViewAtEnd() }) } } From 77f5f16fb10ad43c04eabe43c1d1b42461397b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sun, 12 Jul 2026 16:30:35 +0200 Subject: [PATCH 27/41] Add deserialization for public key on server --- src/server/server.cpp | 165 +++++++++++++++++++++++++++++++++++------- 1 file changed, 140 insertions(+), 25 deletions(-) diff --git a/src/server/server.cpp b/src/server/server.cpp index 570205b..a25f296 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -1,14 +1,21 @@ #include "server.h" #include "session.h" +#include "keyPair.h" +#include "signature.h" #include +#include #include #include #include #include #include +#include +#include +#include +#include using messenger::protocol::CurrentProtocolVersion; using messenger::protocol::DefaultPort; @@ -19,9 +26,30 @@ namespace { bool isValidChatMessage(const Message& message) { return message.messageType == static_cast(MessageType::ChatMessage) - && !message.senderName.trimmed().isEmpty() && !message.text.trimmed().isEmpty() - && message.timestamp.isValid(); + && message.text.size() <= messenger::protocol::MaximumMessageSize; +} + +std::vector toByteVector(const QByteArray& bytes) { + const auto* begin = reinterpret_cast(bytes.constData()); + return {begin, begin + bytes.size()}; +} + +std::optional deserializePublicKey(const QByteArray& serializedKey) { + try { + PublicKey publicKey; + const auto bytes = toByteVector(serializedKey); + const operations::BigInt one(1); + if (!keyPair::s_deserialize(bytes, publicKey.n, publicKey.e) + || publicKey.n <= one + || publicKey.e <= one + || publicKey.serialize() != bytes) { + return std::nullopt; + } + return publicKey; + } catch (const std::exception&) { + return std::nullopt; + } } } // namespace @@ -74,10 +102,6 @@ void server::handleNewConnection() { connect(session, &Session::messageReceived, this, &server::handleMessageReceived); connect(session, &Session::disconnected, this, &server::handleSessionDisconnected); - const auto previousMessages = messageStore_.loadMessages(); - for (const auto& message : previousMessages) { - session->sendMessage(message); - } } } @@ -91,40 +115,131 @@ void server::handleMessageReceived(const Message& message, Session* session) { return; } - if (message.messageType == static_cast(MessageType::ChatMessage)) { - if (!isValidChatMessage(message)) { - qWarning() << "Ignoring invalid chat message from session"; + const auto messageType = static_cast(message.messageType); + + if (session->authenticationState() == Session::AuthenticationState::AwaitingHello) { + if (messageType != MessageType::AuthHello) { + qWarning() << "Rejecting unexpected message before AuthHello"; + session->rejectAuthentication(); return; } - const auto senderName = message.senderName.trimmed(); + const auto requestedUserName = message.senderName.trimmed(); + if (requestedUserName.isEmpty() + || requestedUserName.size() > messenger::protocol::MaximumUserNameSize + || message.clientNonce.size() != messenger::protocol::AuthenticationNonceSize) { + qWarning() << "Rejecting malformed AuthHello"; + session->rejectAuthentication(); + return; + } - if (!messageStore_.hasUser(senderName)) { - qWarning() << "Ignoring chat message from unknown user:" << senderName; + const auto user = messageStore_.findUserForAuthentication(requestedUserName); + if (!user) { + qWarning() << "Authentication requested for unknown user:" << requestedUserName; + session->rejectAuthentication(); return; } - // TODO: Replace this self-declared session binding with login and authentication - // once user creation/authentication exist. - if (!session->hasUserName()) { - session->setUserName(senderName); - } else if (session->userName() != senderName) { - qWarning() << "Ignoring chat message with sender mismatch. Session user:" - << session->userName() << "message sender:" << senderName; + const auto publicKey = deserializePublicKey(user->publicKey); + if (!publicKey) { + qWarning() << "Stored public key is invalid for user:" << user->userName; + session->rejectAuthentication(); return; } - Message verifiedMessage = message; - verifiedMessage.senderName = senderName; + const auto authenticationId = messenger::protocol::generateSecureRandomBytes( + messenger::protocol::AuthenticationIdSize); + const auto serverNonce = messenger::protocol::generateSecureRandomBytes( + messenger::protocol::AuthenticationNonceSize); + session->beginAuthentication(user->userId, + user->userName, + *publicKey, + authenticationId, + message.clientNonce, + serverNonce); + + Message challenge; + challenge.messageType = static_cast(MessageType::AuthChallenge); + challenge.authenticationId = authenticationId; + challenge.serverNonce = serverNonce; + session->sendMessage(challenge); + qInfo() << "Sent authentication challenge for user" << user->userName; + return; + } - qInfo() << "Message from" << verifiedMessage.senderName << ":" << verifiedMessage.text; + if (session->authenticationState() == Session::AuthenticationState::AwaitingProof) { + if (messageType != MessageType::AuthProof + || session->authenticationExpired() + || message.authenticationId.size() != messenger::protocol::AuthenticationIdSize + || message.authenticationId != session->authenticationId() + || message.signature.isEmpty()) { + qWarning() << "Rejecting malformed or expired AuthProof for" << session->userName(); + session->rejectAuthentication(); + return; + } - if (!messageStore_.saveMessage(verifiedMessage)) { - qWarning() << "Message was not persisted"; + const auto transcript = messenger::protocol::authenticationTranscript( + session->userName(), + session->authenticationId(), + session->clientNonce(), + session->serverNonce()); + const auto digest = QCryptographicHash::hash(transcript, QCryptographicHash::Sha256); + const auto signatureValid = core::signature::verifyDigest( + session->authenticationPublicKey(), + toByteVector(digest), + toByteVector(message.signature)); + if (!signatureValid) { + qWarning() << "Authentication signature verification failed for" << session->userName(); + session->rejectAuthentication(); return; } - for (auto* connectedSession : std::as_const(sessions_)) { + session->completeAuthentication(); + qInfo() << "Authenticated user" << session->userName(); + + Message success; + success.messageType = static_cast(MessageType::AuthSuccess); + success.senderName = session->userName(); + success.text = QStringLiteral("Authentication successful."); + session->sendMessage(success); + + const auto previousMessages = messageStore_.loadMessages(); + for (const auto& previousMessage : previousMessages) { + session->sendMessage(previousMessage); + } + return; + } + + if (!session->isAuthenticated()) { + session->disconnectFromHost(); + return; + } + + if (messageType != MessageType::ChatMessage || !isValidChatMessage(message)) { + qWarning() << "Disconnecting authenticated session after invalid message from" + << session->userName(); + session->disconnectFromHost(); + return; + } + + Message verifiedMessage = message; + verifiedMessage.senderName = session->userName(); + verifiedMessage.text = message.text.trimmed(); + verifiedMessage.timestamp = QDateTime::currentDateTimeUtc(); + verifiedMessage.authenticationId.clear(); + verifiedMessage.clientNonce.clear(); + verifiedMessage.serverNonce.clear(); + verifiedMessage.signature.clear(); + + qInfo() << "Message from" << verifiedMessage.senderName << ":" << verifiedMessage.text; + + if (!messageStore_.saveMessage(verifiedMessage)) { + qWarning() << "Message was not persisted"; + return; + } + + for (auto* connectedSession : std::as_const(sessions_)) { + if (connectedSession->isAuthenticated()) { connectedSession->sendMessage(verifiedMessage); } } From e7355c9ea25da898328aa04f5127ec88a1ef5e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sun, 12 Jul 2026 16:33:08 +0200 Subject: [PATCH 28/41] Expand protocol for authentication --- src/shared/message.cpp | 42 ++++++++++++++++++++++++++++++++++++++++++ src/shared/message.h | 31 +++++++++++++++++++++++++++---- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/shared/message.cpp b/src/shared/message.cpp index ff9aceb..46270d1 100644 --- a/src/shared/message.cpp +++ b/src/shared/message.cpp @@ -1,5 +1,8 @@ #include "message.h" +#include +#include + namespace messenger::protocol { QDataStream& operator<<(QDataStream& out, const Message& message) { @@ -8,6 +11,10 @@ QDataStream& operator<<(QDataStream& out, const Message& message) { out << message.senderName; out << message.text; out << message.timestamp; + out << message.authenticationId; + out << message.clientNonce; + out << message.serverNonce; + out << message.signature; return out; } @@ -18,8 +25,43 @@ QDataStream& operator>>(QDataStream& in, Message& message) { in >> message.senderName; in >> message.text; in >> message.timestamp; + in >> message.authenticationId; + in >> message.clientNonce; + in >> message.serverNonce; + in >> message.signature; return in; } +QByteArray authenticationTranscript( + const QString& userName, + const QByteArray& authenticationId, + const QByteArray& clientNonce, + const QByteArray& serverNonce) { + QByteArray transcript; + QDataStream stream(&transcript, QIODevice::WriteOnly); + stream.setVersion(DataStreamVersion); + stream.setByteOrder(QDataStream::BigEndian); + stream << QByteArrayLiteral("MessengerAuth/v1"); + stream << CurrentProtocolVersion; + stream << userName.trimmed().toUtf8(); + stream << authenticationId; + stream << clientNonce; + stream << serverNonce; + return transcript; +} + +QByteArray generateSecureRandomBytes(qsizetype size) { + if (size <= 0) { + return {}; + } + + QByteArray bytes(size, Qt::Uninitialized); + auto* generator = QRandomGenerator::system(); + for (qsizetype index = 0; index < size; ++index) { + bytes[index] = static_cast(generator->generate() & 0xFFU); + } + return bytes; +} + } // namespace messenger::protocol diff --git a/src/shared/message.h b/src/shared/message.h index 541ef65..db31040 100644 --- a/src/shared/message.h +++ b/src/shared/message.h @@ -3,20 +3,31 @@ #include #include +#include #include #include namespace messenger::protocol { -inline constexpr quint32 CurrentProtocolVersion = 1; +inline constexpr quint32 CurrentProtocolVersion = 2; inline constexpr quint16 DefaultPort = 4242; inline constexpr qint64 ReadBufferSize = 1024 * 1024; inline constexpr auto DataStreamVersion = QDataStream::Qt_6_5; +inline constexpr qsizetype AuthenticationIdSize = 16; +inline constexpr qsizetype AuthenticationNonceSize = 32; +inline constexpr int AuthenticationTimeoutMs = 30'000; +inline constexpr qsizetype MaximumUserNameSize = 64; +inline constexpr qsizetype MaximumMessageSize = 16 * 1024; enum class MessageType : quint32 { - ChatMessage = 1, - SystemMessage = 2, - ErrorMessage = 3, + AuthHello = 1, + AuthChallenge = 2, + AuthProof = 3, + AuthSuccess = 4, + AuthFailure = 5, + ChatMessage = 100, + SystemMessage = 101, + ErrorMessage = 102, }; struct Message { @@ -25,11 +36,23 @@ struct Message { QString senderName; QString text; QDateTime timestamp = QDateTime::currentDateTimeUtc(); + QByteArray authenticationId; + QByteArray clientNonce; + QByteArray serverNonce; + QByteArray signature; }; QDataStream& operator<<(QDataStream& out, const Message& message); QDataStream& operator>>(QDataStream& in, Message& message); +[[nodiscard]] QByteArray authenticationTranscript( + const QString& userName, + const QByteArray& authenticationId, + const QByteArray& clientNonce, + const QByteArray& serverNonce); + +[[nodiscard]] QByteArray generateSecureRandomBytes(qsizetype size); + } // namespace messenger::protocol #endif // MESSENGER_MESSAGE_H From e3c80808a4d2fee2723abdce8780ec2b8194b000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sun, 12 Jul 2026 16:35:30 +0200 Subject: [PATCH 29/41] Change networkManager start order --- src/client/client.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/client.cpp b/src/client/client.cpp index ecad322..c22d202 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -22,8 +22,8 @@ int client::run(QGuiApplication& app) { QCoreApplication::setOrganizationName(QStringLiteral("ParallelEngineering")); QCoreApplication::setApplicationName(QStringLiteral("Messenger")); - networkManager_ = std::make_unique(); connectionStore_ = std::make_unique(); + networkManager_ = std::make_unique(connectionStore_.get()); networkManager_->setUserName(connectionStore_->userName()); engine_ = std::make_unique(); @@ -33,15 +33,15 @@ int client::run(QGuiApplication& app) { if (engine_->rootObjects().isEmpty()) { engine_.reset(); - connectionStore_.reset(); networkManager_.reset(); + connectionStore_.reset(); return -1; } const auto exitCode = app.exec(); engine_.reset(); - connectionStore_.reset(); networkManager_.reset(); + connectionStore_.reset(); return exitCode; } From 384e822ed9a7914cb2e646143abe45c904b62f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sun, 12 Jul 2026 16:39:02 +0200 Subject: [PATCH 30/41] Handle new message types in db connector --- src/server/Storage/message_store.cpp | 51 +++++++++++++++++++++++++++- src/server/Storage/message_store.h | 10 ++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/server/Storage/message_store.cpp b/src/server/Storage/message_store.cpp index e2e89a0..902bf52 100644 --- a/src/server/Storage/message_store.cpp +++ b/src/server/Storage/message_store.cpp @@ -20,6 +20,7 @@ using messenger::protocol::CurrentProtocolVersion; using messenger::protocol::Message; +using messenger::protocol::MessageType; namespace { @@ -253,7 +254,27 @@ QList MessageStore::loadMessages() const { Message message; message.protocolVersion = CurrentProtocolVersion; message.senderName = query.value(QStringLiteral("username")).toString(); - message.messageType = query.value(QStringLiteral("message_type")).toUInt(); + const auto storedMessageType = query.value(QStringLiteral("message_type")).toUInt(); + switch (storedMessageType) { + case 1: // Protocol version 1 ChatMessage + message.messageType = static_cast(MessageType::ChatMessage); + break; + case 2: // Protocol version 1 SystemMessage + message.messageType = static_cast(MessageType::SystemMessage); + break; + case 3: // Protocol version 1 ErrorMessage + message.messageType = static_cast(MessageType::ErrorMessage); + break; + case static_cast(MessageType::ChatMessage): + case static_cast(MessageType::SystemMessage): + case static_cast(MessageType::ErrorMessage): + message.messageType = storedMessageType; + break; + default: + qWarning() << "Skipping stored message with unsupported type" + << storedMessageType; + continue; + } message.text = query.value(QStringLiteral("body")).toString(); message.timestamp = QDateTime::fromString(query.value(QStringLiteral("client_timestamp")).toString(), Qt::ISODateWithMs); @@ -270,6 +291,34 @@ bool MessageStore::hasUser(const QString& userName) const { return userIdForUserName(userName) != InvalidUserId; } +std::optional MessageStore::findUserForAuthentication( + const QString& userName) const { + if (!initialized_) { + qWarning() << "Cannot authenticate a user before message store initialization"; + return std::nullopt; + } + + QSqlQuery query(QSqlDatabase::database(connectionName_)); + query.prepare(QStringLiteral( + "SELECT id, username, public_key FROM users WHERE username = :username")); + query.bindValue(QStringLiteral(":username"), userName.trimmed()); + + if (!query.exec()) { + qWarning() << "Could not load user authentication data:" << lastErrorText(query); + return std::nullopt; + } + + if (!query.next()) { + return std::nullopt; + } + + return UserAuthenticationRecord{ + query.value(QStringLiteral("id")).toInt(), + query.value(QStringLiteral("username")).toString(), + query.value(QStringLiteral("public_key")).toByteArray(), + }; +} + int MessageStore::userIdForUserName(const QString& userName) const { if (!initialized_) { qWarning() << "Cannot look up user before message store initialization"; diff --git a/src/server/Storage/message_store.h b/src/server/Storage/message_store.h index f33f70e..9054326 100644 --- a/src/server/Storage/message_store.h +++ b/src/server/Storage/message_store.h @@ -7,6 +7,14 @@ #include #include +#include + +struct UserAuthenticationRecord { + int userId; + QString userName; + QByteArray publicKey; +}; + class MessageStore { public: MessageStore(); @@ -20,6 +28,8 @@ class MessageStore { bool initialize(); void close(); bool hasUser(const QString& userName) const; + [[nodiscard]] std::optional findUserForAuthentication( + const QString& userName) const; bool saveMessage(const messenger::protocol::Message& message); QList loadMessages() const; From cf8b243cee5e2352dbb953aa261f09cc227592db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sun, 12 Jul 2026 16:49:25 +0200 Subject: [PATCH 31/41] Implement authentication flow in NetworkManager --- src/client/network_manager.cpp | 238 ++++++++++++++++++++++++++++++--- src/client/network_manager.h | 38 +++++- 2 files changed, 259 insertions(+), 17 deletions(-) diff --git a/src/client/network_manager.cpp b/src/client/network_manager.cpp index cf723f1..b332f55 100644 --- a/src/client/network_manager.cpp +++ b/src/client/network_manager.cpp @@ -1,9 +1,17 @@ #include "network_manager.h" +#include "connection_store.h" +#include "signature.h" + #include +#include #include +#include #include +#include +#include + using messenger::protocol::CurrentProtocolVersion; using messenger::protocol::DataStreamVersion; using messenger::protocol::DefaultPort; @@ -11,8 +19,23 @@ using messenger::protocol::Message; using messenger::protocol::MessageType; using messenger::protocol::ReadBufferSize; -NetworkManager::NetworkManager(QObject* parent) +namespace { + +std::vector toByteVector(const QByteArray& bytes) { + const auto* begin = reinterpret_cast(bytes.constData()); + return {begin, begin + bytes.size()}; +} + +QByteArray toByteArray(const std::vector& bytes) { + return QByteArray(reinterpret_cast(bytes.data()), + static_cast(bytes.size())); +} + +} // namespace + +NetworkManager::NetworkManager(ConnectionStore* connectionStore, QObject* parent) : QObject(parent), + connectionStore_(connectionStore), stream_(&socket_), statusText_(tr("Disconnected")), userName_(QStringLiteral("anonymous")) { @@ -23,10 +46,21 @@ NetworkManager::NetworkManager(QObject* parent) connect(&socket_, &QTcpSocket::disconnected, this, &NetworkManager::handleDisconnected); connect(&socket_, &QTcpSocket::errorOccurred, this, &NetworkManager::handleError); connect(&socket_, &QTcpSocket::readyRead, this, &NetworkManager::readAvailable); + + authenticationTimer_.setSingleShot(true); + connect(&authenticationTimer_, &QTimer::timeout, this, [this]() { + if (!connected() && connectionState_ != ConnectionState::Disconnected) { + failAuthentication(tr("Authentication timed out.")); + } + }); } bool NetworkManager::connected() const { - return socket_.state() == QAbstractSocket::ConnectedState; + return connectionState_ == ConnectionState::Authenticated; +} + +bool NetworkManager::busy() const { + return connectionState_ != ConnectionState::Disconnected && !connected(); } QString NetworkManager::statusText() const { @@ -60,25 +94,44 @@ void NetworkManager::connectToServer(const QString& host, quint16 port) { return; } - const auto previousConnected = connected(); + if (userName_.trimmed().isEmpty()) { + const auto message = tr("User name must not be empty."); + setStatusText(message); + emit connectionError(message); + return; + } + + const auto* selectedKeyPair = connectionStore_ ? connectionStore_->currentKeyPair() : nullptr; + if (selectedKeyPair == nullptr) { + const auto message = tr("Select a valid RSA key pair before connecting."); + setStatusText(message); + emit connectionError(message); + return; + } + if (socket_.state() != QAbstractSocket::UnconnectedState) { socket_.abort(); } + clearAuthenticationData(); + authenticationKeyPair_ = *selectedKeyPair; + authenticationUserName_ = userName_.trimmed(); + preserveStatusOnDisconnect_ = false; + userRequestedDisconnect_ = false; + setConnectionState(ConnectionState::Connecting); setStatusText(tr("Connecting to %1:%2 ...").arg(trimmedHost).arg(port)); - emitConnectedIfChanged(previousConnected); socket_.connectToHost(trimmedHost, port); } void NetworkManager::disconnectFromServer() { - const auto previousConnected = connected(); - if (socket_.state() == QAbstractSocket::UnconnectedState) { + clearAuthenticationData(); + setConnectionState(ConnectionState::Disconnected); setStatusText(tr("Disconnected")); - emitConnectedIfChanged(previousConnected); return; } + userRequestedDisconnect_ = true; setStatusText(tr("Disconnecting ...")); socket_.disconnectFromHost(); } @@ -112,13 +165,26 @@ void NetworkManager::sendChatMessage(const QString& text) { } void NetworkManager::handleConnected() { - setStatusText(tr("Connected")); - emit connectedChanged(); + setConnectionState(ConnectionState::AwaitingChallenge); + setStatusText(tr("Connected to server. Starting authentication ...")); + authenticationTimer_.start(messenger::protocol::AuthenticationTimeoutMs); + sendAuthenticationHello(); } void NetworkManager::handleDisconnected() { - setStatusText(tr("Disconnected")); - emit connectedChanged(); + const auto wasAuthenticated = connected(); + const auto wasUserRequested = userRequestedDisconnect_; + clearAuthenticationData(); + setConnectionState(ConnectionState::Disconnected); + + if (wasAuthenticated || wasUserRequested || !preserveStatusOnDisconnect_) { + setStatusText(wasAuthenticated || wasUserRequested + ? tr("Disconnected") + : tr("Connection closed before authentication completed.")); + } + + preserveStatusOnDisconnect_ = false; + userRequestedDisconnect_ = false; } void NetworkManager::handleError() { @@ -126,7 +192,8 @@ void NetworkManager::handleError() { return; } - const auto message = socket_.errorString(); + const auto message = tr("Connection error: %1").arg(socket_.errorString()); + preserveStatusOnDisconnect_ = true; setStatusText(message); emit connectionError(message); } @@ -150,12 +217,54 @@ void NetworkManager::readAvailable() { return; } + const auto messageType = static_cast(message.messageType); + if (!connected()) { + if (messageType == MessageType::AuthChallenge) { + handleAuthenticationChallenge(message); + } else if (messageType == MessageType::AuthSuccess) { + handleAuthenticationSuccess(message); + } else if (messageType == MessageType::AuthFailure) { + failAuthentication(message.text.isEmpty() + ? tr("Authentication failed.") + : message.text); + } else { + failAuthentication(tr("Server sent an unexpected message during authentication.")); + } + continue; + } + + if (messageType != MessageType::ChatMessage + && messageType != MessageType::SystemMessage + && messageType != MessageType::ErrorMessage) { + const auto errorMessage = tr("Server sent an unexpected message."); + setStatusText(errorMessage); + emit connectionError(errorMessage); + socket_.disconnectFromHost(); + return; + } + emit messageReceived(message.senderName, message.text, message.timestamp.toLocalTime().toString(QStringLiteral("HH:mm"))); } } +void NetworkManager::setConnectionState(ConnectionState state) { + if (connectionState_ == state) { + return; + } + + const auto wasConnected = connected(); + const auto wasBusy = busy(); + connectionState_ = state; + if (wasConnected != connected()) { + emit connectedChanged(); + } + if (wasBusy != busy()) { + emit busyChanged(); + } +} + void NetworkManager::setStatusText(const QString& statusText) { if (statusText_ == statusText) { return; @@ -165,8 +274,107 @@ void NetworkManager::setStatusText(const QString& statusText) { emit statusTextChanged(); } -void NetworkManager::emitConnectedIfChanged(bool previousConnected) { - if (previousConnected != connected()) { - emit connectedChanged(); +void NetworkManager::sendAuthenticationHello() { + if (!authenticationKeyPair_ || authenticationUserName_.isEmpty()) { + failAuthentication(tr("No user or RSA key is available for authentication.")); + return; + } + + clientNonce_ = messenger::protocol::generateSecureRandomBytes( + messenger::protocol::AuthenticationNonceSize); + + Message hello; + hello.messageType = static_cast(MessageType::AuthHello); + hello.senderName = authenticationUserName_; + hello.clientNonce = clientNonce_; + + QDataStream out(&socket_); + out.setVersion(DataStreamVersion); + out << hello; + socket_.flush(); + setStatusText(tr("Authentication request sent. Waiting for server challenge ...")); +} + +void NetworkManager::handleAuthenticationChallenge(const Message& message) { + if (connectionState_ != ConnectionState::AwaitingChallenge + || message.authenticationId.size() != messenger::protocol::AuthenticationIdSize + || message.serverNonce.size() != messenger::protocol::AuthenticationNonceSize) { + failAuthentication(tr("The server sent an invalid authentication challenge.")); + return; + } + + authenticationId_ = message.authenticationId; + serverNonce_ = message.serverNonce; + setConnectionState(ConnectionState::SigningChallenge); + setStatusText(tr("Challenge received. Signing authentication proof ...")); + + QTimer::singleShot(0, this, [this]() { + if (connectionState_ != ConnectionState::SigningChallenge + || !authenticationKeyPair_) { + return; + } + + const auto transcript = messenger::protocol::authenticationTranscript( + authenticationUserName_, authenticationId_, clientNonce_, serverNonce_); + const auto digest = QCryptographicHash::hash(transcript, QCryptographicHash::Sha256); + const auto signature = core::signature::signDigest( + authenticationKeyPair_->getPrivateKey(), toByteVector(digest)); + if (signature.empty()) { + failAuthentication(tr("The RSA authentication proof could not be created.")); + return; + } + + Message proof; + proof.messageType = static_cast(MessageType::AuthProof); + proof.authenticationId = authenticationId_; + proof.signature = toByteArray(signature); + + QDataStream out(&socket_); + out.setVersion(DataStreamVersion); + out << proof; + socket_.flush(); + setConnectionState(ConnectionState::AwaitingAuthenticationResult); + setStatusText(tr("Authentication proof sent. Waiting for verification ...")); + }); +} + +void NetworkManager::handleAuthenticationSuccess(const Message& message) { + if (connectionState_ != ConnectionState::AwaitingAuthenticationResult) { + failAuthentication(tr("The server confirmed authentication unexpectedly.")); + return; + } + + authenticationTimer_.stop(); + if (!message.senderName.trimmed().isEmpty() + && userName_ != message.senderName.trimmed()) { + userName_ = message.senderName.trimmed(); + emit userNameChanged(); } + clearAuthenticationData(); + setStatusText(tr("Authenticated as %1.").arg(userName_)); + setConnectionState(ConnectionState::Authenticated); +} + +void NetworkManager::failAuthentication(const QString& message) { + if (connected()) { + return; + } + + authenticationTimer_.stop(); + preserveStatusOnDisconnect_ = true; + const auto visibleMessage = message.isEmpty() ? tr("Authentication failed.") : message; + setStatusText(visibleMessage); + emit connectionError(visibleMessage); + if (socket_.state() != QAbstractSocket::UnconnectedState) { + socket_.disconnectFromHost(); + } +} + +void NetworkManager::clearAuthenticationData() { + authenticationTimer_.stop(); + authenticationUserName_.clear(); + authenticationId_.clear(); + clientNonce_.clear(); + serverNonce_.clear(); + authenticationKeyPair_.reset(); } diff --git a/src/client/network_manager.h b/src/client/network_manager.h index 110e787..e11f8e1 100644 --- a/src/client/network_manager.h +++ b/src/client/network_manager.h @@ -6,18 +6,27 @@ #include #include #include +#include + +#include + +#include "keyPair.h" + +class ConnectionStore; class NetworkManager final : public QObject { Q_OBJECT Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged) + Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) Q_PROPERTY(QString statusText READ statusText NOTIFY statusTextChanged) Q_PROPERTY(QString userName READ userName WRITE setUserName NOTIFY userNameChanged) Q_PROPERTY(int defaultPort READ defaultPort CONSTANT) public: - explicit NetworkManager(QObject* parent = nullptr); + explicit NetworkManager(ConnectionStore* connectionStore, QObject* parent = nullptr); [[nodiscard]] bool connected() const; + [[nodiscard]] bool busy() const; [[nodiscard]] QString statusText() const; [[nodiscard]] QString userName() const; [[nodiscard]] int defaultPort() const; @@ -31,6 +40,7 @@ class NetworkManager final : public QObject { signals: // These methods are implemented in qt void connectedChanged(); + void busyChanged(); void statusTextChanged(); void userNameChanged(); void messageReceived(const QString& senderName, const QString& text, const QString& sentAt); @@ -43,13 +53,37 @@ class NetworkManager final : public QObject { void readAvailable(); private: + enum class ConnectionState { + Disconnected, + Connecting, + AwaitingChallenge, + SigningChallenge, + AwaitingAuthenticationResult, + Authenticated, + }; + + void setConnectionState(ConnectionState state); void setStatusText(const QString& statusText); - void emitConnectedIfChanged(bool previousConnected); + void sendAuthenticationHello(); + void handleAuthenticationChallenge(const messenger::protocol::Message& message); + void handleAuthenticationSuccess(const messenger::protocol::Message& message); + void failAuthentication(const QString& message); + void clearAuthenticationData(); + ConnectionStore* connectionStore_; QTcpSocket socket_; QDataStream stream_; + QTimer authenticationTimer_; + ConnectionState connectionState_ = ConnectionState::Disconnected; QString statusText_; QString userName_; + QString authenticationUserName_; + QByteArray authenticationId_; + QByteArray clientNonce_; + QByteArray serverNonce_; + std::optional authenticationKeyPair_; + bool preserveStatusOnDisconnect_ = false; + bool userRequestedDisconnect_ = false; }; #endif // MESSENGER_NETWORK_MANAGER_H From a27dc74bd98b0c24d70ce568f643b3e247b2fe17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sun, 12 Jul 2026 16:51:48 +0200 Subject: [PATCH 32/41] Implement authentication handling in Session class --- src/server/session.cpp | 100 +++++++++++++++++++++++++++++++++++++++-- src/server/session.h | 38 +++++++++++++++- 2 files changed, 132 insertions(+), 6 deletions(-) diff --git a/src/server/session.cpp b/src/server/session.cpp index 858a890..d39c2c0 100644 --- a/src/server/session.cpp +++ b/src/server/session.cpp @@ -8,7 +8,9 @@ using messenger::protocol::CurrentProtocolVersion; using messenger::protocol::DataStreamVersion; using messenger::protocol::Message; +using messenger::protocol::MessageType; using messenger::protocol::ReadBufferSize; +using messenger::protocol::AuthenticationTimeoutMs; Session::Session(QTcpSocket* socket, QObject* parent) : QObject(parent), socket_(socket), stream_(socket) { @@ -20,6 +22,17 @@ Session::Session(QTcpSocket* socket, QObject* parent) connect(socket_, &QTcpSocket::disconnected, this, &Session::handleDisconnected); connect(socket_, &QTcpSocket::errorOccurred, this, &Session::handleError); + authenticationDeadline_.setRemainingTime(AuthenticationTimeoutMs); + authenticationTimer_.setSingleShot(true); + authenticationTimer_.start(AuthenticationTimeoutMs); + connect(&authenticationTimer_, &QTimer::timeout, this, [this]() { + if (!isAuthenticated()) { + qWarning() << "Client authentication timed out for" + << socket_->peerAddress().toString(); + rejectAuthentication(QStringLiteral("Authentication timed out.")); + } + }); + qInfo() << "Client connected from" << socket_->peerAddress().toString() << socket_->peerPort(); } @@ -34,16 +47,95 @@ void Session::sendMessage(const Message& message) { socket_->flush(); } +void Session::beginAuthentication(int userId, + const QString& userName, + const PublicKey& publicKey, + const QByteArray& authenticationId, + const QByteArray& clientNonce, + const QByteArray& serverNonce) { + if (authenticationState_ != AuthenticationState::AwaitingHello) { + return; + } + + userId_ = userId; + userName_ = userName.trimmed(); + authenticationPublicKey_ = publicKey; + authenticationId_ = authenticationId; + clientNonce_ = clientNonce; + serverNonce_ = serverNonce; + authenticationState_ = AuthenticationState::AwaitingProof; +} + +void Session::completeAuthentication() { + if (authenticationState_ != AuthenticationState::AwaitingProof) { + return; + } + + authenticationState_ = AuthenticationState::Authenticated; + authenticationTimer_.stop(); + authenticationPublicKey_ = {}; + authenticationId_.clear(); + clientNonce_.clear(); + serverNonce_.clear(); +} + +void Session::rejectAuthentication(const QString& reason) { + if (authenticationState_ == AuthenticationState::Rejected) { + return; + } + + authenticationState_ = AuthenticationState::Rejected; + authenticationTimer_.stop(); + authenticationPublicKey_ = {}; + authenticationId_.clear(); + clientNonce_.clear(); + serverNonce_.clear(); + + Message failure; + failure.messageType = static_cast(MessageType::AuthFailure); + failure.text = reason; + sendMessage(failure); + socket_->disconnectFromHost(); +} + +void Session::disconnectFromHost() { + socket_->disconnectFromHost(); +} + +Session::AuthenticationState Session::authenticationState() const { + return authenticationState_; +} + +bool Session::isAuthenticated() const { + return authenticationState_ == AuthenticationState::Authenticated; +} + +bool Session::authenticationExpired() const { + return authenticationDeadline_.hasExpired(); +} + +int Session::userId() const { + return userId_; +} + QString Session::userName() const { return userName_; } -bool Session::hasUserName() const { - return !userName_.isEmpty(); +const PublicKey& Session::authenticationPublicKey() const { + return authenticationPublicKey_; } -void Session::setUserName(const QString& userName) { - userName_ = userName.trimmed(); +const QByteArray& Session::authenticationId() const { + return authenticationId_; +} + +const QByteArray& Session::clientNonce() const { + return clientNonce_; +} + +const QByteArray& Session::serverNonce() const { + return serverNonce_; } void Session::readAvailable() { diff --git a/src/server/session.h b/src/server/session.h index ddeff11..31aeb8b 100644 --- a/src/server/session.h +++ b/src/server/session.h @@ -2,9 +2,12 @@ #define MESSENGER_SESSION_H #include "message.h" +#include "keyPair.h" #include +#include #include +#include class QTcpSocket; @@ -12,12 +15,35 @@ class Session final : public QObject { Q_OBJECT public: + enum class AuthenticationState { + AwaitingHello, + AwaitingProof, + Authenticated, + Rejected, + }; + explicit Session(QTcpSocket* socket, QObject* parent = nullptr); void sendMessage(const messenger::protocol::Message& message); + void beginAuthentication(int userId, + const QString& userName, + const PublicKey& publicKey, + const QByteArray& authenticationId, + const QByteArray& clientNonce, + const QByteArray& serverNonce); + void completeAuthentication(); + void rejectAuthentication(const QString& reason = QStringLiteral("Authentication failed.")); + void disconnectFromHost(); + + [[nodiscard]] AuthenticationState authenticationState() const; + [[nodiscard]] bool isAuthenticated() const; + [[nodiscard]] bool authenticationExpired() const; + [[nodiscard]] int userId() const; [[nodiscard]] QString userName() const; - [[nodiscard]] bool hasUserName() const; - void setUserName(const QString& userName); + [[nodiscard]] const PublicKey& authenticationPublicKey() const; + [[nodiscard]] const QByteArray& authenticationId() const; + [[nodiscard]] const QByteArray& clientNonce() const; + [[nodiscard]] const QByteArray& serverNonce() const; signals: void messageReceived(const messenger::protocol::Message& message, Session* session); @@ -31,7 +57,15 @@ class Session final : public QObject { private: QTcpSocket* socket_; QDataStream stream_; + AuthenticationState authenticationState_ = AuthenticationState::AwaitingHello; + int userId_ = -1; QString userName_; + PublicKey authenticationPublicKey_; + QByteArray authenticationId_; + QByteArray clientNonce_; + QByteArray serverNonce_; + QDeadlineTimer authenticationDeadline_; + QTimer authenticationTimer_; }; #endif // MESSENGER_SESSION_H From 031e0ffaa1685b1dca09f14de2684171265853c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sun, 12 Jul 2026 17:08:15 +0200 Subject: [PATCH 33/41] Revise connection establishment and authentication protocol details in documentation --- docs/connection-establishment.md | 433 ++++--------------------------- 1 file changed, 57 insertions(+), 376 deletions(-) diff --git a/docs/connection-establishment.md b/docs/connection-establishment.md index 1920026..45485e0 100644 --- a/docs/connection-establishment.md +++ b/docs/connection-establishment.md @@ -2,47 +2,9 @@ ## Purpose -The Messenger server must authenticate every client immediately after the TCP connection is established. A client is authenticated by proving that it owns the private RSA key belonging to a public key stored for its user account in the server database. +Every client must authenticate immediately after establishing a TCP connection. Authentication proves that the client owns the private RSA key matching the public key stored for its user account in the server database. -The private key never leaves the client. The server stores only public keys. Initially, the database contains only the `admin` user and the admin public key loaded during server initialization. - -The authentication flow is inspired by SSH public-key authentication and deliberately kept small enough for this learning project. - -## Security goals - -The connection establishment must ensure that: - -- a TCP connection alone does not grant access to server functionality; -- a client must prove possession of the matching private RSA key; -- an authentication response cannot be reused for a later connection; -- the authenticated user identity is bound to the server-side session; -- the client cannot select or change its sender identity after authentication; -- chat history and other application data are sent only after authentication; -- failed, malformed, or timed-out authentication attempts are disconnected; -- private keys are never transmitted to or stored on the server. - -This design authenticates the client to the server. It does not, by itself, authenticate the server to the client or encrypt chat traffic. TLS or an SSH-like pinned server host key can be added separately for those properties. - -## Existing project state - -The project already provides the main building blocks: - -- `users` contains a unique username and a serialized RSA public key; -- the server imports the initial admin public key from `admin.public.rsa`; -- `MessageStore::hasUser()` can determine whether a username exists; -- `Session` represents one TCP connection; -- client and server exchange versioned messages through `QDataStream`; -- the RSA library provides key serialization, `BigInt`, and modular exponentiation; -- Qt provides SHA-256 through `QCryptographicHash`. - -The current server behavior is not yet authenticated: - -- chat history is sent as soon as a TCP client connects; -- the first chat message binds its self-declared sender name to the session; -- the client considers an established TCP socket to be fully connected; -- the existing RSA API supports encryption and decryption, but no signature API. - -These behaviors must be replaced by the flow below. +The private key remains on the client. The server stores only public keys. Initially, the database contains the `admin` user whose public key is loaded from `admin.public.rsa`. ## Protocol overview @@ -81,7 +43,7 @@ sequenceDiagram ## Authentication messages -Authentication messages are protocol messages, but they should have dedicated payload structures instead of overloading the chat `Message` fields. +Protocol version 2 defines the following message types: ```cpp enum class MessageType : quint32 { @@ -90,144 +52,62 @@ enum class MessageType : quint32 { AuthProof = 3, AuthSuccess = 4, AuthFailure = 5, - ChatMessage = 100, SystemMessage = 101, ErrorMessage = 102, }; ``` -Suggested payloads: - -```cpp -struct AuthHello { - QString username; - QByteArray clientNonce; // Exactly 32 bytes -}; +The handshake uses these fields: -struct AuthChallenge { - QByteArray authId; // Exactly 16 bytes - QByteArray serverNonce; // Exactly 32 bytes -}; +| Message | Content | +|---|---| +| `AuthHello` | Username and a 32-byte `clientNonce` | +| `AuthChallenge` | 16-byte `authId` and 32-byte `serverNonce` | +| `AuthProof` | Matching `authId` and RSA signature | +| `AuthSuccess` | Canonical authenticated username | +| `AuthFailure` | Generic authentication error | -struct AuthProof { - QByteArray authId; - QByteArray signature; -}; - -struct AuthResult { - bool success; - QString reason; // Generic, non-sensitive error text -}; -``` +All messages include the protocol version and message type. Field sizes and the expected message order are validated by both sides. -Every serialized protocol frame must continue to include `protocolVersion` and `messageType`. Payload lengths must be validated before allocating or processing data. +## Authentication proof -## Authentication transcript - -The signature must cover the full connection-specific context, not only the server nonce. Client and server independently create the same canonical byte sequence: +Client and server independently create the same canonical authentication transcript: ```text -domainSeparator = "MessengerAuth/v1" -protocolVersion = CurrentProtocolVersion -username = normalized username from AuthHello -authId = random authentication attempt ID -clientNonce = random client nonce -serverNonce = random server nonce +domainSeparator = "MessengerAuth/v1" +protocolVersion = CurrentProtocolVersion +username = normalized username +authId = random authentication attempt ID +clientNonce = random client nonce +serverNonce = random server nonce ``` -Conceptually: +The values are encoded in a fixed order with `QDataStream`. The client calculates the digest with Qt: ```text -authData = Encode( - domainSeparator, - protocolVersion, - username, - authId, - clientNonce, - serverNonce -) - -digest = SHA-256(authData) +digest = SHA-256(authenticationTranscript) ``` -`Encode` must be deterministic. It should use fixed field order, explicit length prefixes, UTF-8 for the username, and fixed-size integer encoding. Client and server must use the same `QDataStream` version and byte order. Concatenating ambiguous strings without lengths is not permitted. - -The domain separator prevents a signature produced for a different feature or protocol from being accepted as a Messenger login proof. - -## Using the existing RSA library - -### What can be reused - -The current RSA library can remain the basis of the implementation. The following parts are reusable: - -- `PublicKey` and `PrivateKey`; -- key serialization and deserialization; -- `operations::BigInt`; -- `operations::math::modPow()`; -- the existing 4096-bit key generation; -- operating-system-backed random generation already used during key generation. - -Qt's `QCryptographicHash` can provide SHA-256, so another library is not required for the initial learning implementation. - -### What must not be used unchanged - -The existing `core::encryptor::encrypt()` must not be used to encrypt the authentication challenge. It encrypts every plaintext byte independently and deterministically. An observer can encrypt all 256 possible byte values with the public key and use the resulting lookup table to recover the challenge without possessing the private key. - -Consequently, the following flow is not acceptable: +The RSA library performs the signature operation using its existing `BigInt` and `modPow` implementation: ```text -server encrypts a string challenge with encrypt() -client decrypts it with decrypt() -client returns the plaintext challenge -``` - -### Minimal signature extension - -For the initial learning version, add a small digest-signing API to the RSA library: - -```cpp -namespace core::signature { - -std::vector signDigest( - const PrivateKey& privateKey, - const std::vector& digest); - -bool verifyDigest( - const PublicKey& publicKey, - const std::vector& digest, - const std::vector& signature); - -} // namespace core::signature -``` - -The minimal implementation uses the existing modular exponentiation: - -```text -digestInteger = OS2IP(SHA-256(authData)) +digestInteger = BigInt(digest) signature = digestInteger^d mod n -verifiedValue = signature^e mod n -valid = verifiedValue == digestInteger ``` -`OS2IP` converts the 32-byte digest to one unsigned integer using a documented byte order. Signatures should be serialized to the fixed byte length of the RSA modulus so that leading zero bytes are preserved. Verification must reject signatures that are empty, incorrectly sized, or greater than or equal to the modulus. - -This direct hash-and-RSA construction demonstrates private-key possession and is suitable only for the project's first educational implementation. It is not equivalent to a standardized SSH signature scheme. - -### Planned security upgrade - -The signature API should later implement RSA-PSS with SHA-256 and MGF1: +The server loads the user's public key from the database and verifies: ```text -signature = RSA-PSS-Sign(privateKey, SHA-256(authData)) -valid = RSA-PSS-Verify(publicKey, SHA-256(authData), signature) +verifiedDigest = signature^e mod n +valid = verifiedDigest == digestInteger ``` -The network handshake and message structures do not need to change when the internal implementation moves from the learning scheme to RSA-PSS. A future protocol version or signature-algorithm field should identify the selected algorithm if both variants are supported at the same time. +The signature has the fixed byte length of the RSA modulus. Empty, incorrectly sized, or out-of-range signatures are rejected. -## Server-side session state +## Session states -`Session` must explicitly track authentication state: +Each server-side connection has an authentication state: ```cpp enum class AuthenticationState { @@ -238,267 +118,68 @@ enum class AuthenticationState { }; ``` -Suggested session data: - -```cpp -AuthenticationState authenticationState_; -int authenticatedUserId_; -QString authenticatedUserName_; -PublicKey authenticationPublicKey_; -QByteArray authId_; -QByteArray clientNonce_; -QByteArray serverNonce_; -QDeadlineTimer authenticationDeadline_; -``` - -Only one message type is valid in each unauthenticated state: - -| Session state | Accepted client message | All other messages | +| Session state | Accepted client message | Other messages | |---|---|---| | `AwaitingHello` | `AuthHello` | Reject and disconnect | | `AwaitingProof` | `AuthProof` | Reject and disconnect | -| `Authenticated` | Authorized application messages | Handle normally | +| `Authenticated` | Authorized application messages | Reject invalid messages | | `Rejected` | None | Disconnect | -An authentication attempt should expire after a short fixed interval, for example 10 seconds. Each challenge is valid only for its originating `Session`, only for its matching `authId`, and only once. Challenge data must be cleared after success, failure, timeout, or disconnect. - -## Detailed server flow +Authentication must finish within 30 seconds. The challenge belongs to one TCP session, matches one `authId`, and can be used only once. Temporary authentication data is cleared after success, failure, timeout, or disconnect. -### 1. Accept TCP connection +## Server behavior -Create a `Session` in `AwaitingHello`. Do not send chat history and do not expose other application functionality. +After accepting a TCP connection, the server waits for `AuthHello` without sending chat history or other application data. -### 2. Process `AuthHello` +For `AuthHello`, the server: -The server: +1. validates the username and client nonce; +2. loads the user ID, canonical username, and public key from the database; +3. generates a random authentication ID and server nonce; +4. stores the authentication context in the session; +5. sends `AuthChallenge`. -1. verifies the protocol version and message shape; -2. normalizes and validates the username; -3. verifies that `clientNonce` is exactly 32 bytes; -4. loads the user ID, canonical username, and serialized public key from the database; -5. validates and deserializes the public key; -6. creates a cryptographically random 16-byte `authId`; -7. creates a cryptographically random 32-byte `serverNonce`; -8. stores all authentication context in the `Session`; -9. changes the state to `AwaitingProof`; -10. sends `AuthChallenge`. +For `AuthProof`, the server reconstructs the transcript, calculates its SHA-256 digest, and verifies the signature with the stored public key. On success, it binds the user ID and username to the session, sends `AuthSuccess`, and then sends the chat history. On failure, it sends `AuthFailure` and closes the connection. -Unknown users should receive the same generic failure behavior as invalid signatures so that authentication errors reveal as little account information as practical. - -### 3. Process `AuthProof` - -The server: - -1. verifies that the session is in `AwaitingProof`; -2. verifies that the attempt has not expired; -3. compares the received `authId` with the session value; -4. reconstructs the canonical authentication transcript; -5. calculates its SHA-256 digest; -6. verifies the signature using the public key loaded for this session; -7. invalidates the challenge regardless of the result. - -On success, the server stores the database user ID and canonical username in the session, changes its state to `Authenticated`, sends `AuthSuccess`, and then sends the chat history. - -On failure, the server sends a generic `AuthFailure`, marks the session as `Rejected`, and closes the socket. It must not allow a second proof attempt for the same challenge. - -## Authorization after authentication - -Authentication establishes the session identity. Every server action must still begin with an authorization check: - -```cpp -if (!session->isAuthenticated()) { - session->disconnectFromHost(); - return; -} -``` - -For incoming chat messages, the server must ignore any sender name supplied by the client and set it from the session: +Only authenticated sessions may send or receive chat messages. The server sets the sender name and timestamp itself: ```cpp Message verifiedMessage = incomingMessage; -verifiedMessage.senderName = session->authenticatedUserName(); +verifiedMessage.senderName = session->userName(); verifiedMessage.timestamp = QDateTime::currentDateTimeUtc(); ``` -Preferably, client chat payloads should no longer contain `senderName` at all. The server is the authority for both sender identity and the timestamp used for ordering and storage. - -Broadcasting must also target only authenticated sessions. An unauthenticated TCP connection must not receive chat messages, history, user data, or administrative responses. +Chat history and broadcasts are sent only to authenticated sessions. -## Client state +## Client behavior -The client must distinguish TCP connectivity from successful authentication: +The client distinguishes an established TCP socket from an authenticated connection: ```cpp enum class ConnectionState { Disconnected, Connecting, - Authenticating, + AwaitingChallenge, + SigningChallenge, + AwaitingAuthenticationResult, Authenticated, - Disconnecting, -}; -``` - -`QTcpSocket::connected` changes the state to `Authenticating`, not `Authenticated`. Chat controls remain disabled until `AuthSuccess` is received. Only the authenticated state should be exposed to the existing UI as ready for messaging. - -The client flow is: - -1. load and validate its private key before connecting; -2. establish the TCP connection; -3. generate a fresh 32-byte `clientNonce`; -4. send `AuthHello`; -5. validate `AuthChallenge` and its field sizes; -6. construct the canonical transcript and calculate SHA-256; -7. sign the digest with the private key; -8. send `AuthProof`; -9. clear temporary authentication data; -10. enable messaging only after `AuthSuccess`. - -Failure to load the private key must prevent connection authentication and produce a clear local error. The private key must never be logged or included in an error message. - -## Database access - -The existing schema is sufficient for the initial implementation: - -```sql -CREATE TABLE users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT NOT NULL UNIQUE, - public_key BLOB NOT NULL, - created_at TEXT NOT NULL -); -``` - -`MessageStore` needs a lookup that returns the user record and public key rather than only a Boolean: - -```cpp -struct UserAuthenticationRecord { - int userId; - QString username; - QByteArray publicKey; }; - -std::optional -findUserForAuthentication(const QString& username) const; ``` -The admin bootstrap remains unchanged: the server validates `admin.public.rsa` and stores it for the initial `admin` account. Future user-management functionality can allow an authenticated and authorized admin to add more usernames and public keys. - -## Randomness and replay protection - -Both nonces and `authId` must come from a cryptographically secure operating-system random source. `std::rand()`, timestamps, counters, and regular pseudo-random generators are not sufficient. - -Replay protection is provided by: - -- a new `clientNonce` for every client attempt; -- a new `serverNonce` and `authId` for every server session; -- binding all values to the signature; -- accepting each challenge only once; -- applying a short authentication deadline; -- keeping challenge state inside the originating session. - -A previously recorded `AuthProof` therefore cannot authenticate a new connection because its transcript will contain different nonces and a different `authId`. - -## Failure handling - -The server should close the connection for: - -- an unsupported protocol version; -- an unexpected message type for the current state; -- malformed or oversized fields; -- an empty or invalid username; -- an unknown user; -- a missing or invalid public key; -- an incorrect `authId`; -- an expired challenge; -- an invalid signature; -- more than one authentication attempt on the same session. - -External errors should stay generic, for example `Authentication failed`. Detailed reasons may be logged on the server, but logs must not include private material, full authentication payloads, or reusable secrets. Repeated failures should eventually be rate-limited by source address and username. - -## Required code changes - -### Shared protocol - -- add authentication message types and payload serialization; -- define strict size limits and canonical transcript encoding; -- increment `CurrentProtocolVersion` because the connection behavior changes incompatibly. - -### RSA library - -- add `signDigest(PrivateKey, digest)`; -- add `verifyDigest(PublicKey, digest, signature)`; -- add fixed-width signature serialization and strict input validation; -- add unit tests for valid, invalid, modified, and wrong-key signatures; -- later replace the minimal implementation with RSA-PSS. - -### Server storage - -- add `findUserForAuthentication()`; -- return the database user ID, canonical username, and public key; -- keep the existing admin public-key bootstrap. - -### Server session - -- add the authentication state and temporary challenge data; -- add the authentication deadline; -- expose authenticated user ID and username only after success; -- reject unexpected pre-authentication traffic. - -### Server - -- stop sending history from `handleNewConnection()`; -- handle `AuthHello` and `AuthProof` before application messages; -- send history only after successful authentication; -- remove the self-declared username binding from the first chat message; -- derive all message identities from the authenticated session; -- broadcast only to authenticated sessions. - -### Client - -- load the private key from a configured local file; -- implement connection and authentication states; -- send `AuthHello` after the socket connects; -- sign `AuthChallenge` and send `AuthProof`; -- keep chat input disabled until `AuthSuccess`; -- clear temporary authentication values after completion. - -## Verification plan - -At minimum, automated tests should cover: +After the TCP socket connects, the client generates `clientNonce` and sends `AuthHello`. After receiving `AuthChallenge`, it creates and signs the authentication digest and sends `AuthProof`. -1. the admin authenticates with the matching private key; -2. authentication fails with a different private key; -3. authentication fails for an unknown username; -4. changing the username, nonce, `authId`, or signature causes failure; -5. replaying a proof in a new connection causes failure; -6. replaying a proof twice in one connection causes disconnection; -7. an expired challenge causes failure; -8. chat messages before authentication cause disconnection; -9. unauthenticated sessions receive neither history nor broadcasts; -10. the server replaces or ignores a client-provided sender name; -11. malformed and oversized authentication messages are rejected; -12. the client UI enables messaging only after `AuthSuccess`; -13. signature serialization preserves leading zero bytes; -14. signing with one key and verification with another key fails. +The chat UI remains disabled until `AuthSuccess` is received. The existing status display reports the current step, including connection, challenge processing, signature creation, verification, success, timeout, and failure. -## Implementation order +## Replay protection and failure handling -1. Add and test digest signing and verification in the RSA library. -2. Add authentication protocol payloads and canonical transcript encoding. -3. Add the public-key lookup to `MessageStore`. -4. Add the authentication state machine to `Session`. -5. Implement the server-side handshake and move history delivery after authentication. -6. Implement private-key loading and the client-side handshake. -7. Remove trust in client-provided sender names. -8. Add integration tests for success, failure, replay, timeout, and pre-authentication access. -9. Upgrade the RSA signature implementation to RSA-PSS before treating the protocol as production security. +Every authentication attempt uses a new client nonce, server nonce, and authentication ID. All three values are covered by the signature, so a recorded `AuthProof` cannot authenticate another connection. -## Final connection rule +The server closes the connection for malformed messages, unexpected message types, unknown users, invalid keys, an incorrect authentication ID, timeout, or an invalid signature. External errors remain generic and private key material is never logged or transmitted. -The central rule is: +## Connection rule ```text TCP connected != authenticated ``` -Until the RSA proof has been verified, the connection may process only the authentication handshake. After successful verification, the server binds the database user to the session and permits authorized application messages. Any deviation from the expected handshake closes the connection. +Before RSA verification succeeds, the connection may process only authentication messages. After verification, the database user is bound to the session and application messages are permitted. From 6aa83230f3ac826c2703217bb1782d21a657c008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sun, 12 Jul 2026 17:13:52 +0200 Subject: [PATCH 34/41] Translate error messages and UI text from German to English --- src/client/Main.qml | 35 ++++++++++++++++++--------------- src/client/connection_store.cpp | 34 ++++++++++++++++---------------- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/src/client/Main.qml b/src/client/Main.qml index 6f8a557..c8f99a1 100644 --- a/src/client/Main.qml +++ b/src/client/Main.qml @@ -261,7 +261,7 @@ Window { spacing: 8 FieldLabel { - text: qsTr("RSA-Schlüssel") + text: qsTr("RSA Key") } Row { @@ -418,8 +418,8 @@ Window { ActionButton { width: parent.width - text: qsTr("Verbinden") - enabledState: connectionInputValid + text: networkManager.busy ? qsTr("Authenticating ...") : qsTr("Connect") + enabledState: connectionInputValid && !networkManager.busy onClicked: connectWithInput() } } @@ -531,7 +531,7 @@ Window { Text { width: Math.max(120, parent.width - closeKeyManagementButton.width - parent.spacing) - text: qsTr("RSA-Schlüssel") + text: qsTr("RSA Keys") color: "#1f2933" font.pixelSize: 22 verticalAlignment: Text.AlignVCenter @@ -566,7 +566,7 @@ Window { Text { anchors.centerIn: parent width: parent.width - text: qsTr("Noch keine RSA-Schlüssel vorhanden.") + text: qsTr("No RSA keys available yet.") color: "#607080" font.pixelSize: 14 horizontalAlignment: Text.AlignHCenter @@ -619,7 +619,7 @@ Window { Text { anchors.centerIn: parent - text: qsTr("Löschen") + text: qsTr("Delete") color: "#7a2830" font.pixelSize: 13 } @@ -652,7 +652,7 @@ Window { visible: !connectionStore.keyGenerationInProgress FieldLabel { - text: qsTr("Neuer Schlüssel") + text: qsTr("New Key") } Row { @@ -668,7 +668,7 @@ Window { ActionButton { id: createManagedKeyButton width: 112 - text: qsTr("Erstellen") + text: qsTr("Create") enabledState: newKeyNameInput.text.trim().length > 0 onClicked: keyManagementPopup.submit() } @@ -683,7 +683,7 @@ Window { FieldLabel { width: parent.width - text: qsTr("Schlüsselerzeugung...") + text: qsTr("Generating key ...") elide: Text.ElideRight } @@ -734,7 +734,7 @@ Window { id: cancelKeyGenerationButton anchors.right: parent.right width: 112 - text: qsTr("Abbrechen") + text: qsTr("Cancel") normalColor: "#ffffff" hoverColor: "#f7f8fa" borderColor: "#c8d0d9" @@ -846,7 +846,7 @@ Window { Text { width: parent.width - text: qsTr("Schlüssel löschen") + text: qsTr("Delete Key") color: "#1f2933" font.pixelSize: 22 elide: Text.ElideRight @@ -854,7 +854,7 @@ Window { Text { width: parent.width - text: qsTr("Der RSA-Schlüssel \"%1\" kann nicht wiederhergestellt werden. Eine Verbindung zum Server unter diesem Benutzer ist anschließend nicht mehr möglich.").arg(pendingDeleteKeyName) + text: qsTr("The RSA key \"%1\" cannot be recovered. After deletion, connecting to the server as this user will no longer be possible.").arg(pendingDeleteKeyName) color: "#43515f" font.pixelSize: 14 wrapMode: Text.Wrap @@ -881,7 +881,7 @@ Window { QuietButton { width: Math.max(120, (parent.width - parent.spacing) / 2) - text: qsTr("Abbrechen") + text: qsTr("Cancel") normalColor: "#ffffff" hoverColor: "#f7f8fa" borderColor: "#c8d0d9" @@ -891,7 +891,7 @@ Window { QuietButton { width: Math.max(120, (parent.width - parent.spacing) / 2) - text: qsTr("Löschen") + text: qsTr("Delete") normalColor: "#f3e7e8" hoverColor: "#ead8da" borderColor: "#ddb9bd" @@ -999,7 +999,7 @@ Window { Text { anchors.centerIn: parent - text: qsTr("Noch keine Nachrichten") + text: qsTr("No messages yet") color: "#7b8794" font.pixelSize: 15 visible: chatMessages.count === 0 @@ -1104,7 +1104,7 @@ Window { ActionButton { id: sendButton width: 140 - text: qsTr("Senden") + text: qsTr("Send") enabledState: networkManager.connected && messageInput.text.length > 0 onClicked: { networkManager.sendChatMessage(messageInput.text) @@ -1137,6 +1137,9 @@ Window { } function onConnectionError(message) { + if (!networkManager.connected) { + return + } chatMessages.append({ "senderName": qsTr("System"), "messageText": message, diff --git a/src/client/connection_store.cpp b/src/client/connection_store.cpp index c9b7fdb..481ca97 100644 --- a/src/client/connection_store.cpp +++ b/src/client/connection_store.cpp @@ -147,17 +147,17 @@ bool ConnectionStore::startKeyPairCreation(const QString& name) { setErrorText({}); if (!keyNameIsValid(keyName)) { - setErrorText(tr("Der Schlüsselname ist ungültig.")); + setErrorText(tr("The key name is invalid.")); return false; } if (!ensureKeyDirectory()) { - setErrorText(tr("Der Schlüsselordner konnte nicht erstellt werden.")); + setErrorText(tr("The key directory could not be created.")); return false; } if (keyExists(keyName)) { - setErrorText(tr("Ein Schlüssel mit diesem Namen existiert bereits.")); + setErrorText(tr("A key with this name already exists.")); return false; } @@ -211,13 +211,13 @@ void ConnectionStore::finishKeyPairCreation(const QString& keyName, const QByteA if (!discarded && workerError.isEmpty()) { if (keyExists(keyName)) { - setErrorText(tr("Ein Schlüssel mit diesem Namen existiert bereits.")); + setErrorText(tr("A key with this name already exists.")); } else { QFile publicKeyFile(publicKeyPath(keyName)); if (!publicKeyFile.open(QIODevice::WriteOnly | QIODevice::NewOnly) || publicKeyFile.write(publicKeyBytes) != publicKeyBytes.size()) { publicKeyFile.remove(); - setErrorText(tr("Der öffentliche Schlüssel konnte nicht gespeichert werden.")); + setErrorText(tr("The public key could not be saved.")); } else { publicKeyFile.close(); QFile privateKeyFile(privateKeyPath(keyName)); @@ -225,7 +225,7 @@ void ConnectionStore::finishKeyPairCreation(const QString& keyName, const QByteA privateKeyFile.write(privateKeyBytes) != privateKeyBytes.size()) { privateKeyFile.remove(); QFile::remove(publicKeyPath(keyName)); - setErrorText(tr("Der private Schlüssel konnte nicht gespeichert werden.")); + setErrorText(tr("The private key could not be saved.")); } else { privateKeyFile.close(); refreshAvailableKeyNames(); @@ -234,7 +234,7 @@ void ConnectionStore::finishKeyPairCreation(const QString& keyName, const QByteA } } } else if (!discarded) { - setErrorText(tr("Das Schlüsselpaar konnte nicht erstellt werden: %1").arg(workerError)); + setErrorText(tr("The key pair could not be created: %1").arg(workerError)); } if (discardGeneratedKey_ == discardGeneratedKey) { @@ -249,19 +249,19 @@ bool ConnectionStore::deleteKeyPair(const QString& name) { setErrorText({}); if (keyGenerationInProgress_) { - setErrorText(tr("Während der Schlüsselerstellung kann kein Schlüssel gelöscht werden.")); + setErrorText(tr("Keys cannot be deleted while a key is being generated.")); return false; } if (!availableKeyNames_.contains(keyName)) { - setErrorText(tr("Der Schlüssel wurde nicht gefunden.")); + setErrorText(tr("The key was not found.")); return false; } const auto publicRemoved = QFile::remove(publicKeyPath(keyName)); const auto privateRemoved = QFile::remove(privateKeyPath(keyName)); if (!publicRemoved || !privateRemoved) { - setErrorText(tr("Der Schlüssel konnte nicht vollständig gelöscht werden.")); + setErrorText(tr("The key could not be deleted completely.")); refreshAvailableKeyNames(); clearInvalidSelectedKey(); return false; @@ -290,7 +290,7 @@ bool ConnectionStore::saveLastConnection(const QString& host, if (trimmedHost.isEmpty() || trimmedUserName.isEmpty() || port < 1 || port > 65535 || !availableKeyNames_.contains(trimmedKeyName)) { - setErrorText(tr("Die Verbindungsdaten sind unvollständig.")); + setErrorText(tr("The connection details are incomplete.")); return false; } @@ -303,7 +303,7 @@ bool ConnectionStore::saveLastConnection(const QString& host, const auto settingsFileInfo = QFileInfo(settingsFilePath()); if (!settingsFileInfo.absoluteDir().exists() && !QDir().mkpath(settingsFileInfo.absolutePath())) { - setErrorText(tr("Der Speicherordner konnte nicht erstellt werden.")); + setErrorText(tr("The storage directory could not be created.")); return false; } @@ -315,13 +315,13 @@ bool ConnectionStore::saveLastConnection(const QString& host, QSaveFile settingsFile(settingsFilePath()); if (!settingsFile.open(QIODevice::WriteOnly)) { - setErrorText(tr("Die letzten Verbindungsdaten konnten nicht gespeichert werden.")); + setErrorText(tr("The most recent connection details could not be saved.")); return false; } settingsFile.write(QJsonDocument(root).toJson(QJsonDocument::Indented)); if (!settingsFile.commit()) { - setErrorText(tr("Die letzten Verbindungsdaten konnten nicht geschrieben werden.")); + setErrorText(tr("The most recent connection details could not be written.")); return false; } @@ -380,14 +380,14 @@ QString ConnectionStore::privateKeyPath(const QString& keyName) const { std::optional ConnectionStore::loadKeyPair(const QString& keyName) { QFile publicKeyFile(publicKeyPath(keyName)); if (!publicKeyFile.open(QIODevice::ReadOnly)) { - setErrorText(tr("Der öffentliche Schlüssel konnte nicht eingelesen werden.")); + setErrorText(tr("The public key could not be read.")); return std::nullopt; } const auto publicKeyBytes = toByteVector(publicKeyFile.readAll()); QFile privateKeyFile(privateKeyPath(keyName)); if (!privateKeyFile.open(QIODevice::ReadOnly)) { - setErrorText(tr("Der private Schlüssel konnte nicht eingelesen werden.")); + setErrorText(tr("The private key could not be read.")); return std::nullopt; } const auto privateKeyBytes = toByteVector(privateKeyFile.readAll()); @@ -395,7 +395,7 @@ std::optional ConnectionStore::loadKeyPair(const QString& keyName) { try { return keyPair::create(publicKeyBytes, privateKeyBytes); } catch (const std::exception&) { - setErrorText(tr("Das Schlüsselpaar konnte nicht deserialisiert werden.")); + setErrorText(tr("The key pair could not be deserialized.")); return std::nullopt; } } From bca5fe517a61924dbfef5c8a53a2259462ab33d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sun, 12 Jul 2026 18:29:29 +0200 Subject: [PATCH 35/41] Implement user registration logic --- README.md | 16 +- docs/connection-establishment.md | 27 +- src/client/network_manager.cpp | 7 + .../002_create_registration_requests.sql | 14 + src/server/Storage/message_store.cpp | 276 ++++++++++++------ src/server/Storage/message_store.h | 28 +- src/server/server.cpp | 104 ++++++- src/server/session.cpp | 8 +- src/server/session.h | 5 +- src/shared/message.cpp | 2 + src/shared/message.h | 5 +- 11 files changed, 383 insertions(+), 109 deletions(-) create mode 100644 src/server/Migrations/002_create_registration_requests.sql diff --git a/README.md b/README.md index 1ccccc7..2584fec 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Messenger is a simple chat program with a client and a server. Users can connect ## Functionality - **Client connection:** enter a server host, connect to the server, disconnect again when terminating the program, and see the current connection status in the UI. -- **Group chat authentication:** each server represents one group chat; user accounts are created on the server with a name and a public key, without passwords. Clients authenticate through RSA-based verification using the public key stored for their account. +- **Group chat authentication:** each server represents one group chat; clients request access with a name and their public key, without passwords. After server-side approval, they authenticate through RSA-based verification using the stored public key. - **Message sending:** compose text messages and send them to the connected server. - **Message receiving:** display incoming messages in the client window as they arrive from the server with timestamp and username. - **Group chat forwarding:** the server keeps track of the clients connected to its group chat and broadcasts chat messages to every active session. @@ -69,3 +69,17 @@ cmake --build cmake-build-debug ``` This creates the `Messenger-Client` and `Messenger-Server` targets. + +## User registration + +When a client connects with an unknown username, the server stores a pending registration request and the client is told to try again after an administrator has reviewed it. The private key always remains on the client. + +Use the server executable from a terminal to review requests in the same database: + +```bash +Messenger-Server requests +Messenger-Server approve 1 +Messenger-Server reject 2 +``` + +After approval, the client connects again manually and uses the normal RSA authentication flow. No administrator account or manually installed public-key file is required. diff --git a/docs/connection-establishment.md b/docs/connection-establishment.md index 45485e0..817ee5f 100644 --- a/docs/connection-establishment.md +++ b/docs/connection-establishment.md @@ -4,7 +4,7 @@ Every client must authenticate immediately after establishing a TCP connection. Authentication proves that the client owns the private RSA key matching the public key stored for its user account in the server database. -The private key remains on the client. The server stores only public keys. Initially, the database contains the `admin` user whose public key is loaded from `admin.public.rsa`. +The private key remains on the client. The server stores only public keys. There is no predefined administrator account: an unknown username creates a pending registration request that is approved or rejected with the server command-line interface. ## Protocol overview @@ -19,6 +19,7 @@ sequenceDiagram C->>S: AuthHello(username, clientNonce) S->>DB: Load user ID and public key + alt User exists DB-->>S: User ID and public key S->>S: Generate authId and serverNonce S-->>C: AuthChallenge(authId, serverNonce) @@ -39,11 +40,17 @@ sequenceDiagram S->>S: Session state: Rejected S->>S: Close connection end + else User is unknown + C->>S: Public key included in AuthHello + S->>DB: Store pending registration request + S-->>C: RegistrationPending + S->>S: Close connection + end ``` ## Authentication messages -Protocol version 2 defines the following message types: +Protocol version 3 defines the following message types: ```cpp enum class MessageType : quint32 { @@ -52,6 +59,8 @@ enum class MessageType : quint32 { AuthProof = 3, AuthSuccess = 4, AuthFailure = 5, + RegistrationPending = 6, + RegistrationRejected = 7, ChatMessage = 100, SystemMessage = 101, ErrorMessage = 102, @@ -62,11 +71,13 @@ The handshake uses these fields: | Message | Content | |---|---| -| `AuthHello` | Username and a 32-byte `clientNonce` | +| `AuthHello` | Username, a 32-byte `clientNonce`, and the client's public key | | `AuthChallenge` | 16-byte `authId` and 32-byte `serverNonce` | | `AuthProof` | Matching `authId` and RSA signature | | `AuthSuccess` | Canonical authenticated username | | `AuthFailure` | Generic authentication error | +| `RegistrationPending` | The unknown user's access request is waiting for server approval | +| `RegistrationRejected` | The matching access request was rejected | All messages include the protocol version and message type. Field sizes and the expected message order are validated by both sides. @@ -139,7 +150,9 @@ For `AuthHello`, the server: 4. stores the authentication context in the session; 5. sends `AuthChallenge`. -For `AuthProof`, the server reconstructs the transcript, calculates its SHA-256 digest, and verifies the signature with the stored public key. On success, it binds the user ID and username to the session, sends `AuthSuccess`, and then sends the chat history. On failure, it sends `AuthFailure` and closes the connection. +If the username does not exist, the server validates the public key from `AuthHello`, stores an idempotent pending request for that username and key, sends `RegistrationPending`, and closes the connection. A rejected matching request produces `RegistrationRejected`. The administrator reviews requests with `Messenger-Server requests` and decides with `Messenger-Server approve ` or `Messenger-Server reject `. + +For a known user and `AuthProof`, the server reconstructs the transcript, calculates its SHA-256 digest, and verifies the signature with the stored public key. On success, it binds the user ID and username to the session, sends `AuthSuccess`, and then sends the chat history. On failure, it sends `AuthFailure` and closes the connection. Only authenticated sessions may send or receive chat messages. The server sets the sender name and timestamp itself: @@ -166,7 +179,9 @@ enum class ConnectionState { }; ``` -After the TCP socket connects, the client generates `clientNonce` and sends `AuthHello`. After receiving `AuthChallenge`, it creates and signs the authentication digest and sends `AuthProof`. +After the TCP socket connects, the client generates `clientNonce` and sends `AuthHello`, including the selected public key. After receiving `AuthChallenge`, it creates and signs the authentication digest and sends `AuthProof`. + +For `RegistrationPending` or `RegistrationRejected`, the client shows the corresponding access status and disconnects. It does not reconnect automatically; after an approval the user simply tries to connect again. The chat UI remains disabled until `AuthSuccess` is received. The existing status display reports the current step, including connection, challenge processing, signature creation, verification, success, timeout, and failure. @@ -174,7 +189,7 @@ The chat UI remains disabled until `AuthSuccess` is received. The existing statu Every authentication attempt uses a new client nonce, server nonce, and authentication ID. All three values are covered by the signature, so a recorded `AuthProof` cannot authenticate another connection. -The server closes the connection for malformed messages, unexpected message types, unknown users, invalid keys, an incorrect authentication ID, timeout, or an invalid signature. External errors remain generic and private key material is never logged or transmitted. +The server closes the connection for malformed messages, unexpected message types, invalid keys, an incorrect authentication ID, timeout, or an invalid signature. Unknown users receive a pending or rejected registration result. Private key material is never logged or transmitted. ## Connection rule diff --git a/src/client/network_manager.cpp b/src/client/network_manager.cpp index b332f55..c85c9e2 100644 --- a/src/client/network_manager.cpp +++ b/src/client/network_manager.cpp @@ -227,6 +227,12 @@ void NetworkManager::readAvailable() { failAuthentication(message.text.isEmpty() ? tr("Authentication failed.") : message.text); + } else if (messageType == MessageType::RegistrationPending) { + failAuthentication(tr( + "You do not have access yet. A registration request was created and must " + "be approved on the server. Please try again afterwards.")); + } else if (messageType == MessageType::RegistrationRejected) { + failAuthentication(tr("Your registration request was rejected on the server.")); } else { failAuthentication(tr("Server sent an unexpected message during authentication.")); } @@ -287,6 +293,7 @@ void NetworkManager::sendAuthenticationHello() { hello.messageType = static_cast(MessageType::AuthHello); hello.senderName = authenticationUserName_; hello.clientNonce = clientNonce_; + hello.publicKey = toByteArray(authenticationKeyPair_->getPublicKey().serialize()); QDataStream out(&socket_); out.setVersion(DataStreamVersion); diff --git a/src/server/Migrations/002_create_registration_requests.sql b/src/server/Migrations/002_create_registration_requests.sql new file mode 100644 index 0000000..950bfdd --- /dev/null +++ b/src/server/Migrations/002_create_registration_requests.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS registration_requests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL, + public_key BLOB NOT NULL, + source_address TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending' + CHECK (status IN ('pending', 'approved', 'rejected')), + created_at TEXT NOT NULL, + reviewed_at TEXT, + UNIQUE(username, public_key) +); + +CREATE INDEX IF NOT EXISTS idx_registration_requests_status +ON registration_requests(status, created_at); diff --git a/src/server/Storage/message_store.cpp b/src/server/Storage/message_store.cpp index 902bf52..63ca29a 100644 --- a/src/server/Storage/message_store.cpp +++ b/src/server/Storage/message_store.cpp @@ -1,7 +1,5 @@ #include "message_store.h" -#include "keyPair.h" - #include #include #include @@ -13,10 +11,7 @@ #include #include -#include -#include #include -#include using messenger::protocol::CurrentProtocolVersion; using messenger::protocol::Message; @@ -25,59 +20,11 @@ using messenger::protocol::MessageType; namespace { constexpr auto InvalidUserId = -1; -constexpr auto AdminPublicKeyFileName = "admin.public.rsa"; QString lastErrorText(const QSqlQuery& query) { return query.lastError().text(); } -std::optional loadAdminPublicKey(const QString& databasePath) { - const auto publicKeyPath = QFileInfo(databasePath).absoluteDir().filePath( - QString::fromLatin1(AdminPublicKeyFileName)); - const QFileInfo publicKeyInfo(publicKeyPath); - if (!publicKeyInfo.exists()) { - qCritical() << "Admin public key file does not exist:" << publicKeyInfo.absoluteFilePath(); - return std::nullopt; - } - if (!publicKeyInfo.isFile()) { - qCritical() << "Admin public key path is not a file:" << publicKeyInfo.absoluteFilePath(); - return std::nullopt; - } - - QFile publicKeyFile(publicKeyPath); - if (!publicKeyFile.open(QIODevice::ReadOnly)) { - qCritical() << "Could not read admin public key file:" << publicKeyInfo.absoluteFilePath() - << publicKeyFile.errorString(); - return std::nullopt; - } - - const auto publicKeyData = publicKeyFile.readAll(); - if (publicKeyData.isEmpty()) { - qCritical() << "Admin public key file is empty:" << publicKeyInfo.absoluteFilePath(); - return std::nullopt; - } - - const std::vector serializedKey(publicKeyData.cbegin(), publicKeyData.cend()); - try { - PublicKey publicKey; - const operations::BigInt one(1); - if (!keyPair::s_deserialize(serializedKey, publicKey.n, publicKey.e) - || publicKey.n <= one - || publicKey.e <= one - || publicKey.serialize() != serializedKey) { - qCritical() << "Admin public key file contains an invalid RSA public key:" - << publicKeyInfo.absoluteFilePath(); - return std::nullopt; - } - } catch (const std::exception& exception) { - qCritical() << "Could not parse admin public key file:" << publicKeyInfo.absoluteFilePath() - << exception.what(); - return std::nullopt; - } - - return publicKeyData; -} - QList splitSqlStatements(const QString& script) { QList statements; QString current; @@ -162,11 +109,6 @@ bool MessageStore::initialize() { return true; } - const auto adminPublicKey = loadAdminPublicKey(databasePath()); - if (!adminPublicKey) { - return false; - } - if (!openDatabase()) { return false; } @@ -175,10 +117,6 @@ bool MessageStore::initialize() { return false; } - if (!ensureAdminUser(*adminPublicKey)) { - return false; - } - initialized_ = true; return true; } @@ -319,6 +257,187 @@ std::optional MessageStore::findUserForAuthentication( }; } +RegistrationRequestResult MessageStore::requestRegistration( + const QString& userName, + const QByteArray& publicKey, + const QString& sourceAddress) const { + if (!initialized_) { + return {}; + } + + auto database = QSqlDatabase::database(connectionName_); + QSqlQuery existingQuery(database); + existingQuery.prepare(R"( + SELECT id, status + FROM registration_requests + WHERE username = :username AND public_key = :public_key + )"); + existingQuery.bindValue(QStringLiteral(":username"), userName.trimmed()); + existingQuery.bindValue(QStringLiteral(":public_key"), publicKey); + if (!existingQuery.exec()) { + qWarning() << "Could not look up registration request:" << lastErrorText(existingQuery); + return {}; + } + if (existingQuery.next()) { + const auto status = existingQuery.value(QStringLiteral("status")).toString(); + return { + status == QStringLiteral("rejected") + ? RegistrationRequestResult::Status::Rejected + : RegistrationRequestResult::Status::Pending, + existingQuery.value(QStringLiteral("id")).toLongLong(), + }; + } + + QSqlQuery insertQuery(database); + insertQuery.prepare(R"( + INSERT INTO registration_requests ( + username, public_key, source_address, status, created_at + ) VALUES ( + :username, :public_key, :source_address, 'pending', :created_at + ) + )"); + insertQuery.bindValue(QStringLiteral(":username"), userName.trimmed()); + insertQuery.bindValue(QStringLiteral(":public_key"), publicKey); + insertQuery.bindValue(QStringLiteral(":source_address"), sourceAddress); + insertQuery.bindValue(QStringLiteral(":created_at"), + QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs)); + if (!insertQuery.exec()) { + qWarning() << "Could not create registration request:" << lastErrorText(insertQuery); + return {}; + } + + return {RegistrationRequestResult::Status::Created, insertQuery.lastInsertId().toLongLong()}; +} + +QList MessageStore::pendingRegistrationRequests() const { + QList requests; + if (!initialized_) { + return requests; + } + + QSqlQuery query(QSqlDatabase::database(connectionName_)); + query.prepare(R"( + SELECT id, username, public_key, source_address, created_at + FROM registration_requests + WHERE status = 'pending' + ORDER BY created_at ASC, id ASC + )"); + if (!query.exec()) { + qWarning() << "Could not list registration requests:" << lastErrorText(query); + return requests; + } + + while (query.next()) { + requests.append({ + query.value(QStringLiteral("id")).toLongLong(), + query.value(QStringLiteral("username")).toString(), + query.value(QStringLiteral("public_key")).toByteArray(), + query.value(QStringLiteral("source_address")).toString(), + query.value(QStringLiteral("created_at")).toString(), + }); + } + return requests; +} + +bool MessageStore::approveRegistrationRequest(qint64 requestId) const { + if (!initialized_) { + return false; + } + + auto database = QSqlDatabase::database(connectionName_); + if (!database.transaction()) { + return false; + } + + QSqlQuery requestQuery(database); + requestQuery.prepare(R"( + SELECT username, public_key + FROM registration_requests + WHERE id = :id AND status = 'pending' + )"); + requestQuery.bindValue(QStringLiteral(":id"), requestId); + if (!requestQuery.exec() || !requestQuery.next()) { + qWarning() << "Pending registration request not found:" << requestId; + database.rollback(); + return false; + } + + const auto userName = requestQuery.value(QStringLiteral("username")).toString(); + const auto publicKey = requestQuery.value(QStringLiteral("public_key")).toByteArray(); + const auto now = QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs); + + QSqlQuery insertUserQuery(database); + insertUserQuery.prepare(R"( + INSERT INTO users (username, public_key, created_at) + VALUES (:username, :public_key, :created_at) + )"); + insertUserQuery.bindValue(QStringLiteral(":username"), userName); + insertUserQuery.bindValue(QStringLiteral(":public_key"), publicKey); + insertUserQuery.bindValue(QStringLiteral(":created_at"), now); + if (!insertUserQuery.exec()) { + qWarning() << "Could not create user from registration request:" + << lastErrorText(insertUserQuery); + database.rollback(); + return false; + } + + QSqlQuery approveQuery(database); + approveQuery.prepare(R"( + UPDATE registration_requests + SET status = 'approved', reviewed_at = :reviewed_at + WHERE id = :id AND status = 'pending' + )"); + approveQuery.bindValue(QStringLiteral(":reviewed_at"), now); + approveQuery.bindValue(QStringLiteral(":id"), requestId); + if (!approveQuery.exec() || approveQuery.numRowsAffected() != 1) { + database.rollback(); + return false; + } + + QSqlQuery rejectOthersQuery(database); + rejectOthersQuery.prepare(R"( + UPDATE registration_requests + SET status = 'rejected', reviewed_at = :reviewed_at + WHERE username = :username AND id <> :id AND status = 'pending' + )"); + rejectOthersQuery.bindValue(QStringLiteral(":reviewed_at"), now); + rejectOthersQuery.bindValue(QStringLiteral(":username"), userName); + rejectOthersQuery.bindValue(QStringLiteral(":id"), requestId); + if (!rejectOthersQuery.exec()) { + database.rollback(); + return false; + } + + if (!database.commit()) { + database.rollback(); + return false; + } + qInfo() << "Approved registration request" << requestId << "and created user" << userName; + return true; +} + +bool MessageStore::rejectRegistrationRequest(qint64 requestId) const { + if (!initialized_) { + return false; + } + + QSqlQuery query(QSqlDatabase::database(connectionName_)); + query.prepare(R"( + UPDATE registration_requests + SET status = 'rejected', reviewed_at = :reviewed_at + WHERE id = :id AND status = 'pending' + )"); + query.bindValue(QStringLiteral(":reviewed_at"), + QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs)); + query.bindValue(QStringLiteral(":id"), requestId); + if (!query.exec() || query.numRowsAffected() != 1) { + qWarning() << "Pending registration request not found:" << requestId; + return false; + } + qInfo() << "Rejected registration request" << requestId; + return true; +} + int MessageStore::userIdForUserName(const QString& userName) const { if (!initialized_) { qWarning() << "Cannot look up user before message store initialization"; @@ -456,39 +575,6 @@ bool MessageStore::runMigrations() { return true; } -bool MessageStore::ensureAdminUser(const QByteArray& publicKey) const { - auto database = QSqlDatabase::database(connectionName_); - if (!database.transaction()) { - qWarning() << "Could not start admin user transaction:" << database.lastError().text(); - return false; - } - - QSqlQuery query(database); - query.prepare(R"( - INSERT INTO users (username, public_key, created_at) - VALUES (:username, :public_key, :created_at) - ON CONFLICT(username) DO UPDATE SET public_key = excluded.public_key - )"); - query.bindValue(QStringLiteral(":username"), QStringLiteral("admin")); - query.bindValue(QStringLiteral(":public_key"), publicKey); - query.bindValue(QStringLiteral(":created_at"), - QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs)); - - if (!query.exec()) { - qWarning() << "Could not create or update admin user:" << lastErrorText(query); - database.rollback(); - return false; - } - - if (!database.commit()) { - qWarning() << "Could not commit admin user transaction:" << database.lastError().text(); - database.rollback(); - return false; - } - - return true; -} - bool MessageStore::executeSqlScript(const QString& script) const { auto database = QSqlDatabase::database(connectionName_); diff --git a/src/server/Storage/message_store.h b/src/server/Storage/message_store.h index 9054326..7a65088 100644 --- a/src/server/Storage/message_store.h +++ b/src/server/Storage/message_store.h @@ -15,6 +15,26 @@ struct UserAuthenticationRecord { QByteArray publicKey; }; +struct RegistrationRequest { + qint64 requestId; + QString userName; + QByteArray publicKey; + QString sourceAddress; + QString createdAt; +}; + +struct RegistrationRequestResult { + enum class Status { + Created, + Pending, + Rejected, + Failed, + }; + + Status status = Status::Failed; + qint64 requestId = -1; +}; + class MessageStore { public: MessageStore(); @@ -30,6 +50,13 @@ class MessageStore { bool hasUser(const QString& userName) const; [[nodiscard]] std::optional findUserForAuthentication( const QString& userName) const; + [[nodiscard]] RegistrationRequestResult requestRegistration( + const QString& userName, + const QByteArray& publicKey, + const QString& sourceAddress) const; + [[nodiscard]] QList pendingRegistrationRequests() const; + bool approveRegistrationRequest(qint64 requestId) const; + bool rejectRegistrationRequest(qint64 requestId) const; bool saveMessage(const messenger::protocol::Message& message); QList loadMessages() const; @@ -37,7 +64,6 @@ class MessageStore { int userIdForUserName(const QString& userName) const; bool openDatabase(); bool runMigrations(); - bool ensureAdminUser(const QByteArray& publicKey) const; bool executeSqlScript(const QString& script) const; QString databasePath() const; diff --git a/src/server/server.cpp b/src/server/server.cpp index a25f296..5488e1c 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -135,8 +135,37 @@ void server::handleMessageReceived(const Message& message, Session* session) { const auto user = messageStore_.findUserForAuthentication(requestedUserName); if (!user) { - qWarning() << "Authentication requested for unknown user:" << requestedUserName; - session->rejectAuthentication(); + if (!deserializePublicKey(message.publicKey)) { + qWarning() << "Registration requested with an invalid public key for" + << requestedUserName; + session->rejectAuthentication(QStringLiteral("The public key is invalid.")); + return; + } + + const auto request = messageStore_.requestRegistration( + requestedUserName, message.publicKey, session->peerAddress()); + switch (request.status) { + case RegistrationRequestResult::Status::Created: + qInfo().noquote() + << QStringLiteral("New registration request %1 for \"%2\" from %3. " + "Run 'Messenger-Server requests' to review it.") + .arg(request.requestId) + .arg(requestedUserName, session->peerAddress()); + [[fallthrough]]; + case RegistrationRequestResult::Status::Pending: + session->rejectAuthentication( + QStringLiteral("Access is pending server approval."), + MessageType::RegistrationPending); + break; + case RegistrationRequestResult::Status::Rejected: + session->rejectAuthentication( + QStringLiteral("The registration request was rejected."), + MessageType::RegistrationRejected); + break; + case RegistrationRequestResult::Status::Failed: + session->rejectAuthentication(); + break; + } return; } @@ -230,6 +259,7 @@ void server::handleMessageReceived(const Message& message, Session* session) { verifiedMessage.clientNonce.clear(); verifiedMessage.serverNonce.clear(); verifiedMessage.signature.clear(); + verifiedMessage.publicKey.clear(); qInfo() << "Message from" << verifiedMessage.senderName << ":" << verifiedMessage.text; @@ -250,11 +280,81 @@ void server::handleSessionDisconnected(Session* session) { session->deleteLater(); } +namespace { + +void printServerUsage(const QString& executable) { + qInfo().noquote() + << QStringLiteral( + "Usage:\n" + " %1 Start the server\n" + " %1 requests List pending registration requests\n" + " %1 approve Approve a registration request\n" + " %1 reject Reject a registration request") + .arg(executable); +} + +int runAdministrationCommand(const QStringList& arguments) { + MessageStore store; + if (!store.initialize()) { + qCritical() << "Could not initialize message storage"; + return 1; + } + + const auto command = arguments.at(1); + if (command == QStringLiteral("requests") && arguments.size() == 2) { + const auto requests = store.pendingRegistrationRequests(); + if (requests.isEmpty()) { + qInfo() << "There are no pending registration requests."; + return 0; + } + + for (const auto& request : requests) { + const auto fingerprint = QCryptographicHash::hash( + request.publicKey, QCryptographicHash::Sha256) + .toHex(':') + .toUpper(); + qInfo().noquote() + << QStringLiteral("[%1] %2\n From: %3\n Created: %4\n Key: %5") + .arg(request.requestId) + .arg(request.userName, + request.sourceAddress, + request.createdAt, + QString::fromLatin1(fingerprint)); + } + return 0; + } + + if ((command == QStringLiteral("approve") || command == QStringLiteral("reject")) + && arguments.size() == 3) { + bool validId = false; + const auto requestId = arguments.at(2).toLongLong(&validId); + if (!validId || requestId <= 0) { + qCritical() << "The request ID must be a positive number."; + return 2; + } + + const auto success = command == QStringLiteral("approve") + ? store.approveRegistrationRequest(requestId) + : store.rejectRegistrationRequest(requestId); + return success ? 0 : 1; + } + + printServerUsage(arguments.first()); + return 2; +} + +} // namespace + int main(int argc, char* argv[]) { QCoreApplication app(argc, argv); QCoreApplication::setApplicationName(QStringLiteral("Messenger")); QCoreApplication::setOrganizationName(QStringLiteral("ParallelEngineering")); + const auto arguments = QCoreApplication::arguments(); + if (arguments.size() > 1) { + return runAdministrationCommand(arguments); + } + auto& serverInstance = server::getInstance(); if (!serverInstance.listen(QHostAddress::Any, DefaultPort)) { serverInstance.shutdown(); diff --git a/src/server/session.cpp b/src/server/session.cpp index d39c2c0..0329169 100644 --- a/src/server/session.cpp +++ b/src/server/session.cpp @@ -79,7 +79,7 @@ void Session::completeAuthentication() { serverNonce_.clear(); } -void Session::rejectAuthentication(const QString& reason) { +void Session::rejectAuthentication(const QString& reason, MessageType messageType) { if (authenticationState_ == AuthenticationState::Rejected) { return; } @@ -92,7 +92,7 @@ void Session::rejectAuthentication(const QString& reason) { serverNonce_.clear(); Message failure; - failure.messageType = static_cast(MessageType::AuthFailure); + failure.messageType = static_cast(messageType); failure.text = reason; sendMessage(failure); socket_->disconnectFromHost(); @@ -138,6 +138,10 @@ const QByteArray& Session::serverNonce() const { return serverNonce_; } +QString Session::peerAddress() const { + return socket_->peerAddress().toString(); +} + void Session::readAvailable() { while (socket_->bytesAvailable() > 0) { stream_.startTransaction(); diff --git a/src/server/session.h b/src/server/session.h index 31aeb8b..bf6f7c7 100644 --- a/src/server/session.h +++ b/src/server/session.h @@ -32,7 +32,9 @@ class Session final : public QObject { const QByteArray& clientNonce, const QByteArray& serverNonce); void completeAuthentication(); - void rejectAuthentication(const QString& reason = QStringLiteral("Authentication failed.")); + void rejectAuthentication( + const QString& reason = QStringLiteral("Authentication failed."), + messenger::protocol::MessageType messageType = messenger::protocol::MessageType::AuthFailure); void disconnectFromHost(); [[nodiscard]] AuthenticationState authenticationState() const; @@ -44,6 +46,7 @@ class Session final : public QObject { [[nodiscard]] const QByteArray& authenticationId() const; [[nodiscard]] const QByteArray& clientNonce() const; [[nodiscard]] const QByteArray& serverNonce() const; + [[nodiscard]] QString peerAddress() const; signals: void messageReceived(const messenger::protocol::Message& message, Session* session); diff --git a/src/shared/message.cpp b/src/shared/message.cpp index 46270d1..3f4416c 100644 --- a/src/shared/message.cpp +++ b/src/shared/message.cpp @@ -15,6 +15,7 @@ QDataStream& operator<<(QDataStream& out, const Message& message) { out << message.clientNonce; out << message.serverNonce; out << message.signature; + out << message.publicKey; return out; } @@ -29,6 +30,7 @@ QDataStream& operator>>(QDataStream& in, Message& message) { in >> message.clientNonce; in >> message.serverNonce; in >> message.signature; + in >> message.publicKey; return in; } diff --git a/src/shared/message.h b/src/shared/message.h index db31040..67b4faf 100644 --- a/src/shared/message.h +++ b/src/shared/message.h @@ -9,7 +9,7 @@ namespace messenger::protocol { -inline constexpr quint32 CurrentProtocolVersion = 2; +inline constexpr quint32 CurrentProtocolVersion = 3; inline constexpr quint16 DefaultPort = 4242; inline constexpr qint64 ReadBufferSize = 1024 * 1024; inline constexpr auto DataStreamVersion = QDataStream::Qt_6_5; @@ -25,6 +25,8 @@ enum class MessageType : quint32 { AuthProof = 3, AuthSuccess = 4, AuthFailure = 5, + RegistrationPending = 6, + RegistrationRejected = 7, ChatMessage = 100, SystemMessage = 101, ErrorMessage = 102, @@ -40,6 +42,7 @@ struct Message { QByteArray clientNonce; QByteArray serverNonce; QByteArray signature; + QByteArray publicKey; }; QDataStream& operator<<(QDataStream& out, const Message& message); From 85b0ea29190e750a3045d70f26bea42d5f352d93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sun, 12 Jul 2026 18:36:04 +0200 Subject: [PATCH 36/41] Add functionality for listing and deleting users --- README.md | 7 +++ docs/connection-establishment.md | 2 + src/server/Storage/message_store.cpp | 83 ++++++++++++++++++++++++++++ src/server/Storage/message_store.h | 8 +++ src/server/server.cpp | 45 ++++++++++++++- 5 files changed, 143 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2584fec..75b5a95 100644 --- a/README.md +++ b/README.md @@ -83,3 +83,10 @@ Messenger-Server reject 2 ``` After approval, the client connects again manually and uses the normal RSA authentication flow. No administrator account or manually installed public-key file is required. + +Existing users can be listed and completely removed by ID. Deletion also removes that user's messages and registration requests, so the username can be registered again later: + +```bash +Messenger-Server users +Messenger-Server delete-user 1 +``` diff --git a/docs/connection-establishment.md b/docs/connection-establishment.md index 817ee5f..367fc9c 100644 --- a/docs/connection-establishment.md +++ b/docs/connection-establishment.md @@ -152,6 +152,8 @@ For `AuthHello`, the server: If the username does not exist, the server validates the public key from `AuthHello`, stores an idempotent pending request for that username and key, sends `RegistrationPending`, and closes the connection. A rejected matching request produces `RegistrationRejected`. The administrator reviews requests with `Messenger-Server requests` and decides with `Messenger-Server approve ` or `Messenger-Server reject `. +The administrator can list accounts with `Messenger-Server users` and remove an account with `Messenger-Server delete-user `. Deletion removes the user, their stored messages, and all registration requests for that username in one transaction. + For a known user and `AuthProof`, the server reconstructs the transcript, calculates its SHA-256 digest, and verifies the signature with the stored public key. On success, it binds the user ID and username to the session, sends `AuthSuccess`, and then sends the chat history. On failure, it sends `AuthFailure` and closes the connection. Only authenticated sessions may send or receive chat messages. The server sets the sender name and timestamp itself: diff --git a/src/server/Storage/message_store.cpp b/src/server/Storage/message_store.cpp index 63ca29a..8bcd758 100644 --- a/src/server/Storage/message_store.cpp +++ b/src/server/Storage/message_store.cpp @@ -438,6 +438,89 @@ bool MessageStore::rejectRegistrationRequest(qint64 requestId) const { return true; } +QList MessageStore::users() const { + QList users; + if (!initialized_) { + return users; + } + + QSqlQuery query(QSqlDatabase::database(connectionName_)); + query.prepare(R"( + SELECT id, username, created_at + FROM users + ORDER BY username COLLATE NOCASE ASC, id ASC + )"); + if (!query.exec()) { + qWarning() << "Could not list users:" << lastErrorText(query); + return users; + } + + while (query.next()) { + users.append({ + query.value(QStringLiteral("id")).toLongLong(), + query.value(QStringLiteral("username")).toString(), + query.value(QStringLiteral("created_at")).toString(), + }); + } + return users; +} + +bool MessageStore::deleteUser(qint64 userId) const { + if (!initialized_) { + return false; + } + + auto database = QSqlDatabase::database(connectionName_); + if (!database.transaction()) { + return false; + } + + QSqlQuery userQuery(database); + userQuery.prepare(QStringLiteral("SELECT username FROM users WHERE id = :id")); + userQuery.bindValue(QStringLiteral(":id"), userId); + if (!userQuery.exec() || !userQuery.next()) { + qWarning() << "User not found:" << userId; + database.rollback(); + return false; + } + const auto userName = userQuery.value(QStringLiteral("username")).toString(); + + QSqlQuery messagesQuery(database); + messagesQuery.prepare(QStringLiteral("DELETE FROM messages WHERE user_id = :user_id")); + messagesQuery.bindValue(QStringLiteral(":user_id"), userId); + if (!messagesQuery.exec()) { + qWarning() << "Could not delete messages for user:" << lastErrorText(messagesQuery); + database.rollback(); + return false; + } + + QSqlQuery requestsQuery(database); + requestsQuery.prepare( + QStringLiteral("DELETE FROM registration_requests WHERE username = :username")); + requestsQuery.bindValue(QStringLiteral(":username"), userName); + if (!requestsQuery.exec()) { + qWarning() << "Could not delete registration requests for user:" + << lastErrorText(requestsQuery); + database.rollback(); + return false; + } + + QSqlQuery deleteUserQuery(database); + deleteUserQuery.prepare(QStringLiteral("DELETE FROM users WHERE id = :id")); + deleteUserQuery.bindValue(QStringLiteral(":id"), userId); + if (!deleteUserQuery.exec() || deleteUserQuery.numRowsAffected() != 1) { + database.rollback(); + return false; + } + + if (!database.commit()) { + database.rollback(); + return false; + } + qInfo() << "Deleted user" << userName << "with ID" << userId; + return true; +} + int MessageStore::userIdForUserName(const QString& userName) const { if (!initialized_) { qWarning() << "Cannot look up user before message store initialization"; diff --git a/src/server/Storage/message_store.h b/src/server/Storage/message_store.h index 7a65088..99bf189 100644 --- a/src/server/Storage/message_store.h +++ b/src/server/Storage/message_store.h @@ -15,6 +15,12 @@ struct UserAuthenticationRecord { QByteArray publicKey; }; +struct StoredUser { + qint64 userId; + QString userName; + QString createdAt; +}; + struct RegistrationRequest { qint64 requestId; QString userName; @@ -57,6 +63,8 @@ class MessageStore { [[nodiscard]] QList pendingRegistrationRequests() const; bool approveRegistrationRequest(qint64 requestId) const; bool rejectRegistrationRequest(qint64 requestId) const; + [[nodiscard]] QList users() const; + bool deleteUser(qint64 userId) const; bool saveMessage(const messenger::protocol::Message& message); QList loadMessages() const; diff --git a/src/server/server.cpp b/src/server/server.cpp index 5488e1c..3130165 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -244,6 +244,12 @@ void server::handleMessageReceived(const Message& message, Session* session) { return; } + if (!messageStore_.hasUser(session->userName())) { + qInfo() << "Disconnecting session for deleted user" << session->userName(); + session->disconnectFromHost(); + return; + } + if (messageType != MessageType::ChatMessage || !isValidChatMessage(message)) { qWarning() << "Disconnecting authenticated session after invalid message from" << session->userName(); @@ -268,11 +274,18 @@ void server::handleMessageReceived(const Message& message, Session* session) { return; } + QList deletedUserSessions; for (auto* connectedSession : std::as_const(sessions_)) { - if (connectedSession->isAuthenticated()) { + if (connectedSession->isAuthenticated() + && !messageStore_.hasUser(connectedSession->userName())) { + deletedUserSessions.append(connectedSession); + } else if (connectedSession->isAuthenticated()) { connectedSession->sendMessage(verifiedMessage); } } + for (auto* deletedUserSession : deletedUserSessions) { + deletedUserSession->disconnectFromHost(); + } } void server::handleSessionDisconnected(Session* session) { @@ -289,7 +302,9 @@ void printServerUsage(const QString& executable) { " %1 Start the server\n" " %1 requests List pending registration requests\n" " %1 approve Approve a registration request\n" - " %1 reject Reject a registration request") + " %1 reject Reject a registration request\n" + " %1 users List all users\n" + " %1 delete-user Delete a user and all associated data") .arg(executable); } @@ -324,6 +339,22 @@ int runAdministrationCommand(const QStringList& arguments) { return 0; } + if (command == QStringLiteral("users") && arguments.size() == 2) { + const auto users = store.users(); + if (users.isEmpty()) { + qInfo() << "There are no users."; + return 0; + } + + for (const auto& user : users) { + qInfo().noquote() + << QStringLiteral("[%1] %2\n Created: %3") + .arg(user.userId) + .arg(user.userName, user.createdAt); + } + return 0; + } + if ((command == QStringLiteral("approve") || command == QStringLiteral("reject")) && arguments.size() == 3) { bool validId = false; @@ -339,6 +370,16 @@ int runAdministrationCommand(const QStringList& arguments) { return success ? 0 : 1; } + if (command == QStringLiteral("delete-user") && arguments.size() == 3) { + bool validId = false; + const auto userId = arguments.at(2).toLongLong(&validId); + if (!validId || userId <= 0) { + qCritical() << "The user ID must be a positive number."; + return 2; + } + return store.deleteUser(userId) ? 0 : 1; + } + printServerUsage(arguments.first()); return 2; } From b9e06f3affd17162dec0e9321804d12dfbfe7a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sun, 12 Jul 2026 19:08:56 +0200 Subject: [PATCH 37/41] Add clear-history command to delete all stored chat messages --- README.md | 6 ++++++ docs/connection-establishment.md | 2 ++ src/server/Storage/message_store.cpp | 15 +++++++++++++++ src/server/Storage/message_store.h | 1 + src/server/server.cpp | 7 ++++++- 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 75b5a95..5f799ca 100644 --- a/README.md +++ b/README.md @@ -90,3 +90,9 @@ Existing users can be listed and completely removed by ID. Deletion also removes Messenger-Server users Messenger-Server delete-user 1 ``` + +The complete stored chat history can be deleted without changing users or registration requests: + +```bash +Messenger-Server clear-history +``` diff --git a/docs/connection-establishment.md b/docs/connection-establishment.md index 367fc9c..81fc4b9 100644 --- a/docs/connection-establishment.md +++ b/docs/connection-establishment.md @@ -154,6 +154,8 @@ If the username does not exist, the server validates the public key from `AuthHe The administrator can list accounts with `Messenger-Server users` and remove an account with `Messenger-Server delete-user `. Deletion removes the user, their stored messages, and all registration requests for that username in one transaction. +The command `Messenger-Server clear-history` deletes all stored chat messages without changing users or registration requests. + For a known user and `AuthProof`, the server reconstructs the transcript, calculates its SHA-256 digest, and verifies the signature with the stored public key. On success, it binds the user ID and username to the session, sends `AuthSuccess`, and then sends the chat history. On failure, it sends `AuthFailure` and closes the connection. Only authenticated sessions may send or receive chat messages. The server sets the sender name and timestamp itself: diff --git a/src/server/Storage/message_store.cpp b/src/server/Storage/message_store.cpp index 8bcd758..af1ee14 100644 --- a/src/server/Storage/message_store.cpp +++ b/src/server/Storage/message_store.cpp @@ -521,6 +521,21 @@ bool MessageStore::deleteUser(qint64 userId) const { return true; } +bool MessageStore::clearMessages() const { + if (!initialized_) { + return false; + } + + QSqlQuery query(QSqlDatabase::database(connectionName_)); + if (!query.exec(QStringLiteral("DELETE FROM messages"))) { + qWarning() << "Could not clear chat history:" << lastErrorText(query); + return false; + } + + qInfo() << "Cleared chat history; deleted" << query.numRowsAffected() << "messages"; + return true; +} + int MessageStore::userIdForUserName(const QString& userName) const { if (!initialized_) { qWarning() << "Cannot look up user before message store initialization"; diff --git a/src/server/Storage/message_store.h b/src/server/Storage/message_store.h index 99bf189..bae7985 100644 --- a/src/server/Storage/message_store.h +++ b/src/server/Storage/message_store.h @@ -65,6 +65,7 @@ class MessageStore { bool rejectRegistrationRequest(qint64 requestId) const; [[nodiscard]] QList users() const; bool deleteUser(qint64 userId) const; + bool clearMessages() const; bool saveMessage(const messenger::protocol::Message& message); QList loadMessages() const; diff --git a/src/server/server.cpp b/src/server/server.cpp index 3130165..b72d9ed 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -304,7 +304,8 @@ void printServerUsage(const QString& executable) { " %1 approve Approve a registration request\n" " %1 reject Reject a registration request\n" " %1 users List all users\n" - " %1 delete-user Delete a user and all associated data") + " %1 delete-user Delete a user and all associated data\n" + " %1 clear-history Delete all stored chat messages") .arg(executable); } @@ -380,6 +381,10 @@ int runAdministrationCommand(const QStringList& arguments) { return store.deleteUser(userId) ? 0 : 1; } + if (command == QStringLiteral("clear-history") && arguments.size() == 2) { + return store.clearMessages() ? 0 : 1; + } + printServerUsage(arguments.first()); return 2; } From 6381094dd27ffacfb5b552b5d442e1cbb4ad6ea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sun, 12 Jul 2026 19:28:39 +0200 Subject: [PATCH 38/41] Add Images in Readme --- README.md | 5 +++++ docs/img/client_chat.png | Bin 0 -> 230507 bytes docs/img/client_start_screen.png | Bin 0 -> 187057 bytes 3 files changed, 5 insertions(+) create mode 100644 docs/img/client_chat.png create mode 100644 docs/img/client_start_screen.png diff --git a/README.md b/README.md index 5f799ca..a1f159e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,11 @@ Messenger is a simple chat program with a client and a server. Users can connect to a server, send messages, receive messages, and see the previous chat history. The messenger is intentionally kept simple: there is only one chat on each server, because the project is mainly a learning project focused on networking and encryption. +

+ Start screen + Client chat +

+ ## Functionality - **Client connection:** enter a server host, connect to the server, disconnect again when terminating the program, and see the current connection status in the UI. diff --git a/docs/img/client_chat.png b/docs/img/client_chat.png new file mode 100644 index 0000000000000000000000000000000000000000..97565c0b0949a6aa17e92b8d9f9948b96ed1f9a6 GIT binary patch literal 230507 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 z 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 P00000NkvXXu0mjf`ApmW literal 0 HcmV?d00001 diff --git a/docs/img/client_start_screen.png b/docs/img/client_start_screen.png new file mode 100644 index 0000000000000000000000000000000000000000..7d1268359a164f4ddcccacbdd0954a5259c58138 GIT binary patch literal 187057 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

o3A$J0Rg9I{YyB<_2B!LPUPF-A6NREJoEhv5`}Q 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 Vh5uv84(b2^002ovPDHLkV1nuJ<=g-O literal 0 HcmV?d00001 From ef153f3638473c8b77037affa81e51b0ae76c19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sun, 12 Jul 2026 20:05:36 +0200 Subject: [PATCH 39/41] Update RSA submodule --- lib/RSA | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/RSA b/lib/RSA index ae48056..0350bb7 160000 --- a/lib/RSA +++ b/lib/RSA @@ -1 +1 @@ -Subproject commit ae48056506456eff9a2488a6d0cd4431b7b8df7d +Subproject commit 0350bb79a2287c6fb5e1c9317e1333be69d1b76e From eef54380650da58efc896d16c321a4641c6365a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= <134922046+LordofGhost@users.noreply.github.com> Date: Sun, 12 Jul 2026 20:24:16 +0200 Subject: [PATCH 40/41] Update Readme --- README.md | 42 ++++++++++-------------------------------- 1 file changed, 10 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index a1f159e..a964ec7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Messenger -Messenger is a simple chat program with a client and a server. Users can connect to a server, send messages, receive messages, and see the previous chat history. The messenger is intentionally kept simple: there is only one chat on each server, because the project is mainly a learning project focused on networking and encryption. +Messenger is a simple chat program with a client and a server. Users can connect to a server, send messages, receive messages, and see the previous chat history. The messenger is intentionally kept simple because it is mainly a learning project focused on networking and encryption, including RSA-based authentication built with a self-implemented RSA library.

Start screen @@ -12,7 +12,7 @@ Messenger is a simple chat program with a client and a server. Users can connect - **Client connection:** enter a server host, connect to the server, disconnect again when terminating the program, and see the current connection status in the UI. - **Group chat authentication:** each server represents one group chat; clients request access with a name and their public key, without passwords. After server-side approval, they authenticate through RSA-based verification using the stored public key. - **Message sending:** compose text messages and send them to the connected server. -- **Message receiving:** display incoming messages in the client window as they arrive from the server with timestamp and username. +- **Message receiving:** display incoming messages in the client window as they arrive from the server. - **Group chat forwarding:** the server keeps track of the clients connected to its group chat and broadcasts chat messages to every active session. - **Chat history:** the server stores the group chat history and sends all previous messages to newly connected clients. - **Protocol validation:** client and server exchange structured messages and reject unsupported protocol versions. @@ -20,9 +20,14 @@ Messenger is a simple chat program with a client and a server. Users can connect ## Architecture -The repository contains two applications: `Messenger-Client` and `Messenger-Server`. The client is built with Qt/QML; the server is a terminal program. +The repository contains two applications, `Messenger-Client` and `Messenger-Server`, as well as a shared protocol library. Communication is based on TCP and serialized with `QDataStream`. -Both applications use a singleton through `getInstance()`. The project is built with CMake and C++20, with the RSA library included as a submodule. +- **Client:** the graphical Qt Quick/QML interface delegates connection handling to `NetworkManager`. It manages the TCP socket, the authentication state machine, RSA challenge signing, and the sending and receiving of protocol messages. `ConnectionStore` stores past connection and handles RSA keys. +- **Shared protocol:** `src/shared` defines the versioned message format, message types, protocol limits, nonce generation, and the canonical authentication transcript used by both applications. +- **Server:** the terminal-based server accepts connections through `QTcpServer` and creates one `Session` per client. A session owns its socket and authentication state, while the central server validates incoming messages, persists chat messages, and broadcasts them to authenticated sessions. +- **Persistence:** `MessageStore` encapsulates the SQLite database. It applies migrations and stores users, public keys, registration requests, and chat history. Private RSA keys remain exclusively on the clients. + +After a TCP connection is established, the client and server perform a challenge-response handshake. The client signs a transcript containing fresh client and server nonces with its private RSA key; the server verifies the signature with the stored public key. Only then is the session marked as authenticated and allowed to receive the chat history or exchange chat messages. Unknown users first create a registration request that must be approved through the server CLI. A detailed sequence is documented in [Connection Establishment and Client Authentication](docs/connection-establishment.md). ## Dependencies @@ -61,7 +66,7 @@ git clone --recurse-submodules https://github.com/ParallelEngineering/Messenger. If the repository has already been cloned without submodules, they can be initialized recursively with remote updates: ```bash -git submodule update --init --recursive --remote +git submodule update --init --recursive ``` ## Build @@ -74,30 +79,3 @@ cmake --build cmake-build-debug ``` This creates the `Messenger-Client` and `Messenger-Server` targets. - -## User registration - -When a client connects with an unknown username, the server stores a pending registration request and the client is told to try again after an administrator has reviewed it. The private key always remains on the client. - -Use the server executable from a terminal to review requests in the same database: - -```bash -Messenger-Server requests -Messenger-Server approve 1 -Messenger-Server reject 2 -``` - -After approval, the client connects again manually and uses the normal RSA authentication flow. No administrator account or manually installed public-key file is required. - -Existing users can be listed and completely removed by ID. Deletion also removes that user's messages and registration requests, so the username can be registered again later: - -```bash -Messenger-Server users -Messenger-Server delete-user 1 -``` - -The complete stored chat history can be deleted without changing users or registration requests: - -```bash -Messenger-Server clear-history -``` From da1d76766a3e17ce1fdab39fdaa55e39be5f4388 Mon Sep 17 00:00:00 2001 From: LordofGhost <134922046+LordofGhost@users.noreply.github.com> Date: Sun, 12 Jul 2026 18:28:46 +0000 Subject: [PATCH 41/41] Apply Clang formatting --- src/client/client.cpp | 6 +- src/client/connection_store.cpp | 88 ++++++++---------- src/client/connection_store.h | 17 ++-- src/client/network_manager.cpp | 57 +++++------- src/client/network_manager.h | 4 +- src/server/Storage/message_store.cpp | 58 ++++++------ src/server/Storage/message_store.h | 12 +-- src/server/server.cpp | 131 ++++++++++++--------------- src/server/server.h | 6 +- src/server/session.cpp | 58 ++++-------- src/server/session.h | 19 ++-- src/shared/message.cpp | 7 +- src/shared/message.h | 11 +-- 13 files changed, 199 insertions(+), 275 deletions(-) diff --git a/src/client/client.cpp b/src/client/client.cpp index c22d202..2b2cd7b 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -1,14 +1,14 @@ #include "client.h" -#include "connection_store.h" -#include "network_manager.h" - #include #include #include #include #include +#include "connection_store.h" +#include "network_manager.h" + client& client::getInstance() { static client instance; return instance; diff --git a/src/client/connection_store.cpp b/src/client/connection_store.cpp index 481ca97..9d2d6c2 100644 --- a/src/client/connection_store.cpp +++ b/src/client/connection_store.cpp @@ -1,23 +1,22 @@ #include "connection_store.h" -#include "keyPair.h" -#include "message.h" - #include #include #include #include #include -#include #include #include +#include #include #include - #include #include #include +#include "keyPair.h" +#include "message.h" + namespace { constexpr auto KeyFolderName = "rsa-keys"; @@ -26,7 +25,8 @@ constexpr auto PublicKeySuffix = ".public.rsa"; constexpr auto PrivateKeySuffix = ".private.rsa"; QByteArray toByteArray(const std::vector& bytes) { - return QByteArray(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + return QByteArray(reinterpret_cast(bytes.data()), + static_cast(bytes.size())); } std::vector toByteVector(const QByteArray& bytes) { @@ -46,37 +46,23 @@ ConnectionStore::ConnectionStore(QObject* parent) clearInvalidSelectedKey(); } -ConnectionStore::~ConnectionStore() { - cancelKeyPairCreation(); -} +ConnectionStore::~ConnectionStore() { cancelKeyPairCreation(); } -QString ConnectionStore::host() const { - return host_; -} +QString ConnectionStore::host() const { return host_; } -int ConnectionStore::port() const { - return port_; -} +int ConnectionStore::port() const { return port_; } -QString ConnectionStore::userName() const { - return userName_; -} +QString ConnectionStore::userName() const { return userName_; } -QString ConnectionStore::selectedKeyName() const { - return selectedKeyName_; -} +QString ConnectionStore::selectedKeyName() const { return selectedKeyName_; } const keyPair* ConnectionStore::currentKeyPair() const { return currentKeyPair_ ? &*currentKeyPair_ : nullptr; } -QStringList ConnectionStore::availableKeyNames() const { - return availableKeyNames_; -} +QStringList ConnectionStore::availableKeyNames() const { return availableKeyNames_; } -QString ConnectionStore::errorText() const { - return errorText_; -} +QString ConnectionStore::errorText() const { return errorText_; } bool ConnectionStore::keyGenerationInProgress() const { return keyGenerationInProgress_; } @@ -185,13 +171,16 @@ bool ConnectionStore::startKeyPairCreation(const QString& name) { } if (!guardedThis) return; const auto discarded = discardGeneratedKey->load(std::memory_order_relaxed); - QMetaObject::invokeMethod(guardedThis, [guardedThis, keyName, publicBytes, privateBytes, error, - discarded, discardGeneratedKey]() { - if (guardedThis) { - guardedThis->finishKeyPairCreation(keyName, publicBytes, privateBytes, error, - discarded, discardGeneratedKey); - } - }, Qt::QueuedConnection); + QMetaObject::invokeMethod( + guardedThis, + [guardedThis, keyName, publicBytes, privateBytes, error, discarded, + discardGeneratedKey]() { + if (guardedThis) { + guardedThis->finishKeyPairCreation(keyName, publicBytes, privateBytes, error, + discarded, discardGeneratedKey); + } + }, + Qt::QueuedConnection); }); return true; } @@ -203,10 +192,10 @@ void ConnectionStore::cancelKeyPairCreation() { emit keyGenerationInProgressChanged(); } -void ConnectionStore::finishKeyPairCreation(const QString& keyName, const QByteArray& publicKeyBytes, - const QByteArray& privateKeyBytes, const QString& workerError, - bool discarded, - const std::shared_ptr& discardGeneratedKey) { +void ConnectionStore::finishKeyPairCreation( + const QString& keyName, const QByteArray& publicKeyBytes, const QByteArray& privateKeyBytes, + const QString& workerError, bool discarded, + const std::shared_ptr& discardGeneratedKey) { discarded = discarded || discardGeneratedKey->load(std::memory_order_relaxed); if (!discarded && workerError.isEmpty()) { @@ -275,14 +264,10 @@ bool ConnectionStore::deleteKeyPair(const QString& name) { return true; } -void ConnectionStore::clearErrorText() { - setErrorText({}); -} +void ConnectionStore::clearErrorText() { setErrorText({}); } -bool ConnectionStore::saveLastConnection(const QString& host, - int port, - const QString& userName, - const QString& selectedKeyName) { +bool ConnectionStore::saveLastConnection(const QString& host, int port, const QString& userName, + const QString& selectedKeyName) { const auto trimmedHost = host.trimmed(); const auto trimmedUserName = userName.trimmed(); const auto trimmedKeyName = selectedKeyName.trimmed(); @@ -302,7 +287,8 @@ bool ConnectionStore::saveLastConnection(const QString& host, } const auto settingsFileInfo = QFileInfo(settingsFilePath()); - if (!settingsFileInfo.absoluteDir().exists() && !QDir().mkpath(settingsFileInfo.absolutePath())) { + if (!settingsFileInfo.absoluteDir().exists() && + !QDir().mkpath(settingsFileInfo.absolutePath())) { setErrorText(tr("The storage directory could not be created.")); return false; } @@ -361,8 +347,8 @@ bool ConnectionStore::ensureKeyDirectory() { bool ConnectionStore::keyNameIsValid(const QString& keyName) const { static const QRegularExpression invalidCharacters(QStringLiteral(R"([\\/:*?"<>|\x00-\x1F])")); - return !keyName.isEmpty() && keyName != QStringLiteral(".") && keyName != QStringLiteral("..") && - !keyName.contains(invalidCharacters); + return !keyName.isEmpty() && keyName != QStringLiteral(".") && + keyName != QStringLiteral("..") && !keyName.contains(invalidCharacters); } bool ConnectionStore::keyExists(const QString& keyName) const { @@ -402,9 +388,9 @@ std::optional ConnectionStore::loadKeyPair(const QString& keyName) { void ConnectionStore::refreshAvailableKeyNames() { const QDir keyDirectory(keyDirectoryPath()); - const auto publicKeyFiles = keyDirectory.entryList({QStringLiteral("*") + QString::fromLatin1(PublicKeySuffix)}, - QDir::Files, - QDir::Name | QDir::IgnoreCase); + const auto publicKeyFiles = + keyDirectory.entryList({QStringLiteral("*") + QString::fromLatin1(PublicKeySuffix)}, + QDir::Files, QDir::Name | QDir::IgnoreCase); QStringList keyNames; for (const auto& publicKeyFile : publicKeyFiles) { diff --git a/src/client/connection_store.h b/src/client/connection_store.h index dc00b23..e9f912a 100644 --- a/src/client/connection_store.h +++ b/src/client/connection_store.h @@ -1,27 +1,28 @@ #ifndef MESSENGER_CONNECTION_STORE_H #define MESSENGER_CONNECTION_STORE_H -#include "keyPair.h" - #include #include #include - -#include #include #include +#include #include #include +#include "keyPair.h" + class ConnectionStore final : public QObject { Q_OBJECT Q_PROPERTY(QString host READ host WRITE setHost NOTIFY hostChanged) Q_PROPERTY(int port READ port WRITE setPort NOTIFY portChanged) Q_PROPERTY(QString userName READ userName WRITE setUserName NOTIFY userNameChanged) - Q_PROPERTY(QString selectedKeyName READ selectedKeyName WRITE setSelectedKeyName NOTIFY selectedKeyNameChanged) + Q_PROPERTY(QString selectedKeyName READ selectedKeyName WRITE setSelectedKeyName NOTIFY + selectedKeyNameChanged) Q_PROPERTY(QStringList availableKeyNames READ availableKeyNames NOTIFY availableKeyNamesChanged) Q_PROPERTY(QString errorText READ errorText NOTIFY errorTextChanged) - Q_PROPERTY(bool keyGenerationInProgress READ keyGenerationInProgress NOTIFY keyGenerationInProgressChanged) + Q_PROPERTY(bool keyGenerationInProgress READ keyGenerationInProgress NOTIFY + keyGenerationInProgressChanged) public: explicit ConnectionStore(QObject* parent = nullptr); @@ -45,9 +46,7 @@ class ConnectionStore final : public QObject { Q_INVOKABLE void cancelKeyPairCreation(); Q_INVOKABLE bool deleteKeyPair(const QString& name); Q_INVOKABLE void clearErrorText(); - Q_INVOKABLE bool saveLastConnection(const QString& host, - int port, - const QString& userName, + Q_INVOKABLE bool saveLastConnection(const QString& host, int port, const QString& userName, const QString& selectedKeyName); signals: diff --git a/src/client/network_manager.cpp b/src/client/network_manager.cpp index c85c9e2..39a5362 100644 --- a/src/client/network_manager.cpp +++ b/src/client/network_manager.cpp @@ -1,17 +1,16 @@ #include "network_manager.h" -#include "connection_store.h" -#include "signature.h" - #include #include #include #include #include - #include #include +#include "connection_store.h" +#include "signature.h" + using messenger::protocol::CurrentProtocolVersion; using messenger::protocol::DataStreamVersion; using messenger::protocol::DefaultPort; @@ -63,17 +62,11 @@ bool NetworkManager::busy() const { return connectionState_ != ConnectionState::Disconnected && !connected(); } -QString NetworkManager::statusText() const { - return statusText_; -} +QString NetworkManager::statusText() const { return statusText_; } -QString NetworkManager::userName() const { - return userName_; -} +QString NetworkManager::userName() const { return userName_; } -int NetworkManager::defaultPort() const { - return DefaultPort; -} +int NetworkManager::defaultPort() const { return DefaultPort; } void NetworkManager::setUserName(const QString& userName) { const auto trimmedUserName = userName.trimmed(); @@ -210,7 +203,8 @@ void NetworkManager::readAvailable() { } if (message.protocolVersion != CurrentProtocolVersion) { - const auto errorMessage = tr("Unsupported protocol version: %1").arg(message.protocolVersion); + const auto errorMessage = + tr("Unsupported protocol version: %1").arg(message.protocolVersion); setStatusText(errorMessage); emit connectionError(errorMessage); socket_.disconnectFromHost(); @@ -224,13 +218,12 @@ void NetworkManager::readAvailable() { } else if (messageType == MessageType::AuthSuccess) { handleAuthenticationSuccess(message); } else if (messageType == MessageType::AuthFailure) { - failAuthentication(message.text.isEmpty() - ? tr("Authentication failed.") - : message.text); + failAuthentication(message.text.isEmpty() ? tr("Authentication failed.") + : message.text); } else if (messageType == MessageType::RegistrationPending) { - failAuthentication(tr( - "You do not have access yet. A registration request was created and must " - "be approved on the server. Please try again afterwards.")); + failAuthentication( + tr("You do not have access yet. A registration request was created and must " + "be approved on the server. Please try again afterwards.")); } else if (messageType == MessageType::RegistrationRejected) { failAuthentication(tr("Your registration request was rejected on the server.")); } else { @@ -239,9 +232,8 @@ void NetworkManager::readAvailable() { continue; } - if (messageType != MessageType::ChatMessage - && messageType != MessageType::SystemMessage - && messageType != MessageType::ErrorMessage) { + if (messageType != MessageType::ChatMessage && messageType != MessageType::SystemMessage && + messageType != MessageType::ErrorMessage) { const auto errorMessage = tr("Server sent an unexpected message."); setStatusText(errorMessage); emit connectionError(errorMessage); @@ -249,8 +241,7 @@ void NetworkManager::readAvailable() { return; } - emit messageReceived(message.senderName, - message.text, + emit messageReceived(message.senderName, message.text, message.timestamp.toLocalTime().toString(QStringLiteral("HH:mm"))); } } @@ -303,9 +294,9 @@ void NetworkManager::sendAuthenticationHello() { } void NetworkManager::handleAuthenticationChallenge(const Message& message) { - if (connectionState_ != ConnectionState::AwaitingChallenge - || message.authenticationId.size() != messenger::protocol::AuthenticationIdSize - || message.serverNonce.size() != messenger::protocol::AuthenticationNonceSize) { + if (connectionState_ != ConnectionState::AwaitingChallenge || + message.authenticationId.size() != messenger::protocol::AuthenticationIdSize || + message.serverNonce.size() != messenger::protocol::AuthenticationNonceSize) { failAuthentication(tr("The server sent an invalid authentication challenge.")); return; } @@ -316,16 +307,15 @@ void NetworkManager::handleAuthenticationChallenge(const Message& message) { setStatusText(tr("Challenge received. Signing authentication proof ...")); QTimer::singleShot(0, this, [this]() { - if (connectionState_ != ConnectionState::SigningChallenge - || !authenticationKeyPair_) { + if (connectionState_ != ConnectionState::SigningChallenge || !authenticationKeyPair_) { return; } const auto transcript = messenger::protocol::authenticationTranscript( authenticationUserName_, authenticationId_, clientNonce_, serverNonce_); const auto digest = QCryptographicHash::hash(transcript, QCryptographicHash::Sha256); - const auto signature = core::signature::signDigest( - authenticationKeyPair_->getPrivateKey(), toByteVector(digest)); + const auto signature = core::signature::signDigest(authenticationKeyPair_->getPrivateKey(), + toByteVector(digest)); if (signature.empty()) { failAuthentication(tr("The RSA authentication proof could not be created.")); return; @@ -352,8 +342,7 @@ void NetworkManager::handleAuthenticationSuccess(const Message& message) { } authenticationTimer_.stop(); - if (!message.senderName.trimmed().isEmpty() - && userName_ != message.senderName.trimmed()) { + if (!message.senderName.trimmed().isEmpty() && userName_ != message.senderName.trimmed()) { userName_ = message.senderName.trimmed(); emit userNameChanged(); } diff --git a/src/client/network_manager.h b/src/client/network_manager.h index e11f8e1..1069960 100644 --- a/src/client/network_manager.h +++ b/src/client/network_manager.h @@ -1,16 +1,14 @@ #ifndef MESSENGER_NETWORK_MANAGER_H #define MESSENGER_NETWORK_MANAGER_H -#include "message.h" - #include #include #include #include - #include #include "keyPair.h" +#include "message.h" class ConnectionStore; diff --git a/src/server/Storage/message_store.cpp b/src/server/Storage/message_store.cpp index af1ee14..9ffc6d4 100644 --- a/src/server/Storage/message_store.cpp +++ b/src/server/Storage/message_store.cpp @@ -10,7 +10,6 @@ #include #include #include - #include using messenger::protocol::CurrentProtocolVersion; @@ -21,9 +20,7 @@ namespace { constexpr auto InvalidUserId = -1; -QString lastErrorText(const QSqlQuery& query) { - return query.lastError().text(); -} +QString lastErrorText(const QSqlQuery& query) { return query.lastError().text(); } QList splitSqlStatements(const QString& script) { QList statements; @@ -43,7 +40,8 @@ QList splitSqlStatements(const QString& script) { continue; } - if (!inSingleQuote && !inDoubleQuote && currentChar == QLatin1Char('-') && nextChar == QLatin1Char('-')) { + if (!inSingleQuote && !inDoubleQuote && currentChar == QLatin1Char('-') && + nextChar == QLatin1Char('-')) { inLineComment = true; ++i; continue; @@ -77,8 +75,7 @@ QList splitSqlStatements(const QString& script) { } // namespace -MessageStore::MessageStore() - : connectionName_(QStringLiteral("messenger_server_storage")) {} +MessageStore::MessageStore() : connectionName_(QStringLiteral("messenger_server_storage")) {} MessageStore::~MessageStore() { if (QCoreApplication::instance() == nullptr) { @@ -194,13 +191,13 @@ QList MessageStore::loadMessages() const { message.senderName = query.value(QStringLiteral("username")).toString(); const auto storedMessageType = query.value(QStringLiteral("message_type")).toUInt(); switch (storedMessageType) { - case 1: // Protocol version 1 ChatMessage + case 1: // Protocol version 1 ChatMessage message.messageType = static_cast(MessageType::ChatMessage); break; - case 2: // Protocol version 1 SystemMessage + case 2: // Protocol version 1 SystemMessage message.messageType = static_cast(MessageType::SystemMessage); break; - case 3: // Protocol version 1 ErrorMessage + case 3: // Protocol version 1 ErrorMessage message.messageType = static_cast(MessageType::ErrorMessage); break; case static_cast(MessageType::ChatMessage): @@ -209,13 +206,12 @@ QList MessageStore::loadMessages() const { message.messageType = storedMessageType; break; default: - qWarning() << "Skipping stored message with unsupported type" - << storedMessageType; + qWarning() << "Skipping stored message with unsupported type" << storedMessageType; continue; } message.text = query.value(QStringLiteral("body")).toString(); - message.timestamp = QDateTime::fromString(query.value(QStringLiteral("client_timestamp")).toString(), - Qt::ISODateWithMs); + message.timestamp = QDateTime::fromString( + query.value(QStringLiteral("client_timestamp")).toString(), Qt::ISODateWithMs); if (!message.timestamp.isValid()) { message.timestamp = QDateTime::currentDateTimeUtc(); } @@ -237,8 +233,8 @@ std::optional MessageStore::findUserForAuthentication( } QSqlQuery query(QSqlDatabase::database(connectionName_)); - query.prepare(QStringLiteral( - "SELECT id, username, public_key FROM users WHERE username = :username")); + query.prepare( + QStringLiteral("SELECT id, username, public_key FROM users WHERE username = :username")); query.bindValue(QStringLiteral(":username"), userName.trimmed()); if (!query.exec()) { @@ -257,10 +253,9 @@ std::optional MessageStore::findUserForAuthentication( }; } -RegistrationRequestResult MessageStore::requestRegistration( - const QString& userName, - const QByteArray& publicKey, - const QString& sourceAddress) const { +RegistrationRequestResult MessageStore::requestRegistration(const QString& userName, + const QByteArray& publicKey, + const QString& sourceAddress) const { if (!initialized_) { return {}; } @@ -281,9 +276,8 @@ RegistrationRequestResult MessageStore::requestRegistration( if (existingQuery.next()) { const auto status = existingQuery.value(QStringLiteral("status")).toString(); return { - status == QStringLiteral("rejected") - ? RegistrationRequestResult::Status::Rejected - : RegistrationRequestResult::Status::Pending, + status == QStringLiteral("rejected") ? RegistrationRequestResult::Status::Rejected + : RegistrationRequestResult::Status::Pending, existingQuery.value(QStringLiteral("id")).toLongLong(), }; } @@ -606,8 +600,10 @@ bool MessageStore::runMigrations() { return false; } - const QDir migrationsDirectory(QCoreApplication::applicationDirPath() + QStringLiteral("/Migrations")); - const auto migrationFiles = migrationsDirectory.entryList({QStringLiteral("*.sql")}, QDir::Files, QDir::Name); + const QDir migrationsDirectory(QCoreApplication::applicationDirPath() + + QStringLiteral("/Migrations")); + const auto migrationFiles = + migrationsDirectory.entryList({QStringLiteral("*.sql")}, QDir::Files, QDir::Name); if (migrationFiles.isEmpty()) { qWarning() << "No database migrations found in" << migrationsDirectory.absolutePath(); @@ -618,10 +614,12 @@ bool MessageStore::runMigrations() { const auto version = migrationFile.section(QLatin1Char('_'), 0, 0); QSqlQuery appliedQuery(database); - appliedQuery.prepare(QStringLiteral("SELECT COUNT(*) FROM schema_migrations WHERE version = :version")); + appliedQuery.prepare( + QStringLiteral("SELECT COUNT(*) FROM schema_migrations WHERE version = :version")); appliedQuery.bindValue(QStringLiteral(":version"), version); if (!appliedQuery.exec() || !appliedQuery.next()) { - qWarning() << "Could not check migration state for" << migrationFile << lastErrorText(appliedQuery); + qWarning() << "Could not check migration state for" << migrationFile + << lastErrorText(appliedQuery); return false; } @@ -656,13 +654,15 @@ bool MessageStore::runMigrations() { QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs)); if (!insertQuery.exec()) { - qWarning() << "Could not record migration" << migrationFile << lastErrorText(insertQuery); + qWarning() << "Could not record migration" << migrationFile + << lastErrorText(insertQuery); database.rollback(); return false; } if (!database.commit()) { - qWarning() << "Could not commit migration" << migrationFile << database.lastError().text(); + qWarning() << "Could not commit migration" << migrationFile + << database.lastError().text(); database.rollback(); return false; } diff --git a/src/server/Storage/message_store.h b/src/server/Storage/message_store.h index bae7985..a9ac53c 100644 --- a/src/server/Storage/message_store.h +++ b/src/server/Storage/message_store.h @@ -1,14 +1,13 @@ #ifndef MESSENGER_MESSAGE_STORE_H #define MESSENGER_MESSAGE_STORE_H -#include "message.h" - #include #include #include - #include +#include "message.h" + struct UserAuthenticationRecord { int userId; QString userName; @@ -56,10 +55,9 @@ class MessageStore { bool hasUser(const QString& userName) const; [[nodiscard]] std::optional findUserForAuthentication( const QString& userName) const; - [[nodiscard]] RegistrationRequestResult requestRegistration( - const QString& userName, - const QByteArray& publicKey, - const QString& sourceAddress) const; + [[nodiscard]] RegistrationRequestResult requestRegistration(const QString& userName, + const QByteArray& publicKey, + const QString& sourceAddress) const; [[nodiscard]] QList pendingRegistrationRequests() const; bool approveRegistrationRequest(qint64 requestId) const; bool rejectRegistrationRequest(qint64 requestId) const; diff --git a/src/server/server.cpp b/src/server/server.cpp index b72d9ed..0420305 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -1,22 +1,21 @@ #include "server.h" -#include "session.h" -#include "keyPair.h" -#include "signature.h" - #include #include #include #include #include #include - -#include #include #include #include +#include #include +#include "keyPair.h" +#include "session.h" +#include "signature.h" + using messenger::protocol::CurrentProtocolVersion; using messenger::protocol::DefaultPort; using messenger::protocol::Message; @@ -25,9 +24,9 @@ using messenger::protocol::MessageType; namespace { bool isValidChatMessage(const Message& message) { - return message.messageType == static_cast(MessageType::ChatMessage) - && !message.text.trimmed().isEmpty() - && message.text.size() <= messenger::protocol::MaximumMessageSize; + return message.messageType == static_cast(MessageType::ChatMessage) && + !message.text.trimmed().isEmpty() && + message.text.size() <= messenger::protocol::MaximumMessageSize; } std::vector toByteVector(const QByteArray& bytes) { @@ -40,10 +39,8 @@ std::optional deserializePublicKey(const QByteArray& serializedKey) { PublicKey publicKey; const auto bytes = toByteVector(serializedKey); const operations::BigInt one(1); - if (!keyPair::s_deserialize(bytes, publicKey.n, publicKey.e) - || publicKey.n <= one - || publicKey.e <= one - || publicKey.serialize() != bytes) { + if (!keyPair::s_deserialize(bytes, publicKey.n, publicKey.e) || publicKey.n <= one || + publicKey.e <= one || publicKey.serialize() != bytes) { return std::nullopt; } return publicKey; @@ -64,9 +61,7 @@ server::server() { connect(&tcpServer_, &QTcpServer::acceptError, this, &server::handleAcceptError); } -server::~server() { - shutdown(); -} +server::~server() { shutdown(); } void server::shutdown() { tcpServer_.close(); @@ -101,7 +96,6 @@ void server::handleNewConnection() { connect(session, &Session::messageReceived, this, &server::handleMessageReceived); connect(session, &Session::disconnected, this, &server::handleSessionDisconnected); - } } @@ -111,7 +105,8 @@ void server::handleAcceptError(QAbstractSocket::SocketError socketError) { void server::handleMessageReceived(const Message& message, Session* session) { if (message.protocolVersion != CurrentProtocolVersion) { - qWarning() << "Ignoring message with unsupported protocol version" << message.protocolVersion; + qWarning() << "Ignoring message with unsupported protocol version" + << message.protocolVersion; return; } @@ -125,9 +120,9 @@ void server::handleMessageReceived(const Message& message, Session* session) { } const auto requestedUserName = message.senderName.trimmed(); - if (requestedUserName.isEmpty() - || requestedUserName.size() > messenger::protocol::MaximumUserNameSize - || message.clientNonce.size() != messenger::protocol::AuthenticationNonceSize) { + if (requestedUserName.isEmpty() || + requestedUserName.size() > messenger::protocol::MaximumUserNameSize || + message.clientNonce.size() != messenger::protocol::AuthenticationNonceSize) { qWarning() << "Rejecting malformed AuthHello"; session->rejectAuthentication(); return; @@ -146,11 +141,11 @@ void server::handleMessageReceived(const Message& message, Session* session) { requestedUserName, message.publicKey, session->peerAddress()); switch (request.status) { case RegistrationRequestResult::Status::Created: - qInfo().noquote() - << QStringLiteral("New registration request %1 for \"%2\" from %3. " - "Run 'Messenger-Server requests' to review it.") - .arg(request.requestId) - .arg(requestedUserName, session->peerAddress()); + qInfo().noquote() << QStringLiteral( + "New registration request %1 for \"%2\" from %3. " + "Run 'Messenger-Server requests' to review it.") + .arg(request.requestId) + .arg(requestedUserName, session->peerAddress()); [[fallthrough]]; case RegistrationRequestResult::Status::Pending: session->rejectAuthentication( @@ -180,12 +175,8 @@ void server::handleMessageReceived(const Message& message, Session* session) { messenger::protocol::AuthenticationIdSize); const auto serverNonce = messenger::protocol::generateSecureRandomBytes( messenger::protocol::AuthenticationNonceSize); - session->beginAuthentication(user->userId, - user->userName, - *publicKey, - authenticationId, - message.clientNonce, - serverNonce); + session->beginAuthentication(user->userId, user->userName, *publicKey, authenticationId, + message.clientNonce, serverNonce); Message challenge; challenge.messageType = static_cast(MessageType::AuthChallenge); @@ -197,26 +188,22 @@ void server::handleMessageReceived(const Message& message, Session* session) { } if (session->authenticationState() == Session::AuthenticationState::AwaitingProof) { - if (messageType != MessageType::AuthProof - || session->authenticationExpired() - || message.authenticationId.size() != messenger::protocol::AuthenticationIdSize - || message.authenticationId != session->authenticationId() - || message.signature.isEmpty()) { + if (messageType != MessageType::AuthProof || session->authenticationExpired() || + message.authenticationId.size() != messenger::protocol::AuthenticationIdSize || + message.authenticationId != session->authenticationId() || + message.signature.isEmpty()) { qWarning() << "Rejecting malformed or expired AuthProof for" << session->userName(); session->rejectAuthentication(); return; } const auto transcript = messenger::protocol::authenticationTranscript( - session->userName(), - session->authenticationId(), - session->clientNonce(), + session->userName(), session->authenticationId(), session->clientNonce(), session->serverNonce()); const auto digest = QCryptographicHash::hash(transcript, QCryptographicHash::Sha256); - const auto signatureValid = core::signature::verifyDigest( - session->authenticationPublicKey(), - toByteVector(digest), - toByteVector(message.signature)); + const auto signatureValid = + core::signature::verifyDigest(session->authenticationPublicKey(), toByteVector(digest), + toByteVector(message.signature)); if (!signatureValid) { qWarning() << "Authentication signature verification failed for" << session->userName(); session->rejectAuthentication(); @@ -276,8 +263,8 @@ void server::handleMessageReceived(const Message& message, Session* session) { QList deletedUserSessions; for (auto* connectedSession : std::as_const(sessions_)) { - if (connectedSession->isAuthenticated() - && !messageStore_.hasUser(connectedSession->userName())) { + if (connectedSession->isAuthenticated() && + !messageStore_.hasUser(connectedSession->userName())) { deletedUserSessions.append(connectedSession); } else if (connectedSession->isAuthenticated()) { connectedSession->sendMessage(verifiedMessage); @@ -296,17 +283,16 @@ void server::handleSessionDisconnected(Session* session) { namespace { void printServerUsage(const QString& executable) { - qInfo().noquote() - << QStringLiteral( - "Usage:\n" - " %1 Start the server\n" - " %1 requests List pending registration requests\n" - " %1 approve Approve a registration request\n" - " %1 reject Reject a registration request\n" - " %1 users List all users\n" - " %1 delete-user Delete a user and all associated data\n" - " %1 clear-history Delete all stored chat messages") - .arg(executable); + qInfo().noquote() << QStringLiteral( + "Usage:\n" + " %1 Start the server\n" + " %1 requests List pending registration requests\n" + " %1 approve Approve a registration request\n" + " %1 reject Reject a registration request\n" + " %1 users List all users\n" + " %1 delete-user Delete a user and all associated data\n" + " %1 clear-history Delete all stored chat messages") + .arg(executable); } int runAdministrationCommand(const QStringList& arguments) { @@ -325,17 +311,15 @@ int runAdministrationCommand(const QStringList& arguments) { } for (const auto& request : requests) { - const auto fingerprint = QCryptographicHash::hash( - request.publicKey, QCryptographicHash::Sha256) - .toHex(':') - .toUpper(); - qInfo().noquote() - << QStringLiteral("[%1] %2\n From: %3\n Created: %4\n Key: %5") - .arg(request.requestId) - .arg(request.userName, - request.sourceAddress, - request.createdAt, - QString::fromLatin1(fingerprint)); + const auto fingerprint = + QCryptographicHash::hash(request.publicKey, QCryptographicHash::Sha256) + .toHex(':') + .toUpper(); + qInfo().noquote() << QStringLiteral( + "[%1] %2\n From: %3\n Created: %4\n Key: %5") + .arg(request.requestId) + .arg(request.userName, request.sourceAddress, + request.createdAt, QString::fromLatin1(fingerprint)); } return 0; } @@ -348,16 +332,15 @@ int runAdministrationCommand(const QStringList& arguments) { } for (const auto& user : users) { - qInfo().noquote() - << QStringLiteral("[%1] %2\n Created: %3") - .arg(user.userId) - .arg(user.userName, user.createdAt); + qInfo().noquote() << QStringLiteral("[%1] %2\n Created: %3") + .arg(user.userId) + .arg(user.userName, user.createdAt); } return 0; } - if ((command == QStringLiteral("approve") || command == QStringLiteral("reject")) - && arguments.size() == 3) { + if ((command == QStringLiteral("approve") || command == QStringLiteral("reject")) && + arguments.size() == 3) { bool validId = false; const auto requestId = arguments.at(2).toLongLong(&validId); if (!validId || requestId <= 0) { diff --git a/src/server/server.h b/src/server/server.h index 84e3686..323c730 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -1,14 +1,14 @@ #ifndef MESSENGER_SERVER_H #define MESSENGER_SERVER_H -#include "message.h" -#include "Storage/message_store.h" - #include #include #include #include +#include "Storage/message_store.h" +#include "message.h" + class Session; class QHostAddress; diff --git a/src/server/session.cpp b/src/server/session.cpp index 0329169..a327b5e 100644 --- a/src/server/session.cpp +++ b/src/server/session.cpp @@ -2,15 +2,15 @@ #include #include -#include #include +#include +using messenger::protocol::AuthenticationTimeoutMs; using messenger::protocol::CurrentProtocolVersion; using messenger::protocol::DataStreamVersion; using messenger::protocol::Message; using messenger::protocol::MessageType; using messenger::protocol::ReadBufferSize; -using messenger::protocol::AuthenticationTimeoutMs; Session::Session(QTcpSocket* socket, QObject* parent) : QObject(parent), socket_(socket), stream_(socket) { @@ -47,11 +47,8 @@ void Session::sendMessage(const Message& message) { socket_->flush(); } -void Session::beginAuthentication(int userId, - const QString& userName, - const PublicKey& publicKey, - const QByteArray& authenticationId, - const QByteArray& clientNonce, +void Session::beginAuthentication(int userId, const QString& userName, const PublicKey& publicKey, + const QByteArray& authenticationId, const QByteArray& clientNonce, const QByteArray& serverNonce) { if (authenticationState_ != AuthenticationState::AwaitingHello) { return; @@ -98,49 +95,29 @@ void Session::rejectAuthentication(const QString& reason, MessageType messageTyp socket_->disconnectFromHost(); } -void Session::disconnectFromHost() { - socket_->disconnectFromHost(); -} +void Session::disconnectFromHost() { socket_->disconnectFromHost(); } -Session::AuthenticationState Session::authenticationState() const { - return authenticationState_; -} +Session::AuthenticationState Session::authenticationState() const { return authenticationState_; } bool Session::isAuthenticated() const { return authenticationState_ == AuthenticationState::Authenticated; } -bool Session::authenticationExpired() const { - return authenticationDeadline_.hasExpired(); -} +bool Session::authenticationExpired() const { return authenticationDeadline_.hasExpired(); } -int Session::userId() const { - return userId_; -} +int Session::userId() const { return userId_; } -QString Session::userName() const { - return userName_; -} +QString Session::userName() const { return userName_; } -const PublicKey& Session::authenticationPublicKey() const { - return authenticationPublicKey_; -} +const PublicKey& Session::authenticationPublicKey() const { return authenticationPublicKey_; } -const QByteArray& Session::authenticationId() const { - return authenticationId_; -} +const QByteArray& Session::authenticationId() const { return authenticationId_; } -const QByteArray& Session::clientNonce() const { - return clientNonce_; -} +const QByteArray& Session::clientNonce() const { return clientNonce_; } -const QByteArray& Session::serverNonce() const { - return serverNonce_; -} +const QByteArray& Session::serverNonce() const { return serverNonce_; } -QString Session::peerAddress() const { - return socket_->peerAddress().toString(); -} +QString Session::peerAddress() const { return socket_->peerAddress().toString(); } void Session::readAvailable() { while (socket_->bytesAvailable() > 0) { @@ -154,8 +131,8 @@ void Session::readAvailable() { } if (message.protocolVersion != CurrentProtocolVersion) { - qWarning() << "Unsupported protocol version" << message.protocolVersion - << "from" << socket_->peerAddress().toString(); + qWarning() << "Unsupported protocol version" << message.protocolVersion << "from" + << socket_->peerAddress().toString(); socket_->disconnectFromHost(); return; } @@ -165,7 +142,8 @@ void Session::readAvailable() { } void Session::handleDisconnected() { - qInfo() << "Client disconnected from" << socket_->peerAddress().toString() << socket_->peerPort(); + qInfo() << "Client disconnected from" << socket_->peerAddress().toString() + << socket_->peerPort(); emit disconnected(this); } diff --git a/src/server/session.h b/src/server/session.h index bf6f7c7..45d4f62 100644 --- a/src/server/session.h +++ b/src/server/session.h @@ -1,14 +1,14 @@ #ifndef MESSENGER_SESSION_H #define MESSENGER_SESSION_H -#include "message.h" -#include "keyPair.h" - #include #include #include #include +#include "keyPair.h" +#include "message.h" + class QTcpSocket; class Session final : public QObject { @@ -25,16 +25,13 @@ class Session final : public QObject { explicit Session(QTcpSocket* socket, QObject* parent = nullptr); void sendMessage(const messenger::protocol::Message& message); - void beginAuthentication(int userId, - const QString& userName, - const PublicKey& publicKey, - const QByteArray& authenticationId, - const QByteArray& clientNonce, + void beginAuthentication(int userId, const QString& userName, const PublicKey& publicKey, + const QByteArray& authenticationId, const QByteArray& clientNonce, const QByteArray& serverNonce); void completeAuthentication(); - void rejectAuthentication( - const QString& reason = QStringLiteral("Authentication failed."), - messenger::protocol::MessageType messageType = messenger::protocol::MessageType::AuthFailure); + void rejectAuthentication(const QString& reason = QStringLiteral("Authentication failed."), + messenger::protocol::MessageType messageType = + messenger::protocol::MessageType::AuthFailure); void disconnectFromHost(); [[nodiscard]] AuthenticationState authenticationState() const; diff --git a/src/shared/message.cpp b/src/shared/message.cpp index 3f4416c..e57ae8a 100644 --- a/src/shared/message.cpp +++ b/src/shared/message.cpp @@ -35,11 +35,8 @@ QDataStream& operator>>(QDataStream& in, Message& message) { return in; } -QByteArray authenticationTranscript( - const QString& userName, - const QByteArray& authenticationId, - const QByteArray& clientNonce, - const QByteArray& serverNonce) { +QByteArray authenticationTranscript(const QString& userName, const QByteArray& authenticationId, + const QByteArray& clientNonce, const QByteArray& serverNonce) { QByteArray transcript; QDataStream stream(&transcript, QIODevice::WriteOnly); stream.setVersion(DataStreamVersion); diff --git a/src/shared/message.h b/src/shared/message.h index 67b4faf..221741c 100644 --- a/src/shared/message.h +++ b/src/shared/message.h @@ -1,9 +1,9 @@ #ifndef MESSENGER_MESSAGE_H #define MESSENGER_MESSAGE_H +#include #include #include -#include #include #include @@ -48,11 +48,10 @@ struct Message { QDataStream& operator<<(QDataStream& out, const Message& message); QDataStream& operator>>(QDataStream& in, Message& message); -[[nodiscard]] QByteArray authenticationTranscript( - const QString& userName, - const QByteArray& authenticationId, - const QByteArray& clientNonce, - const QByteArray& serverNonce); +[[nodiscard]] QByteArray authenticationTranscript(const QString& userName, + const QByteArray& authenticationId, + const QByteArray& clientNonce, + const QByteArray& serverNonce); [[nodiscard]] QByteArray generateSecureRandomBytes(qsizetype size);