Page MenuHome GnuPG

No OneTemporary

diff --git a/client/connectioncontroller.cpp b/client/connectioncontroller.cpp
index eb6e3be..fa5646b 100644
--- a/client/connectioncontroller.cpp
+++ b/client/connectioncontroller.cpp
@@ -1,66 +1,67 @@
// SPDX-FileCopyrightText: 2026 g10 code Gmbh
// SPDX-Contributor: Thomas Friedrichsmeier <thomas.friedrichsmeier@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "connectioncontroller.h"
#include "config.h"
#include "websocketclient.h"
#include <QUuid>
#include <QUrl>
using namespace Qt::StringLiterals;
ConnectionController* ConnectionController::m_instance = nullptr;
ConnectionController::ConnectionController()
: QObject()
{
Q_ASSERT(m_instance == nullptr);
m_instance = this;
connect(&m_serverProcess, &QProcess::stateChanged, this, [this](QProcess::ProcessState state) {
Q_EMIT serverProcessStatusChanged();
if (state == QProcess::Running) {
m_state = LocalServerRunning;
} else if (state == QProcess::NotRunning) {
m_state = LocalServerFailed;
} else if (m_state != LocalServerFailed) {
m_state = LocalServerStarting;
}
});
}
ConnectionController* ConnectionController::instance()
{
return m_instance;
}
ConnectionController::ServerProcessState ConnectionController::serverProcessState() const
{
if (!Config::self()->isLocalServer()) {
return RemoteServer;
}
return m_state;
}
-void ConnectionController::startLocalServer()
+void ConnectionController::startStopLocalServer()
{
- if (m_serverProcess.state() != QProcess::NotRunning || !Config::self()->isLocalServer()) {
- return;
+ if (Config::self()->isLocalServer()) {
+ if (m_serverProcess.state() == QProcess::NotRunning) {
+ // TODO: Make sure to merge authorization code from pairing_singleuser
+ m_serverProcess.start(u"gpgol-server"_s);
+ }
+ } else if (m_serverProcess.state() != QProcess::NotRunning) {
+ m_serverProcess.kill();
}
- // TODO: Make sure to merge authorization code from pairing_singleuser
-
- m_serverProcess.start(u"gpgol-server"_s);
}
void ConnectionController::startWebsocketClient()
{
- const auto clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
- auto &websocketClient = WebsocketClient::self(QUrl(u"wss://"_s + serverDomain() + u"/websocket"_s), clientId);
+ WebsocketClient::self().connectToProxy(QUrl(u"wss://"_s + serverDomain() + u"/websocket"_s));
}
QString ConnectionController::serverDomain()
{
return (Config::self()->isLocalServer() ? u"localhost:5656"_s : Config::self()->remoteAddress().toString());
}
diff --git a/client/connectioncontroller.h b/client/connectioncontroller.h
index 29a6196..f95e043 100644
--- a/client/connectioncontroller.h
+++ b/client/connectioncontroller.h
@@ -1,35 +1,35 @@
// SPDX-FileCopyrightText: 2026 g10 code Gmbh
// SPDX-Contributor: Thomas Friedrichsmeier <thomas.friedrichsmeier@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QObject>
#include <QProcess>
class ConnectionController : public QObject
{
Q_OBJECT
public:
explicit ConnectionController();
static QString serverDomain();
static ConnectionController* instance();
- void startLocalServer();
+ void startStopLocalServer();
void startWebsocketClient();
enum ServerProcessState {
RemoteServer,
LocalServerStarting,
LocalServerRunning,
LocalServerFailed
};
ServerProcessState serverProcessState() const;
Q_SIGNALS:
void serverProcessStatusChanged();
private:
QProcess m_serverProcess;
static ConnectionController* m_instance;
ServerProcessState m_state;
};
diff --git a/client/firsttimedialog.cpp b/client/firsttimedialog.cpp
index 7d2764b..cac9687 100644
--- a/client/firsttimedialog.cpp
+++ b/client/firsttimedialog.cpp
@@ -1,384 +1,384 @@
// SPDX-FileCopyrightText: 2024 g10 code Gmbh
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "firsttimedialog.h"
#include "config.h"
#include "gpgolweb_version.h"
#include "rootcagenerator/controller.h"
#include "ui_confpageinstalladdin.h"
#include "ui_confpageproxyoptions.h"
#include "ui_confpagetlscertificate.h"
#include "ui_confpagewelcome.h"
#include "ui_firsttimedialog.h"
#include "websocketclient.h"
#include "statusdialog.h"
#include "setupdialogs.h"
#include <QCheckBox>
#include <QClipboard>
#include <QCloseEvent>
#include <QDesktopServices>
#include <QFile>
#include <QMargins>
#include <QDialog>
#include <QDialogButtonBox>
#include <QSaveFile>
#include <QSettings>
#include <QStandardPaths>
#include <QStatusBar>
#include <QStyle>
#include <QTemporaryDir>
#include <QToolBar>
#include <Libkleo/Compliance>
#include <KAssistantDialog>
#include <KColorScheme>
#include <KIO/OpenFileManagerWindowJob>
#include <KTitleWidget>
using namespace Qt::StringLiterals;
FirstTimeDialog::FirstTimeDialog(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::FirstTimeDialog)
, confPageWelcome(new Ui::ConfPageWelcome)
, confPageInstallAddin(new Ui::ConfPageInstallAddin)
, confPageProxyOptions(new Ui::ConfPageProxyOptions)
, confPageTLSCertificate(new Ui::ConfPageTLSCertificate)
, m_systemTrayIcon(QIcon::fromTheme(u"com.gnupg.gpgolweb"_s))
{
ui->setupUi(this);
confPageWelcome->setupUi(ui->confPageWelcome);
ui->confPageWelcome->setProperty("title", i18nc("@title", "Welcome to GpgOL/Web"));
confPageProxyOptions->setupUi(ui->confPageProxyOptions);
ui->confPageProxyOptions->setProperty("title", i18nc("@title", "Configure Proxy and Optional Features"));
confPageInstallAddin->setupUi(ui->confPageInstallAddin);
ui->confPageInstallAddin->setProperty("title", i18nc("@title", "Install Outlook Add-In"));
confPageTLSCertificate->setupUi(ui->confPageTLSCertificate);
ui->confPageTLSCertificate->setProperty("title", i18nc("@title", "Setting Up TLS Certificate for Local Proxy"));
if (ui->stack->indexOf(ui->confPageWelcome) != ConfPageWelcome) {
qFatal("Welcome page misplaced");
}
if (ui->stack->indexOf(ui->confPageTLSCertificate) != ConfPageTLSCertificate) {
qFatal("Tls certification page misplaced");
}
if (ui->stack->indexOf(ui->confPageProxyOptions) != ConfPageProxyOptions) {
qFatal("Proxy options page misplaced");
}
if (ui->stack->indexOf(ui->confPageInstallAddin) != ManifestPage) {
qFatal("Manifest install page misplaced");
}
confPageProxyOptions->reencryptOption->setChecked(Config::self()->reencrypt());
connect(confPageProxyOptions->reencryptOption, &QCheckBox::stateChanged, this, [](int state) {
Config::self()->setReencrypt(state == Qt::Checked);
Config::self()->save();
});
auto margins = confPageProxyOptions->remoteServerLayout->contentsMargins();
margins.setLeft(margins.left() + style()->pixelMetric(QStyle::PM_RadioButtonLabelSpacing) + style()->pixelMetric(QStyle::PM_ExclusiveIndicatorWidth));
confPageProxyOptions->remoteServerLayout->setContentsMargins(margins);
m_systemTrayIcon.show();
m_backAction = new QAction(this);
connect(m_backAction, &QAction::triggered, this, [this]() {
if (Controller::certificateAlreadyGenerated() || !Config::self()->isLocalServer()) {
ui->stack->setCurrentIndex(ui->stack->currentIndex() > 1 ? ConfPageProxyOptions : ConfPageWelcome);
} else {
ui->stack->setCurrentIndex(ui->stack->currentIndex() == ConfPageProxyOptions ? ConfPageWelcome : ConfPageProxyOptions);
}
});
connect(confPageTLSCertificate->backButton, &QAbstractButton::clicked, m_backAction, &QAction::trigger);
connect(confPageInstallAddin->backButton, &QAbstractButton::clicked, m_backAction, &QAction::trigger);
connect(confPageProxyOptions->backButton, &QAbstractButton::clicked, m_backAction, &QAction::trigger);
auto toolbar = new QToolBar(this);
toolbar->setMovable(false);
auto titleWidget = new KTitleWidget(this);
toolbar->addWidget(titleWidget);
addToolBar(Qt::TopToolBarArea, toolbar);
titleWidget->setText(ui->stack->currentWidget()->property("title").toString());
connect(ui->stack, &QStackedWidget::currentChanged, this, [titleWidget, this]() {
titleWidget->setText(ui->stack->currentWidget()->property("title").toString());
});
QPixmap logo = QIcon::fromTheme(u"com.gnupg.gpgolweb"_s).pixmap(64, 64);
confPageWelcome->logo->setPixmap(logo);
confPageWelcome->titleWelcome->setText(i18nc("@info", "GpgOL/Web %1", QString::fromLocal8Bit(GPGOLWEB_VERSION_STRING)));
auto statusBar = new QStatusBar(this);
confPageInstallAddin->showOnStartup->setChecked(Config::self()->showLauncher());
connect(confPageInstallAddin->showOnStartup, &QCheckBox::toggled, this, [](bool checked) {
Config::self()->setShowLauncher(checked);
Config::self()->save();
});
m_status = new QLabel;
statusBar->addPermanentWidget(m_status);
auto version = new QLabel(i18nc("@info", "Version: %1", QString::fromLocal8Bit(GPGOLWEB_VERSION_STRING)));
statusBar->addPermanentWidget(version);
if (Kleo::DeVSCompliance::isActive()) {
auto statusLbl = std::make_unique<QLabel>(Kleo::DeVSCompliance::name());
{
auto statusPalette = qApp->palette();
KColorScheme::adjustForeground(statusPalette,
Kleo::DeVSCompliance::isCompliant() ? KColorScheme::NormalText : KColorScheme::NegativeText,
statusLbl->foregroundRole(),
KColorScheme::View);
statusLbl->setAutoFillBackground(true);
KColorScheme::adjustBackground(statusPalette,
Kleo::DeVSCompliance::isCompliant() ? KColorScheme::PositiveBackground : KColorScheme::NegativeBackground,
QPalette::Window,
KColorScheme::View);
statusLbl->setPalette(statusPalette);
}
statusBar->addPermanentWidget(statusLbl.release());
}
setStatusBar(statusBar);
connect(confPageProxyOptions->continueButton, &QPushButton::clicked, this, &FirstTimeDialog::slotSetup);
connect(confPageWelcome->configureButton, &QPushButton::clicked, this, [this]() {
ui->stack->setCurrentIndex(ConfPageProxyOptions);
});
connect(confPageTLSCertificate->continueButton, &QPushButton::clicked, this, &FirstTimeDialog::slotSetup);
confPageTLSCertificate->continueButton->setEnabled(false);
confPageTLSCertificate->installButton->setVisible(false);
confPageTLSCertificate->label->setVisible(false);
connect(confPageTLSCertificate->installButton, &QPushButton::clicked, this, [this]() {
if (m_controller) {
m_controller->install();
}
});
confPageInstallAddin->manifestPath->setText(QLatin1StringView(DATAROUTDIR) + u"/gpgol/manifest.xml"_s);
connect(confPageInstallAddin->testPageButton, &QPushButton::clicked, this, [this]() {
QDesktopServices::openUrl(QUrl(u"https://"_s + serverDomain() + u"/test"_s));
});
connect(confPageInstallAddin->pairWebClientButton, &QPushButton::clicked, this, [this]() {
PairingDialog d(this);
d.exec();
WebsocketClient::self().quitPairingMode();
});
connect(confPageInstallAddin->minimizeButton, &QPushButton::clicked, this, &QWidget::hide);
connect(confPageInstallAddin->manifestPathCopy, &QPushButton::clicked, this, [this]() {
QGuiApplication::clipboard()->setText(confPageInstallAddin->manifestPath->text());
});
connect(confPageInstallAddin->manifestPathOpenFolder, &QPushButton::clicked, this, [this]() {
auto job = new KIO::OpenFileManagerWindowJob();
job->setHighlightUrls({QUrl::fromUserInput(confPageInstallAddin->manifestPath->text())});
if (!qEnvironmentVariableIsEmpty("XDG_ACTIVATION_TOKEN")) {
job->setStartupId(qgetenv("XDG_ACTIVATION_TOKEN"));
}
job->start();
});
confPageProxyOptions->remoteLabel->setEnabled(!Config::self()->isLocalServer());
confPageProxyOptions->remoteServer->setEnabled(!Config::self()->isLocalServer());
confPageProxyOptions->remoteServer->setText(Config::self()->remoteAddress().toString());
confPageProxyOptions->remoteOption->setChecked(!Config::self()->isLocalServer());
connect(confPageProxyOptions->remoteOption, &QRadioButton::toggled, this, [this](bool checked) {
Config::self()->setIsLocalServer(!checked);
Config::self()->save();
confPageProxyOptions->remoteLabel->setEnabled(!Config::self()->isLocalServer());
confPageProxyOptions->remoteServer->setEnabled(!Config::self()->isLocalServer());
});
connect(confPageProxyOptions->remoteServer, &QLineEdit::textChanged, this, [this]() {
Config::self()->setRemoteAddress(QUrl::fromUserInput(confPageProxyOptions->remoteServer->text()));
Config::self()->save();
});
if (Controller::certificateAlreadyGenerated() || !Config::self()->isLocalServer()) {
ui->stack->setCurrentIndex(ConfPageWelcome);
if (Controller::certificateAlreadyGenerated() && Config::self()->isLocalServer()) {
startLocalServer();
}
startWebsocketClient();
} else {
ui->stack->setCurrentIndex(ConfPageProxyOptions);
}
connect(&m_serverProcess, &QProcess::readyReadStandardError, this, [this]() {
qWarning().noquote() << m_serverProcess.readAllStandardError();
});
connect(&m_serverProcess, &QProcess::readyReadStandardOutput, this, [this]() {
qWarning().noquote() << m_serverProcess.readAllStandardOutput();
});
connect(&m_serverProcess, &QProcess::errorOccurred, this, [this](QProcess::ProcessError err) {
qWarning() << "Process error" << err;
qWarning().noquote() << m_serverProcess.readAllStandardError();
});
connect(&m_serverProcess, &QProcess::finished, this, [this](int exitCode, QProcess::ExitStatus status) {
qWarning() << "Process finished" << exitCode << status;
if (status == QProcess::NormalExit) {
qWarning() << "Status code" << m_serverProcess.exitCode();
}
qWarning().noquote() << m_serverProcess.readAllStandardError();
});
#ifdef Q_OS_WIN
// We intentionally don't use our own config for this: If users disable autostart via
// the Windows settings menu, we want to respect that, too.
{
QSettings winreg(u"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"_s, QSettings::NativeFormat);
confPageWelcome->autostartBox->setChecked(!winreg.value(QCoreApplication::applicationName()).toString().isEmpty());
}
connect(confPageWelcome->autostartBox, &QCheckBox::checkStateChanged, this, [this](Qt::CheckState state) {
QSettings winreg(u"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"_s, QSettings::NativeFormat);
if (state) {
winreg.setValue(QCoreApplication::applicationName(),
QDir::toNativeSeparators(QCoreApplication::applicationFilePath()));
} else {
winreg.remove(QCoreApplication::applicationName());
}
});
#else
confPageWelcome->autostartBox->setVisible(false);
#endif
}
FirstTimeDialog::~FirstTimeDialog() = default;
void FirstTimeDialog::slotStateChanged(const QString &stateDisplay)
{
m_status->setText(stateDisplay);
m_systemTrayIcon.stateChanged(stateDisplay, WebsocketClient::self().state());
}
void FirstTimeDialog::closeEvent(QCloseEvent *e)
{
e->ignore();
hide();
}
void FirstTimeDialog::slotSetup()
{
if (confPageProxyOptions->localOption->isChecked()) {
if (!Controller::certificateAlreadyGenerated()) {
delete m_controller;
m_controller = new Controller(this);
confPageTLSCertificate->plainTextEdit->clear();
connect(m_controller, &Controller::generationDone, this, [this]() {
confPageTLSCertificate->installButton->setVisible(true);
confPageTLSCertificate->installButton->setEnabled(true);
confPageTLSCertificate->label->setText(
i18nc("@info", "About to install certificate with fingerprint: %1 ", m_controller->rootFingerprint()));
confPageTLSCertificate->label->setVisible(true);
confPageTLSCertificate->continueButton->setVisible(false);
});
ui->stack->setCurrentIndex(ConfPageTLSCertificate);
connect(m_controller, &Controller::result, this, [this](KJob *) {
if (m_controller->error()) {
confPageTLSCertificate->plainTextEdit->appendPlainText(m_controller->errorText());
return;
}
confPageTLSCertificate->installButton->setVisible(false);
confPageTLSCertificate->continueButton->setVisible(true);
confPageTLSCertificate->label->setText(i18nc("@info", "Installed certificate with fingerprint: %1", m_controller->rootFingerprint()));
confPageTLSCertificate->continueButton->setEnabled(true);
});
connect(m_controller, &Controller::debutOutput, this, &FirstTimeDialog::slotTlsDebutOutput);
m_controller->start();
} else {
startLocalServer();
startWebsocketClient();
generateManifest();
}
} else {
generateManifest();
}
}
void FirstTimeDialog::startLocalServer()
{
if (m_serverProcess.state() != QProcess::NotRunning) {
return;
}
m_serverProcess.start(u"gpgol-server"_s);
}
void FirstTimeDialog::startWebsocketClient()
{
const auto clientId = QUuid::createUuid().toString(QUuid::WithoutBraces);
- auto &websocketClient = WebsocketClient::self(QUrl(u"wss://"_s + serverDomain() + u"/websocket"_s), clientId);
+ auto &websocketClient = WebsocketClient::self();
connect(&websocketClient, &WebsocketClient::stateChanged, this, &FirstTimeDialog::slotStateChanged);
slotStateChanged(websocketClient.stateDisplay());
}
void FirstTimeDialog::slotTlsDebutOutput(const QString &output)
{
confPageTLSCertificate->plainTextEdit->appendPlainText(output);
}
void FirstTimeDialog::generateManifest()
{
QFile file(u":/gpgol-client/manifest.xml.in"_s);
if (!file.open(QIODeviceBase::ReadOnly)) {
Q_ASSERT(false);
return;
}
ui->stack->setCurrentIndex(ManifestPage);
QByteArray manifest = file.readAll();
manifest.replace("%HOST%", serverDomain().toUtf8());
manifest.replace("%VERSION%", GPGOLWEB_VERSION_STRING);
const auto saveFilePath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + u"/gpgol-web-manifest.xml"_s;
QSaveFile saveFile(saveFilePath);
if (!saveFile.open(QIODeviceBase::WriteOnly)) {
Q_ASSERT(false);
return;
}
saveFile.write(manifest);
saveFile.commit();
confPageInstallAddin->manifestPath->setText(QDir::toNativeSeparators(saveFilePath));
}
QString FirstTimeDialog::serverDomain() const
{
return confPageProxyOptions->localOption->isChecked() ? u"localhost:5656"_s : confPageProxyOptions->remoteServer->text();
}
#ifdef Q_OS_WIN
#include <windows.h>
#endif
void FirstTimeDialog::strongActivateWindow(QWidget* window)
{
#ifdef Q_OS_WIN
// HACK: Simulate Alt-keyPress while bringing the window to the front.
// This helps when our app does not currently have focus - and
// frequently it does not, because we have just clicked in browser/outlook.
// https://stackoverflow.com/questions/72620538/whats-the-correct-way-to-bring-a-window-to-the-front
keybd_event(VK_RMENU, 0, 0, 0);
#endif
window->show();
window->activateWindow();
window->raise();
#ifdef Q_OS_WIN
keybd_event(VK_RMENU, 0, KEYEVENTF_KEYUP, 0);
#endif
}
diff --git a/client/main.cpp b/client/main.cpp
index a9fe5aa..2522603 100644
--- a/client/main.cpp
+++ b/client/main.cpp
@@ -1,152 +1,150 @@
// SPDX-FileCopyrightText: 2023 g10 code Gmbh
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QApplication>
#include <QCommandLineParser>
#include <QElapsedTimer>
#include <QFile>
#include <QHttpServer>
#include <QHttpServerResponse>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
#include <QPointer>
#include <QTimer>
#include <QUuid>
#include <Libkleo/FileSystemWatcher>
#include <Libkleo/GnuPG>
#include <Libkleo/KeyCache>
#include <KAboutData>
#include <KJob>
#include <KLocalizedString>
#include "../common/log.h"
#include "config.h"
#include "connectioncontroller.h"
#include "controller.h"
#include "firsttimedialog.h"
#include "gpgol_client_debug.h"
#include "gpgolweb_version.h"
#include "setupdialogs.h"
#include "statusdialog.h"
#include "utils/kuniqueservice.h"
#include "utils/systemtrayicon.h"
using namespace Qt::Literals::StringLiterals;
using namespace std::chrono;
#ifdef Q_OS_WINDOWS
#include <windows.h>
#endif
#define STARTUP_TIMING qCDebug(GPGOL_CLIENT_LOG) << "Startup timing:" << startupTimer.elapsed() << "ms:"
#define STARTUP_TRACE qCDebug(GPGOL_CLIENT_LOG) << "Startup timing:" << startupTimer.elapsed() << "ms:" << SRCNAME << __func__ << __LINE__;
static void setupLogging(std::shared_ptr<Kleo::Log> log)
{
const QByteArray dirNative = qgetenv("GPGOL_CLIENT_LOGDIR");
if (dirNative.isEmpty()) {
return;
}
log->setOutputDirectory(QFile::decodeName(dirNative));
qInstallMessageHandler(Kleo::Log::messageHandler);
}
int main(int argc, char *argv[])
{
#ifdef Q_OS_WINDOWS
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
freopen("CONOUT$", "w", stdout);
freopen("CONOUT$", "w", stderr);
}
#endif
QApplication app(argc, argv);
app.setQuitOnLastWindowClosed(false);
// On Windows, broken heuristics appear to be in place WRT whether our app should exit
// when the last QEventLocker goes out of scope, resulting in sudden quits, after handling a
// websocket request while minimized to systray (the specific conditions appear to include that no
// other window besides the FirstTimeDialog had been visible before minimzing).
// We prefer to quit on our own terms, thank you!
app.setQuitLockEnabled(false);
KLocalizedString::setApplicationDomain(QByteArrayLiteral("gpgol-js-native"));
KAboutData about(QStringLiteral("gpgol-client"),
i18nc("@title:window", "GnuPG Outlook Add-in"),
QStringLiteral(GPGOLWEB_VERSION_STRING),
i18nc("@info", "GPG Outlook add-in"),
KAboutLicense::GPL,
i18nc("@info:credit", "© 2023-2025 g10 Code GmbH"));
about.setDesktopFileName(u"com.gnupg.gpgolweb"_s);
about.setProgramLogo(QIcon::fromTheme(u"com.gnupg.gpgolweb"_s));
about.addAuthor(i18nc("@info:credit", "Carl Schwan"), i18nc("@info:credit", "Maintainer"), u"carl.schwan@gnupg.com"_s, u"https://carlschwan.eu"_s);
about.setTranslator(i18nc("NAME OF TRANSLATORS", "Your names"), i18nc("EMAIL OF TRANSLATORS", "Your emails"));
about.setOrganizationDomain("gnupg.com");
about.setBugAddress("https://dev.gnupg.org/maniphest/task/edit/form/1/");
QCommandLineParser parser;
parser.addOption({u"setup"_s, u"Run first time setup assistant."_s});
KAboutData::setApplicationData(about);
about.setupCommandLine(&parser);
parser.process(app);
about.processCommandLine(&parser);
QElapsedTimer startupTimer;
startupTimer.start();
STARTUP_TIMING << "Application created";
/* Create the unique service ASAP to prevent double starts if
* the application is started twice very quickly. */
KUniqueService service;
QObject::connect(&service, &KUniqueService::activateRequested, &service, [&service](const QStringList &arguments, const QString &workingDirectory) {
Q_UNUSED(arguments);
Q_UNUSED(workingDirectory);
service.setExitValue(0);
});
auto log = Kleo::Log::mutableInstance();
setupLogging(log);
STARTUP_TIMING << "Service created";
+ ConnectionController connection;
+ connection.startStopLocalServer();
+ connection.startWebsocketClient();
bool setup = parser.isSet(u"setup"_s) || (Config::self()->isLocalServer() && !Controller::certificateAlreadyGenerated());
if (setup) {
DialogController::doFirstTimeAssistant();
}
- // TODO: remove me
- //QPointer<FirstTimeDialog> launcher = new FirstTimeDialog;
- ConnectionController connection;
- connection.startLocalServer();
- connection.startWebsocketClient();
SystemTrayIcon icon(QIcon::fromTheme(u"com.gnupg.gpgolweb"_s));
icon.show();
if (Config::self()->showLauncher()) {
auto dialog = StatusDialog::getOrCreate();
dialog->show();
}
STARTUP_TIMING << "KeyCache creation";
auto keyCache = Kleo::KeyCache::mutableInstance();
auto fsWatcher = std::make_shared<Kleo::FileSystemWatcher>();
fsWatcher->whitelistFiles(Kleo::gnupgFileWhitelist());
fsWatcher->addPaths(Kleo::gnupgFolderWhitelist());
fsWatcher->setDelay(1000);
keyCache->addFileSystemWatcher(fsWatcher);
keyCache->startKeyListing();
return app.exec();
}
diff --git a/client/setupdialogs.cpp b/client/setupdialogs.cpp
index 798dfee..883baa8 100644
--- a/client/setupdialogs.cpp
+++ b/client/setupdialogs.cpp
@@ -1,420 +1,422 @@
// SPDX-FileCopyrightText: 2026 g10 code Gmbh
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-Contributor: Thomas Friedrichsmeier <thomas.friedrichsmeier@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "setupdialogs.h"
#include "config.h"
#include "connectioncontroller.h"
#include "gpgolweb_version.h"
#include "rootcagenerator/controller.h"
#include "websocketclient.h"
#include <KAssistantDialog>
#include <KLocalizedString>
#include <KTitleWidget>
#include <QApplication>
#include <QButtonGroup>
#include <QClipboard>
#include <QDialog>
#include <QDialogButtonBox>
#include <QDir>
#include <QDesktopServices>
#include <QFile>
#include <QGroupBox>
#include <QLabel>
#include <QPushButton>
#include <QRadioButton>
#include <QSaveFile>
#include <QSettings>
#include <QToolTip>
#include <QVBoxLayout>
#include "ui_confpageproxyoptions.h"
#include "ui_confpagetlscertificate.h"
using namespace Qt::StringLiterals;
PairingDialog::PairingDialog(QWidget *parent)
: QDialog(parent)
{
setWindowTitle(i18n("Pairing mode active"));
auto l = new QVBoxLayout(this);
auto lab = new QLabel(i18n("<p>Copy and paste the code shown below into the input field shown at the top of the Add-In:</p>"));
lab->setWordWrap(true);
l->addWidget(lab);
m_pairingTokenLabel = new QLabel(i18nc("status message", "<p align='center'><b>Obtaining token...</b></p>"));
l->addWidget(m_pairingTokenLabel);
auto bb = new QDialogButtonBox;
auto endButton = bb->addButton(i18nc("@button", "End pairing mode"), QDialogButtonBox::RejectRole);
connect(endButton, &QAbstractButton::clicked, this, &QDialog::reject);
m_copyButton = bb->addButton(i18nc("@button", "Copy code to clipboard"), QDialogButtonBox::ActionRole);
m_copyButton->setEnabled(false);
l->addWidget(bb);
setFixedSize(sizeHint());
connect(&WebsocketClient::self(), &WebsocketClient::pairingStatusChanged, this, &PairingDialog::pairingStatusChanged);
WebsocketClient::self().enterPairingMode();
}
void PairingDialog::pairingStatusChanged(const QString& token, bool pairingActive) {
if (!pairingActive) {
reject();
return;
}
m_pairingTokenLabel->setText(QString(u"<p align='center'><b>%1</b></p>").arg(token));
m_copyButton->setEnabled(true);
connect(m_copyButton, &QAbstractButton::clicked, [this, token]() {
qApp->clipboard()->setText(token);
});
}
class GenerateCertificateWidget : public QWidget {
Q_OBJECT
public:
GenerateCertificateWidget(bool assistant, QWidget *parent)
: QWidget(parent)
, m_controller(nullptr)
, m_installed(false)
, m_dialog(nullptr)
{
auto hbox = new QHBoxLayout(this);
hbox->setContentsMargins(0, 0, 0, 0);
m_label = new QLabel();
hbox->addWidget(m_label);
m_generateButton = new QPushButton();
m_generateButton->setVisible(!assistant);
if (!assistant) {
connect(m_generateButton, &QPushButton::clicked, this, [this]() {
KAssistantDialog dialog;
addAssistantPages(&dialog);
auto cancelButton = dialog.button(QDialogButtonBox::Cancel);
if (cancelButton) {
cancelButton->hide(); // it does not really have defined behavior
}
startGenerate();
dialog.exec();
});
}
hbox->addWidget(m_generateButton);
updateStatus();
}
void addAssistantPages(KAssistantDialog *dialog) {
m_dialog = dialog;
auto genPage = new QWidget();
auto vbox = new QVBoxLayout(genPage);
m_genProgress = new QPlainTextEdit();
vbox->addWidget(m_genProgress);
m_genDoneLabel = new QLabel();
vbox->addWidget(m_genDoneLabel);
m_genPageItem = new KPageWidgetItem(genPage);
m_genPageItem->setHeader(i18n("Generating TLS certificate"));
m_dialog->addPage(m_genPageItem);
auto installPage = new QWidget();
vbox = new QVBoxLayout(installPage);
m_installProgress = new QPlainTextEdit();
vbox->addWidget(m_installProgress);
m_installDoneLabel = new QLabel();
vbox->addWidget(m_installDoneLabel);
m_installPageItem = new KPageWidgetItem(installPage);
m_installPageItem->setHeader(i18n("Installing certificate"));
m_dialog->addPage(m_installPageItem);
connect(dialog, &KPageDialog::currentPageChanged, this,
[this](KPageWidgetItem *current, KPageWidgetItem *) {
if (current == m_genPageItem) {
startGenerate();
} else if (current == m_installPageItem) {
if (!m_installed) {
m_controller->install();
}
}
});
}
void updateStatus() {
if (Config::self()->isLocalServer()) {
setVisible(true);
if (Controller::certificateAlreadyGenerated()) {
m_label->setText(i18n("A TLS certificate has already been generated."));
m_generateButton->setText(i18n("Re-generate certificate"));
m_generateButton->setFixedSize(m_generateButton->minimumSizeHint());
//m_generateButton->setToolTip(i18n("Re-generates and installs a TLS certificate for a secure connection to the local proxy server. It is not usually necessary to repeat this step."));
} else {
m_label->setText(i18n("A TLS certificate is needed for the secure connection to the proxy."));
m_generateButton->setText(i18n("Generate and install certificate"));
if (m_dialog) {
m_label->setText(i18n("A TLS certificate is needed for the secure connection to the proxy. This will be generated in the next step."));
}
}
} else {
setVisible(false);
}
if (m_dialog) {
m_dialog->setAppropriate(m_genPageItem, Config::self()->isLocalServer() && !Controller::certificateAlreadyGenerated());
m_dialog->setAppropriate(m_installPageItem, Config::self()->isLocalServer() && !Controller::certificateAlreadyGenerated());
}
}
private:
QLabel *m_label;
QPushButton *m_generateButton;
QPlainTextEdit *m_genProgress, *m_installProgress;
QLabel *m_genDoneLabel, *m_installDoneLabel;
KPageWidgetItem *m_genPageItem, *m_installPageItem;
QPointer<Controller> m_controller; // it's a KJob and will auto-delete
bool m_installed;
KAssistantDialog *m_dialog;
void startGenerate() {
if (m_controller || m_installed) {
return;
}
m_dialog->setValid(m_genPageItem, false);
m_dialog->setValid(m_installPageItem, false);
m_controller = new Controller(this);
m_dialog->setValid(m_genPageItem, false);
connect(m_controller, &Controller::generationDone, this, [this]() {
m_genDoneLabel->setText(i18nc("@info", "Successfully generated certificate with fingerprint %1", m_controller->rootFingerprint()));
m_dialog->setValid(m_genPageItem, true);
});
connect(m_controller, &Controller::debutOutput, this, [this](const QString &output) {
m_genProgress->appendPlainText(output);
m_installProgress->appendPlainText(output);
});
connect(m_controller, &Controller::result, this, [this](KJob *) {
if (m_controller->error()) {
m_genProgress->appendPlainText(m_controller->errorText());
m_installProgress->appendPlainText(m_controller->errorText());
return;
}
m_label->setText(i18nc("@info", "Installed certificate with fingerprint: %1", m_controller->rootFingerprint()));
m_dialog->setValid(m_installPageItem, true);
m_installed = true;
});
m_controller->start();
}
};
void DialogController::doDialog(const QList<PageID> &pageIds, const bool assistant) {
KPageDialog *dialog = assistant ? new KAssistantDialog() : new KPageDialog();
auto cancelButton = dialog->button(QDialogButtonBox::Cancel);
if (cancelButton) {
cancelButton->hide(); // it does not really have defined behavior
}
if (pageIds.contains(PageProxy)) {
auto widget = new QWidget();
auto vbox = new QVBoxLayout(widget);
auto item = new KPageWidgetItem(widget, i18n("Proxy"));
item->setHeader(i18nc("@title", "Configure Proxy"));
dialog->addPage(item);
auto label = new QLabel(i18n("Choose your configuration for the proxy component:"));
vbox->addWidget(label);
label = new QLabel(i18n("Note: Changes to this setting only take effect after uploading the adjusted manifest file to Outlook on the next page!"));
auto font = label->font();
font.setItalic(true);
label->setFont(font);
label->setWordWrap(true);
vbox->addWidget(label);
auto certcontrol = new GenerateCertificateWidget(assistant, nullptr);
if (assistant) {
certcontrol->addAssistantPages(static_cast<KAssistantDialog*>(dialog));
}
auto grp = new QButtonGroup(widget);
auto localOption = new QRadioButton(i18n("Run a local proxy on this machine"));
localOption->setChecked(Config::self()->isLocalServer());
auto remoteOption = new QRadioButton(i18n("Use proxy from a remote server (EXPERIMENTAL)"));
remoteOption->setChecked(!Config::self()->isLocalServer());
grp->addButton(localOption);
vbox->addWidget(localOption);
grp->addButton(remoteOption);
vbox->addWidget(remoteOption);
auto hbox = new QHBoxLayout();
auto remoteLabel = new QLabel(i18n("Remote proxy server"));
hbox->addWidget(remoteLabel);
auto remoteServer = new QLineEdit();
remoteServer->setPlaceholderText(u"internal.company.com"_s);
remoteServer->setClearButtonEnabled(true);
hbox->addWidget(remoteServer);
vbox->addLayout(hbox);
QObject::connect(remoteOption, &QRadioButton::toggled, dialog, [remoteLabel, remoteServer, certcontrol](bool checked) {
Config::self()->setIsLocalServer(!checked);
Config::self()->save();
remoteLabel->setEnabled(!Config::self()->isLocalServer());
remoteServer->setEnabled(!Config::self()->isLocalServer());
certcontrol->updateStatus();
+ ConnectionController::instance()->startStopLocalServer();
});
QObject::connect(remoteServer, &QLineEdit::textChanged, dialog, [remoteServer]() {
Config::self()->setRemoteAddress(QUrl::fromUserInput(remoteServer->text()));
Config::self()->save();
+ ConnectionController::instance()->startWebsocketClient();
});
vbox->addWidget(certcontrol);
vbox->addStretch();
}
if (pageIds.contains(PageInstallAddin)) {
auto widget = new QWidget();
auto vbox = new QVBoxLayout(widget);
auto item = new KPageWidgetItem(widget);
item->setHeader(i18nc("@title", "Install Outlook Add-In"));
dialog->addPage(item);
auto grid = new QGridLayout();
vbox->addLayout(grid);
auto makeLabel = [](const QString &label) {
auto ret = new QLabel(label);
ret->setWordWrap(true);
return ret;
};
grid->addWidget(makeLabel(i18n("Before the first use, the add-in has to be activated in Outlook:")), 0, 0, 1, 3);
grid->addWidget(new QLabel(u"1."_s), 1, 0);
grid->addWidget(makeLabel(i18n("Go to the Outlook Extension Manager (you may be prompted to log in):")), 1, 1);
grid->addWidget(new QLabel(u"2."_s), 2, 0);
grid->addWidget(makeLabel(i18n("Generate a manifest file (the filename will be copied to the clipboard)")), 2, 1);
grid->addWidget(new QLabel(u"3."_s), 3, 0);
grid->addWidget(makeLabel(i18n("In Outlook, register this via <tt>My Add-Ins -> Custom Add-Ins -> Add a custom Add-In</tt>")), 3, 1, 1, 2);
grid->addWidget(new QLabel(u"4."_s), 4, 0);
grid->addWidget(makeLabel(i18n("Click on any E-Mail, and activate the add-in by clicking on the GnuPG icon shown about the email header.")), 4, 1, 1, 2);
grid->setColumnStretch(1, 2);
auto extMgrButton = new QPushButton(i18nc("@button", "Open Outlook Extension Manager"));
QObject::connect(extMgrButton, &QPushButton::clicked, dialog, []() {
QDesktopServices::openUrl(QUrl(u"https://outlook.office.com/mail/jsmvvmdeeplink/?path=/options/manageapps&amp;bO=4"_s));
});
grid->addWidget(extMgrButton, 1, 2);
auto generateManifestButton = new QPushButton(i18nc("@button", "Generate Manifest"));
QObject::connect(generateManifestButton, &QPushButton::clicked, dialog, [generateManifestButton]() {
QFile file(u":/gpgol-client/manifest.xml.in"_s);
if (!file.open(QIODeviceBase::ReadOnly)) {
Q_ASSERT(false);
return;
}
QByteArray manifest = file.readAll();
manifest.replace("%HOST%", ConnectionController::serverDomain().toUtf8());
manifest.replace("%VERSION%", GPGOLWEB_VERSION_STRING);
const auto saveFilePath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + u"/gpgol-web-manifest.xml"_s;
QSaveFile saveFile(saveFilePath);
if (!saveFile.open(QIODeviceBase::WriteOnly)) {
Q_ASSERT(false);
return;
}
saveFile.write(manifest);
saveFile.commit();
QGuiApplication::clipboard()->setText(saveFilePath);
QToolTip::showText(generateManifestButton->mapToGlobal(QPoint(10, 10)), i18n("Copied to clipboard."), generateManifestButton, QRect(), 2000);
});
grid->addWidget(generateManifestButton, 2, 2);
//if (!Config::isLocalServer()) {
grid->addWidget(new QLabel(u"5."_s), 5, 0);
grid->addWidget(makeLabel(i18n("When prompted for a pairing code, click here to enter pairing mode:")), 5, 1);
auto pairingButton = new QPushButton(i18nc("@button", "Enter Pairing Mode"));
QObject::connect(pairingButton, &QPushButton::clicked, dialog, [dialog]() {
PairingDialog d(dialog);
d.exec();
WebsocketClient::self().quitPairingMode();
});
grid->addWidget(pairingButton, 5, 2);
//}
auto title = new KTitleWidget();
title->setText(i18n("Troubleshooting"));
vbox->addWidget(title);
grid = new QGridLayout();
vbox->addLayout(grid);
//grid->addWidget(makeLabel(i18n("If the extension is not connected:")), 0, 0, 1, 2);
grid->addWidget(makeLabel(i18n("Test for problems with the TLS-certificate installation, by opening this test page in your browser:")), 0, 0);
/* grid->addWidget(makeLabel(i18n("Sometimes the add-in icon is not immediately visible in Outlook's menu ribbon. Make sure to select an existing message in Outlook. "
"You may also have to click on the \"Apps\" icon.")), 2, 0, 1, 2);
grid->addWidget(makeLabel(i18n("Once you see the add-in, you may want to \"pin\" it for easier access.")), 3, 0, 1, 2);
grid->addWidget(makeLabel(i18n("If you have just added the manifest, it may be necessary to reload / restart Outlook.")), 4, 0, 1, 2);
grid->addWidget(makeLabel(i18n("If your account is organization managed, your administrator may have to allow usage of the GPGOL/Web add-in, manually.")), 5, 0, 1, 2); */
auto testPageButton = new QPushButton(i18nc("@button", "Open Test Page"));
QObject::connect(testPageButton, &QPushButton::clicked, dialog, []() {
QDesktopServices::openUrl(QUrl(u"https://"_s + ConnectionController::serverDomain() + u"/test"_s));
});
grid->addWidget(testPageButton, 0, 1);
grid->setColumnStretch(0, 2);
}
if (pageIds.contains(PageSettings)) {
auto widget = new QWidget();
auto vbox = new QVBoxLayout(widget);
auto featuresbox = new QGroupBox(i18n("Optional features"));
auto boxlayout = new QVBoxLayout(featuresbox);
auto reencrypt = new QCheckBox(i18n("Reencrypt email folders with new keys"), featuresbox);
reencrypt->setChecked(Config::self()->reencrypt());
QObject::connect(reencrypt, &QCheckBox::checkStateChanged, dialog, [](Qt::CheckState state) {
Config::self()->setReencrypt(state == Qt::Checked);
Config::self()->save();
});
boxlayout->addWidget(reencrypt);
vbox->addWidget(featuresbox);
auto startupbox = new QGroupBox(i18n("Startup behavior"));
boxlayout = new QVBoxLayout(startupbox);
#ifdef Q_OS_WIN
auto autoStartBox = new QCheckBox(i18n("Start GPGOL/Web automatically"));
// We intentionally don't use our own config for this: If users disable autostart via
// the Windows settings menu, we want to respect that, too.
{
QSettings winreg(u"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"_s, QSettings::NativeFormat);
autoStartBox->setChecked(!winreg.value(QCoreApplication::applicationName()).toString().isEmpty());
}
QObject::connect(autoStartBox, &QCheckBox::checkStateChanged, dialog, [](Qt::CheckState state) {
QSettings winreg(u"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"_s, QSettings::NativeFormat);
if (state) {
winreg.setValue(QCoreApplication::applicationName(),
QDir::toNativeSeparators(QCoreApplication::applicationFilePath()));
} else {
winreg.remove(QCoreApplication::applicationName());
}
});
boxlayout->addWidget(autoStartBox);
#endif
auto showOnStartup = new QCheckBox(i18n("Show status dialog when starting"));
showOnStartup->setChecked(Config::self()->showLauncher());
QObject::connect(showOnStartup, &QCheckBox::checkStateChanged, dialog, [](Qt::CheckState state) {
Config::self()->setShowLauncher(state == Qt::Checked);
Config::self()->save();
});
boxlayout->addWidget(showOnStartup);
vbox->addWidget(startupbox);
vbox->addStretch();
auto item = new KPageWidgetItem(widget);
item->setHeader(i18nc("@title", "Options"));
dialog->addPage(item);
}
dialog->exec();
}
void DialogController::doFirstTimeAssistant()
{
doDialog(QList{PageProxy, PageInstallAddin, PageSettings}, true);
}
#include "setupdialogs.moc"
diff --git a/client/websocketclient.cpp b/client/websocketclient.cpp
index 42e3e21..2683ed9 100644
--- a/client/websocketclient.cpp
+++ b/client/websocketclient.cpp
@@ -1,458 +1,462 @@
// SPDX-FileCopyrightText: 2023 g10 code Gmbh
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "websocketclient.h"
// Qt headers
#include <QFile>
#include <QHostInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QStandardPaths>
#include <QTimer>
#include <QUuid>
// KDE headers
#include <KConfigGroup>
#include <KLocalizedString>
#include <KMime/Message>
#include <KSharedConfig>
#include <Libkleo/Formatting>
#include <Libkleo/KeyCache>
#include <MimeTreeParserCore/ObjectTreeParser>
// gpgme headers
#include <QGpgME/KeyListJob>
#include <QGpgME/Protocol>
#include <gpgme++/global.h>
#include <gpgme++/key.h>
#include <gpgme++/keylistresult.h>
#include "config.h"
#include "draft/draftmanager.h"
#include "editor/composerwindow.h"
#include "editor/composerwindowfactory.h"
#include "emailviewer.h"
#include "firsttimedialog.h"
#include "gpgol_client_debug.h"
#include "gpgolweb_version.h"
#include "mailapi.h"
#include "protocol.h"
#include "reencrypt/reencryptjob.h"
#include "websocket_debug.h"
using namespace Qt::Literals::StringLiterals;
-WebsocketClient &WebsocketClient::self(const QUrl &url, const QString &clientId)
+WebsocketClient &WebsocketClient::self()
{
static WebsocketClient *client = nullptr;
- if (!client && url.isEmpty()) {
- qFatal() << "Unable to create a client without an url";
- } else if (!client) {
- client = new WebsocketClient(url, clientId);
+ if (!client) {
+ client = new WebsocketClient();
}
return *client;
};
-WebsocketClient::WebsocketClient(const QUrl &url, const QString &clientId)
+WebsocketClient::WebsocketClient()
: QObject(nullptr)
, m_webSocket(QWebSocket(QStringLiteral("Client")))
- , m_url(url)
- , m_clientId(clientId)
, m_state(NotConnected)
, m_stateDisplay(i18nc("@info", "Loading..."))
{
auto job = QGpgME::openpgp()->keyListJob();
connect(job, &QGpgME::KeyListJob::result, this, &WebsocketClient::slotKeyListingDone);
job->start({}, true);
qCWarning(GPGOL_CLIENT_LOG) << "Found the following trusted emails" << m_emails;
connect(&m_webSocket, &QWebSocket::connected, this, &WebsocketClient::slotConnected);
connect(&m_webSocket, &QWebSocket::disconnected, this, [this] {
m_state = NotConnected;
m_stateDisplay = i18nc("@info", "Connection to outlook lost due to a disconnection with the broker.");
Q_EMIT stateChanged(m_stateDisplay);
});
connect(&m_webSocket, &QWebSocket::errorOccurred, this, &WebsocketClient::slotErrorOccurred);
connect(&m_webSocket, &QWebSocket::textMessageReceived, this, &WebsocketClient::slotTextMessageReceived);
connect(&m_webSocket, QOverload<const QList<QSslError> &>::of(&QWebSocket::sslErrors), this, [this](const QList<QSslError> &errors) {
// TODO remove
m_webSocket.ignoreSslErrors(errors);
});
QSslConfiguration sslConfiguration;
auto certPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("certificate.pem"));
Q_ASSERT(!certPath.isEmpty());
QFile certFile(certPath);
if (!certFile.open(QIODevice::ReadOnly)) {
qFatal() << "Couldn't read certificate" << certPath;
}
QSslCertificate certificate(&certFile, QSsl::Pem);
certFile.close();
sslConfiguration.addCaCertificate(certificate);
m_webSocket.setSslConfiguration(sslConfiguration);
-
- m_webSocket.open(url);
}
void WebsocketClient::slotKeyListingDone(const GpgME::KeyListResult &result, const std::vector<GpgME::Key> &keys, const QString &, const GpgME::Error &error)
{
Q_UNUSED(result);
Q_UNUSED(error);
if (error) {
m_stateDisplay = i18nc("@info", "Connection to the Outlook extension lost. Make sure the extension pane is open.");
Q_EMIT stateChanged(m_stateDisplay);
return;
}
QStringList oldEmails = m_emails;
for (const auto &key : keys) {
for (const auto &userId : key.userIDs()) {
const auto email = QString::fromLatin1(userId.email()).toLower();
if (!m_emails.contains(email)) {
m_emails << email;
}
}
}
if (m_emails == oldEmails) {
return;
}
qCWarning(GPGOL_CLIENT_LOG) << "Found the following trusted emails" << m_emails;
if (m_webSocket.state() == QAbstractSocket::ConnectedState) {
slotConnected();
}
}
void WebsocketClient::slotConnected()
{
qCInfo(WEBSOCKET_LOG) << "websocket connected";
sendCommand(Protocol::Register, QJsonObject{
{"emails"_L1, QJsonArray::fromStringList(m_emails)}, // TODO: keep this?
{"type"_L1, "native"_L1},
{"name"_L1, QString(QHostInfo::localHostName() + u" - GpgOL/Web ("_s + QStringLiteral(GPGOLWEB_VERSION_STRING) + u')') }, // TODO: unused
});
sendStatusUpdate(); // in case web client was started before native client
m_state = ConnectedToProxy; /// We still need to connect to the web client
m_stateDisplay = i18nc("@info", "Waiting for web client.");
Q_EMIT stateChanged(m_stateDisplay);
}
void WebsocketClient::slotErrorOccurred(QAbstractSocket::SocketError error)
{
qCWarning(WEBSOCKET_LOG) << error << m_webSocket.errorString();
m_state = (m_webSocket.state() == QAbstractSocket::ConnectedState) ? ConnectedToProxy : NotConnected;
m_stateDisplay = i18nc("@info", "Could not reach the Outlook extension.");
Q_EMIT stateChanged(m_stateDisplay);
- reconnect();
+ QTimer::singleShot(1000, this, [this]() {
+ if (m_webSocket.state() != QAbstractSocket::ConnectedState) {
+ connectToProxy(m_url);
+ }
+ });
}
void WebsocketClient::enterPairingMode()
{
sendCommand(Protocol::PairingRequest, QJsonObject{
{"type"_L1, "native-start-pairing"_L1},
});
}
void WebsocketClient::quitPairingMode()
{
sendCommand(Protocol::PairingRequest, QJsonObject{
{"type"_L1, "native-end-pairing"_L1},
});
}
bool WebsocketClient::sendEWSRequest(const QString &fromEmail, const QString &requestId, const QString &requestBody)
{
KMime::Types::Mailbox mailbox;
mailbox.fromUnicodeString(fromEmail);
sendCommand(Protocol::Ews, QJsonObject{
{"body"_L1, requestBody},
{"email"_L1, QString::fromUtf8(mailbox.address())},
{"requestId"_L1, requestId}
});
return true;
}
// TODO: We should really centralize all calls to this as a single call in the connection stage. Afterwards
// the webclient will not send a new token, anyway.
// However, fixing this is currently on hold pending changes in the pairing process,
// so for now, calls to this are littered all around various requests, but at least we can indentify them
// easily as calls to this function.
void WebsocketClient::initMailApiFromArgs(const QJsonObject &args)
{
MailApiController::init(
(args[u"api"_s].toString() == u"ews"_s) ? MailApiController::EWSApi : MailApiController::GraphApi,
args[u"apiendpoint"_s].toString(),
args[u"ewsAccessToken"_s].toString()
);
}
void WebsocketClient::slotTextMessageReceived(QString message)
{
const auto doc = QJsonDocument::fromJson(message.toUtf8());
if (!doc.isObject()) {
qCWarning(WEBSOCKET_LOG) << "invalid text message received" << message;
return;
}
const auto object = doc.object();
const auto command = Protocol::commandFromString(object["command"_L1].toString());
const auto args = object["arguments"_L1].toObject();
switch (command) {
case Protocol::Disconnection:
// disconnection of the web client
m_state = ConnectedToProxy;
m_stateDisplay = i18nc("@info", "Connection to the Outlook extension lost. Make sure the extension pane is open.");
Q_EMIT stateChanged(m_stateDisplay);
// TODO: handle multiple clients
return;
case Protocol::PairingRequest: {
const auto token = args["token"_L1].toString();
if (token.isEmpty()) {
Q_EMIT pairingStatusChanged(QString(), false);
qCWarning(GPGOL_CLIENT_LOG) << "Pairing complete";
} else {
Q_EMIT pairingStatusChanged(token, true);
}
return;
}
case Protocol::Connection:
// reconnection of the web client
m_state = ConnectedToWebclient;
m_stateDisplay = i18nc("@info", "Connected.");
Q_EMIT stateChanged(m_stateDisplay);
sendStatusUpdate();
return;
case Protocol::View: {
const auto email = args["email"_L1].toString();
const auto displayName = args["displayName"_L1].toString();
const auto id = args["itemId"_L1].toString();
const auto content = m_cachedMime[id];
if (content.isEmpty()) {
return;
}
initMailApiFromArgs(args);
if (!m_emailViewer) {
m_emailViewer = new EmailViewer(QString::fromUtf8(content), email, displayName);
m_emailViewer->setAttribute(Qt::WA_DeleteOnClose);
} else {
m_emailViewer->view(QString::fromUtf8(content), email, displayName);
}
FirstTimeDialog::strongActivateWindow(m_emailViewer);
return;
}
case Protocol::RestoreAutosave: {
const auto email = args["email"_L1].toString();
const auto displayName = args["displayName"_L1].toString();
initMailApiFromArgs(args);
ComposerWindowFactory::self().restoreAutosave(email, displayName);
return;
}
case Protocol::EwsResponse: {
// confirmation that the email was sent
const auto args = object["arguments"_L1].toObject();
Q_EMIT ewsResponseReceived(args["requestId"_L1].toString(), args["body"_L1].toString());
return;
}
case Protocol::Composer:
case Protocol::Reply:
case Protocol::Forward:
case Protocol::OpenDraft: {
const auto email = args["email"_L1].toString();
const auto displayName = args["displayName"_L1].toString();
initMailApiFromArgs(args);
auto dialog = ComposerWindowFactory::self().create(email, displayName);
if (command == Protocol::Reply || command == Protocol::Forward) {
const auto id = args["itemId"_L1].toString();
const auto content = m_cachedMime[id];
if (content.isEmpty()) {
return;
}
KMime::Message::Ptr message(new KMime::Message());
message->setContent(KMime::CRLFtoLF(content));
message->parse();
if (command == Protocol::Reply) {
dialog->reply(message);
} else {
dialog->forward(message);
}
} else if (command == Protocol::OpenDraft) {
const auto draftId = args["draftId"_L1].toString();
if (draftId.isEmpty()) {
return;
}
const auto draft = DraftManager::self().draftById(draftId.toUtf8());
dialog->setMessage(draft.mime());
}
FirstTimeDialog::strongActivateWindow(dialog);
return;
}
case Protocol::DeleteDraft: {
const auto draftId = args["draftId"_L1].toString();
if (draftId.isEmpty()) {
qWarning() << "Draft not valid";
return;
}
const auto draft = DraftManager::self().draftById(draftId.toUtf8());
if (!draft.isValid()) {
qWarning() << "Draft not valid";
return;
}
if (!DraftManager::self().remove(draft)) {
qCWarning(GPGOL_CLIENT_LOG) << "Could not delete draft";
return;
}
sendStatusUpdate();
return;
}
case Protocol::Reencrypt: {
initMailApiFromArgs(args);
reencrypt(args);
return;
}
case Protocol::Info: {
initMailApiFromArgs(args);
info(args);
return;
}
default:
qCWarning(WEBSOCKET_LOG) << "Unhandled command" << command;
}
}
void WebsocketClient::reencrypt(const QJsonObject &args)
{
if (m_reencryptJob) {
if (m_reencryptJob->hasStarted()) {
m_reencryptJob->tryRaiseDialog();
return;
}
m_reencryptJob->deleteLater();
}
// TODO: Looking up the folderId by itemId would allow some simplification in the web.js
m_reencryptJob = new ReencryptJob(args["folderId"_L1].toString());
m_reencryptJob->start();
}
-void WebsocketClient::reconnect()
+void WebsocketClient::connectToProxy(const QUrl &url)
{
- QTimer::singleShot(1000ms, this, [this]() {
+ if (m_url != url) {
+ m_url = url;
+ if (m_webSocket.state() != QAbstractSocket::UnconnectedState) {
+ m_webSocket.abort();
+ }
+ }
+ if (m_webSocket.state() != QAbstractSocket::ConnectedState) {
m_webSocket.open(m_url);
- });
+ }
}
WebsocketClient::State WebsocketClient::state() const
{
return m_state;
}
QString WebsocketClient::stateDisplay() const
{
return m_stateDisplay;
}
void WebsocketClient::sendCommand(Protocol::Command command, const QJsonObject &arguments)
{
const auto json = Protocol::makeCommand(command, arguments, getId());
m_webSocket.sendTextMessage(QString::fromUtf8(QJsonDocument(json).toJson()));
}
void WebsocketClient::sendStatusUpdate(bool viewerJustClosed)
{
QJsonArray features;
if (Config::self()->reencrypt()) {
features << u"reencrypt"_s;
}
sendCommand(Protocol::StatusUpdate, QJsonObject{
{"drafts"_L1, DraftManager::self().toJson()},
{"viewerOpen"_L1, !viewerJustClosed && !m_emailViewer.isNull()},
{"features"_L1, features}
});
}
void WebsocketClient::info(const QJsonObject &args)
{
const auto email = args["email"_L1].toString();
sendStatusUpdate(false); // web client expects to know that info before info-fetched
const QString id(args["itemId"_L1].toString());
qCWarning(GPGOL_CLIENT_LOG) << "Info for" << id << "requested";
if (m_cachedInfo.contains(id)) {
sendCommand(Protocol::InfoFetched, m_cachedInfo[id]);
return;
}
MailApiController::self().setAccessToken(args["ewsAccessToken"_L1].toString());
auto request = MailApiController::self().getMails({id}, GetMailsJob::GetMimeContent | GetMailsJob::GetParentFolderId);
connect(request, &GetMailsJob::finished, this, [this, id, args, request]() {
if (request->error() != KJob::NoError) {
sendCommand(Protocol::Error, QJsonObject{{"error"_L1, request->errorString()}});
qCWarning(GPGOL_CLIENT_LOG) << "Failure to get mail:" << request->errorText();
return;
}
qCWarning(GPGOL_CLIENT_LOG) << "Info for" << id << "fetched";
const auto responses = request->takeResponses();
if (responses.isEmpty()) {
return;
}
const auto item = responses.first();
const auto mimeContent = GetMailsJob::mimeContent(item);
KMime::Message::Ptr message(new KMime::Message());
message->setContent(KMime::CRLFtoLF(mimeContent));
message->parse();
MimeTreeParser::ObjectTreeParser treeParser;
treeParser.parseObjectTree(message.get());
const auto data = QJsonObject{
{"itemId"_L1, args["itemId"_L1]},
{"folderId"_L1, GetMailsJob::parentFolderId(item)},
{"email"_L1, args["email"_L1]},
{"encrypted"_L1, treeParser.hasEncryptedParts()},
{"signed"_L1, treeParser.hasSignedParts()},
{"version"_L1, QStringLiteral(GPGOLWEB_VERSION_STRING)},
};
m_cachedInfo[id] = data;
m_cachedMime[id] = mimeContent;
sendCommand(Protocol::InfoFetched, data);
});
request->start();
}
QString WebsocketClient::getId() const
{
auto config = KSharedConfig::openStateConfig();
auto machineGroup = config->group(u"Machine"_s);
if (machineGroup.exists() && machineGroup.hasKey(u"Id"_s)) {
return machineGroup.readEntry(u"Id"_s);
}
const auto id = QUuid::createUuid().toString(QUuid::WithoutBraces);
machineGroup.writeEntry("Id", id);
config->sync();
return id;
}
diff --git a/client/websocketclient.h b/client/websocketclient.h
index 96c3a75..b153004 100644
--- a/client/websocketclient.h
+++ b/client/websocketclient.h
@@ -1,95 +1,94 @@
// SPDX-FileCopyrightText: 2023 g10 code Gmbh
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "../common/protocol.h"
#include <Libkleo/KeyCache>
#include <QPointer>
#include <QWebSocket>
#include <QJsonObject>
#include <chrono>
class EmailViewer;
class ReencryptJob;
class KJob;
namespace GpgME
{
class KeyListResult;
class Key;
class Error;
}
using namespace std::chrono;
class WebsocketClient : public QObject
{
Q_OBJECT
Q_PROPERTY(State state READ state NOTIFY stateChanged)
Q_PROPERTY(QString stateDisplay READ stateDisplay NOTIFY stateChanged)
public:
enum State {
NotConnected,
ConnectedToProxy,
ConnectedToWebclient,
};
- static WebsocketClient &self(const QUrl &url = {}, const QString &clientId = {});
+ static WebsocketClient &self();
State state() const;
QString stateDisplay() const;
/// \params fromEmail The email address who should send this EWS request.
/// \params requestId Identifier of the request, will be send back in ewsResponseReceived
/// \params requestBody The SOAP request body.
bool sendEWSRequest(const QString &fromEmail, const QString &requestId, const QString &requestBody);
void sendStatusUpdate(bool viewerJustClosed=false);
void enterPairingMode();
void quitPairingMode();
+ void connectToProxy(const QUrl &url);
Q_SIGNALS:
void stateChanged(const QString &state);
void pairingStatusChanged(const QString& token, bool pairingActive);
void ewsResponseReceived(const QString &requestId, const QString &responseBody);
void emailSent(const QString &requestId, const QString &error);
private Q_SLOTS:
void slotConnected();
void slotErrorOccurred(QAbstractSocket::SocketError error);
void slotTextMessageReceived(QString message);
void slotKeyListingDone(const GpgME::KeyListResult &result,
const std::vector<GpgME::Key> &keys,
const QString &auditLogAsHtml,
const GpgME::Error &auditLogError);
QString getId() const;
private:
void sendCommand(Protocol::Command command, const QJsonObject &arguments);
- explicit WebsocketClient(const QUrl &url, const QString &clientId);
- void reconnect();
+ WebsocketClient();
void reencrypt(const QJsonObject &args);
void info(const QJsonObject &args);
void initMailApiFromArgs(const QJsonObject &args);
bool m_wasConnected = false;
QWebSocket m_webSocket;
QUrl m_url;
QStringList m_emails;
- QString m_clientId;
std::chrono::milliseconds m_delay = 2000ms;
State m_state = NotConnected;
QString m_stateDisplay;
QPointer<EmailViewer> m_emailViewer;
QPointer<ReencryptJob> m_reencryptJob;
QHash<QString, QJsonObject> m_cachedInfo;
QHash<QString, QByteArray> m_cachedMime;
};

File Metadata

Mime Type
text/x-diff
Expires
Thu, Feb 26, 6:27 PM (14 h, 40 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
87/0e/3f481cf85c41176ef117e210d0bf

Event Timeline