feat(ipc): Unrecognised IPC client, retry messages, and TLS fingerprint

Also adds version checking for IPC server and client
This commit is contained in:
Nick Bolton 2026-03-26 14:17:56 +00:00
parent 66c9b6607c
commit 59a4747113
28 changed files with 170 additions and 826 deletions

View file

@ -14,7 +14,6 @@
#include "base/NetworkProtocol.h"
#include "client/ServerProxy.h"
#include "common/Settings.h"
#include "deskflow/ipc/CoreIpc.h"
#include "deskflow/Clipboard.h"
#include "deskflow/IPlatformScreen.h"
#include "deskflow/PacketStreamFilter.h"
@ -22,11 +21,14 @@
#include "deskflow/ProtocolUtil.h"
#include "deskflow/Screen.h"
#include "deskflow/StreamChunker.h"
#include "deskflow/ipc/CoreIpc.h"
#include "net/IDataSocket.h"
#include "net/ISocketFactory.h"
#include "net/SecureSocket.h"
#include "net/TCPSocket.h"
#include <QMetaEnum>
#include <cstdlib>
#include <cstring>
@ -133,8 +135,11 @@ void Client::disconnect(const char *msg)
}
}
void Client::refuseConnection(const char *msg)
void Client::refuseConnection(deskflow::core::ConnectionRefusal reason, const char *msg)
{
const auto metaEnum = QMetaEnum::fromType<deskflow::core::ConnectionRefusal>();
ipcSendToClient("connectionRefused", metaEnum.valueToKey(static_cast<int>(reason)));
cleanup();
if (msg) {

View file

@ -11,6 +11,7 @@
#include "deskflow/IClient.h"
#include "base/EventTypes.h"
#include "common/Enums.h"
#include "deskflow/IClipboard.h"
#include "net/NetworkAddress.h"
@ -88,7 +89,7 @@ public:
Disconnects from the server with an optional error message.
Unlike disconnect this function doesn't try to use other ip addresses
*/
void refuseConnection(const char *msg);
void refuseConnection(deskflow::core::ConnectionRefusal reason, const char *msg);
//! Notify of handshake complete
/*!

View file

@ -124,6 +124,7 @@ void ServerProxy::handleData()
ServerProxy::ConnectionResult ServerProxy::parseHandshakeMessage(const uint8_t *code)
{
using enum ConnectionResult;
using enum deskflow::core::ConnectionRefusal;
if (memcmp(code, kMsgQInfo, 4) == 0) {
queryInfo();
@ -168,25 +169,25 @@ ServerProxy::ConnectionResult ServerProxy::parseHandshakeMessage(const uint8_t *
int32_t minor;
ProtocolUtil::readf(m_stream, kMsgEIncompatible + 4, &major, &minor);
LOG_ERR("server has incompatible version %d.%d", major, minor);
m_client->refuseConnection("server has incompatible version");
m_client->refuseConnection(IncompatibleVersion, "server has incompatible version");
return Disconnect;
}
else if (memcmp(code, kMsgEBusy, 4) == 0) {
LOG_ERR("server already has a connected client with name \"%s\"", m_client->getName().c_str());
m_client->refuseConnection("server already has a connected client with our name");
m_client->refuseConnection(AlreadyConnected, "server already has a connected client with our name");
return Disconnect;
}
else if (memcmp(code, kMsgEUnknown, 4) == 0) {
LOG_ERR("server refused client with name \"%s\"", m_client->getName().c_str());
m_client->refuseConnection("server refused client with our name");
m_client->refuseConnection(UnknownClient, "server refused client with our name");
return Disconnect;
}
else if (memcmp(code, kMsgEBad, 4) == 0) {
LOG_ERR("server disconnected due to a protocol error");
m_client->refuseConnection("server reported a protocol error");
m_client->refuseConnection(ProtocolError, "server reported a protocol error");
return Disconnect;
} else if (memcmp(code, kMsgDLanguageSynchronisation, 4) == 0) {
setServerLanguages();

View file

@ -43,4 +43,14 @@ enum class ConnectionState
Listening
};
Q_ENUM_NS(ConnectionState)
enum class ConnectionRefusal
{
IncompatibleVersion,
AlreadyConnected,
UnknownClient,
ProtocolError
};
Q_ENUM_NS(ConnectionRefusal)
} // namespace deskflow::core

View file

@ -7,6 +7,7 @@
#include "IpcServer.h"
#include "base/Log.h"
#include "common/VersionInfo.h"
#include <QLocalServer>
#include <QLocalSocket>
@ -110,8 +111,21 @@ void IpcServer::processMessage(QLocalSocket *clientSocket, const QString &messag
}
if (const auto &command = parts.at(0); command == "hello") {
const auto versionId = QStringLiteral("%1+%2").arg(kVersion, kVersionGitSha);
const auto clientVersion = parts.size() >= 2 ? parts.at(1) : QString();
if (clientVersion != versionId) {
LOG_ERR(
"ipc client version mismatch (client: %s, server: %s)",
clientVersion.isEmpty() ? "unknown" : clientVersion.toUtf8().constData(), versionId.toUtf8().constData()
);
writeToClientSocket(clientSocket, "error");
clientSocket->flush();
clientSocket->disconnectFromServer();
return;
}
LOG_DEBUG("ipc server got hello message, sending hello back");
writeToClientSocket(clientSocket, "hello");
writeToClientSocket(clientSocket, QString("hello=%1").arg(versionId));
// Replay messages that were queued before any clients connected.
LOG_DEBUG1("ipc server replaying %d pending messages", m_pendingMessages.size());

View file

@ -39,7 +39,6 @@ add_library(${target} STATIC
TlsUtility.h
VersionChecker.cpp
VersionChecker.h
config/IServerConfig.h
config/Screen.cpp
config/Screen.h
config/ScreenConfig.cpp
@ -48,16 +47,10 @@ add_library(${target} STATIC
config/ScreenList.h
config/ServerConfig.cpp
config/ServerConfig.h
core/ClientConnection.cpp
core/ClientConnection.h
core/CoreProcess.cpp
core/CoreProcess.h
core/NetworkMonitor.cpp
core/NetworkMonitor.h
core/ServerConnection.cpp
core/ServerConnection.h
core/ServerMessage.cpp
core/ServerMessage.h
dialogs/AboutDialog.cpp
dialogs/AboutDialog.h
dialogs/AboutDialog.ui

View file

@ -58,8 +58,6 @@ using namespace deskflow::gui;
MainWindow::MainWindow()
: ui{std::make_unique<Ui::MainWindow>()},
m_coreProcess(m_serverConfig),
m_serverConnection(this, m_serverConfig),
m_clientConnection(this),
m_trayIcon{new QSystemTrayIcon(this)},
m_guiDupeChecker{new QLocalServer(this)},
m_daemonIpcClient{new ipc::DaemonIpcClient(this)},
@ -269,15 +267,11 @@ void MainWindow::connectSlots()
if (!deskflow::platform::isMac())
connect(m_trayIcon, &QSystemTrayIcon::activated, this, &MainWindow::trayIconActivated);
connect(&m_serverConnection, &ServerConnection::configureClient, this, &MainWindow::serverConnectionConfigureClient);
connect(&m_serverConnection, &ServerConnection::clientsChanged, this, &MainWindow::serverClientsChanged);
connect(&m_coreProcess, &CoreProcess::connectedClientsChanged, this, &MainWindow::serverClientsChanged);
connect(
&m_serverConnection, &ServerConnection::requestNewClientPrompt, this, &MainWindow::handleNewClientPromptRequest
);
connect(&m_clientConnection, &ClientConnection::requestShowError, this, &MainWindow::showClientError);
connect(&m_clientConnection, &ClientConnection::updateTimeoutDelay, this, &MainWindow::updateTimeoutDelay);
connect(&m_coreProcess, &CoreProcess::unrecognisedClient, this, &MainWindow::handleUnrecognisedClient);
connect(&m_coreProcess, &CoreProcess::connectionRefused, this, &MainWindow::handleConnectionRefused);
connect(&m_coreProcess, &CoreProcess::retryIn, this, &MainWindow::updateTimeoutDelay);
connect(&m_coreProcess, &CoreProcess::peerFingerprint, this, &MainWindow::handlePeerFingerprint);
if (Settings::value(Settings::Gui::AutoStartCore).toBool()) {
connect(ui->btnToggleCore, &QPushButton::clicked, m_actionStopCore, &QAction::trigger, Qt::UniqueConnection);
@ -430,8 +424,6 @@ void MainWindow::clearSettings()
m_networkMonitor->stopMonitoring();
disconnect(&m_coreProcess, nullptr, this, nullptr);
disconnect(&m_serverConnection, nullptr, this, nullptr);
disconnect(&m_clientConnection, nullptr, this, nullptr);
disconnect(&m_versionChecker, nullptr, this, nullptr);
disconnect(m_guiDupeChecker, nullptr, this, nullptr);
disconnect(m_trayIcon, nullptr, this, nullptr);
@ -598,12 +590,12 @@ void MainWindow::updateNetworkInfo()
void MainWindow::serverConnectionConfigureClient(const QString &clientName)
{
m_serverConnection.serverConfigDialogVisible(true);
m_serverConfigDialogVisible = true;
ServerConfigDialog dialog(this, m_serverConfig);
if (dialog.addClient(clientName) && dialog.exec() == QDialog::Accepted) {
m_coreProcess.restart();
}
m_serverConnection.serverConfigDialogVisible(false);
m_serverConfigDialogVisible = false;
}
//////////////////////////////////////////////////////////////////////////////
@ -755,35 +747,64 @@ void MainWindow::setTrayIcon()
void MainWindow::handleLogLine(const QString &line)
{
m_logDock->appendLine(line);
updateFromLogLine(line);
}
void MainWindow::updateFromLogLine(const QString &line)
void MainWindow::handleUnrecognisedClient(const QString &clientName)
{
checkConnected(line);
checkFingerprint(line);
}
void MainWindow::checkConnected(const QString &line)
{
if (ui->rbModeServer->isChecked()) {
m_serverConnection.handleLogLine(line);
} else {
m_clientConnection.handleLogLine(line);
if (m_ignoredClients.contains(clientName)) {
qDebug("ignoring %s:", qPrintable(clientName));
return;
}
}
void MainWindow::checkFingerprint(const QString &line)
{
static const auto tlsPeerMessage = QStringLiteral("peer fingerprint: ");
static const qsizetype msgLen = QString(tlsPeerMessage).length();
const qsizetype midStart = line.indexOf(tlsPeerMessage);
if (midStart == -1)
if (m_newClientPromptShowing || m_serverConfigDialogVisible)
return;
const auto sha256Text = line.mid(midStart + msgLen).remove(':');
if (Settings::value(Settings::Server::ExternalConfig).toBool())
return;
if (m_serverConfig.isFull() || m_serverConfig.screenExists(clientName))
return;
m_newClientPromptShowing = true;
showAndActivate();
if (deskflow::gui::messages::showNewClientPrompt(this, clientName)) {
serverConnectionConfigureClient(clientName);
} else {
m_ignoredClients.insert(clientName);
}
m_newClientPromptShowing = false;
}
void MainWindow::handleConnectionRefused(deskflow::core::ConnectionRefusal reason)
{
if (reason != deskflow::core::ConnectionRefusal::AlreadyConnected)
return;
if (!isVisible() || m_clientErrorVisible)
return;
m_clientErrorVisible = true;
showAndActivate();
const auto address = Settings::value(Settings::Client::RemoteHost).toString();
QMessageBox::warning(
this, tr("%1 Connection Error").arg(kAppName),
tr("<p>Failed to connect to the server '%1'.</p>"
"<p>A Client with your name is already connected to the server.</p>"
"Please ensure that you're using a unique name and that only a "
"single instance of the client process is running.</p>")
.arg(address)
);
m_clientErrorVisible = false;
}
void MainWindow::handlePeerFingerprint(const QString &fingerprint)
{
const auto sha256Text = QString(fingerprint).remove(':');
const Fingerprint sha256 = {QCryptographicHash::Sha256, QByteArray::fromHex(sha256Text.toLatin1())};
const bool isClient = m_coreProcess.mode() == CoreMode::Client;
@ -959,7 +980,7 @@ void MainWindow::changeEvent(QEvent *e)
updateModeControlLabels();
updateNetworkInfo();
updateStatus();
serverClientsChanged(m_serverConnection.connectedClients());
serverClientsChanged({});
updateText();
}
}
@ -1167,34 +1188,6 @@ void MainWindow::remoteHostChanged(const QString &newRemoteHost)
}
}
void MainWindow::showClientError(deskflow::client::ErrorType error, const QString &address)
{
if (!isVisible() || m_clientErrorVisible || error != deskflow::client::ErrorType::AlreadyConnected)
return;
m_clientErrorVisible = true;
showAndActivate();
QMessageBox::warning(
this, tr("%1 Connection Error").arg(kAppName),
tr("<p>Failed to connect to the server '%1'.</p>"
"<p>A Client with your name is already connected to the server.</p>"
"Please ensure that you're using a unique name and that only a "
"single instance of the client process is running.</p>")
.arg(address)
);
m_clientErrorVisible = false;
}
void MainWindow::handleNewClientPromptRequest(const QString &clientName)
{
showAndActivate();
bool result = deskflow::gui::messages::showNewClientPrompt(this, clientName);
m_serverConnection.handleNewClientResult(clientName, result);
}
void MainWindow::updateIpLabel(const QStringList &addresses)
{
if (m_coreProcess.mode() != CoreMode::Server) {

View file

@ -16,10 +16,8 @@
#include "VersionChecker.h"
#include "config/ServerConfig.h"
#include "gui/core/ClientConnection.h"
#include "gui/core/CoreProcess.h"
#include "gui/core/NetworkMonitor.h"
#include "gui/core/ServerConnection.h"
#include "net/Fingerprint.h"
#ifdef Q_OS_MACOS
@ -118,9 +116,9 @@ private:
void setupTrayIcon();
void applyConfig();
void setTrayIcon();
void updateFromLogLine(const QString &line);
void checkConnected(const QString &line);
void checkFingerprint(const QString &line);
void handleUnrecognisedClient(const QString &clientName);
void handleConnectionRefused(deskflow::core::ConnectionRefusal reason);
void handlePeerFingerprint(const QString &fingerprint);
void closeEvent(QCloseEvent *event) override;
void secureSocket(bool secureSocket);
void connectSlots();
@ -140,17 +138,10 @@ private:
void daemonIpcClientConnectionFailed();
void toggleCanRunCore(bool enableButtons);
void remoteHostChanged(const QString &newRemoteHost);
void handleNewClientPromptRequest(const QString &clientName);
void updateIpLabel(const QStringList &addresses);
void updateTimeoutDelay(int newDelay);
bool canRunCore() const;
/**
* @brief showClientError
* @param error Error Type
* @param address
*/
void showClientError(deskflow::client::ErrorType error, const QString &address);
/**
* @brief trustedFingerprintDatabase get the FingerprintDatabase for the trusted clients or trusted servers.
@ -177,8 +168,9 @@ private:
bool m_clientErrorVisible = false;
ServerConfig m_serverConfig;
deskflow::gui::CoreProcess m_coreProcess;
deskflow::gui::ServerConnection m_serverConnection;
deskflow::gui::ClientConnection m_clientConnection;
QSet<QString> m_ignoredClients;
bool m_newClientPromptShowing = false;
bool m_serverConfigDialogVisible = false;
QSize m_expandedSize = QSize();
QStringList m_checkedClients;
QStringList m_checkedServers;

View file

@ -1,28 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2024 Symless Ltd.
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#pragma once
#include <QFile>
#include <QString>
#include "ScreenList.h"
namespace deskflow::gui {
// TODO: Remove this class, when the ServerConnectionTests are ported to QTests
class IServerConfig
{
public:
virtual ~IServerConfig() = default;
virtual bool isFull() const = 0;
virtual bool screenExists(const QString &screenName) const = 0;
virtual bool save(const QString &fileName) const = 0;
virtual void save(QFile &file) const = 0;
virtual const ScreenList &screens() const = 0;
};
} // namespace deskflow::gui

View file

@ -9,7 +9,6 @@
#include "base/NetworkProtocol.h"
#include "gui/Hotkey.h"
#include "gui/config/IServerConfig.h"
#include "gui/config/ScreenConfig.h"
#include "gui/config/ScreenList.h"
@ -31,21 +30,18 @@ const auto kDefaultProtocol = NetworkProtocol::Barrier;
} // namespace deskflow::gui
class ServerConfig : public ScreenConfig, public deskflow::gui::IServerConfig
class ServerConfig : public ScreenConfig
{
friend class ServerConfigDialog;
friend QTextStream &operator<<(QTextStream &outStream, const ServerConfig &config);
public:
explicit ServerConfig(int columns = kDefaultColumns, int rows = kDefaultRows);
~ServerConfig() override = default;
~ServerConfig() = default;
bool operator==(const ServerConfig &sc) const;
//
// Overrides
//
const ScreenList &screens() const override
const ScreenList &screens() const
{
return m_Screens;
}
@ -131,17 +127,10 @@ public:
}
static size_t defaultClipboardSharingSize();
//
// Overrides
//
bool save(const QString &fileName) const override;
bool screenExists(const QString &screenName) const override;
void save(QFile &file) const override;
bool isFull() const override;
//
// New methods
//
bool save(const QString &fileName) const;
bool screenExists(const QString &screenName) const;
void save(QFile &file) const;
bool isFull() const;
void commit();
int numScreens() const;
QString getServerName() const;

View file

@ -1,75 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2025 Deskflow Developers
* SPDX-FileCopyrightText: (C) 2021 Symless Ltd.
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#include "ClientConnection.h"
#include "common/Settings.h"
#include <QHostAddress>
namespace deskflow::gui {
void ClientConnection::handleLogLine(const QString &logLine)
{
if (logLine.contains("disconnected from server")) {
m_supressMessage = false;
return;
}
if (logLine.contains("retry in ") && Settings::value(Settings::Client::DynamicConnectionRetry).toBool()) {
auto line = logLine.mid(logLine.indexOf("in ") + 3);
int seconds = line.mid(0, line.indexOf(" ")).toInt();
Q_EMIT updateTimeoutDelay(seconds);
return;
}
if (logLine.contains("connected to server")) {
m_supressMessage = true;
return;
}
if (logLine.contains("failed to connect to server")) {
if (m_supressMessage) {
qDebug("message already shown, skipping");
return;
}
// ignore the message if it's about the server refusing by name as
// this will trigger the server to show an 'add client' dialog.
if (logLine.contains("server refused client with our name")) {
qDebug("ignoring client name refused message");
return;
}
showMessage(logLine);
}
}
void ClientConnection::showMessage(const QString &logLine)
{
using enum deskflow::client::ErrorType;
if (logLine.isEmpty())
return;
const auto address = Settings::value(Settings::Client::RemoteHost).toString();
auto error = NoError;
if (logLine.contains("server already has a connected client with our name")) {
error = AlreadyConnected;
} else if (QHostAddress a(address); a.isNull()) {
qDebug("ip not detected, showing hostname error");
error = HostnameError;
} else {
qDebug("ip detected, showing generic error");
error = GenericError;
}
if (error == NoError)
return;
Q_EMIT requestShowError(error, address);
}
} // namespace deskflow::gui

View file

@ -1,48 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2025 Deskflow Developers
* SPDX-FileCopyrightText: (C) 2021 Symless Ltd.
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#pragma once
#include "common/Enums.h"
#include <QString>
#include <QWidget>
class QWidget;
namespace deskflow::gui {
class ClientConnection : public QObject
{
Q_OBJECT
public:
explicit ClientConnection(QWidget *parent) : m_pParent(parent)
{
// do nothing
}
void handleLogLine(const QString &line);
Q_SIGNALS:
/**
* @brief requestShowError, This signal is emitted when the client
* connection would like the owning process to report an error message
* @param error the type of error being reported
* @param address of the host
*/
void requestShowError(deskflow::client::ErrorType error, const QString &address);
void updateTimeoutDelay(int newTimeout);
private:
void showMessage(const QString &logLine);
QWidget *m_pParent;
bool m_supressMessage = false;
};
} // namespace deskflow::gui

