Page MenuHome GnuPG

No OneTemporary

diff --git a/server/webserver.cpp b/server/webserver.cpp
index 3bc7499..46ff474 100644
--- a/server/webserver.cpp
+++ b/server/webserver.cpp
@@ -1,453 +1,453 @@
// SPDX-FileCopyrightText: 2023 g10 code GmbH
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "webserver.h"
#include <QDebug>
#include <QFile>
#include <QHttpServer>
#include <QHttpServerResponse>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSslCertificate>
#include <QSslKey>
#include <QSslServer>
#include <QStandardPaths>
#include <QWebSocket>
#include <QWebSocketCorsAuthenticator>
#include <QWebSocketServer>
#include <protocol.h>
#include "controllers/staticcontroller.h"
#include "http_debug.h"
#include "websocket_debug.h"
#include <KLocalizedString>
using namespace Qt::Literals::StringLiterals;
using namespace Protocol;
WebServer::WebServer(QObject *parent)
: QObject(parent)
, m_httpServer(new QHttpServer(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"));
auto rootCertPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("root-ca.pem"));
Q_ASSERT(!keyPath.isEmpty());
Q_ASSERT(!certPath.isEmpty());
QFile privateKeyFile(keyPath);
if (!privateKeyFile.open(QIODevice::ReadOnly)) {
qCFatal(HTTP_LOG) << "Couldn't open file" << keyPath << "for reading:" << privateKeyFile.errorString();
return false;
}
const QSslKey sslKey(&privateKeyFile, QSsl::Rsa);
privateKeyFile.close();
auto sslCertificateChain = QSslCertificate::fromPath(certPath);
if (sslCertificateChain.isEmpty()) {
qCFatal(HTTP_LOG) << u"Couldn't retrieve SSL certificate from file:"_s << certPath;
return false;
}
sslCertificateChain.append(QSslCertificate::fromPath(rootCertPath));
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
m_httpServer->addWebSocketUpgradeVerifier(this, [](const QHttpServerRequest &request) {
Q_UNUSED(request);
if (request.url().path() == "/websocket"_L1) {
return QHttpServerWebSocketUpgradeResponse::accept();
} else {
return QHttpServerWebSocketUpgradeResponse::passToNext();
}
});
#else
// HAKC: ensure we handle the request so that in QHttpServerStream::handleReadyRead
// the request is updated to a websocket while still sending nothing so that we don't
// break the websocket clients
m_httpServer->route(u"/websocket"_s, [](const QHttpServerRequest &request, QHttpServerResponder &&responder) {
Q_UNUSED(request);
Q_UNUSED(responder);
});
#endif
// Static assets controller
m_httpServer->route(u"/home"_s, &StaticController::homeAction);
m_httpServer->route(u"/assets/"_s, &StaticController::assetsAction);
m_httpServer->route(u"/test"_s, &StaticController::testAction);
QSslConfiguration sslConfiguration;
sslConfiguration.setPeerVerifyMode(QSslSocket::VerifyNone);
sslConfiguration.setLocalCertificateChain(sslCertificateChain);
sslConfiguration.setPrivateKey(sslKey);
m_tcpserver = std::make_unique<QSslServer>();
m_tcpserver->setSslConfiguration(sslConfiguration);
if (!m_tcpserver->listen(QHostAddress::LocalHost, WebServer::Port)) {
qCFatal(HTTP_LOG) << "Server failed to listen on a port.";
return false;
}
// Note: Later versions of QHttpServer::bind returns a bool
// to check succes state. Though the only ways to return false
// is if tcpserver is nullpointer or if the tcpserver isn't listening
m_httpServer->bind(m_tcpserver.get());
quint16 port = m_tcpserver->serverPort();
if (!port) {
qCFatal(HTTP_LOG) << "Server failed to listen on a port.";
return false;
}
qInfo(HTTP_LOG) << u"Running http server on https://127.0.0.1:%1/ (Press CTRL+C to quit)"_s.arg(port);
connect(m_httpServer, &QHttpServer::newWebSocketConnection, this, &WebServer::onNewConnection);
#if defined(Q_OS_WINDOWS) || QT_VERSION >= QT_VERSION_CHECK(6, 11, 0)
connect(m_httpServer, &QHttpServer::webSocketOriginAuthenticationRequired, this, [](QWebSocketCorsAuthenticator *authenticator) {
const auto origin = authenticator->origin();
// Only allow the gpgol-client and localhost:5656 to connect to this host
// Otherwise any tab from the browser is able to access the websockets
authenticator->setAllowed(origin == u"Client"_s || origin == u"https://localhost:5656"_s);
});
#endif
return true;
}
void WebServer::onNewConnection()
{
auto pSocket = m_httpServer->nextPendingWebSocketConnection();
if (!pSocket) {
return;
}
qCInfo(WEBSOCKET_LOG) << "Client connected:" << pSocket->peerName() << pSocket->origin() << pSocket->localAddress() << pSocket->localPort();
connect(pSocket.get(), &QWebSocket::textMessageReceived, this, &WebServer::processTextMessage);
connect(pSocket.get(), &QWebSocket::binaryMessageReceived, this, &WebServer::processBinaryMessage);
connect(pSocket.get(), &QWebSocket::disconnected, this, &WebServer::socketDisconnected);
- m_clients.push_back(pSocket.release());
+ // such that the socket will be deleted in the d'tor (unless socketDisconnected is called, earlier)
+ m_clients.add(pSocket.release());
}
void WebServer::processTextMessage(QString message)
{
auto webClient = qobject_cast<QWebSocket *>(sender());
if (webClient) {
QJsonParseError error;
const auto doc = QJsonDocument::fromJson(message.toUtf8(), &error);
if (error.error != QJsonParseError::NoError) {
qCWarning(WEBSOCKET_LOG) << "Error parsing json" << error.errorString();
return;
}
if (!doc.isObject()) {
qCWarning(WEBSOCKET_LOG) << "Invalid json received";
return;
}
const auto object = doc.object();
processCommand(object, webClient);
}
}
void WebServer::processCommand(const QJsonObject &object, QWebSocket *socket)
{
if (!object.contains("command"_L1) || !object["command"_L1].isString() || !object.contains("arguments"_L1) || !object["arguments"_L1].isObject()) {
qCWarning(WEBSOCKET_LOG) << "Invalid json received: no type or arguments set" << object;
return;
}
const auto arguments = object["arguments"_L1].toObject();
const auto command = commandFromString(object["command"_L1].toString());
switch (command) {
case Command::Register: {
const auto type = arguments["type"_L1].toString();
qCWarning(WEBSOCKET_LOG) << "Register" << arguments;
if (type.isEmpty()) {
qCWarning(WEBSOCKET_LOG) << "Empty client type given when registering";
return;
}
const auto emails = arguments["emails"_L1].toArray();
if (type == "webclient"_L1) {
if (emails.isEmpty()) {
qCWarning(WEBSOCKET_LOG) << "Empty email given";
}
for (const auto &value : emails) {
const auto email = value.toString();
m_webClientsMappingToEmail[email] = socket;
qCWarning(WEBSOCKET_LOG) << "Email" << email << "mapped to a web client";
// clang-format off
const QJsonObject command{
{"command"_L1, Protocol::commandToString(Protocol::Connection)},
{"arguments"_L1, QJsonObject{
{"type"_L1, "web"_L1},
{"verified"_L1, "web"_L1},
}},
};
// clang-format on
sendMessageToNativeClient(email, command, false);
auto nativeClient = m_nativeClientsMappingToEmail[email];
if (!nativeClient.isValid()) {
qCWarning(WEBSOCKET_LOG) << "Native client for email not found" << email;
continue;
}
// clang-format off
const QJsonDocument docCon(QJsonObject{ //
{"command"_L1, Protocol::commandToString(Protocol::Connection)},
{"arguments"_L1, QJsonObject{
{"type"_L1, "native"_L1},
{"id"_L1, nativeClient.id},
{"name"_L1, nativeClient.name},
},
}});
// clang-format on
sendMessageToWebClient(email, docCon.toJson());
}
} else {
if (emails.isEmpty()) {
qCWarning(WEBSOCKET_LOG) << "Empty email given";
}
NativeClient nativeClient{
socket,
arguments["name"_L1].toString(),
arguments["id"_L1].toString(),
};
if (!nativeClient.isValid()) {
qCWarning(WEBSOCKET_LOG) << "Invalid native client trying to register";
}
for (const auto &value : emails) {
const auto email = value.toString();
m_nativeClientsMappingToEmail[email] = nativeClient;
qCWarning(WEBSOCKET_LOG) << "Email" << email << "mapped to a native client";
// clang-format off
const QJsonDocument doc(QJsonObject{ //
{"command"_L1, Protocol::commandToString(Protocol::Connection)},
{"arguments"_L1, QJsonObject{
{"type"_L1, "native"_L1},
{"id"_L1, nativeClient.id},
{"name"_L1, nativeClient.name},
},
}});
// clang-format on
const auto json = doc.toJson();
sendMessageToWebClient(email, json);
}
}
return;
}
case Command::EwsResponse: {
const auto email = arguments["email"_L1].toString();
QJsonObject object{
{"command"_L1, Protocol::commandToString(Protocol::EwsResponse)},
{"arguments"_L1, arguments},
};
sendMessageToNativeClient(email, object);
return;
}
case Command::View:
case Command::Reply:
case Command::Forward:
case Command::Composer:
case Command::OpenDraft:
case Command::RestoreAutosave:
case Command::Info:
case Command::Reencrypt:
case Command::DeleteDraft: {
const auto email = arguments["email"_L1].toString();
const auto ok = sendMessageToNativeClient(email, object);
if (!ok) {
const QJsonDocument doc(QJsonObject{
{"command"_L1, Protocol::commandToString(Protocol::Error)},
{"arguments"_L1,
QJsonObject{
{"error"_L1, i18n("Unable to find corresponding native client. Ensure GpgOL/Web is started and you have a key pair for \"%1\".", email)},
}},
});
sendMessageToWebClient(email, doc.toJson());
}
return;
}
case Command::InfoFetched: {
const auto email = arguments["email"_L1].toString();
QJsonDocument doc(QJsonObject{
{"command"_L1, Protocol::commandToString(Protocol::InfoFetched)},
{"arguments"_L1, arguments},
});
const auto ok = sendMessageToWebClient(email, doc.toJson());
if (!ok) {
qCWarning(WEBSOCKET_LOG) << "Unable to send info fetched to web client";
}
return;
}
case Command::Error: {
const auto email = arguments["email"_L1].toString();
QJsonDocument doc(QJsonObject{
{"command"_L1, Protocol::commandToString(Protocol::Error)},
{"arguments"_L1, arguments},
});
const auto ok = sendMessageToWebClient(email, doc.toJson());
if (!ok) {
qCWarning(WEBSOCKET_LOG) << "Unable to send info fetched to web client";
}
return;
}
case Command::Ews: {
const auto email = arguments["email"_L1].toString();
QJsonDocument doc(QJsonObject{
{"command"_L1, Protocol::commandToString(Protocol::Ews)},
{"arguments"_L1, arguments},
});
const auto ok = sendMessageToWebClient(email, doc.toJson());
if (!ok) {
qCWarning(WEBSOCKET_LOG) << "Unable to send ews request to web client";
}
return;
}
case Command::ViewerClosed:
case Command::ViewerOpened: {
const auto email = arguments["email"_L1].toString();
QJsonDocument doc(QJsonObject{
{"command"_L1, Protocol::commandToString(command)},
{"arguments"_L1, arguments},
});
const auto ok = sendMessageToWebClient(email, doc.toJson());
if (!ok) {
qCWarning(WEBSOCKET_LOG) << "Unable to send viewer close/open request to web client";
}
return;
}
case Command::Log:
qCWarning(WEBSOCKET_LOG) << arguments["message"_L1].toString() << arguments["args"_L1].toString();
return;
default:
qCWarning(WEBSOCKET_LOG) << "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;
}
bool WebServer::sendMessageToNativeClient(const QString &email, const QJsonObject &obj, bool verify)
{
auto device = m_nativeClientsMappingToEmail[email];
if (!device.isValid()) {
const auto sockets = m_nativeClientsMappingToEmail.values();
const QSet<NativeClient> uniqueClients(sockets.cbegin(), sockets.cend());
if (uniqueClients.size() != 1) {
return false;
}
device = *uniqueClients.cbegin();
}
if (verify) {
const auto verifiedNativeClients = obj["verifiedNativeClients"_L1].toArray();
bool isInVerified = false;
for (const auto value : verifiedNativeClients) {
const auto verifiedNativeClient = value.toString();
if (verifiedNativeClient == device.id) {
isInVerified = true;
}
}
if (isInVerified) {
return false;
}
}
const QJsonDocument doc(obj);
device.socket->sendTextMessage(QString::fromUtf8(doc.toJson()));
return true;
}
void WebServer::processBinaryMessage(QByteArray message)
{
QWebSocket *pClient = qobject_cast<QWebSocket *>(sender());
if (pClient) {
pClient->sendBinaryMessage(message);
}
}
void WebServer::socketDisconnected()
{
auto pClient = qobject_cast<QWebSocket *>(sender());
if (!pClient) {
return;
}
qCWarning(WEBSOCKET_LOG) << "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 device = m_nativeClientsMappingToEmail[email];
qCInfo(WEBSOCKET_LOG) << "Web client for" << email << "was disconnected.";
if (device.isValid()) {
QJsonDocument doc(QJsonObject{
{"command"_L1, Protocol::commandToString(Protocol::Disconnection)},
});
device.socket->sendTextMessage(QString::fromUtf8(doc.toJson()));
}
m_webClientsMappingToEmail.removeIf([pClient](auto device) {
return pClient == device.value();
});
}
}
// Native client was disconnected
const auto emails = m_nativeClientsMappingToEmail.keys();
for (const auto &email : emails) {
const auto device = m_nativeClientsMappingToEmail[email];
if (device.socket != pClient) {
continue;
}
qCInfo(WEBSOCKET_LOG) << "Native client for" << email << "was disconnected.";
QJsonDocument doc(QJsonObject{
{"command"_L1, Protocol::commandToString(Protocol::Disconnection)},
});
sendMessageToWebClient(email, doc.toJson());
}
m_nativeClientsMappingToEmail.removeIf([pClient](auto device) {
return pClient == device.value().socket;
});
- m_clients.removeAll(pClient);
pClient->deleteLater();
}
diff --git a/server/webserver.h b/server/webserver.h
index 60a2f0a..c03589e 100644
--- a/server/webserver.h
+++ b/server/webserver.h
@@ -1,74 +1,75 @@
// 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 <QByteArray>
#include <QList>
#include <QObject>
+#include <QObjectCleanupHandler>
#include <QSslError>
QT_FORWARD_DECLARE_CLASS(QWebSocketServer)
class QHttpServer;
class QSslServer;
class QWebSocket;
struct NativeClient {
QWebSocket *socket;
QString name;
QString id;
bool isValid() const
{
return socket != nullptr && !name.isEmpty() && !id.isEmpty();
}
bool operator==(const NativeClient &c) const
{
return socket == c.socket;
}
};
class WebServer : public QObject
{
Q_OBJECT
public:
explicit WebServer(QObject *parent = nullptr);
~WebServer();
/// Start web server.
bool run();
/// is a valid WebServer instance
bool isValid() const;
bool sendMessageToWebClient(const QString &email, const QByteArray &payload);
bool sendMessageToNativeClient(const QString &email, const QJsonObject &obj, bool verify = true);
private Q_SLOTS:
void onNewConnection();
void processTextMessage(QString message);
void processBinaryMessage(QByteArray message);
void socketDisconnected();
private:
enum SpecialValues {
Port = 5656,
};
void processCommand(const QJsonObject &object, QWebSocket *socket);
QHttpServer *const m_httpServer;
std::unique_ptr<QSslServer> m_tcpserver;
- QList<QWebSocket*> m_clients;
+ QObjectCleanupHandler m_clients;
QHash<QString, QWebSocket *> m_webClientsMappingToEmail;
QHash<QString, NativeClient> m_nativeClientsMappingToEmail;
};
inline size_t qHash(const NativeClient &client, size_t seed) noexcept
{
return qHash(client.socket, seed);
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Dec 20, 1:48 PM (21 h, 27 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
d2/c2/71ded39ef32d0bd02da4e9c117ca

Event Timeline