diff --git a/CMakeLists.txt b/CMakeLists.txt index ffbf940..16c8909 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,6 +61,8 @@ find_package(Opus REQUIRED) find_package(spdlog REQUIRED) find_package(Jemalloc REQUIRED) find_package(Protobuf REQUIRED) +message("${zstd_DIR}") +find_package(zstd REQUIRED) include_directories(${StringVariable_INCLUDE_DIR}) add_subdirectory(music/) diff --git a/git-teaspeak b/git-teaspeak index 0dc8e28..ef2d684 160000 --- a/git-teaspeak +++ b/git-teaspeak @@ -1 +1 @@ -Subproject commit 0dc8e28125f9727f2dffef4b6f93b397099db1f0 +Subproject commit ef2d6842d547b8f84ebbe4fe9fa6f132bcd84d35 diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index faab19b..de323ab 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -296,6 +296,7 @@ target_link_libraries(TeaSpeakServer jsoncpp_lib ${ed25519_LIBRARIES_STATIC} + zstd::libzstd_static ) if (COMPILE_WEB_CLIENT) diff --git a/server/src/DatabaseHelper.cpp b/server/src/DatabaseHelper.cpp index efafc8f..3885d60 100644 --- a/server/src/DatabaseHelper.cpp +++ b/server/src/DatabaseHelper.cpp @@ -13,38 +13,42 @@ using namespace ts::permission; //#define DISABLE_CACHING +struct ts::server::CachedPermissionManager { + ServerId server_id{0}; + ClientDbId client_database_id{0}; + std::weak_ptr instance{}; + + std::shared_ptr instance_ref{}; /* reference to the current instance, will be refreshed every time the instance gets accessed */ + std::chrono::time_point last_access{}; +}; + + +struct ts::server::StartupCacheEntry { + ServerId sid{0}; + + std::deque> permissions{}; + std::deque> properties{}; +}; + DatabaseHelper::DatabaseHelper(sql::SqlManager* srv) : sql(srv) {} DatabaseHelper::~DatabaseHelper() { - for(const auto& elm : cachedPermissionManagers) - delete elm; - cachedPermissionManagers.clear(); + this->cached_permission_managers.clear(); } void DatabaseHelper::tick() { + auto cache_timeout = std::chrono::system_clock::now() - std::chrono::minutes{10}; { - threads::MutexLock l(this->permManagerLock); - auto cpy = this->cachedPermissionManagers; - for(const auto& mgr : cpy){ - //if(mgr->ownLock && system_clock::now() - mgr->lastAccess > minutes(5)) //TODO remove instand delete! - mgr->ownLock.reset(); - if(mgr->manager.expired()){ - this->cachedPermissionManagers.erase(std::find(this->cachedPermissionManagers.begin(), this->cachedPermissionManagers.end(), mgr)); - delete mgr; - } - } - } + std::lock_guard cp_lock{this->cached_permission_manager_lock}; - { - threads::MutexLock l(this->propsLock); - auto pcpy = this->cachedProperties; - for(const auto& mgr : pcpy) { - if(mgr->ownLock && system_clock::now() - mgr->lastAccess > minutes(5)) - mgr->ownLock.reset(); - if(mgr->properties.expired()) { - this->cachedProperties.erase(std::find(this->cachedProperties.begin(), this->cachedProperties.end(), mgr)); - delete mgr; - } - } + this->cached_permission_managers.erase(std::remove_if(this->cached_permission_managers.begin(), this->cached_permission_managers.end(), [&](const std::unique_ptr& manager) { + if(manager->last_access < cache_timeout) + manager->instance_ref = nullptr; + + if(manager->instance.expired()) + return true; + + return false; + }), this->cached_permission_managers.end()); } } @@ -136,15 +140,12 @@ bool DatabaseHelper::validClientDatabaseId(const std::shared_ptr& void DatabaseHelper::deleteClient(const std::shared_ptr& server, ClientDbId cldbid) { auto serverId = (ServerId) (server ? server->getServerId() : 0); { - lock_guard lock(permManagerLock); - for(auto permMgr : this->cachedPermissionManagers) - if(serverId == 0 || (permMgr->cldbid == cldbid && permMgr->sid == serverId)) { - this->cachedPermissionManagers.erase(std::find(this->cachedPermissionManagers.begin(), this->cachedPermissionManagers.end(), permMgr)); - delete permMgr; - break; - } + lock_guard lock{cached_permission_manager_lock}; + + this->cached_permission_managers.erase(std::remove_if(this->cached_permission_managers.begin(), this->cached_permission_managers.end(), [&](const auto& entry) { + return entry->server_id == serverId && entry->client_database_id == cldbid; + }), this->cached_permission_managers.end()); } - //TODO remove from props cache? sql::result state{}; @@ -236,26 +237,38 @@ inline sql::result load_permissions_v2( logTrace(server_id, "[SQL] load_permissions(\"{}\") took {}ms", command.sqlCommand(), duration_cast(time).count()); } -#define UPDATE_COMMAND "UPDATE `permissions` SET `value` = :value, `grant` = :grant, `flag_skip` = :flag_skip, `flag_negate` = :flag_negate WHERE `serverId` = :serverId AND `type` = :type AND `id` = :id AND `permId` = :permId AND `channelId` = :chId" -#define INSERT_COMMAND "INSERT INTO `permissions` (`serverId`, `type`, `id`, `channelId`, `permId`, `value`, `grant`, `flag_skip`, `flag_negate`) VALUES (:serverId, :type, :id, :chId, :permId, :value, :grant, :flag_skip, :flag_negate)" -#define DELETE_COMMAND "DELETE FROM `permissions` WHERE `serverId` = :serverId AND `type` = :type AND `id` = :id AND `permId` = :permId AND `channelId` = :chId" +constexpr static std::string_view kPermissionUpdateCommand{"UPDATE `permissions` SET `value` = :value, `grant` = :grant, `flag_skip` = :flag_skip, `flag_negate` = :flag_negate WHERE `serverId` = :serverId AND `type` = :type AND `id` = :id AND `permId` = :permId AND `channelId` = :chId"}; +constexpr static std::string_view kPermissionInsertCommand{"INSERT INTO `permissions` (`serverId`, `type`, `id`, `channelId`, `permId`, `value`, `grant`, `flag_skip`, `flag_negate`) VALUES (:serverId, :type, :id, :chId, :permId, :value, :grant, :flag_skip, :flag_negate)"}; +constexpr static std::string_view kPermissionDeleteCommand{"DELETE FROM `permissions` WHERE `serverId` = :serverId AND `type` = :type AND `id` = :id AND `permId` = :permId AND `channelId` = :chId"}; + +std::shared_ptr DatabaseHelper::find_cached_permission_manager(ServerId server_id, + ClientDbId client_database_id) { + for(auto it = this->cached_permission_managers.begin(); it != this->cached_permission_managers.end(); it++) { + auto& cached_manager = *it; + + if(cached_manager->client_database_id == client_database_id && cached_manager->server_id == server_id) { + auto manager = cached_manager->instance.lock(); + if(!manager){ + this->cached_permission_managers.erase(it); + break; + } + + cached_manager->last_access = system_clock::now(); + cached_manager->instance_ref = manager; + return manager; + } + } + + return nullptr; +} std::shared_ptr DatabaseHelper::loadClientPermissionManager(const std::shared_ptr& server, ClientDbId cldbid) { auto server_id = server ? server->getServerId() : 0; #ifndef DISABLE_CACHING { - lock_guard lock(permManagerLock); - for(auto permMgr : this->cachedPermissionManagers) - if(permMgr->cldbid == cldbid && permMgr->sid == (server ? server->getServerId() : 0)) { - auto ptr = permMgr->manager.lock(); - if(!ptr){ - this->cachedPermissionManagers.erase(std::find(this->cachedPermissionManagers.begin(), this->cachedPermissionManagers.end(), permMgr)); - delete permMgr; - break; - } - permMgr->lastAccess = system_clock::now(); - return ptr; - } + std::lock_guard lock{cached_permission_manager_lock}; + auto manager = this->find_cached_permission_manager(server_id, cldbid); + if(manager) return manager; } #endif @@ -298,15 +311,23 @@ std::shared_ptr DatabaseHelper::loadClientPermissionManag #ifndef DISABLE_CACHING - this->permManagerLock.lock(); - auto entry = new CachedPermissionManager(); - entry->sid = server_id; - entry->manager = permission_manager; - entry->ownLock = permission_manager; - entry->cldbid = cldbid; - entry->lastAccess = system_clock::now(); - this->cachedPermissionManagers.push_back(entry); - this->permManagerLock.unlock(); + auto cache_entry = std::make_unique(); + cache_entry->server_id = server_id; + cache_entry->instance = permission_manager; + cache_entry->instance_ref = permission_manager; + cache_entry->client_database_id = cldbid; + cache_entry->last_access = system_clock::now(); + + { + std::lock_guard cache_lock{this->cached_permission_manager_lock}; + + /* test if we might not got a second instance */ + auto manager = this->find_cached_permission_manager(server_id, cldbid); + if(manager) return manager; + + this->cached_permission_managers.push_back(std::move(cache_entry)); + } + #endif return permission_manager; } @@ -319,7 +340,7 @@ void DatabaseHelper::saveClientPermissions(const std::shared_ptrgetServerId() : 0; for(auto& update : updates) { - std::string query = update.flag_delete ? DELETE_COMMAND : (update.flag_db ? UPDATE_COMMAND : INSERT_COMMAND); + std::string query{update.flag_delete ? kPermissionDeleteCommand : (update.flag_db ? kPermissionUpdateCommand : kPermissionInsertCommand)}; auto permission_data = permission::resolvePermissionData(update.permission); auto value = update.update_value == v2::delete_value ? permNotGranted : update.values.value; @@ -386,7 +407,7 @@ void DatabaseHelper::saveGroupPermissions(const std::shared_ptrgetServerId() : 0; for(auto& update : updates) { - std::string query = update.flag_delete ? DELETE_COMMAND : (update.flag_db ? UPDATE_COMMAND : INSERT_COMMAND); + std::string query{update.flag_delete ? kPermissionDeleteCommand : (update.flag_db ? kPermissionUpdateCommand : kPermissionInsertCommand)}; auto permission_data = permission::resolvePermissionData(update.permission); auto value = update.update_value == v2::delete_value ? permNotGranted : update.values.value; @@ -458,7 +479,7 @@ void DatabaseHelper::savePlaylistPermissions(const std::shared_ptrgetServerId() : 0; for(auto& update : updates) { - std::string query = update.flag_delete ? DELETE_COMMAND : (update.flag_db ? UPDATE_COMMAND : INSERT_COMMAND); + std::string query{update.flag_delete ? kPermissionDeleteCommand : (update.flag_db ? kPermissionUpdateCommand : kPermissionInsertCommand)}; auto permission_data = permission::resolvePermissionData(update.permission); auto value = update.update_value == v2::delete_value ? permNotGranted : update.values.value; @@ -524,7 +545,7 @@ void DatabaseHelper::saveChannelPermissions(const std::shared_ptrgetServerId() : 0; for(auto& update : updates) { - std::string query = update.flag_delete ? DELETE_COMMAND : (update.flag_db ? UPDATE_COMMAND : INSERT_COMMAND); + std::string query{update.flag_delete ? kPermissionDeleteCommand : (update.flag_db ? kPermissionUpdateCommand : kPermissionInsertCommand)}; auto value = update.update_value == v2::delete_value ? permNotGranted : update.values.value; auto grant = update.update_grant == v2::delete_value ? permNotGranted : update.values.grant; @@ -1058,6 +1079,15 @@ void DatabaseHelper::clearStartupCache(ts::ServerId sid) { } } +void DatabaseHelper::handleServerDelete(ServerId server_id) { + { + std::lock_guard pm_lock{this->cached_permission_manager_lock}; + this->cached_permission_managers.erase(std::remove_if(this->cached_permission_managers.begin(), this->cached_permission_managers.end(), [&](const auto& entry) { + return entry->server_id == server_id; + }), this->cached_permission_managers.end()); + } +} + //SELECT `serverId`, `type`, `id`, `key`, `value` FROM properties ORDER BY `serverId` //SELECT `serverId`, `type`, `id`, `channelId`, `permId`, `value`, `grant`, `flag_skip`, `flag_negate` FROM permissions ORDER BY `serverId` struct StartupPermissionArgument { @@ -1246,4 +1276,82 @@ bool DatabaseHelper::deletePlaylist(const std::shared_ptr& offset, + const std::optional& limit, + void (* callback)(void *, const DatabaseClient &), + void *user_argument) { + + DatabaseClient client; + size_t set_index{0}; + auto sqlResult = sql::command{this->sql, kDBListQuery, + variable{":serverId", server_id}, + variable{":offset", offset.has_value() ? *offset : 0}, + variable{":limit", limit.has_value() ? *limit : -1} + }.query([&](int length, std::string* values, std::string* names) { + set_index++; + + auto index{0}; + try { + assert(names[index] == "client_database_id"); + client.client_database_id = std::stoull(values[index++]); + + assert(names[index] == "client_unique_id"); + client.client_unique_id = values[index++]; + + assert(names[index] == "client_nickname"); + client.client_nickname = values[index++]; + + assert(names[index] == "client_ip"); + client.client_ip = values[index++]; + + assert(names[index] == "client_created"); + client.client_last_connected = values[index++]; + + assert(names[index] == "client_last_connected"); + client.client_created = values[index++]; + + assert(names[index] == "client_total_connections"); + client.client_total_connections = values[index++]; + + assert(names[index] == "client_login_name"); + client.client_login_name = values[index++]; + + assert(names[index] == "client_description"); + client.client_description = values[index++]; + + assert(index == length); + } catch (std::exception& ex) { + logError(server_id, "Failed to parse client base properties at index {}: {}. Offset: {} Limit: {} Set: {}", + index - 1, + ex.what(), + offset.has_value() ? std::to_string(*offset) : "not given", + limit.has_value() ? std::to_string(*limit) : "not given", + set_index - 1 + ); + return; + } + + callback(user_argument, client); + }); } \ No newline at end of file diff --git a/server/src/DatabaseHelper.h b/server/src/DatabaseHelper.h index 2da23fd..0703bb7 100644 --- a/server/src/DatabaseHelper.h +++ b/server/src/DatabaseHelper.h @@ -25,26 +25,6 @@ namespace ts::server { uint32_t client_total_connections; }; - struct CachedPermissionManager { - ServerId sid; - ClientDbId cldbid; - std::weak_ptr manager; - std::shared_ptr ownLock; - std::chrono::time_point lastAccess; - }; - - struct CachedProperties { - ServerId sid; - ClientDbId cldbid; - std::weak_ptr properties; - std::shared_ptr ownLock; - std::chrono::time_point lastAccess; - }; - - /* - CREATE_TABLE("properties", "`serverId` INTEGER DEFAULT -1, `type` INTEGER, `id` INTEGER, `key` VARCHAR(" UNKNOWN_KEY_LENGTH "), `value` TEXT", command_append_utf8); - CREATE_TABLE("permissions", "`serverId` INT NOT NULL, `type` INT, `id` INT, `channelId` INT, `permId` VARCHAR(" UNKNOWN_KEY_LENGTH "), `value` INT, `grant` INT", command_append_utf8); - */ struct StartupPermissionEntry { permission::PermissionSqlType type = permission::SQL_PERM_CHANNEL; uint64_t id = 0; @@ -64,11 +44,19 @@ namespace ts::server { std::string value; }; - struct StartupCacheEntry { - ServerId sid; + struct DatabaseClient { + ClientDbId client_database_id; + std::string client_unique_id; - std::deque> permissions; - std::deque> properties; + std::string client_nickname; + std::string client_ip; + + std::string client_created; /* seconds since epoch */ + std::string client_last_connected; /* seconds since epoch */ + std::string client_total_connections; + + std::string client_login_name; + std::string client_description; /* optional and only given sometimes */ }; struct FastPropertyEntry { @@ -76,6 +64,8 @@ namespace ts::server { std::string value; }; + struct CachedPermissionManager; + struct StartupCacheEntry; class DatabaseHelper { public: static std::shared_ptr default_properties_client(std::shared_ptr /* properties */, ClientType /* type */); @@ -88,6 +78,15 @@ namespace ts::server { size_t cacheBinarySize(); void clearStartupCache(ServerId sid = 0); + void handleServerDelete(ServerId /* server id */); + + void listDatabaseClients( + ServerId /* server id */, + const std::optional& offset, + const std::optional& limit, + void(* /* callback */)(void* /* user argument */, const DatabaseClient& /* client */), + void* /* user argument */); + void deleteClient(const std::shared_ptr&,ClientDbId); bool validClientDatabaseId(const std::shared_ptr&, ClientDbId); std::deque> queryDatabaseInfo(const std::shared_ptr&, const std::deque&); @@ -125,9 +124,11 @@ namespace ts::server { std::deque> startup_entries; sql::SqlManager* sql = nullptr; - threads::Mutex permManagerLock; - std::deque cachedPermissionManagers; - threads::Mutex propsLock; - std::deque cachedProperties; + + threads::Mutex cached_permission_manager_lock; + std::deque> cached_permission_managers; + + /* Attention: cached_permission_manager_lock should be locked! */ + [[nodiscard]] inline std::shared_ptr find_cached_permission_manager(ServerId /* server id */, ClientDbId /* client id */); }; } \ No newline at end of file diff --git a/server/src/ServerManagerSnapshotDeploy.cpp b/server/src/ServerManagerSnapshotDeploy.cpp index cbe5b07..b76efc3 100644 --- a/server/src/ServerManagerSnapshotDeploy.cpp +++ b/server/src/ServerManagerSnapshotDeploy.cpp @@ -163,6 +163,7 @@ struct SnapshotPermissionEntry { } }; +#if 0 std::shared_ptr VirtualServerManager::createServerFromSnapshot(shared_ptr old, std::string host, uint16_t port, const ts::Command &arguments, std::string &error) { @@ -692,6 +693,7 @@ std::shared_ptr VirtualServerManager::createServerFromSnapshot(sh server->ensureValidDefaultGroups(); return server; } +#endif struct CommandTuple { Command& cmd; @@ -771,8 +773,8 @@ bool VirtualServerManager::createServerSnapshot(Command &cmd, shared_ptr 2) { + if(version == -1) version = 3; //Auto versioned + if(version < 0 || version > 3) { error = "Invalid snapshot version!"; return false; } @@ -817,44 +819,26 @@ bool VirtualServerManager::createServerSnapshot(Command &cmd, shared_ptrgetSql(), "SELECT `cldbid`, `clientUid`, `firstConnect`, `lastName` FROM `clients` WHERE `serverId` = :sid", - variable{":sid", server->getServerId()} - ).query([&](CommandTuple* commandIndex, int length, char** value, char** name) { - ClientDbId cldbid = 0; - int64_t clientCreated = 0; - string clientUid; - string lastName; - string description; //TODO description - for(int idx = 0; idx < length; idx++) { - try { - if(strcmp(name[idx], "cldbid") == 0) - cldbid = value[idx] && strlen(value[idx]) > 0 ? stoul(value[idx]) : 0; - else if(strcmp(name[idx], "clientUid") == 0) - clientUid = value[idx]; - else if(strcmp(name[idx], "firstConnect") == 0) - clientCreated = value[idx] && strlen(value[idx]) > 0 ? stoll(value[idx]) : 0L; - else if(strcmp(name[idx], "lastName") == 0) - lastName = value[idx]; - } catch (std::exception& ex) { - logError(0, "Failed to write snapshot client (Skipping it)! Message: {} @ {} => {}", ex.what(), name[idx], value[idx]); - return 0; - } - } + struct CallbackArgument { + Command& command; + int& index; + int version; + }; + CallbackArgument callback_argument{cmd, index, version}; + this->handle->databaseHelper()->listDatabaseClients(server->getServerId(), std::nullopt, std::nullopt, [](void* ptr_argument, const DatabaseClient& client) { + auto argument = (CallbackArgument*) ptr_argument; - commandIndex->cmd[commandIndex->index]["client_id"] = cldbid; - commandIndex->cmd[commandIndex->index]["client_unique_id"] = clientUid; - commandIndex->cmd[commandIndex->index]["client_nickname"] = lastName; - commandIndex->cmd[commandIndex->index]["client_created"] = clientCreated; - commandIndex->cmd[commandIndex->index]["client_description"] = description; - if(commandIndex->version == 0) - commandIndex->cmd[commandIndex->index]["client_unread_messages"] = 0; - commandIndex->index++; - return 0; - }, &parm); - LOG_SQL_CMD(res); + argument->command[argument->index]["client_id"] = client.client_database_id; + argument->command[argument->index]["client_unique_id"] = client.client_unique_id; + argument->command[argument->index]["client_nickname"] = client.client_nickname; + argument->command[argument->index]["client_created"] = client.client_created; + argument->command[argument->index]["client_description"] = client.client_description; + if(argument->version == 0) + argument->command[argument->index]["client_unread_messages"] = 0; + argument->index++; + }, &callback_argument); cmd[index++]["end_clients"] = ""; } diff --git a/server/src/VirtualServerManager.cpp b/server/src/VirtualServerManager.cpp index 83f4d65..81d192e 100644 --- a/server/src/VirtualServerManager.cpp +++ b/server/src/VirtualServerManager.cpp @@ -110,6 +110,9 @@ bool VirtualServerManager::initialize(bool autostart) { if(id == 0) { logError(LOG_INSTANCE, "Failed to load virtual server from database. Server id is zero!"); return 0; + } else if(id == 0xFFFF) { + /* snapshot server */ + return 0; } if(host.empty()) { @@ -341,7 +344,7 @@ shared_ptr VirtualServerManager::create_server(std::string hosts, if(!sid_success) return nullptr; - this->delete_server_in_db(serverId, true); /* just to ensure */ + this->delete_server_in_db(serverId, false); /* just to ensure */ sql::command(this->handle->getSql(), "INSERT INTO `servers` (`serverId`, `host`, `port`) VALUES (:sid, :host, :port)", variable{":sid", serverId}, variable{":host", hosts}, variable{":port", port}).executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); auto prop_copy = sql::command(this->handle->getSql(), "INSERT INTO `properties` (`serverId`, `type`, `id`, `key`, `value`) SELECT :target_sid AS `serverId`, `type`, `id`, `key`, `value` FROM `properties` WHERE `type` = :type AND `id` = 0 AND `serverId` = 0;", @@ -419,7 +422,8 @@ bool VirtualServerManager::deleteServer(shared_ptr server) { } this->handle->properties()[property::SERVERINSTANCE_SPOKEN_TIME_DELETED] += server->properties()[property::VIRTUALSERVER_SPOKEN_TIME].as(); - this->delete_server_in_db(server->serverId, true); + this->delete_server_in_db(server->serverId, false); + this->handle->databaseHelper()->handleServerDelete(server->serverId); if(auto file_vs = file::server()->find_virtual_server(server->getServerId()); file_vs) { using ErrorType = file::filesystem::ServerCommandErrorType; @@ -517,7 +521,7 @@ if(!result) { \ } #define execute_change(table, column) \ -result = sql::command(this->handle->getSql(), "UPDATE TABLE `" table "` SET `" column "` = :nsid WHERE `" column "` = :osid;", \ +result = sql::command(this->handle->getSql(), "UPDATE `" table "` SET `" column "` = :nsid WHERE `" column "` = :osid;", \ variable{":osid", old_id}, variable{":nsid", new_id}).execute(); \ if(!result) { \ logWarning(LOG_INSTANCE, "Failed to execute server id change on table {} (column {}): {}", table, column, result.fmtStr()); \ diff --git a/server/src/VirtualServerManager.h b/server/src/VirtualServerManager.h index f0e8c93..34663fd 100644 --- a/server/src/VirtualServerManager.h +++ b/server/src/VirtualServerManager.h @@ -64,12 +64,9 @@ namespace ts::server { void executeAutostart(); void shutdownAll(const std::string&); - //Dotn use shared_ptr references to keep sure that they be hold in memory + //Don't use shared_ptr references to keep sure that they be hold in memory bool createServerSnapshot(Command &cmd, std::shared_ptr server, int version, std::string &error); - std::shared_ptr createServerFromSnapshot(std::shared_ptr old, std::string, uint16_t, const ts::Command &, std::string &); - - //Dotn use shared_ptr references to keep sure that they be hold in memory - SnapshotDeployResult deploy_snapshot(std::string& /* error */, std::shared_ptr /* target server */, const command_parser& /* source */); + SnapshotDeployResult deploy_snapshot(std::string& /* error */, std::shared_ptr& /* target server */, const command_parser& /* source */); udp::PuzzleManager* rsaPuzzles() { return this->puzzles; } diff --git a/server/src/channel/ServerChannel.cpp b/server/src/channel/ServerChannel.cpp index 0d3b72f..42f8664 100644 --- a/server/src/channel/ServerChannel.cpp +++ b/server/src/channel/ServerChannel.cpp @@ -529,6 +529,8 @@ int ServerChannelTree::loadChannelFromData(int argc, char **data, char **column) //assert(type != 0xFF); assert(channelId != 0); + if(channelId == 0) + return 0; auto channel = std::make_shared(parentId, channelId); auto server = this->server.lock(); diff --git a/server/src/client/ConnectedClient.cpp b/server/src/client/ConnectedClient.cpp index 513bae6..ce69df9 100644 --- a/server/src/client/ConnectedClient.cpp +++ b/server/src/client/ConnectedClient.cpp @@ -798,6 +798,30 @@ bool ConnectedClient::handleCommandFull(Command& cmd, bool disconnectOnFail) { command_result result; try { result.reset(this->handleCommand(cmd)); + } catch(command_value_cast_failed& ex){ + auto message = ex.key() + " at " + std::to_string(ex.index()) + " could not be casted to " + ex.target_type().name(); + if(disconnectOnFail) { + this->disconnect(message); + return false; + } else { + result.reset(command_result{error::parameter_convert, message}); + } + } catch(command_bulk_index_out_of_bounds_exception& ex){ + auto message = "missing bulk for index " + std::to_string(ex.index()); + if(disconnectOnFail) { + this->disconnect(message); + return false; + } else { + result.reset(command_result{error::parameter_invalid_count, message}); + } + } catch(command_value_missing_exception& ex){ + auto message = "missing value for " + ex.key() + (ex.bulk_index() == std::string::npos ? "" : " at " + std::to_string(ex.bulk_index())); + if(disconnectOnFail) { + this->disconnect(message); + return false; + } else { + result.reset(command_result{error::parameter_missing, message}); + } } catch(invalid_argument& ex){ logWarning(this->getServerId(), "{}[Command] Failed to handle command. Received invalid argument exception: {}", CLIENT_STR_LOG_PREFIX, ex.what()); if(disconnectOnFail) { diff --git a/server/src/client/ConnectedClientTextCommandHandler.cpp b/server/src/client/ConnectedClientTextCommandHandler.cpp index 7e9546a..7726c06 100644 --- a/server/src/client/ConnectedClientTextCommandHandler.cpp +++ b/server/src/client/ConnectedClientTextCommandHandler.cpp @@ -647,7 +647,7 @@ bool ConnectedClient::handle_text_command( //send_message(_this.lock(), " IN " + type.name() + " => generation: " + to_string(genestis[type.type()].generation()) + " id: " + to_string(genestis[type.type()].current_packet_id())); } return true; - } else if(TARG(0, "ping")) { + } else if(TARG(0, "rtt")) { auto vc = dynamic_pointer_cast(_this.lock()); if(!vc) return false; @@ -681,6 +681,16 @@ bool ConnectedClient::handle_text_command( return true; } return true; + } else if(TARG(0, "ping")) { + auto vc = dynamic_pointer_cast(_this.lock()); + if(!vc) return false; + + auto ping_handler = vc->getConnection()->ping_handler(); + send_message(this->ref(), "Ping: " + std::to_string(ping_handler.current_ping().count())); + + auto last_response_ms = std::chrono::ceil(std::chrono::system_clock::now() - ping_handler.last_ping_response()).count(); + send_message(this->ref(), "Last ping response: " + std::to_string(last_response_ms)); + return true; } else if(TARG(0, "disconnect")) { auto vc = dynamic_pointer_cast(_this.lock()); if(!vc) return false; diff --git a/server/src/client/SpeakingClientHandshake.cpp b/server/src/client/SpeakingClientHandshake.cpp index b224647..97678bf 100644 --- a/server/src/client/SpeakingClientHandshake.cpp +++ b/server/src/client/SpeakingClientHandshake.cpp @@ -35,7 +35,7 @@ command_result SpeakingClient::handleCommandHandshakeBegin(Command& cmd) { //If if(authenticationMethod == IdentityType::TEAMSPEAK) { this->handshake.identityType = IdentityType::TEAMSPEAK; - auto identity = base64::decode(cmd["publicKey"]); + auto identity = base64::decode(cmd["publicKey"].string()); this->properties()[property::CLIENT_UNIQUE_IDENTIFIER] = base64::encode(digest::sha1(cmd["publicKey"].string())); this->handshake.identityKey = shared_ptr(new ecc_key{}, free_ecc); @@ -144,7 +144,7 @@ command_result SpeakingClient::handleCommandHandshakeIdentityProof(Command& cmd) this->properties()[property::CLIENT_TEAFORO_NAME] = (*this->handshake.identityData)["user_name"].asString(); this->handshake.state = HandshakeState::SUCCEEDED; } else if(this->handshake.identityType == IdentityType::TEAMSPEAK) { - auto proof = base64::decode(cmd["proof"]); + auto proof = base64::decode(cmd["proof"].string()); int result; if(ecc_verify_hash((u_char*) proof.data(), proof.length(), (u_char*) this->handshake.proof_message.data(), this->handshake.proof_message.length(), &result, this->handshake.identityKey.get()) != CRYPT_OK) diff --git a/server/src/client/command_handler/client.cpp b/server/src/client/command_handler/client.cpp index 51a6173..62e27df 100644 --- a/server/src/client/command_handler/client.cpp +++ b/server/src/client/command_handler/client.cpp @@ -367,23 +367,6 @@ command_result ConnectedClient::handleCommandClientChatClosed(Command &cmd) { return command_result{error::ok}; } -constexpr static auto kDBListQuery{R"( -SELECT `clients`.*, `properties`.`value` as `client_description` FROM ( - SELECT - `clients_server`.`client_database_id`, - `clients_server`.`client_unique_id`, - `clients_server`.`client_nickname`, - `clients_server`.`client_ip`, - `clients_server`.`client_created`, - `clients_server`.`client_last_connected`, - `clients_server`.`client_total_connections`, - `clients`.`client_login_name` FROM `clients_server` - INNER JOIN `clients` ON `clients`.`client_database_id` = `clients_server`.`client_database_id` - WHERE `server_id` = :serverId LIMIT :offset, :limit -) AS `clients` -LEFT JOIN `properties` ON `properties`.serverId = :serverId AND `properties`.key = 'client_description' AND `properties`.`id` = `clients`.`client_database_id` -)"}; - command_result ConnectedClient::handleCommandClientDbList(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; @@ -398,59 +381,32 @@ command_result ConnectedClient::handleCommandClientDbList(Command &cmd) { ts::command_builder result{this->notify_response_command("notifyclientdblist")}; result.reserve_bulks(limit); - size_t command_index{0}, set_index{0}; + struct CallbackArgument { + ts::command_builder& result; + bool show_ip{false}; + size_t command_index{0}; + }; - auto show_ip = permission::v2::permission_granted(1, this->calculate_permission(permission::b_client_remoteaddress_view, 0)); + CallbackArgument callback_argument{result}; + callback_argument.show_ip = permission::v2::permission_granted(1, this->calculate_permission(permission::b_client_remoteaddress_view, 0)); + serverInstance->databaseHelper()->listDatabaseClients(this->getServerId(), { offset }, { limit }, [](void* ptr_data, const DatabaseClient& client) { + auto argument = (CallbackArgument*) ptr_data; - auto sqlResult = sql::command{this->server->getSql(), kDBListQuery, variable{":serverId", this->getServerId()}, variable{":offset", offset}, variable{":limit", limit}} - .query([&](int length, std::string* values, std::string* names) { - set_index++; - auto bulk = result.bulk(command_index++); - bulk.reserve(300); + auto bulk = argument->result.bulk(argument->command_index++); + bulk.reserve(300); - auto index{0}; - try { - assert(names[index] == "client_database_id"); - bulk.put_unchecked("cldbid", values[index++]); + bulk.put_unchecked("cldbid", client.client_database_id); + bulk.put_unchecked("client_unique_identifier", client.client_unique_id); + bulk.put_unchecked("client_nickname", client.client_nickname); + bulk.put_unchecked("client_lastip", argument->show_ip ? client.client_ip : "hidden"); + bulk.put_unchecked("client_lastconnected", client.client_last_connected); + bulk.put_unchecked("client_created", client.client_created); + bulk.put_unchecked("client_totalconnections", client.client_total_connections); + bulk.put_unchecked("client_login_name", client.client_login_name); + bulk.put_unchecked("client_description", client.client_description); + }, &callback_argument); - assert(names[index] == "client_unique_id"); - bulk.put_unchecked("client_unique_identifier", values[index++]); - - assert(names[index] == "client_nickname"); - bulk.put_unchecked("client_nickname", values[index++]); - - assert(names[index] == "client_ip"); - bulk.put_unchecked("client_lastip", show_ip ? values[index++] : "hidden"); - - assert(names[index] == "client_created"); - bulk.put_unchecked("client_lastconnected", values[index++]); - - assert(names[index] == "client_last_connected"); - bulk.put_unchecked("client_created", values[index++]); - - assert(names[index] == "client_total_connections"); - bulk.put_unchecked("client_totalconnections", values[index++]); - - assert(names[index] == "client_login_name"); - bulk.put_unchecked("client_login_name", values[index++]); - - assert(names[index] == "client_description"); - bulk.put_unchecked("client_description", values[index++]); - - assert(index == length); - } catch (std::exception& ex) { - command_index--; - logError(this->getServerId(), "Failed to parse client base properties at index {}: {}. Offset: {} Limit: {} Set: {}", - index - 1, - ex.what(), - offset, - limit, - set_index - 1 - ); - } - }); - - if (command_index == 0) + if (callback_argument.command_index == 0) return command_result{error::database_empty_result}; if (cmd.hasParm("count")) { diff --git a/server/src/client/query/QueryClientCommands.cpp b/server/src/client/query/QueryClientCommands.cpp index 0807629..9cd8939 100644 --- a/server/src/client/query/QueryClientCommands.cpp +++ b/server/src/client/query/QueryClientCommands.cpp @@ -1,6 +1,7 @@ #include "Properties.h" #include "query/Command.h" #include +#include #include #include #include @@ -834,6 +835,7 @@ command_result QueryClient::handleCommandBindingList(Command& cmd) { //TODO with mapping! command_result QueryClient::handleCommandServerSnapshotDeploy(Command& cmd) { +#if 0 CMD_RESET_IDLE; auto start = system_clock::now(); @@ -903,20 +905,23 @@ command_result QueryClient::handleCommandServerSnapshotDeploy(Command& cmd) { res["time"] = duration_cast(end - start).count(); this->sendCommand(res); return command_result{error::ok}; +#else + return command_result{error::command_not_found}; +#endif } command_result QueryClient::handleCommandServerSnapshotDeployNew(const ts::command_parser &command) { CMD_RESET_IDLE; if(this->server) { - return command_result{error::not_implemented}; ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_virtualserver_snapshot_deploy, 1); } else { ACTION_REQUIRES_INSTANCE_PERMISSION(permission::b_virtualserver_snapshot_deploy, 1); } std::string error{}; - auto result = serverInstance->getVoiceServerManager()->deploy_snapshot(error, this->server, command); + auto server = this->server; + auto result = serverInstance->getVoiceServerManager()->deploy_snapshot(error, server, command); using SnapshotDeployResult = VirtualServerManager::SnapshotDeployResult; switch (result) { @@ -935,6 +940,12 @@ command_result QueryClient::handleCommandServerSnapshotDeployNew(const ts::comma case SnapshotDeployResult::CUSTOM_ERROR: return command_result{error::vs_critical, error}; } + + ts::command_builder notify{""}; + notify.put_unchecked(0, "virtualserver_port", server->properties()[property::VIRTUALSERVER_PORT].value()); + notify.put_unchecked(0, "sid", server->getServerId()); + this->sendCommand(notify, false); + return command_result{error::ok}; } @@ -943,22 +954,50 @@ command_result QueryClient::handleCommandServerSnapshotCreate(Command& cmd) { CMD_RESET_IDLE; CMD_REQ_SERVER; - Command result(""); + Command snapshot_command(""); string error; int version = cmd[0].has("version") ? cmd[0]["version"] : -1; - if(version == -1 && (cmd.hasParm("lagacy") || cmd.hasParm("legacy"))) + if(version == -1 && (cmd.hasParm("lagacy") || cmd.hasParm("legacy"))) { version = 0; - if(!serverInstance->getVoiceServerManager()->createServerSnapshot(result, this->server, version, error)) + } + + if(!serverInstance->getVoiceServerManager()->createServerSnapshot(snapshot_command, this->server, version, error)) { return command_result{error::vs_critical, error}; + } - string data = result.build(); - auto buildHash = base64::encode(digest::sha1(data)); + if(version == -1 || version >= 3) { + auto build_version = snapshot_command[0]["snapshot_version"].as(); + snapshot_command.pop_bulk(); - result.push_bulk_front(); - result[0]["hash"] = buildHash; + auto snapshot_data = snapshot_command.build(); + auto max_compressed_size = ZSTD_compressBound(snapshot_data.size()); + if(ZSTD_isError(max_compressed_size)) { + return command_result{error::vs_critical, "failed to calculate compressed size: " + std::string{ZSTD_getErrorName(max_compressed_size)}}; + } - this->sendCommand(result); + std::string buffer{}; + buffer.resize(max_compressed_size); + + auto compressed_size = ZSTD_compress(buffer.data(), buffer.size(), snapshot_data.data(), snapshot_data.length(), 1); + if(ZSTD_isError(compressed_size)) { + return command_result{error::vs_critical, "failed to compressed snapshot: " + std::string{ZSTD_getErrorName(compressed_size)}}; + } + + ts::command_builder result{""}; + result.bulk(0).reserve(100 + compressed_size * 4/3); + result.put_unchecked(0, "snapshot_version", build_version); + result.put_unchecked(0, "data", base64::encode(std::string_view{buffer.data(), compressed_size})); + this->sendCommand(result, false); + } else { + auto data = snapshot_command.build(); + auto buildHash = base64::encode(digest::sha1(data)); + + snapshot_command.push_bulk_front(); + snapshot_command[0]["hash"] = buildHash; + + this->sendCommand(snapshot_command); + } return command_result{error::ok}; } diff --git a/server/src/client/voice/CryptSetupHandler.cpp b/server/src/client/voice/CryptSetupHandler.cpp index 2485f09..32f6e96 100644 --- a/server/src/client/voice/CryptSetupHandler.cpp +++ b/server/src/client/voice/CryptSetupHandler.cpp @@ -71,7 +71,7 @@ CryptSetupHandler::CommandResult CryptSetupHandler::handleCommandClientInitIv(co std::unique_lock state_lock{client->state_lock}; if(client->connectionState() == ConnectionState::CONNECTED) { /* we've a reconnect */ - auto lastPingResponse = this->connection->ping_handler().last_ping_response(); + auto lastPingResponse = std::max(this->connection->ping_handler().last_ping_response(), this->connection->ping_handler().last_command_acknowledged()); if(std::chrono::system_clock::now() - lastPingResponse < std::chrono::seconds(5)) { logMessage(this->connection->virtual_server_id(), "{} Client initialized session reconnect, but last ping response is not older then 5 seconds ({}). Ignoring attempt", this->connection->log_prefix(), @@ -162,6 +162,11 @@ CryptSetupHandler::CommandResult CryptSetupHandler::handleCommandClientInitIv(co this->seed_server = std::string{beta, server_seed_length}; } + auto server_public_key = client->getServer()->publicServerKey(); + if(server_public_key.empty()) { + return ts::command_result{error::vs_critical, "failed to export server public key"}; + } + if(this->new_protocol) { //Pre setup //Generate chain @@ -193,7 +198,7 @@ CryptSetupHandler::CommandResult CryptSetupHandler::handleCommandClientInitIv(co answer.put_unchecked(0, "time", std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()); answer.put_unchecked(0, "l", base64::encode(crypto_chain)); answer.put_unchecked(0, "beta", base64::encode(this->seed_server)); - answer.put_unchecked(0, "omega", client->getServer()->publicServerKey()); + answer.put_unchecked(0, "omega", server_public_key); answer.put_unchecked(0, "proof", base64::encode((const char*) sign_buffer, sign_buffer_size)); answer.put_unchecked(0, "tvd", ""); answer.put_unchecked(0, "root", base64::encode((char*) this->chain_data->public_key, 32)); @@ -206,11 +211,6 @@ CryptSetupHandler::CommandResult CryptSetupHandler::handleCommandClientInitIv(co } else { debugMessage(this->connection->virtual_server_id(), "{} Got non client 3.1 protocol with build timestamp {}", this->connection->log_prefix(), this->client_protocol_time_, this->client_protocol_time_); - auto server_public_key = client->getServer()->publicServerKey(); - if(server_public_key.empty()) { - return ts::command_result{error::vs_critical, "failed to export server public key"}; - } - { ts::command_builder answer{"initivexpand"}; answer.put_unchecked(0, "alpha", base64::encode(this->seed_client)); diff --git a/server/src/client/voice/PacketEncoder.cpp b/server/src/client/voice/PacketEncoder.cpp index bf47ae6..4b051a5 100644 --- a/server/src/client/voice/PacketEncoder.cpp +++ b/server/src/client/voice/PacketEncoder.cpp @@ -4,12 +4,9 @@ #include "PacketEncoder.h" #include -#include #include #include -#include "../../ConnectionStatistics.h" - using namespace ts; using namespace ts::server::server::udp; @@ -164,34 +161,40 @@ void PacketEncoder::send_command(const std::string_view &command, bool low, std: packets_tail = &packet->next; } - { std::lock_guard id_lock{this->packet_id_mutex}; - uint32_t full_id; - auto head = packets_head; - while(head) { - full_id = this->packet_id_manager.generate_full_id(ptype); + { + uint32_t full_id; + auto head = packets_head; + while(head) { + full_id = this->packet_id_manager.generate_full_id(ptype); - head->set_packet_id(full_id & 0xFFFFU); - head->generation = full_id >> 16U; - head = head->next; + head->set_packet_id(full_id & 0xFFFFU); + head->generation = full_id >> 16U; + + /* loss stats (In order required so we're using the this->packet_id_mutex) */ + this->packet_statistics_->send_command(head->packet_type(), full_id); + + head = head->next; + } } } packets_head->type_and_flags |= head_pflags; /* general stats */ - auto head = packets_head; - while(head) { - this->callback_connection_stats(this->callback_data, StatisticsCategory::COMMAND, head->packet_length() + 96); /* 96 for the UDP overhead */ - head = head->next; + { + auto head = packets_head; + while(head) { + this->callback_connection_stats(this->callback_data, StatisticsCategory::COMMAND, head->packet_length() + 96); /* 96 for the UDP overhead */ + head = head->next; + } } - /* loss stats */ + /* ack handler */ { auto head = packets_head; while(head) { auto full_packet_id = (uint32_t) (head->generation << 16U) | head->packet_id(); - this->packet_statistics_->send_command(head->packet_type(), full_packet_id); /* increase a reference for the ack handler */ head->ref(); @@ -230,9 +233,18 @@ void PacketEncoder::encrypt_pending_packets() { return; auto packet = packets_head; + auto packet_tail = packet; while(packet) { this->prepare_outgoing_packet(packet); packet = packet->next; + if(packet) + packet_tail = packet; + } + + { + std::lock_guard wlock{this->write_queue_mutex}; + *this->resend_queue_tail = packets_head; + this->resend_queue_tail = &packet_tail->next; } } @@ -267,7 +279,7 @@ bool PacketEncoder::prepare_outgoing_packet(ts::protocol::OutgoingServerPacket * } PacketEncoder::BufferPopResult PacketEncoder::pop_write_buffer(protocol::OutgoingServerPacket *&result) { - bool need_prepare_packet{false}, more_packets{false}; + bool need_prepare_packet{false}, more_packets; { std::lock_guard wlock{this->write_queue_mutex}; if(this->resend_queue_head) { diff --git a/server/src/client/voice/PingHandler.h b/server/src/client/voice/PingHandler.h index a43223e..b4187a7 100644 --- a/server/src/client/voice/PingHandler.h +++ b/server/src/client/voice/PingHandler.h @@ -18,13 +18,14 @@ namespace ts::server::server::udp { [[nodiscard]] inline std::chrono::milliseconds current_ping() const { return this->current_ping_; } [[nodiscard]] inline std::chrono::system_clock::time_point last_ping_response() const { return this->last_response_; } + [[nodiscard]] inline std::chrono::system_clock::time_point last_command_acknowledged() const { return this->last_command_acknowledge_; } void* callback_argument{nullptr}; callback_send_ping_t callback_send_ping{nullptr}; callback_send_recovery_command_t callback_send_recovery_command{nullptr}; callback_time_outed_t callback_time_outed{nullptr}; private: - constexpr static std::chrono::milliseconds kPingRequestInterval{2500}; + constexpr static std::chrono::milliseconds kPingRequestInterval{1000}; constexpr static std::chrono::milliseconds kPingTimeout{15 * 1000}; constexpr static std::chrono::milliseconds kRecoveryRequestInterval{1000}; diff --git a/server/src/manager/SqlDataManager.cpp b/server/src/manager/SqlDataManager.cpp index fbe5eb8..fef3f2d 100644 --- a/server/src/manager/SqlDataManager.cpp +++ b/server/src/manager/SqlDataManager.cpp @@ -170,11 +170,18 @@ bool SqlDataManager::initialize(std::string& error) { } else { res = sql::command(this->sql(), "UPDATE `general` SET `value`= :value WHERE `key`= :key;", variable{":key", "lock_test"}, variable{":value", "TeaSpeak created by WolverinDEV <3"}).execute(); } + if(!res) { if(res.msg().find("database is locked") != string::npos) error = "database is locked"; else error = "Failed to execute lock test! Command result: " + res.fmtStr(); return false; } + + res = sql::command{this->sql(), "DELETE FROM `general` WHERE `key` = :key ORDER BY `key` LIMIT -1 OFFSET 1;", variable{":key", "lock_test"}}.execute(); + if(!res) { + error = res.fmtStr(); + return false; + } } if(!this->update_permissions(error)) { @@ -500,16 +507,17 @@ bool SqlDataManager::update_database(std::string &error) { } case 13: { - constexpr static std::array kUpdateCommands{ + constexpr static std::array kUpdateCommands{ "CREATE INDEX `idx_properties_serverid_id_value` ON `properties` (`serverId`, `id`, `key`);", + "CREATE INDEX `idx_properties_serverid_id_type` ON `properties` (`serverId`, `id`, `type`);", - "CREATE TABLE `groups_v2` (`serverId` INT NOT NULL, `groupId` INTEGER AUTO_INCREMENT NOT NULL PRIMARY KEY, `target` INT, `type` INT, `displayName` VARCHAR(128), `original_group_id` INTEGER);", + "CREATE TABLE `groups_v2` (`serverId` INT NOT NULL, `groupId` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `target` INT, `type` INT, `displayName` VARCHAR(128), `original_group_id` INTEGER);", "INSERT INTO `groups_v2` (`serverId`, `groupId`, `target`, `type`, `displayName`) SELECT `serverId`, `groupId`, `target`, `type`, `displayName` FROM `groups`;", "DROP TABLE `groups`;", "ALTER TABLE `groups_v2` RENAME TO groups;", - "CREATE INDEX `idx_groups_serverId_groupId` ON `groups` (`server_id`);", - "CREATE INDEX `idx_groups_serverid` ON `groups` (`server_id`, `groupId`);" + "CREATE INDEX `idx_groups_serverId_groupId` ON `groups` (`serverId`);", + "CREATE INDEX `idx_groups_serverid` ON `groups` (`serverId`, `groupId`);" }; if(!execute_commands(this->sql(), error, kUpdateCommands)) diff --git a/server/src/manager/TokeManager.cpp b/server/src/manager/TokeManager.cpp index 3fa1a1b..c922f4e 100644 --- a/server/src/manager/TokeManager.cpp +++ b/server/src/manager/TokeManager.cpp @@ -30,7 +30,7 @@ bool TokenManager::loadTokens() { } int TokenManager::loadTokenFromDb(int length, char **values, char **columns) { - std::string token, description = ""; + std::string token, description; auto type = static_cast(0xFF); GroupId group = 0; ChannelId chId = 0; diff --git a/server/src/snapshots/deploy.cpp b/server/src/snapshots/deploy.cpp index e02b529..5295d8b 100644 --- a/server/src/snapshots/deploy.cpp +++ b/server/src/snapshots/deploy.cpp @@ -16,17 +16,15 @@ using namespace ts; using namespace ts::server; -using SnapshotType = ts::server::snapshots::type; -using SnapshotVersion = ts::server::snapshots::version_t; constexpr static ServerId kSnapshotServerId{0xFFFF}; -VirtualServerManager::SnapshotDeployResult VirtualServerManager::deploy_snapshot(std::string &error, std::shared_ptr server, const command_parser &command) { +VirtualServerManager::SnapshotDeployResult VirtualServerManager::deploy_snapshot(std::string &error, std::shared_ptr& server, const command_parser &command) { if(!server) { - auto instances = this->serverInstances(); - if(config::server::max_virtual_server != -1 && instances.size() > config::server::max_virtual_server) + auto instance_count = this->serverInstances().size(); + if(config::server::max_virtual_server != -1 && instance_count > config::server::max_virtual_server) return SnapshotDeployResult::REACHED_CONFIG_SERVER_LIMIT; - if(instances.size() >= 65534) + if(instance_count >= 65534) return SnapshotDeployResult::REACHED_SOFTWARE_SERVER_LIMIT; } @@ -36,7 +34,7 @@ VirtualServerManager::SnapshotDeployResult VirtualServerManager::deploy_snapshot return SnapshotDeployResult::REACHED_SERVER_ID_LIMIT; } - this->delete_server_in_db(kSnapshotServerId, true); + this->delete_server_in_db(kSnapshotServerId, false); auto result = sql::command{this->handle->getSql(), "INSERT INTO `servers` (`serverId`, `host`, `port`) VALUES (:sid, :host, :port)"} .value(":sid", kSnapshotServerId) .value(":host", server ? server->properties()[property::VIRTUALSERVER_HOST].value() : config::binding::DefaultVoiceHost) @@ -53,7 +51,7 @@ VirtualServerManager::SnapshotDeployResult VirtualServerManager::deploy_snapshot } if(!server) { - this->delete_server_in_db(target_server_id, true); + this->delete_server_in_db(target_server_id, false); this->change_server_id_in_db(kSnapshotServerId, target_server_id); } else { this->deleteServer(server); @@ -66,6 +64,7 @@ VirtualServerManager::SnapshotDeployResult VirtualServerManager::deploy_snapshot //FIXME error handling } + server->properties()[property::VIRTUALSERVER_ASK_FOR_PRIVILEGEKEY] = false; { threads::MutexLock l(this->instanceLock); this->instances.push_back(server); @@ -74,614 +73,781 @@ VirtualServerManager::SnapshotDeployResult VirtualServerManager::deploy_snapshot return SnapshotDeployResult::SUCCESS; } -#if 0 -bool VirtualServerManager::deploy_snapshot(std::string &error, ts::ServerId server_id, const ts::command_parser &command) { - auto result = this->try_deploy_snapshot(error, server_id, command); - if(!result) this->delete_server_in_db(server_id, true); - return result; -} -#endif - bool VirtualServerManager::try_deploy_snapshot(std::string &error, ts::ServerId target_server_id, ts::ServerId logging_server_id, const ts::command_parser &command) { snapshots::snapshot_data snapshot_data{}; if(!snapshots::parse_snapshot(snapshot_data, error, logging_server_id, command)) return false; - std::map client_id_mapping{}; - std::map channel_id_mapping{}; - std::map channel_group_id_mapping{}; - std::map server_group_id_mapping{}; - std::map playlist_id_mapping{}; + snapshots::snapshot_mappings mappings{}; - /* lets start inserting data to the database */ + /* register clients */ { - /* register server & properties */ - { + sql::InsertQuery insert_general_query{"clients", + sql::Column("client_unique_id"), + sql::Column("client_created") + }; - sql::InsertQuery insert_property_query{"properties", - sql::Column("serverId"), - sql::Column("type"), - sql::Column("id"), - sql::Column("key"), - sql::Column("value") - }; + auto current_time_seconds = std::chrono::floor(std::chrono::system_clock::now().time_since_epoch()).count(); + for(const auto& client : snapshot_data.parsed_clients) { + insert_general_query.add_entry( + client.unique_id, + std::chrono::floor(client.timestamp_created.time_since_epoch()).count() + ); + } - for(const auto& property : snapshot_data.parsed_server.properties.list_properties(property::FLAG_SAVE)) { + auto result = insert_general_query.execute(this->handle->getSql(), true); + for(const auto& fail : result.failed_entries) + logWarning(logging_server_id, "Failed to insert client {} into the general client database: {} (This should not happen!)", snapshot_data.parsed_clients[std::get<0>(fail)].database_id, std::get<1>(fail).fmtStr()); + + + sql::model insert_command{this->handle->getSql(), + "INSERT INTO `clients_server` (`server_id`, `client_unique_id`, `client_database_id`, `client_created`, `original_client_id`) SELECT :serverId, :uniqueId, `client_database_id`, :timestamp, :org_id FROM `clients` WHERE `client_unique_id` = :uniqueId;"}; + + insert_command.value(":serverId", target_server_id); + + for(const auto& client : snapshot_data.parsed_clients) { + auto sqlResult = insert_command.command().values( + variable{":uniqueId", client.unique_id}, + variable{":timestamp", std::chrono::floor(client.timestamp_created.time_since_epoch()).count()}, + variable{":org_id", client.database_id} + ).execute(); + + if(!sqlResult) + logWarning(logging_server_id, "Failed to insert client {} ({}) into the database: {}", client.unique_id, client.nickname, sqlResult.fmtStr()); + } + + for(const auto& client : snapshot_data.music_bots) { + auto sqlResult = insert_command.command().values( + variable{":uniqueId", client.unique_id}, + variable{":timestamp", current_time_seconds}, + variable{":org_id", client.database_id} + ).execute(); + + if(!sqlResult) + logWarning(logging_server_id, "Failed to insert music bot {} into the database: {}", client.unique_id, sqlResult.fmtStr()); + } + + auto map_query_result = sql::command{this->handle->getSql(), "SELECT `original_client_id`,`client_database_id` FROM `clients_server` WHERE `server_id` = :serverId;"} + .value(":serverId", target_server_id) + .query([&](int length, std::string* values, std::string* names) { + ClientDbId original_id, new_id; + try { + original_id = std::stoull(values[0]); + new_id = std::stoull(values[1]); + } catch (std::exception& ex) { + logWarning(logging_server_id, "Failed to parse client database entry mapping for group id {} (New ID: {})", values[1], values[0]); + return; + } + mappings.client_id[original_id] = new_id; + }); + if(!map_query_result) { + error = "failed to query client database id mappings (" + map_query_result.fmtStr() + ")"; + return false; + } + } + + /* channels */ + { + /* Assign each channel a new id */ + ChannelId current_id{1}; + for(auto& channel : snapshot_data.parsed_channels) { + const auto new_id = current_id++; + mappings.channel_id[channel.properties[property::CHANNEL_ID]] = new_id; + channel.properties[property::CHANNEL_ID] = new_id; + } + + /* Update channel parents */ + for(auto& channel : snapshot_data.parsed_channels) { + auto pid = channel.properties[property::CHANNEL_PID].as(); + if(pid > 0) { + auto new_id = mappings.channel_id.find(pid); + if(new_id == mappings.channel_id.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; + } + } + + + sql::InsertQuery insert_query{"channels", + sql::Column("serverId"), + sql::Column("channelId"), + sql::Column("parentId") + }; + + sql::InsertQuery insert_property_query{"properties", + sql::Column("serverId"), + sql::Column("type"), + sql::Column("id"), + sql::Column("key"), + sql::Column("value") + }; + + for(auto& channel : snapshot_data.parsed_channels) { + auto channel_id = channel.properties[property::CHANNEL_ID].as(); + insert_query.add_entry( + target_server_id, + channel_id, + channel.properties[property::CHANNEL_PID].as() + ); + + for(const auto& property : channel.properties.list_properties(property::FLAG_SAVE)) { if(!property.isModified()) continue; insert_property_query.add_entry( - target_server_id, - property::PROP_TYPE_SERVER, - 0, - std::string{property.type().name}, - property.value() + target_server_id, + property::PROP_TYPE_CHANNEL, + channel_id, + std::string{property.type().name}, + property.value() ); } - - { - auto result = insert_property_query.execute(this->handle->getSql(), true); - if(!result.failed_entries.empty()) - logWarning(logging_server_id, "Failed to insert all server properties into the database. Failed property count: {}", result.failed_entries.size()); - } } - /* register clients and bots */ { - sql::InsertQuery insert_general_query{"clients", - sql::Column("client_unique_id"), - sql::Column("client_created") - }; - - auto current_time_seconds = std::chrono::floor(std::chrono::system_clock::now().time_since_epoch()).count(); - for(const auto& client : snapshot_data.parsed_clients) { - insert_general_query.add_entry( - client.unique_id, - std::chrono::floor(client.timestamp_created.time_since_epoch()).count() - ); - } - for(const auto& client : snapshot_data.music_bots) { - insert_general_query.add_entry( - client.unique_id, - current_time_seconds - ); - } - - auto result = insert_general_query.execute(this->handle->getSql(), true); + auto result = insert_query.execute(this->handle->getSql(), true); for(const auto& fail : result.failed_entries) - logWarning(logging_server_id, "Failed to insert client {} into the general client database: {} (This should not happen!)", snapshot_data.parsed_clients[std::get<0>(fail)].database_id, std::get<1>(fail).fmtStr()); - - - sql::model insert_command{this->handle->getSql(), - "INSERT INTO `clients_server` (`server_id`, `client_unique_id`, `client_database_id`, `client_created`, `original_client_id`) SELECT :serverId, :uniqueId, `client_database_id`, :timestamp, :org_id FROM `clients` WHERE `client_unique_id` = :uniqueId;"}; - - insert_command.value(":serverId", target_server_id); - - for(const auto& client : snapshot_data.parsed_clients) { - auto sqlResult = insert_command.command().values( - variable{":uniqueId", client.unique_id}, - variable{":timestamp", std::chrono::floor(client.timestamp_created.time_since_epoch()).count()}, - variable{":org_id", client.database_id} - ).execute(); - - if(!sqlResult) - logWarning(logging_server_id, "Failed to insert client {} ({}) into the database: {}", client.unique_id, client.nickname, sqlResult.fmtStr()); - } - - for(const auto& client : snapshot_data.music_bots) { - auto sqlResult = insert_command.command().values( - variable{":uniqueId", client.unique_id}, - variable{":timestamp", current_time_seconds}, - variable{":org_id", client.database_id} - ).execute(); - - if(!sqlResult) - logWarning(logging_server_id, "Failed to insert music bot {} into the database: {}", client.unique_id, sqlResult.fmtStr()); - } - - auto map_query_result = sql::command{this->handle->getSql(), "SELECT `original_client_id`,`client_database_id` FROM `clients_server` WHERE `server_id` = :sid;"} - .value(":serverId", target_server_id) - .query([&](int length, std::string* values, std::string* names) { - ClientDbId original_id{0}, new_id{0}; - try { - original_id = std::stoull(values[0]); - new_id = std::stoull(values[1]); - } catch (std::exception& ex) { - logWarning(logging_server_id, "Failed to parse client database entry mapping for group id {} (New ID: {})", values[1], values[0]); - return; - } - client_id_mapping[original_id] = new_id; - }); - if(!map_query_result) { - error = "failed to query client database id mappings (" + map_query_result.fmtStr() + ")"; - return false; - } + logWarning(logging_server_id, "Failed to insert channel {} into the server database: {}", snapshot_data.parsed_channels[std::get<0>(fail)].properties[property::CHANNEL_NAME].value(), std::get<1>(fail).fmtStr()); } - /* channels */ { - /* Assign each channel a new id */ - ChannelId current_id{0}; - for(auto& channel : snapshot_data.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; - } + auto result = insert_property_query.execute(this->handle->getSql(), true); + if(!result.failed_entries.empty()) + logWarning(logging_server_id, "Failed to insert some channel properties into the database. Failed property count: {}", result.failed_entries.size()); + } + } - /* Update channel parents */ - for(auto& channel : snapshot_data.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; + /* channel permissions */ + { + sql::InsertQuery insert_query{"permissions", + sql::Column("serverId"), + sql::Column("type"), + sql::Column("id"), + sql::Column("channelId"), + sql::Column("permId"), + + sql::Column("value"), + sql::Column("grant"), + sql::Column("flag_skip"), + sql::Column("flag_negate") + }; + + for(auto& entry : snapshot_data.channel_permissions) { + { + auto new_id = mappings.channel_id.find(entry.id1); + if(new_id == mappings.channel_id.end()) { + logWarning(logging_server_id, "Missing channel id mapping for channel permission entry (channel id: {}). Skipping permission insert.", entry.id1); + continue; } + entry.id1 = new_id->second; } - - sql::InsertQuery insert_query{"channels", - sql::Column("serverId"), - sql::Column("channelId"), - sql::Column("parentId") - }; - - sql::InsertQuery insert_property_query{"properties", - sql::Column("serverId"), - sql::Column("type"), - sql::Column("id"), - sql::Column("key"), - sql::Column("value") - }; - - for(auto& channel : snapshot_data.parsed_channels) { - auto channel_id = channel.properties[property::CHANNEL_ID].as(); + for(const auto& permission : entry.permissions) { insert_query.add_entry( target_server_id, - channel_id, - channel.properties[property::CHANNEL_PID].as() - ); - - for(const auto& property : channel.properties.list_properties(property::FLAG_SAVE)) { - if(!property.isModified()) continue; - - insert_property_query.add_entry( - target_server_id, - property::PROP_TYPE_CHANNEL, - channel_id, - std::string{property.type().name}, - property.value() - ); - } - } - - { - auto result = insert_query.execute(this->handle->getSql(), true); - for(const auto& fail : result.failed_entries) - logWarning(logging_server_id, "Failed to insert channel {} into the server database: {}", snapshot_data.parsed_channels[std::get<0>(fail)].properties[property::CHANNEL_NAME].value(), std::get<1>(fail).fmtStr()); - } - - { - auto result = insert_property_query.execute(this->handle->getSql(), true); - if(!result.failed_entries.empty()) - logWarning(logging_server_id, "Failed to insert some channel properties into the database. Failed property count: {}", result.failed_entries.size()); - } - } - - /* channel permissions */ - { - sql::InsertQuery insert_query{"permissions", - sql::Column("serverId"), - sql::Column("type"), - sql::Column("id"), - sql::Column("channelId"), - sql::Column("permId"), - - sql::Column("value"), - sql::Column("grant"), - sql::Column("flag_skip"), - sql::Column("flag_negate") - }; - - for(auto& entry : snapshot_data.channel_permissions) { - { - auto new_id = channel_id_mapping.find(entry.id1); - if(new_id == channel_id_mapping.end()) { - logWarning(logging_server_id, "Missing channel id mapping for channel permission entry (channel id: {}). Skipping permission insert.", entry.id1); - continue; - } - entry.id1 = new_id->second; - } - - for(const auto& permission : entry.permissions) { - insert_query.add_entry( - target_server_id, - permission::SQL_PERM_CHANNEL, - entry.id1, - 0, - permission.type->name, - permission.value.has_value ? permission.value.value : permNotGranted, - permission.granted.has_value ? permission.granted.value : permNotGranted, - permission.flag_skip, - permission.flag_negate - ); - } - } - - auto result = insert_query.execute(this->handle->getSql(), true); - if(!result.failed_entries.empty()) - logWarning(logging_server_id, "Failed to insert all channel permissions into the database. Failed permission count: {}", result.failed_entries.size()); - } - - /* server groups */ - { - sql::model insert_model{this->handle->getSql(), "INSERT INTO `groups` (`serverId`, `target`, `type`, `displayName`, `original_group_id`) VALUES (:serverId, :target, :type, :name, :id)"}; - insert_model.value(":serverId", target_server_id).value(":target", GroupTarget::GROUPTARGET_SERVER).value(":type", GroupType::GROUP_TYPE_NORMAL); - - for(auto& group : snapshot_data.parsed_server_groups) { - auto result = insert_model.command().value(":name", group.name).value(":id", group.group_id).execute(); - if(!result) - logWarning(logging_server_id, "Failed to insert server group \"{}\" into the database", group.name); - } - - sql::command{this->handle->getSql(), "SELECT `original_group_id`,`groupId` FROM `groups` WHERE `serverId` = :sid AND `target` = :target AND `type` = :type"} - .value(":serverId", target_server_id).value(":target", GroupTarget::GROUPTARGET_SERVER).value(":type", GroupType::GROUP_TYPE_NORMAL) - .query([&](int length, std::string* values, std::string* names) { - GroupId original_id{0}, new_id{0}; - try { - original_id = std::stoull(values[0]); - new_id = std::stoull(values[1]); - } catch (std::exception& ex) { - logWarning(logging_server_id, "Failed to parse server group mapping for group id {} (New ID: {})", values[1], values[0]); - return; - } - server_group_id_mapping[original_id] = new_id; - }); - } - - /* server group relations */ - { - sql::InsertQuery insert_query{"assignedGroups", - sql::Column("serverId"), - sql::Column("cldbid"), - sql::Column("groupId"), - sql::Column("channelId") - }; - - for(auto& relation : snapshot_data.parsed_server_group_relations) { - for(auto& entry : relation.second) { - ClientId client_id{}; - { - auto new_id = client_id_mapping.find(entry.client_id); - if(new_id == client_id_mapping.end()) { - logWarning(logging_server_id, "Missing client id mapping for channel group relation permission entry (client id: {}). Skipping relation insert.", entry.client_id); - continue; - } - client_id = new_id->second; - } - - insert_query.add_entry( - target_server_id, - client_id, - entry.group_id, - 0 - ); - } - } - - auto result = insert_query.execute(this->handle->getSql(), true); - if(!result.failed_entries.empty()) - logWarning(logging_server_id, "Failed to insert all server group relations into the database. Failed insert count: {}", result.failed_entries.size()); - } - - /* channel groups */ - { - sql::model insert_model{this->handle->getSql(), "INSERT INTO `groups` (`serverId`, `target`, `type`, `displayName`, `original_group_id`) VALUES (:serverId, :target, :type, :name, :id)"}; - insert_model.value(":serverId", target_server_id).value(":target", GroupTarget::GROUPTARGET_CHANNEL).value(":type", GroupType::GROUP_TYPE_NORMAL); - - for(auto& group : snapshot_data.parsed_server_groups) { - auto result = insert_model.command().value(":name", group.name).value(":id", group.group_id).execute(); - if(!result) - logWarning(logging_server_id, "Failed to insert channel group \"{}\" into the database", group.name); - } - - sql::command{this->handle->getSql(), "SELECT `original_group_id`,`groupId` FROM `groups` WHERE `serverId` = :sid AND `target` = :target AND `type` = :type"} - .value(":serverId", target_server_id).value(":target", GroupTarget::GROUPTARGET_CHANNEL).value(":type", GroupType::GROUP_TYPE_NORMAL) - .query([&](int length, std::string* values, std::string* names) { - GroupId original_id{0}, new_id{0}; - try { - original_id = std::stoull(values[0]); - new_id = std::stoull(values[1]); - } catch (std::exception& ex) { - logWarning(logging_server_id, "Failed to parse channel group mapping for group id {} (New ID: {})", values[1], values[0]); - return; - } - channel_group_id_mapping[original_id] = new_id; - }); - } - - /* channel group relations */ - { - sql::InsertQuery insert_query{"assignedGroups", - sql::Column("serverId"), - sql::Column("cldbid"), - sql::Column("groupId"), - sql::Column("channelId") - }; - - for(auto& relation : snapshot_data.parsed_channel_group_relations) { - ChannelId channel_id{}; - { - auto new_id = channel_id_mapping.find(relation.first); - if(new_id == channel_id_mapping.end()) { - logWarning(logging_server_id, "Missing channel id mapping for channel group relation entry (channel id: {}). Skipping relation insert.", relation.first); - continue; - } - channel_id = new_id->second; - } - - for(auto& entry : relation.second) { - ClientId client_id{}; - { - auto new_id = client_id_mapping.find(entry.client_id); - if(new_id == client_id_mapping.end()) { - logWarning(logging_server_id, "Missing client id mapping for channel group relation permission entry (client id: {}, channel id: {}). Skipping relation insert.", entry.client_id, relation.first); - continue; - } - client_id = new_id->second; - } - - insert_query.add_entry( - target_server_id, - client_id, - entry.group_id, - channel_id - ); - } - } - - auto result = insert_query.execute(this->handle->getSql(), true); - if(!result.failed_entries.empty()) - logWarning(logging_server_id, "Failed to insert all channel group relations into the database. Failed insert count: {}", result.failed_entries.size()); - } - - /* client permissions */ - { - sql::InsertQuery insert_query{"permissions", - sql::Column("serverId"), - sql::Column("type"), - sql::Column("id"), - sql::Column("channelId"), - sql::Column("permId"), - - sql::Column("value"), - sql::Column("grant"), - sql::Column("flag_skip"), - sql::Column("flag_negate"), - }; - - for(auto& entry : snapshot_data.client_permissions) { - { - auto new_id = client_id_mapping.find(entry.id1); - if(new_id == client_id_mapping.end()) { - logWarning(logging_server_id, "Missing client id mapping for client permission entry (client id: {}). Skipping permission insert.", entry.id1); - continue; - } - entry.id1 = new_id->second; - } - - for(const auto& permission : entry.permissions) { - insert_query.add_entry( - target_server_id, - permission::SQL_PERM_USER, - entry.id1, - 0, - permission.type->name, - permission.value.has_value ? permission.value.value : permNotGranted, - permission.granted.has_value ? permission.granted.value : permNotGranted, - permission.flag_skip, - permission.flag_negate - ); - } - } - - auto result = insert_query.execute(this->handle->getSql(), true); - if(!result.failed_entries.empty()) - logWarning(logging_server_id, "Failed to insert all client permissions into the database. Failed permission count: {}", result.failed_entries.size()); - } - - /* client channel permissions */ - { - sql::InsertQuery insert_query{"permissions", - sql::Column("serverId"), - sql::Column("type"), - sql::Column("id"), - sql::Column("channelId"), - sql::Column("permId"), - - sql::Column("value"), - sql::Column("grant"), - sql::Column("flag_skip"), - sql::Column("flag_negate"), - }; - - for(auto& entry : snapshot_data.client_channel_permissions) { - { - auto new_id = channel_id_mapping.find(entry.id1); - if(new_id == channel_id_mapping.end()) { - logWarning(logging_server_id, "Missing channel id mapping for client channel permission entry (client id: {}, channel id: {}). Skipping permission insert.", entry.id2, entry.id1); - continue; - } - entry.id1 = new_id->second; - } - - { - auto new_id = client_id_mapping.find(entry.id2); - if(new_id == client_id_mapping.end()) { - logWarning(logging_server_id, "Missing client id mapping for client channel permission entry (client id: {}, channel id: {}). Skipping permission insert.", entry.id2, entry.id1); - continue; - } - entry.id2 = new_id->second; - } - - for(const auto& permission : entry.permissions) { - insert_query.add_entry( - target_server_id, - permission::SQL_PERM_USER, - entry.id2, - (ChannelId) entry.id1, + permission::SQL_PERM_CHANNEL, + 0, + entry.id1, permission.type->name, permission.value.has_value ? permission.value.value : permNotGranted, permission.granted.has_value ? permission.granted.value : permNotGranted, permission.flag_skip, permission.flag_negate - ); - } + ); } - - auto result = insert_query.execute(this->handle->getSql(), true); - if(!result.failed_entries.empty()) - logWarning(logging_server_id, "Failed to insert all client channel permissions into the database. Failed permission count: {}", result.failed_entries.size()); } - /* music bot attributes */ + auto result = insert_query.execute(this->handle->getSql(), true); + if(!result.failed_entries.empty()) + logWarning(logging_server_id, "Failed to insert all channel permissions into the database. Failed permission count: {}", result.failed_entries.size()); + } + + /* server groups */ + { + sql::model insert_model{this->handle->getSql(), "INSERT INTO `groups` (`serverId`, `target`, `type`, `displayName`, `original_group_id`) VALUES (:serverId, :target, :type, :name, :id)"}; + insert_model.value(":serverId", target_server_id).value(":target", GroupTarget::GROUPTARGET_SERVER).value(":type", GroupType::GROUP_TYPE_NORMAL); + + for(auto& group : snapshot_data.parsed_server_groups) { + auto result = insert_model.command().value(":name", group.name).value(":id", group.group_id).execute(); + if(!result) + logWarning(logging_server_id, "Failed to insert server group \"{}\" into the database: {}", group.name, result.fmtStr()); + } + + sql::command{this->handle->getSql(), "SELECT `original_group_id`,`groupId` FROM `groups` WHERE `serverId` = :serverId AND `target` = :target AND `type` = :type"} + .value(":serverId", target_server_id).value(":target", GroupTarget::GROUPTARGET_SERVER).value(":type", GroupType::GROUP_TYPE_NORMAL) + .query([&](int length, std::string* values, std::string* names) { + GroupId original_id, new_id; + try { + original_id = std::stoull(values[0]); + new_id = std::stoull(values[1]); + } catch (std::exception& ex) { + logWarning(logging_server_id, "Failed to parse server group mapping for group id {} (New ID: {})", values[1], values[0]); + return; + } + mappings.server_group_id[original_id] = new_id; + }); + } + + /* server group relations */ + { + sql::InsertQuery insert_query{"assignedGroups", + sql::Column("serverId"), + sql::Column("cldbid"), + sql::Column("groupId"), + sql::Column("channelId") + }; + + for(auto& relation : snapshot_data.parsed_server_group_relations) { + for(auto& entry : relation.second) { + ClientId client_id{}; + { + auto new_id = mappings.client_id.find(entry.client_id); + if(new_id == mappings.client_id.end()) { + logWarning(logging_server_id, "Missing client id mapping for channel group relation permission entry (client id: {}). Skipping relation insert.", entry.client_id); + continue; + } + client_id = new_id->second; + } + + GroupId group_id; + { + auto new_id = mappings.server_group_id.find(entry.group_id); + if(new_id == mappings.server_group_id.end()) { + logWarning(logging_server_id, "Missing server group mapping of group {} for client {}. Skipping relation insert.", entry.group_id, entry.client_id); + continue; + } + group_id = new_id->second; + } + + insert_query.add_entry( + target_server_id, + client_id, + group_id, + 0 + ); + } + } + + auto result = insert_query.execute(this->handle->getSql(), true); + if(!result.failed_entries.empty()) + logWarning(logging_server_id, "Failed to insert all server group relations into the database. Failed insert count: {}", result.failed_entries.size()); + } + + /* server group permissions */ + { + sql::InsertQuery insert_query{"permissions", + sql::Column("serverId"), + sql::Column("type"), + sql::Column("id"), + sql::Column("channelId"), + sql::Column("permId"), + + sql::Column("value"), + sql::Column("grant"), + sql::Column("flag_skip"), + sql::Column("flag_negate"), + }; + + for(auto& group : snapshot_data.parsed_server_groups) { + GroupId group_id; + { + auto new_id = mappings.server_group_id.find(group.group_id); + if(new_id == mappings.server_group_id.end()) { + logWarning(logging_server_id, "Missing server group mapping of group {}. Skipping permission insert.", group.group_id); + continue; + } + group_id = new_id->second; + } + + for(const auto& permission : group.permissions) { + insert_query.add_entry( + target_server_id, + permission::SQL_PERM_GROUP, + group_id, + 0, + permission.type->name, + permission.value.has_value ? permission.value.value : permNotGranted, + permission.granted.has_value ? permission.granted.value : permNotGranted, + permission.flag_skip, + permission.flag_negate + ); + } + } + + auto result = insert_query.execute(this->handle->getSql(), true); + if(!result.failed_entries.empty()) + logWarning(logging_server_id, "Failed to insert some channel group permissions into the database. Failed permission count: {}", result.failed_entries.size()); + } + + /* channel groups */ + { + sql::model insert_model{this->handle->getSql(), "INSERT INTO `groups` (`serverId`, `target`, `type`, `displayName`, `original_group_id`) VALUES (:serverId, :target, :type, :name, :id)"}; + insert_model.value(":serverId", target_server_id).value(":target", GroupTarget::GROUPTARGET_CHANNEL).value(":type", GroupType::GROUP_TYPE_NORMAL); + + for(auto& group : snapshot_data.parsed_channel_groups) { + auto result = insert_model.command().value(":name", group.name).value(":id", group.group_id).execute(); + if(!result) + logWarning(logging_server_id, "Failed to insert channel group \"{}\" into the database: {}", group.name, result.fmtStr()); + } + + sql::command{this->handle->getSql(), "SELECT `original_group_id`,`groupId` FROM `groups` WHERE `serverId` = :serverId AND `target` = :target AND `type` = :type"} + .value(":serverId", target_server_id).value(":target", GroupTarget::GROUPTARGET_CHANNEL).value(":type", GroupType::GROUP_TYPE_NORMAL) + .query([&](int length, std::string* values, std::string* names) { + GroupId original_id, new_id; + try { + original_id = std::stoull(values[0]); + new_id = std::stoull(values[1]); + } catch (std::exception& ex) { + logWarning(logging_server_id, "Failed to parse channel group mapping for group id {} (New ID: {})", values[1], values[0]); + return; + } + mappings.channel_group_id[original_id] = new_id; + }); + } + + /* channel group relations */ + { + sql::InsertQuery insert_query{"assignedGroups", + sql::Column("serverId"), + sql::Column("cldbid"), + sql::Column("groupId"), + sql::Column("channelId") + }; + + for(auto& relation : snapshot_data.parsed_channel_group_relations) { + ChannelId channel_id; + { + auto new_id = mappings.channel_id.find(relation.first); + if(new_id == mappings.channel_id.end()) { + logWarning(logging_server_id, "Missing channel id mapping for channel group relation entry (channel id: {}). Skipping relation insert.", relation.first); + continue; + } + channel_id = new_id->second; + } + + for(auto& entry : relation.second) { + ClientId client_id; + { + auto new_id = mappings.client_id.find(entry.client_id); + if(new_id == mappings.client_id.end()) { + logWarning(logging_server_id, "Missing client id mapping for channel group relation permission entry (client id: {}, channel id: {}). Skipping relation insert.", entry.client_id, relation.first); + continue; + } + client_id = new_id->second; + } + + GroupId group_id; + { + auto new_id = mappings.channel_group_id.find(entry.group_id); + if(new_id == mappings.channel_group_id.end()) { + logWarning(logging_server_id, "Missing channel group mapping of group {} for client {} for channel {}. Skipping relation insert.", entry.group_id, entry.client_id, channel_id); + continue; + } + group_id = new_id->second; + } + + insert_query.add_entry( + target_server_id, + client_id, + group_id, + channel_id + ); + } + } + + auto result = insert_query.execute(this->handle->getSql(), true); + if(!result.failed_entries.empty()) + logWarning(logging_server_id, "Failed to insert all channel group relations into the database. Failed insert count: {}", result.failed_entries.size()); + } + + /* channel group permissions */ + { + sql::InsertQuery insert_query{"permissions", + sql::Column("serverId"), + sql::Column("type"), + sql::Column("id"), + sql::Column("channelId"), + sql::Column("permId"), + + sql::Column("value"), + sql::Column("grant"), + sql::Column("flag_skip"), + sql::Column("flag_negate"), + }; + + for(auto& group : snapshot_data.parsed_channel_groups) { + GroupId group_id; + { + auto new_id = mappings.channel_group_id.find(group.group_id); + if(new_id == mappings.channel_group_id.end()) { + logWarning(logging_server_id, "Missing channel group mapping of group {}. Skipping permission insert.", group.group_id); + continue; + } + group_id = new_id->second; + } + + for(const auto& permission : group.permissions) { + insert_query.add_entry( + target_server_id, + permission::SQL_PERM_GROUP, + group_id, + 0, + permission.type->name, + permission.value.has_value ? permission.value.value : permNotGranted, + permission.granted.has_value ? permission.granted.value : permNotGranted, + permission.flag_skip, + permission.flag_negate + ); + } + } + + auto result = insert_query.execute(this->handle->getSql(), true); + if(!result.failed_entries.empty()) + logWarning(logging_server_id, "Failed to insert some channel group permissions into the database. Failed permission count: {}", result.failed_entries.size()); + } + + /* register server & properties */ + { + + sql::InsertQuery insert_property_query{"properties", + sql::Column("serverId"), + sql::Column("type"), + sql::Column("id"), + sql::Column("key"), + sql::Column("value") + }; + + for(const auto& property : snapshot_data.parsed_server.properties.list_properties(property::FLAG_SAVE)) { + if(!property.isModified()) continue; + + auto value = property.value(); + switch(property.type().property_index) { + case property::VIRTUALSERVER_DEFAULT_SERVER_GROUP: { + auto new_id = mappings.server_group_id.find(property.as()); + if(new_id == mappings.server_group_id.end()) { + logWarning(logging_server_id, "Missing server group mapping for group {}. {} will reference an invalid group.", property.value(), property.type().name); + break; + } + + value = std::to_string(new_id->second); + break; + } + + case property::VIRTUALSERVER_DEFAULT_CHANNEL_ADMIN_GROUP: + case property::VIRTUALSERVER_DEFAULT_CHANNEL_GROUP: { + auto new_id = mappings.channel_group_id.find(property.as()); + if(new_id == mappings.channel_group_id.end()) { + logWarning(logging_server_id, "Missing channel group mapping for group {}. {} will reference an invalid group.", property.value(), property.type().name); + break; + } + + value = std::to_string(new_id->second); + break; + } + + default: + break; + } + + insert_property_query.add_entry( + target_server_id, + property::PROP_TYPE_SERVER, + 0, + std::string{property.type().name}, + value + ); + } + { + auto result = insert_property_query.execute(this->handle->getSql(), true); + if(!result.failed_entries.empty()) + logWarning(logging_server_id, "Failed to insert all server properties into the database. Failed property count: {}", result.failed_entries.size()); + } + } - sql::InsertQuery insert_property_query{"properties", - sql::Column("serverId"), - sql::Column("type"), - sql::Column("id"), - sql::Column("key"), - sql::Column("value") - }; + /* client permissions */ + { + sql::InsertQuery insert_query{"permissions", + sql::Column("serverId"), + sql::Column("type"), + sql::Column("id"), + sql::Column("channelId"), + sql::Column("permId"), - for(auto& bot : snapshot_data.music_bots) { - auto new_id = client_id_mapping.find(bot.database_id); - if(new_id == client_id_mapping.end()) { - logWarning(logging_server_id, "Missing client id mapping for music bot (Unique ID: {}). Skipping permission insert.", bot.unique_id); + sql::Column("value"), + sql::Column("grant"), + sql::Column("flag_skip"), + sql::Column("flag_negate"), + }; + + for(auto& entry : snapshot_data.client_permissions) { + { + auto new_id = mappings.client_id.find(entry.id1); + if(new_id == mappings.client_id.end()) { + logWarning(logging_server_id, "Missing client id mapping for client permission entry (client id: {}). Skipping permission insert.", entry.id1); + continue; + } + entry.id1 = new_id->second; + } + + for(const auto& permission : entry.permissions) { + insert_query.add_entry( + target_server_id, + permission::SQL_PERM_USER, + entry.id1, + 0, + permission.type->name, + permission.value.has_value ? permission.value.value : permNotGranted, + permission.granted.has_value ? permission.granted.value : permNotGranted, + permission.flag_skip, + permission.flag_negate + ); + } + } + + auto result = insert_query.execute(this->handle->getSql(), true); + if(!result.failed_entries.empty()) + logWarning(logging_server_id, "Failed to insert all client permissions into the database. Failed permission count: {}", result.failed_entries.size()); + } + + /* client channel permissions */ + { + sql::InsertQuery insert_query{"permissions", + sql::Column("serverId"), + sql::Column("type"), + sql::Column("id"), + sql::Column("channelId"), + sql::Column("permId"), + + sql::Column("value"), + sql::Column("grant"), + sql::Column("flag_skip"), + sql::Column("flag_negate"), + }; + + for(auto& entry : snapshot_data.client_channel_permissions) { + { + auto new_id = mappings.channel_id.find(entry.id1); + if(new_id == mappings.channel_id.end()) { + logWarning(logging_server_id, "Missing channel id mapping for client channel permission entry (client id: {}, channel id: {}). Skipping permission insert.", entry.id2, entry.id1); + continue; + } + entry.id1 = new_id->second; + } + + { + auto new_id = mappings.client_id.find(entry.id2); + if(new_id == mappings.client_id.end()) { + logWarning(logging_server_id, "Missing client id mapping for client channel permission entry (client id: {}, channel id: {}). Skipping permission insert.", entry.id2, entry.id1); + continue; + } + entry.id2 = new_id->second; + } + + for(const auto& permission : entry.permissions) { + insert_query.add_entry( + target_server_id, + permission::SQL_PERM_USER, + entry.id2, + (ChannelId) entry.id1, + permission.type->name, + permission.value.has_value ? permission.value.value : permNotGranted, + permission.granted.has_value ? permission.granted.value : permNotGranted, + permission.flag_skip, + permission.flag_negate + ); + } + } + + auto result = insert_query.execute(this->handle->getSql(), true); + if(!result.failed_entries.empty()) + logWarning(logging_server_id, "Failed to insert all client channel permissions into the database. Failed permission count: {}", result.failed_entries.size()); + } + + /* music bots */ + { + sql::InsertQuery insert_query{"musicbots", + sql::Column("serverId"), + sql::Column("botId"), + sql::Column("uniqueId"), + sql::Column("owner"), + }; + + for(const auto& client : snapshot_data.music_bots) { + ClientDbId bot_id; + { + auto new_id = mappings.client_id.find(client.database_id); + if(new_id == mappings.client_id.end()) { + logWarning(logging_server_id, "Missing music bot database id mapping for bot {}. Skipping bot register.", client.unique_id); continue; } - for(const auto& property : bot.properties.list_properties(property::FLAG_SAVE | property::FLAG_SAVE_MUSIC)) { - if(!property.isModified()) continue; + bot_id = new_id->second; + } - auto value = property.value(); - if(property.type() == property::CLIENT_LAST_CHANNEL) { - auto channel_id = property.as(); - - auto new_channel_id = channel_id_mapping.find(channel_id); - if(new_channel_id == channel_id_mapping.end()) { - logWarning(logging_server_id, "Missing channel id mapping for channel {} for music bot {}.", channel_id, bot.unique_id); - continue; - } - value = std::to_string(new_channel_id->second); - } - insert_property_query.add_entry( - target_server_id, - property::PROP_TYPE_CLIENT, - new_id->second, - std::string{property.type().name}, - value - ); + ClientDbId bot_owner_id; + { + auto new_id = mappings.client_id.find(client.bot_owner_id); + if(new_id == mappings.client_id.end()) { + logWarning(logging_server_id, "Missing music bots owner database id mapping for bot {}. Resetting it to zero.", client.unique_id); + bot_owner_id = 0; + } else { + bot_owner_id = new_id->second; } } - auto result = insert_property_query.execute(this->handle->getSql(), true); + insert_query.add_entry( + target_server_id, + bot_id, + client.unique_id, + bot_owner_id + ); + } + + { + auto result = insert_query.execute(this->handle->getSql(), true); if(!result.failed_entries.empty()) - logWarning(logging_server_id, "Failed to insert some music bot properties into the database. Failed property count: {}", result.failed_entries.size()); + logWarning(logging_server_id, "Failed to insert some music bots into the database. Failed bot count: {}", result.failed_entries.size()); } + } - /* playlists */ - { + /* music bot attributes */ + { - sql::InsertQuery insert_playlist_query{"playlists", - sql::Column("serverId"), - sql::Column("playlist_id") - }; + sql::InsertQuery insert_property_query{"properties", + sql::Column("serverId"), + sql::Column("type"), + sql::Column("id"), + sql::Column("key"), + sql::Column("value") + }; - sql::InsertQuery insert_property_query{"properties", - sql::Column("serverId"), - sql::Column("type"), - sql::Column("id"), - sql::Column("key"), - sql::Column("value") - }; - - for(auto& playlist : snapshot_data.playlists) { - insert_playlist_query.add_entry(target_server_id, playlist.playlist_id); - - for(const auto& property : playlist.properties.list_properties(property::FLAG_SAVE | property::FLAG_SAVE_MUSIC)) { - if(!property.isModified()) continue; - - insert_property_query.add_entry( - target_server_id, - property::PROP_TYPE_PLAYLIST, - playlist.playlist_id, - std::string{property.type().name}, - property.value() - ); - } + for(auto& bot : snapshot_data.music_bots) { + auto new_id = mappings.client_id.find(bot.database_id); + if(new_id == mappings.client_id.end()) { + logWarning(logging_server_id, "Missing client id mapping for music bot (Unique ID: {}). Skipping permission insert.", bot.unique_id); + continue; } - { - auto result = insert_playlist_query.execute(this->handle->getSql(), true); - if(!result.failed_entries.empty()) - logWarning(logging_server_id, "Failed to insert some playlists into the database. Failed playlist count: {}", result.failed_entries.size()); - } + for(const auto& property : bot.properties.list_properties(property::FLAG_SAVE | property::FLAG_SAVE_MUSIC)) { + if(!property.isModified()) continue; - { - auto result = insert_property_query.execute(this->handle->getSql(), true); - if(!result.failed_entries.empty()) - logWarning(logging_server_id, "Failed to insert some playlist properties into the database. Failed playlist count: {}", result.failed_entries.size()); - } - } + auto value = property.value(); + if(property.type() == property::CLIENT_LAST_CHANNEL) { + auto channel_id = property.as(); - /* playlist songs */ - { - sql::InsertQuery insert_song_query{"playlist_songs", - sql::Column("serverId"), - sql::Column("playlist_id"), - sql::Column("song_id"), - sql::Column("order_id"), - sql::Column("invoker_dbid"), - sql::Column("url"), - sql::Column("url_loader"), - sql::Column("loaded"), - sql::Column("metadata") - }; - - for(auto& song : snapshot_data.playlist_songs) { - { - auto new_id = client_id_mapping.find(song.invoker_id); - if(new_id == client_id_mapping.end()) { - logWarning(logging_server_id, "Missing client id mapping for playlist song invoker (Invoker ID: {}). Changing it to zero.", song.invoker_id); - song.invoker_id = 0; - } else { - song.invoker_id = new_id->second; + auto new_channel_id = mappings.channel_id.find(channel_id); + if(new_channel_id == mappings.channel_id.end()) { + logWarning(logging_server_id, "Missing channel id mapping for channel {} for music bot {}.", channel_id, bot.unique_id); + continue; } + value = std::to_string(new_channel_id->second); } - - insert_song_query.add_entry( + insert_property_query.add_entry( target_server_id, - song.playlist_id, - song.song_id, - song.order_id, - song.invoker_id, - song.url, - song.loader, - song.loaded, - song.metadata + property::PROP_TYPE_CLIENT, + new_id->second, + std::string{property.type().name}, + value ); } + } - { - auto result = insert_song_query.execute(this->handle->getSql(), true); - if(!result.failed_entries.empty()) - logWarning(logging_server_id, "Failed to insert some playlist songs into the database. Failed song count: {}", result.failed_entries.size()); + auto result = insert_property_query.execute(this->handle->getSql(), true); + if(!result.failed_entries.empty()) + logWarning(logging_server_id, "Failed to insert some music bot properties into the database. Failed property count: {}", result.failed_entries.size()); + } + + /* playlists */ + { + + sql::InsertQuery insert_playlist_query{"playlists", + sql::Column("serverId"), + sql::Column("playlist_id") + }; + + sql::InsertQuery insert_property_query{"properties", + sql::Column("serverId"), + sql::Column("type"), + sql::Column("id"), + sql::Column("key"), + sql::Column("value") + }; + + for(auto& playlist : snapshot_data.playlists) { + insert_playlist_query.add_entry(target_server_id, playlist.playlist_id); + + for(const auto& property : playlist.properties.list_properties(property::FLAG_SAVE | property::FLAG_SAVE_MUSIC)) { + if(!property.isModified()) continue; + + insert_property_query.add_entry( + target_server_id, + property::PROP_TYPE_PLAYLIST, + playlist.playlist_id, + std::string{property.type().name}, + property.value() + ); } } + + { + auto result = insert_playlist_query.execute(this->handle->getSql(), true); + if(!result.failed_entries.empty()) + logWarning(logging_server_id, "Failed to insert some playlists into the database. Failed playlist count: {}", result.failed_entries.size()); + } + + { + auto result = insert_property_query.execute(this->handle->getSql(), true); + if(!result.failed_entries.empty()) + logWarning(logging_server_id, "Failed to insert some playlist properties into the database. Failed playlist count: {}", result.failed_entries.size()); + } + } + + /* playlist songs */ + { + sql::InsertQuery insert_song_query{"playlist_songs", + sql::Column("serverId"), + sql::Column("playlist_id"), + sql::Column("song_id"), + sql::Column("order_id"), + sql::Column("invoker_dbid"), + sql::Column("url"), + sql::Column("url_loader"), + sql::Column("loaded"), + sql::Column("metadata") + }; + + for(auto& song : snapshot_data.playlist_songs) { + { + auto new_id = mappings.client_id.find(song.invoker_id); + if(new_id == mappings.client_id.end()) { + logWarning(logging_server_id, "Missing client id mapping for playlist song invoker (Invoker ID: {}). Changing it to zero.", song.invoker_id); + song.invoker_id = 0; + } else { + song.invoker_id = new_id->second; + } + } + + insert_song_query.add_entry( + target_server_id, + song.playlist_id, + song.song_id, + song.order_id, + song.invoker_id, + song.url, + song.loader, + song.loaded, + song.metadata + ); + } + + { + auto result = insert_song_query.execute(this->handle->getSql(), true); + if(!result.failed_entries.empty()) + logWarning(logging_server_id, "Failed to insert some playlist songs into the database. Failed song count: {}", result.failed_entries.size()); + } } return true; diff --git a/server/src/snapshots/groups.cpp b/server/src/snapshots/groups.cpp index 66ecae5..0df5c82 100644 --- a/server/src/snapshots/groups.cpp +++ b/server/src/snapshots/groups.cpp @@ -43,21 +43,28 @@ bool relation_parser::parse(std::string &error, group_relations &result, size_t bool key_found; while(offset < *relation_end) { + ChannelId channel_id; 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; + if(begin_bulk.has_key("iid")) { + channel_id = begin_bulk.value_as("iid"); + } else { + /* may be the case for the global assignments (iid may be not send in the first few generations) */ + channel_id = 0; } - auto& relations = result[begin_bulk.value_as("iid")]; + auto& relations = result[channel_id]; auto next_iid = this->command.next_bulk_containing("iid", offset + 1); - if(next_iid.has_value() && *next_iid < relation_end) + size_t end_index; + if(next_iid.has_value() && *next_iid < relation_end) { + end_index = *next_iid; relations.reserve(*next_iid - offset); - else + } else { + end_index = *relation_end; relations.reserve(*relation_end - offset); + } - while(offset < *next_iid) { + while(offset < end_index) { auto relation_data = this->command.bulk(offset++); auto& relation = relations.emplace_back(); diff --git a/server/src/snapshots/music.cpp b/server/src/snapshots/music.cpp index 957e0c2..be8c402 100644 --- a/server/src/snapshots/music.cpp +++ b/server/src/snapshots/music.cpp @@ -9,6 +9,8 @@ using namespace ts::server::snapshots; bool music_bot_parser::parse(std::string &error, music_bot_entry &entry, size_t &index) { auto data = this->command[index]; + entry.properties.register_property_type(); + size_t entry_index{0}; std::string_view key{}; std::string value{}; @@ -23,7 +25,12 @@ bool music_bot_parser::parse(std::string &error, music_bot_entry &entry, size_t } else if(key == "bot_unique_id") { entry.unique_id = value; } else if(key == "bot_owner_id") { - entry.bot_owner_id = value; + char* end_ptr{nullptr}; + entry.bot_owner_id = strtoull(value.c_str(), &end_ptr, 10); + if (*end_ptr) { + error = "failed to parse bot owner database id at character " + std::to_string(data.key_command_character_index(key) + key.length()); + return false; + } } else if(key == "end_bots") { continue; } else { @@ -52,6 +59,8 @@ bool music_bot_parser::parse(std::string &error, music_bot_entry &entry, size_t bool playlist_parser::parse(std::string &error, playlist_entry &entry, size_t &index) { auto data = this->command[index]; + entry.properties.register_property_type(); + size_t entry_index{0}; std::string_view key{}; std::string value{}; @@ -82,7 +91,7 @@ bool playlist_parser::parse(std::string &error, playlist_entry &entry, size_t &i return false; } - return false; + return true; } bool playlist_song_parser::parse(std::string &error, playlist_song_entry &entry, size_t &index) { diff --git a/server/src/snapshots/music.h b/server/src/snapshots/music.h index 3f4b6ad..380174d 100644 --- a/server/src/snapshots/music.h +++ b/server/src/snapshots/music.h @@ -11,7 +11,7 @@ namespace ts::server::snapshots { ClientDbId database_id{0}; std::string unique_id{}; - std::string bot_owner_id{}; + ClientDbId bot_owner_id{}; Properties properties{}; }; diff --git a/server/src/snapshots/parser.cpp b/server/src/snapshots/parser.cpp index cd6c968..c2d07e0 100644 --- a/server/src/snapshots/parser.cpp +++ b/server/src/snapshots/parser.cpp @@ -4,6 +4,10 @@ #include #include +#include +#include + +#include #include "./snapshot.h" #include "./server.h" @@ -13,12 +17,33 @@ #include "./groups.h" #include "./snapshot_data.h" +using namespace ts; using namespace ts::server; +inline bool verify_hash(std::string &error, ServerId server_id, const command_parser &data, size_t hash_offset, const std::string_view& expected_hash) { + if(!expected_hash.empty() && !data.has_switch("ignorehash")) { + auto payload = data.payload_view(hash_offset); + if(auto switches{payload.find(" -")}; switches != std::string::npos) { + debugMessage(server_id, "Truncating the snapshot payload after index {} (First switch).", switches); + payload = payload.substr(0, switches); + } + + auto calculated_hash = base64::encode(digest::sha1(payload)); + + /* debugMessage(server_id, "SHA1 for playload: {}", payload); */ + debugMessage(server_id, "Expected hash: {}, Calculated hash: {}", expected_hash, calculated_hash); + if(expected_hash != calculated_hash) { + error = "invalid snapshot hash.\nExpected: " + std::string{expected_hash} + "\nCalculated: " + calculated_hash; + return false; + } + } + return true; +} + bool snapshots::parse_snapshot(snapshot_data &result, std::string &error, ServerId server_id, const command_parser &data) { if(data.bulk(0).has_key("version")) { return snapshots::parse_snapshot_ts3(result, error, server_id, data); - } else if(data.bulk(1).has_key("snapshot_version")) { + } else if(data.bulk(0).has_key("snapshot_version") || (data.bulk_count() > 1 && data.bulk(1).has_key("snapshot_version"))) { /* teaspeak snapshot */ return snapshots::parse_snapshot_teaspeak(result, error, server_id, data); } else { @@ -27,24 +52,83 @@ bool snapshots::parse_snapshot(snapshot_data &result, std::string &error, Server } } -bool snapshots::parse_snapshot_teaspeak(snapshot_data &result, 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; +inline std::unique_ptr decompress_snapshot(std::string& error, size_t& decompressed_length, const std::string& data) { + std::unique_ptr result{nullptr, ::free}; + + auto uncompressed_length = ZSTD_getFrameContentSize(data.data(), data.length()); + if(uncompressed_length == ZSTD_CONTENTSIZE_UNKNOWN) { + error = "unknown uncompressed size"; + return result; + } else if(uncompressed_length == ZSTD_CONTENTSIZE_ERROR) { + error = "failed to calculate uncompressed size"; + return result; + } else if(ZSTD_isError(uncompressed_length)) { + error = "failed to calculate uncompressed size: " + std::string{ZSTD_getErrorName(uncompressed_length)}; + return result; + } else if(uncompressed_length > 512 * 1024 * 1028) { + error = "uncompressed size exceeds half a gigabyte!"; + return result; + } + + result.reset((char*) ::malloc(uncompressed_length)); + if(!result) { + error = "failed to allocate decompress buffer"; + return result; + } + + decompressed_length = ZSTD_decompress(&*result, uncompressed_length, data.data(), data.length()); + if(ZSTD_isError(decompressed_length)) { + error = "decompress error occurred: " + std::string{ZSTD_getErrorName(decompressed_length)}; + result = nullptr; + return result; + } + + return result; +} + +bool snapshots::parse_snapshot_teaspeak(snapshot_data &result, std::string &error, ts::ServerId server_id, const ts::command_parser &data) { + snapshots::version_t version; + if(data.bulk(0).has_key("snapshot_version")) { + version = data.bulk(0).value_as("snapshot_version"); + } else { + version = data.bulk(1).value_as("snapshot_version"); } - 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) { + auto hash = data.bulk(0).value("hash"); + + if(!verify_hash(error, server_id, data, 1, hash)) + return false; + + /* the actual snapshot begins at index 2 */ + return snapshots::parse_snapshot_raw(result, error, server_id, data, 2, type::TEASPEAK, version); } else if(version > 2) { + auto compressed_data{base64::decode(data.value("data"))}; + + size_t decompressed_length; + auto decompressed = decompress_snapshot(error, decompressed_length, compressed_data); + if(!decompressed) { + return false; + } + + std::string_view decompressed_data{&*decompressed, decompressed_length}; + ts::command_parser decompressed_parser{decompressed_data}; + if(!decompressed_parser.parse(false)) { + error = "failed to parse snapshot payload"; + return false; + } + + return snapshots::parse_snapshot_raw(result, error, server_id, decompressed_parser, 0, type::TEASPEAK, version); + } else if(version > 3) { error = "snapshot version is too new"; return false; + } else { + error = "invalid snapshot version"; + return false; } - - /* the actual snapshot begins at index 2 */ - return snapshots::parse_snapshot_raw(result, error, server_id, data, hash, 2, type::TEASPEAK, version); } bool snapshots::parse_snapshot_ts3(snapshot_data &result, std::string &error, ts::ServerId server_id, const ts::command_parser &data) { @@ -52,24 +136,42 @@ bool snapshots::parse_snapshot_ts3(snapshot_data &result, std::string &error, ts 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 snapshots::parse_snapshot_raw(result, error, server_id, data, hash, 1, type::TEAMSPEAK, version); + auto hash = data.bulk(0).value("hash"); + if(!verify_hash(error, server_id, data, 1, hash)) + return false; + + return snapshots::parse_snapshot_raw(result, error, server_id, data, 1, type::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 if(version >= 2 && version <= 3) { + std::string compressed_data; + if(version == 2) { + compressed_data = base64::decode(data.payload_view(1)); + } else { + compressed_data = base64::decode(data.value("data")); + } + + size_t decompressed_length; + auto decompressed = decompress_snapshot(error, decompressed_length, compressed_data); + if(!decompressed) { + return false; + } + + std::string_view decompressed_data{&*decompressed, decompressed_length}; + ts::command_parser decompressed_parser{decompressed_data}; + if(!decompressed_parser.parse(false)) { + error = "failed to parse snapshot payload"; + return false; + } + + return snapshots::parse_snapshot_raw(result, error, server_id, decompressed_parser, 0, type::TEAMSPEAK, version); } else { error = "snapshots with version 1-3 are currently supported"; return false; @@ -77,9 +179,7 @@ bool snapshots::parse_snapshot_ts3(snapshot_data &result, std::string &error, ts } bool snapshots::parse_snapshot_raw(snapshot_data &snapshot_data, std::string &error, ServerId server_id, const command_parser &command, - const std::string &hash, size_t command_offset, snapshots::type type, snapshots::version_t version) { - //TODO: Verify hash - + size_t command_offset, snapshots::type type, snapshots::version_t version) { /* all snapshots start with the virtual server properties */ { snapshots::server_parser parser{type, version, command}; @@ -105,7 +205,6 @@ bool snapshots::parse_snapshot_raw(snapshot_data &snapshot_data, std::string &er return false; } snapshot_data.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 = snapshot_data.parsed_channels.emplace_back(); @@ -130,7 +229,6 @@ bool snapshots::parse_snapshot_raw(snapshot_data &snapshot_data, std::string &er return false; } snapshot_data.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 = snapshot_data.parsed_clients.emplace_back(); @@ -140,6 +238,93 @@ bool snapshots::parse_snapshot_raw(snapshot_data &snapshot_data, std::string &er command_offset++; /* the "end_clients" token */ } + if(type == snapshots::type::TEASPEAK && version >= 2) { + + bool music_bots_parsed{false}, + playlist_parsed{false}, + playlist_songs_parsed{false}; + + if(!command.bulk(command_offset).has_key("begin_music")) { + error = "missing begin music key"; + return false; + } + + while(!command.bulk(command_offset).has_key("end_music")) { + if(command.bulk(command_offset).has_key("begin_bots")) { + if(std::exchange(music_bots_parsed, true)) { + error = "duplicated music bot list"; + return false; + } + + snapshots::music_bot_parser parser{type, version, command}; + while(!command.bulk(command_offset).has_key("end_bots")) { + auto& bot = snapshot_data.music_bots.emplace_back(); + + if(!parser.parse(error, bot, command_offset)) + return false; + + command_offset++; + } + } else if(command.bulk(command_offset).has_key("begin_playlist")) { + if(std::exchange(playlist_parsed, true)) { + error = "duplicated playlist list"; + return false; + } + + snapshots::playlist_parser parser{type, version, command}; + while(!command.bulk(command_offset).has_key("end_playlist")) { + auto& playlist = snapshot_data.playlists.emplace_back(); + + if(!parser.parse(error, playlist, command_offset)) + return false; + + command_offset++; + } + } else if(command.bulk(command_offset).has_key("begin_playlist_songs")) { + if(std::exchange(playlist_songs_parsed, true)) { + error = "duplicated playlist songs list"; + return false; + } + + snapshots::playlist_song_parser parser{type, version, command}; + + PlaylistId current_playlist_id{0}; + while(!command.bulk(command_offset).has_key("end_playlist_songs")) { + if(snapshot_data.playlist_songs.empty() || command.bulk(command_offset).has_key("song_playlist_id")) + current_playlist_id = command.bulk(command_offset).value_as("song_playlist_id"); + + auto& playlist_song = snapshot_data.playlist_songs.emplace_back(); + playlist_song.playlist_id = current_playlist_id; + + if(!parser.parse(error, playlist_song, command_offset)) + return false; + + command_offset++; + } + } else { + __asm__("nop"); + } + command_offset++; + } + command_offset++; + + /* check if everything has been parsed */ + if(!music_bots_parsed) { + error = "missing music bots"; + return false; + } + + if(!playlist_parsed) { + error = "missing playlists"; + return false; + } + + if(!playlist_songs_parsed) { + error = "missing playlist songs"; + return false; + } + } + /* permissions */ { bool server_groups_parsed{false}, @@ -274,82 +459,14 @@ bool snapshots::parse_snapshot_raw(snapshot_data &snapshot_data, std::string &er } } - if(type == snapshots::type::TEASPEAK && version >= 2) { - - bool music_bots_parsed{false}, - playlist_parsed{false}, - playlist_songs_parsed{false}; - - if(!command.bulk(command_offset++).has_key("begin_music")) { - error = "missing begin music key"; - return false; - } - - while(!command.bulk(command_offset).has_key("end_music")) { - if(command.bulk(command_offset).has_key("begin_bots")) { - if(std::exchange(music_bots_parsed, true)) { - error = "duplicated music bot list"; - return false; - } - - snapshots::music_bot_parser parser{type, version, command}; - while(!command.bulk(command_offset).has_key("end_bots")) { - auto& bot = snapshot_data.music_bots.emplace_back(); - - if(!parser.parse(error, bot, command_offset)) - return false; - } - } else if(command.bulk(command_offset).has_key("begin_playlist")) { - if(std::exchange(playlist_parsed, true)) { - error = "duplicated playlist list"; - return false; - } - - snapshots::playlist_parser parser{type, version, command}; - while(!command.bulk(command_offset).has_key("end_playlist")) { - auto& playlist = snapshot_data.playlists.emplace_back(); - - if(!parser.parse(error, playlist, command_offset)) - return false; - } - } else if(command.bulk(command_offset).has_key("begin_playlist_songs")) { - if(std::exchange(playlist_songs_parsed, true)) { - error = "duplicated playlist songs list"; - return false; - } - - snapshots::playlist_song_parser parser{type, version, command}; - - PlaylistId current_playlist_id{0}; - while(!command.bulk(command_offset).has_key("end_playlist_songs")) { - if(snapshot_data.playlist_songs.empty() || command.bulk(command_offset).has_key("song_playlist_id")) - current_playlist_id = command.bulk(command_offset).value_as("song_playlist_id"); - - auto& playlist_song = snapshot_data.playlist_songs.emplace_back(); - playlist_song.playlist_id = current_playlist_id; - - if(!parser.parse(error, playlist_song, command_offset)) - return false; - } - } - } - - /* check if everything has been parsed */ - if(!music_bots_parsed) { - error = "missing music bots"; - return false; - } - - if(!playlist_parsed) { - error = "missing playlists"; - return false; - } - - if(!playlist_songs_parsed) { - error = "missing playlist songs"; - return false; - } - } + debugMessage(server_id, "Parsed snapshot containing {} channels, {} server groups, {} channel groups, {} clients, {} music bots and {} playlists.", + snapshot_data.parsed_channels.size(), + snapshot_data.parsed_server_groups.size(), + snapshot_data.parsed_channel_groups.size(), + snapshot_data.parsed_clients.size(), + snapshot_data.music_bots.size(), + snapshot_data.playlists.size() + ); return true; } \ No newline at end of file diff --git a/server/src/snapshots/permission.cpp b/server/src/snapshots/permission.cpp index 9b581cd..0547cb4 100644 --- a/server/src/snapshots/permission.cpp +++ b/server/src/snapshots/permission.cpp @@ -143,7 +143,7 @@ bool permission_parser::parse_entry_teaspeak_v1(std::string &error, std::vector< return false; } - permission::PermissionValue value{}; + permission::PermissionValue value; { auto value_string = data.value("value", key_found); if(!key_found) { @@ -159,7 +159,7 @@ bool permission_parser::parse_entry_teaspeak_v1(std::string &error, std::vector< } } - permission::PermissionValue granted{}; + permission::PermissionValue granted; { auto value_string = data.value("grant", key_found); if(!key_found) { diff --git a/server/src/snapshots/snapshot.h b/server/src/snapshots/snapshot.h index ed41642..50c45ac 100644 --- a/server/src/snapshots/snapshot.h +++ b/server/src/snapshots/snapshot.h @@ -54,6 +54,6 @@ namespace ts::server::snapshots { extern bool parse_snapshot_ts3(snapshot_data& /* result */, std::string& /* error */, ServerId /* target server id */, const command_parser& /* source */); extern bool parse_snapshot_teaspeak(snapshot_data& /* result */, std::string& /* error */, ServerId /* target server id */, const command_parser& /* source */); - extern bool parse_snapshot_raw(snapshot_data& /* result */, std::string& /* error */, ServerId /* target server id */, const command_parser& /* source */, const std::string& /* hash */, size_t /* offset */, snapshots::type /* type */, snapshots::version_t /* version */); + extern bool parse_snapshot_raw(snapshot_data& /* result */, std::string& /* error */, ServerId /* target server id */, const command_parser& /* source */, size_t /* offset */, snapshots::type /* type */, snapshots::version_t /* version */); } \ No newline at end of file diff --git a/server/src/snapshots/snapshot_data.h b/server/src/snapshots/snapshot_data.h index cd6db8f..9ab8de9 100644 --- a/server/src/snapshots/snapshot_data.h +++ b/server/src/snapshots/snapshot_data.h @@ -29,4 +29,28 @@ namespace ts::server::snapshots { std::deque playlists{}; std::deque playlist_songs{}; }; + + struct snapshot_mappings { + ServerId new_server_id{}; + + std::map client_id{}; + std::map channel_id{}; + std::map channel_group_id{}; + std::map server_group_id{}; + std::map playlist_id{}; + }; + + struct snapshot_deploy_timings { + std::chrono::system_clock::time_point initial_timestamp{}; + std::chrono::system_clock::time_point decode_timestamp{}; /* end decode */ + std::chrono::system_clock::time_point parse_timestamp{}; /* end parse */ + std::chrono::system_clock::time_point deploy_timestamp{}; /* else deploy */ + std::chrono::system_clock::time_point startup_timestamp{}; /* end startup */ + }; + + struct snapshot_deploy { + snapshot_deploy_timings timings{}; + snapshot_mappings mappings{}; + snapshot_data data{}; + }; } diff --git a/shared b/shared index a634396..c3188cd 160000 --- a/shared +++ b/shared @@ -1 +1 @@ -Subproject commit a6343968450d4e626bcf99ec622b4a325bf8c4ba +Subproject commit c3188cd9e5f499f4b5813953d2bc62f56800222f