View file

@ -99,7 +99,7 @@ QString CoreProcess::wrapIpv6(const QString &address)
// CoreProcess
//
CoreProcess::CoreProcess(const IServerConfig &serverConfig)
CoreProcess::CoreProcess(const ServerConfig &serverConfig)
: m_serverConfig(serverConfig),
m_daemonIpcClient{new ipc::DaemonIpcClient(this)}
{
@ -430,7 +430,7 @@ void CoreProcess::stop(std::optional<ProcessMode> processModeOption)
if (m_coreIpcClient) {
m_coreIpcClient->disconnectFromServer();
delete m_coreIpcClient;
m_coreIpcClient->deleteLater();
m_coreIpcClient = nullptr;
}
@ -548,6 +548,22 @@ void CoreProcess::onCoreIpcMessageReceived(const QString &command, const QString
m_secureSocketVersion = args;
Q_EMIT securityLevelChanged(args);
}
} else if (command == "unrecognisedClient") {
Q_EMIT unrecognisedClient(args);
} else if (command == "connectionRefused") {
const auto metaEnum = QMetaEnum::fromType<deskflow::core::ConnectionRefusal>();
bool ok = false;
const auto reason =
static_cast<deskflow::core::ConnectionRefusal>(metaEnum.keyToValue(args.toUtf8().constData(), &ok));
if (ok) {
Q_EMIT connectionRefused(reason);
} else {
qWarning("core ipc got unknown connection refusal: %s", args.toUtf8().constData());
}
} else if (command == "retryIn") {
Q_EMIT retryIn(args.toInt());
} else if (command == "peerFingerprint") {
Q_EMIT peerFingerprint(args);
}
}

