Page MenuHome GnuPG

No OneTemporary

diff --git a/server/webserver.cpp b/server/webserver.cpp
index 2e582b4..c73069f 100644
--- a/server/webserver.cpp
+++ b/server/webserver.cpp
@@ -1,461 +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 "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);
// 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.value(email);
- if (!nativeClient.isValid()) {
+ const auto it = m_nativeClientsMappingToEmail.constFind(email);
+ if (it == m_nativeClientsMappingToEmail.cend()) {
qCWarning(WEBSOCKET_LOG) << "Native client for email not found" << email;
continue;
}
+ const auto nativeClient = it.value();
// 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";
return;
}
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.value(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.value(email);
- if (!device.isValid()) {
+ NativeClient device;
+ auto it = m_nativeClientsMappingToEmail.constFind(email);
+ if (it == m_nativeClientsMappingToEmail.cend()) {
// If no native client has this email *but* there is only a single (i.e. local) native
// client, use that, instead.
const auto sockets = m_nativeClientsMappingToEmail.values();
const QSet<NativeClient> uniqueClients(sockets.cbegin(), sockets.cend());
if (uniqueClients.size() != 1) {
return false;
}
device = *uniqueClients.cbegin();
if (!device.isValid()) {
// Invalid client should never get into the mapping, but let's be defensive
qCWarning(WEBSOCKET_LOG) << "Invalid native client in mapping";
return false;
}
+ } else {
+ device = it.value();
}
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.value(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()));
- }
+ QJsonObject object({
+ {"command"_L1, Protocol::commandToString(Protocol::Disconnection)},
+ });
+ sendMessageToNativeClient(email, object);
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.value(email);
- if (device.socket != pClient) {
+ const auto native = m_nativeClientsMappingToEmail.constFind(email);
+ if (native == m_nativeClientsMappingToEmail.cend() || native.value().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;
});
pClient->deleteLater();
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Apr 14, 9:30 PM (1 d, 12 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
68/e0/0106c8c8b82a8fc4dbe916550cf0

Event Timeline