Page MenuHome GnuPG

No OneTemporary

diff --git a/server/webserver.cpp b/server/webserver.cpp
index 971f610..b83a6ee 100644
--- a/server/webserver.cpp
+++ b/server/webserver.cpp
@@ -1,457 +1,461 @@
// 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 <gpgme++/context.h>
#include <gpgme++/randomresults.h>
#include "controllers/staticcontroller.h"
#include "http_debug.h"
#include "websocket_debug.h"
#include <KLocalizedString>
#define PAIRING_TOKEN_LENGTH 30
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(bool singleUser, const QString &clientSecretHash)
{
m_singleUser = singleUser;
m_clientSecretHash = clientSecretHash;
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;
}
#if !defined(Q_OS_WINDOWS)
// bug in Qt 6.10.1, Windows only: SSL handshake fails, if we offer the full certificate chain, here.
// TODO: track this down in Qt. A mixup, which cert is which?
// For now just omit the root cert. It is installed in the system trust store, anyway.
sslCertificateChain.append(QSslCertificate::fromPath(rootCertPath));
#endif
#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();
if (m_singleUser && !pSocket->peerAddress().isEqual(QHostAddress::LocalHost, QHostAddress::ConvertLocalHost)) {
qCWarning(WEBSOCKET_LOG) << "Refused connection attempt from remote host" << pSocket->peerAddress();
pSocket->close();
return;
}
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);
}
}
// prefix ids with remote IP, so web client and native client are always on same IP
static QString qualifiedId(const QString &id, const QWebSocket *socket)
{
return socket->peerAddress().toString() + u'/' + id;
}
static QString nonQualifiedId(const QString &id)
{
return id.section(u'/', 1);
}
static QString fromNativeId(const QString &id)
{
//qCWarning(WEBSOCKET_LOG) << id << QCryptographicHash::hash(id.toUtf8().data(), QCryptographicHash::Sha256);
return QString::fromLatin1(QCryptographicHash::hash(id.toUtf8().data(), QCryptographicHash::Sha256).toHex());
}
static QJsonObject errorCommand(const QString &message)
{
return Protocol::makeCommand(Protocol::Error, QJsonObject{{"error"_L1, message}}, QString());
}
void WebServer::tryPairWebclient(const QString &token, QWebSocket *socket)
{
QString id;
- if (!token.isEmpty()) {
+ if (m_singleUser) {
+ if (m_nativeClientsMappingToId.size() == 1) {
+ id = m_nativeClientsMappingToId.cbegin().key();
+ }
+ } else if (!token.isEmpty()) {
if (token.length() != PAIRING_TOKEN_LENGTH) {
// If users type the token, instead of pasting, we'll be sent partial tokens. Ignore those.
return;
}
const auto it = std::find_if(m_nativeClientsMappingToId.cbegin(), m_nativeClientsMappingToId.cend(), [token, socket](NativeClient client) {
return client.isPairing() && (client.socket->peerAddress().isEqual(socket->peerAddress(), QHostAddress::ConvertLocalHost)) && (token == client.pairingToken);
});
if (it == m_nativeClientsMappingToId.cend()) {
qCWarning(WEBSOCKET_LOG) << "Invalid pairing code supplied" << token;
const auto json = QJsonDocument(errorCommand(i18n("Invalid pairing code.")));
socket->sendTextMessage(QString::fromUtf8(json.toJson()));
} else {
id = it.key();
}
}
if (id.isEmpty()) {
return;
}
const auto rawid = nonQualifiedId(id);
m_webClientsMappingToId[id] = socket;
const auto command = Protocol::makeCommand(Protocol::Connection, QJsonObject(), rawid);
sendMessageToWebClient(id, command);
// also pass on request to native client, in order to signal successful pairing
sendMessageToNativeClient(id, Protocol::makeCommand(Protocol::PairingRequest, QJsonObject(), rawid));
qCInfo(WEBSOCKET_LOG) << "Pairing success on connection" << id;
}
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) {
// requests originating in proxy:
// case Command::Connection:
// requests originating in both native client and web client:
case Command::Register: {
const auto type = arguments["type"_L1].toString();
qInfo(WEBSOCKET_LOG) << "Register" << arguments;
if (type.isEmpty()) {
qCWarning(WEBSOCKET_LOG) << "Empty client type given when registering";
return;
}
if (type == "webclient"_L1) {
const auto email = arguments["email"_L1].toString();
if (email.isEmpty()) {
qCWarning(WEBSOCKET_LOG) << "Empty email given";
}
const auto rawid = object["id"_L1].toString();
if (rawid.isEmpty()) {
qWarning(WEBSOCKET_LOG) << "Unpaired web client registered. Waiting for pairing.";
tryPairWebclient(QString(), socket);
return;
}
const auto id = qualifiedId(rawid, socket);
qCWarning(WEBSOCKET_LOG) << "Web client registered for connection id" << id << "and email" << email;
m_webClientsMappingToId[id] = socket; // TODO: Allow multiple webclients per native client
const auto command = Protocol::makeCommand(Protocol::Connection, QJsonObject{{"email"_L1, email}}, rawid);
sendMessageToNativeClient(id, command);
// NOTE: the native client will respond with a status update to the web client (if already connected)
} else {
const auto rawid = object["id"_L1].toString();
if (rawid.isEmpty()) {
qCWarning(WEBSOCKET_LOG) << "Attempt to register native client with empty id";
socket->close();
return;
}
if (m_singleUser) {
const auto clientSecret = arguments["secret"_L1].toString();
if (fromNativeId(clientSecret) != m_clientSecretHash) {
qCWarning(WEBSOCKET_LOG) << "Attempt to register native client with invalid secret";
socket->close();
return;
}
}
const auto id = qualifiedId(fromNativeId(rawid), socket);
NativeClient nativeClient{
socket,
arguments["name"_L1].toString(),
id,
QString()
};
m_nativeClientsMappingToId[id] = nativeClient;
qCWarning(WEBSOCKET_LOG) << "Native client registered for connection id" << id;
const auto command = Protocol::makeCommand(Protocol::Connection, QJsonObject{std::pair{"emails"_L1, arguments["emails"_L1]}}, rawid);
sendMessageToWebClient(id, command);
// NOTE: native client will follow up with a status update to any connected web clients
}
return;
}
case Command::PairingRequest: {
const auto type = arguments["type"_L1].toString();
if (type == "web"_L1) { // originates from web client
tryPairWebclient(arguments["token"_L1].toString(), socket);
} else { // originates from native client
const auto rawid = fromNativeId(object["id"_L1].toString());
const auto id = qualifiedId(rawid, socket);
if (m_nativeClientsMappingToId.contains(id)) {
auto client = m_nativeClientsMappingToId[id];
if (client.socket != socket) {
qCWarning(WEBSOCKET_LOG) << "Rejecting pairing request for foreign id";
return;
}
if (type == "native-start-pairing"_L1) {
bool unique = false;
QString token;
qCInfo(WEBSOCKET_LOG) << "Client" << client.id << "entered pairing mode";
auto ctx = GpgME::Context::createForProtocol(GpgME::Protocol::OpenPGP);
do {
const auto rnd = ctx->generateRandomBytes(PAIRING_TOKEN_LENGTH+1, GpgME::Context::RandomMode::ZBase32);
token = QString::fromLatin1(QByteArrayView(rnd.value()).chopped(1)).toUpper();
unique = std::find_if(m_nativeClientsMappingToId.cbegin(), m_nativeClientsMappingToId.cend(), [token](auto client) {
return token == client.pairingToken;
}) == m_nativeClientsMappingToId.cend();
if (!unique) {
// log potential DOS attacks
qCWarning(WEBSOCKET_LOG) << "Collision while generating token for" << client.id << "address" << socket->peerAddress();
}
} while(!unique);
delete(ctx);
client.pairingToken = token;
m_nativeClientsMappingToId[id] = client;
sendMessageToNativeClient(id, Protocol::makeCommand(Protocol::PairingRequest, QJsonObject{{ "token"_L1, token }}, rawid));
} else { // "native-end-pairing"
client.pairingToken.clear();
m_nativeClientsMappingToId[id] = client;
sendMessageToNativeClient(id, Protocol::makeCommand(Protocol::PairingRequest, QJsonObject(), rawid));
}
}
}
return;
}
// requests originating in web client -> forward to native client
case Command::EwsResponse:
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: {
sendMessageToNativeClient(qualifiedId(object["id"_L1].toString(), socket), object);
return;
}
// requests originating in native client -> forward to web client
case Command::InfoFetched:
case Command::Error:
case Command::Ews:
case Command::StatusUpdate: {
const auto rawid = fromNativeId(object["id"_L1].toString());
const auto id = qualifiedId(rawid, socket);
sendMessageToWebClient(id, Protocol::makeCommand(command, arguments, rawid));
return;
}
// debug
case Command::Log:
qCWarning(WEBSOCKET_LOG) << arguments["message"_L1].toString() << arguments["args"_L1].toString();
return;
default:
qCWarning(WEBSOCKET_LOG) << "Invalid json received: invalid command" << command;
return;
}
}
bool WebServer::sendMessageToWebClient(const QString &id, const QJsonObject &obj)
{
auto socket = m_webClientsMappingToId.value(id);
if (!socket) {
return false;
}
const QJsonDocument doc(obj);
socket->sendTextMessage(QString::fromUtf8(doc.toJson()));
return true;
// TODO: centrally send error to native client, if web client is not available?
}
bool WebServer::sendMessageToNativeClient(const QString &id, const QJsonObject &obj)
{
auto it = m_nativeClientsMappingToId.constFind(id);
if (it == m_nativeClientsMappingToId.cend()) {
sendMessageToWebClient(id, errorCommand(i18n("Unable to find GpgOL/Web native client. Ensure GpgOL/Web is started.")));
return false;
}
auto device = it.value();
const QJsonDocument doc(obj);
device.socket->sendTextMessage(QString::fromUtf8(doc.toJson()));
return true;
}
void WebServer::processBinaryMessage(QByteArray message)
{
// TODO: what's this?!
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_webClientsMappingToId.cbegin(), m_webClientsMappingToId.cend(), [pClient](QWebSocket *webSocket) {
return pClient == webSocket;
});
if (it != m_webClientsMappingToId.cend()) {
const auto id = it.key();
qCInfo(WEBSOCKET_LOG) << "Web client for" << id << "was disconnected.";
sendMessageToNativeClient(id, Protocol::makeCommand(Protocol::Disconnection, QJsonObject(), QString()));
m_webClientsMappingToId.removeIf([pClient](auto device) {
return pClient == device.value();
});
}
}
// Native client was disconnected
const auto it = std::find_if(m_nativeClientsMappingToId.cbegin(), m_nativeClientsMappingToId.cend(), [pClient](NativeClient client) {
return pClient == client.socket;
});
if (it != m_nativeClientsMappingToId.cend()) {
const auto id = it.key();
qCInfo(WEBSOCKET_LOG) << "Native client for" << id << "was disconnected.";
sendMessageToWebClient(id, Protocol::makeCommand(Protocol::Disconnection, QJsonObject(), QString()));
}
m_nativeClientsMappingToId.removeIf([pClient](auto device) {
return pClient == device.value().socket;
});
pClient->deleteLater();
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Feb 26, 7:13 PM (19 h, 21 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
fe/7d/c1598407a3fb521ff94c32ca7374

Event Timeline