diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index b9e84e9..ae38f43 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -1,278 +1,280 @@ # SPDX-FileCopyrightText: 2023 g10 code GmbH # SPDX-Contributor: Carl Schwan # SPDX-License-Identifier: BSD-2-Clause add_library(gpgol-client-static STATIC) target_sources(gpgol-client-static PRIVATE websocketclient.cpp websocketclient.h webserver.h webserver.cpp # Identity identity/addressvalidationjob.cpp identity/addressvalidationjob.h identity/identitymanager.cpp identity/identitymanager.h identity/identitydialog.cpp identity/identitydialog.h identity/identity.cpp identity/identity.h identity/signature.h identity/signature.cpp identity/signatureconfigurator.cpp identity/signatureconfigurator.h identity/signaturerichtexteditor.cpp identity/signaturerichtexteditor_p.h # HTTP Controller controllers/emailcontroller.cpp controllers/emailcontroller.h + controllers/registrationcontroller.cpp + controllers/registrationcontroller.h # Draft draft/draft.cpp draft/draft.h draft/draftmanager.cpp draft/draftmanager.h # EWS integration ews/ewsattachment.cpp ews/ewsattachment.h ews/ewsattendee.cpp ews/ewsattendee.h ews/ewsclient_debug.cpp ews/ewsclient_debug.h ews/ewsid.cpp ews/ewsid.h ews/ewsitem.cpp ews/ewsitem.h ews/ewsitembase.cpp ews/ewsitembase.h ews/ewsitembase_p.h ews/ewsmailbox.cpp ews/ewsmailbox.h ews/ewsmailfactory.cpp ews/ewsmailfactory.h ews/ewsoccurrence.cpp ews/ewsoccurrence.h ews/ewspropertyfield.cpp ews/ewspropertyfield.h ews/ewsrecurrence.cpp ews/ewsrecurrence.h ews/ewsserverversion.cpp ews/ewsserverversion.h ews/ewstypes.cpp ews/ewstypes.h ews/ewsxml.cpp ews/ewsxml.h # Editor editor/addresseelineedit.cpp editor/addresseelineedit.h editor/addresseelineeditmanager.cpp editor/addresseelineeditmanager.h editor/composer.cpp editor/composer.h editor/composerviewbase.cpp editor/composerviewbase.h editor/composerwindow.cpp editor/composerwindow.h editor/composerwindowfactory.cpp editor/composerwindowfactory.h editor/cryptostateindicatorwidget.cpp editor/cryptostateindicatorwidget.h editor/kmcomposerglobalaction.cpp editor/kmcomposerglobalaction.h editor/nearexpirywarning.cpp editor/nearexpirywarning.h editor/mailtemplates.cpp editor/mailtemplates.h editor/recipient.cpp editor/recipient.h editor/recipientline.cpp editor/recipientline.h editor/recipientseditor.cpp editor/recipientseditor.h editor/util.h editor/util.cpp editor/kmailcompletion.cpp editor/kmailcompletion.h editor/richtextcomposerng.cpp editor/richtextcomposerng.h editor/richtextcomposersignatures.cpp editor/richtextcomposersignatures.h editor/nodehelper.cpp editor/nodehelper.h editor/signaturecontroller.cpp editor/signaturecontroller.h editor/spellcheckerconfigdialog.cpp editor/spellcheckerconfigdialog.h # Editor job editor/job/abstractencryptjob.h editor/job/autocryptheadersjob.h editor/job/contentjobbase.h editor/job/contentjobbase_p.h editor/job/encryptjob.h editor/job/inserttextfilejob.h editor/job/itipjob.h editor/job/jobbase.h editor/job/jobbase_p.h editor/job/maintextjob.h editor/job/multipartjob.h editor/job/protectedheadersjob.h editor/job/signencryptjob.h editor/job/signjob.h editor/job/singlepartjob.h editor/job/skeletonmessagejob.h editor/job/transparentjob.h editor/job/autocryptheadersjob.cpp editor/job/contentjobbase.cpp editor/job/encryptjob.cpp editor/job/inserttextfilejob.cpp editor/job/itipjob.cpp editor/job/jobbase.cpp editor/job/maintextjob.cpp editor/job/multipartjob.cpp editor/job/protectedheadersjob.cpp editor/job/saveasfilejob.cpp editor/job/saveasfilejob.h editor/job/signencryptjob.cpp editor/job/signjob.cpp editor/job/singlepartjob.cpp editor/job/skeletonmessagejob.cpp editor/job/transparentjob.cpp ## Editor Part editor/part/globalpart.h editor/part/infopart.h editor/part/itippart.h editor/part/messagepart.h editor/part/textpart.h editor/part/globalpart.cpp editor/part/infopart.cpp editor/part/itippart.cpp editor/part/messagepart.cpp editor/part/textpart.cpp ## Attachment editor/attachment/attachmentjob.cpp editor/attachment/attachmentjob.h editor/attachment/attachmentclipboardjob.cpp editor/attachment/attachmentclipboardjob.h editor/attachment/attachmentcompressjob.cpp editor/attachment/attachmentcompressjob.h editor/attachment/attachmentcontroller.cpp editor/attachment/attachmentcontroller.h editor/attachment/attachmentcontrollerbase.cpp editor/attachment/attachmentcontrollerbase.h editor/attachment/attachmentfromfolderjob.cpp editor/attachment/attachmentfromfolderjob.h editor/attachment/attachmentfrommimecontentjob.cpp editor/attachment/attachmentfrommimecontentjob.h editor/attachment/attachmentfromurlbasejob.cpp editor/attachment/attachmentfromurlbasejob.h editor/attachment/attachmentfromurljob.cpp editor/attachment/attachmentfromurljob.h editor/attachment/attachmentfromurlutils.cpp editor/attachment/attachmentfromurlutils.h editor/attachment/attachmentfrompublickeyjob.cpp editor/attachment/attachmentfrompublickeyjob.h editor/attachment/attachmentloadjob.cpp editor/attachment/attachmentloadjob.h editor/attachment/attachmentmodel.cpp editor/attachment/attachmentmodel.h editor/attachment/attachmentpart.cpp editor/attachment/attachmentpart.h editor/attachment/attachmentpropertiesdialog.cpp editor/attachment/attachmentpropertiesdialog.h editor/attachment/attachmentupdatejob.cpp editor/attachment/attachmentupdatejob.h editor/attachment/attachmentview.cpp editor/attachment/attachmentview.h ) ki18n_wrap_ui(gpgol-client-static editor/attachment/ui/attachmentpropertiesdialog.ui editor/attachment/ui/attachmentpropertiesdialog_readonly.ui ) ecm_qt_declare_logging_category(gpgol-client-static_SRCS HEADER websocket_debug.h IDENTIFIER WEBSOCKET_LOG CATEGORY_NAME org.gpgol.client.websocket DESCRIPTION "Websocket connection in the client" EXPORT GPGOL ) ecm_qt_declare_logging_category(gpgol-client-static_SRCS HEADER ewsresource_debug.h IDENTIFIER EWSRES_LOG CATEGORY_NAME org.gpgol.ews DESCRIPTION "Ews mail client" EXPORT GPGOL ) ecm_qt_declare_logging_category(gpgol-client-static_SRCS HEADER ewscli_debug.h IDENTIFIER EWSCLI_LOG CATEGORY_NAME org.gpgol.ews.client DESCRIPTION "ews client (gpgol-client)" EXPORT GPGOL ) ecm_qt_declare_logging_category(gpgol-client-static_SRCS HEADER editor_debug.h IDENTIFIER EDITOR_LOG CATEGORY_NAME org.gpgol.editor DESCRIPTION "mail composer" EXPORT GPGOL ) set(WARN_TOOMANY_RECIPIENTS_DEFAULT true) set(ALLOW_SEMICOLON_AS_ADDRESS_SEPARATOR_DEFAULT true) configure_file(editor/settings/messagecomposer.kcfg.in ${CMAKE_CURRENT_BINARY_DIR}/messagecomposer.kcfg) kconfig_add_kcfg_files(gpgol-client-static editor/settings/messagecomposersettings.kcfgc) install(FILES composerui.rc DESTINATION ${KDE_INSTALL_KXMLGUIDIR}/gpgol-client) target_sources(gpgol-client-static PUBLIC ${gpgol-client-static_SRCS}) target_link_libraries(gpgol-client-static PUBLIC common Qt6::HttpServer Qt6::Widgets Qt6::PrintSupport KF6::JobWidgets KF6::CalendarCore KF6::ConfigCore KF6::ConfigGui KF6::Contacts KF6::Completion KF6::CoreAddons KF6::ColorScheme KF6::Codecs KF6::GuiAddons KF6::SonnetUi KF6::WidgetsAddons KF6::XmlGui KF6::Archive KF6::TextAutoCorrectionCore KPim6::MimeTreeParserWidgets KPim6::Libkleo KPim6::Libkdepim KPim6::PimTextEdit ) add_executable(gpgol-client main.cpp) target_link_libraries(gpgol-client PRIVATE gpgol-client-static) if (BUILD_TESTING) add_subdirectory(autotests) endif() diff --git a/client/autotests/emailcontrollertest.cpp b/client/autotests/emailcontrollertest.cpp index 8227d5b..62956e5 100644 --- a/client/autotests/emailcontrollertest.cpp +++ b/client/autotests/emailcontrollertest.cpp @@ -1,155 +1,155 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../webserver.h" #include "../websocketclient.h" #include "../draft/draftmanager.h" #include "../editor/composerwindow.h" #include "../editor/recipientseditor.h" #include #include #include using namespace Qt::Literals::StringLiterals; class EmailControllerTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase() { DraftManager::self(true); QCoreApplication::setApplicationName(u"gpgol-server"_s); KLocalizedString::setApplicationDomain(QByteArrayLiteral("gpgol")); - m_webServer = new WebServer; + m_webServer = new WebServer(u"TestId"_s); m_webServer->run(); auto webSocketServer = new QWebSocketServer(QStringLiteral("SSL Server"), QWebSocketServer::NonSecureMode); if (webSocketServer->listen(QHostAddress::Any, 5657)) { } } void testInfoEmailAction() { QFile file(QStringLiteral(DATA_DIR) + u"/encrypted.mbox"_s); QVERIFY(file.open(QIODeviceBase::ReadOnly)); QNetworkRequest request(QUrl(u"http://127.0.0.1:%1/info"_s.arg(m_webServer->port()))); auto reply = m_qnam.post(request, file.readAll()); QSignalSpy spy(reply, &QNetworkReply::finished); spy.wait(); QVERIFY(reply->error() == QNetworkReply::NoError); const auto doc = QJsonDocument::fromJson(reply->readAll()); QVERIFY(!doc.isNull() && doc.isObject()); const auto object = doc.object(); QVERIFY(object["drafts"_L1].toArray().isEmpty()); QVERIFY(object["encrypted"_L1].toBool()); QVERIFY(!object["signed"_L1].toBool()); } void testViewEmailAction() { QFile file(QStringLiteral(DATA_DIR) + u"/plaintext.mbox"_s); QVERIFY(file.open(QIODeviceBase::ReadOnly)); QNetworkRequest request(QUrl(u"http://127.0.0.1:%1/view"_s.arg(m_webServer->port()))); auto reply = m_qnam.post(request, file.readAll()); QSignalSpy spy(reply, &QNetworkReply::finished); spy.wait(); QVERIFY(reply->error() == QNetworkReply::NoError); const auto widgets = qApp->topLevelWidgets(); QVERIFY(!widgets.isEmpty()); MimeTreeParser::Widgets::MessageViewerDialog *dialog = nullptr; for (auto widget : widgets) { if (!widget->isHidden()) { if (const auto messageViewer = qobject_cast(widget)) { dialog = messageViewer; break; } } } QVERIFY(dialog); WebsocketClient::self(QUrl(u"ws://127.0.0.1"_s), 5656); const auto toolBar = dialog->toolBar(); QVERIFY(toolBar->isVisible()); const auto actions = toolBar->actions(); QCOMPARE(actions.count(), 3); qWarning() << actions; QCOMPARE(actions[1]->icon().name(), u"mail-reply-sender-symbolic"_s); actions[1]->trigger(); const auto widgets2 = qApp->topLevelWidgets(); QVERIFY(!widgets2.isEmpty()); ComposerWindow *composer = nullptr; for (auto widget : widgets2) { if (!widget->isHidden()) { if (const auto composerWindow = qobject_cast(widget)) { composer = composerWindow; break; } } } QVERIFY(composer); QSignalSpy spyInit(composer, &ComposerWindow::initialized); spyInit.wait(); QCOMPARE(composer->subject(), u"RE: A random subject with alternative contenttype"_s); const auto recipients = composer->recipientsEditor()->recipients(); QCOMPARE(recipients.count(), 2); QCOMPARE(recipients[0]->email(), u"konqi@example.org"_s); QCOMPARE(recipients[0]->name(), u"Konqi"_s); QCOMPARE(recipients[1]->email(), u"konqi@kde.org"_s); QVERIFY(recipients[1]->name().isEmpty()); } void cleanupTestCase() { m_webServer->deleteLater(); } private: QThread *m_thread = nullptr; WebServer *m_webServer = nullptr; QWebSocketServer *m_webSocketServer = nullptr; QNetworkAccessManager m_qnam; }; QTEST_MAIN(EmailControllerTest) #include "emailcontrollertest.moc" diff --git a/client/controllers/registrationcontroller.cpp b/client/controllers/registrationcontroller.cpp new file mode 100644 index 0000000..e8a824f --- /dev/null +++ b/client/controllers/registrationcontroller.cpp @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2023 g10 code GmbH +// SPDX-Contributor: Carl Schwan +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "registrationcontroller.h" + +#include +#include + +using namespace Qt::Literals::StringLiterals; + +QHttpServerResponse RegistrationController::confirmRegistrationAction(const QHttpServerRequest &request, const QString &clientId) +{ + const auto json = QJsonDocument::fromJson(request.body()); + if (json.isNull() || !json.isObject()) { + return QHttpServerResponse(QJsonObject { + { "status"_L1, "500"_L1 }, + { "error"_L1, "Invalid json"_L1 }, + }); + } + + const auto obj = json.object(); + const auto token = obj["token"_L1].toString(); + + if (token.isEmpty()) { + return QHttpServerResponse(QJsonObject { + { "status"_L1, "500"_L1 }, + { "error"_L1, "Empty token"_L1 }, + }); + } + + const auto hash = QCryptographicHash::hash((token + clientId).toUtf8(), QCryptographicHash::Sha256).toHex(); + + return QHttpServerResponse(QJsonObject { + { "hash"_L1, QString::fromUtf8(hash) } + }); +} diff --git a/client/controllers/registrationcontroller.h b/client/controllers/registrationcontroller.h new file mode 100644 index 0000000..873a92d --- /dev/null +++ b/client/controllers/registrationcontroller.h @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2023 g10 code GmbH +// SPDX-Contributor: Carl Schwan +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +namespace RegistrationController +{ + QHttpServerResponse confirmRegistrationAction(const QHttpServerRequest &request, const QString &clientId); +} diff --git a/client/main.cpp b/client/main.cpp index 2663051..66cde30 100644 --- a/client/main.cpp +++ b/client/main.cpp @@ -1,50 +1,52 @@ // SPDX-FileCopyrightText: 2023 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "controllers/emailcontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include "websocketclient.h" #include "webserver.h" #include "qnam.h" using namespace Qt::Literals::StringLiterals; using namespace std::chrono; int main(int argc, char *argv[]) { QApplication app(argc, argv); app.setQuitOnLastWindowClosed(false); KLocalizedString::setApplicationDomain(QByteArrayLiteral("gpgol")); QObject::connect(qnam, &QNetworkAccessManager::sslErrors, qnam, [](QNetworkReply *reply, const QList &) { reply->ignoreSslErrors(); }); - WebServer server; + const auto clientId = QUuid::createUuid().toString(QUuid::WithoutBraces); + + WebServer server(clientId); server.run(); if (!server.running()) { qWarning() << "Server failed to listen on a port."; return 1; } const auto port = server.port(); - WebsocketClient::self(QUrl(u"wss://localhost:5657/"_s), port); + WebsocketClient::self(QUrl(u"wss://localhost:5657/"_s), port, clientId); auto keyCache = Kleo::KeyCache::mutableInstance(); keyCache->startKeyListing(); return app.exec(); } diff --git a/client/webserver.cpp b/client/webserver.cpp index b67066a..cdc1ac0 100644 --- a/client/webserver.cpp +++ b/client/webserver.cpp @@ -1,46 +1,53 @@ // SPDX-FileCopyrightText: 2024 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "webserver.h" #include "controllers/emailcontroller.h" +#include "controllers/registrationcontroller.h" +#include +#include using namespace Qt::Literals::StringLiterals; -WebServer::WebServer(QObject *parent) +WebServer::WebServer(const QString &clientId, QObject *parent) : QObject(parent) + , m_clientId(clientId) { m_server.route(u"/view"_s, &EmailController::viewEmailAction); m_server.route(u"/info"_s, &EmailController::infoEmailAction); m_server.route(u"/new"_s, &EmailController::newEmailAction); m_server.route(u"/forward"_s, &EmailController::forwardEmailAction); m_server.route(u"/reply"_s, &EmailController::replyEmailAction); m_server.route(u"/draft/"_s, &EmailController::draftAction); + m_server.route(u"/confirm-registration"_s, [this](const QHttpServerRequest &request) { + return RegistrationController::confirmRegistrationAction(request, m_clientId); + }); m_server.afterRequest([](QHttpServerResponse &&resp) { resp.setHeader("Access-Control-Allow-Origin", "*"); return std::move(resp); }); } void WebServer::run() { m_port = m_server.listen(); if (!m_port) { qWarning() << "Server failed to listen on a port."; m_running = false; return; } qWarning() << u"Running on http://127.0.0.1:%1/ (Press CTRL+C to quit)"_s.arg(m_port); m_running = true; } int WebServer::port() const { return m_port; } bool WebServer::running() const { return m_running; } diff --git a/client/webserver.h b/client/webserver.h index 44aeab8..61e2572 100644 --- a/client/webserver.h +++ b/client/webserver.h @@ -1,33 +1,34 @@ // SPDX-FileCopyrightText: 2024 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include #include /// The webserver of the native client /// /// This webserver will receive webrequests from the outlook web client via /// the broker. class WebServer : public QObject { Q_OBJECT public: /// Default contructor. - WebServer(QObject *parent = nullptr); + WebServer(const QString &clientId, QObject *parent = nullptr); /// Start webserver. void run(); /// \return the port of the webserver. Qt will randomly assign a free port to this /// process. int port() const; /// \return whether the webserver is running. bool running() const; private: int m_port; bool m_running = false; QHttpServer m_server; + QString m_clientId; }; diff --git a/client/websocketclient.cpp b/client/websocketclient.cpp index 012f3da..23c3a78 100644 --- a/client/websocketclient.cpp +++ b/client/websocketclient.cpp @@ -1,190 +1,190 @@ // SPDX-FileCopyrightText: 2023 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "websocketclient.h" // Qt headers #include #include #include #include #include #include #include #include // KDE headers #include #include // gpgme headers #include #include "websocket_debug.h" #include "qnam.h" using namespace std::chrono; using namespace Qt::Literals::StringLiterals; namespace { QStringList trustedEmails(const std::shared_ptr &keyCache) { QStringList emails; const auto keys = keyCache->keys(); for (const auto &key : keys) { for (const auto &userId : key.userIDs()) { if (key.ownerTrust() == GpgME::Key::Ultimate) { emails << QString::fromLatin1(userId.email()).toLower(); break; } } } return emails; } auto delay = 2000ms; -void registerServer(int port, const QStringList &emails, const QString &serverId) +void registerServer(int port, const QStringList &emails, const QString &clientId) { QNetworkRequest registerRequest(QUrl(u"https://127.0.0.1:5656/register"_s)); registerRequest.setHeader(QNetworkRequest::ContentTypeHeader, u"application/json"_s); auto registerReply = qnam->post(registerRequest, QJsonDocument(QJsonObject{ { "port"_L1, port }, { "emails"_L1, QJsonArray::fromStringList(emails) }, - { "serverId"_L1, serverId }, + { "clientId"_L1, clientId }, }).toJson()); - QObject::connect(registerReply, &QNetworkReply::finished, qnam, [port, emails, serverId, registerReply]() { + QObject::connect(registerReply, &QNetworkReply::finished, qnam, [port, emails, clientId, registerReply]() { if (registerReply->error() != QNetworkReply::NoError) { - QTimer::singleShot(delay, [emails, port, serverId]() { + QTimer::singleShot(delay, [emails, port, clientId]() { delay *= 2; - registerServer(port, emails, serverId); + registerServer(port, emails, clientId); }); qWarning() << "Failed to register" << registerReply->errorString() << "retrying in" << delay; } else { qWarning() << "Register"; } registerReply->deleteLater(); }); } } -WebsocketClient &WebsocketClient::self(const QUrl &url, int port) +WebsocketClient &WebsocketClient::self(const QUrl &url, int port, const QString &clientId) { static WebsocketClient *client = nullptr; if (!client && url.isEmpty()) { qFatal() << "Unable to create a client without an url"; } else if (!client) { - client = new WebsocketClient(url, port); + client = new WebsocketClient(url, port, clientId); } return *client; }; -WebsocketClient::WebsocketClient(const QUrl &url, int port) +WebsocketClient::WebsocketClient(const QUrl &url, int port, const QString &clientId) : QObject(nullptr) + , m_webSocket(QWebSocket(QStringLiteral("Client"))) , m_url(url) , m_port(port) + , m_clientId(clientId) { connect(&m_webSocket, &QWebSocket::connected, this, &WebsocketClient::slotConnected); connect(&m_webSocket, &QWebSocket::disconnected, this, [this] { Q_EMIT closed(i18nc("@info", "Connection to outlook lost due to a disconnection with the broker.")); }); connect(&m_webSocket, &QWebSocket::errorOccurred, this, &WebsocketClient::slotErrorOccurred); connect(&m_webSocket, &QWebSocket::textMessageReceived, this, &WebsocketClient::slotTextMessageReceived); connect(&m_webSocket, QOverload&>::of(&QWebSocket::sslErrors), this, [this](const QList &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"; } QSslCertificate certificate(&certFile, QSsl::Pem); certFile.close(); sslConfiguration.addCaCertificate(certificate); m_webSocket.setSslConfiguration(sslConfiguration); m_webSocket.open(url); auto globalKeyCache = Kleo::KeyCache::instance(); m_emails = trustedEmails(globalKeyCache); if (m_emails.isEmpty()) { qWarning() << "No ultimately keys found in keychain"; return; } - m_serverId = QUuid::createUuid().toString(QUuid::WithoutBraces); - // TODO remove me QObject::connect(qnam, &QNetworkAccessManager::sslErrors, qnam, [](QNetworkReply *reply, const QList &errors) { reply->ignoreSslErrors(); }); - registerServer(m_port, m_emails, m_serverId); + registerServer(m_port, m_emails, m_clientId); } void WebsocketClient::slotConnected() { qCInfo(WEBSOCKET_LOG) << "websocket connected"; QJsonDocument doc(QJsonObject{ { "command"_L1, "register"_L1 }, { "arguments"_L1, QJsonObject { { "emails"_L1, QJsonArray::fromStringList(m_emails) }, { "type"_L1, "nativeclient"_L1 } }} }); m_webSocket.sendTextMessage(QString::fromUtf8(doc.toJson())); - registerServer(m_port, m_emails, m_serverId); + registerServer(m_port, m_emails, m_clientId); Q_EMIT connected(); } void WebsocketClient::slotErrorOccurred(QAbstractSocket::SocketError error) { qCWarning(WEBSOCKET_LOG) << error << m_webSocket.errorString(); Q_EMIT closed(i18nc("@info", "Connection to outlook lost due to a connection error.")); reconnect(); } 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(); qCDebug(WEBSOCKET_LOG) << object; if (object["type"_L1] == "disconnection"_L1) { // disconnection of the web client Q_EMIT closed(i18nc("@info", "Connection to outlook lost. Make sure the extension tab is open.")); } else if (object["type"_L1] == "connection"_L1) { // reconnection of the web client Q_EMIT connected(); } else if (object["type"_L1] == "email-sent"_L1) { // confirmation that the email was sent const auto args = object["arguments"_L1].toObject(); Q_EMIT emailSentSuccessfully(args["id"_L1].toString()); } } void WebsocketClient::reconnect() { QTimer::singleShot(1000ms, this, [this]() { m_webSocket.open(m_url); }); } diff --git a/client/websocketclient.h b/client/websocketclient.h index 5ae0c35..caab6d7 100644 --- a/client/websocketclient.h +++ b/client/websocketclient.h @@ -1,41 +1,41 @@ // SPDX-FileCopyrightText: 2023 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include class WebsocketClient : public QObject { Q_OBJECT public: - static WebsocketClient &self(const QUrl &url = {}, int port = -1); + static WebsocketClient &self(const QUrl &url = {}, int port = -1, const QString &clientId = {}); Q_SIGNALS: /// Signal emited when the connection to the broker or outlook web is lost. void closed(const QString &errorString); /// Signal emited when the native client is connected or reconnected to the broker /// and outlook web. void connected(); /// Signal emited when an email was sent succesffully, void emailSentSuccessfully(const QString &id); private Q_SLOTS: void slotConnected(); void slotErrorOccurred(QAbstractSocket::SocketError error); void slotTextMessageReceived(QString message); private: - explicit WebsocketClient(const QUrl &url, int port); + explicit WebsocketClient(const QUrl &url, int port, const QString &clientId); void reconnect(); QWebSocket m_webSocket; QUrl m_url; const int m_port; QStringList m_emails; - QString m_serverId; + QString m_clientId; }; diff --git a/server/controllers/abstractcontroller.cpp b/server/controllers/abstractcontroller.cpp index 71265c7..c8ea38f 100644 --- a/server/controllers/abstractcontroller.cpp +++ b/server/controllers/abstractcontroller.cpp @@ -1,47 +1,53 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "abstractcontroller.h" #include "../model/serverstate.h" #include #include #include using namespace Qt::Literals::StringLiterals; QHttpServerResponse AbstractController::badRequest(const QString &reason) { if (reason.isEmpty()) { return QHttpServerResponse(QJsonObject { { "errorMessage"_L1, "Invalid request"_L1 } }, QHttpServerResponse::StatusCode::BadRequest); } else { return QHttpServerResponse(QJsonObject { { "errorMessage"_L1, QJsonValue("Invalid request: "_L1 + reason) } }, QHttpServerResponse::StatusCode::BadRequest); } } QHttpServerResponse AbstractController::forbidden() { return QHttpServerResponse(QJsonObject { { "errorMessage"_L1, "Unable to authentificate"_L1 } }, QHttpServerResponse::StatusCode::Forbidden); } -std::optional AbstractController::checkAuthentification(const QHttpServerRequest &request) +std::optional AbstractController::checkAuthentification(const QHttpServerRequest &request) { const auto &state = ServerState::instance(); const auto email = QString::fromUtf8(Utils::findHeader(request.headers(), "X-EMAIL")); - if (email.isEmpty() || !state.servers.contains(email)) { + if (email.isEmpty() || !state.clients.contains(email)) { qWarning() << "no email found" << email; return std::nullopt; } + auto client = state.clients[email]; - return state.servers[email]; + if (!client.trusted) { + qWarning() << "Client is not trusted" << email; + return std::nullopt; + } + + return state.clients[email]; } diff --git a/server/controllers/abstractcontroller.h b/server/controllers/abstractcontroller.h index 771490f..3169e41 100644 --- a/server/controllers/abstractcontroller.h +++ b/server/controllers/abstractcontroller.h @@ -1,20 +1,20 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include #include #include "../model/serverstate.h" class AbstractController { protected: Q_REQUIRED_RESULT static QHttpServerResponse badRequest(const QString &reason = {}); static QHttpServerResponse forbidden(); - static std::optional checkAuthentification(const QHttpServerRequest &request); + static std::optional checkAuthentification(const QHttpServerRequest &request); }; diff --git a/server/controllers/emailcontroller.cpp b/server/controllers/emailcontroller.cpp index 39e67e7..ae42ff0 100644 --- a/server/controllers/emailcontroller.cpp +++ b/server/controllers/emailcontroller.cpp @@ -1,163 +1,163 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "emailcontroller.h" #include #include #include #include #include #include #include #include #include #include "webserver.h" using namespace Qt::Literals::StringLiterals; QHttpServerResponse EmailController::abstractEmailAction(const QHttpServerRequest &request, const QString &action, QHttpServerRequest::Method method) { - const auto server = checkAuthentification(request); - if (!server) { + const auto client = checkAuthentification(request); + if (!client) { return forbidden(); } - QNetworkRequest viewEmailRequest(QUrl(u"http://127.0.0.1:"_s + QString::number(server->port) + u'/' + action)); + QNetworkRequest viewEmailRequest(QUrl(u"http://127.0.0.1:"_s + QString::number(client->port) + u'/' + action)); viewEmailRequest.setHeader(QNetworkRequest::ContentTypeHeader, u"application/json"_s); auto email = Utils::findHeader(request.headers(), "X-EMAIL"); auto displayName = Utils::findHeader(request.headers(), "X-NAME"); auto token = QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8(); viewEmailRequest.setRawHeader("X-EMAIL", email); viewEmailRequest.setRawHeader("X-TOKEN", token); viewEmailRequest.setRawHeader("X-NAME", displayName); auto &serverState = ServerState::instance(); serverState.composerRequest[token] = QString::fromUtf8(email); auto &state = ServerState::instance(); QNetworkReply *reply; if (method == QHttpServerRequest::Method::Post) { const auto body = request.body(); reply = state.qnam.post(viewEmailRequest, body); } else { reply = state.qnam.deleteResource(viewEmailRequest); } QObject::connect(reply, &QNetworkReply::finished, reply, [reply]() { if (reply->error() != QNetworkReply::NoError) { qWarning() << reply->error() << reply->errorString(); } else { qWarning() << "sent request to view message to server"; } }); return QHttpServerResponse(QJsonObject { {"status"_L1, "ok"_L1}, }); } QHttpServerResponse EmailController::viewEmailAction(const QHttpServerRequest &request) { if (request.method() != QHttpServerRequest::Method::Post) { return badRequest(u"Endpoint only supports POST request"_s); } return abstractEmailAction(request, u"view"_s); } QHttpServerResponse EmailController::newEmailAction(const QHttpServerRequest &request) { if (request.method() != QHttpServerRequest::Method::Post) { return badRequest(u"Endpoint only supports POST request"_s); } return abstractEmailAction(request, u"new"_s); } QHttpServerResponse EmailController::draftAction(QString, const QHttpServerRequest &request) { if (request.method() != QHttpServerRequest::Method::Post && request.method() != QHttpServerRequest::Method::Delete) { return badRequest(u"Endpoint only supports POST request"_s); } return abstractEmailAction(request, request.url().path(), request.method()); } QHttpServerResponse EmailController::replyEmailAction(const QHttpServerRequest &request) { if (request.method() != QHttpServerRequest::Method::Post) { return badRequest(u"Endpoint only supports POST request"_s); } return abstractEmailAction(request, u"reply"_s); } QHttpServerResponse EmailController::forwardEmailAction(const QHttpServerRequest &request) { if (request.method() != QHttpServerRequest::Method::Post) { return badRequest(u"Endpoint only supports POST request"_s); } return abstractEmailAction(request, u"forward"_s); } QHttpServerResponse checkStatus(int port, const QByteArray &body) { QNetworkRequest infoEmailRequest(QUrl(u"http://127.0.0.1:"_s + QString::number(port) + u"/info"_s)); infoEmailRequest.setHeader(QNetworkRequest::ContentTypeHeader, u"application/json"_s); auto &state = ServerState::instance(); QEventLoop eventLoop; auto reply = state.qnam.post(infoEmailRequest, body); QObject::connect(reply, &QNetworkReply::finished, &eventLoop, &QEventLoop::quit); eventLoop.exec(); QJsonParseError error; const auto resultBody = QJsonDocument::fromJson(reply->readAll(), &error); if (resultBody.isNull()) { return QHttpServerResponse(QHttpServerResponse::StatusCode::BadRequest); } if (!resultBody.isObject()) { return QHttpServerResponse(QHttpServerResponse::StatusCode::BadRequest); } return QHttpServerResponse{resultBody.object()}; } QHttpServerResponse EmailController::infoEmailAction(const QHttpServerRequest &request) { if (request.method() != QHttpServerRequest::Method::Post) { return badRequest(u"Endpoint only supports POST request"_s); } const auto server = checkAuthentification(request); if (!server) { return forbidden(); } return checkStatus(server->port, request.body()); } QHttpServerResponse EmailController::socketWebAction(const QHttpServerRequest &request) { const auto email = QString::fromUtf8(Utils::findHeader(request.headers(), "X-EMAIL")); const auto token = Utils::findHeader(request.headers(), "X-TOKEN"); const auto &serverState = ServerState::instance(); qDebug() << serverState.composerRequest << email << token; if (serverState.composerRequest[token] != email) { return forbidden(); } WebServer::self().sendMessageToWebClient(email, request.body()); return QHttpServerResponse(QJsonObject{ { "status"_L1, "OK"_L1 }, }); } diff --git a/server/controllers/registrationcontroller.cpp b/server/controllers/registrationcontroller.cpp index 2ee1cab..77f13f6 100644 --- a/server/controllers/registrationcontroller.cpp +++ b/server/controllers/registrationcontroller.cpp @@ -1,40 +1,73 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "registrationcontroller.h" #include #include #include #include +#include +#include #include "../model/serverstate.h" using namespace Qt::Literals::StringLiterals; QHttpServerResponse RegistrationController::registerAction(const QHttpServerRequest &request) { const auto json = QJsonDocument::fromJson(request.body()); if (json.isEmpty() || !json.isObject()) { return badRequest(); } const auto object = json.object(); if (object.isEmpty() || !object.contains("port"_L1) || !object.contains("emails"_L1)) { return badRequest(); } const auto emails = object["emails"_L1].toArray(); + const Client::Id clientId = object["clientId"_L1].toString(); + const auto port = object["port"_L1].toInt(); for (const auto email : emails) { - ServerState::instance().servers[email.toString().toLower()] = Server { - object["serverId"_L1].toString(), - object["port"_L1].toInt(), + ServerState::instance().clients[email.toString().toLower()] = Client { + clientId, + port, + false, }; - qDebug() << "Registration of email" << email.toString() << "on port" << object["port"_L1].toInt(); + qDebug() << "Registration of email" << email.toString() << "on port" << object["port"_L1].toInt() << "with clientId" << clientId; } + const QString token = QUuid::createUuid().toString(QUuid::WithoutBraces); + QNetworkRequest confirmationRequest(QUrl(u"http://127.0.0.1:"_s + QString::number(port) + u"/confirm-registration")); + + auto reply = ServerState::instance().qnam.post(confirmationRequest, QJsonDocument(QJsonObject{ + { "token"_L1, token }, + }).toJson()); + + qWarning() << QJsonDocument(QJsonObject{ + { "token"_L1, token }, + }); + + QObject::connect(reply, &QNetworkReply::finished, reply, [token, reply, clientId]() { + const auto hash = QCryptographicHash::hash((token + clientId).toUtf8(), QCryptographicHash::Sha256).toHex(); + + const auto json = QJsonDocument::fromJson(reply->readAll()); + if (json.isNull() || !json.isObject()) { + qWarning() << "Invalid json"; + return; + } + + const auto obj = json.object(); + if (hash == obj["hash"_L1].toString().toUtf8()) { + ServerState::instance().trustClientWithId(clientId); + } else { + qWarning() << "Not trusting" << clientId << hash << obj["hash"_L1].toString().toUtf8(); + } + }); + return QHttpServerResponse("text/plain", "Server added\n"); } diff --git a/server/model/serverstate.cpp b/server/model/serverstate.cpp index f3601fa..b491fb9 100644 --- a/server/model/serverstate.cpp +++ b/server/model/serverstate.cpp @@ -1,14 +1,29 @@ // SPDX-FileCopyrightText: 2023 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "serverstate.h" ServerState::ServerState() {} ServerState &ServerState::instance() { static ServerState s; return s; } + +void ServerState::trustClientWithId(const Client::Id &id) +{ + // iterate over const copy as we mutate the list while iterating it + const auto c = clients; + for (const auto &[email, client] : c.asKeyValueRange()) { + if (client.id == id) { + auto newClient = client; + newClient.trusted = true; + clients[email] = newClient; + } + } + + qWarning() << "trusting client with id" << id; +} diff --git a/server/model/serverstate.h b/server/model/serverstate.h index 241a042..f23f8be 100644 --- a/server/model/serverstate.h +++ b/server/model/serverstate.h @@ -1,35 +1,38 @@ // SPDX-FileCopyrightText: 2023 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include #include #include using Token = QByteArray; using ClientId = QString; using Email = QString; using Port = int; -struct Server { +struct Client { using Id = QString; Id id; Port port; + bool trusted; }; class ServerState { public: static ServerState &instance(); - QHash servers; + QHash clients; + + void trustClientWithId(const Client::Id &id); QNetworkAccessManager qnam; QHash composerRequest; private: ServerState(); }; diff --git a/server/webserver.cpp b/server/webserver.cpp index 2a7d5a7..7a7ef1c 100644 --- a/server/webserver.cpp +++ b/server/webserver.cpp @@ -1,300 +1,300 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "webserver.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "controllers/registrationcontroller.h" #include "controllers/staticcontroller.h" #include "controllers/emailcontroller.h" using namespace Qt::Literals::StringLiterals; WebServer WebServer::s_instance = WebServer(); WebServer &WebServer::self() { return s_instance; } WebServer::WebServer() : QObject(nullptr) , m_httpServer(new QHttpServer(this)) , m_webSocketServer(new QWebSocketServer(u"GPGOL"_s, QWebSocketServer::SslMode::SecureMode, this)) { } WebServer::~WebServer() = default; bool WebServer::run() { auto keyPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("certificate-key.pem")); auto certPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("certificate.pem")); Q_ASSERT(!keyPath.isEmpty()); Q_ASSERT(!certPath.isEmpty()); QFile privateKeyFile(keyPath); if (!privateKeyFile.open(QIODevice::ReadOnly)) { qWarning() << u"Couldn't open file for reading: %1"_s.arg(privateKeyFile.errorString()); return false; } const QSslKey sslKey(&privateKeyFile, QSsl::Rsa); privateKeyFile.close(); const auto sslCertificateChain = QSslCertificate::fromPath(certPath); if (sslCertificateChain.isEmpty()) { qWarning() << u"Couldn't retrieve SSL certificate from file."_s; return false; } // Static assets controller m_httpServer->route(u"/home"_s, &StaticController::homeAction); m_httpServer->route(u"/assets/"_s, &StaticController::assetsAction); // Registration controller m_httpServer->route(u"/register"_s, &RegistrationController::registerAction); // Email controller m_httpServer->route(u"/view"_s, &EmailController::viewEmailAction); m_httpServer->route(u"/info"_s, &EmailController::infoEmailAction); m_httpServer->route(u"/reply"_s, &EmailController::replyEmailAction); m_httpServer->route(u"/forward"_s, &EmailController::forwardEmailAction); m_httpServer->route(u"/new"_s, &EmailController::newEmailAction); m_httpServer->route(u"/socket-web"_s, &EmailController::socketWebAction); m_httpServer->route(u"/draft/"_s, &EmailController::draftAction); m_httpServer->sslSetup(sslCertificateChain.front(), sslKey); const auto port = m_httpServer->listen(QHostAddress::Any, WebServer::Port); if (!port) { qWarning() << "Server failed to listen on a port."; return false; } qWarning() << u"Running http server on https://127.0.0.1:%1/ (Press CTRL+C to quit)"_s.arg(port); QSslConfiguration sslConfiguration; sslConfiguration.setPeerVerifyMode(QSslSocket::VerifyNone); sslConfiguration.setLocalCertificate(sslCertificateChain.front()); sslConfiguration.setPrivateKey(sslKey); m_webSocketServer->setSslConfiguration(sslConfiguration); if (m_webSocketServer->listen(QHostAddress::Any, WebServer::WebSocketPort)) { qWarning() << u"Running websocket server on wss://127.0.0.1:%1/ (Press CTRL+C to quit)"_s.arg(WebServer::Port + 1); connect(m_webSocketServer, &QWebSocketServer::newConnection, this, &WebServer::onNewConnection); } return true; } void WebServer::onNewConnection() { auto pSocket = m_webSocketServer->nextPendingConnection(); if (!pSocket) { return; } - qDebug() << "Client connected:" << pSocket->peerName() << pSocket->origin(); + qDebug() << "Client connected:" << pSocket->peerName() << pSocket->origin() << pSocket->localAddress() << pSocket->localPort(); connect(pSocket, &QWebSocket::textMessageReceived, this, &WebServer::processTextMessage); connect(pSocket, &QWebSocket::binaryMessageReceived, this, &WebServer::processBinaryMessage); connect(pSocket, &QWebSocket::disconnected, this, &WebServer::socketDisconnected); m_clients << pSocket; } void WebServer::processTextMessage(QString message) { auto webClient = qobject_cast(sender()); if (webClient) { QJsonParseError error; const auto doc = QJsonDocument::fromJson(message.toUtf8(), &error); if (error.error != QJsonParseError::NoError) { qWarning() << "Error parsing json" << error.errorString(); return; } if (!doc.isObject()) { qWarning() << "Invalid json received"; return; } const auto object = doc.object(); if (!object.contains("command"_L1) || !object["command"_L1].isString() || !object.contains("arguments"_L1) || !object["arguments"_L1].isObject()) { qWarning() << "Invalid json received: no command or arguments set" ; return; } static QHash commandMapping { { "register"_L1, WebServer::Command::Register }, { "email-sent"_L1, WebServer::Command::EmailSent }, }; const auto command = commandMapping[doc["command"_L1].toString()]; processCommand(command, object["arguments"_L1].toObject(), webClient); } } void WebServer::processCommand(Command command, const QJsonObject &arguments, QWebSocket *socket) { switch (command) { case Command::Register: { const auto type = arguments["type"_L1].toString(); qDebug() << "Register" << arguments; if (type.isEmpty()) { qWarning() << "empty client type given when registering"; return; } const auto emails = arguments["emails"_L1].toArray(); if (type == "webclient"_L1) { if (emails.isEmpty()) { qWarning() << "empty email given"; } for (const auto &email : emails) { m_webClientsMappingToEmail[email.toString()] = socket; qWarning() << "email" << email.toString() << "mapped to a web client"; const auto nativeClient = m_nativeClientsMappingToEmail[email.toString()]; if (nativeClient) { QJsonDocument doc(QJsonObject{ { "type"_L1, "connection"_L1 }, { "payload"_L1, QJsonObject{ { "client_type"_L1, "web_client"_L1 } }} }); nativeClient->sendTextMessage(QString::fromUtf8(doc.toJson())); } } } else { if (emails.isEmpty()) { qWarning() << "empty email given"; } for (const auto &email : emails) { m_nativeClientsMappingToEmail[email.toString()] = socket; qWarning() << "email" << email.toString() << "mapped to a native client"; const auto webClient = m_webClientsMappingToEmail[email.toString()]; if (webClient) { QJsonDocument doc(QJsonObject{ { "type"_L1, "connection"_L1 }, { "payload"_L1, QJsonObject{ { "client_type"_L1, "native_client"_L1 } }} }); webClient->sendTextMessage(QString::fromUtf8(doc.toJson())); } } } return; } case Command::EmailSent: { const auto email = arguments["email"_L1].toString(); const auto socket = m_nativeClientsMappingToEmail[email]; if (!socket) { return; } QJsonDocument doc(QJsonObject{ { "type"_L1, "email-sent"_L1 }, { "arguments"_L1, arguments }, }); socket->sendTextMessage(QString::fromUtf8(doc.toJson())); return; } case Command::Undefined: qWarning() << "Invalid json received: invalid command" ; return; } } bool WebServer::sendMessageToWebClient(const QString &email, const QByteArray &payload) { auto socket = m_webClientsMappingToEmail[email]; if (!socket) { return false; } socket->sendTextMessage(QString::fromUtf8(payload)); return true; } void WebServer::processBinaryMessage(QByteArray message) { qWarning() << "got binary message" << message; QWebSocket *pClient = qobject_cast(sender()); if (pClient) { pClient->sendBinaryMessage(message); } } void WebServer::socketDisconnected() { QWebSocket *pClient = qobject_cast(sender()); if (pClient) { qDebug() << "Client disconnected" << pClient; // Web client was disconnected { const auto it = std::find_if(m_webClientsMappingToEmail.cbegin(), m_webClientsMappingToEmail.cend(), [pClient](QWebSocket *webSocket) { return pClient == webSocket; }); if (it != m_webClientsMappingToEmail.cend()) { const auto email = it.key(); const auto nativeClient = m_nativeClientsMappingToEmail[email]; qDebug() << "webclient with email disconnected" << email << nativeClient; if (nativeClient) { QJsonDocument doc(QJsonObject{ { "type"_L1, "disconnection"_L1 }, }); nativeClient->sendTextMessage(QString::fromUtf8(doc.toJson())); } m_webClientsMappingToEmail.removeIf([pClient](auto socket) { return pClient == socket.value(); }); } } // Native client was disconnected const auto emails = m_nativeClientsMappingToEmail.keys(); for (const auto &email : emails) { const auto webSocket = m_nativeClientsMappingToEmail[email]; if (webSocket != pClient) { qDebug() << "webSocket not equal" << email << webSocket << pClient; continue; } qDebug() << "native client for" << email << "was disconnected."; QJsonDocument doc(QJsonObject{ { "type"_L1, "disconnection"_L1 }, }); sendMessageToWebClient(email, doc.toJson()); } m_nativeClientsMappingToEmail.removeIf([pClient](auto socket) { return pClient == socket.value(); }); m_clients.removeAll(pClient); } }