diff --git a/broker/CMakeLists.txt b/broker/CMakeLists.txt index 56cdad7..bcecaf4 100644 --- a/broker/CMakeLists.txt +++ b/broker/CMakeLists.txt @@ -1,47 +1,45 @@ # SPDX-FileCopyrightText: 2023 g10 code GmbH # SPDX-Contributor: Carl Schwan # SPDX-License-Identifier: BSD-2-Clause add_executable(gpgol-broker) target_sources(gpgol-broker PRIVATE # 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 - # websocket sever - websocketserver.cpp - websocketserver.h - websocketrequest.cpp - websocketrequest.h + # web sever + webserver.cpp + webserver.h main.cpp ) qt_add_resources(gpgol-broker PREFIX "/" FILES assets/certificate.crt assets/private.key assets/document-decrypt-16.png assets/document-decrypt-32.png assets/document-decrypt-64.png assets/document-decrypt-80.png assets/script.js web/index.html ) target_link_libraries(gpgol-broker PRIVATE Qt6::HttpServer Qt6::Core) diff --git a/broker/assets/script.js b/broker/assets/script.js index a1ece3c..a4cddf4 100644 --- a/broker/assets/script.js +++ b/broker/assets/script.js @@ -1,278 +1,278 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later function downloadViaRest(callback) { const context = { isRest: true }; Office.context.mailbox.getCallbackTokenAsync(context, (tokenResults) => { if (tokenResults.status === Office.AsyncResultStatus.Failed) { console.error('Failed to get rest api auth token'); return; } const request = '' + '' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' IdOnly' + ' true' + ' ' + ' ' + ' ' + ' ' + ''; Office.context.mailbox.makeEwsRequestAsync(request, (asyncResult) => { const parser = new DOMParser(); xmlDoc = parser.parseFromString(asyncResult.value, "text/xml"); const mimeContent = xmlDoc.getElementsByTagName('t:MimeContent')[0].innerHTML; callback(atob(mimeContent)); }); }); } async function view(content) { const response = await fetch('https://localhost:5656/view', { method: 'POST', body: content, headers: { 'X-EMAIL': Office.context.mailbox.userProfile.emailAddress, 'X-NAME': Office.context.mailbox.userProfile.displayName, }, }); const json = await response.json(); return json; } async function info(content) { const response = await fetch('https://localhost:5656/info', { method: 'POST', body: content, headers: { 'X-EMAIL': Office.context.mailbox.userProfile.emailAddress, 'X-NAME': Office.context.mailbox.userProfile.displayName, }, }); const json = await response.json(); return json; } async function newEmail(content) { const response = await fetch('https://localhost:5656/new', { method: 'POST', headers: { 'X-EMAIL': Office.context.mailbox.userProfile.emailAddress, 'X-NAME': Office.context.mailbox.userProfile.displayName, }, }); const json = await response.json(); return json; } async function reply(content) { const response = await fetch('https://localhost:5656/reply', { method: 'POST', body: content, headers: { 'X-EMAIL': Office.context.mailbox.userProfile.emailAddress, 'X-NAME': Office.context.mailbox.userProfile.displayName, }, }); const json = await response.json(); return json; } async function forward(content) { const response = await fetch('https://localhost:5656/forward', { method: 'POST', body: content, headers: { 'X-EMAIL': Office.context.mailbox.userProfile.emailAddress, 'X-NAME': Office.context.mailbox.userProfile.displayName, }, }); const json = await response.json(); return json; } async function openDraft(id) { const response = await fetch(`https://localhost:5656/draft/${id}`, { method: 'POST', headers: { 'X-EMAIL': Office.context.mailbox.userProfile.emailAddress, 'X-NAME': Office.context.mailbox.userProfile.displayName, }, }); const json = await response.json(); return json; } async function deleteDraft(id) { const response = await fetch(`https://localhost:5656/draft/${id}`, { method: 'DELETE', headers: { 'X-EMAIL': Office.context.mailbox.userProfile.emailAddress, 'X-NAME': Office.context.mailbox.userProfile.displayName, }, }); const json = await response.json(); return json; } function showError(errorMessage) { const errorElement = document.getElementById('error'); errorElement.innerHTML = errorMessage; errorElement.classList.remove('d-none'); } function hideError() { const errorElement = document.getElementById('error'); errorElement.classList.add('d-none'); } Office.onReady(). then(()=> { downloadViaRest(async (content) => { const status = await info(content) const statusText = document.getElementById('status-text'); if (status.encrypted || status.signed) { const decryptButton = document.getElementById('decrypt-button'); decryptButton.classList.remove('d-none'); if (status.encrypted) { decryptButton.innerText = "Decrypt"; statusText.innerText = status.signed ? "This mail is encrypted and signed." : "This mail is encrypted."; } else if (status.signed) { decryptButton.innerText = "Show signature"; statusText.innerText = "This mail is signed"; } decryptButton.addEventListener('click', (event) => { view(content); }); } document.getElementById('reply-button').addEventListener('click', (event) => { reply(content); }); document.getElementById('forward-button').addEventListener('click', (event) => { forward(content); }); document.getElementById('new-button').addEventListener('click', (event) => { newEmail(); }); if (status.drafts.length === 0) { document.getElementById('no-draft').classList.remove('d-none'); } else { const draftsContainer = document.getElementById('drafts'); status.drafts.forEach(draft => { const draftElementContainer = document.createElement('li'); const draftElement = document.createElement('button'); draftElement.classList.add('btn', 'w-100', 'd-flex', 'flex-row', 'align-items-center'); draftElement.addEventListener('click', (event) => { openDraft(draft.id); }); const date = new Date(draft.last_modification * 1000); let todaysDate = new Date(); let lastModification = ''; if ((new Date(date)).setHours(0, 0, 0, 0) == todaysDate.setHours(0, 0, 0, 0)) { lastModification = date.toLocaleTimeString([], { hour: 'numeric', minute: 'numeric', }); } else { lastModification = date.toLocaleDateString(); } const content = document.createTextNode('Last Modified: ' + lastModification); draftElement.appendChild(content); const deleteDraftButton = document.createElement('button'); deleteDraftButton.classList.add('btn', 'btn-danger', 'ms-auto', 'py-1'); deleteDraftButton.addEventListener('click', (event) => { deleteDraft(draft.id); draftElement.remove(); }); const deleteDraftButtonContent = document.createTextNode('X'); deleteDraftButton.appendChild(deleteDraftButtonContent); draftElement.appendChild(deleteDraftButton); draftElementContainer.appendChild(draftElement); draftsContainer.appendChild(draftElementContainer); }); } function webSocketConnect() { // Create WebSocket connection. - const socket = new WebSocket("wss://localhost:5657"); + const socket = new WebSocket("wss://localhost:5656"); // Connection opened socket.addEventListener("open", (event) => { hideError(); socket.send(JSON.stringify({ command: "register", arguments: { emails: [Office.context.mailbox.userProfile.emailAddress], type: 'webclient', }, })); }); socket.addEventListener("close", (event) => { showError('Native client was disconnected'); setTimeout(function() { webSocketConnect(); }, 1000); }); socket.addEventListener("error", (event) => { showError('Native client received an error'); setTimeout(function() { webSocketConnect(); }, 1000); }); // Listen for messages socket.addEventListener("message", ({ data }) => { const message = JSON.parse(data); console.log("Message from server ", message); switch (message.type) { case 'ews': Office.context.mailbox.makeEwsRequestAsync(message.payload, (asyncResult) => { console.log('Email sent') // let the client known that the email was sent socket.send(JSON.stringify({ command: 'email-sent', arguments: { id: message.id, email: Office.context.mailbox.userProfile.emailAddress, } })); }); break; case 'disconnection': - showError('Native client was disconnected'); + showError('Native client was disconnected (disconnection)'); break; case 'connection': hideError(); break; } }); } webSocketConnect(); }); }); diff --git a/broker/controllers/emailcontroller.cpp b/broker/controllers/emailcontroller.cpp index b277bbb..6e7db8b 100644 --- a/broker/controllers/emailcontroller.cpp +++ b/broker/controllers/emailcontroller.cpp @@ -1,164 +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 "websocketrequest.h" +#include "webserver.h" using namespace Qt::Literals::StringLiterals; QHttpServerResponse EmailController::abstractEmailAction(const QHttpServerRequest &request, const QString &action, QHttpServerRequest::Method method) { const auto server = checkAuthentification(request); if (!server) { return forbidden(); } QNetworkRequest viewEmailRequest(QUrl(u"http://127.0.0.1:"_s + QString::number(server->port) + u'/' + action)); viewEmailRequest.setHeader(QNetworkRequest::ContentTypeHeader, u"application/json"_s); auto email = AbstractController::findHeader(request.headers(), "X-EMAIL"); auto displayName = AbstractController::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"; } }); 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(findHeader(request.headers(), "X-EMAIL")); const auto token = findHeader(request.headers(), "X-TOKEN"); const auto &serverState = ServerState::instance(); qDebug() << serverState.composerRequest << email << token; if (serverState.composerRequest[token] != email) { return forbidden(); } - WebsocketRequest websocketRequest(email); - websocketRequest.setPayload(request.body()); - websocketRequest.send(); + WebServer::self().sendMessageToWebClient(email, request.body()); return QHttpServerResponse(QJsonObject{ { "status"_L1, "OK"_L1 }, }); } diff --git a/broker/main.cpp b/broker/main.cpp index 13b220a..a1b7bcb 100644 --- a/broker/main.cpp +++ b/broker/main.cpp @@ -1,90 +1,30 @@ // 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 #include -#include "controllers/registrationcontroller.h" -#include "controllers/staticcontroller.h" -#include "controllers/emailcontroller.h" -#include "websocketserver.h" -#include "websocketrequest.h" +#include "webserver.h" using namespace Qt::Literals::StringLiterals; int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); - QHttpServer server; - QNetworkAccessManager qnam; + auto &webServer = WebServer::self(); - // Static assets controller - server.route(u"/"_s, &StaticController::homeAction); - server.route(u"/assets/"_s, &StaticController::assetsAction); - - // Registration controller - server.route(u"/register"_s, &RegistrationController::registerAction); - - // Email controller - server.route(u"/view"_s, &EmailController::viewEmailAction); - server.route(u"/info"_s, &EmailController::infoEmailAction); - server.route(u"/reply"_s, &EmailController::replyEmailAction); - server.route(u"/forward"_s, &EmailController::forwardEmailAction); - server.route(u"/new"_s, &EmailController::newEmailAction); - server.route(u"/socket-web"_s, &EmailController::socketWebAction); - - server.route(u"/draft/"_s, &EmailController::draftAction); - - server.afterRequest([](QHttpServerResponse &&resp) { - resp.setHeader("Access-Control-Allow-Origin", "*"); - return std::move(resp); - }); - - const auto sslCertificateChain = - QSslCertificate::fromPath(QStringLiteral(":/assets/certificate.crt")); - if (sslCertificateChain.empty()) { - qWarning() << QCoreApplication::translate("QHttpServerExample", - "Couldn't retrieve SSL certificate from file."); - return -1; - } - QFile privateKeyFile(QStringLiteral(":/assets/private.key")); - if (!privateKeyFile.open(QIODevice::ReadOnly)) { - qWarning() << QCoreApplication::translate("QHttpServerExample", - "Couldn't open file for reading: %1") - .arg(privateKeyFile.errorString()); - return -1; - } - const QSslKey sslKey(&privateKeyFile, QSsl::Rsa); - - server.sslSetup(sslCertificateChain.front(), sslKey); - privateKeyFile.close(); - - const auto sslPort = server.listen(QHostAddress::Any); - if (!sslPort) { - qWarning() << QCoreApplication::translate("QHttpServerExample", - "Server failed to listen on a port."); - return -1; - } - - const auto port = server.listen(QHostAddress::Any, 5656); - if (!port) { + if (!webServer.run()) { qWarning() << "Server failed to listen on a port."; return 1; } - qWarning() << u"Running on https://127.0.0.1:%1/ (Press CTRL+C to quit)"_s.arg(port); - - WebsocketServer websocketServer(sslCertificateChain.front(), sslKey, 5657); - WebsocketRequestBackend::instance().setServer(&websocketServer); return app.exec(); } diff --git a/broker/manifest.xml b/broker/manifest.xml index f646ada..e83b503 100644 --- a/broker/manifest.xml +++ b/broker/manifest.xml @@ -1,112 +1,112 @@ 95b7e9a9-1ce6-49c2-adf8-48aa704f156d 1.0.0.0 g10code de-DE https://www.gnupg.org
250
ReadWriteMailbox false