// // Created by wolverindev on 26.01.20. // #include #include #include #include #include #include "../../build.h" #include "../ConnectedClient.h" #include "../InternalClient.h" #include "../../server/VoiceServer.h" #include "../voice/VoiceClient.h" #include "../../InstanceHandler.h" #include "../../server/QueryServer.h" #include "../music/MusicClient.h" #include "../query/QueryClient.h" #include "../../manager/ConversationManager.h" #include "../../manager/PermissionNameMapper.h" #include "../../manager/ActionLogger.h" #include "../../groups/GroupManager.h" #include "../../absl/btree/map.h" #include "../../PermissionCalculator.h" #include #include #include #include #include "helpers.h" #include #include #include #include namespace fs = std::experimental::filesystem; using namespace std::chrono; using namespace std; using namespace ts; using namespace ts::server; using namespace ts::server::token; #define QUERY_PASSWORD_LENGTH 12 command_result ConnectedClient::handleCommand(Command &cmd) { threads::MutexLock l2(this->command_lock); auto command = cmd.command(); if (command == "servergetvariables") return this->handleCommandServerGetVariables(cmd); else if (command == "serverrequestconnectioninfo") return this->handleCommandServerRequestConnectionInfo(cmd); else if (command == "getconnectioninfo") return this->handleCommandGetConnectionInfo(cmd); else if (command == "setconnectioninfo") return this->handleCommandSetConnectionInfo(cmd); else if (command == "clientgetvariables") return this->handleCommandClientGetVariables(cmd); else if (command == "serveredit") return this->handleCommandServerEdit(cmd); else if (command == "clientedit") return this->handleCommandClientEdit(cmd); else if (command == "channelgetdescription") return this->handleCommandChannelGetDescription(cmd); else if (command == "connectioninfoautoupdate") return this->handleCommandConnectionInfoAutoUpdate(cmd); else if (command == "permissionlist") return this->handleCommandPermissionList(cmd); else if (command == "propertylist") return this->handleCommandPropertyList(cmd); //Server group else if (command == "servergrouplist") return this->handleCommandServerGroupList(cmd); else if (command == "servergroupadd") return this->handleCommandServerGroupAdd(cmd); else if (command == "servergroupcopy") return this->handleCommandServerGroupCopy(cmd); else if (command == "servergroupdel") return this->handleCommandServerGroupDel(cmd); else if (command == "servergrouprename") return this->handleCommandServerGroupRename(cmd); else if (command == "servergroupclientlist") return this->handleCommandServerGroupClientList(cmd); else if (command == "servergroupaddclient" || command == "clientaddservergroup") return this->handleCommandServerGroupAddClient(cmd); else if (command == "servergroupdelclient" || command == "clientdelservergroup") return this->handleCommandServerGroupDelClient(cmd); else if (command == "servergrouppermlist") return this->handleCommandServerGroupPermList(cmd); else if (command == "servergroupaddperm") return this->handleCommandServerGroupAddPerm(cmd); else if (command == "servergroupdelperm") return this->handleCommandServerGroupDelPerm(cmd); else if (command == "setclientchannelgroup") return this->handleCommandSetClientChannelGroup(cmd); //Channel basic actions else if (command == "channelcreate") return this->handleCommandChannelCreate(cmd); else if (command == "channelmove") return this->handleCommandChannelMove(cmd); else if (command == "channeledit") return this->handleCommandChannelEdit(cmd); else if (command == "channeldelete") return this->handleCommandChannelDelete(cmd); //Find a channel and get informations else if (command == "channelfind") return this->handleCommandChannelFind(cmd); else if (command == "channelinfo") return this->handleCommandChannelInfo(cmd); //Channel perm actions else if (command == "channelpermlist") return this->handleCommandChannelPermList(cmd); else if (command == "channeladdperm") return this->handleCommandChannelAddPerm(cmd); else if (command == "channeldelperm") return this->handleCommandChannelDelPerm(cmd); //Channel group actions else if (command == "channelgroupadd") return this->handleCommandChannelGroupAdd(cmd); else if (command == "channelgroupcopy") return this->handleCommandChannelGroupCopy(cmd); else if (command == "channelgrouprename") return this->handleCommandChannelGroupRename(cmd); else if (command == "channelgroupdel") return this->handleCommandChannelGroupDel(cmd); else if (command == "channelgrouplist") return this->handleCommandChannelGroupList(cmd); else if (command == "channelgroupclientlist") return this->handleCommandChannelGroupClientList(cmd); else if (command == "channelgrouppermlist") return this->handleCommandChannelGroupPermList(cmd); else if (command == "channelgroupaddperm") return this->handleCommandChannelGroupAddPerm(cmd); else if (command == "channelgroupdelperm") return this->handleCommandChannelGroupDelPerm(cmd); //Channel sub/unsubscribe else if (command == "channelsubscribe") return this->handleCommandChannelSubscribe(cmd); else if (command == "channelsubscribeall") return this->handleCommandChannelSubscribeAll(cmd); else if (command == "channelunsubscribe") return this->handleCommandChannelUnsubscribe(cmd); else if (command == "channelunsubscribeall") return this->handleCommandChannelUnsubscribeAll(cmd); //manager channel permissions else if (command == "channelclientpermlist") return this->handleCommandChannelClientPermList(cmd); else if (command == "channelclientaddperm") return this->handleCommandChannelClientAddPerm(cmd); else if (command == "channelclientdelperm") return this->handleCommandChannelClientDelPerm(cmd); //Client actions else if (command == "clientupdate") return this->handleCommandClientUpdate(cmd); else if (command == "clientmove") return this->handleCommandClientMove(cmd); else if (command == "clientgetids") return this->handleCommandClientGetIds(cmd); else if (command == "clientkick") return this->handleCommandClientKick(cmd); else if (command == "clientpoke") return this->handleCommandClientPoke(cmd); else if (command == "sendtextmessage") return this->handleCommandSendTextMessage(cmd); else if (command == "clientchatcomposing") return this->handleCommandClientChatComposing(cmd); else if (command == "clientchatclosed") return this->handleCommandClientChatClosed(cmd); else if (command == "clientfind") return this->handleCommandClientFind(cmd); else if (command == "clientinfo") return this->handleCommandClientInfo(cmd); else if (command == "clientaddperm") return this->handleCommandClientAddPerm(cmd); else if (command == "clientdelperm") return this->handleCommandClientDelPerm(cmd); else if (command == "clientpermlist") return this->handleCommandClientPermList(cmd); //File transfare else if (command == "ftgetfilelist") return this->handleCommandFTGetFileList(cmd); else if (command == "ftcreatedir") return this->handleCommandFTCreateDir(cmd); else if (command == "ftdeletefile") return this->handleCommandFTDeleteFile(cmd); else if (command == "ftinitupload") { auto result = this->handleCommandFTInitUpload(cmd); if(result.has_error() && this->getType() == ClientType::CLIENT_TEAMSPEAK) { ts::command_builder notify{"notifystatusfiletransfer"}; notify.put_unchecked(0, "clientftfid", cmd["clientftfid"].string()); notify.put(0, "size", 0); this->writeCommandResult(notify, result, "status"); this->sendCommand(notify); result.release_data(); return command_result{error::ok}; } return result; } else if (command == "ftinitdownload") { auto result = this->handleCommandFTInitDownload(cmd); if(result.has_error() && this->getType() == ClientType::CLIENT_TEAMSPEAK) { ts::command_builder notify{"notifystatusfiletransfer"}; notify.put_unchecked(0, "clientftfid", cmd["clientftfid"].string()); notify.put(0, "size", 0); this->writeCommandResult(notify, result, "status"); this->sendCommand(notify); result.release_data(); return command_result{error::ok}; } return result; } else if (command == "ftgetfileinfo") return this->handleCommandFTGetFileInfo(cmd); else if (command == "ftrenamefile") return this->handleCommandFTRenameFile(cmd); else if (command == "ftlist") return this->handleCommandFTList(cmd); else if (command == "ftstop") return this->handleCommandFTStop(cmd); //Banlist else if (command == "banlist") return this->handleCommandBanList(cmd); else if (command == "banadd") return this->handleCommandBanAdd(cmd); else if (command == "banedit") return this->handleCommandBanEdit(cmd); else if (command == "banclient") return this->handleCommandBanClient(cmd); else if (command == "bandel") return this->handleCommandBanDel(cmd); else if (command == "bandelall") return this->handleCommandBanDelAll(cmd); else if (command == "bantriggerlist") return this->handleCommandBanTriggerList(cmd); //Tokens else if (command == "tokenactionlist") return this->handleCommandTokenActionList(cmd); else if (command == "tokenlist" || command == "privilegekeylist") return this->handleCommandTokenList(cmd); else if (command == "tokenadd" || command == "privilegekeyadd") return this->handleCommandTokenAdd(cmd); else if (command == "tokenedit") return this->handleCommandTokenEdit(cmd); else if (command == "tokenuse" || command == "privilegekeyuse") return this->handleCommandTokenUse(cmd); else if (command == "tokendelete" || command == "privilegekeydelete") return this->handleCommandTokenDelete(cmd); //DB stuff else if (command == "clientdblist") return this->handleCommandClientDbList(cmd); else if (command == "clientdbinfo") return this->handleCommandClientDbInfo(cmd); else if (command == "clientdbedit") return this->handleCommandClientDBEdit(cmd); else if (command == "clientdbfind") return this->handleCommandClientDBFind(cmd); else if (command == "clientdbdelete") return this->handleCommandClientDBDelete(cmd); else if (command == "plugincmd") return this->handleCommandPluginCmd(cmd); else if (command == "clientmute") return this->handleCommandClientMute(cmd); else if (command == "clientunmute") return this->handleCommandClientUnmute(cmd); else if (command == "clientlist") return this->handleCommandClientList(cmd); else if (command == "whoami") return this->handleCommandWhoAmI(cmd); else if (command == "servergroupsbyclientid") return this->handleCommandServerGroupsByClientId(cmd); else if (command == "clientgetdbidfromuid") return this->handleCommandClientGetDBIDfromUID(cmd); else if (command == "clientgetnamefromdbid") return this->handleCommandClientGetNameFromDBID(cmd); else if (command == "clientgetnamefromuid") return this->handleCommandClientGetNameFromUid(cmd); else if (command == "clientgetuidfromclid") return this->handleCommandClientGetUidFromClid(cmd); else if (command == "complainadd") return this->handleCommandComplainAdd(cmd); else if (command == "complainlist") return this->handleCommandComplainList(cmd); else if (command == "complaindel") return this->handleCommandComplainDel(cmd); else if (command == "complaindelall") return this->handleCommandComplainDelAll(cmd); else if (command == "version") return this->handleCommandVersion(cmd); else if (command == "verifyserverpassword") return this->handleCommandVerifyServerPassword(cmd); else if (command == "verifychannelpassword") return this->handleCommandVerifyChannelPassword(cmd); else if (command == "messagelist") return this->handleCommandMessageList(cmd); else if (command == "messageadd") return this->handleCommandMessageAdd(cmd); else if (command == "messageget") return this->handleCommandMessageGet(cmd); else if (command == "messagedel") return this->handleCommandMessageDel(cmd); else if (command == "messageupdateflag") return this->handleCommandMessageUpdateFlag(cmd); else if (command == "permget") return this->handleCommandPermGet(cmd); else if (command == "permfind") return this->handleCommandPermFind(cmd); else if (command == "permidgetbyname") return this->handleCommandPermIdGetByName(cmd); else if (command == "permoverview") return this->handleCommandPermOverview(cmd); else if (command == "permreset") return this->handleCommandPermReset(cmd); else if (command == "clientsetserverquerylogin") return this->handleCommandClientSetServerQueryLogin(cmd); //Music stuff else if (command == "musicbotcreate") return this->handleCommandMusicBotCreate(cmd); else if (command == "musicbotdelete") return this->handleCommandMusicBotDelete(cmd); else if (command == "musicbotsetsubscription") return this->handleCommandMusicBotSetSubscription(cmd); else if (command == "musicbotplayerinfo") return this->handleCommandMusicBotPlayerInfo(cmd); else if (command == "musicbotplayeraction") return this->handleCommandMusicBotPlayerAction(cmd); else if (command == "musicbotqueuelist") return this->handleCommandMusicBotQueueList(cmd); else if (command == "musicbotqueueadd") return this->handleCommandMusicBotQueueAdd(cmd); else if (command == "musicbotqueueremove") return this->handleCommandMusicBotQueueRemove(cmd); else if (command == "musicbotqueuereorder") return this->handleCommandMusicBotQueueReorder(cmd); else if (command == "musicbotplaylistassign") return this->handleCommandMusicBotPlaylistAssign(cmd); else if (command == "help") return this->handleCommandHelp(cmd); else if (command == "logview") return this->handleCommandLogView(cmd); else if (command == "logquery") return this->handleCommandLogQuery(cmd); else if (command == "logadd") return this->handleCommandLogAdd(cmd); else if (command == "servergroupautoaddperm") return this->handleCommandServerGroupAutoAddPerm(cmd); else if (command == "servergroupautodelperm") return this->handleCommandServerGroupAutoDelPerm(cmd); else if (command == "updatemytsid") return this->handleCommandUpdateMyTsId(cmd); else if (command == "updatemytsdata") return this->handleCommandUpdateMyTsData(cmd); else if (command == "querycreate") return this->handleCommandQueryCreate(cmd); else if (command == "querydelete") return this->handleCommandQueryDelete(cmd); else if (command == "querylist") return this->handleCommandQueryList(cmd); else if (command == "queryrename") return this->handleCommandQueryRename(cmd); else if (command == "querychangepassword") return this->handleCommandQueryChangePassword(cmd); else if (command == "playlistlist") return this->handleCommandPlaylistList(cmd); else if (command == "playlistcreate") return this->handleCommandPlaylistCreate(cmd); else if (command == "playlistdelete") return this->handleCommandPlaylistDelete(cmd); else if (command == "playlistsetsubscription") return this->handleCommandPlaylistSetSubscription(cmd); else if (command == "playlistpermlist") return this->handleCommandPlaylistPermList(cmd); else if (command == "playlistaddperm") return this->handleCommandPlaylistAddPerm(cmd); else if (command == "playlistdelperm") return this->handleCommandPlaylistDelPerm(cmd); else if (command == "playlistclientlist") return this->handleCommandPlaylistClientList(cmd); else if (command == "playlistclientpermlist") return this->handleCommandPlaylistClientPermList(cmd); else if (command == "playlistclientaddperm") return this->handleCommandPlaylistClientAddPerm(cmd); else if (command == "playlistclientdelperm") return this->handleCommandPlaylistClientDelPerm(cmd); else if (command == "playlistinfo") return this->handleCommandPlaylistInfo(cmd); else if (command == "playlistedit") return this->handleCommandPlaylistEdit(cmd); else if (command == "playlistsonglist") return this->handleCommandPlaylistSongList(cmd); else if (command == "playlistsongsetcurrent") return this->handleCommandPlaylistSongSetCurrent(cmd); else if (command == "playlistsongadd") return this->handleCommandPlaylistSongAdd(cmd); else if (command == "playlistsongreorder" || command == "playlistsongmove") return this->handleCommandPlaylistSongReorder(cmd); else if (command == "playlistsongremove") return this->handleCommandPlaylistSongRemove(cmd); else if (command == "dummy_ipchange") return this->handleCommandDummy_IpChange(cmd); else if (command == "conversationhistory") return this->handleCommandConversationHistory(cmd); else if (command == "conversationfetch") return this->handleCommandConversationFetch(cmd); else if (command == "conversationmessagedelete") return this->handleCommandConversationMessageDelete(cmd); else if (command == "listfeaturesupport") return this->handleCommandListFeatureSupport(cmd); if (this->getType() == ClientType::CLIENT_QUERY) return command_result{error::command_not_found}; //Dont log query invalid commands if (this->getType() == ClientType::CLIENT_TEAMSPEAK) if (command.empty() || command.find_first_not_of(' ') == -1) { if (!permission::v2::permission_granted(1, this->calculate_permission(permission::b_client_allow_invalid_packet, this->getChannelId()))) ((VoiceClient *) this)->disconnect(VREASON_SERVER_KICK, config::messages::kick_invalid_command, this->server ? this->server->serverAdmin : static_pointer_cast(serverInstance->getInitialServerAdmin()), true); } logError(this->getServerId(), "Missing command '{}'", command); return command_result{error::command_not_found}; }; command_result ConnectedClient::handleCommandGetConnectionInfo(Command &cmd) { CMD_REQ_SERVER; CMD_CHK_AND_INC_FLOOD_POINTS(5); ConnectedLockedClient client{this->server->find_client_by_id(cmd["clid"].as())}; if (!client) return command_result{error::client_invalid_id}; bool send_temp{false}; auto info = client->request_connection_info(this->ref(), send_temp); if (info || send_temp) { this->notifyConnectionInfo(client.client, info); } else if(this->getType() != ClientType::CLIENT_TEAMSPEAK) return command_result{error::no_cached_connection_info}; return command_result{error::ok}; } command_result ConnectedClient::handleCommandSetConnectionInfo(Command &cmd) { auto info = std::make_shared(); info->timestamp = chrono::system_clock::now(); for (const auto &key : cmd[0].keys()) { info->properties.insert({key, cmd[key].string()}); } /* CONNECTION_FILETRANSFER_BANDWIDTH_SENT, //how many bytes per second are currently being sent by file transfers CONNECTION_FILETRANSFER_BANDWIDTH_RECEIVED, //how many bytes per second are currently being received by file transfers CONNECTION_FILETRANSFER_BYTES_RECEIVED_TOTAL, //how many bytes we received in total through file transfers CONNECTION_FILETRANSFER_BYTES_SENT_TOTAL, //how many bytes we sent in total through file transfers */ deque> receivers; { lock_guard info_lock(this->connection_info.lock); for(const auto& weak_receiver : this->connection_info.receiver) { auto receiver = weak_receiver.lock(); if(!receiver) continue; receivers.push_back(receiver); } this->connection_info.receiver.clear(); this->connection_info.data = info; this->connection_info.data_age = system_clock::now(); } for(const auto& receiver : receivers) receiver->notifyConnectionInfo(this->ref(), info); return command_result{error::ok}; } //connectioninfoautoupdate connection_server2client_packetloss_speech=0.0000 connection_server2client_packetloss_keepalive=0.0010 connection_server2client_packetloss_control=0.0000 connection_server2client_packetloss_total=0.0009 command_result ConnectedClient::handleCommandConnectionInfoAutoUpdate(Command &cmd) { /* FIXME: Reimplement this! this->properties()[property::CONNECTION_SERVER2CLIENT_PACKETLOSS_KEEPALIVE] = cmd["connection_server2client_packetloss_keepalive"].as(); this->properties()[property::CONNECTION_SERVER2CLIENT_PACKETLOSS_CONTROL] = cmd["connection_server2client_packetloss_control"].as(); this->properties()[property::CONNECTION_SERVER2CLIENT_PACKETLOSS_SPEECH] = cmd["connection_server2client_packetloss_speech"].as(); this->properties()[property::CONNECTION_SERVER2CLIENT_PACKETLOSS_TOTAL] = cmd["connection_server2client_packetloss_total"].as(); */ return command_result{error::ok}; } command_result ConnectedClient::handleCommandPermissionList(Command &cmd) { CMD_CHK_AND_INC_FLOOD_POINTS(5); static std::string permission_list_string[ClientType::MAX]; static std::mutex permission_list_string_lock; static auto build_permission_list = [](const std::string& command, const ClientType& type) { Command list_builder(command); int index = 0; for (auto group : permission::availableGroups) list_builder[index++]["group_id_end"] = group; auto avPerms = permission::availablePermissions; std::sort(avPerms.begin(), avPerms.end(), [](const std::shared_ptr &a, const std::shared_ptr &b) { return a->type < b->type; }); auto mapper = serverInstance->getPermissionMapper(); for (const auto& permission : avPerms) { if (!permission->clientSupported) continue; auto &blk = list_builder[index++]; blk["permname"] = permission->name; blk["permname"] = mapper->permission_name(type, permission->type); blk["permdesc"] = permission->description; blk["permid"] = permission->type; } return list_builder; }; auto type = this->getType(); if(type == CLIENT_TEASPEAK || type == CLIENT_TEAMSPEAK || type == CLIENT_QUERY) { Command response{this->notify_response_command("notifypermissionlist")}; { lock_guard lock(permission_list_string_lock); if(permission_list_string[type].empty()) permission_list_string[type] = build_permission_list("", type).build(); response[0][""] = permission_list_string[type]; } this->sendCommand(response); } else { this->sendCommand(build_permission_list(this->notify_response_command("notifypermissionlist"), type)); } return command_result{error::ok}; } #define M(ptype) \ do { \ for(const auto& prop : property::list()) { \ if((prop->flags & property::FLAG_INTERNAL) > 0) continue; \ response[index]["name"] = prop->name; \ response[index]["flags"] = prop->flags; \ response[index]["type"] = property::PropertyType_Names[prop->type_property]; \ response[index]["value"] = prop->default_value; \ response[index]["value_type"] = property::ValueType_Names[(int) prop->type_value]; \ index++; \ } \ } while(0) command_result ConnectedClient::handleCommandPropertyList(ts::Command& cmd) { Command response(this->notify_response_command("notifypropertylist")); { string pattern; for (auto flag_name : property::flag_names) { if(flag_name) { pattern = flag_name + string("|") + pattern; } } pattern = pattern.substr(0, pattern.length() - 1); response["flag_set"] = pattern; } int index = 0; if(cmd.hasParm("all") || cmd.hasParm("server")) M(property::VirtualServerProperties); if(cmd.hasParm("all") || cmd.hasParm("channel")) M(property::ChannelProperties); if(cmd.hasParm("all") || cmd.hasParm("client")) M(property::ClientProperties); if(cmd.hasParm("all") || cmd.hasParm("instance")) M(property::InstanceProperties); if(cmd.hasParm("all") || cmd.hasParm("connection")) M(property::ConnectionProperties); if(cmd.hasParm("all") || cmd.hasParm("playlist")) M(property::PlaylistProperties); this->sendCommand(response); return command_result{error::ok}; } command_result ConnectedClient::handleCommandSetClientChannelGroup(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(25); auto target_channel_group_id = cmd["cgid"].as(); std::shared_ptr target_channel_group{}, default_channel_group{}; default_channel_group = this->server->default_channel_group(); if(target_channel_group_id == 0) { target_channel_group = default_channel_group; if(!target_channel_group) { return command_result{error::vs_critical}; } } else { target_channel_group = this->server->group_manager()->channel_groups()->find_group(groups::GroupCalculateMode::GLOBAL, cmd["cgid"].as()); if(!target_channel_group) { return command_result{error::group_invalid_id}; } } std::shared_lock server_channel_lock{this->server->channel_tree_mutex}; /* ensure we dont get moved or somebody could move us */ auto channel_id = cmd["cid"].as(); auto channel = this->server->channelTree->findChannel(channel_id); if (!channel) { return command_result{error::channel_invalid_id}; } auto target_cldbid = cmd["cldbid"].as(); { auto channel_group_member_add_power = this->calculate_permission(permission::i_channel_group_member_add_power, channel_id); if(!target_channel_group->permission_granted(permission::i_channel_group_needed_member_add_power, channel_group_member_add_power, true)) { if(target_cldbid != this->getClientDatabaseId()) { return command_result{permission::i_channel_group_member_add_power}; } auto channel_group_self_add_power = this->calculate_permission(permission::i_channel_group_self_add_power, channel_id); if(!target_channel_group->permission_granted(permission::i_channel_group_needed_member_add_power, channel_group_self_add_power, true)) { return command_result{permission::i_channel_group_self_add_power}; } } auto client_permission_modify_power = this->calculate_permission(permission::i_client_permission_modify_power, channel_id); auto client_needed_permission_modify_power = this->server->calculate_permission( permission::i_client_needed_permission_modify_power, target_cldbid, ClientType::CLIENT_TEAMSPEAK, channel_id ).zero_if_unset(); if(client_needed_permission_modify_power.has_value) { if(!permission::v2::permission_granted(client_needed_permission_modify_power, client_permission_modify_power)) { return command_result{permission::i_client_permission_modify_power}; } } } auto old_group_id = this->server->group_manager()->assignments().exact_channel_group_of_client(groups::GroupAssignmentCalculateMode::GLOBAL, target_cldbid, channel->channelId()); std::shared_ptr old_group{}; if(old_group_id.has_value()) { old_group = this->server->group_manager()->channel_groups()->find_group(groups::GroupCalculateMode::GLOBAL, old_group_id->group_id); if(old_group) { auto channel_group_member_remove_power = this->calculate_permission(permission::i_channel_group_member_remove_power, channel_id); if(!old_group->permission_granted(permission::i_channel_group_needed_member_remove_power, channel_group_member_remove_power, true)) { if(target_cldbid != this->getClientDatabaseId()) { return command_result{permission::i_channel_group_member_remove_power}; } auto channel_group_self_remove_power = this->calculate_permission(permission::i_channel_group_self_remove_power, channel_id); if(!old_group->permission_granted(permission::i_channel_group_needed_member_remove_power, channel_group_self_remove_power, true)) { return command_result{permission::i_channel_group_self_remove_power}; } } } } this->server->group_manager()->assignments().set_channel_group(target_cldbid, target_channel_group->group_id(), channel->channelId(), !target_channel_group->is_permanent()); std::shared_ptr connected_client{}; for (const auto &targetClient : this->server->findClientsByCldbId(target_cldbid)) { connected_client = targetClient; /* Permissions might changed because of group inheritance */ targetClient->join_state_id++; bool channel_group_changed, server_group_changed; targetClient->update_displayed_client_groups(server_group_changed, channel_group_changed); auto client_channel = targetClient->getChannel(); if(!client_channel) { continue; } if(channel_group_changed) { targetClient->task_update_needed_permissions.enqueue(); targetClient->task_update_displayed_groups.enqueue(); std::optional notify{}; for (const auto &viewer : this->server->getClients()) { viewer->notifyClientChannelGroupChanged( notify, this->ref(), targetClient, client_channel->channelId(), targetClient->properties()[property::CLIENT_CHANNEL_GROUP_INHERITED_CHANNEL_ID], targetClient->properties()[property::CLIENT_CHANNEL_GROUP_ID]); } } } if(old_group) { serverInstance->action_logger()->group_assignment_logger.log_group_assignment_remove(this->getServerId(), this->ref(), log::GroupTarget::CHANNEL, old_group->group_id(), old_group->display_name(), target_cldbid, connected_client ? connected_client->getDisplayName() : "" ); } if(target_channel_group != default_channel_group) { serverInstance->action_logger()->group_assignment_logger.log_group_assignment_add(this->getServerId(), this->ref(), log::GroupTarget::CHANNEL, target_channel_group->group_id(), target_channel_group->display_name(), target_cldbid, connected_client ? connected_client->getDisplayName() : "" ); } return command_result{error::ok}; } //sendtextmessage targetmode=1 <1 = direct | 2 = channel | 3 = server> msg=asd target=1 command_result ConnectedClient::handleCommandSendTextMessage(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(5); auto timestamp = system_clock::now(); if (cmd["targetmode"].as() == ChatMessageMode::TEXTMODE_PRIVATE) { ConnectedLockedClient target{this->server->find_client_by_id(cmd["target"].as())}; if (!target) return command_result{error::client_invalid_id}; bool chat_open{false}; { std::unique_lock self_channel_lock{this->channel_tree_mutex}; this->open_private_conversations.erase(std::remove_if(this->open_private_conversations.begin(), this->open_private_conversations.end(), [](const weak_ptr& weak) { return !weak.lock(); }), this->open_private_conversations.end()); for(const auto& entry : this->open_private_conversations) { if(entry.lock() == target) { chat_open = true; break; } } } if(!chat_open) { if(target.client == this) { ACTION_REQUIRES_PERMISSION(permission::b_client_even_textmessage_send, 1, this->getChannelId()); } if(!permission::v2::permission_granted(target->calculate_permission(permission::i_client_needed_private_textmessage_power, target->getClientId()), this->calculate_permission(permission::i_client_private_textmessage_power, this->getClientId()))) { return command_result{permission::i_client_private_textmessage_power}; } { std::unique_lock target_channel_lock{target->get_channel_lock()}; target->open_private_conversations.push_back(_this); } { std::unique_lock self_channel_lock{this->channel_tree_mutex}; this->open_private_conversations.push_back(target.client); } } if(this->handleTextMessage(ChatMessageMode::TEXTMODE_PRIVATE, cmd["msg"], target.client)) { return command_result{error::ok}; } target->notifyTextMessage(ChatMessageMode::TEXTMODE_PRIVATE, this->ref(), target->getClientId(), 0, timestamp, cmd["msg"].string()); this->notifyTextMessage(ChatMessageMode::TEXTMODE_PRIVATE, this->ref(), target->getClientId(), 0, timestamp, cmd["msg"].string()); } else if (cmd["targetmode"] == ChatMessageMode::TEXTMODE_CHANNEL) { if(cmd[0].has("cid")) { cmd["target"] = cmd["cid"].string(); } if(!cmd[0].has("target")) { cmd["target"] = 0; } RESOLVE_CHANNEL_R(cmd["target"], false); auto channel = l_channel ? dynamic_pointer_cast(l_channel->entry) : nullptr; if(!channel) { CMD_REQ_CHANNEL; channel = this->currentChannel; channel_id = this->currentChannel->channelId(); } if(!permission::v2::permission_granted(1, this->calculate_permission(permission::b_client_channel_textmessage_send, channel_id))) \ return command_result{permission::b_client_channel_textmessage_send}; if(channel == this->currentChannel) { channel_tree_read_lock.unlock(); //Method may creates a music bot which modifies the channel tree if(this->handleTextMessage(ChatMessageMode::TEXTMODE_CHANNEL, cmd["msg"], nullptr)) { return command_result{error::ok}; } channel_tree_read_lock.lock(); } auto conversation_mode = channel->properties()[property::CHANNEL_CONVERSATION_MODE].as_unchecked(); if(conversation_mode == ChannelConversationMode::CHANNELCONVERSATIONMODE_NONE) { return command_result{error::conversation_not_exists}; } else if(channel != this->currentChannel) { if(conversation_mode == ChannelConversationMode::CHANNELCONVERSATIONMODE_PRIVATE) { return command_result{error::conversation_is_private}; } if(auto fail_perm{this->calculate_and_get_join_state(channel)}; fail_perm != permission::ok) { return command_result{fail_perm}; /* You're not allowed to send messages :) */ } } this->server->send_text_message(channel, this->ref(), cmd["msg"].string()); } else if (cmd["targetmode"] == ChatMessageMode::TEXTMODE_SERVER) { ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_client_server_textmessage_send, 1); if(this->handleTextMessage(ChatMessageMode::TEXTMODE_SERVER, cmd["msg"], nullptr)) return command_result{error::ok}; for(const auto& client : this->server->getClients()) { if (client->connectionState() != ConnectionState::CONNECTED) continue; auto type = client->getType(); if (type == ClientType::CLIENT_INTERNAL || type == ClientType::CLIENT_MUSIC) continue; client->notifyTextMessage(ChatMessageMode::TEXTMODE_SERVER, this->ref(), this->getClientId(), 0, timestamp, cmd["msg"].string()); } { auto conversations = this->server->conversation_manager(); auto conversation = conversations->get_or_create(0); conversation->register_message(this->getClientDatabaseId(), this->getUid(), this->getDisplayName(), timestamp, cmd["msg"].string()); } } else return command_result{error::parameter_invalid, "invalid target mode"}; return command_result{error::ok}; } //notifybanlist banid=3 ip name uid=zbex8X3bFRTIKLI7mzeyJGZsh64= lastnickname=Wolf\sC++\sXXXX created=1510357269 duration=3600 invokername=WolverinDEV invokercldbid=5 invokeruid=xxjnc14LmvTk+Lyrm8OOeo4tOqw= reason=Prefix\sFake\s\p\sName enforcements=3 command_result ConnectedClient::handleCommandBanList(Command &cmd) { CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(25); ServerId sid = this->getServerId(); if (cmd[0].has("sid")) { sid = cmd["sid"]; } if (sid == 0) { ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_client_ban_list_global, 1); } else { auto server = serverInstance->getVoiceServerManager()->findServerById(sid); if (!server) return command_result{error::parameter_invalid}; if (!permission::v2::permission_granted(1, server->calculate_permission(permission::b_client_ban_list, this->getClientDatabaseId(), this->getType(), 0))) { return command_result{permission::b_client_ban_list}; } } //When empty: return command_result{error::database_empty_result}; auto banList = serverInstance->banManager()->listBans(sid); if (banList.empty()) return command_result{error::database_empty_result}; auto allow_ip = permission::v2::permission_granted(1, this->calculate_permission(permission::b_client_remoteaddress_view, 0)); Command notify(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifybanlist" : ""); int index = 0; for (const auto &elm : banList) { notify[index]["sid"] = elm->serverId; notify[index]["banid"] = elm->banId; if(allow_ip) notify[index]["ip"] = elm->ip; else notify[index]["ip"] = "hidden"; notify[index]["name"] = elm->name; notify[index]["uid"] = elm->uid; notify[index]["hwid"] = elm->hwid; notify[index]["lastnickname"] = elm->name; //Maybe update? notify[index]["created"] = chrono::duration_cast(elm->created.time_since_epoch()).count(); if (elm->until.time_since_epoch().count() != 0) notify[index]["duration"] = chrono::duration_cast(elm->until - elm->created).count(); else notify[index]["duration"] = 0; notify[index]["reason"] = elm->reason; notify[index]["enforcements"] = elm->triggered; notify[index]["invokername"] = elm->invokerName; notify[index]["invokercldbid"] = elm->invokerDbId; notify[index]["invokeruid"] = elm->invokerUid; index++; } this->sendCommand(notify); return command_result{error::ok}; } command_result ConnectedClient::handleCommandBanAdd(Command &cmd) { CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(25); string ip = cmd[0].has("ip") ? cmd["ip"].string() : ""; string name = cmd[0].has("name") ? cmd["name"].string() : ""; string uid = cmd[0].has("uid") ? cmd["uid"].string() : ""; string hwid = cmd[0].has("hwid") ? cmd["hwid"].string() : ""; string banreason = cmd[0].has("banreason") ? cmd["banreason"].string() : "No reason set"; auto time = cmd[0].has("time") ? cmd["time"].as() : 0L; ServerId sid = this->getServerId(); if (cmd[0].has("sid")) sid = cmd["sid"]; if (sid == 0) { ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_client_ban_create_global, 1); } else { auto server = serverInstance->getVoiceServerManager()->findServerById(sid); if (!server) return command_result{error::parameter_invalid}; if (!permission::v2::permission_granted(1, server->calculate_permission(permission::b_client_ban_create, this->getClientDatabaseId(), this->getType(), 0))) return command_result{permission::b_client_ban_create}; } auto max_ban_time = this->calculate_permission(permission::i_client_ban_max_bantime, this->getClientDatabaseId()); if(!max_ban_time.has_value) return command_result{permission::i_client_ban_max_bantime}; if (!max_ban_time.has_infinite_power()) { if (max_ban_time.value < time) return command_result{permission::i_client_ban_max_bantime}; } chrono::time_point until = time > 0 ? chrono::system_clock::now() + chrono::seconds(time) : chrono::time_point(); auto existing = serverInstance->banManager()->findBanExact(sid, banreason, uid, ip, name, hwid); bool banned = false; if(existing) { if(existing->invokerDbId == this->getClientDatabaseId()) { if(existing->until == until) { return command_result{error::database_duplicate_entry}; } else { existing->until = until; serverInstance->banManager()->updateBan(existing); banned = true; } } else if(!banned) { serverInstance->banManager()->unban(existing); } } if(!banned) { serverInstance->banManager()->registerBan(sid, this->getClientDatabaseId(), banreason, uid, ip, name, hwid, until); } for(auto server : (this->server ? std::deque>{this->server} : serverInstance->getVoiceServerManager()->serverInstances())) server->testBanStateChange(this->ref()); return command_result{error::ok}; } command_result ConnectedClient::handleCommandBanEdit(Command &cmd) { CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(25); ServerId sid = this->getServerId(); if (cmd[0].has("sid")) sid = cmd["sid"]; auto ban = serverInstance->banManager()->findBanById(sid, cmd["banid"].as()); if (!ban) return command_result{error::database_empty_result}; if (sid == 0) { ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_client_ban_edit_global, 1); } else { auto server = serverInstance->getVoiceServerManager()->findServerById(sid); if (!server) return command_result{error::parameter_invalid}; if (!permission::v2::permission_granted(1, server->calculate_permission(permission::b_client_ban_edit, this->getClientDatabaseId(), this->getType(), 0))) return command_result{permission::b_client_ban_edit}; } /* ip name uid reason time hwid */ bool changed = false; if (cmd[0].has("ip")) { ban->ip = cmd["ip"].as(); changed |= true; } if (cmd[0].has("name")) { ban->name = cmd["name"].as(); changed |= true; } if (cmd[0].has("uid")) { ban->uid = cmd["uid"].as(); changed |= true; } if (cmd[0].has("reason")) { ban->reason = cmd["reason"].as(); changed |= true; } if (cmd[0].has("banreason")) { ban->reason = cmd["banreason"].as(); changed |= true; } if (cmd[0].has("time")) { ban->until = ban->created + seconds(cmd["time"].as()); changed |= true; } if (cmd[0].has("hwid")) { ban->hwid = cmd["hwid"].as(); changed |= true; } if (changed) serverInstance->banManager()->updateBan(ban); else return command_result{error::parameter_invalid}; return command_result{error::ok}; } command_result ConnectedClient::handleCommandBanClient(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(25); std::string target_unique_id{}; ClientDbId target_database_id{0}; string reason = cmd[0].has("banreason") ? cmd["banreason"].string() : ""; auto time = cmd[0].has("time") ? cmd["time"].as() : 0UL; chrono::time_point until = time > 0 ? chrono::system_clock::now() + chrono::seconds(time) : chrono::time_point(); const auto no_nickname = cmd.hasParm("no-nickname"); const auto no_hwid = cmd.hasParm("no-hardware-id"); const auto no_ip = cmd.hasParm("no-ip"); std::deque> target_clients; if (cmd[0].has("uid")) { target_clients = this->server->findClientsByUid(target_unique_id = cmd["uid"].string()); } else if(cmd[0].has("cldbid")) { target_clients = this->server->findClientsByCldbId(target_database_id = cmd["cldbid"].as()); } else { target_clients = {this->server->find_client_by_id(cmd["clid"].as())}; if(!target_clients[0]) { return command_result{error::client_invalid_id, "Could not find target client"}; } } for(const auto& client : target_clients) if(client->getType() == ClientType::CLIENT_MUSIC) return command_result{error::client_invalid_id, "You cant ban a music bot!"}; if(!target_clients.empty()) { if(target_unique_id.empty()) target_unique_id = target_clients.back()->getUid(); if(!target_database_id) target_database_id = target_clients.back()->getClientDatabaseId(); } if(!permission::v2::permission_granted(this->server->calculate_permission(permission::i_client_needed_ban_power, target_database_id, ClientType::CLIENT_TEAMSPEAK, 0).zero_if_unset(), this->calculate_permission(permission::i_client_ban_power, 0))) return command_result{permission::i_client_ban_power}; if (permission::v2::permission_granted(1, this->server->calculate_permission(permission::b_client_ignore_bans, target_database_id, ClientType::CLIENT_TEAMSPEAK, 0))) return command_result{permission::b_client_ignore_bans}; deque ban_ids; if(!target_unique_id.empty()) { auto _id = serverInstance->banManager()->registerBan(this->getServer()->getServerId(), this->getClientDatabaseId(), reason, target_unique_id, "", "", "", until); ban_ids.push_back(_id); } auto b_ban_name = permission::v2::permission_granted(1, this->calculate_permission(permission::b_client_ban_name, 0)); auto b_ban_ip = permission::v2::permission_granted(1, this->calculate_permission(permission::b_client_ban_ip, 0)); auto b_ban_hwid = permission::v2::permission_granted(1, this->calculate_permission(permission::b_client_ban_hwid, 0)); auto max_ban_time = this->calculate_permission(permission::i_client_ban_max_bantime, 0); if(!max_ban_time.has_value) return command_result{permission::i_client_ban_max_bantime}; if (!max_ban_time.has_infinite_power()) { if (max_ban_time.value < time) return command_result{permission::i_client_ban_max_bantime}; } for (const auto &client : target_clients) { if (client->getType() != CLIENT_TEAMSPEAK && client->getType() != CLIENT_QUERY) continue; //Remember if you add new type you have to change stuff here this->server->notify_client_ban(client, this->ref(), reason, time); client->close_connection(system_clock::now() + seconds(2)); string entry_name, entry_ip, entry_hardware_id; if(b_ban_name && !no_nickname) { entry_name = client->getDisplayName(); } if(b_ban_ip && !no_ip && !config::server::disable_ip_saving) { entry_ip = client->getPeerIp(); } if(b_ban_hwid && !no_hwid) { entry_hardware_id = client->getHardwareId(); } auto exact = serverInstance->banManager()->findBanExact(this->getServer()->getServerId(), reason, "", entry_ip, entry_name, entry_hardware_id); if(exact) { exact->until = until; exact->invokerDbId = this->getClientDatabaseId(); serverInstance->banManager()->updateBan(exact); ban_ids.push_back(exact->banId); } else { auto id = serverInstance->banManager()->registerBan(this->getServer()->getServerId(), this->getClientDatabaseId(), reason, "", entry_ip, entry_name, entry_hardware_id, until); ban_ids.push_back(id); } } this->server->testBanStateChange(this->ref()); if (this->getType() == CLIENT_QUERY) { Command notify(""); int index = 0; for(const auto& ban_id : ban_ids) notify[index++]["banid"] = ban_id; this->sendCommand(notify); } return command_result{error::ok}; } command_result ConnectedClient::handleCommandBanDel(Command &cmd) { CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(5); ServerId sid = this->getServerId(); if (cmd[0].has("sid")) sid = cmd["sid"]; auto ban = serverInstance->banManager()->findBanById(sid, cmd["banid"].as()); if (!ban) return command_result{error::database_empty_result}; if (sid == 0) { const auto permission = ban->invokerDbId == this->getClientDatabaseId() ? permission::b_client_ban_delete_own_global : permission::b_client_ban_delete_global; ACTION_REQUIRES_GLOBAL_PERMISSION(permission, 1); } else { auto server = serverInstance->getVoiceServerManager()->findServerById(sid); if (!server) return command_result{error::parameter_invalid}; auto perm = ban->invokerDbId == this->getClientDatabaseId() ? permission::b_client_ban_delete_own : permission::b_client_ban_delete; if (!permission::v2::permission_granted(1, server->calculate_permission(perm, this->getClientDatabaseId(), this->getType(), 0))) return command_result{perm}; } serverInstance->banManager()->unban(ban); return command_result{error::ok}; } command_result ConnectedClient::handleCommandBanDelAll(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(25); ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_client_ban_delete, 1); serverInstance->banManager()->deleteAllBans(server->getServerId()); return command_result{error::ok}; } command_result ConnectedClient::handleCommandBanTriggerList(ts::Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(25); ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_client_ban_trigger_list, 1); CMD_REQ_PARM("banid"); auto record = serverInstance->banManager()->findBanById(this->getServerId(), cmd["banid"]); if(!record) return command_result{error::parameter_invalid, "Invalid ban id"}; Command notify(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifybantriggerlist" : ""); notify["banid"] = record->banId; notify["serverid"] = record->serverId; auto allow_ip = permission::v2::permission_granted(1, this->calculate_permission(permission::b_client_remoteaddress_view, 0)); int index = 0; for (auto &entry : serverInstance->banManager()->trigger_list(record, this->getServerId(), cmd[0].has("offset") ? cmd["offset"].as() : 0, cmd[0].has("limit") ? cmd["limit"].as() : -1)) { notify[index]["client_unique_identifier"] = entry->unique_id; notify[index]["client_hardware_identifier"] = entry->hardware_id; notify[index]["client_nickname"] = entry->name; if(allow_ip) notify[index]["connection_client_ip"] = entry->ip; else notify[index]["connection_client_ip"] = "hidden"; notify[index]["timestamp"] = duration_cast(entry->timestamp.time_since_epoch()).count(); index++; } if (index == 0) return command_result{error::database_empty_result}; this->sendCommand(notify); return command_result{error::ok}; } command_result ConnectedClient::handleCommandTokenList(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(5); auto& token_manager = this->server->getTokenManager(); size_t offset = cmd[0].has("offset") ? cmd["offset"].as() : 0; size_t limit = cmd[0].has("limit") ? cmd["limit"].as() : 0; if(limit > 2000 || limit < 1) { limit = 1000; } size_t total_tokens{0}; std::deque> tokens{}; auto own_tokens_only = cmd.hasParm("own-only") || !permission::v2::permission_granted(1, this->calculate_permission(permission::b_virtualserver_token_list_all, 0)); if(own_tokens_only) { tokens = token_manager.list_tokens(total_tokens, { offset }, { limit }, { this->getClientDatabaseId() }); } else { tokens = token_manager.list_tokens(total_tokens, { offset }, { limit }, std::nullopt); } if(tokens.empty()) { return ts::command_result{error::database_empty_result}; } auto new_command = cmd.hasParm("new"); auto own_database_id = this->getClientDatabaseId(); ts::command_builder notify{this->notify_response_command("notifytokenlist")}; size_t notify_index{0}; for(size_t index{0}; index < tokens.size(); index++) { auto& token = tokens[index]; if(own_tokens_only && token->issuer_database_id != own_database_id) { continue; } auto bulk = notify.bulk(notify_index++); bulk.put_unchecked("token", token->token); bulk.put_unchecked("token_id", token->id); bulk.put_unchecked("token_created", std::chrono::duration_cast(token->timestamp_created.time_since_epoch()).count()); bulk.put_unchecked("token_expired", std::chrono::duration_cast(token->timestamp_expired.time_since_epoch()).count()); bulk.put_unchecked("token_use_count", token->use_count); bulk.put_unchecked("token_max_uses", token->max_uses); bulk.put_unchecked("token_issuer_database_id", token->issuer_database_id); bulk.put_unchecked("token_description", token->description); if(!new_command) { bulk.put_unchecked("token_type", 0); bulk.put_unchecked("token_id1", 0); bulk.put_unchecked("token_id2", 0); } } if(notify_index == 0) { return ts::command_result{error::database_empty_result}; } notify.put_unchecked(0, "token_count", total_tokens); this->sendCommand(notify); return command_result{error::ok}; } command_result ConnectedClient::handleCommandTokenActionList(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(5); auto& token_manager = this->server->getTokenManager(); std::shared_ptr token_info{}; if(cmd[0].has("token_id")) { token_info = this->server->getTokenManager().load_token_by_id(cmd["token_id"].as(), true); } else if(cmd[0].has("token")) { token_info = this->server->getTokenManager().load_token(cmd["token"], true); } if(!token_info) { return ts::command_result{error::token_invalid_id}; } if(token_info->issuer_database_id != this->getClientDatabaseId()) { ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_virtualserver_token_list_all, 1); } std::deque actions{}; if(!token_manager.query_token_actions(token_info->id, actions)) { return ts::command_result{error::vs_critical}; } ts::command_builder notify{this->notify_response_command("notifytokenactions")}; for(size_t index{0}; index < actions.size(); index++) { auto bulk = notify.bulk(index); auto& action = actions[index]; bulk.put_unchecked("action_type", (uint8_t) action.type); bulk.put_unchecked("action_id", action.id); bulk.put_unchecked("action_id1", action.id1); bulk.put_unchecked("action_id2", action.id2); bulk.put_unchecked("action_text", action.text); } this->sendCommand(notify); return command_result{error::ok}; } /** * * @param result Should already contain cmd.bulkCount() results * @param cmd * @param token_description * @param max_uses * @param timestamp_expired * @param actions * @return */ ts::command_result parseTokenParameters( ts::command_result_bulk& result, Command &cmd, std::optional& token_description, std::optional& max_uses, std::optional& timestamp_expired, std::vector& actions ) { actions.reserve(cmd.bulkCount()); /* legacy (should not be provided on token edit) */ if(cmd[0].has("tokentype")) { auto ttype = cmd["tokentype"].as(); auto gId = cmd["tokenid1"].as(); auto cId = cmd["tokenid2"].as(); if(ttype == 0) { auto& action = actions.emplace_back(); action.type = token::ActionType::AddServerGroup; action.id1 = gId; } else if(ttype == 1) { auto& action = actions.emplace_back(); action.type = token::ActionType::SetChannelGroup; action.id1 = gId; action.id2 = cId; } else { result.set_result(0, ts::command_result{error::parameter_invalid, "tokentype"}); } } else { for(size_t index{0}; index < cmd.bulkCount(); index++) { auto& action = actions.emplace_back(); action.id = cmd[index].has("action_id") ? cmd[index]["action_id"] : 0; if(!cmd[index].has("action_type")) { if(action.id > 0) { /* we want to delete the action */ action.type = token::ActionType::ActionDeleted; continue; } /* Missing the parameter action type */ action.type = token::ActionType::ActionIgnore; if(index == 0) { /* no actions provided */ break; } else { result.set_result(index, ts::command_result{error::parameter_missing, "action_type"}); continue; } } action.type = cmd[index]["action_type"]; switch(action.type) { case token::ActionType::AddServerGroup: case token::ActionType::RemoveServerGroup: case token::ActionType::SetChannelGroup: case token::ActionType::AllowChannelJoin: break; case token::ActionType::ActionSqlFailed: case token::ActionType::ActionDeleted: case token::ActionType::ActionIgnore: default: result.set_result(index, ts::command_result{error::parameter_invalid, "action_type"}); continue; } if(cmd[index].has("action_id1")) { action.id1 = cmd[index]["action_id1"]; } if(cmd[index].has("action_id2")) { action.id2 = cmd[index]["action_id2"]; } if(cmd[index].has("action_text")) { action.text = cmd[index]["action_text"].string(); } } } if(cmd[0].has("token_description")) { token_description = { cmd["token_description"].string() }; } else if(cmd[0].has("tokendescription")) { token_description = { cmd["tokendescription"].string() }; } if(token_description.has_value() && token_description->length() > 255) { return ts::command_result{error::parameter_invalid, "token_description"}; } if(cmd[0].has("token_max_uses")) { max_uses = { cmd["token_max_uses"].as() }; } if(cmd[0].has("token_expired")) { auto seconds = cmd["token_expired"].as(); timestamp_expired = { std::chrono::system_clock::time_point{} + std::chrono::seconds{seconds} }; } return ts::command_result{error::ok}; } /** * Check if the client has permissions to do such actions. * If not the toke will be set to ignored and a proper error will be emplaced */ void check_token_action_permissions(const std::shared_ptr& client, ts::command_result_bulk& result, std::vector& actions) { for(size_t index{0}; index < actions.size(); index++) { auto& action = actions[index]; switch(action.type) { case ActionType::AddServerGroup: { auto target_group = client->getServer()->group_manager()->server_groups()->find_group(groups::GroupCalculateMode::GLOBAL, action.id1); if(!target_group || target_group->group_type() == groups::GroupType::GROUP_TYPE_TEMPLATE) { action.type = ActionType::ActionIgnore; result.set_result(index, ts::command_result{error::group_invalid_id}); break; } if(!target_group->permission_granted(permission::i_server_group_needed_member_add_power, client->calculate_permission(permission::i_server_group_member_add_power, 0), true)) { action.type = ActionType::ActionIgnore; result.set_result(index, ts::command_result{permission::i_server_group_member_add_power}); break; } break; } case ActionType::RemoveServerGroup: { auto target_group = client->getServer()->group_manager()->channel_groups()->find_group(groups::GroupCalculateMode::GLOBAL, action.id1); if(!target_group || target_group->group_type() == groups::GroupType::GROUP_TYPE_TEMPLATE) { action.type = ActionType::ActionIgnore; result.set_result(index, ts::command_result{error::group_invalid_id}); break; } if(!target_group->permission_granted(permission::i_server_group_needed_member_remove_power, client->calculate_permission(permission::i_server_group_member_remove_power, 0), true)) { action.type = ActionType::ActionIgnore; result.set_result(index, ts::command_result{permission::i_server_group_member_remove_power}); break; } break; } case ActionType::SetChannelGroup: { auto target_group = client->getServer()->group_manager()->server_groups()->find_group(groups::GroupCalculateMode::GLOBAL, action.id1); if(!target_group || target_group->group_type() == groups::GroupType::GROUP_TYPE_TEMPLATE) { action.type = ActionType::ActionIgnore; result.set_result(index, ts::command_result{error::group_invalid_id}); break; } auto granted_permission = client->calculate_permission(permission::i_channel_group_member_add_power, action.id2); if(!target_group->permission_granted(permission::i_channel_group_needed_member_add_power, granted_permission, true)) { action.type = ActionType::ActionIgnore; result.set_result(index, ts::command_result{permission::i_channel_group_member_add_power}); break; } { std::shared_lock tree_lock{client->getServer()->get_channel_tree_lock()}; auto target_channel = client->getServer()->getChannelTree()->findChannel(action.id2); tree_lock.unlock(); if(!target_channel) { action.type = ActionType::ActionIgnore; result.set_result(index, ts::command_result{error::channel_invalid_id}); break; } } break; } case ActionType::AllowChannelJoin: { std::shared_lock tree_lock{client->getServer()->get_channel_tree_lock()}; auto target_channel = client->getServer()->getChannelTree()->findChannel(action.id2); if(!target_channel) { action.type = ActionType::ActionIgnore; result.set_result(index, ts::command_result{error::channel_invalid_id}); break; } auto join_permission = client->calculate_and_get_join_state(target_channel); if(join_permission != permission::ok) { action.type = ActionType::ActionIgnore; result.set_result(index, ts::command_result{join_permission}); break; } if(client->getType() == ClientType::CLIENT_QUERY && !action.text.empty()) { action.text = digest::sha1(action.text); } if(permission::v2::permission_granted(1, client->calculate_permission(permission::b_channel_join_ignore_password, target_channel->channelId()))) { action.text = "ignore"; } else if(!target_channel->verify_password(action.text, true)) { action.type = ActionType::ActionIgnore; result.set_result(index, ts::command_result{error::channel_invalid_password}); break; } break; } case ActionType::ActionSqlFailed: case ActionType::ActionIgnore: case ActionType::ActionDeleted: default: continue; } } } command_result ConnectedClient::handleCommandTokenAdd(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(5); auto client_tokens = this->server->tokenManager->client_token_count(this->getClientDatabaseId()); auto token_limit = this->calculate_permission(permission::i_virtualserver_token_limit, 0); if(token_limit.has_value) { if(!permission::v2::permission_granted(client_tokens + 1, token_limit)) { return ts::command_result{permission::i_virtualserver_token_limit}; } } std::vector actions{}; actions.reserve(cmd.bulkCount()); std::optional token_description{}; std::optional max_uses{}; std::optional timestamp_expired{}; ts::command_result_bulk result{}; result.emplace_result_n(cmd.bulkCount(), error::ok); auto parse_result = parseTokenParameters( result, cmd, token_description, max_uses, timestamp_expired, actions ); if(parse_result.has_error()) { return parse_result; } parse_result.release_data(); check_token_action_permissions(this->ref(), result, actions); auto token = this->server->getTokenManager().create_token( this->getClientDatabaseId(), token_description.value_or(""), max_uses.value_or(0), timestamp_expired.value_or(std::chrono::system_clock::time_point{}) ); if(!token) { return ts::command_result{error::vs_critical}; } ts::command_builder notify{this->notify_response_command("notifytokenadd")}; if(!actions.empty()) { this->server->getTokenManager().add_token_actions(token->id, actions); for(size_t index{0}; index < actions.size(); index++) { auto& action = actions[index]; if(action.type == token::ActionType::ActionIgnore || action.type == token::ActionType::ActionDeleted) { continue; } else if(action.type == token::ActionType::ActionSqlFailed) { result.set_result(index, ts::command_result{error::database_constraint}); } else { notify.put_unchecked(index, "action_id", action.id); } } } notify.put_unchecked(0, "token", token->token); notify.put_unchecked(0, "token_id", token->id); this->sendCommand(notify); return command_result{std::move(result)}; } command_result ConnectedClient::handleCommandTokenEdit(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(15); std::shared_ptr token{}; if(cmd[0].has("token_id")) { token = this->server->getTokenManager().load_token_by_id(cmd["token_id"].as(), true); } else if(cmd[0].has("token")) { token = this->server->getTokenManager().load_token(cmd["token"], true); } if(!token) { return ts::command_result{error::token_invalid_id}; } if(token->issuer_database_id != this->getClientDatabaseId()) { ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_virtualserver_token_edit_all, 1); } std::vector actions{}; actions.reserve(cmd.bulkCount()); std::optional token_description{}; std::optional max_uses{}; std::optional timestamp_expired{}; ts::command_result_bulk result{}; result.emplace_result_n(cmd.bulkCount(), error::ok); auto parse_result = parseTokenParameters( result, cmd, token_description, max_uses, timestamp_expired, actions ); if(parse_result.has_error()) { return parse_result; } parse_result.release_data(); /* filter actions for duplicates etc */ { std::deque current_actions{}; if(!this->server->getTokenManager().query_token_actions(token->id, current_actions)) { return ts::command_result{error::vs_critical}; } for(const auto& action : actions) { if (action.type == ActionType::ActionDeleted) { auto index = std::find_if(current_actions.begin(), current_actions.end(), [&](const auto& caction) { return caction.id == action.id; }); if(index != current_actions.end()) { current_actions.erase(index); } } } size_t action_index{0}; for(auto& action : actions) { switch(action.type) { case ActionType::AddServerGroup: case ActionType::RemoveServerGroup: case ActionType::SetChannelGroup: case ActionType::AllowChannelJoin: { auto index = std::find_if(current_actions.begin(), current_actions.end(), [&](const TokenAction& caction) { if(caction.type != action.type) { return false; } if(caction.text != action.text) { return false; } if(caction.id1 != action.id1) { return false; } return caction.id2 == action.id2; }); if(index != current_actions.end()) { action.type = ActionType::ActionIgnore; result.set_result(action_index, ts::command_result{error::database_no_modifications}); break; } current_actions.push_back(action); break; } case ActionType::ActionSqlFailed: case ActionType::ActionIgnore: case ActionType::ActionDeleted: default: break; } action_index++; } } check_token_action_permissions(this->ref(), result, actions); if(token_description.has_value() || max_uses.has_value() || timestamp_expired.has_value()) { token->description = token_description.value_or(token->description); token->max_uses = max_uses.value_or(token->max_uses); token->timestamp_expired = timestamp_expired.value_or(token->timestamp_expired); this->server->getTokenManager().save_token(token); } if(!actions.empty()) { size_t new_actions{0}; std::vector actions_removed{}; actions_removed.reserve(actions.size()); for(const auto& action : actions) { if(action.type == ActionType::ActionDeleted) { actions_removed.push_back(action.id); } else if(action.type == ActionType::ActionIgnore) { /* just ignore the action */ } else { new_actions++; } } if(!actions_removed.empty()) { this->server->getTokenManager().delete_token_actions(token->id, actions_removed); } if(new_actions > 0) { this->server->getTokenManager().add_token_actions(token->id, actions); ts::command_builder notify{this->notify_response_command("notifytokenedit")}; for(size_t index{0}; index < actions.size(); index++) { auto& action = actions[index]; if(action.type == token::ActionType::ActionIgnore || action.type == token::ActionType::ActionDeleted) { continue; } else if(action.type == token::ActionType::ActionSqlFailed) { result.set_result(0, ts::command_result{error::database_constraint}); } else { notify.put_unchecked(index, "action_id", action.id); } } this->sendCommand(notify); } } return command_result{std::move(result)}; } command_result ConnectedClient::handleCommandTokenUse(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(5); //ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_virtualserver_token_use, 1); auto& token_manager = this->server->getTokenManager(); auto token = token_manager.load_token(cmd["token"], true); if(!token) { return ts::command_result{error::token_invalid_id}; } if(token->is_expired()) { return ts::command_result{error::token_expired}; } if(token->use_limit_reached()) { return ts::command_result{error::token_use_limit_exceeded}; } this->server->properties()[property::VIRTUALSERVER_ASK_FOR_PRIVILEGEKEY] = false; //TODO test if its the default token token_manager.log_token_use(token->id); this->useToken(token->id); return command_result{error::ok}; } command_result ConnectedClient::handleCommandTokenDelete(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(5); std::shared_ptr token{}; if(cmd[0].has("token_id")) { token = this->server->getTokenManager().load_token_by_id(cmd["token_id"].as(), true); } else if(cmd[0].has("token")) { token = this->server->getTokenManager().load_token(cmd["token"], true); } if(!token) { return ts::command_result{error::token_invalid_id}; } if(token->issuer_database_id != this->getClientDatabaseId()) { ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_virtualserver_token_delete_all, 1); } this->server->getTokenManager().delete_token(token->id); if(token->token == this->server->properties()[property::VIRTUALSERVER_AUTOGENERATED_PRIVILEGEKEY].as_unchecked()) { this->server->properties()[property::VIRTUALSERVER_AUTOGENERATED_PRIVILEGEKEY] = ""; this->server->properties()[property::VIRTUALSERVER_ASK_FOR_PRIVILEGEKEY] = false; logMessage(this->getServerId(), "{} Deleting the default server token. Don't ask anymore for this a token!", CLIENT_STR_LOG_PREFIX); } return command_result{error::ok}; } command_result ConnectedClient::handleCommandPluginCmd(Command &cmd) { CMD_REQ_SERVER; CMD_CHK_AND_INC_FLOOD_POINTS(5); auto mode = cmd["targetmode"].as(); if (mode == PluginTargetMode::PLUGINCMD_CURRENT_CHANNEL) { CMD_REQ_CHANNEL; for (auto &cl : this->server->getClientsByChannel(this->currentChannel)) cl->notifyPluginCmd(cmd["name"], cmd["data"], this->ref()); } else if (mode == PluginTargetMode::PLUGINCMD_SUBSCRIBED_CLIENTS) { for (auto &cl : this->server->getClients()) if (cl->isClientVisible(this->ref(), true)) cl->notifyPluginCmd(cmd["name"], cmd["data"], this->ref()); } else if (mode == PluginTargetMode::PLUGINCMD_SERVER) { for (auto &cl : this->server->getClients()) cl->notifyPluginCmd(cmd["name"], cmd["data"], this->ref()); } else if (mode == PluginTargetMode::PLUGINCMD_CLIENT) { for (int index = 0; index < cmd.bulkCount(); index++) { auto target = cmd[index]["target"].as(); ConnectedLockedClient cl{this->server->find_client_by_id(target)}; if (!cl) return command_result{error::client_invalid_id}; cl->notifyPluginCmd(cmd["name"], cmd["data"], this->ref()); } } /* else if(mode == PluginTargetMode::PLUGINCMD_SEND_COMMAND) { auto target = this->ref(); if(cmd[0].has("target")) target = this->server->findClient(cmd["target"].as()); if(!target) return command_result{error::client_invalid_id, "invalid target id"}; target->sendCommand(Command(cmd["command"].string()), cmd[0].has("low") ? cmd["low"] : false); } */ else return command_result{error::not_implemented}; return command_result{error::ok}; } command_result ConnectedClient::handleCommandWhoAmI(Command &cmd) { CMD_RESET_IDLE; Command result(""); if (this->server) { result["virtualserver_status"] = ServerState::string(this->getServer()->state); result["virtualserver_id"] = this->server->getServerId(); result["virtualserver_unique_identifier"] = this->server->properties()[property::VIRTUALSERVER_UNIQUE_IDENTIFIER].as_unchecked(); result["virtualserver_port"] = 0; if (this->server->udpVoiceServer) { result["virtualserver_port"] = this->server->properties()[property::VIRTUALSERVER_PORT].as_or(0); } } else { result["virtualserver_status"] = "template"; result["virtualserver_id"] = "0"; result["virtualserver_unique_identifier"] = ""; //TODO generate uid result["virtualserver_port"] = "0"; } result["client_id"] = this->getClientId(); result["client_channel_id"] = this->currentChannel ? this->currentChannel->channelId() : 0; result["client_nickname"] = this->getDisplayName(); result["client_database_id"] = this->getClientDatabaseId(); result["client_login_name"] = this->properties()[property::CLIENT_LOGIN_NAME].as_unchecked(); result["client_unique_identifier"] = this->getUid(); { auto query = dynamic_cast(this); if(query) { auto account = query->getQueryAccount(); result["client_origin_server_id"] = account ? account->bound_server : 0; } else result["client_origin_server_id"] = 0; } this->sendCommand(result); return command_result{error::ok}; } struct DBFindArgs { int index = 0; bool full = false; bool ip = false; Command cmd{""}; }; command_result ConnectedClient::handleCommandVersion(Command &) { CMD_RESET_IDLE; Command res(""); res["version"] = build::version()->string(false); res["build_count"] = build::buildCount(); res["build"] = duration_cast(build::version()->timestamp.time_since_epoch()).count(); #ifdef WIN32 res["platform"] = "Windows"; #else res["platform"] = "Linux"; #endif this->sendCommand(res); return command_result{error::ok}; } //cid=%d password=%s command_result ConnectedClient::handleCommandVerifyChannelPassword(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(5); std::shared_ptr channel = (this->server ? this->server->channelTree : serverInstance->getChannelTree().get())->findChannel(cmd["cid"].as()); if (!channel) return command_result{error::channel_invalid_id, "Cant resolve channel"}; if (!channel->verify_password(cmd["password"].optional_string(), this->getType() != ClientType::CLIENT_QUERY)) return command_result{error::server_invalid_password}; return command_result{error::ok}; } command_result ConnectedClient::handleCommandVerifyServerPassword(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(5); std::string password = cmd["password"]; if (!this->server->verifyServerPassword(password, false)) return command_result{error::server_invalid_password}; return command_result{error::ok}; } //msgid=2 cluid=IkBXingb46\/z1Q3hhMvJEweb3lw= subject=The\sSubject timestamp=1512224138 flag_read=0 //notifymessagelist msgid=2 cluid=IkBXingb46\/z1Q3hhMvJEweb3lw= subject=The\sSubject timestamp=1512224138 flag_read=0 command_result ConnectedClient::handleCommandMessageList(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(5); auto msgList = this->server->letters->avariableLetters(this->getUid()); if (msgList.empty()) return command_result{error::database_empty_result, "no letters available"}; Command notify(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifymessagelist" : ""); int index = 0; for (const auto &elm : msgList) { notify[index]["msgid"] = elm->id; notify[index]["cluid"] = elm->sender; notify[index]["subject"] = elm->subject; notify[index]["timestamp"] = duration_cast(elm->created.time_since_epoch()).count(); notify[index]["flag_read"] = elm->read; index++; } this->sendCommand(notify); return command_result{error::ok}; } //messageadd cluid=ePHuXhcai9nk\/4Fd\/xkxrokvnNk= subject=Test message=Message command_result ConnectedClient::handleCommandMessageAdd(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(25); ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_client_offline_textmessage_send, 1); this->server->letters->createLetter(this->getUid(), cmd["cluid"], cmd["subject"], cmd["message"]); return command_result{error::ok}; } command_result ConnectedClient::handleCommandMessageGet(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(10); auto letter = this->server->letters->getFullLetter(cmd["msgid"]); //msgid=2 cluid=IkBXingb46\/z1Q3hhMvJEweb3lw= subject=The\sSubject message=The\sbody timestamp=1512224138 Command notify(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifymessage" : ""); notify["msgid"] = cmd["msgid"]; notify["cluid"] = letter->sender; notify["subject"] = letter->subject; notify["message"] = letter->message; notify["timestamp"] = duration_cast(letter->created.time_since_epoch()).count(); this->sendCommand(notify); return command_result{error::ok}; } command_result ConnectedClient::handleCommandMessageUpdateFlag(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(5); this->server->letters->updateReadFlag(cmd["msgid"], cmd["flag"]); return command_result{error::ok}; } command_result ConnectedClient::handleCommandMessageDel(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(5); this->server->letters->deleteLetter(cmd["msgid"]); return command_result{error::ok}; } command_result ConnectedClient::handleCommandPermGet(Command &cmd) { CMD_RESET_IDLE; ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_client_permissionoverview_own, 1); Command res(""); deque requrested; auto permission_mapper = serverInstance->getPermissionMapper(); auto type = this->getType(); for (int index = 0; index < cmd.bulkCount(); index++) { permission::PermissionType permType = permission::unknown; if (cmd[index].has("permid")) permType = cmd[index]["permid"].as(); else if (cmd[index].has("permsid")) permType = permission::resolvePermissionData(cmd[index]["permsid"].as())->type; //TODO: Map the other way around! if (permission::resolvePermissionData(permType)->type == permission::PermissionType::unknown) return command_result{error::parameter_invalid, "could not resolve permission"}; requrested.push_back(permType); } int index = 0; for(const auto& entry : this->calculate_permissions(requrested, this->getChannelId())) { if(!entry.second.has_value) continue; res[index]["permsid"] = permission_mapper->permission_name(type, entry.first);; res[index]["permid"] = entry.first; res[index++]["permvalue"] = entry.second.value; } if(index == 0) return command_result{error::database_empty_result}; this->sendCommand(res); return command_result{error::ok}; } command_result ConnectedClient::handleCommandPermIdGetByName(Command &cmd) { auto found = permission::resolvePermissionData(cmd["permsid"].as()); //TODO: Map the other way around Command res(""); res["permid"] = found->type; this->sendCommand(res); return command_result{error::ok}; } command_result ConnectedClient::handleCommandPermFind(Command &cmd) { struct PermissionEntry { permission::PermissionType permission_type; permission::PermissionValue permission_value; permission::PermissionSqlType type; GroupId group_id; ChannelId channel_id; ClientDbId client_id; bool negate; bool skip; }; CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(5); ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_virtualserver_permission_find, 1); std::vector> requested_permissions{}; requested_permissions.reserve(cmd.bulkCount()); std::shared_ptr permission; for(size_t index = 0; index < cmd.bulkCount(); index++) { bool granted = false; if (cmd[index].has("permid")) { permission = permission::resolvePermissionData((permission::PermissionType) (cmd[index]["permid"].as() & (~PERM_ID_GRANT))); granted = (cmd[index]["permid"].as() & PERM_ID_GRANT) > 0; if(permission->type == permission::PermissionType::unknown) return command_result{error::parameter_invalid, "could not resolve permission (id=" + cmd[index]["permid"].string() + ")"}; } else if (cmd[index].has("permsid")) { permission = permission::resolvePermissionData(cmd[index]["permsid"].as()); //TODO: Map the other way around granted = permission->grant_name == cmd[index]["permsid"].as(); if(permission->type == permission::PermissionType::unknown) return command_result{error::parameter_invalid, "could not resolve permission (id=" + cmd[index]["permid"].string() + ")"}; } else { continue; } requested_permissions.emplace_back(permission->name, permission->type, granted); } if(requested_permissions.empty()) return command_result{error::database_empty_result}; std::map> db_lookup_mapping{}; std::string query_string{}; for(const auto& [name, id, as_granted] : requested_permissions) { auto& mapping = db_lookup_mapping[name]; if(std::get<0>(mapping) == 0) { std::get<0>(mapping) = id; query_string += std::string{query_string.empty() ? "" : " OR "} + "`permId` = '" + name + "'"; } std::get<1>(mapping) |= (1U << as_granted); } deque> entries; //`serverId` INT NOT NULL, `type` INT, `id` INT, `channelId` INT, `permId` VARCHAR(" UNKNOWN_KEY_LENGTH "), `value` INT, `grant` INT sql::command(this->sql, "SELECT `permId`, `type`, `id`, `channelId`, `value`, `grant`, `flag_skip`, `flag_negate` FROM `permissions` WHERE `serverId` = :sid AND (" + query_string + ") AND `type` != :playlist", variable{":sid", this->server->getServerId()}, variable{":playlist", permission::SQL_PERM_PLAYLIST} ).query([&](int length, string* values, string* columns) { permission::PermissionSqlType type{permission::SQL_PERM_GROUP}; uint64_t id{0}; ChannelId channel_id{0}; permission::PermissionValue value{0}, granted_value{0}; string permission_name{}; bool negate = false, skip = false; #if 0 for (int index = 0; index < length; index++) { try { if(columns[index] == "type") type = static_cast(stoll(values[index])); else if(columns[index] == "permId") permission_name = values[index]; else if(columns[index] == "id") id = static_cast(stoll(values[index])); else if(columns[index] == "channelId") channel_id = static_cast(stoll(values[index])); else if(columns[index] == "value") value = static_cast(stoll(values[index])); else if(columns[index] == "grant") granted_value = static_cast(stoll(values[index])); else if(columns[index] == "flag_negate") negate = !values[index].empty() && stol(values[index]) == 1; else if(columns[index] == "flag_skip") skip = !values[index].empty() && stol(values[index]) == 1; } catch(std::exception& ex) { debugMessage(this->getServerId(), "[{}] 'permfind' iterates over invalid permission entry. Key: {}, Value: {}, Error: {}", CLIENT_STR_LOG_PREFIX, columns[index], values[index], ex.what()); return 0; } } #else assert(length == 8); try { type = static_cast(stoll(values[1])); permission_name = values[0]; id = static_cast(stoll(values[2])); channel_id = static_cast(stoll(values[3])); value = static_cast(stoll(values[4])); granted_value = static_cast(stoll(values[5])); negate = values[7] == "1"; skip = values[6] == "1"; } catch(std::exception& ex) { return 0; } #endif auto request = db_lookup_mapping.find(permission_name); if(request == db_lookup_mapping.end()) return 0; /* shall not happen */ auto flags = std::get<1>(request->second); /* value */ if((flags & 0x1U) > 0 && value > 0) { auto result = make_unique(); result->permission_type = std::get<0>(request->second); result->permission_value = value; result->type = type; result->channel_id = channel_id; result->negate = negate; result->skip = skip; if (type == permission::SQL_PERM_GROUP) { result->group_id = id; } else if(type == permission::SQL_PERM_USER) { result->client_id = id; } entries.push_back(std::move(result)); } /* granted */ if((flags & 0x2U) > 0 && granted_value > 0) { auto result = make_unique(); result->permission_type = (permission::PermissionType) (std::get<0>(request->second) | PERM_ID_GRANT); result->permission_value = granted_value; result->type = type; result->channel_id = channel_id; result->negate = negate; result->skip = skip; if (type == permission::SQL_PERM_GROUP) { result->group_id = id; } else if(type == permission::SQL_PERM_USER) { result->client_id = id; } entries.push_back(std::move(result)); } return 0; }); if(entries.empty()) { return command_result{error::database_empty_result}; } struct CommandPerm { permission::PermissionType p; permission::PermissionValue v; int64_t id1; int64_t id2; uint8_t t; }; std::vector perms; perms.resize(entries.size()); size_t index{0}; /* 1 := Server | 2 := Channel */ btree::map group_types{}; for(const auto& s_group : this->server->group_manager()->server_groups()->available_groups(groups::GroupCalculateMode::GLOBAL)) { group_types[s_group->group_id()] = 1; } for(const auto& c_group : this->server->group_manager()->channel_groups()->available_groups(groups::GroupCalculateMode::GLOBAL)) { group_types[c_group->group_id()] = 2; } for(const auto& entry : entries) { auto& perm = perms[index++]; #if 0 /* TS3 switched the oder and YatQa as well, to keep compatibility we do it as well */ if(entry->type == permission::SQL_PERM_USER) { if(entry->channel_id > 0) { perm.id1 = entry->client_id; perm.id2 = entry->channel_id; perm.t = 4; /* client channel */ } else { perm.id1 = 0; perm.id2 = entry->client_id; perm.t = 1; /* client server */ } } else if(entry->type == permission::SQL_PERM_CHANNEL) { perm.id1 = 0; perm.id2 = entry->channel_id; perm.t = 2; /* channel permission */ } else if(entry->type == permission::SQL_PERM_GROUP) { if(entry->channel_id > 0) { perm.id1 = entry->group_id; perm.id2 = 0; perm.t = 3; /* channel group */ } else { perm.id1 = entry->group_id; perm.id2 = 0; perm.t = 0; /* server group */ } } #else if(entry->type == permission::SQL_PERM_USER) { if(entry->channel_id > 0) { perm.id1 = entry->channel_id; perm.id2 = entry->client_id; perm.t = 4; /* client channel */ } else { perm.id1 = entry->client_id; perm.id2 = 0; perm.t = 1; /* client server */ } } else if(entry->type == permission::SQL_PERM_CHANNEL) { perm.id1 = entry->channel_id; perm.id2 = 0; perm.t = 2; /* channel permission */ } else if(entry->type == permission::SQL_PERM_GROUP) { auto group_type = group_types[entry->group_id]; switch(group_type) { case 1: perm.id1 = entry->group_id; perm.id2 = 0; perm.t = 0; /* server group */ break; case 2: perm.id1 = 0; perm.id2 = entry->group_id; perm.t = 3; /* channel group */ break; case 0: default: /* unknown group */ break; } } #endif perm.p = entry->permission_type; perm.v = entry->permission_value; } perms.erase(perms.begin() + index, perms.end()); sort(perms.begin(), perms.end(), [](const CommandPerm& a, const CommandPerm& b) { if(a.t < b.t) return true; else if(b.t < a.t) return false; if(a.id1 < b.id1) return true; else if(b.id1 < a.id1) return false; if(a.id2 < b.id2) return true; else if(b.id2 < a.id2) return false; if(a.p < b.p) return true; else if(b.p < a.p) return false; return &a > &b; }); command_builder result{this->notify_response_command("notifypermfind"), 64, perms.size()}; index = 0; for(const auto& e : perms) { auto bulk = result.bulk(index++); bulk.put("t", e.t); bulk.put("p", (uint16_t) e.p); bulk.put("v", e.v); bulk.put("id1", e.id1); bulk.put("id2", e.id2); } this->sendCommand(result); return command_result{error::ok}; } /* * - Alle rechte der aktuellen server gruppen vom client * - Alle client rechte | channel cleint rechte * - Alle rechte des channels */ command_result ConnectedClient::handleCommandPermOverview(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(5); auto client_dbid = cmd["cldbid"].as(); if(!serverInstance->databaseHelper()->validClientDatabaseId(this->getServer(), client_dbid)) { return command_result{error::client_invalid_id}; } if(client_dbid == this->getClientDatabaseId()) { ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_client_permissionoverview_own, 1); } else { ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_client_permissionoverview_view, 1); } string channel_query, perm_query; auto channel = this->server ? this->server->channelTree->findChannel(cmd["cid"]) : serverInstance->getChannelTree()->findChannel(cmd["cid"]); if(!channel) { return command_result{error::channel_invalid_id}; } auto permission_manager = serverInstance->databaseHelper()->loadClientPermissionManager(this->getServerId(), client_dbid); Command result(this->getExternalType() == ClientType::CLIENT_TEAMSPEAK ? "notifypermoverview" : ""); size_t index = 0; result["cldbid"] = client_dbid; result["cid"] = channel->channelId(); if(cmd["return_code"].size() > 0) { result["return_code"] = cmd["return_code"].string(); } for(const auto& server_group : this->assignedServerGroups()) { auto permission_manager = server_group->permissions(); for(const auto& permission_data : permission_manager->permissions()) { auto& permission = std::get<1>(permission_data); if(permission.flags.value_set) { result[index]["t"] = 0; /* server group */ result[index]["id1"] = server_group->group_id(); result[index]["id2"] = 0; result[index]["p"] = std::get<0>(permission_data); result[index]["v"] = permission.values.value; result[index]["n"] = permission.flags.negate; result[index]["s"] = permission.flags.skip; index++; } if(permission.flags.grant_set) { result[index]["t"] = 0; /* server group */ result[index]["id1"] = server_group->group_id(); result[index]["id2"] = 0; result[index]["p"] = (std::get<0>(permission_data) | PERM_ID_GRANT); result[index]["v"] = permission.values.grant; result[index]["n"] = false; result[index]["s"] = false; index++; } } } { for(const auto& permission_data : permission_manager->permissions()) { auto& permission = std::get<1>(permission_data); if(permission.flags.value_set) { result[index]["t"] = 1; /* client */ result[index]["id1"] = client_dbid; result[index]["id2"] = 0; result[index]["p"] = std::get<0>(permission_data); result[index]["v"] = permission.values.value; result[index]["n"] = permission.flags.negate; result[index]["s"] = permission.flags.skip; index++; } if(permission.flags.grant_set) { result[index]["t"] = 1; /* client */ result[index]["id1"] = client_dbid; result[index]["id2"] = 0; result[index]["p"] = (std::get<0>(permission_data) | PERM_ID_GRANT); result[index]["v"] = permission.values.grant; result[index]["n"] = false; result[index]["s"] = false; index++; } } } { auto permission_manager = channel->permissions(); for(const auto& permission_data : permission_manager->permissions()) { auto& permission = std::get<1>(permission_data); if(permission.flags.value_set) { result[index]["t"] = 2; /* server channel */ result[index]["id1"] = channel->channelId(); result[index]["id2"] = 0; result[index]["p"] = std::get<0>(permission_data); result[index]["v"] = permission.values.value; result[index]["n"] = permission.flags.negate; result[index]["s"] = permission.flags.skip; index++; } if(permission.flags.grant_set) { result[index]["t"] = 2; /* server channel */ result[index]["id1"] = channel->channelId(); result[index]["id2"] = 0; result[index]["p"] = (std::get<0>(permission_data) | PERM_ID_GRANT); result[index]["v"] = permission.values.grant; result[index]["n"] = false; result[index]["s"] = false; index++; } } } auto interited_channel{channel}; auto channel_group = this->assignedChannelGroup(interited_channel); if(channel_group) { assert(interited_channel); auto permission_manager = channel_group->permissions(); for(const auto& permission_data : permission_manager->permissions()) { auto& permission = std::get<1>(permission_data); if(permission.flags.value_set) { result[index]["t"] = 3; /* channel group */ result[index]["id1"] = interited_channel->channelId(); result[index]["id2"] = channel_group->group_id(); result[index]["p"] = std::get<0>(permission_data); result[index]["v"] = permission.values.value; result[index]["n"] = permission.flags.negate; result[index]["s"] = permission.flags.skip; index++; } if(permission.flags.grant_set) { result[index]["t"] = 3; /* channel group */ result[index]["id1"] = interited_channel->channelId(); result[index]["id2"] = channel_group->group_id(); result[index]["p"] = (std::get<0>(permission_data) | PERM_ID_GRANT); result[index]["v"] = permission.values.grant; result[index]["n"] = false; result[index]["s"] = false; index++; } } } { for(const auto& permission_data : permission_manager->channel_permissions()) { auto& permission = std::get<2>(permission_data); if(permission.flags.value_set) { result[index]["t"] = 4; /* client channel */ result[index]["id1"] = std::get<1>(permission_data); result[index]["id2"] = client_dbid; result[index]["p"] = std::get<0>(permission_data); result[index]["v"] = permission.values.value; result[index]["n"] = permission.flags.negate; result[index]["s"] = permission.flags.skip; index++; } if(permission.flags.grant_set) { result[index]["t"] = 1; /* client */ result[index]["id1"] = std::get<1>(permission_data); result[index]["id2"] = client_dbid; result[index]["p"] = (std::get<0>(permission_data) | PERM_ID_GRANT); result[index]["v"] = permission.values.grant; result[index]["n"] = false; result[index]["s"] = false; index++; } } } if (index == 0) return command_result{error::database_empty_result}; this->sendCommand(result); return command_result{error::ok}; } command_result ConnectedClient::handleCommandComplainAdd(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(25); ClientDbId target = cmd["tcldbid"]; std::string msg = cmd["message"]; auto cl = this->server->findClientsByCldbId(target); if (cl.empty()) return command_result{error::client_invalid_id}; ACTION_REQUIRES_GLOBAL_PERMISSION(permission::i_client_complain_power, cl[0]->calculate_permission(permission::i_client_needed_complain_power, 0)); /* if(!serverInstance->databaseHelper()->validClientDatabaseId(target)) return command_result{error::client_invalid_id, "invalid database id"}; */ for (const auto &elm : this->server->complains->findComplainsFromTarget(target)) if (elm->invoker == this->getClientDatabaseId()) return command_result{error::database_duplicate_entry, "you already send a complain"}; if (!this->server->complains->createComplain(target, this->getClientDatabaseId(), msg)) return command_result{error::vs_critical, "could not create complains"}; return command_result{error::ok}; } command_result ConnectedClient::handleCommandComplainList(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(25); ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_client_complain_list, 1); ClientDbId id = cmd[0].has("tcldbid") ? cmd["tcldbid"].as() : 0; auto list = id == 0 ? this->server->complains->complains() : this->server->complains->findComplainsFromTarget(id); if (list.empty()) return command_result{error::database_empty_result}; deque nameQuery; for (const auto &elm : list) { if (std::find(nameQuery.begin(), nameQuery.end(), elm->invoker) == nameQuery.end()) nameQuery.push_back(elm->invoker); if (std::find(nameQuery.begin(), nameQuery.end(), elm->target) == nameQuery.end()) nameQuery.push_back(elm->target); } auto dbInfo = serverInstance->databaseHelper()->queryDatabaseInfo(this->server, nameQuery); Command result(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifycomplainlist" : ""); int index = 0; for (const auto &elm : list) { result[index]["tcldbid"] = elm->target; result[index]["tname"] = "unknown"; result[index]["fcldbid"] = elm->invoker; result[index]["fname"] = "unknown"; result[index]["message"] = elm->reason; result[index]["timestamp"] = chrono::duration_cast(elm->created.time_since_epoch()).count(); for (const auto &e : dbInfo) { if (e->client_database_id == elm->target) result[index]["tname"] = e->client_nickname; if (e->client_database_id == elm->invoker) result[index]["fname"] = e->client_nickname; } index++; } this->sendCommand(result); return command_result{error::ok}; } command_result ConnectedClient::handleCommandComplainDel(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(5); ClientDbId tid = cmd["tcldbid"]; ClientDbId fid = cmd["fcldbid"]; shared_ptr entry; for (const auto &elm : this->server->complains->findComplainsFromTarget(tid)) if (elm->invoker == fid) { entry = elm; break; } if (!entry) return command_result{error::database_empty_result}; if (entry->invoker == this->getClientDatabaseId()) ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_client_complain_delete_own, 1); else ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_client_complain_delete, 1); this->server->complains->deleteComplain(entry); return command_result{error::ok}; } command_result ConnectedClient::handleCommandComplainDelAll(Command &cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(25); ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_client_complain_delete, 1); ClientDbId tid = cmd["tcldbid"]; if (!this->server->complains->deleteComplainsFromTarget(tid)) return command_result{error::database_empty_result}; return command_result{error::ok}; } command_result ConnectedClient::handleCommandHelp(Command& cmd) { CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(5); ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_serverinstance_help_view, 1); string command = cmd[0].has("command") ? cmd["command"].as() : ""; if(command.empty()) for(const auto& key : cmd[0].keys()) { command = key; break; } if(command.empty()) command = "help"; std::transform(command.begin(), command.end(), command.begin(), ::tolower); auto file = fs::u8path("commanddocs/" + command + ".txt"); if(!fs::exists(file)) return command_result{error::file_not_found}; string line; ifstream stream(file); if(!stream) return command_result{error::file_io_error}; while(getline(stream, line)) this->sendCommand(Command{line}); return command_result{error::ok}; } command_result ConnectedClient::handleCommandPermReset(ts::Command& cmd) { CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(50); ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_virtualserver_permission_reset, 1); string token; if(!this->server->resetPermissions(token)) return command_result{error::vs_critical}; Command result(""); result["token"] = token; this->sendCommand(result); return command_result{error::ok}; } command_result ConnectedClient::handleCommandLogView(ts::Command& cmd) { CMD_CHK_AND_INC_FLOOD_POINTS(50); ServerId target_server = cmd[0].has("instance") && cmd["instance"].as() ? (ServerId) 0 : this->getServerId(); if(target_server == 0) ACTION_REQUIRES_INSTANCE_PERMISSION(permission::b_serverinstance_log_view, 1); else ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_virtualserver_log_view, 1); auto lagacy = this->getType() == CLIENT_TEAMSPEAK || cmd.hasParm("lagacy") || cmd.hasParm("legacy"); #if 0 string log_path; string server_identifier; if(target_server > 0) server_identifier = to_string(target_server); else server_identifier = "[A-Z]{0,7}"; for(const auto& sink : logger::logger(target_server)->sinks()) { if(dynamic_pointer_cast(sink)) { log_path = dynamic_pointer_cast(sink)->filename(); break; } else if(dynamic_pointer_cast(sink)) { log_path = dynamic_pointer_cast(sink)->filename(); break; } } if(log_path.empty()) return command_result{error::file_not_found, "Cant find log file (May log disabled?)"}; { //Replace " within the log path size_t index = 0; while((index = log_path.find('"', index)) != string::npos) { log_path.replace(index, 1, "\\\""); index += 2; } } string command = "cat \"" + log_path + "\""; command += " | grep -E "; command += R"("\] \[.*\]( ){0,6}?)" + server_identifier + " \\|\""; size_t beginpos = cmd[0].has("begin_pos") ? cmd["begin_pos"].as() : 0ULL; //TODO test it? size_t file_index = 0; size_t max_lines = cmd[0].has("lines") ? cmd["lines"].as() : 100ULL; //TODO bounds? deque> lines; { debugMessage(target_server, "Logview command: \"{}\"", command); array buffer{}; string line_buffer; std::shared_ptr pipe(popen(command.c_str(), "r"), pclose); if (!pipe) return command_result{error::file_io_error, "Could not execute command"}; while (!feof(pipe.get())) { auto read = fread(buffer.data(), 1, buffer.size(), pipe.get()); if(read > 0) { if(beginpos == 0 || file_index < beginpos) { if(beginpos != 0 && file_index + read > beginpos) { //We're done we just want to get the size later line_buffer += string(buffer.data(), beginpos - file_index); lines.emplace_back(file_index, line_buffer); if(lines.size() > max_lines) lines.pop_front(); //debugMessage(LOG_GENERAL, "Final line {}", line_buffer); line_buffer = ""; } else { line_buffer += string(buffer.data(), read); size_t index; size_t length; size_t cut_offset = 0; while((index = line_buffer.find("\n")) != string::npos || (index = line_buffer.find("\r")) != string::npos) { length = 0; if(index > 0) { if(line_buffer[index - 1] == '\r' || line_buffer[index - 1] == '\n') { length = 2; index--; } } if(length == 0) { if(index + 1 < line_buffer.length()) { if(line_buffer[index + 1] == '\r' || line_buffer[index + 1] == '\n') { length = 2; } } } if(length == 0) length = 1; //debugMessage(LOG_GENERAL, "Got line {}", line_buffer.substr(0, index)); lines.emplace_back(file_index + cut_offset, line_buffer.substr(0, index)); if(lines.size() > max_lines) lines.pop_front(); cut_offset += index + length; line_buffer = line_buffer.substr(index + length); } } } file_index += read; } else if(read < 0) return command_result{error::file_io_error, "fread(...) returned " + to_string(read) + " (" + to_string(errno) + ")"}; } if(!line_buffer.empty()) { lines.emplace_back(file_index - line_buffer.length(), line_buffer); if(lines.size() > max_lines) lines.pop_front(); } } //last_pos=1558 file_size=1764 l if(lines.empty()) return command_result{error::database_empty_result}; Command result(this->getExternalType() == ClientType::CLIENT_TEAMSPEAK ? "notifyserverlog" : ""); result["last_pos"] = lines.front().first; result["file_size"] = file_index; if(!(cmd.hasParm("reverse") && cmd["revers"].as())) std::reverse(lines.begin(), lines.end()); int index = 0; for(const auto& index_line : lines) { auto line = index_line.second; //2018-07-15 21:01:46.488639 //TeamSpeak format: //YYYY-MM-DD hh:mm:ss.millis|{:<8}|{:<14}|{:<3}|.... //2018-07-15 21:01:47.066367|INFO |VirtualServer |1 |listening on 0.0.0.0:9989, [::]:9989 //TeaSpeak: //[2018-07-15 23:21:47] [ERROR] Timer sql_test tick needs more than 9437 microseconds. Max allowed was 5000 microseconds. if(lagacy) { string ts = line.substr(1, 19) + ".000000|"; { string type = "unknown"; auto idx = line.find_first_of('[', 2); if(idx != string::npos) { type = line.substr(idx + 1, line.find(']', idx + 1) - idx - 1); } ts += type + " | | |" + line.substr(line.find('|') + 1); } if(ts.length() > 1024) ts = ts.substr(0, 1024) + "..."; result[index++]["l"] = ts; } else { if(line.length() > 1024) line = line.substr(0, 1024) + "..."; result[index++]["l"] = line; } } this->sendCommand(result); #else constexpr static std::array log_output{ "located at your TeaSpeak installation folder. All logs could be found there.", "If you need to lookup the TeaSpeak - Server logs, please visit the 'logs/' folder,", "", "In order to lookup the server actions use 'logquery'.", "The command 'logview' is not supported anymore." }; command_builder result{this->getExternalType() == ClientType::CLIENT_TEAMSPEAK ? "notifyserverlog" : ""}; result.put_unchecked(0, "last_pos", 0); result.put_unchecked(0, "file_size", 0); size_t index{0}; if(lagacy) { for(const auto& message : log_output) { std::string line{"2020-06-27 00:00.000" + std::to_string(index) + "|CRITICAL|Server Instance | |"}; line += message; result.put_unchecked(index++, "l", line); } } else { for(const auto& message : log_output) { std::string line{"[2020-06-27 00:00:0" + std::to_string(index) + "][ERROR] "}; line += message; result.put_unchecked(index++, "l", line); } } this->sendCommand(result); #endif return command_result{error::ok}; } command_result ConnectedClient::handleCommandLogQuery(ts::Command &cmd) { CMD_CHK_AND_INC_FLOOD_POINTS(50); uint64_t target_server = (cmd[0].has("instance") && cmd["instance"].as()) || cmd.hasParm("instance") ? (ServerId) 0 : this->getServerId(); if(target_server == 0) { ACTION_REQUIRES_INSTANCE_PERMISSION(permission::b_serverinstance_log_view, 1); } else { ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_virtualserver_log_view, 1); } std::chrono::system_clock::time_point timestamp_begin{}, timestamp_end{}; if(cmd[0].has("begin")) timestamp_begin += std::chrono::milliseconds{cmd["begin"].as()}; if(cmd[0].has("end")) timestamp_end += std::chrono::milliseconds{cmd["end"].as()}; if(timestamp_begin <= timestamp_end && timestamp_begin.time_since_epoch().count() != 0) return command_result{error::parameter_constraint_violation, "begin > end"}; size_t limit{100}; if(cmd[0].has("limit")) limit = std::min((size_t) 2000, cmd["limit"].as()); std::vector groups{}; if(cmd[0].has("groups")) { auto groups_string = cmd["groups"].string(); size_t offset{0}, findex; do { findex = groups_string.find(',', offset); auto group_name = groups_string.substr(offset, findex - offset); offset = findex + 1; size_t index{0}; for(; index < (size_t) log::LoggerGroup::MAX; index++) { auto group = static_cast(index); if(log::kLoggerGroupName[(int) group] == group_name) { if(std::find(groups.begin(), groups.end(), group) != groups.end()) return command_result{error::parameter_invalid, "groups"}; groups.push_back(group); } } if(index == (size_t) log::LoggerGroup::MAX) return command_result{error::parameter_invalid, "groups"}; } while(offset != 0); } auto result = serverInstance->action_logger()->query(groups, target_server, timestamp_begin, timestamp_end, limit); if(result.empty()) return command_result{error::database_empty_result}; command_builder notify{this->notify_response_command("notifylogquery")}; size_t index{0}; size_t threshold = this->getType() == ClientType::CLIENT_QUERY ? (size_t) -1 : 4096; /* limit each command to 4096 bytes */ for(log::LogEntryInfo& entry : result) { notify.set_bulk(index, std::move(entry.info)); if(index == 0) notify.put_unchecked(0, "sid", this->getServerId()); if(notify.current_size() > threshold) { this->sendCommand(notify); notify.reset(); index = 0; } index++; } if(index> 0) this->sendCommand(notify); if(this->getType() != ClientType::CLIENT_QUERY) this->sendCommand(command_builder{"notifylogqueryfinished"}); return command_result{error::ok}; } command_result ConnectedClient::handleCommandLogAdd(ts::Command& cmd) { CMD_CHK_AND_INC_FLOOD_POINTS(50); uint64_t target_server = cmd[0].has("instance") && cmd["instance"].as() ? (ServerId) 0 : this->getServerId(); if(target_server == 0) { ACTION_REQUIRES_INSTANCE_PERMISSION(permission::b_serverinstance_log_add, 1); } else { ACTION_REQUIRES_GLOBAL_PERMISSION(permission::b_virtualserver_log_add, 1); } serverInstance->action_logger()->custom_logger.add_log_message(target_server, this->ref(), cmd["msg"]); return command_result{error::ok}; } command_result ConnectedClient::handleCommandUpdateMyTsId(ts::Command &) { if(config::voice::suppress_myts_warnings) return command_result{error::ok}; return command_result{error::not_implemented}; } command_result ConnectedClient::handleCommandUpdateMyTsData(ts::Command &) { if(config::voice::suppress_myts_warnings) return command_result{error::ok}; return command_result{error::not_implemented}; } command_result ConnectedClient::handleCommandQueryList(ts::Command &cmd) { OptionalServerId server_id = EmptyServerId; if(this->getExternalType() == ClientType::CLIENT_TEAMSPEAK) server_id = this->getServerId(); if(cmd[0].has("server_id")) server_id = cmd["server_id"]; if(cmd[0].has("sid")) server_id = cmd["sid"]; auto server = server_id == EmptyServerId ? nullptr : serverInstance->getVoiceServerManager()->findServerById(server_id); if(!server && server_id != EmptyServerId && server_id != 0) return command_result{error::server_invalid_id}; ClientPermissionCalculator client_permissions{server, this->getClientDatabaseId(), this->getType(), 0}; auto global_list = permission::v2::permission_granted(1, client_permissions.calculate_permission(permission::b_client_query_list)); auto own_list = global_list || permission::v2::permission_granted(1, client_permissions.calculate_permission(permission::b_client_query_list_own)); if(!own_list && !global_list) return command_result{permission::b_client_query_list}; auto accounts = serverInstance->getQueryServer()->list_query_accounts(server_id); if(!global_list) { accounts.erase(remove_if(accounts.begin(), accounts.end(), [&](const std::shared_ptr& account) { return account->unique_id != this->getUid(); }), accounts.end()); } if(accounts.empty()) return command_result{error::database_empty_result}; Command result(this->getExternalType() == ClientType::CLIENT_TEAMSPEAK ? "notifyquerylist" : ""); result["server_id"] = server_id; result["flag_own"] = own_list; result["flag_all"] = global_list; size_t index = 0; for(const auto& account : accounts) { result[index]["client_unique_identifier"] = account->unique_id; result[index]["client_login_name"] = account->username; result[index]["client_bound_server"] = account->bound_server; index++; } this->sendCommand(result); return command_result{error::ok}; } command_result ConnectedClient::handleCommandQueryCreate(ts::Command &cmd) { OptionalServerId server_id = this->getServerId(); if(cmd[0].has("server_id")) server_id = cmd["server_id"]; if(cmd[0].has("sid")) server_id = cmd["sid"]; auto server = server_id == EmptyServerId ? nullptr : serverInstance->getVoiceServerManager()->findServerById(server_id); if(!server && server_id != EmptyServerId && server_id != 0) return command_result{error::server_invalid_id}; auto username = cmd["client_login_name"].as(); auto password = cmd[0].has("client_login_password") ? cmd["client_login_password"].as() : ""; auto account = serverInstance->getQueryServer()->find_query_account_by_name(username); if(account) return command_result{error::query_already_exists}; ClientPermissionCalculator client_permissions{server, this->getClientDatabaseId(), this->getType(), 0}; auto uid{this->getUid()}; if(cmd[0].has("cldbid")){ if(!serverInstance->databaseHelper()->validClientDatabaseId(server, cmd["cldbid"].as())) return command_result{error::database_empty_result}; if(!permission::v2::permission_granted(1, client_permissions.calculate_permission(permission::b_client_query_create))) { return command_result{permission::b_client_query_create}; } auto info = serverInstance->databaseHelper()->queryDatabaseInfo(server, {cmd["cldbid"].as()}); if(info.empty()) return command_result{error::database_empty_result}; uid = info[0]->client_unique_id; } else { if(!permission::v2::permission_granted(1, client_permissions.calculate_permission(permission::b_client_query_create_own))) { return command_result{permission::b_client_query_create_own}; } } if(password.empty()) password = rnd_string(QUERY_PASSWORD_LENGTH); account = serverInstance->getQueryServer()->create_query_account(username, server_id, uid, password); if(!account) return command_result{error::vs_critical}; Command result(this->getExternalType() == ClientType::CLIENT_TEAMSPEAK ? "notifyquerycreated" : ""); result["client_unique_identifier"] = account->unique_id; result["client_login_name"] = account->username; result["client_login_password"] = password; result["client_bound_server"] = account->bound_server; this->sendCommand(result); return command_result{error::ok}; } command_result ConnectedClient::handleCommandQueryDelete(ts::Command &cmd) { auto username = cmd["client_login_name"].as(); auto account = serverInstance->getQueryServer()->find_query_account_by_name(username); if(!account) return command_result{error::query_not_exists}; auto server = serverInstance->getVoiceServerManager()->findServerById(account->bound_server); /* If the server is not existing anymore, we're asking for global permissions if(!server && account->bounded_server != 0) return command_result{error::server_invalid_id}; */ ClientPermissionCalculator client_permissions{server, this->getClientDatabaseId(), this->getType(), 0}; auto delete_all = permission::v2::permission_granted(1, client_permissions.calculate_permission(permission::b_client_query_delete)); auto delete_own = delete_all || permission::v2::permission_granted(1, client_permissions.calculate_permission(permission::b_client_query_delete_own)); if(account->unique_id == this->getUid()) { if(!delete_own) { return command_result{permission::b_client_query_delete_own}; } } else { if(!delete_all) { return command_result{permission::b_client_query_delete}; } } if(account->unique_id == "serveradmin" && account->username == "serveradmin") { return command_result{error::vs_critical}; } serverInstance->getQueryServer()->delete_query_account(account); return command_result{error::ok}; } command_result ConnectedClient::handleCommandQueryRename(ts::Command &cmd) { auto username = cmd["client_login_name"].as(); auto new_username = cmd["client_new_login_name"].as(); auto account = serverInstance->getQueryServer()->find_query_account_by_name(username); if(!account) return command_result{error::query_not_exists}; auto server = serverInstance->getVoiceServerManager()->findServerById(account->bound_server); if(!server && account->bound_server != 0) return command_result{error::server_invalid_id}; ClientPermissionCalculator client_permissions{server, this->getClientDatabaseId(), this->getType(), 0}; auto rename_all = permission::v2::permission_granted(1, client_permissions.calculate_permission(permission::b_client_query_rename)); auto rename_own = rename_all || permission::v2::permission_granted(1, client_permissions.calculate_permission(permission::b_client_query_rename_own)); if(account->unique_id == this->getUid()) { if(!rename_own) { return command_result{permission::b_client_query_rename_own}; } } else { if(!rename_all) { return command_result{permission::b_client_query_rename}; } } auto target_account = serverInstance->getQueryServer()->find_query_account_by_name(new_username); if(target_account) return command_result{error::query_already_exists}; if(account->unique_id == "serveradmin" && account->username == "serveradmin") return command_result{error::parameter_invalid}; if(!serverInstance->getQueryServer()->rename_query_account(account, new_username)) return command_result{error::vs_critical}; return command_result{error::ok}; } command_result ConnectedClient::handleCommandQueryChangePassword(ts::Command &cmd) { auto username = cmd["client_login_name"].as(); auto account = serverInstance->getQueryServer()->find_query_account_by_name(username); if(!account) return command_result{error::query_not_exists}; auto server = serverInstance->getVoiceServerManager()->findServerById(account->bound_server); if(!server && account->bound_server != 0) return command_result{error::server_invalid_id}; ClientPermissionCalculator client_permissions{server, this->getClientDatabaseId(), this->getType(), 0}; auto change_all = permission::v2::permission_granted(1, client_permissions.calculate_permission(permission::b_client_query_change_password)); auto change_own = change_all || permission::v2::permission_granted(1, client_permissions.calculate_permission(permission::b_client_query_change_own_password)); auto password = cmd[0].has("client_login_password") ? cmd["client_login_password"].as() : ""; if(password.empty()) password = rnd_string(QUERY_PASSWORD_LENGTH); if(account->unique_id == this->getUid()) { if(!change_own) return command_result{permission::b_client_query_change_own_password}; } else { if(!change_all) return command_result{server ? permission::b_client_query_change_password : permission::b_client_query_change_password_global}; } if(!serverInstance->getQueryServer()->change_query_password(account, password)) return command_result{error::vs_critical}; Command result(this->getExternalType() == ClientType::CLIENT_TEAMSPEAK ? "notifyquerypasswordchanges" : ""); result["client_login_name"] = account->username; result["client_login_password"] = password; this->sendCommand(result); return command_result{error::ok}; } command_result ConnectedClient::handleCommandDummy_IpChange(ts::Command &cmd) { CMD_REF_SERVER(server); logMessage(this->getServerId(), "[{}] Address changed from {} to {}", CLIENT_STR_LOG_PREFIX, cmd["old_ip"].string(), cmd["new_ip"].string()); if(geoloc::provider_vpn && !permission::v2::permission_granted(1, this->calculate_permission(permission::b_client_ignore_vpn, 0))) { auto provider = this->isAddressV4() ? geoloc::provider_vpn->resolveInfoV4(this->getPeerIp(), true) : geoloc::provider_vpn->resolveInfoV6(this->getPeerIp(), true); if(provider) { this->disconnect(strvar::transform(ts::config::messages::kick_vpn, strvar::StringValue{"provider.name", provider->name}, strvar::StringValue{"provider.website", provider->side})); return command_result{error::ok}; } } string new_country = config::geo::countryFlag; if(geoloc::provider) { auto loc = this->isAddressV4() ? geoloc::provider->resolveInfoV4(this->getPeerIp(), false) : geoloc::provider->resolveInfoV6(this->getPeerIp(), false); if(loc) { logMessage(this->getServerId(), "[{}] Received new ip location. IP {} traced to {} ({}).", CLIENT_STR_LOG_PREFIX, this->getLoggingPeerIp(), loc->name, loc->identifier); this->properties()[property::CLIENT_COUNTRY] = loc->identifier; server->notifyClientPropertyUpdates(this->ref(), deque{property::CLIENT_COUNTRY}); new_country = loc->identifier; } else { logError(this->getServerId(), "[{}] Failed to resolve ip location for IP {}.", CLIENT_STR_LOG_PREFIX, this->getLoggingPeerIp()); } } serverInstance->databaseHelper()->updateClientIpAddress(this->getServerId(), this->getClientDatabaseId(), this->getLoggingPeerIp()); return command_result{error::ok}; } //conversationhistory cid=1 [cpw=xxx] [timestamp_begin] [timestamp_end (0 := no end)] [message_count (default 25| max 100)] [-merge] command_result ConnectedClient::handleCommandConversationHistory(ts::Command &command) { CMD_REF_SERVER(ref_server); CMD_CHK_AND_INC_FLOOD_POINTS(25); if(!command[0].has("cid") || !command[0]["cid"].castable()) return command_result{error::conversation_invalid_id}; auto conversation_id = command[0]["cid"].as(); /* test if we have access to the conversation */ if(conversation_id > 0) { /* test if we're able to see the channel */ { shared_lock channel_view_lock(this->channel_tree_mutex); auto channel = this->channel_view()->find_channel(conversation_id); if(!channel) return command_result{error::conversation_invalid_id}; auto conversation_mode = channel->channel()->properties()[property::CHANNEL_CONVERSATION_MODE].as_unchecked(); switch (conversation_mode) { case ChannelConversationMode::CHANNELCONVERSATIONMODE_PRIVATE: return command_result{error::conversation_is_private}; case ChannelConversationMode::CHANNELCONVERSATIONMODE_NONE: return command_result{error::conversation_not_exists}; case ChannelConversationMode::CHANNELCONVERSATIONMODE_PUBLIC: break; } } /* test if there is a channel password or join power which denies that we see the conversation */ { shared_lock channel_view_lock(ref_server->channel_tree_mutex); auto channel = ref_server->getChannelTree()->findChannel(conversation_id); if(!channel) /* should never happen! */ return command_result{error::conversation_invalid_id}; if (!channel->verify_password(command["cpw"].optional_string(), this->getType() != ClientType::CLIENT_QUERY)) { ACTION_REQUIRES_PERMISSION(permission::b_channel_join_ignore_password, 1, channel->channelId()); } auto error = this->calculate_and_get_join_state(channel); if(error) return command_result{error}; } } auto conversation_manager = ref_server->conversation_manager(); auto conversation = conversation_manager->get(conversation_id); if(!conversation) return command_result{error::database_empty_result}; system_clock::time_point timestamp_begin = system_clock::now(); system_clock::time_point timestamp_end; size_t message_count = 25; if(command[0].has("timestamp_begin")) timestamp_begin = system_clock::time_point{} + milliseconds(command[0]["timestamp_begin"].as()); if(command[0].has("timestamp_end")) timestamp_end = system_clock::time_point{} + milliseconds(command[0]["timestamp_end"].as()); if(command[0].has("message_count")) message_count = command[0]["message_count"].as(); if(timestamp_begin < timestamp_end) return command_result{error::parameter_invalid}; if(message_count > 100) message_count = 100; auto messages = conversation->message_history(timestamp_begin, message_count + 1, timestamp_end); /* query one more to test for more data */ if(messages.empty()) return command_result{error::database_empty_result}; bool more_data = messages.size() > message_count; if(more_data) messages.pop_back(); Command notify(this->notify_response_command("notifyconversationhistory")); size_t index = 0; size_t length = 0; bool merge = command.hasParm("merge"); for(auto it = messages.rbegin(); it != messages.rend(); it++) { if(index == 0) { notify[index]["cid"] = conversation_id; notify[index]["flag_volatile"] = conversation->volatile_only(); } auto& message = *it; notify[index]["timestamp"] = duration_cast(message->message_timestamp.time_since_epoch()).count(); notify[index]["sender_database_id"] = message->sender_database_id; notify[index]["sender_unique_id"] = message->sender_unique_id; notify[index]["sender_name"] = message->sender_name; notify[index]["msg"] = message->message; length += message->message.size(); length += message->sender_name.size(); length += message->sender_unique_id.size(); if(length > 1024 * 8 || !merge) { index = 0; this->sendCommand(notify); notify = Command{this->notify_response_command("notifyconversationhistory")}; } else index++; } if(index > 0) this->sendCommand(notify); if(more_data) return command_result{error::conversation_more_data}; return command_result{error::ok}; } command_result ConnectedClient::handleCommandConversationFetch(ts::Command &cmd) { CMD_REF_SERVER(ref_server); CMD_CHK_AND_INC_FLOOD_POINTS(25); Command result(this->notify_response_command("notifyconversationindex")); size_t result_index = 0; auto conversation_manager = ref_server->conversation_manager(); for(size_t index = 0; index < cmd.bulkCount(); index++) { auto& bulk = cmd[index]; if(!bulk.has("cid") || !bulk["cid"].castable()) continue; auto conversation_id = bulk["cid"].as(); auto& result_bulk = result[result_index++]; result_bulk["cid"] = conversation_id; /* test if we have access to the conversation */ if(conversation_id > 0) { /* test if we're able to see the channel */ { shared_lock channel_view_lock(this->channel_tree_mutex); auto channel = this->channel_view()->find_channel(conversation_id); if(!channel) { auto error = findError("conversation_invalid_id"); result_bulk["error_id"] = error.errorId; result_bulk["error_msg"] = error.message; continue; } auto conversation_mode = channel->channel()->properties()[property::CHANNEL_CONVERSATION_MODE].as_unchecked(); switch (conversation_mode) { case ChannelConversationMode::CHANNELCONVERSATIONMODE_PRIVATE: { auto error = findError("conversation_is_private"); result_bulk["error_id"] = error.errorId; result_bulk["error_msg"] = error.message; continue; } case ChannelConversationMode::CHANNELCONVERSATIONMODE_NONE: { auto error = findError("conversation_not_exists"); result_bulk["error_id"] = error.errorId; result_bulk["error_msg"] = error.message; continue; } case ChannelConversationMode::CHANNELCONVERSATIONMODE_PUBLIC: break; } } /* test if there is a channel password or join power which denies that we see the conversation */ { shared_lock channel_view_lock(ref_server->channel_tree_mutex); auto channel = ref_server->getChannelTree()->findChannel(conversation_id); if(!channel) { /* should never happen! */ auto error = findError("conversation_invalid_id"); result_bulk["error_id"] = error.errorId; result_bulk["error_msg"] = error.message; continue; } if (!channel->verify_password( bulk.has("cpw") ? std::make_optional(bulk["cpw"].string()) : std::nullopt, this->getType() != ClientType::CLIENT_QUERY)) { if(!permission::v2::permission_granted(1, this->calculate_permission(permission::b_channel_join_ignore_password, 1, channel->channelId()))) { auto error = findError("channel_invalid_password"); result_bulk["error_id"] = error.errorId; result_bulk["error_msg"] = error.message; continue; } } if(auto error_perm = this->calculate_and_get_join_state(channel); error_perm) { auto error = findError("server_insufficeient_permissions"); result_bulk["error_id"] = error.errorId; result_bulk["error_msg"] = error.message; result_bulk["failed_permid"] = (int) error_perm; continue; } } } auto conversation = conversation_manager->get(conversation_id); if(conversation) { result_bulk["timestamp"] = duration_cast(conversation->last_message().time_since_epoch()).count(); result_bulk["flag_volatile"] = conversation->volatile_only(); } else { result_bulk["timestamp"] = 0; result_bulk["flag_volatile"] = false; } } if(result_index == 0) return command_result{error::database_empty_result}; this->sendCommand(result); return command_result{error::ok}; } command_result ConnectedClient::handleCommandConversationMessageDelete(ts::Command &cmd) { CMD_REF_SERVER(ref_server); CMD_CHK_AND_INC_FLOOD_POINTS(25); auto conversation_manager = ref_server->conversation_manager(); std::shared_ptr current_conversation; ChannelId current_conversation_id = 0; for(size_t index = 0; index < cmd.bulkCount(); index++) { auto &bulk = cmd[index]; if(!bulk.has("cid") || !bulk["cid"].castable()) continue; /* test if we have access to the conversation */ if(current_conversation_id != bulk["cid"].as()) { current_conversation_id = bulk["cid"].as(); /* test if we're able to see the channel */ { shared_lock channel_view_lock(this->channel_tree_mutex); auto channel = this->channel_view()->find_channel(current_conversation_id); if(!channel) return command_result{error::conversation_invalid_id}; } /* test if there is a channel password or join power which denies that we see the conversation */ { shared_lock channel_view_lock(ref_server->channel_tree_mutex); auto channel = ref_server->getChannelTree()->findChannel(current_conversation_id); if(!channel) return command_result{error::conversation_invalid_id}; if (!channel->verify_password( bulk.has("cpw") ? std::make_optional(bulk["cpw"].string()) : std::nullopt, this->getType() != ClientType::CLIENT_QUERY)) { ACTION_REQUIRES_PERMISSION(permission::b_channel_join_ignore_password, 1, channel->channelId()); } if (!permission::v2::permission_granted(1, this->calculate_permission(permission::b_channel_conversation_message_delete, channel->channelId()))) return command_result{permission::b_channel_conversation_message_delete}; if(auto error_perm = this->calculate_and_get_join_state(channel); error_perm != permission::ok && error_perm != permission::b_client_is_sticky) return command_result{error_perm}; } } current_conversation = conversation_manager->get(current_conversation_id); if(!current_conversation) continue; auto timestamp_begin = system_clock::time_point{} + milliseconds{bulk["timestamp_begin"]}; auto timestamp_end = system_clock::time_point{} + milliseconds{bulk.has("timestamp_end") ? bulk["timestamp_end"].as() : 0}; auto limit = bulk.has("limit") ? bulk["limit"].as() : 1; if(limit > 100) limit = 100; auto delete_count = current_conversation->delete_messages(timestamp_end, limit, timestamp_begin, bulk["cldbid"]); if(delete_count > 0) { for(const auto& client : ref_server->getClients()) { if(client->connectionState() != ConnectionState::CONNECTED) continue; auto type = client->getType(); if(type == ClientType::CLIENT_INTERNAL || type == ClientType::CLIENT_MUSIC) continue; client->notifyConversationMessageDelete(current_conversation_id, timestamp_begin, timestamp_end, bulk["cldbid"], delete_count); } } } return command_result{error::ok}; } enum struct FeatureSupportMode { NONE, FULL, EXPERIMENTAL, DEPRECATED }; #define REGISTER_FEATURE(name, support, version) \ notify.put_unchecked(index, "name", name); \ notify.put_unchecked(index, "support", (int) support); \ notify.put_unchecked(index, "version", version); \ index++ command_result ConnectedClient::handleCommandListFeatureSupport(ts::Command &cmd) { ts::command_builder notify{this->notify_response_command("notifyfeaturesupport")}; int index{0}; REGISTER_FEATURE("error-bulks", FeatureSupportMode::FULL, 1); REGISTER_FEATURE("advanced-channel-chat", FeatureSupportMode::FULL, 1); REGISTER_FEATURE("log-query", FeatureSupportMode::FULL, 1); REGISTER_FEATURE("whisper-echo", FeatureSupportMode::FULL, 1); REGISTER_FEATURE("video", FeatureSupportMode::EXPERIMENTAL, 1); REGISTER_FEATURE("sidebar-mode", FeatureSupportMode::FULL, 1); REGISTER_FEATURE("token", FeatureSupportMode::FULL, 1); this->sendCommand(notify); return command_result{error::ok}; }