View file

@ -10,7 +10,7 @@
#include "common/Enums.h"
#include "common/Settings.h"
#include "gui/FileTail.h"
#include "gui/config/IServerConfig.h"
#include "gui/config/ServerConfig.h"
#include <QMutex>
#include <QObject>
@ -29,8 +29,6 @@ class CoreProcess : public QObject
using ConnectionState = deskflow::core::ConnectionState;
using ProcessMode = Settings::ProcessMode;
using ProcessState = deskflow::core::ProcessState;
using IServerConfig = deskflow::gui::IServerConfig;
Q_OBJECT
public:
@ -40,7 +38,7 @@ public:
StartFailed
};
explicit CoreProcess(const IServerConfig &serverConfig);
explicit CoreProcess(const ServerConfig &serverConfig);
void start(std::optional<ProcessMode> processMode = std::nullopt);
void stop(std::optional<ProcessMode> processMode = std::nullopt);
@ -91,6 +89,10 @@ Q_SIGNALS:
void daemonIpcClientConnectionFailed();
void connectedClientsChanged(const QStringList &clients);
void securityLevelChanged(QString securityLevel);
void unrecognisedClient(const QString &clientName);
void connectionRefused(deskflow::core::ConnectionRefusal reason);
void retryIn(int seconds);
void peerFingerprint(const QString &fingerprint);
private Q_SLOTS:
void onProcessFinished(int exitCode, QProcess::ExitStatus);
@ -116,7 +118,7 @@ private:
static QString processStateToString(const CoreProcess::ProcessState state);
static QString wrapIpv6(const QString &address);
const IServerConfig &m_serverConfig;
const ServerConfig &m_serverConfig;
QString m_address;
ProcessState m_processState = ProcessState::Stopped;
ConnectionState m_connectionState = ConnectionState::Disconnected;

