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