diff --git a/server/src/snapshots/channel.cpp b/server/src/snapshots/channel.cpp new file mode 100644 index 0000000..370d8a8 --- /dev/null +++ b/server/src/snapshots/channel.cpp @@ -0,0 +1,60 @@ +// +// Created by WolverinDEV on 11/04/2020. +// + +#include "channel.h" + +using namespace ts::server::snapshots; + +bool channel_parser::parse(std::string &error, channel_entry &channel, size_t &offset) { + auto data = this->command.bulk(offset++); + channel.properties.register_property_type(); + + std::optional channel_id{}; + std::optional parent_channel_id{}; + + size_t entry_index{0}; + std::string_view key{}; + std::string value{}; + while(data.next_entry(entry_index, key, value)) { + if(key == "begin_channels") + continue; + else if(key == "channel_id") { + char* end_ptr{nullptr}; + channel_id = strtoull(value.c_str(), &end_ptr, 10); + if (*end_ptr) { + error = "failed to parse channel id at character " + std::to_string(data.key_command_character_index(key) + key.length()); + return false; + } + } else if(key == "channel_pid") { + char* end_ptr{nullptr}; + parent_channel_id = strtoull(value.c_str(), &end_ptr, 10); + if (*end_ptr) { + error = "failed to parse channel parent id at character " + std::to_string(data.key_command_character_index(key) + key.length()); + return false; + } + } else { + const auto& property = property::find(key); + if(property.is_undefined()) { + //TODO: Issue a warning + continue; + } + + //TODO: Validate value + channel.properties[property] = value; + } + } + + if(!channel_id.has_value()) { + error = "channel entry at character index " + std::to_string(data.command_character_index()) + " misses a channel id"; + return false; + } + + if(!parent_channel_id.has_value()) { + error = "channel entry at character index " + std::to_string(data.command_character_index()) + " misses a channel parent id"; + return false; + } + channel.properties[property::CHANNEL_ID] = *channel_id; + channel.properties[property::CHANNEL_PID] = *parent_channel_id; + return true; +} \ No newline at end of file diff --git a/server/src/snapshots/channel.h b/server/src/snapshots/channel.h new file mode 100644 index 0000000..c32e837 --- /dev/null +++ b/server/src/snapshots/channel.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include +#include +#include "./snapshot.h" + +namespace ts::server::snapshots { + struct channel_entry { + Properties properties{}; + }; + + class channel_parser : public parser { + public: + channel_parser(type type_, version_t version, const command_parser& command) : parser{type_, version, command} {} + + bool parse( + std::string& /* error */, + channel_entry& /* result */, + size_t& /* offset */) override; + }; +} \ No newline at end of file diff --git a/server/src/snapshots/client.cpp b/server/src/snapshots/client.cpp new file mode 100644 index 0000000..12634e5 --- /dev/null +++ b/server/src/snapshots/client.cpp @@ -0,0 +1,108 @@ +// +// Created by WolverinDEV on 11/04/2020. +// + +#include "client.h" + +using namespace ts::server::snapshots; + +bool client_parser::parse(std::string &error, client_entry &client, size_t &offset) { + bool key_found; + auto data = this->command.bulk(offset++); + + { + auto value_string = data.value("client_id", key_found); + if(!key_found) { + error = "missing id for client entry at character " + std::to_string(data.command_character_index()); + return false; + } + + char* end_ptr{nullptr}; + client.database_id = strtoll(value_string.c_str(), &end_ptr, 10); + if (*end_ptr) { + error = "unparsable id for client entry at character " + std::to_string(data.key_command_character_index("client_id") + 9); + return false; + } + } + + { + auto value_string = data.value("client_created", key_found); + if(!key_found) { + error = "missing created timestamp for client entry at character " + std::to_string(data.command_character_index()); + return false; + } + + char* end_ptr{nullptr}; + auto value = strtoll(value_string.c_str(), &end_ptr, 10); + if (*end_ptr) { + error = "unparsable created timestamp for client entry at character " + std::to_string(data.key_command_character_index("client_created") + 14); + return false; + } + client.timestamp_created = std::chrono::system_clock::time_point{} + std::chrono::seconds{value}; + } + + /* optional */ + { + auto value_string = data.value("client_lastconnected", key_found); + if(key_found) { + char* end_ptr{nullptr}; + auto value = strtoll(value_string.c_str(), &end_ptr, 10); + if (*end_ptr) { + error = "unparsable last connected timestamp for client entry at character " + std::to_string(data.key_command_character_index("client_lastconnected") + 20); + return false; + } + client.timestamp_last_connected = std::chrono::system_clock::time_point{} + std::chrono::seconds{value}; + } else { + client.timestamp_last_connected = std::chrono::system_clock::time_point{}; + } + } + + /* optional */ + { + auto value_string = data.value("client_totalconnections", key_found); + if(key_found) { + char* end_ptr{nullptr}; + client.client_total_connections = strtoll(value_string.c_str(), &end_ptr, 10); + if (*end_ptr) { + error = "unparsable total connection count for client entry at character " + std::to_string(data.key_command_character_index("client_totalconnections") + 23); + return false; + } + } else { + client.client_total_connections = 0; + } + } + + client.unique_id = data.value("client_unique_id", key_found); + if(!key_found) { + error = "missing unique id for client entry at character " + std::to_string(data.command_character_index()); + return false; + } + + client.nickname = data.value("client_nickname", key_found); + if(!key_found) { + error = "missing nickname for client entry at character " + std::to_string(data.command_character_index()); + return false; + } + + client.description = data.value("client_description", key_found); + if(!key_found) { + error = "missing description for client entry at character " + std::to_string(data.command_character_index()); + return false; + } + + return true; +} + +bool client_writer::write(std::string &error, size_t &offset, const client_entry &client) { + auto data = this->command.bulk(offset++); + data.put_unchecked("client_id", client.database_id); + data.put_unchecked("client_unique_id", client.unique_id); + data.put_unchecked("client_nickname", client.nickname); + data.put_unchecked("client_description", client.description); + data.put_unchecked("client_created", std::chrono::floor(client.timestamp_created.time_since_epoch()).count()); + data.put_unchecked("client_lastconnected", std::chrono::floor(client.timestamp_last_connected.time_since_epoch()).count()); + data.put_unchecked("client_totalconnections", client.client_total_connections); + if(this->type_ == type::TEAMSPEAK) + data.put_unchecked("client_unread_messages", "0"); + return true; +} \ No newline at end of file diff --git a/server/src/snapshots/client.h b/server/src/snapshots/client.h new file mode 100644 index 0000000..013a1e6 --- /dev/null +++ b/server/src/snapshots/client.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include "./snapshot.h" + +namespace ts::server::snapshots { + struct client_entry { + ClientDbId database_id; + std::string unique_id; + std::string nickname; + std::string description; + + std::chrono::system_clock::time_point timestamp_created; + std::chrono::system_clock::time_point timestamp_last_connected; + size_t client_total_connections; + }; + + class client_parser : public parser { + public: + client_parser(type type_, version_t version, const command_parser& command) : parser{type_, version, command} {} + + bool parse( + std::string& /* error */, + client_entry& /* result */, + size_t& /* offset */) override; + }; + + class client_writer : public writer { + public: + client_writer(type type_, version_t version, command_builder& command) : writer{type_, version, command} {} + + bool write(std::string &, size_t &, const client_entry &) override; + }; +} \ No newline at end of file diff --git a/server/src/snapshots/deploy.cpp b/server/src/snapshots/deploy.cpp new file mode 100644 index 0000000..55807d2 --- /dev/null +++ b/server/src/snapshots/deploy.cpp @@ -0,0 +1,374 @@ +// +// Created by WolverinDEV on 11/04/2020. +// +#include "./snapshot.h" +#include "./server.h" +#include "./channel.h" +#include "./permission.h" +#include "./client.h" +#include "./groups.h" +#include "../VirtualServerManager.h" +#include + +using namespace ts; +using namespace ts::server; +using SnapshotType = ts::server::snapshots::type; +using SnapshotVersion = ts::server::snapshots::version_t; + +bool VirtualServerManager::deploy_snapshot(std::string &error, ServerId server_id, const command_parser &data) { + if(data.bulk(0).has_key("version")) { + return this->deploy_ts3_snapshot(error, server_id, data); + } else if(data.bulk(1).has_key("snapshot_version")) { + /* teaspeak snapshot */ + return this->deploy_teaspeak_snapshot(error, server_id, data); + } else { + /* old TS3 snapshot format */ + return this->deploy_ts3_snapshot(error, server_id, data); + } +} + +bool VirtualServerManager::deploy_teaspeak_snapshot(std::string &error, ts::ServerId server_id, const ts::command_parser &data) { + if(!data.bulk(1).has_key("snapshot_version")) { + error = "Missing snapshot version"; + return false; + } + auto version = data.bulk(1).value_as("snapshot_version"); + auto hash = data.bulk(0).value("hash"); + + if(version < 1) { + error = "snapshot version too old"; + return false; + } else if(version > 2) { + error = "snapshot version is too new"; + return false; + } + + /* the actual snapshot begins at index 2 */ + return this->deploy_raw_snapshot(error, server_id, data, hash, 2, SnapshotType::TEASPEAK, version); +} + +bool VirtualServerManager::deploy_ts3_snapshot(std::string &error, ts::ServerId server_id, const ts::command_parser &data) { + snapshots::version_t version{0}; + if(data.bulk(0).has_key("version")) + version = data.bulk(0).value_as("version"); + + auto hash = data.bulk(0).value("hash"); + if(data.bulk(0).has_key("salt")) { + error = "TeaSpeak dosn't support encrypted snapshots yet"; + return false; + } + + if(version == 0) { + return this->deploy_raw_snapshot(error, server_id, data, hash, 1, SnapshotType::TEAMSPEAK, version); + } else if(version == 1) { + error = "version 1 is an invalid version"; + return false; + } else if(version == 2) { + /* compressed data */ + error = "version 2 isn't currently supported"; + return false; + } else if(version == 3) { + error = "version 3 isn't currently supported"; + return false; + } else { + error = "snapshots with version 1-3 are currently supported"; + return false; + } +} + +struct parse_client_entry { + snapshots::client_entry parsed_data{}; +}; + +struct parsed_group_entry { + snapshots::group_entry parsed_data{}; +}; + +bool VirtualServerManager::deploy_raw_snapshot(std::string &error, ts::ServerId server_id, const ts::command_parser &command, const std::string& /* hash */, size_t command_offset, + snapshots::type type, snapshots::version_t version) { + snapshots::server_entry parsed_server{}; + //TODO: Verify hash + + /* all snapshots start with the virtual server properties */ + { + snapshots::server_parser parser{type, version, command}; + if(!parser.parse(error, parsed_server, command_offset)) + return false; + } + + std::vector parsed_channels{}; + /* afterwards all channels */ + { + snapshots::channel_parser parser{type, version, command}; + auto data = command.bulk(command_offset); + if(!data.has_key("begin_channels")) { + error = "missing begin channels token at " + std::to_string(data.command_character_index()); + return false; + } + + auto end_bulk = command.next_bulk_containing("end_channels", command_offset); + if(!end_bulk.has_value()) { + error = "missing end channels token"; + return false; + } else if(*end_bulk == command_offset) { + error = "snapshot contains no channels"; + return false; + } + parsed_channels.reserve(*end_bulk - command_offset); + debugMessage(server_id, "Snapshot contains {} channels", *end_bulk - command_offset); + + while(!command.bulk(command_offset).has_key("end_channels")) { + auto& entry = parsed_channels.emplace_back(); + if(!parser.parse(error, entry, command_offset)) + return false; + } + command_offset++; /* the "end_channels" token */ + } + + std::vector parsed_clients{}; + /* after channels all clients */ + { + snapshots::client_parser parser{type, version, command}; + auto data = command.bulk(command_offset); + if(!data.has_key("begin_clients")) { + error = "missing begin clients token at " + std::to_string(data.command_character_index()); + return false; + } + + auto end_bulk = command.next_bulk_containing("end_clients", command_offset); + if(!end_bulk.has_value()) { + error = "missing end clients token"; + return false; + } + parsed_channels.reserve(*end_bulk - command_offset); + debugMessage(server_id, "Snapshot contains {} clients", *end_bulk - command_offset); + + while(!command.bulk(command_offset).has_key("end_clients")) { + auto& entry = parsed_clients.emplace_back(); + if(!parser.parse(error, entry.parsed_data, command_offset)) + return false; + } + command_offset++; /* the "end_clients" token */ + } + + bool server_groups_parsed{false}, + channel_groups_parsed{false}, + client_permissions_parsed{false}, + channel_permissions_parsed{false}, + client_channel_permissions_parsed{false}; + + std::vector parsed_server_groups{}; + snapshots::group_relations parsed_server_group_relations{}; + + std::vector parsed_channel_groups{}; + snapshots::group_relations parsed_channel_group_relations{}; + + std::deque client_permissions{}; + std::deque channel_permissions{}; + std::deque client_channel_permissions{}; + + /* permissions */ + { + if(!command.bulk(command_offset++).has_key("begin_permissions")) { + error = "missing begin permissions key"; + return false; + } + + snapshots::relation_parser relation_parser{type, version, command}; + while(!command.bulk(command_offset).has_key("end_permissions")) { + if(command.bulk(command_offset).has_key("server_groups")) { + if(server_groups_parsed) { + error = "duplicated server group list"; + return false; + } else server_groups_parsed = true; + snapshots::group_parser group_parser{type, version, command, "id", permission::teamspeak::GroupType::SERVER}; + + /* parse all groups */ + while(!command.bulk(command_offset).has_key("end_groups")){ + auto& group = parsed_server_groups.emplace_back(); + if(!group_parser.parse(error, group.parsed_data, command_offset)) /* will consume the end group token */ + return false; + command_offset++; /* for the "end_group" token */ + } + command_offset++; /* for the "end_groups" token */ + + /* parse relations */ + if(!relation_parser.parse(error, parsed_server_group_relations, command_offset)) + return false; + command_offset++; /* for the "end_relations" token */ + + if(parsed_server_group_relations.size() > 1) { + error = "all group relations should be for channel id 0 but received more than one different channel."; + return false; + } else if(!parsed_server_group_relations.empty() && parsed_server_group_relations.begin()->first != 0) { + error = "all group relations should be for channel id 0 but received it for " + std::to_string(parsed_server_group_relations.begin()->first); + return false; + } + } else if(command.bulk(command_offset).has_key("channel_groups")) { + if(channel_groups_parsed) { + error = "duplicated channel group list"; + return false; + } else channel_groups_parsed = true; + snapshots::group_parser group_parser{type, version, command, "id", permission::teamspeak::GroupType::CHANNEL}; + + /* parse all groups */ + while(!command.bulk(command_offset).has_key("end_groups")){ + auto& group = parsed_channel_groups.emplace_back(); + if(!group_parser.parse(error, group.parsed_data, command_offset)) + return false; + command_offset++; /* for the "end_group" token */ + } + command_offset++; /* for the "end_groups" token */ + + /* parse relations */ + if(!relation_parser.parse(error, parsed_channel_group_relations, command_offset)) + return false; + command_offset++; /* for the "end_relations" token */ + } else if(command.bulk(command_offset).has_key("client_flat")) { + /* client permissions */ + if(client_permissions_parsed) { + error = "duplicated client permissions list"; + return false; + } else client_permissions_parsed = true; + snapshots::flat_parser flat_parser{type, version, command, permission::teamspeak::GroupType::CLIENT}; + if(!flat_parser.parse(error, client_permissions, command_offset)) + return false; + command_offset++; /* for the "end_flat" token */ + } else if(command.bulk(command_offset).has_key("channel_flat")) { + /* channel permissions */ + if(channel_permissions_parsed) { + error = "duplicated channel permissions list"; + return false; + } else channel_permissions_parsed = true; + snapshots::flat_parser flat_parser{type, version, command, permission::teamspeak::GroupType::CHANNEL}; + if(!flat_parser.parse(error, channel_permissions, command_offset)) + return false; + + command_offset++; /* for the "end_flat" token */ + } else if(command.bulk(command_offset).has_key("channel_client_flat")) { + /* channel client permissions */ + if(client_channel_permissions_parsed) { + error = "duplicated client channel permissions list"; + return false; + } else client_channel_permissions_parsed = true; + snapshots::flat_parser flat_parser{type, version, command, permission::teamspeak::GroupType::CLIENT}; + if(!flat_parser.parse(error, client_channel_permissions, command_offset)) + return false; + + command_offset++; /* for the "end_flat" token */ + } else { + command_offset++; + } + } + } + + /* check if everything has been parsed */ + { + /* basic stuff */ + if(!server_groups_parsed) { + error = "missing server groups"; + return false; + } + + if(!channel_groups_parsed) { + error = "missing channel groups"; + return false; + } + + if(!client_permissions_parsed) { + error = "missing client permissions"; + return false; + } + + if(!channel_permissions_parsed) { + error = "missing channel permissions"; + return false; + } + + if(!client_channel_permissions_parsed) { + error = "missing client channel permissions"; + return false; + } + } + + std::map channel_id_mapping{}; + std::map channel_group_id_mapping{}; + std::map server_group_id_mapping{}; + + /* lets start inserting data to the database */ + { + /* channels */ + { + /* Assign each channel a new id */ + ChannelId current_id{0}; + for(auto& channel : parsed_channels) { + const auto new_id = current_id++; + channel_id_mapping[channel.properties[property::CHANNEL_ID]] = new_id; + channel.properties[property::CHANNEL_ID] = new_id; + } + + /* Update channel parents */ + for(auto& channel : parsed_channels) { + auto pid = channel.properties[property::CHANNEL_PID].as(); + if(pid > 0) { + auto new_id = channel_id_mapping.find(pid); + if(new_id == channel_id_mapping.end()) { + error = "failed to remap channel parent id for channel \"" + channel.properties[property::CHANNEL_NAME].value() + "\" (snapshot/channel tree broken?)"; + return false; + } + channel.properties[property::CHANNEL_PID] = new_id->second; + } + } + + //TODO: Insert them into the database + } + + /* channel permissions */ + { + + for(auto& entry : channel_permissions) { + auto new_id = channel_id_mapping.find(entry.id1); + if(new_id == channel_id_mapping.end()) { + error = "missing channel id mapping for channel permission entry"; + return false; + } + entry.id1 = new_id->second; + } + } + + /* server groups */ + { + /* TODO: Insert to the database & load result */ + + } + + /* channel groups */ + { + /* TODO: Insert to the database & load the result into the mapping */ + } + + /* client permissions */ + { + + } + + /* register clients in the database */ + { + + } + + /* client channel permissions */ + { + for(auto& entry : client_channel_permissions) { + auto new_id = channel_id_mapping.find(entry.id1); + if(new_id == channel_id_mapping.end()) { + error = "missing channel id mapping for client channel permission entry"; + return false; + } + entry.id1 = new_id->second; + } + } + } + error = "not implemented"; + return false; +} \ No newline at end of file diff --git a/server/src/snapshots/groups.cpp b/server/src/snapshots/groups.cpp new file mode 100644 index 0000000..66ecae5 --- /dev/null +++ b/server/src/snapshots/groups.cpp @@ -0,0 +1,97 @@ +// +// Created by WolverinDEV on 11/04/2020. +// + +#include "groups.h" + +using namespace ts::server::snapshots; + +bool group_parser::parse(std::string &error, group_entry &group, size_t &offset) { + auto group_data = this->command.bulk(offset); + bool key_found; + + { + auto value_string = group_data.value(this->id_key, key_found); + if(!key_found) { + error = "missing id for group entry at character " + std::to_string(group_data.command_character_index()); + return false; + } + + char* end_ptr{nullptr}; + group.group_id = strtoll(value_string.c_str(), &end_ptr, 10); + if (*end_ptr) { + error = "unparsable id for group entry at character " + std::to_string(group_data.key_command_character_index(this->id_key) + this->id_key.length()); + return false; + } + } + + group.name = group_data.value("name", key_found); + if(!key_found) { + error = "missing name for group entry at character " + std::to_string(group_data.command_character_index()); + return false; + } + + return this->pparser.parse(error, group.permissions, offset); +} + +bool relation_parser::parse(std::string &error, group_relations &result, size_t &offset) { + auto relation_end = this->command.next_bulk_containing("end_relations", offset); + if(!relation_end.has_value()) { + error = "missing end relations token"; + return false; + } + + bool key_found; + while(offset < *relation_end) { + auto begin_bulk = this->command.bulk(offset); + if(!begin_bulk.has_key("iid")) { + error = "missing iid at character " + std::to_string(begin_bulk.command_character_index()); + return false; + } + + auto& relations = result[begin_bulk.value_as("iid")]; + auto next_iid = this->command.next_bulk_containing("iid", offset + 1); + + if(next_iid.has_value() && *next_iid < relation_end) + relations.reserve(*next_iid - offset); + else + relations.reserve(*relation_end - offset); + + while(offset < *next_iid) { + auto relation_data = this->command.bulk(offset++); + auto& relation = relations.emplace_back(); + + { + auto value_string = relation_data.value("cldbid", key_found); + if(!key_found) { + error = "missing client id for group relation entry at character " + std::to_string(relation_data.command_character_index()); + return false; + } + + char* end_ptr{nullptr}; + relation.client_id = strtoll(value_string.c_str(), &end_ptr, 10); + if (*end_ptr) { + error = "unparsable client id for group relation entry at character " + std::to_string(relation_data.key_command_character_index("cldbid") + 4); + return false; + } + } + + + { + auto value_string = relation_data.value("gid", key_found); + if(!key_found) { + error = "missing group id for group relation entry at character " + std::to_string(relation_data.command_character_index()); + return false; + } + + char* end_ptr{nullptr}; + relation.group_id = strtoll(value_string.c_str(), &end_ptr, 10); + if (*end_ptr) { + error = "unparsable group id for group relation entry at character " + std::to_string(relation_data.key_command_character_index("gid") + 3); + return false; + } + } + } + } + return true; +} \ No newline at end of file diff --git a/server/src/snapshots/groups.h b/server/src/snapshots/groups.h new file mode 100644 index 0000000..17a1883 --- /dev/null +++ b/server/src/snapshots/groups.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include +#include "./snapshot.h" +#include "./permission.h" + +namespace ts::server::snapshots { + struct group_entry { + GroupId group_id; + std::string name; + + std::vector permissions{}; + }; + + struct group_relation { + ClientDbId client_id; + GroupId group_id; + }; + + typedef std::map> group_relations; + + class group_parser : public parser { + public: + group_parser(type type_, version_t version, const command_parser& command, std::string id_key, permission::teamspeak::GroupType target_permission_type) + : parser{type_, version, command}, id_key{std::move(id_key)}, pparser{type_, version, command, { + target_permission_type, + {"end_group"}, + false + }} {} + + bool parse( + std::string& /* error */, + group_entry& /* result */, + size_t& /* offset */) override; + + private: + std::string id_key{}; + + permission_parser pparser; + }; + + class relation_parser : public parser { + public: + relation_parser(type type_, version_t version, const command_parser& command) : parser{type_, version, command} {} + + bool parse( + std::string& /* error */, + group_relations& /* result */, + size_t& /* offset */) override; + }; +} \ No newline at end of file diff --git a/server/src/snapshots/permission.cpp b/server/src/snapshots/permission.cpp new file mode 100644 index 0000000..9b581cd --- /dev/null +++ b/server/src/snapshots/permission.cpp @@ -0,0 +1,305 @@ +// +// Created by WolverinDEV on 11/04/2020. +// + +#include "permission.h" + +using namespace ts::server::snapshots; + +permission_parser::permission_parser(ts::server::snapshots::type type_, ts::server::snapshots::version_t version, + const ts::command_parser &command, permission_parser_options options) : parser{type_, version, command}, options{std::move(options)} { + if(type_ == type::TEAMSPEAK) { + this->parser_impl = &permission_parser::parse_entry_teamspeak_v0; + } else if(type_ == type::TEASPEAK) { + if(version >= 1) { + this->parser_impl = &permission_parser::parse_entry_teaspeak_v1; + } else { + /* TeaSpeak has no snapshot version 0. 0 implies a TeamSpeak snapshot */ + assert(false); + } + } else { + assert(false); + } +} + +bool permission_parser::parse( + std::string &error, + std::vector &result, + size_t &offset) { + + size_t end_offset{(size_t) -1}; + + { + size_t end_begin_offset{offset + (this->options.ignore_delimiter_at_index_0 ? 1 : 0)}; + for(const auto& token : this->options.delimiter) { + auto index = this->command.next_bulk_containing(token, end_begin_offset); + if(index.has_value() && *index < end_offset) + end_offset = *index; + } + + if(end_offset == (size_t) -1) { + error = "missing end token"; + return false; + } + } + + if(end_offset == offset) { + /* no entries at all */ + return true; + } + result.reserve((end_offset - offset) * 2); /* reserve some extra space because we might import permissions */ + + assert(this->type_ == type::TEAMSPEAK || this->type_ == type::TEASPEAK); + + while(offset < end_offset) { + if(!(this->*(this->parser_impl))(error, result, this->command[offset])) + return false; + + offset++; + } + + return true; +} + +bool permission_parser::parse_entry_teamspeak_v0(std::string &error, std::vector &result, + const ts::command_bulk &data) { + bool key_found; + auto original_name = data.value("permid", key_found); + if(!key_found) { + error = "missing id for permission entry at character " + std::to_string(data.command_character_index()); + return false; + } + + permission::PermissionValue value{}; + { + auto value_string = data.value("permvalue", key_found); + if(!key_found) { + error = "missing value for permission entry at character " + std::to_string(data.command_character_index()); + return false; + } + + char* end_ptr{nullptr}; + value = strtoll(value_string.c_str(), &end_ptr, 10); + if (*end_ptr) { + error = "unparsable permission value at index " + std::to_string(data.key_command_character_index("permvalue") + 9); + return false; + } + } + + auto flag_skip = data.value("permskip", key_found) == "1"; + if(!key_found) { + error = "missing skip flag for permission entry at character " + std::to_string(data.command_character_index()); + return false; + } + + auto flag_negate = data.value("permnegated", key_found) == "1"; + if(!key_found) { + error = "missing skip flag for permission entry at character " + std::to_string(data.command_character_index()); + return false; + } + + for(const auto& mapped : permission::teamspeak::map_key(original_name, this->options.target_permission_type)) { + auto type = permission::resolvePermissionData(mapped); + if(type == permission::PermissionTypeEntry::unknown) + continue; + + permission_entry* entry{nullptr}; + for(auto& e : result) + if(e.type == type) { + entry = &e; + break; + } + if(!entry) { + entry = &result.emplace_back(); + entry->type = type; + } + entry->value = {value, true}; + if(mapped != type->grant_name) { + entry->flag_negate = flag_negate; + entry->flag_skip = flag_skip; + } + } + return true; +} + +bool permission_parser::parse_entry_teaspeak_v1(std::string &error, std::vector &result, + const ts::command_bulk &data) { + bool key_found; + auto permission_name = data.value("perm", key_found); + if(!key_found) { + error = "missing id for permission entry at character " + std::to_string(data.command_character_index()); + return false; + } + + auto flag_skip = data.value("flag_skip", key_found) == "1"; + if(!key_found) { + error = "missing skip flag for permission entry at character " + std::to_string(data.command_character_index()); + return false; + } + + auto flag_negated = data.value("flag_negated", key_found) == "1"; + if(!key_found) { + error = "missing negate flag for permission entry at character " + std::to_string(data.command_character_index()); + return false; + } + + permission::PermissionValue value{}; + { + auto value_string = data.value("value", key_found); + if(!key_found) { + error = "missing value for permission entry at character " + std::to_string(data.command_character_index()); + return false; + } + + char* end_ptr{nullptr}; + value = strtoll(value_string.c_str(), &end_ptr, 10); + if (*end_ptr) { + error = "unparsable permission value at index " + std::to_string(data.key_command_character_index("value") + 5); + return false; + } + } + + permission::PermissionValue granted{}; + { + auto value_string = data.value("grant", key_found); + if(!key_found) { + error = "missing grant for permission entry at character " + std::to_string(data.command_character_index()); + return false; + } + + char* end_ptr{nullptr}; + granted = strtoll(value_string.c_str(), &end_ptr, 10); + if (*end_ptr) { + error = "unparsable permission granted value at index " + std::to_string(data.key_command_character_index("grant") + 5); + return false; + } + } + + auto type = permission::resolvePermissionData(permission_name); + if(type == permission::PermissionTypeEntry::unknown) + return true; /* we just drop unknown permissions */ //TODO: Log this drop + + auto& entry = result.emplace_back(); + entry.type = type; + entry.flag_skip = flag_skip; + entry.flag_negate = flag_negated; + entry.value = {value, value != permNotGranted}; + entry.granted = {granted, granted != permNotGranted}; + return true; +} + +permission_writer::permission_writer(type type_, version_t version, + ts::command_builder &command, permission::teamspeak::GroupType target_permission_type) : command{command}, type_{type_}, version_{version}, target_permission_type_{target_permission_type} { + if(type_ == type::TEAMSPEAK) { + this->write_impl = &permission_writer::write_entry_teamspeak_v0; + } else if(type_ == type::TEASPEAK) { + if(version >= 1) { + this->write_impl = &permission_writer::write_entry_teaspeak_v1; + } else { + /* TeaSpeak has no snapshot version 0. 0 implies a TeamSpeak snapshot */ + assert(false); + } + } else { + assert(false); + } +} + +bool permission_writer::write(std::string &error, size_t &offset, const std::deque &entries) { + this->command.reserve_bulks(entries.size() * 2); + for(auto& entry : entries) + if(!this->write_entry(error, offset, entry)) + return false; + return true; +} + +bool permission_writer::write_entry(std::string &error, size_t &offset, const ts::server::snapshots::permission_entry &entry) { + return (this->*(this->write_impl))(error, offset, entry); +} + +bool permission_writer::write_entry_teamspeak_v0(std::string &error, size_t& offset, + const ts::server::snapshots::permission_entry &entry) { + if(entry.value.has_value) { + for(const auto& name : permission::teamspeak::unmap_key(entry.type->name, this->target_permission_type_)) { + auto bulk = this->command.bulk(offset++); + bulk.put_unchecked("permid", name); + bulk.put_unchecked("permvalue", entry.value.value); + bulk.put_unchecked("permskip", entry.flag_skip); + bulk.put_unchecked("permnegated", entry.flag_negate); + } + } + + if(entry.granted.has_value) { + for(const auto& name : permission::teamspeak::unmap_key(entry.type->grant_name, this->target_permission_type_)) { + auto bulk = this->command.bulk(offset++); + bulk.put_unchecked("permid", name); + bulk.put_unchecked("permvalue", entry.granted.value); + bulk.put_unchecked("permskip", "0"); + bulk.put_unchecked("permnegated", "0"); + } + } + return true; +} + +bool permission_writer::write_entry_teaspeak_v1(std::string &error, size_t &offset, + const ts::server::snapshots::permission_entry &entry) { + if(!entry.value.has_value && !entry.granted.has_value) + return true; /* should not happen, but we skip that here */ + + auto bulk = this->command.bulk(offset++); + bulk.put_unchecked("perm", entry.type->name); + bulk.put_unchecked("value", entry.value.has_value ? entry.value.value : permNotGranted); + bulk.put_unchecked("grant", entry.granted.has_value ? entry.granted.value : permNotGranted); + bulk.put_unchecked("flag_skip", entry.flag_skip); + bulk.put_unchecked("flag_negated", entry.flag_negate); + return true; +} + +bool flat_parser::parse(std::string &error, std::deque &result, size_t &offset) { + auto flat_end = this->command.next_bulk_containing("end_flat", offset); + if(!flat_end.has_value()) { + error = "missing flat end for " + std::to_string(this->command.bulk(offset).command_character_index()); + return false; + } + + bool key_found; + while(offset < *flat_end) { + auto flat_data = this->command.bulk(offset); + auto& flat_entry = result.emplace_back(); + + /* id1 */ + { + auto value_string = flat_data.value("id1", key_found); + if(!key_found) { + error = "missing id1 for flat entry at character " + std::to_string(flat_data.command_character_index()); + return false; + } + + char* end_ptr{nullptr}; + flat_entry.id1 = strtoll(value_string.c_str(), &end_ptr, 10); + if (*end_ptr) { + error = "unparsable id1 for flat entry at character " + std::to_string(flat_data.key_command_character_index("id1") + 3); + return false; + } + } + + /* id2 */ + { + auto value_string = flat_data.value("id2", key_found); + if(!key_found) { + error = "missing id2 for flat entry at character " + std::to_string(flat_data.command_character_index()); + return false; + } + + char* end_ptr{nullptr}; + flat_entry.id2 = strtoll(value_string.c_str(), &end_ptr, 10); + if (*end_ptr) { + error = "unparsable id2 for flat entry at character " + std::to_string(flat_data.key_command_character_index("id2") + 3); + return false; + } + } + + if(!this->pparser.parse(error, flat_entry.permissions, offset)) + return false; + } + return true; +} \ No newline at end of file diff --git a/server/src/snapshots/permission.h b/server/src/snapshots/permission.h new file mode 100644 index 0000000..6b724a0 --- /dev/null +++ b/server/src/snapshots/permission.h @@ -0,0 +1,110 @@ +#pragma once + +#include +#include +#include "./snapshot.h" + +namespace ts::server::snapshots { + struct permission_entry { + std::shared_ptr type{nullptr}; + + permission::v2::PermissionFlaggedValue value{0, false}; + permission::v2::PermissionFlaggedValue granted{0, false}; + + bool flag_skip{false}; + bool flag_negate{false}; + }; + + struct permission_parser_options { + permission::teamspeak::GroupType target_permission_type; + std::vector delimiter; + bool ignore_delimiter_at_index_0; + }; + + class permission_parser : public parser> { + public: + permission_parser(type type_, version_t version, const command_parser& command, permission_parser_options /* options */); + + bool parse( + std::string& /* error */, + std::vector& /* result */, + size_t& /* offset */) override; + private: + typedef bool(permission_parser::*parse_impl_t)(std::string &error, std::vector &result, const ts::command_bulk &data); + + const permission_parser_options options; + parse_impl_t parser_impl; + + bool parse_entry_teamspeak_v0( + std::string& /* error */, + std::vector& /* result */, + const command_bulk& /* entry */ + ); + + bool parse_entry_teaspeak_v1( + std::string& /* error */, + std::vector& /* result */, + const command_bulk& /* entry */ + ); + }; + + class permission_writer { + public: + permission_writer(type type_, version_t version, command_builder& command, permission::teamspeak::GroupType target_permission_type); + + bool write( + std::string& /* error */, + size_t& /* offset */, + const std::deque& /* permissions */); + + bool write_entry( + std::string& /* error */, + size_t& /* offset */, + const permission_entry& /* permissions */); + private: + typedef bool(permission_writer::*write_impl_t)(std::string &error, size_t& offset,const permission_entry& /* permissions */); + + command_builder& command; + const type type_; + const version_t version_; + const permission::teamspeak::GroupType target_permission_type_; + write_impl_t write_impl; + + bool write_entry_teamspeak_v0( + std::string &error, + size_t& offset, + const permission_entry& /* permissions */ + ); + + bool write_entry_teaspeak_v1( + std::string &error, + size_t& offset, + const permission_entry& /* permissions */ + ); + }; + + struct permissions_flat_entry { + uint64_t id1; + uint64_t id2; + + std::vector permissions; + }; + + class flat_parser : public parser> { + public: + flat_parser(type type_, version_t version, const command_parser& command, permission::teamspeak::GroupType target_permission_type) + : parser{type_, version, command}, pparser{type_, version, command, { + target_permission_type, + {"id1", "id2", "end_flat"}, /* only id1 should be enough, because if id2 changes id1 will be set as well but we just wan't to get sure */ + true + }} {} + + bool parse( + std::string& /* error */, + std::deque& /* result */, + size_t& /* offset */) override; + + private: + permission_parser pparser; + }; +} \ No newline at end of file diff --git a/server/src/snapshots/server.cpp b/server/src/snapshots/server.cpp new file mode 100644 index 0000000..6087da5 --- /dev/null +++ b/server/src/snapshots/server.cpp @@ -0,0 +1,42 @@ +// +// Created by WolverinDEV on 11/04/2020. +// + +#include "server.h" + +using namespace ts::server::snapshots; + +bool server_parser::parse(std::string &error, server_entry &result, size_t &offset) { + auto data = this->command.bulk(offset++); + if(!data.has_key("end_virtualserver")) { + error = "missing virtual server end token at character " + std::to_string(data.command_character_index()); + return false; + } + + result.properties.register_property_type(); + + size_t entry_index{0}; + std::string_view key{}; + std::string value{}; + while(data.next_entry(entry_index, key, value)) { + if(key == "end_virtualserver" || + key == property::describe(property::VIRTUALSERVER_PORT).name || + key == property::describe(property::VIRTUALSERVER_HOST).name || + key == property::describe(property::VIRTUALSERVER_WEB_PORT).name || + key == property::describe(property::VIRTUALSERVER_WEB_HOST).name || + key == property::describe(property::VIRTUALSERVER_VERSION).name || + key == property::describe(property::VIRTUALSERVER_PLATFORM).name) + continue; + + const auto& property = property::find(key); + if(property.is_undefined()) { + //TODO: Issue a warning + continue; + } + + //TODO: Validate value? + result.properties[property] = value; + } + + return true; +} \ No newline at end of file diff --git a/server/src/snapshots/server.h b/server/src/snapshots/server.h new file mode 100644 index 0000000..03aa134 --- /dev/null +++ b/server/src/snapshots/server.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include +#include "./snapshot.h" + +namespace ts::server::snapshots { + struct server_entry { + Properties properties{}; + }; + + class server_parser : public parser { + public: + server_parser(type type_, version_t version, const command_parser& command) : parser{type_, version, command} {} + + bool parse( + std::string & /* error */, + server_entry & /* result */, + size_t & /* offset */) override; + }; + + /* + class server_writer : public writer { + public: + server_writer(type type_, version_t version, command_builder& command) : writer{type_, version, command} {} + + bool write(std::string &, size_t &, const server_entry &) override; + }; + */ +} \ No newline at end of file diff --git a/server/src/snapshots/snapshot.h b/server/src/snapshots/snapshot.h new file mode 100644 index 0000000..c1d5460 --- /dev/null +++ b/server/src/snapshots/snapshot.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include + +namespace ts::server::snapshots { + enum struct type { + TEAMSPEAK, + TEASPEAK + }; + + typedef int32_t version_t; + constexpr version_t unknown_version{-1}; + + template + class parser { + public: + parser(type type_, version_t version, const command_parser& command) : + command{command}, type_{type_}, version_{version} {} + + virtual bool parse( + std::string& /* error */, + result_t& /* result */, + size_t& /* offset */) = 0; + protected: + const command_parser& command; + const type type_; + const version_t version_; + }; + + template + class writer { + public: + writer(type type_, version_t version, command_builder& command) : + command{command}, type_{type_}, version_{version} {} + + virtual bool write( + std::string& /* error */, + size_t& /* offset */, + const entry_t& /* entry */) = 0; + protected: + command_builder& command; + const type type_; + const version_t version_; + }; +} \ No newline at end of file