View file

@ -1,108 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2025 Deskflow Developers
* SPDX-FileCopyrightText: (C) 2021 Symless Ltd.
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#include "ServerConnection.h"
#include "ServerMessage.h"
#include "common/Settings.h"
#include <QPushButton>
namespace deskflow::gui {
ServerConnection::ServerConnection(QWidget *parent, IServerConfig &serverConfig)
: m_pParent(parent),
m_serverConfig(serverConfig)
{
}
void ServerConnection::handleLogLine(const QString &logLine)
{
ServerMessage message(logLine);
const auto &clientName = message.getClientName();
if (m_ignoredClients.contains(clientName)) {
qDebug("ignoring %s:", qPrintable(clientName));
return;
}
if (message.isDisconnectedMessage()) {
m_connectedClients.remove(clientName);
Q_EMIT clientsChanged(connectedClients());
return;
}
if (message.isConnectedMessage()) {
m_connectedClients.insert(clientName);
Q_EMIT clientsChanged(connectedClients());
return;
}
if (!message.isNewClientMessage()) {
return;
}
if (m_messageShowing) {
qDebug("new client message already shown, skipping for now");
return;
}
if (m_serverConfigDialogVisible) {
qDebug("server config dialog visible, skipping new client prompt");
return;
}
if (Settings::value(Settings::Server::ExternalConfig).toBool()) {
qDebug("external config enabled, skipping new client prompt");
return;
}
if (m_connectedClients.contains(clientName)) {
qDebug("already got request, skipping new client prompt for: %s", qPrintable(clientName));
return;
}
handleNewClient(clientName);
}
void ServerConnection::handleNewClient(const QString &clientName)
{
if (m_serverConfig.isFull()) {
qDebug("server config full, skipping new client prompt for: %s", qPrintable(clientName));
return;
}
if (m_serverConfig.screenExists(clientName)) {
qDebug("client already added, skipping new client prompt for: %s", qPrintable(clientName));
return;
}
m_messageShowing = true;
Q_EMIT requestNewClientPrompt(clientName);
}
void ServerConnection::handleNewClientResult(const QString &clientName, bool acceptClient)
{
m_messageShowing = false;
if (!acceptClient) {
qDebug("declined dialog, ignoring client: %s", qPrintable(clientName));
m_ignoredClients.insert(clientName);
return;
}
qDebug("accepted dialog, adding client: %s", qPrintable(clientName));
Q_EMIT configureClient(clientName);
m_connectedClients.insert(clientName);
Q_EMIT clientsChanged(connectedClients());
}
QStringList ServerConnection::connectedClients() const
{
return QStringList(m_connectedClients.begin(), m_connectedClients.end());
}
} // namespace deskflow::gui

