Browser Integration: Add support for WebSocket listener

This commit is contained in:
varjolintu 2025-05-11 11:24:21 +03:00 committed by Jonathan White
parent ada379fddd
commit d8440490a3
No known key found for this signature in database
GPG key ID: 440FC65F2E0C6E01
17 changed files with 302 additions and 37 deletions

View file

@ -37,7 +37,7 @@ jobs:
run: | run: |
sudo apt update sudo apt update
sudo apt install build-essential cmake g++ 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. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL

View file

@ -8,7 +8,7 @@ on:
- .github/workflows/copilot-setup-steps.yml - .github/workflows/copilot-setup-steps.yml
pull_request: pull_request:
paths: paths:
- .github/workflows/copilot-setup-steps.yml - .github/workflows/copilot-setup-steps.yml©
jobs: jobs:
copilot-setup-steps: copilot-setup-steps:
@ -26,4 +26,4 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
sudo apt update 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

View file

@ -510,7 +510,7 @@ endif()
include(CLangFormat) 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(UNIX AND NOT APPLE)
if(WITH_XC_X11) if(WITH_XC_X11)
list(APPEND QT_COMPONENTS X11Extras) list(APPEND QT_COMPONENTS X11Extras)

View file

@ -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_SET_LOGIN = QStringLiteral("set-login");
static const QString BROWSER_REQUEST_TEST_ASSOCIATE = QStringLiteral("test-associate"); static const QString BROWSER_REQUEST_TEST_ASSOCIATE = QStringLiteral("test-associate");
QJsonObject BrowserAction::processClientMessage(QLocalSocket* socket, const QJsonObject& json) template <typename T> QJsonObject BrowserAction::processClientMessage(T* socket, const QJsonObject& json)
{ {
if (json.isEmpty()) { if (json.isEmpty()) {
return getErrorReply("", ERROR_KEEPASS_EMPTY_MESSAGE_RECEIVED); return getErrorReply("", ERROR_KEEPASS_EMPTY_MESSAGE_RECEIVED);
@ -75,10 +75,14 @@ QJsonObject BrowserAction::processClientMessage(QLocalSocket* socket, const QJso
return handleAction(socket, json); return handleAction(socket, json);
} }
// Explicit template instantiation
template QJsonObject BrowserAction::processClientMessage<QLocalSocket>(QLocalSocket*, const QJsonObject&);
template QJsonObject BrowserAction::processClientMessage<QWebSocket>(QWebSocket*, const QJsonObject&);
// Private functions // Private functions
/////////////////////// ///////////////////////
QJsonObject BrowserAction::handleAction(QLocalSocket* socket, const QJsonObject& json) template <typename T> QJsonObject BrowserAction::handleAction(T* socket, const QJsonObject& json)
{ {
QString action = json.value("action").toString(); QString action = json.value("action").toString();
@ -262,7 +266,8 @@ QJsonObject BrowserAction::handleGetLogins(const QJsonObject& json, const QStrin
return buildResponse(action, browserRequest.incrementedNonce, params); return buildResponse(action, browserRequest.incrementedNonce, params);
} }
QJsonObject BrowserAction::handleGeneratePassword(QLocalSocket* socket, const QJsonObject& json, const QString& action) template <typename T>
QJsonObject BrowserAction::handleGeneratePassword(T* socket, const QJsonObject& json, const QString& action)
{ {
const auto browserRequest = decodeRequest(json); const auto browserRequest = decodeRequest(json);
if (browserRequest.isEmpty()) { if (browserRequest.isEmpty()) {
@ -281,11 +286,12 @@ QJsonObject BrowserAction::handleGeneratePassword(QLocalSocket* socket, const QJ
} }
// Show the existing password generator // Show the existing password generator
browserService()->showPasswordGenerator({}); // browserService()->showPasswordGenerator({});
browserService()->showPasswordGenerator(KeyPairMessage<T>{});
return errorReply; return errorReply;
} }
KeyPairMessage keyPairMessage{socket, browserRequest.incrementedNonce, m_clientPublicKey, m_secretKey}; KeyPairMessage<T> keyPairMessage{socket, browserRequest.incrementedNonce, m_clientPublicKey, m_secretKey};
browserService()->showPasswordGenerator(keyPairMessage); browserService()->showPasswordGenerator(keyPairMessage);
return {}; return {};

View file

@ -26,6 +26,7 @@
#include <QString> #include <QString>
class QLocalSocket; class QLocalSocket;
class QWebSocket;
struct BrowserRequest struct BrowserRequest
{ {
@ -66,16 +67,16 @@ public:
explicit BrowserAction() = default; explicit BrowserAction() = default;
~BrowserAction() = default; ~BrowserAction() = default;
QJsonObject processClientMessage(QLocalSocket* socket, const QJsonObject& json); template <typename T> QJsonObject processClientMessage(T* socket, const QJsonObject& json);
private: private:
QJsonObject handleAction(QLocalSocket* socket, const QJsonObject& json); template <typename T> QJsonObject handleAction(T* socket, const QJsonObject& json);
QJsonObject handleChangePublicKeys(const QJsonObject& json, const QString& action); QJsonObject handleChangePublicKeys(const QJsonObject& json, const QString& action);
QJsonObject handleGetDatabaseHash(const QJsonObject& json, const QString& action); QJsonObject handleGetDatabaseHash(const QJsonObject& json, const QString& action);
QJsonObject handleAssociate(const QJsonObject& json, const QString& action); QJsonObject handleAssociate(const QJsonObject& json, const QString& action);
QJsonObject handleTestAssociate(const QJsonObject& json, const QString& action); QJsonObject handleTestAssociate(const QJsonObject& json, const QString& action);
QJsonObject handleGetLogins(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 <typename T> QJsonObject handleGeneratePassword(T* socket, const QJsonObject& json, const QString& action);
QJsonObject handleSetLogin(const QJsonObject& json, const QString& action); QJsonObject handleSetLogin(const QJsonObject& json, const QString& action);
QJsonObject handleLockDatabase(const QJsonObject& json, const QString& action); QJsonObject handleLockDatabase(const QJsonObject& json, const QString& action);
QJsonObject handleGetDatabaseGroups(const QJsonObject& json, const QString& action); QJsonObject handleGetDatabaseGroups(const QJsonObject& json, const QString& action);

View file

@ -24,6 +24,7 @@
#include "BrowserHost.h" #include "BrowserHost.h"
#include "BrowserMessageBuilder.h" #include "BrowserMessageBuilder.h"
#include "BrowserSettings.h" #include "BrowserSettings.h"
#include "BrowserWebSocketHost.h"
#include "core/EntryAttributes.h" #include "core/EntryAttributes.h"
#include "core/Tools.h" #include "core/Tools.h"
#include "gui/MainWindow.h" #include "gui/MainWindow.h"
@ -53,6 +54,7 @@
#include <QProgressDialog> #include <QProgressDialog>
#include <QStringView> #include <QStringView>
#include <QUrl> #include <QUrl>
#include <QtWebSockets/qwebsocket.h>
const QString BrowserService::KEEPASSXCBROWSER_NAME = QStringLiteral("KeePassXC-Browser Settings"); const QString BrowserService::KEEPASSXCBROWSER_NAME = QStringLiteral("KeePassXC-Browser Settings");
const QString BrowserService::KEEPASSXCBROWSER_OLD_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() BrowserService::BrowserService()
: QObject() : QObject()
, m_browserHost(new BrowserHost) , m_browserHost(new BrowserHost)
, m_browserWebSocketHost(new BrowserWebSocketHost)
, m_dialogActive(false) , m_dialogActive(false)
, m_bringToFrontRequested(false) , m_bringToFrontRequested(false)
, m_prevWindowState(WindowState::Normal) , m_prevWindowState(WindowState::Normal)
, m_keepassBrowserUUID(Tools::hexToUuid("de887cc3036343b8974b5911b8816224")) , 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::databaseUnlocked, this, &BrowserService::databaseUnlocked);
connect(getMainWindow(), &MainWindow::databaseLocked, this, &BrowserService::databaseLocked); connect(getMainWindow(), &MainWindow::databaseLocked, this, &BrowserService::databaseLocked);
connect(getMainWindow(), &MainWindow::activeDatabaseChanged, this, &BrowserService::activeDatabaseChanged); connect(getMainWindow(), &MainWindow::activeDatabaseChanged, this, &BrowserService::activeDatabaseChanged);
@ -109,8 +116,12 @@ void BrowserService::setEnabled(bool enabled)
} }
m_browserHost->start(); m_browserHost->start();
if (browserSettings()->webSocketSupport()) {
m_browserWebSocketHost->start();
}
} else { } else {
m_browserHost->stop(); m_browserHost->stop();
m_browserWebSocketHost->stop();
} }
} }
@ -528,7 +539,7 @@ QList<Entry*> BrowserService::confirmEntries(QList<Entry*>& entriesToConfirm,
return allowedEntries; return allowedEntries;
} }
void BrowserService::showPasswordGenerator(const KeyPairMessage& keyPairMessage) template <typename T> void BrowserService::showPasswordGenerator(const KeyPairMessage<T>& keyPairMessage)
{ {
if (!m_passwordGenerator) { if (!m_passwordGenerator) {
m_passwordGenerator = PasswordGeneratorWidget::popupGenerator(); m_passwordGenerator = PasswordGeneratorWidget::popupGenerator();
@ -540,7 +551,11 @@ void BrowserService::showPasswordGenerator(const KeyPairMessage& keyPairMessage)
if (!m_passwordGenerator->isPasswordGenerated()) { if (!m_passwordGenerator->isPasswordGenerated()) {
auto errorMessage = browserMessageBuilder()->getErrorReply( auto errorMessage = browserMessageBuilder()->getErrorReply(
"generate-password", ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED); "generate-password", ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED);
m_browserHost->sendClientMessage(keyPairMessage.socket, errorMessage); if constexpr (std::is_same<T, QWebSocket>::value) {
m_browserWebSocketHost->sendClientMessage(keyPairMessage.socket, errorMessage);
} else {
m_browserHost->sendClientMessage(keyPairMessage.socket, errorMessage);
}
} }
QTimer::singleShot(50, this, [&] { hideWindow(); }); QTimer::singleShot(50, this, [&] { hideWindow(); });
@ -551,12 +566,24 @@ void BrowserService::showPasswordGenerator(const KeyPairMessage& keyPairMessage)
m_passwordGenerator.data(), m_passwordGenerator.data(),
[this, keyPairMessage](const QString& password) { [this, keyPairMessage](const QString& password) {
const Parameters params{{"password", password}}; const Parameters params{{"password", password}};
m_browserHost->sendClientMessage(keyPairMessage.socket, if constexpr (std::is_same<T, QWebSocket>::value) {
browserMessageBuilder()->buildResponse("generate-password", m_browserWebSocketHost->sendClientMessage(
keyPairMessage.nonce, keyPairMessage.socket,
params, browserMessageBuilder()->buildResponse("generate-password",
keyPairMessage.publicKey, keyPairMessage.nonce,
keyPairMessage.secretKey)); 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(); m_passwordGenerator->activateWindow();
} }
template void BrowserService::showPasswordGenerator<QLocalSocket>(const KeyPairMessage<QLocalSocket>&);
template void BrowserService::showPasswordGenerator<QWebSocket>(const KeyPairMessage<QWebSocket>&);
bool BrowserService::isPasswordGeneratorRequested() const bool BrowserService::isPasswordGeneratorRequested() const
{ {
return m_passwordGenerator && m_passwordGenerator->isVisible(); return m_passwordGenerator && m_passwordGenerator->isVisible();
@ -1715,6 +1745,7 @@ void BrowserService::databaseLocked(DatabaseWidget* dbWidget)
QJsonObject msg; QJsonObject msg;
msg["action"] = QString("database-locked"); msg["action"] = QString("database-locked");
m_browserHost->broadcastClientMessage(msg); m_browserHost->broadcastClientMessage(msg);
m_browserWebSocketHost->broadcastClientMessage(msg);
} }
} }
@ -1729,6 +1760,7 @@ void BrowserService::databaseUnlocked(DatabaseWidget* dbWidget)
QJsonObject msg; QJsonObject msg;
msg["action"] = QString("database-unlocked"); msg["action"] = QString("database-unlocked");
m_browserHost->broadcastClientMessage(msg); 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<QLocalSocket>(socket, message);
m_browserHost->sendClientMessage(socket, response);
}
void BrowserService::processWebSocketClientMessage(QWebSocket* socket, const QJsonObject& message)
{
auto response = processClientMessage<QWebSocket>(socket, message);
m_browserWebSocketHost->sendClientMessage(socket, response);
}
template <typename T> QJsonObject BrowserService::processClientMessage(T* socket, const QJsonObject& message)
{ {
auto clientID = message["clientID"].toString(); auto clientID = message["clientID"].toString();
if (clientID.isEmpty()) { if (clientID.isEmpty()) {
return; return {};
} }
// Create a new client action if we haven't seen this id yet // 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& action = m_browserClients.value(clientID);
auto response = action->processClientMessage(socket, message); return action->processClientMessage<T>(socket, message);
m_browserHost->sendClientMessage(socket, response); // auto response = action->processClientMessage(socket, message);
// m_browserHost->sendClientMessage(socket, response);
} }

View file

@ -26,6 +26,7 @@
#include "gui/PasswordGeneratorWidget.h" #include "gui/PasswordGeneratorWidget.h"
class QLocalSocket; class QLocalSocket;
class QWebSocket;
typedef QPair<QString, QString> StringPair; typedef QPair<QString, QString> StringPair;
typedef QList<StringPair> StringPairList; typedef QList<StringPair> StringPairList;
@ -35,9 +36,9 @@ enum
max_length = 16 * 1024 max_length = 16 * 1024
}; };
struct KeyPairMessage template <typename T> struct KeyPairMessage
{ {
QLocalSocket* socket; T* socket;
QString nonce; QString nonce;
QString publicKey; QString publicKey;
QString secretKey; QString secretKey;
@ -58,6 +59,7 @@ struct EntryParameters
class DatabaseWidget; class DatabaseWidget;
class BrowserHost; class BrowserHost;
class BrowserWebSocketHost;
class BrowserAction; class BrowserAction;
class BrowserService : public QObject class BrowserService : public QObject
@ -82,7 +84,7 @@ public:
QJsonArray getDatabaseEntries(); QJsonArray getDatabaseEntries();
QJsonObject createNewGroup(const QString& groupName, bool isPasskeysGroup = false); QJsonObject createNewGroup(const QString& groupName, bool isPasskeysGroup = false);
QString getCurrentTotp(const QString& uuid); QString getCurrentTotp(const QString& uuid);
void showPasswordGenerator(const KeyPairMessage& keyPairMessage); template <typename T> void showPasswordGenerator(const KeyPairMessage<T>& keyPairMessage);
bool isPasswordGeneratorRequested() const; bool isPasswordGeneratorRequested() const;
QSharedPointer<Database> getDatabase(const QUuid& rootGroupUuid = {}); QSharedPointer<Database> getDatabase(const QUuid& rootGroupUuid = {});
QSharedPointer<Database> selectedDatabase(); QSharedPointer<Database> selectedDatabase();
@ -137,7 +139,7 @@ public:
signals: signals:
void requestUnlock(); void requestUnlock();
void passwordGenerated(QLocalSocket* socket, const QString& password, const QString& nonce); void passwordGenerated(QWebSocket* socket, const QString& password, const QString& nonce);
public slots: public slots:
void databaseLocked(DatabaseWidget* dbWidget); void databaseLocked(DatabaseWidget* dbWidget);
@ -145,7 +147,8 @@ public slots:
void activeDatabaseChanged(DatabaseWidget* dbWidget); void activeDatabaseChanged(DatabaseWidget* dbWidget);
private slots: 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); void handleDatabaseUnlockDialogFinished(bool accepted, DatabaseWidget* dbWidget);
private: private:
@ -208,8 +211,10 @@ private:
void hideWindow() const; void hideWindow() const;
void raiseWindow(const bool force = false); void raiseWindow(const bool force = false);
void updateWindowState(); void updateWindowState();
template <typename T> QJsonObject processClientMessage(T* socket, const QJsonObject& message);
QPointer<BrowserHost> m_browserHost; QPointer<BrowserHost> m_browserHost;
QPointer<BrowserWebSocketHost> m_browserWebSocketHost;
QHash<QString, QSharedPointer<BrowserAction>> m_browserClients; QHash<QString, QSharedPointer<BrowserAction>> m_browserClients;
bool m_dialogActive; bool m_dialogActive;

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> * Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com>
* Copyright (C) 2013 Francois Ferrand * Copyright (C) 2013 Francois Ferrand
* *
@ -295,3 +295,12 @@ QString BrowserSettings::replaceTildeHomePath(QString location)
return location; return location;
} }
void BrowserSettings:: setWebSocketSupport(bool enabled)
{
config()->set(Config::Browser_WebSocketSupport, enabled);
}
bool BrowserSettings::webSocketSupport()
{
return config()->get(Config::Browser_WebSocketSupport).toBool();
}

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> * Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com>
* Copyright (C) 2013 Francois Ferrand * Copyright (C) 2013 Francois Ferrand
* *
@ -82,6 +82,8 @@ public:
void updateBinaryPaths(); void updateBinaryPaths();
QString replaceHomePath(QString location); QString replaceHomePath(QString location);
QString replaceTildeHomePath(QString location); QString replaceTildeHomePath(QString location);
void setWebSocketSupport(bool enabled);
bool webSocketSupport();
private: private:
static BrowserSettings* m_instance; static BrowserSettings* m_instance;

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2020 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * 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 * 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->browserTypeComboBox->setCurrentIndex(typeIndex);
} }
m_ui->customBrowserLocation->setText(settings->replaceHomePath(settings->customBrowserLocation())); m_ui->customBrowserLocation->setText(settings->replaceHomePath(settings->customBrowserLocation()));
m_ui->webSocketSupport->setChecked(settings->webSocketSupport());
#ifdef QT_DEBUG #ifdef QT_DEBUG
m_ui->customExtensionId->setText(settings->customExtensionId()); m_ui->customExtensionId->setText(settings->customExtensionId());
@ -241,6 +242,7 @@ void BrowserSettingsWidget::saveSettings()
settings->setSupportKphFields(m_ui->supportKphFields->isChecked()); settings->setSupportKphFields(m_ui->supportKphFields->isChecked());
settings->setAllowLocalhostWithPasskeys(m_ui->allowLocalhostWithPasskeys->isChecked()); settings->setAllowLocalhostWithPasskeys(m_ui->allowLocalhostWithPasskeys->isChecked());
settings->setNoMigrationPrompt(m_ui->noMigrationPrompt->isChecked()); settings->setNoMigrationPrompt(m_ui->noMigrationPrompt->isChecked());
settings->setWebSocketSupport(m_ui->webSocketSupport->isChecked());
#ifdef QT_DEBUG #ifdef QT_DEBUG
settings->setCustomExtensionId(m_ui->customExtensionId->text()); settings->setCustomExtensionId(m_ui->customExtensionId->text());

