Page Menu
Home
GnuPG
Search
Configure Global Search
Log In
Files
F22948056
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Size
23 KB
Subscribers
None
View Options
diff --git a/broker/CMakeLists.txt b/broker/CMakeLists.txt
index 89c91d0..e7473a7 100644
--- a/broker/CMakeLists.txt
+++ b/broker/CMakeLists.txt
@@ -1,7 +1,22 @@
# SPDX-FileCopyrightText: 2023 g10 code GmbH
# SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
# SPDX-License-Identifier: BSD-2-Clauses
add_executable(gpgol-broker)
-target_sources(gpgol-broker PRIVATE main.cpp resources.qrc)
+target_sources(gpgol-broker PRIVATE
+ controllers/abstractcontroller.cpp
+ controllers/abstractcontroller.h
+ controllers/registrationcontroller.cpp
+ controllers/registrationcontroller.h
+ controllers/verificationcontroller.cpp
+ controllers/verificationcontroller.h
+ controllers/staticcontroller.h
+ controllers/staticcontroller.cpp
+
+ model/serverstate.cpp
+ model/serverstate.h
+
+ main.cpp
+ resources.qrc
+)
target_link_libraries(gpgol-broker PRIVATE Qt6::HttpServer Qt6::Core)
diff --git a/broker/controllers/abstractcontroller.cpp b/broker/controllers/abstractcontroller.cpp
new file mode 100644
index 0000000..22079a1
--- /dev/null
+++ b/broker/controllers/abstractcontroller.cpp
@@ -0,0 +1,14 @@
+// SPDX-FileCopyrightText: 2023 g10 code GmbH
+// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "abstractcontroller.h"
+
+QHttpServerResponse AbstractController::badRequest(const QString &reason)
+{
+ if (reason.isEmpty()) {
+ return QHttpServerResponse("text/plain", "Invalid request\n", QHttpServerResponse::StatusCode::BadRequest);
+ } else {
+ return QHttpServerResponse("text/plain", "Invalid request: " + reason.toUtf8() + "\n", QHttpServerResponse::StatusCode::BadRequest);
+ }
+}
diff --git a/broker/controllers/abstractcontroller.h b/broker/controllers/abstractcontroller.h
new file mode 100644
index 0000000..e78981f
--- /dev/null
+++ b/broker/controllers/abstractcontroller.h
@@ -0,0 +1,15 @@
+// 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 <QHttpServerResponse>
+#include <QString>
+
+class AbstractController
+{
+protected:
+ Q_REQUIRED_RESULT
+ static QHttpServerResponse badRequest(const QString &reason = {});
+};
diff --git a/broker/controllers/registrationcontroller.cpp b/broker/controllers/registrationcontroller.cpp
new file mode 100644
index 0000000..57c2a2f
--- /dev/null
+++ b/broker/controllers/registrationcontroller.cpp
@@ -0,0 +1,34 @@
+// SPDX-FileCopyrightText: 2023 g10 code GmbH
+// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "registrationcontroller.h"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QString>
+
+#include "../model/serverstate.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("email"_L1)) {
+ return badRequest();
+ }
+
+ ServerState::instance().servers[object["email"_L1].toString()] = Server {
+ object["serverId"_L1].toString(),
+ object["port"_L1].toInt(),
+ };
+
+ return QHttpServerResponse("text/plain", "Server added\n");
+}
diff --git a/broker/controllers/registrationcontroller.h b/broker/controllers/registrationcontroller.h
new file mode 100644
index 0000000..c91c9ee
--- /dev/null
+++ b/broker/controllers/registrationcontroller.h
@@ -0,0 +1,16 @@
+// 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 "abstractcontroller.h"
+
+#include <QHttpServerResponse>
+#include <QHttpServerRequest>
+
+class RegistrationController : public AbstractController
+{
+public:
+ static QHttpServerResponse registerAction(const QHttpServerRequest &request);
+};
diff --git a/broker/controllers/staticcontroller.cpp b/broker/controllers/staticcontroller.cpp
new file mode 100644
index 0000000..e855715
--- /dev/null
+++ b/broker/controllers/staticcontroller.cpp
@@ -0,0 +1,20 @@
+// SPDX-FileCopyrightText: 2023 g10 code GmbH
+// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "staticcontroller.h"
+
+#include <QFile>
+#include <QDebug>
+
+using namespace Qt::Literals::StringLiterals;
+
+QHttpServerResponse StaticController::homeAction(const QHttpServerRequest &request)
+{
+ QFile file(u":/web/index.html"_s);
+ if (!file.open(QIODeviceBase::ReadOnly)) {
+ qWarning() << file.errorString();
+ }
+
+ return QHttpServerResponse("text/html", file.readAll());
+}
diff --git a/broker/controllers/staticcontroller.h b/broker/controllers/staticcontroller.h
new file mode 100644
index 0000000..6448a02
--- /dev/null
+++ b/broker/controllers/staticcontroller.h
@@ -0,0 +1,13 @@
+// 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 "abstractcontroller.h"
+
+class StaticController : public AbstractController
+{
+public:
+ static QHttpServerResponse homeAction(const QHttpServerRequest &request);
+};
diff --git a/broker/controllers/verificationcontroller.cpp b/broker/controllers/verificationcontroller.cpp
new file mode 100644
index 0000000..5c78edd
--- /dev/null
+++ b/broker/controllers/verificationcontroller.cpp
@@ -0,0 +1,143 @@
+// SPDX-FileCopyrightText: 2023 g10 code GmbH
+// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "verificationcontroller.h"
+
+#include "../model/serverstate.h"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QNetworkRequest>
+#include <QNetworkAccessManager>
+
+using namespace Qt::Literals::StringLiterals;
+
+QHttpServerResponse VerificationController::verifyAction(const QHttpServerRequest &request) {
+ auto &state = ServerState::instance();
+
+ const auto doc = QJsonDocument::fromJson(request.body());
+ if (doc.isEmpty() || !doc.isObject() || !doc.object().contains("email"_L1) || !doc.object().contains("clientId"_L1)) {
+ return badRequest("Missing email or client id key in body");
+ }
+
+ const auto object = doc.object();
+ const auto email = object["email"_L1].toString();
+ const auto clientId = object["clientId"_L1].toString();
+
+ if (!state.servers.contains(email)) {
+ return badRequest("No kleopatra client registered with this email");
+ }
+ const auto port = state.servers[email].port;
+ const auto serverId = state.servers[email].id;
+
+ if (clientId.isEmpty()) {
+ return badRequest("Missing clientid in body");
+ }
+
+ // TODO use gpgme random generation
+ const auto token = QUuid::createUuid().toString(QUuid::WithoutBraces);
+ const auto jsonToken = QJsonDocument(QJsonObject{
+ { "token"_L1, token },
+ }).toJson();
+
+ // Trigger verification request in the kleopatra client
+ QNetworkRequest verifyRequest(QUrl(u"http://127.0.0.1:"_s + QString::number(port) + u"/client-verify"_s));
+ verifyRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ auto verifyReply = ServerState::instance().qnam.post(verifyRequest, jsonToken);
+
+ state.currentVerifications.insert(token, Verification(clientId, serverId));
+
+ return QHttpServerResponse("application/json", jsonToken);
+}
+
+QHttpServerResponse VerificationController::verifyConfirmWebAction(const QHttpServerRequest &request) {
+ auto &state = ServerState::instance();
+
+ const auto doc = QJsonDocument::fromJson(request.body());
+ if (doc.isEmpty() || !doc.isObject() || !doc.object().contains("email"_L1)
+ || !doc.object().contains("clientId"_L1)
+ || !doc.object().contains("token"_L1)) {
+ return badRequest("Missing email, token or client id key in body");
+ }
+
+ const auto object = doc.object();
+ const auto email = object["email"_L1].toString();
+ const auto clientId = object["clientId"_L1].toString();
+ const auto token = object["token"_L1].toString();
+
+ if (!state.servers.contains(email)) {
+ return badRequest("No kleopatra client registered with this email");
+ }
+ const auto port = state.servers[email].port;
+
+ if (clientId.isEmpty()) {
+ return badRequest("Missing clientid in body");
+ }
+
+ if (!state.currentVerifications.contains(token)) {
+ return badRequest("No current validation in progress with the given token");
+ }
+
+ auto &verification = state.currentVerifications[token];
+
+ if (verification.webclient) {
+ return badRequest("Already verified");
+ }
+
+ verification.webclient = true;
+
+ return QHttpServerResponse("application/json", QJsonDocument(QJsonObject{
+ { "confirmed"_L1, verification.isConfirmed(), },
+ }).toJson());
+}
+
+QHttpServerResponse VerificationController::verifyConfirmServerAction(const QHttpServerRequest &request) {
+ auto &state = ServerState::instance();
+
+ const auto doc = QJsonDocument::fromJson(request.body());
+ if (doc.isEmpty() || !doc.isObject() || !doc.object().contains("serverId"_L1)
+ || !doc.object().contains("token"_L1)) {
+ return badRequest("Missing email, token or client id key in body");
+ }
+
+ const auto object = doc.object();
+ const auto serverId = object["serverId"_L1].toString();
+ const auto token = object["token"_L1].toString();
+
+ if (!state.currentVerifications.contains(token)) {
+ return badRequest("No current validation in progress with the given token");
+ }
+
+ auto &verification = state.currentVerifications[token];
+
+ if (verification.serverId != serverId) {
+ return badRequest("Wrong server id:" + verification.serverId + " --- " + serverId);
+ }
+
+ if (verification.desktopserver) {
+ return badRequest("Already verified");
+ }
+
+ verification.desktopserver = true;
+
+ return QHttpServerResponse("application/json", QJsonDocument(QJsonObject{
+ { "confirmed"_L1, verification.isConfirmed(), },
+ }).toJson());
+}
+
+
+QHttpServerResponse VerificationController::verifyStatusAction(const QString token, const QHttpServerRequest &request)
+{
+ const auto &state = ServerState::instance();
+
+ if (!state.currentVerifications.contains(token)) {
+ return badRequest("No current validation in progress with the given token");
+ }
+
+ const auto &verification = state.currentVerifications[token];
+
+ return QHttpServerResponse("application/json", QJsonDocument(QJsonObject{
+ { "confirmed"_L1, verification.isConfirmed(), },
+ }).toJson());
+}
diff --git a/broker/controllers/verificationcontroller.h b/broker/controllers/verificationcontroller.h
new file mode 100644
index 0000000..ce597a2
--- /dev/null
+++ b/broker/controllers/verificationcontroller.h
@@ -0,0 +1,19 @@
+// 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 "abstractcontroller.h"
+
+#include <QHttpServerResponse>
+#include <QHttpServerRequest>
+
+class VerificationController : public AbstractController
+{
+public:
+ static QHttpServerResponse verifyAction(const QHttpServerRequest &request);
+ static QHttpServerResponse verifyConfirmWebAction(const QHttpServerRequest &request);
+ static QHttpServerResponse verifyConfirmServerAction(const QHttpServerRequest &request);
+ static QHttpServerResponse verifyStatusAction(const QString token, const QHttpServerRequest &request);
+};
diff --git a/broker/main.cpp b/broker/main.cpp
index ae19942..7743494 100644
--- a/broker/main.cpp
+++ b/broker/main.cpp
@@ -1,277 +1,93 @@
-// SPDX-FileCopyrightText: 2023 g10 code Gmbh
+// SPDX-FileCopyrightText: 2023 g10 code GmbH
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QHttpServer>
#include <QHttpServerResponse>
#include <QCoreApplication>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QUuid>
#include <QFile>
+#include "controllers/registrationcontroller.h"
+#include "controllers/verificationcontroller.h"
+#include "controllers/staticcontroller.h"
+
using namespace Qt::Literals::StringLiterals;
namespace
{
QByteArray findHeader(QList<QPair<QByteArray, QByteArray>> headers, const QByteArray &key)
{
const auto it = std::find_if(std::cbegin(headers), std::cend(headers), [&key](auto header) {
return header.first == key;
});
if (it == std::cend(headers)) {
return {};
}
return it->second;
}
-
-Q_REQUIRED_RESULT
-QHttpServerResponse badRequest(const QString &reason = {})
-{
- if (reason.isEmpty()) {
- return QHttpServerResponse("text/plain", "Invalid request\n", QHttpServerResponse::StatusCode::BadRequest);
- } else {
- return QHttpServerResponse("text/plain", "Invalid request: " + reason.toUtf8() + "\n", QHttpServerResponse::StatusCode::BadRequest);
- }
-}
}
-using Token = QString;
-using ClientId = QString;
-using Email = QString;
-using Port = int;
-
-struct Server {
- using Id = QString;
- Id id;
- Port port;
-};
-
-struct Verification {
- Verification()
- {}
-
- Verification(const QString &_clientId, Server::Id _serverId)
- : clientId(_clientId)
- , serverId(_serverId)
- {}
-
- bool isEmpty() const {
- return clientId.isEmpty();
- }
-
- bool isConfirmed() const
- {
- return webclient && desktopserver;
- }
-
- QString clientId;
- Server::Id serverId;
- bool webclient = false;
- bool desktopserver = false;
-};
-
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QHttpServer server;
- QHash<Email, Server> servers;
- QHash<Email, QHash<Server::Id, ClientId>> verified;
-
- QHash<Token, Verification> currentVerifications;
-
QNetworkAccessManager qnam;
- server.route(u"/"_s, [](const QHttpServerRequest &request) {
- QFile file(u":/web/index.html"_s);
- if (!file.open(QIODeviceBase::ReadOnly)) {
- qWarning() << file.errorString();
- }
-
- return QHttpServerResponse("text/html", file.readAll());
- });
-
- server.route(u"/register"_s, [&servers](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("email"_L1)) {
- return badRequest();
- }
-
- servers[object["email"_L1].toString()] = Server {
- object["serverId"_L1].toString(),
- object["port"_L1].toInt(),
- };
-
- return QHttpServerResponse("text/plain", "Server added\n");
- });
+ server.route(u"/"_s, &StaticController::homeAction);
+ server.route(u"/register"_s, &RegistrationController::registerAction);
+ /*
server.route(u"/command/<arg>/"_s, [&servers, &verified](const QString arg, const QHttpServerRequest &request) {
const auto email = findHeader(request.headers(), "EMAIL");
if (email.isEmpty()) {
return badRequest("Missing EMAIL header");
}
if (!servers.contains(email)) {
return badRequest("No kleopatra client registered with this email");
}
const auto port = servers[email].port;
const auto serverId = servers[email].id;
const auto clientId = findHeader(request.headers(), "CLIENT_ID");
if (clientId.isEmpty()) {
return badRequest("Missing CLIENT_ID header");
}
if (!verified.contains(clientId) || !verified[clientId].contains(serverId)) {
return badRequest("Not verified client");
}
return QHttpServerResponse("text/plain", email + "\n" + QString::number(port).toUtf8() + "\n");
});
+ */
- server.route(u"/verify"_s, [&qnam, ¤tVerifications, &verified, &servers](const QHttpServerRequest &request) {
- const auto doc = QJsonDocument::fromJson(request.body());
- if (doc.isEmpty() || !doc.isObject() || !doc.object().contains("email"_L1) || !doc.object().contains("clientId"_L1)) {
- return badRequest("Missing email or client id key in body");
- }
-
- const auto object = doc.object();
- const auto email = object["email"_L1].toString();
- const auto clientId = object["clientId"_L1].toString();
-
- if (!servers.contains(email)) {
- return badRequest("No kleopatra client registered with this email");
- }
- const auto port = servers[email].port;
- const auto serverId = servers[email].id;
-
- if (clientId.isEmpty()) {
- return badRequest("Missing clientid in body");
- }
-
- // TODO use gpgme random generation
- const auto token = QUuid::createUuid().toString(QUuid::WithoutBraces);
- const auto jsonToken = QJsonDocument(QJsonObject{
- { "token"_L1, token },
- }).toJson();
-
- // Trigger verification request in the kleopatra client
- QNetworkRequest verifyRequest(QUrl(u"http://127.0.0.1:"_s + QString::number(port) + u"/client-verify"_s));
- verifyRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
- auto verifyReply = qnam.post(verifyRequest, jsonToken);
-
- currentVerifications.insert(token, Verification(clientId, serverId));
-
- return QHttpServerResponse("application/json", jsonToken);
- });
-
- server.route(u"/verify/confirm/web"_s, [&qnam, ¤tVerifications, &verified, &servers](const QHttpServerRequest &request) {
- const auto doc = QJsonDocument::fromJson(request.body());
- if (doc.isEmpty() || !doc.isObject() || !doc.object().contains("email"_L1)
- || !doc.object().contains("clientId"_L1)
- || !doc.object().contains("token"_L1)) {
- return badRequest("Missing email, token or client id key in body");
- }
-
- const auto object = doc.object();
- const auto email = object["email"_L1].toString();
- const auto clientId = object["clientId"_L1].toString();
- const auto token = object["token"_L1].toString();
-
- if (!servers.contains(email)) {
- return badRequest("No kleopatra client registered with this email");
- }
- const auto port = servers[email].port;
-
- if (clientId.isEmpty()) {
- return badRequest("Missing clientid in body");
- }
-
- if (!currentVerifications.contains(token)) {
- return badRequest("No current validation in progress with the given token");
- }
-
- auto &verification = currentVerifications[token];
-
- if (verification.webclient) {
- return badRequest("Already verified");
- }
-
- verification.webclient = true;
-
- return QHttpServerResponse("application/json", QJsonDocument(QJsonObject{
- { "confirmed"_L1, verification.isConfirmed(), },
- }).toJson());
- });
-
- server.route(u"/verify/confirm/server"_s, [&qnam, ¤tVerifications, &verified, &servers](const QHttpServerRequest &request) {
- const auto doc = QJsonDocument::fromJson(request.body());
- if (doc.isEmpty() || !doc.isObject() || !doc.object().contains("serverId"_L1)
- || !doc.object().contains("token"_L1)) {
- return badRequest("Missing email, token or client id key in body");
- }
-
- const auto object = doc.object();
- const auto serverId = object["serverId"_L1].toString();
- const auto token = object["token"_L1].toString();
-
- if (!currentVerifications.contains(token)) {
- return badRequest("No current validation in progress with the given token");
- }
-
- auto &verification = currentVerifications[token];
-
- if (verification.serverId != serverId) {
- return badRequest("Wrong server id:" + verification.serverId + " --- " + serverId);
- }
-
- if (verification.desktopserver) {
- return badRequest("Already verified");
- }
-
- verification.desktopserver = true;
-
- return QHttpServerResponse("application/json", QJsonDocument(QJsonObject{
- { "confirmed"_L1, verification.isConfirmed(), },
- }).toJson());
- });
-
- server.route(u"/verify/status/"_s, [&qnam, ¤tVerifications, &verified, &servers](const QString token, const QHttpServerRequest &request) {
- if (!currentVerifications.contains(token)) {
- return badRequest("No current validation in progress with the given token");
- }
-
- const auto &verification = currentVerifications[token];
-
- return QHttpServerResponse("application/json", QJsonDocument(QJsonObject{
- { "confirmed"_L1, verification.isConfirmed(), },
- }).toJson());
- });
+ server.route(u"/verify"_s, &VerificationController::verifyAction);
+ server.route(u"/verify/confirm/web"_s, &VerificationController::verifyConfirmWebAction);
+ server.route(u"/verify/confirm/server"_s, &VerificationController::verifyConfirmServerAction);
+ server.route(u"/verify/status"_s, &VerificationController::verifyStatusAction);
server.afterRequest([](QHttpServerResponse &&resp) {
resp.setHeader("Access-Control-Allow-Origin", "*");
return std::move(resp);
});
const auto port = server.listen(QHostAddress::Any, 5656);
if (!port) {
qWarning() << "Server failed to listen on a port.";
return 1;
}
qWarning() << u"Running on http://127.0.0.1:%1/ (Press CTRL+C to quit)"_s.arg(port);
return app.exec();
}
diff --git a/broker/model/serverstate.cpp b/broker/model/serverstate.cpp
new file mode 100644
index 0000000..f3601fa
--- /dev/null
+++ b/broker/model/serverstate.cpp
@@ -0,0 +1,14 @@
+// SPDX-FileCopyrightText: 2023 g10 code Gmbh
+// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "serverstate.h"
+
+ServerState::ServerState()
+{}
+
+ServerState &ServerState::instance()
+{
+ static ServerState s;
+ return s;
+}
diff --git a/broker/model/serverstate.h b/broker/model/serverstate.h
new file mode 100644
index 0000000..ecbed92
--- /dev/null
+++ b/broker/model/serverstate.h
@@ -0,0 +1,60 @@
+// 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 <QString>
+#include <QHash>
+#include <QNetworkAccessManager>
+
+using Token = QString;
+using ClientId = QString;
+using Email = QString;
+using Port = int;
+
+struct Server {
+ using Id = QString;
+ Id id;
+ Port port;
+};
+
+struct Verification {
+ Verification()
+ {}
+
+ Verification(const QString &_clientId, Server::Id _serverId)
+ : clientId(_clientId)
+ , serverId(_serverId)
+ {}
+
+ bool isEmpty() const {
+ return clientId.isEmpty();
+ }
+
+ bool isConfirmed() const
+ {
+ return webclient && desktopserver;
+ }
+
+ QString clientId;
+ Server::Id serverId;
+ bool webclient = false;
+ bool desktopserver = false;
+};
+
+class ServerState
+{
+public:
+ static ServerState &instance();
+
+ QHash<Email, Server> servers;
+ QHash<Email, QHash<Server::Id, ClientId>> verified;
+
+ QHash<Token, Verification> currentVerifications;
+
+ QNetworkAccessManager qnam;
+
+private:
+ ServerState();
+};
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, May 10, 8:53 AM (1 d, 7 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
c0/9f/416d813fe77cfd1a628782b83b2b
Attached To
rOJ GpgOL.js
Event Timeline
Log In to Comment