View file

@ -1,49 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2025 Deskflow Developers
* SPDX-FileCopyrightText: (C) 2021 Symless Ltd.
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#pragma once
#include <QString>
#include "gui/Messages.h"
#include "gui/config/IServerConfig.h"
namespace deskflow::gui {
class ServerConnection : public QObject
{
Q_OBJECT
using IServerConfig = deskflow::gui::IServerConfig;
public:
explicit ServerConnection(QWidget *parent, IServerConfig &serverConfig);
void handleLogLine(const QString &logLine);
void serverConfigDialogVisible(bool visible)
{
m_serverConfigDialogVisible = visible;
}
QStringList connectedClients() const;
void handleNewClientResult(const QString &clientName, bool acceptClient);
Q_SIGNALS:
void requestNewClientPrompt(const QString &clientName);
void configureClient(const QString &clientName);
void clientsChanged(const QStringList &clients);
private:
void handleNewClient(const QString &clientName);
QWidget *m_pParent;
IServerConfig &m_serverConfig;
QSet<QString> m_connectedClients;
QSet<QString> m_ignoredClients;
bool m_messageShowing = false;
bool m_serverConfigDialogVisible = false;
};
} // namespace deskflow::gui

View file

@ -1,54 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2021 Symless Ltd.
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#include "ServerMessage.h"
namespace deskflow::gui {
ServerMessage::ServerMessage(const QString &message) : m_message(message), m_clientName(parseClientName(message))
{
// do nothing
}
bool ServerMessage::isNewClientMessage() const
{
return m_message.contains("unrecognised client name");
}
bool ServerMessage::isExitMessage() const
{
return m_message.contains("process exited");
}
bool ServerMessage::isConnectedMessage() const
{
return m_message.contains("has connected");
}
bool ServerMessage::isDisconnectedMessage() const
{
return m_message.contains("has disconnected");
}
const QString &ServerMessage::getClientName() const
{
return m_clientName;
}
QString ServerMessage::parseClientName(const QString &line) const
{
QString clientName("Unknown");
auto nameStart = line.indexOf('"') + 1;
if (auto nameEnd = line.indexOf('"', nameStart); nameEnd > nameStart) {
clientName = line.mid(nameStart, nameEnd - nameStart);
}
return clientName;
}
} // namespace deskflow::gui

