Page Menu
Home
GnuPG
Search
Configure Global Search
Log In
Files
F34219862
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Size
18 KB
Subscribers
None
View Options
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
Details
Attached
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
Attached To
rOJ GpgOL.js
Event Timeline
Log In to Comment