From d8440490a3ca2ade88397aae3c3f1aec2f6345e0 Mon Sep 17 00:00:00 2001 From: varjolintu Date: Sun, 11 May 2025 11:24:21 +0300 Subject: [PATCH 1/2] Browser Integration: Add support for WebSocket listener --- .github/workflows/codeql.yml | 2 +- .github/workflows/copilot-setup-steps.yml | 4 +- CMakeLists.txt | 2 +- src/browser/BrowserAction.cpp | 16 ++- src/browser/BrowserAction.h | 7 +- src/browser/BrowserService.cpp | 71 ++++++++++--- src/browser/BrowserService.h | 15 ++- src/browser/BrowserSettings.cpp | 11 +- src/browser/BrowserSettings.h | 4 +- src/browser/BrowserSettingsWidget.cpp | 4 +- src/browser/BrowserSettingsWidget.ui | 10 ++ src/browser/BrowserWebSocketHost.cpp | 123 ++++++++++++++++++++++ src/browser/BrowserWebSocketHost.h | 59 +++++++++++ src/browser/CMakeLists.txt | 5 +- src/core/Config.cpp | 1 + src/core/Config.h | 3 +- tests/TestBrowser.cpp | 2 +- 17 files changed, 302 insertions(+), 37 deletions(-) create mode 100644 src/browser/BrowserWebSocketHost.cpp create mode 100644 src/browser/BrowserWebSocketHost.h diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 07d4104b7..7f1521c5e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -37,7 +37,7 @@ jobs: run: | sudo apt update sudo apt install build-essential cmake g++ - sudo apt install qtbase5-dev qtbase5-private-dev qttools5-dev qttools5-dev-tools libqt5svg5-dev libargon2-dev libkeyutils-dev libminizip-dev libbotan-2-dev libqrencode-dev zlib1g-dev asciidoctor libreadline-dev libpcsclite-dev libusb-1.0-0-dev libxi-dev libxtst-dev libqt5x11extras5-dev + sudo apt install qtbase5-dev qtbase5-private-dev qttools5-dev qttools5-dev-tools libqt5svg5-dev libargon2-dev libkeyutils-dev libminizip-dev libbotan-2-dev libqrencode-dev zlib1g-dev asciidoctor libreadline-dev libpcsclite-dev libusb-1.0-0-dev libxi-dev libxtst-dev libqt5x11extras5-dev libqt5websockets5-dev # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index cfd6b46e7..13848a5b5 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -8,7 +8,7 @@ on: - .github/workflows/copilot-setup-steps.yml pull_request: paths: - - .github/workflows/copilot-setup-steps.yml + - .github/workflows/copilot-setup-steps.yml© jobs: copilot-setup-steps: @@ -26,4 +26,4 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt install --no-install-recommends build-essential cmake g++ ninja-build qtbase5-dev qtbase5-private-dev qttools5-dev qttools5-dev-tools libqt5svg5-dev libargon2-dev libkeyutils-dev libminizip-dev libbotan-2-dev libqrencode-dev zlib1g-dev asciidoctor libreadline-dev libpcsclite-dev libusb-1.0-0-dev libxi-dev libxtst-dev libqt5x11extras5-dev + sudo apt install --no-install-recommends build-essential cmake g++ ninja-build qtbase5-dev qtbase5-private-dev qttools5-dev qttools5-dev-tools libqt5svg5-dev libargon2-dev libkeyutils-dev libminizip-dev libbotan-2-dev libqrencode-dev zlib1g-dev asciidoctor libreadline-dev libpcsclite-dev libusb-1.0-0-dev libxi-dev libxtst-dev libqt5x11extras5-dev libqt5websockets5-dev diff --git a/CMakeLists.txt b/CMakeLists.txt index 95d6e0b96..dd18dc191 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -510,7 +510,7 @@ endif() include(CLangFormat) -set(QT_COMPONENTS Core Network Concurrent Gui Svg Widgets Test LinguistTools) +set(QT_COMPONENTS Core Network Concurrent Gui Svg Widgets Test LinguistTools WebSockets) if(UNIX AND NOT APPLE) if(WITH_XC_X11) list(APPEND QT_COMPONENTS X11Extras) diff --git a/src/browser/BrowserAction.cpp b/src/browser/BrowserAction.cpp index 67cf7f0df..e16ca4aeb 100644 --- a/src/browser/BrowserAction.cpp +++ b/src/browser/BrowserAction.cpp @@ -46,7 +46,7 @@ static const QString BROWSER_REQUEST_REQUEST_AUTOTYPE = QStringLiteral("request- static const QString BROWSER_REQUEST_SET_LOGIN = QStringLiteral("set-login"); static const QString BROWSER_REQUEST_TEST_ASSOCIATE = QStringLiteral("test-associate"); -QJsonObject BrowserAction::processClientMessage(QLocalSocket* socket, const QJsonObject& json) +template QJsonObject BrowserAction::processClientMessage(T* socket, const QJsonObject& json) { if (json.isEmpty()) { return getErrorReply("", ERROR_KEEPASS_EMPTY_MESSAGE_RECEIVED); @@ -75,10 +75,14 @@ QJsonObject BrowserAction::processClientMessage(QLocalSocket* socket, const QJso return handleAction(socket, json); } +// Explicit template instantiation +template QJsonObject BrowserAction::processClientMessage(QLocalSocket*, const QJsonObject&); +template QJsonObject BrowserAction::processClientMessage(QWebSocket*, const QJsonObject&); + // Private functions /////////////////////// -QJsonObject BrowserAction::handleAction(QLocalSocket* socket, const QJsonObject& json) +template QJsonObject BrowserAction::handleAction(T* socket, const QJsonObject& json) { QString action = json.value("action").toString(); @@ -262,7 +266,8 @@ QJsonObject BrowserAction::handleGetLogins(const QJsonObject& json, const QStrin return buildResponse(action, browserRequest.incrementedNonce, params); } -QJsonObject BrowserAction::handleGeneratePassword(QLocalSocket* socket, const QJsonObject& json, const QString& action) +template +QJsonObject BrowserAction::handleGeneratePassword(T* socket, const QJsonObject& json, const QString& action) { const auto browserRequest = decodeRequest(json); if (browserRequest.isEmpty()) { @@ -281,11 +286,12 @@ QJsonObject BrowserAction::handleGeneratePassword(QLocalSocket* socket, const QJ } // Show the existing password generator - browserService()->showPasswordGenerator({}); + // browserService()->showPasswordGenerator({}); + browserService()->showPasswordGenerator(KeyPairMessage{}); return errorReply; } - KeyPairMessage keyPairMessage{socket, browserRequest.incrementedNonce, m_clientPublicKey, m_secretKey}; + KeyPairMessage keyPairMessage{socket, browserRequest.incrementedNonce, m_clientPublicKey, m_secretKey}; browserService()->showPasswordGenerator(keyPairMessage); return {}; diff --git a/src/browser/BrowserAction.h b/src/browser/BrowserAction.h index 5c115f5f1..c128f2133 100644 --- a/src/browser/BrowserAction.h +++ b/src/browser/BrowserAction.h @@ -26,6 +26,7 @@ #include class QLocalSocket; +class QWebSocket; struct BrowserRequest { @@ -66,16 +67,16 @@ public: explicit BrowserAction() = default; ~BrowserAction() = default; - QJsonObject processClientMessage(QLocalSocket* socket, const QJsonObject& json); + template QJsonObject processClientMessage(T* socket, const QJsonObject& json); private: - QJsonObject handleAction(QLocalSocket* socket, const QJsonObject& json); + template QJsonObject handleAction(T* socket, const QJsonObject& json); QJsonObject handleChangePublicKeys(const QJsonObject& json, const QString& action); QJsonObject handleGetDatabaseHash(const QJsonObject& json, const QString& action); QJsonObject handleAssociate(const QJsonObject& json, const QString& action); QJsonObject handleTestAssociate(const QJsonObject& json, const QString& action); QJsonObject handleGetLogins(const QJsonObject& json, const QString& action); - QJsonObject handleGeneratePassword(QLocalSocket* socket, const QJsonObject& json, const QString& action); + template QJsonObject handleGeneratePassword(T* socket, const QJsonObject& json, const QString& action); QJsonObject handleSetLogin(const QJsonObject& json, const QString& action); QJsonObject handleLockDatabase(const QJsonObject& json, const QString& action); QJsonObject handleGetDatabaseGroups(const QJsonObject& json, const QString& action); diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index 9ce1293f1..f73d7d257 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -24,6 +24,7 @@ #include "BrowserHost.h" #include "BrowserMessageBuilder.h" #include "BrowserSettings.h" +#include "BrowserWebSocketHost.h" #include "core/EntryAttributes.h" #include "core/Tools.h" #include "gui/MainWindow.h" @@ -53,6 +54,7 @@ #include #include #include +#include const QString BrowserService::KEEPASSXCBROWSER_NAME = QStringLiteral("KeePassXC-Browser Settings"); const QString BrowserService::KEEPASSXCBROWSER_OLD_NAME = QStringLiteral("keepassxc-browser Settings"); @@ -78,12 +80,17 @@ Q_GLOBAL_STATIC(BrowserService, s_browserService); BrowserService::BrowserService() : QObject() , m_browserHost(new BrowserHost) + , m_browserWebSocketHost(new BrowserWebSocketHost) , m_dialogActive(false) , m_bringToFrontRequested(false) , m_prevWindowState(WindowState::Normal) , m_keepassBrowserUUID(Tools::hexToUuid("de887cc3036343b8974b5911b8816224")) { - connect(m_browserHost, &BrowserHost::clientMessageReceived, this, &BrowserService::processClientMessage); + connect(m_browserHost, &BrowserHost::clientMessageReceived, this, &BrowserService::processLocalSocketClientMessage); + connect(m_browserWebSocketHost, + &BrowserWebSocketHost::clientMessageReceived, + this, + &BrowserService::processWebSocketClientMessage); connect(getMainWindow(), &MainWindow::databaseUnlocked, this, &BrowserService::databaseUnlocked); connect(getMainWindow(), &MainWindow::databaseLocked, this, &BrowserService::databaseLocked); connect(getMainWindow(), &MainWindow::activeDatabaseChanged, this, &BrowserService::activeDatabaseChanged); @@ -109,8 +116,12 @@ void BrowserService::setEnabled(bool enabled) } m_browserHost->start(); + if (browserSettings()->webSocketSupport()) { + m_browserWebSocketHost->start(); + } } else { m_browserHost->stop(); + m_browserWebSocketHost->stop(); } } @@ -528,7 +539,7 @@ QList BrowserService::confirmEntries(QList& entriesToConfirm, return allowedEntries; } -void BrowserService::showPasswordGenerator(const KeyPairMessage& keyPairMessage) +template void BrowserService::showPasswordGenerator(const KeyPairMessage& keyPairMessage) { if (!m_passwordGenerator) { m_passwordGenerator = PasswordGeneratorWidget::popupGenerator(); @@ -540,7 +551,11 @@ void BrowserService::showPasswordGenerator(const KeyPairMessage& keyPairMessage) if (!m_passwordGenerator->isPasswordGenerated()) { auto errorMessage = browserMessageBuilder()->getErrorReply( "generate-password", ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED); - m_browserHost->sendClientMessage(keyPairMessage.socket, errorMessage); + if constexpr (std::is_same::value) { + m_browserWebSocketHost->sendClientMessage(keyPairMessage.socket, errorMessage); + } else { + m_browserHost->sendClientMessage(keyPairMessage.socket, errorMessage); + } } QTimer::singleShot(50, this, [&] { hideWindow(); }); @@ -551,12 +566,24 @@ void BrowserService::showPasswordGenerator(const KeyPairMessage& keyPairMessage) m_passwordGenerator.data(), [this, keyPairMessage](const QString& password) { const Parameters params{{"password", password}}; - m_browserHost->sendClientMessage(keyPairMessage.socket, - browserMessageBuilder()->buildResponse("generate-password", - keyPairMessage.nonce, - params, - keyPairMessage.publicKey, - keyPairMessage.secretKey)); + if constexpr (std::is_same::value) { + m_browserWebSocketHost->sendClientMessage( + keyPairMessage.socket, + browserMessageBuilder()->buildResponse("generate-password", + keyPairMessage.nonce, + params, + keyPairMessage.publicKey, + keyPairMessage.secretKey)); + + } else { + m_browserHost->sendClientMessage( + keyPairMessage.socket, + browserMessageBuilder()->buildResponse("generate-password", + keyPairMessage.nonce, + params, + keyPairMessage.publicKey, + keyPairMessage.secretKey)); + } }); } @@ -566,6 +593,9 @@ void BrowserService::showPasswordGenerator(const KeyPairMessage& keyPairMessage) m_passwordGenerator->activateWindow(); } +template void BrowserService::showPasswordGenerator(const KeyPairMessage&); +template void BrowserService::showPasswordGenerator(const KeyPairMessage&); + bool BrowserService::isPasswordGeneratorRequested() const { return m_passwordGenerator && m_passwordGenerator->isVisible(); @@ -1715,6 +1745,7 @@ void BrowserService::databaseLocked(DatabaseWidget* dbWidget) QJsonObject msg; msg["action"] = QString("database-locked"); m_browserHost->broadcastClientMessage(msg); + m_browserWebSocketHost->broadcastClientMessage(msg); } } @@ -1729,6 +1760,7 @@ void BrowserService::databaseUnlocked(DatabaseWidget* dbWidget) QJsonObject msg; msg["action"] = QString("database-unlocked"); m_browserHost->broadcastClientMessage(msg); + m_browserWebSocketHost->broadcastClientMessage(msg); } } @@ -1754,11 +1786,23 @@ void BrowserService::handleDatabaseUnlockDialogFinished(bool accepted, DatabaseW } } -void BrowserService::processClientMessage(QLocalSocket* socket, const QJsonObject& message) +void BrowserService::processLocalSocketClientMessage(QLocalSocket* socket, const QJsonObject& message) +{ + auto response = processClientMessage(socket, message); + m_browserHost->sendClientMessage(socket, response); +} + +void BrowserService::processWebSocketClientMessage(QWebSocket* socket, const QJsonObject& message) +{ + auto response = processClientMessage(socket, message); + m_browserWebSocketHost->sendClientMessage(socket, response); +} + +template QJsonObject BrowserService::processClientMessage(T* socket, const QJsonObject& message) { auto clientID = message["clientID"].toString(); if (clientID.isEmpty()) { - return; + return {}; } // Create a new client action if we haven't seen this id yet @@ -1767,6 +1811,7 @@ void BrowserService::processClientMessage(QLocalSocket* socket, const QJsonObjec } auto& action = m_browserClients.value(clientID); - auto response = action->processClientMessage(socket, message); - m_browserHost->sendClientMessage(socket, response); + return action->processClientMessage(socket, message); + // auto response = action->processClientMessage(socket, message); + // m_browserHost->sendClientMessage(socket, response); } diff --git a/src/browser/BrowserService.h b/src/browser/BrowserService.h index c59f9303d..4a0f89e40 100644 --- a/src/browser/BrowserService.h +++ b/src/browser/BrowserService.h @@ -26,6 +26,7 @@ #include "gui/PasswordGeneratorWidget.h" class QLocalSocket; +class QWebSocket; typedef QPair StringPair; typedef QList StringPairList; @@ -35,9 +36,9 @@ enum max_length = 16 * 1024 }; -struct KeyPairMessage +template struct KeyPairMessage { - QLocalSocket* socket; + T* socket; QString nonce; QString publicKey; QString secretKey; @@ -58,6 +59,7 @@ struct EntryParameters class DatabaseWidget; class BrowserHost; +class BrowserWebSocketHost; class BrowserAction; class BrowserService : public QObject @@ -82,7 +84,7 @@ public: QJsonArray getDatabaseEntries(); QJsonObject createNewGroup(const QString& groupName, bool isPasskeysGroup = false); QString getCurrentTotp(const QString& uuid); - void showPasswordGenerator(const KeyPairMessage& keyPairMessage); + template void showPasswordGenerator(const KeyPairMessage& keyPairMessage); bool isPasswordGeneratorRequested() const; QSharedPointer getDatabase(const QUuid& rootGroupUuid = {}); QSharedPointer selectedDatabase(); @@ -137,7 +139,7 @@ public: signals: void requestUnlock(); - void passwordGenerated(QLocalSocket* socket, const QString& password, const QString& nonce); + void passwordGenerated(QWebSocket* socket, const QString& password, const QString& nonce); public slots: void databaseLocked(DatabaseWidget* dbWidget); @@ -145,7 +147,8 @@ public slots: void activeDatabaseChanged(DatabaseWidget* dbWidget); private slots: - void processClientMessage(QLocalSocket* socket, const QJsonObject& message); + void processLocalSocketClientMessage(QLocalSocket* socket, const QJsonObject& message); + void processWebSocketClientMessage(QWebSocket* socket, const QJsonObject& message); void handleDatabaseUnlockDialogFinished(bool accepted, DatabaseWidget* dbWidget); private: @@ -208,8 +211,10 @@ private: void hideWindow() const; void raiseWindow(const bool force = false); void updateWindowState(); + template QJsonObject processClientMessage(T* socket, const QJsonObject& message); QPointer m_browserHost; + QPointer m_browserWebSocketHost; QHash> m_browserClients; bool m_dialogActive; diff --git a/src/browser/BrowserSettings.cpp b/src/browser/BrowserSettings.cpp index 0a8226c12..e17c1be9b 100644 --- a/src/browser/BrowserSettings.cpp +++ b/src/browser/BrowserSettings.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 KeePassXC Team + * Copyright (C) 2025 KeePassXC Team * Copyright (C) 2017 Sami Vänttinen * Copyright (C) 2013 Francois Ferrand * @@ -295,3 +295,12 @@ QString BrowserSettings::replaceTildeHomePath(QString location) return location; } + +void BrowserSettings:: setWebSocketSupport(bool enabled) +{ + config()->set(Config::Browser_WebSocketSupport, enabled); +} +bool BrowserSettings::webSocketSupport() +{ + return config()->get(Config::Browser_WebSocketSupport).toBool(); +} diff --git a/src/browser/BrowserSettings.h b/src/browser/BrowserSettings.h index 9c0b3718e..c76c65ee6 100644 --- a/src/browser/BrowserSettings.h +++ b/src/browser/BrowserSettings.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 KeePassXC Team + * Copyright (C) 2025 KeePassXC Team * Copyright (C) 2017 Sami Vänttinen * Copyright (C) 2013 Francois Ferrand * @@ -82,6 +82,8 @@ public: void updateBinaryPaths(); QString replaceHomePath(QString location); QString replaceTildeHomePath(QString location); + void setWebSocketSupport(bool enabled); + bool webSocketSupport(); private: static BrowserSettings* m_instance; diff --git a/src/browser/BrowserSettingsWidget.cpp b/src/browser/BrowserSettingsWidget.cpp index 5a4ccce8d..a38c4d9ad 100644 --- a/src/browser/BrowserSettingsWidget.cpp +++ b/src/browser/BrowserSettingsWidget.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 KeePassXC Team + * Copyright (C) 2025 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -166,6 +166,7 @@ void BrowserSettingsWidget::loadSettings() m_ui->browserTypeComboBox->setCurrentIndex(typeIndex); } m_ui->customBrowserLocation->setText(settings->replaceHomePath(settings->customBrowserLocation())); + m_ui->webSocketSupport->setChecked(settings->webSocketSupport()); #ifdef QT_DEBUG m_ui->customExtensionId->setText(settings->customExtensionId()); @@ -241,6 +242,7 @@ void BrowserSettingsWidget::saveSettings() settings->setSupportKphFields(m_ui->supportKphFields->isChecked()); settings->setAllowLocalhostWithPasskeys(m_ui->allowLocalhostWithPasskeys->isChecked()); settings->setNoMigrationPrompt(m_ui->noMigrationPrompt->isChecked()); + settings->setWebSocketSupport(m_ui->webSocketSupport->isChecked()); #ifdef QT_DEBUG settings->setCustomExtensionId(m_ui->customExtensionId->text()); diff --git a/src/browser/BrowserSettingsWidget.ui b/src/browser/BrowserSettingsWidget.ui index 99db4ede6..c968797e6 100644 --- a/src/browser/BrowserSettingsWidget.ui +++ b/src/browser/BrowserSettingsWidget.ui @@ -340,6 +340,16 @@ + + + + Listens to connections using WebSocket in addition to native messaging. + + + Enable WebSocket listener + + + diff --git a/src/browser/BrowserWebSocketHost.cpp b/src/browser/BrowserWebSocketHost.cpp new file mode 100644 index 000000000..f47478b45 --- /dev/null +++ b/src/browser/BrowserWebSocketHost.cpp @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "BrowserShared.h" +#include "BrowserWebSocketHost.h" + +#include +#include +#include + +#ifdef Q_OS_WIN +#include +#undef NOMINMAX +#define NOMINMAX +#include +#else +#include +#endif + +BrowserWebSocketHost::BrowserWebSocketHost(QObject* parent) + : QObject(parent) +{ + m_webSocketServer = + new QWebSocketServer(QStringLiteral("KeePassXC HTTP server"), QWebSocketServer::NonSecureMode, this); +} + +BrowserWebSocketHost::~BrowserWebSocketHost() +{ + stop(); +} + +void BrowserWebSocketHost::start() +{ + int socketDesc = m_webSocketServer->nativeDescriptor(); + if (socketDesc) { + int max = BrowserShared::NATIVEMSG_MAX_LENGTH; + setsockopt(socketDesc, SOL_SOCKET, SO_SNDBUF, reinterpret_cast(&max), sizeof(max)); + } + + if (!m_webSocketServer->isListening()) { + m_webSocketServer->listen(QHostAddress::LocalHost, 7580); + connect(m_webSocketServer, &QWebSocketServer::newConnection, this, &BrowserWebSocketHost::clientConnected); + connect(m_webSocketServer, &QWebSocketServer::closed, this, &BrowserWebSocketHost::stop); + } +} + +void BrowserWebSocketHost::stop() +{ + m_socketList.clear(); + m_webSocketServer->close(); +} + +void BrowserWebSocketHost::clientConnected() +{ + auto socket = m_webSocketServer->nextPendingConnection(); + if (socket) { + m_socketList.append(socket); + connect(socket, &QWebSocket::textMessageReceived, this, &BrowserWebSocketHost::readClientMessage); + connect(socket, &QWebSocket::disconnected, this, &BrowserWebSocketHost::clientDisconnected); + } +} + +void BrowserWebSocketHost::readClientMessage(QString message) +{ + auto* socket = qobject_cast(QObject::sender()); + if (!socket || !socket->isValid()) { + return; + } + + socket->setReadBufferSize(BrowserShared::NATIVEMSG_MAX_LENGTH); + socket->setOutgoingFrameSize(BrowserShared::NATIVEMSG_MAX_LENGTH); + + QJsonParseError error; + auto json = QJsonDocument::fromJson(message.toUtf8(), &error); + if (json.isNull()) { + qWarning() << "Failed to read proxy message: " << error.errorString(); + return; + } + + emit clientMessageReceived(socket, json.object()); +} + +void BrowserWebSocketHost::broadcastClientMessage(const QJsonObject& json) +{ + QString reply(QJsonDocument(json).toJson(QJsonDocument::Compact)); + for (const auto socket : m_socketList) { + sendClientData(socket, reply); + } +} + +void BrowserWebSocketHost::sendClientMessage(QWebSocket* socket, const QJsonObject& json) +{ + QString reply(QJsonDocument(json).toJson(QJsonDocument::Compact)); + sendClientData(socket, reply); +} + +void BrowserWebSocketHost::sendClientData(QWebSocket* socket, const QString& data) +{ + if (socket && socket->isValid() && socket->state() == QAbstractSocket::ConnectedState) { + socket->sendTextMessage(data); + socket->flush(); + } +} + +void BrowserWebSocketHost::clientDisconnected() +{ + auto socket = qobject_cast(QObject::sender()); + m_socketList.removeOne(socket); +} diff --git a/src/browser/BrowserWebSocketHost.h b/src/browser/BrowserWebSocketHost.h new file mode 100644 index 000000000..040f7f964 --- /dev/null +++ b/src/browser/BrowserWebSocketHost.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2025 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_BROWSERWEBSOCKETHOST_H +#define KEEPASSXC_BROWSERWEBSOCKETHOST_H + +#include +#include +#include + +class QWebSocketServer; +class QWebSocket; +class QString; + +class BrowserWebSocketHost : public QObject +{ + Q_OBJECT + +public: + explicit BrowserWebSocketHost(QObject* parent = nullptr); + ~BrowserWebSocketHost() override; + + void start(); + void stop(); + + void broadcastClientMessage(const QJsonObject& json); + void sendClientMessage(QWebSocket* socket, const QJsonObject& json); + +signals: + void clientMessageReceived(QWebSocket* socket, const QJsonObject& json); + +private slots: + void clientConnected(); + void readClientMessage(QString message); + void clientDisconnected(); + +private: + void sendClientData(QWebSocket* socket, const QString& data); + +private: + QPointer m_webSocketServer; + QList m_socketList; +}; + +#endif // KEEPASSXC_BROWSERWEBSOCKETHOST_H diff --git a/src/browser/CMakeLists.txt b/src/browser/CMakeLists.txt index 7942be430..b7143bec0 100644 --- a/src/browser/CMakeLists.txt +++ b/src/browser/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright (C) 2024 KeePassXC Team +# Copyright (C) 2025 KeePassXC Team # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -28,6 +28,7 @@ if(WITH_XC_BROWSER) BrowserService.cpp BrowserSettings.cpp BrowserShared.cpp + BrowserWebSocketHost.cpp CustomTableWidget.cpp NativeMessageInstaller.cpp) @@ -41,5 +42,5 @@ if(WITH_XC_BROWSER) endif() add_library(browser STATIC ${browser_SOURCES}) - target_link_libraries(browser Qt5::Core Qt5::Concurrent Qt5::Widgets Qt5::Network ${BOTAN_LIBRARIES}) + target_link_libraries(browser Qt5::Core Qt5::Concurrent Qt5::Widgets Qt5::Network Qt5::WebSockets ${BOTAN_LIBRARIES}) endif() diff --git a/src/core/Config.cpp b/src/core/Config.cpp index 2c2b0bc57..35e0cb480 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -179,6 +179,7 @@ static const QHash configStrings = { {Config::Browser_CustomBrowserType, {QS("Browser/CustomBrowserType"), Local, -1}}, {Config::Browser_CustomBrowserLocation, {QS("Browser/CustomBrowserLocation"), Local, {}}}, {Config::Browser_AllowLocalhostWithPasskeys, {QS("Browser/Browser_AllowLocalhostWithPasskeys"), Roaming, false}}, + {Config::Browser_WebSocketSupport, {QS("Browser/WebSocketSupport"), Roaming, false}}, #ifdef QT_DEBUG {Config::Browser_CustomExtensionId, {QS("Browser/CustomExtensionId"), Local, {}}}, #endif diff --git a/src/core/Config.h b/src/core/Config.h index 8f54f9c01..a2d79188c 100644 --- a/src/core/Config.h +++ b/src/core/Config.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 KeePassXC Team + * Copyright (C) 2025 KeePassXC Team * Copyright (C) 2011 Felix Geyer * * This program is free software: you can redistribute it and/or modify @@ -159,6 +159,7 @@ public: Browser_CustomBrowserType, Browser_CustomBrowserLocation, Browser_AllowLocalhostWithPasskeys, + Browser_WebSocketSupport, #ifdef QT_DEBUG Browser_CustomExtensionId, #endif diff --git a/tests/TestBrowser.cpp b/tests/TestBrowser.cpp index a2610748a..972bcdd29 100644 --- a/tests/TestBrowser.cpp +++ b/tests/TestBrowser.cpp @@ -63,7 +63,7 @@ void TestBrowser::testChangePublicKeys() json["publicKey"] = PUBLICKEY; json["nonce"] = NONCE; - auto response = m_browserAction->processClientMessage(nullptr, json); + auto response = m_browserAction->processClientMessage(nullptr, json); QCOMPARE(response["action"].toString(), QString("change-public-keys")); QCOMPARE(response["publicKey"].toString() == PUBLICKEY, false); QCOMPARE(response["success"].toString(), TRUE_STR); From 3563b27a4e43e28eeb8875852f8e3437521a15c5 Mon Sep 17 00:00:00 2001 From: varjolintu Date: Sat, 24 Jan 2026 13:14:08 +0200 Subject: [PATCH 2/2] Add websockets to vcpkg.json --- vcpkg.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vcpkg.json b/vcpkg.json index 9c38d3d6c..6a61b882c 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -43,7 +43,8 @@ "imageformats", "svg", "tools", - "translations" + "translations", + "websockets" ], "default-features": false },