View file

@ -1,32 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2021 Symless Ltd.
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#pragma once
#include <QString>
namespace deskflow::gui {
class ServerMessage
{
QString m_message;
QString m_clientName;
public:
explicit ServerMessage(const QString &message);
bool isNewClientMessage() const;
bool isExitMessage() const;
bool isConnectedMessage() const;
bool isDisconnectedMessage() const;
const QString &getClientName() const;
private:
QString parseClientName(const QString &line) const;
};
} // namespace deskflow::gui

View file

@ -23,7 +23,7 @@
using namespace deskflow::gui;
SettingsDialog::SettingsDialog(QWidget *parent, const IServerConfig &serverConfig)
SettingsDialog::SettingsDialog(QWidget *parent, const ServerConfig &serverConfig)
: QDialog(parent),
ui{std::make_unique<Ui::SettingsDialog>()},
m_serverConfig(serverConfig)

View file

@ -9,7 +9,7 @@
#pragma once
#include <QDialog>
#include "gui/config/IServerConfig.h"
#include "gui/config/ServerConfig.h"
namespace Ui {
class SettingsDialog;
@ -17,13 +17,11 @@ class SettingsDialog;
class SettingsDialog : public QDialog
{
using IServerConfig = deskflow::gui::IServerConfig;
Q_OBJECT
public:
void extracted();
SettingsDialog(QWidget *parent, const IServerConfig &serverConfig);
SettingsDialog(QWidget *parent, const ServerConfig &serverConfig);
~SettingsDialog() override;
Q_SIGNALS:
@ -86,5 +84,5 @@ private:
bool m_interfaceSetOnLoad = false;
std::unique_ptr<Ui::SettingsDialog> ui;
const IServerConfig &m_serverConfig;
const ServerConfig &m_serverConfig;
};

View file

@ -6,15 +6,14 @@
#include "IpcClient.h"
#include "common/VersionInfo.h"
#include <QDebug>
#include <QLocalSocket>
#include <QTimer>
namespace deskflow::gui::ipc {
const auto kTimeout = 1000;
const auto kRetryLimit = 3;
IpcClient::IpcClient(QObject *parent, const QString &socketName)
: QObject(parent),
m_socket{new QLocalSocket(this)},
@ -48,6 +47,8 @@ void IpcClient::connectToServer()
void IpcClient::attemptConnection()
{
const auto kRetryLimit = 3;
if (m_retryCount >= kRetryLimit) {
qWarning() << "ipc client failed to connect after" << kRetryLimit << "attempts";
m_state = State::Unconnected;
@ -67,8 +68,9 @@ void IpcClient::attemptConnection()
connect(
m_socket, &QLocalSocket::connected, this,
[this] {
m_socket->write("hello\n");
qDebug() << "ipc client sent hello";
const auto versionId = QStringLiteral("%1+%2").arg(kVersion, kVersionGitSha);
m_socket->write(QString("hello=%1\n").arg(versionId).toUtf8());
qDebug() << "ipc client sent hello with version:" << versionId;
},
Qt::SingleShotConnection
);
@ -141,10 +143,8 @@ void IpcClient::handleReadyRead()
continue;
}
if (m_state == State::Connecting && parts.at(0) == "hello") {
m_state = State::Connected;
qDebug() << "ipc client connected";
Q_EMIT connected();
if (m_state == State::Connecting) {
handleHandshakeMessage(parts);
continue;
}
@ -156,6 +156,26 @@ void IpcClient::handleReadyRead()
}
}
void IpcClient::handleHandshakeMessage(const QStringList &parts)
{
if (parts.at(0) != "hello") {
return;
}
const auto versionId = QStringLiteral("%1+%2").arg(kVersion, kVersionGitSha);
const auto serverVersion = parts.size() >= 2 ? parts.at(1) : QString();
if (serverVersion != versionId) {
qCritical() << "ipc version mismatch (client:" << versionId << "server:" << serverVersion << ")";
disconnectFromServer();
Q_EMIT connectionFailed();
return;
}
m_state = State::Connected;
qDebug() << "ipc client connected";
Q_EMIT connected();
}
void IpcClient::sendMessage(const QString &message)
{
if (m_state != State::Connected) {

View file

@ -55,6 +55,7 @@ protected:
private:
void attemptConnection();
void handleHandshakeMessage(const QStringList &parts);
QLocalSocket *m_socket;
State m_state{State::Unconnected};

View file

@ -11,7 +11,6 @@
#include "base/IEventQueue.h"
#include "base/Log.h"
#include "deskflow/AppUtil.h"
#include "deskflow/ipc/CoreIpc.h"
#include "deskflow/DeskflowException.h"
#include "deskflow/IPlatformScreen.h"
#include "deskflow/OptionTypes.h"
@ -19,6 +18,7 @@
#include "deskflow/ProtocolTypes.h"
#include "deskflow/Screen.h"
#include "deskflow/StreamChunker.h"
#include "deskflow/ipc/CoreIpc.h"
#include "net/TCPSocket.h"
#include "server/ClientListener.h"
#include "server/ClientProxy.h"
@ -235,6 +235,7 @@ void Server::adoptClient(BaseClientProxy *client)
// name must be in our configuration
if (!m_config->isScreen(client->getName())) {
LOG_WARN("unrecognised client name \"%s\", check server config", client->getName().c_str());
ipcSendToClient("unrecognisedClient", QString::fromStdString(client->getName()));
closeClient(client, kMsgEUnknown);
return;
}

View file

@ -1,25 +1,9 @@
# SPDX-FileCopyrightText: (C) 2025 Deskflow Developers
# SPDX-License-Identifier: MIT
create_test(
NAME ClientConnectionTests
DEPENDS gui
SOURCE ClientConnectionTests.cpp
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/src/lib/gui"
)
create_test(
NAME ServerConnectionTests
DEPENDS gui
SOURCE ServerConnectionTests.cpp
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/src/lib/gui"
)
create_test(
NAME NetworkMonitorTests
DEPENDS gui
SOURCE NetworkMonitorTests.cpp
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/src/lib/gui"
)

View file

@ -1,130 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2025 Chris Rizzitello <sithlord48@gmail.com>
* SPDX-FileCopyrightText: (C) 2024 Symless Ltd.
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#include "ClientConnectionTests.h"
#include "gui/core/ClientConnection.h"
#include <common/Settings.h>
#include <QSignalSpy>
using namespace deskflow::gui;
void ClientConnectionTests::initTestCase()
{
QDir dir;
QVERIFY(dir.mkpath(m_settingsPath));
QFile oldSettings(m_settingsFile);
if (oldSettings.exists())
oldSettings.remove();
Settings::setSettingsFile(m_settingsFile);
Settings::setStateFile(m_stateFile);
}
void ClientConnectionTests::handleLogLine_alreadyConnected_showError()
{
ClientConnection clientConnection(nullptr);
const auto serverName = QStringLiteral("test server");
Settings::setValue(Settings::Client::RemoteHost, serverName);
QSignalSpy spy(&clientConnection, &ClientConnection::requestShowError);
QVERIFY(spy.isValid());
clientConnection.handleLogLine(
"failed to connect to server\n"
"server already has a connected client with our name"
);
QCOMPARE(spy.count(), 1);
}
void ClientConnectionTests::handleLogLine_withHostname_showError()
{
ClientConnection clientConnection(nullptr);
const auto serverName = QStringLiteral("test server");
Settings::setValue(Settings::Client::RemoteHost, serverName);
QSignalSpy spy(&clientConnection, &ClientConnection::requestShowError);
QVERIFY(spy.isValid());
clientConnection.handleLogLine("failed to connect to server");
QCOMPARE(spy.count(), 1);
}
void ClientConnectionTests::handleLogLine_withIpAddress_showError()
{
ClientConnection clientConnection(nullptr);
const auto serverName = QStringLiteral("1.1.1.1");
Settings::setValue(Settings::Client::RemoteHost, serverName);
QSignalSpy spy(&clientConnection, &ClientConnection::requestShowError);
QVERIFY(spy.isValid());
clientConnection.handleLogLine("failed to connect to server");
QCOMPARE(spy.count(), 1);
}
void ClientConnectionTests::handleLogLine_serverRefusedClient_shouldNotShowError()
{
ClientConnection clientConnection(nullptr);
QSignalSpy spy(&clientConnection, &ClientConnection::requestShowError);
QVERIFY(spy.isValid());
clientConnection.handleLogLine(
"failed to connect to server\n"
"server refused client with our name"
);
QCOMPARE(spy.count(), 0);
}
void ClientConnectionTests::handleLogLine_connected_shouldPreventFutureError()
{
ClientConnection clientConnection(nullptr);
clientConnection.handleLogLine("connected to server");
QSignalSpy spy(&clientConnection, &ClientConnection::requestShowError);
QVERIFY(spy.isValid());
clientConnection.handleLogLine("failed to connect to server");
QCOMPARE(spy.count(), 0);
}
void ClientConnectionTests::handleLogLine_connectToggled_showAfterDisconnect()
{
ClientConnection clientConnection(nullptr);
clientConnection.handleLogLine("connected to server");
QSignalSpy spy(&clientConnection, &ClientConnection::requestShowError);
QVERIFY(spy.isValid());
clientConnection.handleLogLine("failed to connect to server");
clientConnection.handleLogLine("disconnected from server");
clientConnection.handleLogLine("failed to connect to server");
QCOMPARE(spy.count(), 1);
}
void ClientConnectionTests::handleLogLine_otherMessage_shouldNotShowError()
{
ClientConnection clientConnection(nullptr);
QSignalSpy spy(&clientConnection, &ClientConnection::requestShowError);
QVERIFY(spy.isValid());
clientConnection.handleLogLine("hello world");
QCOMPARE(spy.count(), 0);
}
QTEST_MAIN(ClientConnectionTests)

View file

@ -1,27 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2025 Chris Rizzitello <sithlord48@gmail.com>
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#include <QTest>
class ClientConnectionTests : public QObject
{
Q_OBJECT
private Q_SLOTS:
// Test are run in order top to bottom
void initTestCase();
void handleLogLine_alreadyConnected_showError();
void handleLogLine_withHostname_showError();
void handleLogLine_withIpAddress_showError();
void handleLogLine_serverRefusedClient_shouldNotShowError();
void handleLogLine_connected_shouldPreventFutureError();
void handleLogLine_connectToggled_showAfterDisconnect();
void handleLogLine_otherMessage_shouldNotShowError();
private:
inline static const QString m_settingsPath = QStringLiteral("tmp/test");
inline static const QString m_settingsFile = QStringLiteral("%1/Deskflow.conf").arg(m_settingsPath);
inline static const QString m_stateFile = QStringLiteral("%1/Deskflow.state").arg(m_settingsPath);
};

View file

@ -1,101 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2025 Chris Rizzitello <sithlord48@gmail.com>
* SPDX-FileCopyrightText: (C) 2024 Symless Ltd.
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#include "ServerConnectionTests.h"
#include "gui/config/ServerConfig.h"
#include "gui/core/ServerConnection.h"
#include <common/Settings.h>
#include <QSignalSpy>
using namespace deskflow::gui;
class FullServerConfig : public ServerConfig
{
public:
bool isFull() const override
{
return true;
}
};
class ScreenExistsServerConfig : public ServerConfig
{
public:
bool screenExists(const QString &screenName) const override
{
return true;
}
};
void ServerConnectionTests::initTestCase()
{
QDir dir;
QVERIFY(dir.mkpath(m_settingsPath));
QFile oldSettings(m_settingsFile);
if (oldSettings.exists())
oldSettings.remove();
Settings::setSettingsFile(m_settingsFile);
Settings::setStateFile(m_stateFile);
}
void ServerConnectionTests::handleLogLine_newClient_shouldShowPrompt()
{
ServerConfig m_serverConfig;
ServerConnection serverConnection(nullptr, m_serverConfig);
QString clientName = "test client";
QSignalSpy spy(&serverConnection, &ServerConnection::requestNewClientPrompt);
QVERIFY(spy.isValid());
serverConnection.handleLogLine(R"(unrecognised client name "test client")");
QCOMPARE(spy.count(), 1);
}
void ServerConnectionTests::handleLogLine_ignoredClient_shouldNotShowPrompt()
{
ServerConfig m_serverConfig;
ServerConnection serverConnection(nullptr, m_serverConfig);
QString clientName = "test client";
serverConnection.handleNewClientResult(clientName, false);
QSignalSpy spy(&serverConnection, &ServerConnection::requestNewClientPrompt);
QVERIFY(spy.isValid());
serverConnection.handleLogLine(R"(unrecognised client name "test client")");
QCOMPARE(spy.count(), 0);
}
void ServerConnectionTests::handleLogLine_serverConfigFull_shouldNotShowPrompt()
{
FullServerConfig m_serverConfig;
ServerConnection serverConnection(nullptr, m_serverConfig);
QSignalSpy spy(&serverConnection, &ServerConnection::requestNewClientPrompt);
QVERIFY(spy.isValid());
serverConnection.handleLogLine(R"(unrecognised client name "test client")");
QCOMPARE(spy.count(), 0);
}
void ServerConnectionTests::handleLogLine_screenExists_shouldNotShowPrompt()
{
ScreenExistsServerConfig m_serverConfig;
ServerConnection serverConnection(nullptr, m_serverConfig);
QSignalSpy spy(&serverConnection, &ServerConnection::requestNewClientPrompt);
QVERIFY(spy.isValid());
serverConnection.handleLogLine(R"(unrecognised client name "test client")");
QCOMPARE(spy.count(), 0);
}
QTEST_MAIN(ServerConnectionTests)

View file

@ -1,24 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2025 Chris Rizzitello <sithlord48@gmail.com>
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#include <QTest>
class ServerConnectionTests : public QObject
{
Q_OBJECT
private Q_SLOTS:
// Test are run in order top to bottom
void initTestCase();
void handleLogLine_newClient_shouldShowPrompt();
void handleLogLine_ignoredClient_shouldNotShowPrompt();
void handleLogLine_serverConfigFull_shouldNotShowPrompt();
void handleLogLine_screenExists_shouldNotShowPrompt();
private:
inline static const QString m_settingsPath = QStringLiteral("tmp/test");
inline static const QString m_settingsFile = QStringLiteral("%1/Deskflow.conf").arg(m_settingsPath);
inline static const QString m_stateFile = QStringLiteral("%1/Deskflow.state").arg(m_settingsPath);
};