View file

@ -340,6 +340,16 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QCheckBox" name="webSocketSupport">
<property name="toolTip">
<string>Listens to connections using WebSocket in addition to native messaging.</string>
</property>
<property name="text">
<string>Enable WebSocket listener</string>
</property>
</widget>
</item>
<item> <item>
<widget class="QCheckBox" name="allowGetDatabaseEntriesRequest"> <widget class="QCheckBox" name="allowGetDatabaseEntriesRequest">
<property name="toolTip"> <property name="toolTip">

View file

@ -0,0 +1,123 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "BrowserShared.h"
#include "BrowserWebSocketHost.h"
#include <QJsonDocument>
#include <QtWebSockets/qwebsocket.h>
#include <QtWebSockets/qwebsocketserver.h>
#ifdef Q_OS_WIN
#include <fcntl.h>
#undef NOMINMAX
#define NOMINMAX
#include <windows.h>
#else
#include <sys/socket.h>
#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<char*>(&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<QWebSocket*>(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<QWebSocket*>(QObject::sender());
m_socketList.removeOne(socket);
}

View file

@ -0,0 +1,59 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_BROWSERWEBSOCKETHOST_H
#define KEEPASSXC_BROWSERWEBSOCKETHOST_H
#include <QJsonObject>
#include <QObject>
#include <QPointer>
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<QWebSocketServer> m_webSocketServer;
QList<QWebSocket*> m_socketList;
};
#endif // KEEPASSXC_BROWSERWEBSOCKETHOST_H

