diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 09283e8..52c8f93 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1,44 +1,63 @@ # SPDX-FileCopyrightText: 2023 g10 code GmbH # SPDX-Contributor: Carl Schwan # SPDX-License-Identifier: BSD-2-Clause add_executable(gpgol-server) + +ecm_qt_declare_logging_category(gpgol-server-static_SRCS + HEADER websocket_debug.h + IDENTIFIER WEBSOCKET_LOG + CATEGORY_NAME org.gpgol.server.websocket + DESCRIPTION "Websocket connection" + EXPORT GPGOL +) + +ecm_qt_declare_logging_category(gpgol-server-static_SRCS + HEADER http_debug.h + IDENTIFIER HTTP_LOG + CATEGORY_NAME org.gpgol.server.http + DESCRIPTION "HTTP connection" + EXPORT GPGOL +) + target_sources(gpgol-server PRIVATE + ${gpgol-server-static_SRCS} + # Controllers controllers/abstractcontroller.cpp controllers/abstractcontroller.h controllers/registrationcontroller.cpp controllers/registrationcontroller.h controllers/staticcontroller.h controllers/staticcontroller.cpp controllers/emailcontroller.cpp controllers/emailcontroller.h # State model/serverstate.cpp model/serverstate.h # web sever webserver.cpp webserver.h main.cpp ) qt_add_resources(gpgol-server PREFIX "/" FILES web/assets/document-decrypt-16.png web/assets/document-decrypt-32.png web/assets/document-decrypt-64.png web/assets/document-decrypt-80.png web/assets/script.js web/assets/translation.js web/assets/vue.global.v3.4.21.js web/assets/main.css web/index.html ) target_link_libraries(gpgol-server PRIVATE Qt6::HttpServer Qt6::Core common) diff --git a/server/controllers/abstractcontroller.cpp b/server/controllers/abstractcontroller.cpp index 8a901a9..0bab60d 100644 --- a/server/controllers/abstractcontroller.cpp +++ b/server/controllers/abstractcontroller.cpp @@ -1,46 +1,47 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "abstractcontroller.h" #include "../model/serverstate.h" +#include "http_debug.h" #include #include #include using namespace Qt::Literals::StringLiterals; QHttpServerResponse AbstractController::badRequest(const QString &reason) { if (reason.isEmpty()) { return QHttpServerResponse(QJsonObject { { "errorMessage"_L1, "Invalid request"_L1 } }, QHttpServerResponse::StatusCode::BadRequest); } else { return QHttpServerResponse(QJsonObject { { "errorMessage"_L1, QJsonValue("Invalid request: "_L1 + reason) } }, QHttpServerResponse::StatusCode::BadRequest); } } QHttpServerResponse AbstractController::forbidden() { return QHttpServerResponse(QJsonObject { { "errorMessage"_L1, "Unable to authentificate"_L1 } }, QHttpServerResponse::StatusCode::Forbidden); } std::optional AbstractController::checkAuthentification(const QHttpServerRequest &request) { const auto &state = ServerState::instance(); const auto email = QString::fromUtf8(Utils::findHeader(request.headers(), "X-EMAIL")); if (email.isEmpty() || !state.clients.contains(email)) { - qWarning() << "no email found" << email; + qCWarning(HTTP_LOG) << "no email found" << email; return std::nullopt; } return state.clients[email]; } diff --git a/server/controllers/emailcontroller.cpp b/server/controllers/emailcontroller.cpp index ae42ff0..0952bc7 100644 --- a/server/controllers/emailcontroller.cpp +++ b/server/controllers/emailcontroller.cpp @@ -1,163 +1,162 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "emailcontroller.h" #include #include #include #include #include #include #include #include #include #include "webserver.h" +#include "http_debug.h" using namespace Qt::Literals::StringLiterals; QHttpServerResponse EmailController::abstractEmailAction(const QHttpServerRequest &request, const QString &action, QHttpServerRequest::Method method) { const auto client = checkAuthentification(request); if (!client) { return forbidden(); } QNetworkRequest viewEmailRequest(QUrl(u"http://127.0.0.1:"_s + QString::number(client->port) + u'/' + action)); viewEmailRequest.setHeader(QNetworkRequest::ContentTypeHeader, u"application/json"_s); auto email = Utils::findHeader(request.headers(), "X-EMAIL"); auto displayName = Utils::findHeader(request.headers(), "X-NAME"); auto token = QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8(); viewEmailRequest.setRawHeader("X-EMAIL", email); viewEmailRequest.setRawHeader("X-TOKEN", token); viewEmailRequest.setRawHeader("X-NAME", displayName); auto &serverState = ServerState::instance(); serverState.composerRequest[token] = QString::fromUtf8(email); auto &state = ServerState::instance(); QNetworkReply *reply; if (method == QHttpServerRequest::Method::Post) { const auto body = request.body(); reply = state.qnam.post(viewEmailRequest, body); } else { reply = state.qnam.deleteResource(viewEmailRequest); } QObject::connect(reply, &QNetworkReply::finished, reply, [reply]() { if (reply->error() != QNetworkReply::NoError) { - qWarning() << reply->error() << reply->errorString(); - } else { - qWarning() << "sent request to view message to server"; + qCWarning(HTTP_LOG) << reply->error() << reply->errorString(); } }); return QHttpServerResponse(QJsonObject { {"status"_L1, "ok"_L1}, }); } QHttpServerResponse EmailController::viewEmailAction(const QHttpServerRequest &request) { if (request.method() != QHttpServerRequest::Method::Post) { return badRequest(u"Endpoint only supports POST request"_s); } return abstractEmailAction(request, u"view"_s); } QHttpServerResponse EmailController::newEmailAction(const QHttpServerRequest &request) { if (request.method() != QHttpServerRequest::Method::Post) { return badRequest(u"Endpoint only supports POST request"_s); } return abstractEmailAction(request, u"new"_s); } QHttpServerResponse EmailController::draftAction(QString, const QHttpServerRequest &request) { if (request.method() != QHttpServerRequest::Method::Post && request.method() != QHttpServerRequest::Method::Delete) { return badRequest(u"Endpoint only supports POST request"_s); } return abstractEmailAction(request, request.url().path(), request.method()); } QHttpServerResponse EmailController::replyEmailAction(const QHttpServerRequest &request) { if (request.method() != QHttpServerRequest::Method::Post) { return badRequest(u"Endpoint only supports POST request"_s); } return abstractEmailAction(request, u"reply"_s); } QHttpServerResponse EmailController::forwardEmailAction(const QHttpServerRequest &request) { if (request.method() != QHttpServerRequest::Method::Post) { return badRequest(u"Endpoint only supports POST request"_s); } return abstractEmailAction(request, u"forward"_s); } QHttpServerResponse checkStatus(int port, const QByteArray &body) { QNetworkRequest infoEmailRequest(QUrl(u"http://127.0.0.1:"_s + QString::number(port) + u"/info"_s)); infoEmailRequest.setHeader(QNetworkRequest::ContentTypeHeader, u"application/json"_s); auto &state = ServerState::instance(); QEventLoop eventLoop; auto reply = state.qnam.post(infoEmailRequest, body); QObject::connect(reply, &QNetworkReply::finished, &eventLoop, &QEventLoop::quit); eventLoop.exec(); QJsonParseError error; const auto resultBody = QJsonDocument::fromJson(reply->readAll(), &error); if (resultBody.isNull()) { return QHttpServerResponse(QHttpServerResponse::StatusCode::BadRequest); } if (!resultBody.isObject()) { return QHttpServerResponse(QHttpServerResponse::StatusCode::BadRequest); } return QHttpServerResponse{resultBody.object()}; } QHttpServerResponse EmailController::infoEmailAction(const QHttpServerRequest &request) { if (request.method() != QHttpServerRequest::Method::Post) { return badRequest(u"Endpoint only supports POST request"_s); } const auto server = checkAuthentification(request); if (!server) { return forbidden(); } return checkStatus(server->port, request.body()); } QHttpServerResponse EmailController::socketWebAction(const QHttpServerRequest &request) { const auto email = QString::fromUtf8(Utils::findHeader(request.headers(), "X-EMAIL")); const auto token = Utils::findHeader(request.headers(), "X-TOKEN"); const auto &serverState = ServerState::instance(); qDebug() << serverState.composerRequest << email << token; if (serverState.composerRequest[token] != email) { return forbidden(); } WebServer::self().sendMessageToWebClient(email, request.body()); return QHttpServerResponse(QJsonObject{ { "status"_L1, "OK"_L1 }, }); } diff --git a/server/controllers/registrationcontroller.cpp b/server/controllers/registrationcontroller.cpp index 4db20ed..ed3e4e0 100644 --- a/server/controllers/registrationcontroller.cpp +++ b/server/controllers/registrationcontroller.cpp @@ -1,44 +1,45 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "registrationcontroller.h" #include #include #include #include #include #include #include "../model/serverstate.h" +#include "http_debug.h" using namespace Qt::Literals::StringLiterals; QHttpServerResponse RegistrationController::registerAction(const QHttpServerRequest &request) { const auto json = QJsonDocument::fromJson(request.body()); if (json.isEmpty() || !json.isObject()) { return badRequest(); } const auto object = json.object(); if (object.isEmpty() || !object.contains("port"_L1) || !object.contains("emails"_L1)) { return badRequest(); } const auto emails = object["emails"_L1].toArray(); const Client::Id clientId = object["clientId"_L1].toString(); const auto port = object["port"_L1].toInt(); for (const auto email : emails) { ServerState::instance().clients[email.toString().toLower()] = Client { clientId, port, }; - qInfo() << "Registration of email" << email.toString() << "on port" << object["port"_L1].toInt() << "with clientId" << clientId; + qCInfo(HTTP_LOG) << "Registration of email" << email.toString() << "on port" << object["port"_L1].toInt() << "with clientId" << clientId; } return QHttpServerResponse("text/plain", "Server added\n"); } diff --git a/server/controllers/staticcontroller.cpp b/server/controllers/staticcontroller.cpp index 8f1cfb3..2e60752 100644 --- a/server/controllers/staticcontroller.cpp +++ b/server/controllers/staticcontroller.cpp @@ -1,38 +1,40 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "staticcontroller.h" #include #include +#include "http_debug.h" using namespace Qt::Literals::StringLiterals; -QHttpServerResponse StaticController::homeAction(const QHttpServerRequest &request) +QHttpServerResponse StaticController::homeAction(const QHttpServerRequest &) { QFile file(u":/web/index.html"_s); if (!file.open(QIODeviceBase::ReadOnly)) { - qWarning() << file.errorString(); + qCWarning(HTTP_LOG) << file.errorString(); + return QHttpServerResponse(QHttpServerResponder::StatusCode::NotFound); } return QHttpServerResponse("text/html", file.readAll()); } -QHttpServerResponse StaticController::assetsAction(QString fileName, const QHttpServerRequest &request) +QHttpServerResponse StaticController::assetsAction(QString fileName, const QHttpServerRequest &) { QFile file(u":/web/assets/"_s + fileName); if (!file.open(QIODeviceBase::ReadOnly)) { - qWarning() << file.errorString(); - return badRequest(); + qCWarning(HTTP_LOG) << file.errorString(); + return QHttpServerResponse(QHttpServerResponder::StatusCode::NotFound); } if (fileName.endsWith(u".png"_s)) { return QHttpServerResponse("image/png", file.readAll()); } else if (fileName.endsWith(u".js"_s)) { return QHttpServerResponse("text/javascript", file.readAll()); } else if (fileName.endsWith(u".css"_s)) { return QHttpServerResponse("text/css", file.readAll()); } return QHttpServerResponse("text/plain", file.readAll()); } diff --git a/server/main.cpp b/server/main.cpp index a1b7bcb..29eb997 100644 --- a/server/main.cpp +++ b/server/main.cpp @@ -1,30 +1,31 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include #include #include #include #include #include "webserver.h" +#include "http_debug.h" using namespace Qt::Literals::StringLiterals; int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); auto &webServer = WebServer::self(); if (!webServer.run()) { - qWarning() << "Server failed to listen on a port."; + qCWarning(HTTP_LOG) << "Server failed to listen on a port."; return 1; } return app.exec(); } diff --git a/server/webserver.cpp b/server/webserver.cpp index 7a7ef1c..75deca3 100644 --- a/server/webserver.cpp +++ b/server/webserver.cpp @@ -1,300 +1,302 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "webserver.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "controllers/registrationcontroller.h" #include "controllers/staticcontroller.h" #include "controllers/emailcontroller.h" +#include "websocket_debug.h" +#include "http_debug.h" using namespace Qt::Literals::StringLiterals; WebServer WebServer::s_instance = WebServer(); WebServer &WebServer::self() { return s_instance; } WebServer::WebServer() : QObject(nullptr) , m_httpServer(new QHttpServer(this)) , m_webSocketServer(new QWebSocketServer(u"GPGOL"_s, QWebSocketServer::SslMode::SecureMode, 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")); Q_ASSERT(!keyPath.isEmpty()); Q_ASSERT(!certPath.isEmpty()); QFile privateKeyFile(keyPath); if (!privateKeyFile.open(QIODevice::ReadOnly)) { - qWarning() << u"Couldn't open file for reading: %1"_s.arg(privateKeyFile.errorString()); + qCFatal(HTTP_LOG) << u"Couldn't open file for reading: %1"_s.arg(privateKeyFile.errorString()); return false; } const QSslKey sslKey(&privateKeyFile, QSsl::Rsa); privateKeyFile.close(); const auto sslCertificateChain = QSslCertificate::fromPath(certPath); if (sslCertificateChain.isEmpty()) { - qWarning() << u"Couldn't retrieve SSL certificate from file."_s; + qCFatal(HTTP_LOG) << u"Couldn't retrieve SSL certificate from file."_s; return false; } // Static assets controller m_httpServer->route(u"/home"_s, &StaticController::homeAction); m_httpServer->route(u"/assets/"_s, &StaticController::assetsAction); // Registration controller m_httpServer->route(u"/register"_s, &RegistrationController::registerAction); // Email controller m_httpServer->route(u"/view"_s, &EmailController::viewEmailAction); m_httpServer->route(u"/info"_s, &EmailController::infoEmailAction); m_httpServer->route(u"/reply"_s, &EmailController::replyEmailAction); m_httpServer->route(u"/forward"_s, &EmailController::forwardEmailAction); m_httpServer->route(u"/new"_s, &EmailController::newEmailAction); m_httpServer->route(u"/socket-web"_s, &EmailController::socketWebAction); m_httpServer->route(u"/draft/"_s, &EmailController::draftAction); m_httpServer->sslSetup(sslCertificateChain.front(), sslKey); const auto port = m_httpServer->listen(QHostAddress::Any, WebServer::Port); if (!port) { - qWarning() << "Server failed to listen on a port."; + qCFatal(HTTP_LOG) << "Server failed to listen on a port."; return false; } - qWarning() << u"Running http server on https://127.0.0.1:%1/ (Press CTRL+C to quit)"_s.arg(port); + qInfo(HTTP_LOG) << u"Running http server on https://127.0.0.1:%1/ (Press CTRL+C to quit)"_s.arg(port); QSslConfiguration sslConfiguration; sslConfiguration.setPeerVerifyMode(QSslSocket::VerifyNone); sslConfiguration.setLocalCertificate(sslCertificateChain.front()); sslConfiguration.setPrivateKey(sslKey); m_webSocketServer->setSslConfiguration(sslConfiguration); if (m_webSocketServer->listen(QHostAddress::Any, WebServer::WebSocketPort)) { - qWarning() << u"Running websocket server on wss://127.0.0.1:%1/ (Press CTRL+C to quit)"_s.arg(WebServer::Port + 1); + qCInfo(WEBSOCKET_LOG) << u"Running websocket server on wss://127.0.0.1:%1/ (Press CTRL+C to quit)"_s.arg(WebServer::Port + 1); connect(m_webSocketServer, &QWebSocketServer::newConnection, this, &WebServer::onNewConnection); } return true; } void WebServer::onNewConnection() { auto pSocket = m_webSocketServer->nextPendingConnection(); if (!pSocket) { return; } - qDebug() << "Client connected:" << pSocket->peerName() << pSocket->origin() << pSocket->localAddress() << pSocket->localPort(); + qCInfo(WEBSOCKET_LOG) << "Client connected:" << pSocket->peerName() << pSocket->origin() << pSocket->localAddress() << pSocket->localPort(); connect(pSocket, &QWebSocket::textMessageReceived, this, &WebServer::processTextMessage); connect(pSocket, &QWebSocket::binaryMessageReceived, this, &WebServer::processBinaryMessage); connect(pSocket, &QWebSocket::disconnected, this, &WebServer::socketDisconnected); m_clients << pSocket; } void WebServer::processTextMessage(QString message) { auto webClient = qobject_cast(sender()); if (webClient) { QJsonParseError error; const auto doc = QJsonDocument::fromJson(message.toUtf8(), &error); if (error.error != QJsonParseError::NoError) { - qWarning() << "Error parsing json" << error.errorString(); + qCWarning(WEBSOCKET_LOG) << "Error parsing json" << error.errorString(); return; } if (!doc.isObject()) { - qWarning() << "Invalid json received"; + qCWarning(WEBSOCKET_LOG) << "Invalid json received"; return; } const auto object = doc.object(); if (!object.contains("command"_L1) || !object["command"_L1].isString() || !object.contains("arguments"_L1) || !object["arguments"_L1].isObject()) { - qWarning() << "Invalid json received: no command or arguments set" ; + qCWarning(WEBSOCKET_LOG) << "Invalid json received: no command or arguments set" ; return; } static QHash commandMapping { { "register"_L1, WebServer::Command::Register }, { "email-sent"_L1, WebServer::Command::EmailSent }, }; const auto command = commandMapping[doc["command"_L1].toString()]; processCommand(command, object["arguments"_L1].toObject(), webClient); } } void WebServer::processCommand(Command command, const QJsonObject &arguments, QWebSocket *socket) { switch (command) { case Command::Register: { const auto type = arguments["type"_L1].toString(); - qDebug() << "Register" << arguments; + qCWarning(WEBSOCKET_LOG) << "Register" << arguments; if (type.isEmpty()) { - qWarning() << "empty client type given when registering"; + qCWarning(WEBSOCKET_LOG) << "empty client type given when registering"; return; } const auto emails = arguments["emails"_L1].toArray(); if (type == "webclient"_L1) { if (emails.isEmpty()) { - qWarning() << "empty email given"; + qCWarning(WEBSOCKET_LOG) << "empty email given"; } for (const auto &email : emails) { m_webClientsMappingToEmail[email.toString()] = socket; - qWarning() << "email" << email.toString() << "mapped to a web client"; + qCWarning(WEBSOCKET_LOG) << "email" << email.toString() << "mapped to a web client"; const auto nativeClient = m_nativeClientsMappingToEmail[email.toString()]; if (nativeClient) { QJsonDocument doc(QJsonObject{ { "type"_L1, "connection"_L1 }, { "payload"_L1, QJsonObject{ { "client_type"_L1, "web_client"_L1 } }} }); nativeClient->sendTextMessage(QString::fromUtf8(doc.toJson())); } } } else { if (emails.isEmpty()) { - qWarning() << "empty email given"; + qCWarning(WEBSOCKET_LOG) << "empty email given"; } for (const auto &email : emails) { m_nativeClientsMappingToEmail[email.toString()] = socket; - qWarning() << "email" << email.toString() << "mapped to a native client"; + qCWarning(WEBSOCKET_LOG) << "email" << email.toString() << "mapped to a native client"; const auto webClient = m_webClientsMappingToEmail[email.toString()]; if (webClient) { QJsonDocument doc(QJsonObject{ { "type"_L1, "connection"_L1 }, { "payload"_L1, QJsonObject{ { "client_type"_L1, "native_client"_L1 } }} }); webClient->sendTextMessage(QString::fromUtf8(doc.toJson())); } } } return; } case Command::EmailSent: { const auto email = arguments["email"_L1].toString(); const auto socket = m_nativeClientsMappingToEmail[email]; if (!socket) { return; } QJsonDocument doc(QJsonObject{ { "type"_L1, "email-sent"_L1 }, { "arguments"_L1, arguments }, }); socket->sendTextMessage(QString::fromUtf8(doc.toJson())); return; } case Command::Undefined: - qWarning() << "Invalid json received: invalid command" ; + qCWarning(WEBSOCKET_LOG) << "Invalid json received: invalid command" ; return; } } bool WebServer::sendMessageToWebClient(const QString &email, const QByteArray &payload) { auto socket = m_webClientsMappingToEmail[email]; if (!socket) { return false; } socket->sendTextMessage(QString::fromUtf8(payload)); return true; } void WebServer::processBinaryMessage(QByteArray message) { - qWarning() << "got binary message" << message; + qCWarning(WEBSOCKET_LOG) << "got binary message" << message; QWebSocket *pClient = qobject_cast(sender()); if (pClient) { pClient->sendBinaryMessage(message); } } void WebServer::socketDisconnected() { QWebSocket *pClient = qobject_cast(sender()); if (pClient) { - qDebug() << "Client disconnected" << pClient; + 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 nativeClient = m_nativeClientsMappingToEmail[email]; - qDebug() << "webclient with email disconnected" << email << nativeClient; + qCInfo(WEBSOCKET_LOG) << "webclient with email disconnected" << email << nativeClient; if (nativeClient) { QJsonDocument doc(QJsonObject{ { "type"_L1, "disconnection"_L1 }, }); nativeClient->sendTextMessage(QString::fromUtf8(doc.toJson())); } m_webClientsMappingToEmail.removeIf([pClient](auto socket) { return pClient == socket.value(); }); } } // Native client was disconnected const auto emails = m_nativeClientsMappingToEmail.keys(); for (const auto &email : emails) { const auto webSocket = m_nativeClientsMappingToEmail[email]; if (webSocket != pClient) { - qDebug() << "webSocket not equal" << email << webSocket << pClient; + qCWarning(WEBSOCKET_LOG) << "webSocket not equal" << email << webSocket << pClient; continue; } - qDebug() << "native client for" << email << "was disconnected."; + qCInfo(WEBSOCKET_LOG) << "native client for" << email << "was disconnected."; QJsonDocument doc(QJsonObject{ { "type"_L1, "disconnection"_L1 }, }); sendMessageToWebClient(email, doc.toJson()); } m_nativeClientsMappingToEmail.removeIf([pClient](auto socket) { return pClient == socket.value(); }); m_clients.removeAll(pClient); } }