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