View file

@ -1,4 +1,4 @@
# Copyright (C) 2024 KeePassXC Team <team@keepassxc.org> # Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@ -28,6 +28,7 @@ if(WITH_XC_BROWSER)
BrowserService.cpp BrowserService.cpp
BrowserSettings.cpp BrowserSettings.cpp
BrowserShared.cpp BrowserShared.cpp
BrowserWebSocketHost.cpp
CustomTableWidget.cpp CustomTableWidget.cpp
NativeMessageInstaller.cpp) NativeMessageInstaller.cpp)
@ -41,5 +42,5 @@ if(WITH_XC_BROWSER)
endif() endif()
add_library(browser STATIC ${browser_SOURCES}) 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() endif()

View file

@ -179,6 +179,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
{Config::Browser_CustomBrowserType, {QS("Browser/CustomBrowserType"), Local, -1}}, {Config::Browser_CustomBrowserType, {QS("Browser/CustomBrowserType"), Local, -1}},
{Config::Browser_CustomBrowserLocation, {QS("Browser/CustomBrowserLocation"), Local, {}}}, {Config::Browser_CustomBrowserLocation, {QS("Browser/CustomBrowserLocation"), Local, {}}},
{Config::Browser_AllowLocalhostWithPasskeys, {QS("Browser/Browser_AllowLocalhostWithPasskeys"), Roaming, false}}, {Config::Browser_AllowLocalhostWithPasskeys, {QS("Browser/Browser_AllowLocalhostWithPasskeys"), Roaming, false}},
{Config::Browser_WebSocketSupport, {QS("Browser/WebSocketSupport"), Roaming, false}},
#ifdef QT_DEBUG #ifdef QT_DEBUG
{Config::Browser_CustomExtensionId, {QS("Browser/CustomExtensionId"), Local, {}}}, {Config::Browser_CustomExtensionId, {QS("Browser/CustomExtensionId"), Local, {}}},
#endif #endif

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2011 Felix Geyer <debfx@fobos.de> * Copyright (C) 2011 Felix Geyer <debfx@fobos.de>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@ -159,6 +159,7 @@ public:
Browser_CustomBrowserType, Browser_CustomBrowserType,
Browser_CustomBrowserLocation, Browser_CustomBrowserLocation,
Browser_AllowLocalhostWithPasskeys, Browser_AllowLocalhostWithPasskeys,
Browser_WebSocketSupport,
#ifdef QT_DEBUG #ifdef QT_DEBUG
Browser_CustomExtensionId, Browser_CustomExtensionId,
#endif #endif

View file

@ -63,7 +63,7 @@ void TestBrowser::testChangePublicKeys()
json["publicKey"] = PUBLICKEY; json["publicKey"] = PUBLICKEY;
json["nonce"] = NONCE; json["nonce"] = NONCE;
auto response = m_browserAction->processClientMessage(nullptr, json); auto response = m_browserAction->processClientMessage<QLocalSocket>(nullptr, json);
QCOMPARE(response["action"].toString(), QString("change-public-keys")); QCOMPARE(response["action"].toString(), QString("change-public-keys"));
QCOMPARE(response["publicKey"].toString() == PUBLICKEY, false); QCOMPARE(response["publicKey"].toString() == PUBLICKEY, false);
QCOMPARE(response["success"].toString(), TRUE_STR); QCOMPARE(response["success"].toString(), TRUE_STR);