Page Menu
Home
GnuPG
Search
Configure Global Search
Log In
Files
F36623264
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Size
22 KB
Subscribers
None
View Options
diff --git a/server/webserver.cpp b/server/webserver.cpp
index 4f019b5..8dcc4bc 100644
--- a/server/webserver.cpp
+++ b/server/webserver.cpp
@@ -1,417 +1,442 @@
// 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()
{
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();
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);
}
}
-static QString nativeIdToPublicId(const QString &id)
+// 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 (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 id = object["id"_L1].toString();
- if (id.isEmpty()) {
+ 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}}, id);
+ 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;
}
const auto id = nativeIdToPublicId(rawid);
+ 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]}}, 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
- const auto token = arguments["token"_L1].toString();
- if (token.length() != PAIRING_TOKEN_LENGTH) {
- // If users type the token, instead of pasting, we be sent partial tokens. Ignore those.
- return;
- }
- const auto it = std::find_if(m_nativeClientsMappingToId.cbegin(), m_nativeClientsMappingToId.cend(), [token](NativeClient client) {
- return client.isPairing() && (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 {
- const auto id = it.key();
- m_webClientsMappingToId[id] = socket;
- const auto command = Protocol::makeCommand(
- Protocol::Connection,
- QJsonObject(),
- id
- );
- sendMessageToWebClient(id, command);
- // also pass on request to native client, in order to signal successful pairing
- sendMessageToNativeClient(id, Protocol::makeCommand(Protocol::PairingRequest, QJsonObject(), id));
- qCInfo(WEBSOCKET_LOG) << "Pairing sucess on connection" << id;
- }
- } else {
- const auto id = nativeIdToPublicId(object["id"_L1].toString());
+ 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 }}, id));
+ 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(), id));
+ 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(object["id"_L1].toString(), object);
+ 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 id = nativeIdToPublicId(object["id"_L1].toString());
- sendMessageToWebClient(id, Protocol::makeCommand(command, arguments, id));
+ 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();
}
diff --git a/server/webserver.h b/server/webserver.h
index 7d3bfcc..ad07516 100644
--- a/server/webserver.h
+++ b/server/webserver.h
@@ -1,80 +1,80 @@
// 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;
QString pairingToken;
bool isValid() const
{
return socket != nullptr && !name.isEmpty() && !id.isEmpty();
}
bool isPairing() const
{
return !pairingToken.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 &id, const QJsonObject &payload);
bool sendMessageToNativeClient(const QString &id, const QJsonObject &obj);
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);
+ void tryPairWebclient(const QString &token, QWebSocket *webclient);
QHttpServer *const m_httpServer;
std::unique_ptr<QSslServer> m_tcpserver;
QObjectCleanupHandler m_clients;
QHash<QString, QWebSocket *> m_webClientsMappingToId;
-
QHash<QString, NativeClient> m_nativeClientsMappingToId;
};
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
Thu, Feb 26, 6:43 PM (16 h, 13 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
67/a1/fb71f0ba5208e2038385b51a4203
Attached To
rOJ GpgOL.js
Event Timeline
Log In to Comment