aboutsummaryrefslogtreecommitdiff
path: root/server-v1
diff options
context:
space:
mode:
authorrtk0c <[email protected]>2022-06-27 00:13:08 +0000
committerrtk0c <[email protected]>2022-06-27 00:13:08 +0000
commit8a23aa89a58d3a90d5851b449b5552e1fcdcaded (patch)
tree86a527705eafe1ba36d6d5744afeddad196c0eeb /server-v1
parent9ad9af9f2596b91e1dd65e71543f75b0644e8283 (diff)
(From git) Initial server setup
git-svn-id: file:///home/arch/svn/epistmool/trunk@4 71f44415-077c-4ad7-a976-72ddbf76608f
Diffstat (limited to 'server-v1')
-rw-r--r--server-v1/.gitignore5
-rw-r--r--server-v1/CMakeLists.txt29
-rw-r--r--server-v1/conanfile.txt7
-rw-r--r--server-v1/source/EpistmoolServer/CMakeLists.txt10
-rw-r--r--server-v1/source/EpistmoolServer/Connection.cpp150
-rw-r--r--server-v1/source/EpistmoolServer/Connection.hpp50
-rw-r--r--server-v1/source/EpistmoolServer/Protocol/CMakeLists.txt6
-rw-r--r--server-v1/source/EpistmoolServer/Protocol/Command.cpp200
-rw-r--r--server-v1/source/EpistmoolServer/Protocol/Command.hpp124
-rw-r--r--server-v1/source/EpistmoolServer/Protocol/Error.cpp1
-rw-r--r--server-v1/source/EpistmoolServer/Protocol/Error.hpp21
-rw-r--r--server-v1/source/EpistmoolServer/Protocol/Version.hpp9
-rw-r--r--server-v1/source/EpistmoolServer/Protocol/fwd.hpp13
-rw-r--r--server-v1/source/EpistmoolServer/Server.cpp89
-rw-r--r--server-v1/source/EpistmoolServer/Server.hpp28
-rw-r--r--server-v1/source/EpistmoolServer/ServerProperties.cpp1
-rw-r--r--server-v1/source/EpistmoolServer/ServerProperties.hpp9
-rw-r--r--server-v1/source/EpistmoolServer/Session.cpp76
-rw-r--r--server-v1/source/EpistmoolServer/Session.hpp59
-rw-r--r--server-v1/source/EpistmoolServer/Workspace/CMakeLists.txt3
-rw-r--r--server-v1/source/EpistmoolServer/Workspace/fwd.hpp5
-rw-r--r--server-v1/source/EpistmoolServer/fwd.hpp21
-rw-r--r--server-v1/source/all_fwd.hpp3
-rw-r--r--server-v1/source/header_only.cpp1
-rw-r--r--server-v1/source/main.cpp12
25 files changed, 932 insertions, 0 deletions
diff --git a/server-v1/.gitignore b/server-v1/.gitignore
new file mode 100644
index 0000000..f2ae28d
--- /dev/null
+++ b/server-v1/.gitignore
@@ -0,0 +1,5 @@
+.idea/
+build/
+cache/
+CMakeLists.txt.user
+compile_commands.json
diff --git a/server-v1/CMakeLists.txt b/server-v1/CMakeLists.txt
new file mode 100644
index 0000000..d869f82
--- /dev/null
+++ b/server-v1/CMakeLists.txt
@@ -0,0 +1,29 @@
+cmake_minimum_required(VERSION 3.18)
+project(EpistmoolServer LANGUAGES CXX)
+
+option(ENABLE_TESTS "Whether to enable doctest on the executable or not." OFF)
+
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+find_package(Qt6 6.2 COMPONENTS Core Network Sql REQUIRED)
+find_package(doctest REQUIRED)
+
+qt_add_executable(EpistmoolServer
+ source/all_fwd.hpp
+ source/header_only.cpp
+ source/main.cpp
+)
+add_subdirectory(source/EpistmoolServer)
+
+target_include_directories(EpistmoolServer PRIVATE source)
+target_compile_definitions(EpistmoolServer PRIVATE
+ DOCTEST_CONFIG_DISABLE=$<NOT:$<BOOL:${ENABLE_TESTS}>>
+)
+target_link_libraries(EpistmoolServer PRIVATE
+ # Qt6::Core handled for us by qt_add_executable
+ Qt6::Network
+ Qt6::Sql
+ doctest::doctest
+)
diff --git a/server-v1/conanfile.txt b/server-v1/conanfile.txt
new file mode 100644
index 0000000..71dbde4
--- /dev/null
+++ b/server-v1/conanfile.txt
@@ -0,0 +1,7 @@
+[requires]
+doctest/2.4.8
+
+[generators]
+cmake_find_package
+
+[options]
diff --git a/server-v1/source/EpistmoolServer/CMakeLists.txt b/server-v1/source/EpistmoolServer/CMakeLists.txt
new file mode 100644
index 0000000..c98c901
--- /dev/null
+++ b/server-v1/source/EpistmoolServer/CMakeLists.txt
@@ -0,0 +1,10 @@
+target_sources(EpistmoolServer PRIVATE
+ fwd.hpp
+ Connection.hpp Connection.cpp
+ Session.hpp Session.cpp
+ Server.hpp Server.cpp
+ ServerProperties.hpp ServerProperties.cpp
+)
+
+add_subdirectory(Protocol)
+add_subdirectory(Workspace)
diff --git a/server-v1/source/EpistmoolServer/Connection.cpp b/server-v1/source/EpistmoolServer/Connection.cpp
new file mode 100644
index 0000000..59dcf38
--- /dev/null
+++ b/server-v1/source/EpistmoolServer/Connection.cpp
@@ -0,0 +1,150 @@
+#include "Connection.hpp"
+
+#include <QJsonDocument>
+#include <QJsonParseError>
+#include <QLocalServer>
+#include <QLocalSocket>
+#include <QString>
+#include <QtDebug>
+#include <vector>
+
+using namespace Epistmool::Server;
+
+namespace {
+constexpr int kInvalidGeneration = 0;
+} // namespace
+
+ConnectionId ConnectionId::createInvalid()
+{
+ return ConnectionId{
+ .index = 0,
+ .generation = kInvalidGeneration,
+ };
+}
+
+bool ConnectionId::isInvalid() const
+{
+ return generation == kInvalidGeneration;
+}
+
+struct ConnectionManager::Connection
+{
+ enum Type
+ {
+ Empty,
+ LocalSocket,
+ };
+
+ union
+ {
+ QLocalSocket* localSocket;
+ };
+
+ Type type = Empty;
+ int generation = kInvalidGeneration + 1;
+
+ void markEmpty()
+ {
+ type = Empty;
+ }
+};
+
+class ConnectionManager::Private
+{
+public:
+ static int findFreeConnectionObj(ConnectionManager& self)
+ {
+ for (int i = 0; i < self.mConnections.size(); ++i) {
+ auto& conn = self.mConnections[i];
+ if (conn.type == Connection::Empty) {
+ return i;
+ }
+ }
+
+ self.mConnections.push_back({});
+ return self.mConnections.size() - 1;
+ }
+};
+
+ConnectionManager::ConnectionManager(QObject* parent)
+ : QObject(parent)
+{
+}
+
+ConnectionManager::~ConnectionManager() = default;
+
+bool ConnectionManager::isLocalConnectionsEnabled() const
+{
+ return mLocal != nullptr;
+}
+
+void ConnectionManager::setLocalConnectionsEnabled(bool enabled)
+{
+ bool currentlyEnabled = isLocalConnectionsEnabled();
+ if (!currentlyEnabled && enabled) {
+ mLocal = new QLocalServer(this);
+
+ if (!mLocal->listen("queue")) {
+ qFatal("Failed to initialized event queue");
+ }
+
+ connect(mLocal, &QLocalServer::newConnection, mLocal, [&]() {
+ auto socket = mLocal->nextPendingConnection();
+
+ ConnectionId connId;
+ connId.index = Private::findFreeConnectionObj(*this);
+ auto& connObj = mConnections[connId.index];
+ connId.generation = connObj.generation++;
+ connObj.type = Connection::LocalSocket;
+ connObj.localSocket = socket;
+
+ connect(socket, &QLocalSocket::disconnected, socket, &QObject::deleteLater);
+ connect(socket, &QLocalSocket::disconnected, this, [this, connId]() {
+ mConnections[connId.index].markEmpty();
+ });
+ connect(socket, &QLocalSocket::readyRead, this, [this, socket, connId]() {
+ while (socket->canReadLine()) {
+ auto line = socket->readLine();
+
+ QJsonParseError error;
+ auto msg = QJsonDocument::fromJson(line, &error);
+ if (error.error != QJsonParseError::NoError) {
+ qWarning() << "Invalid message from client: " << error.errorString();
+ qWarning() << "Message: " << QString(line);
+ } else {
+ qDebug() << QString(line);
+ }
+
+ emit messageRecieved(msg, connId);
+ }
+ });
+
+ emit connectionEstablished(connId);
+ });
+ } else if (currentlyEnabled && !enabled) {
+ mLocal->deleteLater();
+ mLocal = nullptr;
+ }
+}
+
+void ConnectionManager::replyMessage(ConnectionId id, const QJsonDocument& message)
+{
+ if (id.index < 0 || id.index >= mConnections.size()) {
+ return;
+ }
+
+ auto& conn = mConnections[id.index];
+ if (id.generation != conn.generation) {
+ return;
+ }
+
+ switch (conn.type) {
+ case Connection::Empty: return;
+
+ case Connection::LocalSocket: {
+ auto socket = conn.localSocket;
+ socket->write(message.toJson(QJsonDocument::Compact));
+ socket->flush();
+ } break;
+ }
+}
diff --git a/server-v1/source/EpistmoolServer/Connection.hpp b/server-v1/source/EpistmoolServer/Connection.hpp
new file mode 100644
index 0000000..78e34db
--- /dev/null
+++ b/server-v1/source/EpistmoolServer/Connection.hpp
@@ -0,0 +1,50 @@
+#pragma once
+
+#include <QObject>
+#include <vector>
+
+class QLocalServer;
+class QJsonDocument;
+
+namespace Epistmool::Server {
+
+struct ConnectionId
+{
+ int index;
+ int generation;
+
+ static ConnectionId createInvalid();
+ bool isInvalid() const;
+
+ bool operator==(const ConnectionId&) const = default;
+};
+
+uint qHash(const ConnectionId& id, uint seed = 0);
+
+class ConnectionManager : public QObject
+{
+ Q_OBJECT
+ class Private;
+
+private:
+ QLocalServer* mLocal = nullptr;
+
+ struct Connection;
+ std::vector<Connection> mConnections;
+
+public:
+ explicit ConnectionManager(QObject* parent = nullptr);
+ ~ConnectionManager();
+
+ bool isLocalConnectionsEnabled() const;
+ void setLocalConnectionsEnabled(bool enabled);
+
+ void replyMessage(ConnectionId id, const QJsonDocument& message);
+
+signals:
+ void connectionEstablished(Epistmool::Server::ConnectionId id);
+ void connectionDropped(Epistmool::Server::ConnectionId id);
+ void messageRecieved(const QJsonDocument& message, Epistmool::Server::ConnectionId id);
+};
+
+} // namespace Epistmool::Server
diff --git a/server-v1/source/EpistmoolServer/Protocol/CMakeLists.txt b/server-v1/source/EpistmoolServer/Protocol/CMakeLists.txt
new file mode 100644
index 0000000..54f1da7
--- /dev/null
+++ b/server-v1/source/EpistmoolServer/Protocol/CMakeLists.txt
@@ -0,0 +1,6 @@
+target_sources(EpistmoolServer PRIVATE
+ fwd.hpp
+ Command.hpp Command.cpp
+ Error.hpp Error.cpp
+ Version.hpp
+)
diff --git a/server-v1/source/EpistmoolServer/Protocol/Command.cpp b/server-v1/source/EpistmoolServer/Protocol/Command.cpp
new file mode 100644
index 0000000..a108e0e
--- /dev/null
+++ b/server-v1/source/EpistmoolServer/Protocol/Command.cpp
@@ -0,0 +1,200 @@
+#include "Command.hpp"
+
+#include <QJsonObject>
+#include <QMetaEnum>
+#include <array>
+
+using namespace Epistmool::Server;
+
+namespace {
+auto kKindTraits = []() {
+ using enum ProtocolMessage::Kind;
+ std::array<ProtocolMessage::KindTrait, static_cast<size_t>(KindCOUNT)> array;
+
+ array[SessionAuth] = { .isC2S = true, .hasReply = true };
+ array[SessionDestroy] = { .isC2S = true, .hasReply = true };
+
+ array[WorkspaceCreate] = { .isC2S = true, .hasReply = true };
+ array[WorkspaceOpen] = { .isC2S = true, .hasReply = true };
+ array[WorkspaceFetchIndex] = { .isC2S = true, .hasReply = true };
+
+ array[WorkspaceFetchKnowledge] = { .isC2S = true, .hasReply = true };
+ array[WorkspaceUpdateKnowledge] = { .isC2S = true, .hasReply = true };
+ array[WorkspaceCreateKnowledge] = { .isC2S = true, .hasReply = true };
+ array[WorkspaceDeleteKnowledge] = { .isC2S = true, .hasReply = true };
+
+ array[WorkspaceFetchKeyword] = { .isC2S = true, .hasReply = true };
+ array[WorkspaceUpdateKeyword] = { .isC2S = true, .hasReply = true };
+ array[WorkspaceCreateKeyword] = { .isC2S = true, .hasReply = true };
+ array[WorkspaceDeleteKeyword] = { .isC2S = true, .hasReply = true };
+
+ array[NotificationWorkspaceUpdated] = { .isC2S = false, .hasReply = false };
+ array[NotificationKnowledgeUpdated] = { .isC2S = false, .hasReply = false };
+
+ return array;
+}();
+}
+
+std::unique_ptr<ProtocolRequest> ProtocolMessage::createRequest(Kind kind)
+{
+ switch(kind) {
+ case SessionAuth: return std::make_unique<ProtocolRequest_SessionAuth>();
+ case SessionDestroy: return nullptr;
+
+ case WorkspaceCreate: return nullptr;
+ case WorkspaceOpen: return nullptr;
+ case WorkspaceFetchIndex: return nullptr;
+
+ case WorkspaceFetchKnowledge: return nullptr;
+ case WorkspaceUpdateKnowledge: return nullptr;
+ case WorkspaceCreateKnowledge: return nullptr;
+ case WorkspaceDeleteKnowledge: return nullptr;
+
+ case WorkspaceFetchKeyword: return nullptr;
+ case WorkspaceUpdateKeyword: return nullptr;
+ case WorkspaceCreateKeyword: return nullptr;
+ case WorkspaceDeleteKeyword: return nullptr;
+
+ case NotificationWorkspaceUpdated: return nullptr;
+ case NotificationKnowledgeUpdated: return nullptr;
+
+ default: return nullptr;
+ }
+}
+
+std::unique_ptr<ProtocolReply> ProtocolMessage::createReply(Kind kind)
+{
+ switch(kind) {
+ case SessionAuth: return std::make_unique<ProtocolReply_SessionAuth>();
+ case SessionDestroy: return nullptr;
+
+ case WorkspaceCreate: return nullptr;
+ case WorkspaceOpen: return nullptr;
+ case WorkspaceFetchIndex: return nullptr;
+
+ case WorkspaceFetchKnowledge: return nullptr;
+ case WorkspaceUpdateKnowledge: return nullptr;
+ case WorkspaceCreateKnowledge: return nullptr;
+ case WorkspaceDeleteKnowledge: return nullptr;
+
+ case WorkspaceFetchKeyword: return nullptr;
+ case WorkspaceUpdateKeyword: return nullptr;
+ case WorkspaceCreateKeyword: return nullptr;
+ case WorkspaceDeleteKeyword: return nullptr;
+
+ case NotificationWorkspaceUpdated: return nullptr;
+ case NotificationKnowledgeUpdated: return nullptr;
+
+ default: return nullptr;
+ }
+}
+
+const ProtocolMessage::KindTrait& ProtocolMessage::getKindTrait(Kind kind)
+{
+ return kKindTraits[kind];
+}
+
+ProtocolMessage::ProtocolMessage(Kind kind, Variant variant)
+ : kind{ kind }
+ , variant{ variant }
+{
+}
+
+ProtocolRequest::ProtocolRequest(Kind kind)
+ : ProtocolMessage(kind, RequestVariant)
+{
+}
+
+QJsonObject ProtocolRequest::serialize(const ProtocolRequest& msg)
+{
+ QJsonObject object;
+ object.insert("method", QMetaEnum::fromType<Kind>().valueToKey(msg.kind));
+
+ QJsonObject fields;
+ msg.serializeFields(fields);
+
+ return object;
+}
+
+std::unique_ptr<ProtocolRequest> ProtocolRequest::deserialize(const QJsonObject& object)
+{
+ bool ok;
+ auto methodName = object.value("method").toString().toStdString();
+ auto method = static_cast<Kind>(QMetaEnum::fromType<Kind>().keysToValue(methodName.c_str(), &ok));
+ if (!ok) return nullptr;
+
+ auto msg = createRequest(method);
+
+ auto fieldsVal = object.value("fields");
+ if (!fieldsVal.isObject()) return nullptr;
+ msg->deserializeFields(fieldsVal.toObject());
+
+ return msg;
+}
+
+ProtocolReply::ProtocolReply(Kind kind)
+ : ProtocolMessage(kind, ReplyVariant)
+{
+}
+
+QJsonObject ProtocolReply::serialize(const ProtocolReply& msg)
+{
+ QJsonObject object;
+ object.insert("method", QMetaEnum::fromType<Kind>().valueToKey(msg.kind));
+ object.insert("sequence", msg.sequence);
+
+ QJsonObject fields;
+ msg.serializeFields(fields);
+
+ return object;
+}
+
+std::unique_ptr<ProtocolReply> ProtocolReply::deserialize(const QJsonObject& object)
+{
+ bool ok;
+ auto methodName = object.value("method").toString().toStdString();
+ auto method = static_cast<Kind>(QMetaEnum::fromType<Kind>().keysToValue(methodName.c_str(), &ok));
+ if (!ok) return nullptr;
+
+ auto msg = createReply(method);
+
+ msg->sequence = object.value("sequence").toInt(-1);
+
+ auto fieldsVal = object.value("fields");
+ if (!fieldsVal.isObject()) return nullptr;
+ msg->deserializeFields(fieldsVal.toObject());
+
+ return msg;
+}
+
+ProtocolRequest_SessionAuth::ProtocolRequest_SessionAuth()
+ : ProtocolRequest(SessionAuth) {}
+
+void ProtocolRequest_SessionAuth::serializeFields(QJsonObject& object) const
+{
+ ProtocolRequest::serializeFields(object);
+ object.insert("session", theSession);
+ object.insert("createIfInvalid", createIfInvalid);
+}
+
+void ProtocolRequest_SessionAuth::deserializeFields(const QJsonObject& object)
+{
+ ProtocolRequest::deserializeFields(object);
+ theSession = object.value("session").toInt();
+ createIfInvalid = object.value("createIfInvalid").toBool();
+}
+
+ProtocolReply_SessionAuth::ProtocolReply_SessionAuth()
+ : ProtocolReply(SessionAuth) {}
+
+void ProtocolReply_SessionAuth::serializeFields(QJsonObject& object) const
+{
+ ProtocolReply::serializeFields(object);
+ object.insert("session", theSession);
+}
+
+void ProtocolReply_SessionAuth::deserializeFields(const QJsonObject& object)
+{
+ ProtocolReply::deserializeFields(object);
+ theSession = object.value("session").toInt();
+}
diff --git a/server-v1/source/EpistmoolServer/Protocol/Command.hpp b/server-v1/source/EpistmoolServer/Protocol/Command.hpp
new file mode 100644
index 0000000..9ec9786
--- /dev/null
+++ b/server-v1/source/EpistmoolServer/Protocol/Command.hpp
@@ -0,0 +1,124 @@
+#pragma once
+
+#include "all_fwd.hpp"
+
+#include <QLatin1String>
+#include <QVersionNumber>
+#include <cstddef>
+#include <memory>
+
+class QJsonObject;
+
+namespace Epistmool::Server {
+
+// Note: not all message kinds have a corresponding reply format. In such case the Kind enum is prefixed with 'Notification'.
+class ProtocolMessage
+{
+ Q_GADGET
+
+public:
+ struct KindTrait
+ {
+ bool isC2S;
+ bool hasReply;
+ };
+
+ enum Kind
+ {
+ SessionAuth,
+ SessionDestroy,
+
+ WorkspaceCreate,
+ WorkspaceOpen,
+ WorkspaceFetchIndex,
+
+ WorkspaceFetchKnowledge,
+ WorkspaceUpdateKnowledge,
+ WorkspaceCreateKnowledge,
+ WorkspaceDeleteKnowledge,
+
+ WorkspaceFetchKeyword,
+ WorkspaceUpdateKeyword,
+ WorkspaceCreateKeyword,
+ WorkspaceDeleteKeyword,
+
+ NotificationWorkspaceUpdated,
+ NotificationKnowledgeUpdated,
+
+ KindCOUNT,
+ };
+ Q_ENUM(Kind)
+
+ static std::unique_ptr<ProtocolRequest> createRequest(Kind kind);
+ static std::unique_ptr<ProtocolReply> createReply(Kind kind);
+ static const KindTrait& getKindTrait(Kind kind);
+
+ enum Variant
+ {
+ RequestVariant,
+ ReplyVariant,
+ };
+ Q_ENUM(Variant)
+
+public:
+ Kind kind;
+ Variant variant;
+
+public:
+ ProtocolMessage(Kind kind, Variant variant);
+ virtual ~ProtocolMessage() = default;
+
+protected:
+ virtual void serializeFields(QJsonObject& object) const = 0;
+ virtual void deserializeFields(const QJsonObject& object) = 0;
+};
+
+struct ProtocolRequest : public ProtocolMessage
+{
+ ProtocolRequest(Kind kind);
+ static QJsonObject serialize(const ProtocolRequest& msg);
+ static std::unique_ptr<ProtocolRequest> deserialize(const QJsonObject& object);
+};
+
+struct ProtocolReply : public ProtocolMessage
+{
+ int sequence;
+
+ ProtocolReply(Kind kind);
+ static QJsonObject serialize(const ProtocolReply& msg);
+ static std::unique_ptr<ProtocolReply> deserialize(const QJsonObject& object);
+};
+
+// ===========================
+// Individual messages classes
+
+struct ProtocolRequest_SessionAuth : public ProtocolRequest
+{
+ int theSession;
+ bool createIfInvalid;
+
+ ProtocolRequest_SessionAuth();
+
+protected:
+ virtual void serializeFields(QJsonObject& object) const override;
+ virtual void deserializeFields(const QJsonObject& object) override;
+};
+
+struct ProtocolReply_SessionAuth : public ProtocolReply
+{
+ /// The same value as provided in the request message.
+ /// If \l ProcotolCommandSessionAuth::createIfValid is set and the given session is invalid, a new session is created and written here instead of the original value.
+ int theSession;
+
+ ProtocolReply_SessionAuth();
+
+protected:
+ virtual void serializeFields(QJsonObject& object) const override;
+ virtual void deserializeFields(const QJsonObject& object) override;
+};
+
+struct ProtocolNotification_WorkspaceUpdate : public ProtocolRequest
+{
+};
+
+} // namespace Epistmool::Server
diff --git a/server-v1/source/EpistmoolServer/Protocol/Error.cpp b/server-v1/source/EpistmoolServer/Protocol/Error.cpp
new file mode 100644
index 0000000..5bf4840
--- /dev/null
+++ b/server-v1/source/EpistmoolServer/Protocol/Error.cpp
@@ -0,0 +1 @@
+#include "Error.hpp"
diff --git a/server-v1/source/EpistmoolServer/Protocol/Error.hpp b/server-v1/source/EpistmoolServer/Protocol/Error.hpp
new file mode 100644
index 0000000..1ddedf9
--- /dev/null
+++ b/server-v1/source/EpistmoolServer/Protocol/Error.hpp
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "EpistmoolServer/Protocol/Version.hpp"
+
+#include <QLatin1String>
+#include <QVersionNumber>
+
+namespace Epistmool::Server {
+struct ProtocolError
+{
+ QVersionNumber since;
+ QLatin1String name;
+};
+
+namespace ProtocolErrors {
+ const ProtocolError kUnsupportedVersion{
+ .since = ProtocolVersions::v0_1,
+ .name = QLatin1String("unsupportedVersion"),
+ };
+}
+} // namespace Epistmool::Server
diff --git a/server-v1/source/EpistmoolServer/Protocol/Version.hpp b/server-v1/source/EpistmoolServer/Protocol/Version.hpp
new file mode 100644
index 0000000..aae72bd
--- /dev/null
+++ b/server-v1/source/EpistmoolServer/Protocol/Version.hpp
@@ -0,0 +1,9 @@
+#pragma once
+
+#include <QVersionNumber>
+
+namespace Epistmool::Server {
+namespace ProtocolVersions {
+ const QVersionNumber v0_1(0, 1);
+}
+} // namespace Epistmool::Server
diff --git a/server-v1/source/EpistmoolServer/Protocol/fwd.hpp b/server-v1/source/EpistmoolServer/Protocol/fwd.hpp
new file mode 100644
index 0000000..adf8138
--- /dev/null
+++ b/server-v1/source/EpistmoolServer/Protocol/fwd.hpp
@@ -0,0 +1,13 @@
+#pragma once
+
+namespace Epistmool::Server{
+
+// Command.hpp
+class ProtocolMessage;
+struct ProtocolRequest;
+struct ProtocolReply;
+
+// Error.hpp
+struct ProtocolError;
+
+} // namespace Epistmool::Server
diff --git a/server-v1/source/EpistmoolServer/Server.cpp b/server-v1/source/EpistmoolServer/Server.cpp
new file mode 100644
index 0000000..4dc3b16
--- /dev/null
+++ b/server-v1/source/EpistmoolServer/Server.cpp
@@ -0,0 +1,89 @@
+#include "Server.hpp"
+
+#include "EpistmoolServer/Protocol/Error.hpp"
+#include "EpistmoolServer/ServerProperties.hpp"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonValue>
+#include <QLatin1String>
+#include <QVersionNumber>
+
+using namespace Epistmool::Server;
+
+// TODO structured error handling similar to messages
+class Server::Private
+{
+public:
+ static bool messageHeader(QJsonObject& msg, SessionId sessionId, int id)
+ {
+ msg.insert("session", sessionId.index);
+ msg.insert("id", id);
+ return true;
+ }
+
+ static bool messageErrInvalidVersion(QJsonObject& msg)
+ {
+ QJsonObject error;
+ error.insert("type", ProtocolErrors::kUnsupportedVersion.name);
+ error.insert("supportedVersion", ServerProperties::kVersion.toString());
+
+ msg.insert("error", error);
+
+ return true;
+ }
+};
+
+Server::Server(QObject* parent)
+ : QObject(parent)
+{
+ mConnectionManager.setLocalConnectionsEnabled(true);
+
+ connect(&mConnectionManager, &ConnectionManager::messageRecieved, this, &Server::onMessage);
+ connect(&mConnectionManager, &ConnectionManager::connectionDropped, &mSessionManager, &SessionManager::dropConnection);
+}
+
+void Server::onMessage(const QJsonDocument& message, ConnectionId connId)
+{
+ if (!message.isObject()) return;
+ auto root = message.object();
+
+ // Extract as empty string if non-existent
+ auto versionString = root.value("version").toString();
+
+ int id = root.value("id").toInt();
+
+ SessionId sessionId;
+ if (auto index = root.value("session").toInt(-1); index == -1) {
+ sessionId = SessionId::makeInvalid();
+ } else {
+ sessionId.index = index;
+ }
+
+ auto version = [&]() -> QVersionNumber {
+ if (versionString.isEmpty()) {
+ return QVersionNumber();
+ } else {
+ return QVersionNumber::fromString(versionString).normalized();
+ }
+ }();
+ if (version.isNull() || version >= ServerProperties::kVersion) {
+ qWarning() << "Message recieved with invalid version " << versionString;
+
+ QJsonObject msg;
+ Private::messageHeader(msg, sessionId, id);
+ Private::messageErrInvalidVersion(msg);
+ mConnectionManager.replyMessage(connId, QJsonDocument(msg));
+
+ return;
+ }
+
+ Session* session;
+ if (sessionId.isInvalid()) {
+ session = nullptr;
+ } else {
+ session = mSessionManager.findSession(sessionId);
+ }
+
+ // TODO dispatch message
+}
diff --git a/server-v1/source/EpistmoolServer/Server.hpp b/server-v1/source/EpistmoolServer/Server.hpp
new file mode 100644
index 0000000..5c2c6d2
--- /dev/null
+++ b/server-v1/source/EpistmoolServer/Server.hpp
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "EpistmoolServer/Connection.hpp"
+#include "EpistmoolServer/Session.hpp"
+#include "all_fwd.hpp"
+
+#include <QJsonDocument>
+#include <QObject>
+
+namespace Epistmool::Server {
+
+class Server : public QObject
+{
+ Q_OBJECT
+ class Private;
+
+private:
+ ConnectionManager mConnectionManager;
+ SessionManager mSessionManager;
+
+public:
+ explicit Server(QObject* parent = nullptr);
+
+public slots:
+ void onMessage(const QJsonDocument& message, Epistmool::Server::ConnectionId connId);
+};
+
+} // namespace Epistmool::Server
diff --git a/server-v1/source/EpistmoolServer/ServerProperties.cpp b/server-v1/source/EpistmoolServer/ServerProperties.cpp
new file mode 100644
index 0000000..dc16b4b
--- /dev/null
+++ b/server-v1/source/EpistmoolServer/ServerProperties.cpp
@@ -0,0 +1 @@
+#include "ServerProperties.hpp"
diff --git a/server-v1/source/EpistmoolServer/ServerProperties.hpp b/server-v1/source/EpistmoolServer/ServerProperties.hpp
new file mode 100644
index 0000000..5865442
--- /dev/null
+++ b/server-v1/source/EpistmoolServer/ServerProperties.hpp
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "EpistmoolServer/Protocol/Version.hpp"
+
+namespace Epistmool::Server {
+namespace ServerProperties {
+ const auto kVersion = ProtocolVersions::v0_1;
+}
+} // namespace Epistmool::Server
diff --git a/server-v1/source/EpistmoolServer/Session.cpp b/server-v1/source/EpistmoolServer/Session.cpp
new file mode 100644
index 0000000..6db51d8
--- /dev/null
+++ b/server-v1/source/EpistmoolServer/Session.cpp
@@ -0,0 +1,76 @@
+#include "Session.hpp"
+
+using namespace Epistmool::Server;
+
+namespace {
+constexpr int kInvalidSessionIndex = -1;
+} // namespace
+
+SessionId SessionId::makeInvalid()
+{
+ return SessionId{
+ .index = kInvalidSessionIndex,
+ };
+}
+
+bool SessionId::isInvalid() const
+{
+ return index == kInvalidSessionIndex;
+}
+
+SessionId Session::getSessionId() const
+{
+ return mSessionId;
+}
+
+const std::vector<ConnectionId>& Session::getConnections() const
+{
+ return mConnections;
+}
+
+Session::Session(SessionId id)
+ : mSessionId{ id }
+{
+}
+
+// TODO delete session if isn't active for N period of time
+SessionManager::SessionManager(QObject* parent)
+ : QObject(parent)
+{
+}
+
+Session* SessionManager::findOrCreateSession(SessionId id)
+{
+ auto it = mSessions.find(id.index);
+ if (it != mSessions.end()) {
+ return &it->second;
+ } else {
+ SessionId newId{ mNextIndex++ };
+ auto [it, _] = mSessions.insert({ newId.index, Session(newId) });
+ return &it->second;
+ }
+}
+
+Session* SessionManager::findSession(SessionId id)
+{
+ auto it = mSessions.find(id.index);
+ if (it != mSessions.end()) {
+ return &it->second;
+ } else {
+ return nullptr;
+ }
+}
+
+void SessionManager::addConnection(SessionId id, ConnectionId connId)
+{
+ auto session = findSession(id);
+ if (!session) return;
+
+ session->mConnections.push_back(connId);
+ mConnection2SessionMap.insert(connId.index, id.index);
+}
+
+void SessionManager::dropConnection(ConnectionId connId)
+{
+ mConnection2SessionMap.remove(connId.index);
+}
diff --git a/server-v1/source/EpistmoolServer/Session.hpp b/server-v1/source/EpistmoolServer/Session.hpp
new file mode 100644
index 0000000..931c428
--- /dev/null
+++ b/server-v1/source/EpistmoolServer/Session.hpp
@@ -0,0 +1,59 @@
+#pragma once
+
+#include "EpistmoolServer/Connection.hpp"
+
+#include <QMultiHash>
+#include <QObject>
+#include <unordered_map>
+#include <vector>
+
+namespace Epistmool::Server {
+
+struct SessionId
+{
+ int index;
+
+ static SessionId makeInvalid();
+ bool isInvalid() const;
+};
+
+class Session
+{
+ friend class SessionManager;
+
+private:
+ std::vector<ConnectionId> mConnections;
+ SessionId mSessionId;
+
+public:
+ Session(SessionId id);
+
+ SessionId getSessionId() const;
+ const std::vector<ConnectionId>& getConnections() const;
+};
+
+class SessionManager : public QObject
+{
+ Q_OBJECT
+
+ friend class Session;
+
+private:
+ // Use this intead of QHash for 1. pointer stability 2. Session is a move-only type
+ std::unordered_map<int, Session> mSessions;
+ QMultiHash<int, int> mConnection2SessionMap;
+ int mNextIndex;
+
+public:
+ explicit SessionManager(QObject* parent = nullptr);
+
+ Session* findOrCreateSession(SessionId id);
+ Session* findSession(SessionId id);
+
+ void addConnection(SessionId id, ConnectionId connId);
+
+public slots:
+ void dropConnection(Epistmool::Server::ConnectionId connId);
+};
+
+} // namespace Epistmool::Server
diff --git a/server-v1/source/EpistmoolServer/Workspace/CMakeLists.txt b/server-v1/source/EpistmoolServer/Workspace/CMakeLists.txt
new file mode 100644
index 0000000..6ef6d59
--- /dev/null
+++ b/server-v1/source/EpistmoolServer/Workspace/CMakeLists.txt
@@ -0,0 +1,3 @@
+target_sources(EpistmoolServer PRIVATE
+ fwd.hpp
+)
diff --git a/server-v1/source/EpistmoolServer/Workspace/fwd.hpp b/server-v1/source/EpistmoolServer/Workspace/fwd.hpp
new file mode 100644
index 0000000..dd572f8
--- /dev/null
+++ b/server-v1/source/EpistmoolServer/Workspace/fwd.hpp
@@ -0,0 +1,5 @@
+#pragma once
+
+namespace Epistmool::Server
+{
+} // namespace Epistmool::Server
diff --git a/server-v1/source/EpistmoolServer/fwd.hpp b/server-v1/source/EpistmoolServer/fwd.hpp
new file mode 100644
index 0000000..dfbcb91
--- /dev/null
+++ b/server-v1/source/EpistmoolServer/fwd.hpp
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "./Protocol/fwd.hpp"
+#include "./Workspace/fwd.hpp"
+
+namespace Epistmool::Server
+{
+
+// Connection.hpp
+struct ConnectionId;
+class ConnectionManager;
+
+// Server.hpp
+class Server;
+
+// Session.hpp
+struct SessionId;
+class Session;
+class SessionManager;
+
+} // namespace Epistmool::Server
diff --git a/server-v1/source/all_fwd.hpp b/server-v1/source/all_fwd.hpp
new file mode 100644
index 0000000..6faf7e4
--- /dev/null
+++ b/server-v1/source/all_fwd.hpp
@@ -0,0 +1,3 @@
+#pragma once
+
+#include "./EpistmoolServer/fwd.hpp"
diff --git a/server-v1/source/header_only.cpp b/server-v1/source/header_only.cpp
new file mode 100644
index 0000000..83e2263
--- /dev/null
+++ b/server-v1/source/header_only.cpp
@@ -0,0 +1 @@
+// This file includes all header only libraries' implementation parts
diff --git a/server-v1/source/main.cpp b/server-v1/source/main.cpp
new file mode 100644
index 0000000..73e4006
--- /dev/null
+++ b/server-v1/source/main.cpp
@@ -0,0 +1,12 @@
+#include "EpistmoolServer/Server.hpp"
+
+#include <QCoreApplication>
+
+using namespace Epistmool::Server;
+
+int main(int argc, char* argv[])
+{
+ QCoreApplication app(argc, argv);
+ Server server;
+ return app.exec();
+}