diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 202e9dddb0..8d73d5a546 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 qt6-base-dev qt6-base-private-dev qt6-tools-dev qt6-base-dev-tools qt6-svg-dev qt6-5compat-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 + sudo apt install qt6-base-dev qt6-base-private-dev qt6-tools-dev qt6-base-dev-tools qt6-svg-dev qt6-5compat-dev qt6-websockets-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 # 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 cfd6b46e72..13848a5b5a 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 2fecaf8635..8e389d8d13 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -410,7 +410,7 @@ if(WITH_COVERAGE) endif() # Find Qt -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) find_package(Qt6 COMPONENTS ${QT_COMPONENTS} DBus REQUIRED) elseif(APPLE) diff --git a/src/browser/BrowserAction.cpp b/src/browser/BrowserAction.cpp index 1e6db64da4..9c4707cf2b 100644 --- a/src/browser/BrowserAction.cpp +++ b/src/browser/BrowserAction.cpp @@ -44,7 +44,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); @@ -73,10 +73,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(); @@ -258,7 +262,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()) { @@ -277,11 +282,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 1ad25c8a0f..f62535c9c4 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 @@ class BrowserAction 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 3540c93812..cc968038fd 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -27,6 +27,7 @@ #include "BrowserPasskeysClient.h" #include "BrowserPasskeysConfirmationDialog.h" #include "BrowserSettings.h" +#include "BrowserWebSocketHost.h" #include "PasskeyUtils.h" #include "core/EntryAttributes.h" #include "core/Tools.h" @@ -51,6 +52,7 @@ #include #include #include +#include const QString BrowserService::KEEPASSXCBROWSER_NAME = QStringLiteral("KeePassXC-Browser Settings"); const QString BrowserService::KEEPASSXCBROWSER_OLD_NAME = QStringLiteral("keepassxc-browser Settings"); @@ -74,12 +76,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); @@ -105,8 +112,12 @@ void BrowserService::setEnabled(bool enabled) } m_browserHost->start(); + if (browserSettings()->webSocketSupport()) { + m_browserWebSocketHost->start(); + } } else { m_browserHost->stop(); + m_browserWebSocketHost->stop(); } } @@ -524,7 +535,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(); @@ -536,7 +547,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(); }); @@ -547,12 +562,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)); + } }); } @@ -562,6 +589,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(); @@ -1720,6 +1750,7 @@ void BrowserService::databaseLocked(DatabaseWidget* dbWidget) QJsonObject msg; msg["action"] = QString("database-locked"); m_browserHost->broadcastClientMessage(msg); + m_browserWebSocketHost->broadcastClientMessage(msg); } } @@ -1734,6 +1765,7 @@ void BrowserService::databaseUnlocked(DatabaseWidget* dbWidget) QJsonObject msg; msg["action"] = QString("database-unlocked"); m_browserHost->broadcastClientMessage(msg); + m_browserWebSocketHost->broadcastClientMessage(msg); } } @@ -1759,11 +1791,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 @@ -1772,6 +1816,7 @@ void BrowserService::processClientMessage(QLocalSocket* socket, const QJsonObjec } const 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 1db8fd50f4..d0979cae51 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 @@ class BrowserService : public QObject 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 @@ class BrowserService : public QObject 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 slots: 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 0a8226c12e..e17c1be9b7 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 9c0b3718e4..c76c65ee6a 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 @@ class BrowserSettings 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 5a4ccce8dd..a38c4d9ad7 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 99db4ede6e..c968797e69 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 0000000000..f47478b458 --- /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 0000000000..040f7f9643 --- /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 7bfd2e047b..9d63061a4d 100644 --- a/src/browser/CMakeLists.txt +++ b/src/browser/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright (C) 2025 KeePassXC Team +# Copyright (C) 2026 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 @@ -32,10 +32,11 @@ if(KPXC_FEATURE_BROWSER) BrowserService.cpp BrowserSettings.cpp BrowserShared.cpp + BrowserWebSocketHost.cpp CustomTableWidget.cpp NativeMessageInstaller.cpp PasskeyUtils.cpp) add_library(browser STATIC ${browser_SOURCES}) - target_link_libraries(browser Qt6::Core Qt6::Concurrent Qt6::Widgets Qt6::Network ${BOTAN_LIBRARIES}) + target_link_libraries(browser Qt6::Core Qt6::Concurrent Qt6::Widgets Qt6::Network Qt6::WebSockets ${BOTAN_LIBRARIES}) endif() diff --git a/src/core/Config.cpp b/src/core/Config.cpp index 261a92af1d..6bd77e4c10 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -181,6 +181,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 8f54f9c013..a2d79188cf 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 @@ class Config : public QObject 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 cbd8757678..d70178b22f 100644 --- a/tests/TestBrowser.cpp +++ b/tests/TestBrowser.cpp @@ -64,7 +64,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);