From 5715acbcb2a5b62b1157caef70a268398dfab867 Mon Sep 17 00:00:00 2001 From: WolverinDEV Date: Wed, 17 Jul 2019 19:37:18 +0200 Subject: [PATCH] Reinitialized project to reduce the git branch size --- .git-old | 1 + .gitignore | 28 + .gitmodules | 4 - CMakeLists.txt | 68 + MusicBot/CMakeLists.txt | 21 + MusicBot/MavStuff.cpp | 198 + MusicBot/Protocol.md | 249 + MusicBot/main.cpp | 210 + MusicBot/src/MusicPlayer.cpp | 115 + client/CMakeLists.txt | 79 + client/HideString.h | 29 + client/changes.md | 27 + client/identityHash.cpp | 63 + client/main.cpp | 1176 +++ client/proto/LicenseKey.proto | 17 + client/src/Identity.cpp | 218 + client/src/Identity.h | 44 + client/src/MultithreadedIdentity.cpp | 189 + client/src/TinySHA1.hpp.h | 196 + client/src/protocol/Connection.cpp | 454 ++ client/src/protocol/Connection.h | 139 + client/src/protocol/ConnectionHandschake.cpp | 361 + .../src/protocol/ConnectionPacketHandler.cpp | 158 + client/src/protocol/HandshakeNew.cpp | 154 + .../src/protocol/socket/FilteredUDPSocket.cpp | 115 + .../src/protocol/socket/FilteredUDPSocket.h | 27 + client/src/protocol/socket/RawUDPSocket.cpp | 151 + client/src/protocol/socket/RawUDPSocket.h | 28 + cmake/Modules/FindGLIB.cmake | 122 + cmake/Modules/FindLibNice.cmake | 34 + cmake/Modules/FindSpdlog.cmake | 8 + cmake/Modules/FindUsrSCTP.cmake | 24 + flooder/CMakeLists.txt | 25 + flooder/main.cpp | 81 + flooder/src/PorxiedClientSock5.cpp | 111 + flooder/src/ProxiedClient.cpp | 175 + flooder/src/ProxiedClient.h | 72 + flooder/src/TSClient.cpp | 108 + flooder/src/TSClient.h | 36 + license/CMakeLists.txt | 162 + license/LicenseClientMain.cpp | 87 + license/LicenseCreatorCLI.cpp | 138 + license/LicenseManager.cpp | 97 + license/LicenseServerMain.cpp | 240 + license/manager/ServerConnection.cpp | 225 + license/manager/ServerConnection.h | 113 + license/manager/ServerConnectionExecutor.cpp | 93 + license/manager/ServerConnectionHandler.cpp | 180 + license/manager/qtHelper.h | 24 + license/manager/ui/LicenseGenerator.cpp | 193 + license/manager/ui/LicenseGenerator.h | 26 + license/manager/ui/LoginWindow.cpp | 68 + license/manager/ui/LoginWindow.h | 24 + license/manager/ui/Overview.cpp | 173 + license/manager/ui/Overview.h | 69 + license/manager/ui/UiLicenseInfo.cpp | 112 + license/manager/ui/UiLicenseInfo.h | 29 + license/manager/ui/licensegenerator.ui | 351 + license/manager/ui/licenseinfo.ui | 242 + license/manager/ui/loginwindow.ui | 92 + license/manager/ui/owerview.ui | 151 + license/packets/LicenseManager.proto | 54 + license/packets/LicenseRequest.proto | 72 + license/server/KeyIdCache.cpp | 67 + license/server/LicenseManager.cpp | 463 ++ license/server/LicenseManager.h | 80 + license/server/LicenseServer.cpp | 374 + license/server/LicenseServer.h | 115 + license/server/LicenseServerHandler.cpp | 338 + license/server/StatisticManager.cpp | 201 + license/server/StatisticManager.h | 61 + license/server/UserManager.cpp | 64 + license/server/UserManager.h | 68 + license/server/WebAPI.cpp | 562 ++ license/server/WebAPI.h | 106 + license/shared/License.cpp | 102 + license/shared/License.h | 186 + license/shared/LicenseRequest.cpp | 301 + license/shared/LicenseRequest.h | 129 + license/shared/LicenseRequestHandler.cpp | 105 + license/shared/crypt.h | 6 + server/CMakeLists.txt | 270 + server/CreateEnviroment.txt | 5 + server/VersionHelper.sh | 13 + server/environment/geoloc | 1 + server/environment/providers | 1 + server/environment/resources | 1 + server/flood.sh | 20 + server/gui/mainwindow.ui | 80 + server/gui/resources/Smoke.png | Bin 0 -> 8817 bytes server/helpers/channel_groups | 9 + server/helpers/permgen.cpp | 267 + server/helpers/server_groups | 15 + server/helpers/server_groups_new | 15 + server/lock_concept | 58 + server/main.cpp | 434 + server/repro/build.sh | 25 + server/repro/build_private_key | 27 + server/repro/build_public_key | 1 + server/repro/deploy_build.sh | 53 + server/repro/env/TeaSpeakServer | 1 + server/repro/env/certs | 1 + server/repro/env/commanddocs | 1 + server/repro/env/geoloc | 1 + server/repro/env/install_libnice.sh | 1 + server/repro/env/install_music.sh | 1 + server/repro/env/libs/libDataPipes.so | 1 + server/repro/env/libs/libTeaMusic.so | 1 + server/repro/env/libs/libcrypto.so | 1 + server/repro/env/libs/libjemalloc.so.2 | 1 + server/repro/env/libs/libmysqlcppconn.so.7 | 1 + server/repro/env/libs/libopus.so.0 | 1 + server/repro/env/libs/libsqlite3.so.0 | 1 + server/repro/env/libs/libssl.so | 1 + server/repro/env/providers | 1 + server/repro/env/resources | 1 + server/repro/env/tealoop.sh | 1 + server/repro/env/teastart.sh | 1 + server/repro/env/teastart_autorestart.sh | 1 + server/repro/env/teastart_minimal.sh | 1 + server/repro/generate_version.sh | 48 + server/repro/make_symbol.sh | 35 + server/repro/package_server.sh | 30 + server/repro/test_docker.sh | 33 + server/repro/test_release.sh | 21 + server/src/Configuration.cpp | 1635 ++++ server/src/Configuration.h | 218 + server/src/ConnectionStatistics.cpp | 326 + server/src/ConnectionStatistics.h | 106 + server/src/DatabaseHelper.cpp | 1361 ++++ server/src/DatabaseHelper.h | 131 + server/src/Group.cpp | 942 +++ server/src/Group.h | 226 + server/src/InstanceHandler.cpp | 662 ++ server/src/InstanceHandler.h | 116 + server/src/InstanceHandlerSetup.cpp | 146 + server/src/ServerManager.cpp | 402 + server/src/ServerManager.h | 99 + server/src/ServerManagerSnapshot.cpp | 12 + server/src/ServerManagerSnapshotDeploy.cpp | 1223 +++ server/src/ShutdownHelper.cpp | 130 + server/src/ShutdownHelper.h | 24 + server/src/SignalHandler.cpp | 101 + server/src/SignalHandler.h | 12 + server/src/TS3ServerClientManager.cpp | 540 ++ server/src/TS3ServerHeartbeat.cpp | 271 + server/src/TSServer.cpp | 1172 +++ server/src/TSServer.h | 333 + server/src/build.cpp | 72 + server/src/build.h | 63 + server/src/channel/ClientChannelView.cpp | 439 + server/src/channel/ClientChannelView.h | 111 + server/src/channel/ServerChannel.cpp | 552 ++ server/src/channel/ServerChannel.h | 69 + server/src/client/ConnectedClient.cpp | 941 +++ server/src/client/ConnectedClient.h | 626 ++ .../client/ConnectedClientCommandHandler.cpp | 7220 +++++++++++++++++ .../client/ConnectedClientNotifyHandler.cpp | 711 ++ .../ConnectedClientTextCommandHandler.cpp | 678 ++ server/src/client/DataClient.cpp | 252 + server/src/client/DataClient.h | 145 + server/src/client/InternalClient.cpp | 44 + server/src/client/InternalClient.h | 24 + server/src/client/SpeakingClient.cpp | 792 ++ server/src/client/SpeakingClient.h | 97 + server/src/client/SpeakingClientHandshake.cpp | 122 + server/src/client/file/FileClient.cpp | 716 ++ server/src/client/file/FileClient.h | 133 + server/src/client/file/FileClientIO.cpp | 248 + server/src/client/music/MusicClient.cpp | 230 + server/src/client/music/MusicClient.h | 150 + server/src/client/music/MusicClientPlayer.cpp | 455 ++ server/src/client/music/MusicQueue.cpp | 131 + server/src/client/music/MusicQueue.h | 61 + server/src/client/music/Song.cpp | 61 + server/src/client/music/Song.h | 94 + .../channel_replay/ChannelProvider.cpp | 179 + .../channel_replay/ChannelProvider.h | 30 + server/src/client/query/QueryClient.cpp | 525 ++ server/src/client/query/QueryClient.h | 185 + .../src/client/query/QueryClientCommands.cpp | 926 +++ server/src/client/query/QueryClientNotify.cpp | 227 + server/src/client/query/XMacroEventTypes.h | 60 + .../src/client/voice/PrecomputedPuzzles.cpp | 88 + server/src/client/voice/PrecomputedPuzzles.h | 44 + server/src/client/voice/VoiceClient.cpp | 294 + server/src/client/voice/VoiceClient.h | 132 + .../voice/VoiceClientCommandHandler.cpp | 81 + .../client/voice/VoiceClientConnection.cpp | 693 ++ .../src/client/voice/VoiceClientConnection.h | 110 + .../client/voice/VoiceClientHandschake.cpp | 179 + .../client/voice/VoiceClientPacketHandler.cpp | 86 + server/src/client/voice/VoiceClientView.cpp | 26 + server/src/client/web/SampleHandler.cpp | 71 + server/src/client/web/SampleHandler.h | 42 + server/src/client/web/VoiceBridge.cpp | 236 + server/src/client/web/VoiceBridge.h | 61 + server/src/client/web/WSWebClient.cpp | 155 + server/src/client/web/WebClient.cpp | 605 ++ server/src/client/web/WebClient.h | 106 + server/src/geo/GeoLocation.cpp | 95 + server/src/geo/GeoLocation.h | 194 + server/src/geo/IP2Location.cpp | 65 + server/src/geo/VPNBlocker.cpp | 33 + server/src/lincense/LicenseHelper.cpp | 197 + server/src/lincense/LicenseHelper.h | 39 + server/src/lincense/TeamSpeakLicense.cpp | 153 + server/src/lincense/TeamSpeakLicense.h | 25 + server/src/manager/BanManager.cpp | 315 + server/src/manager/BanManager.h | 109 + server/src/manager/ComplainManager.cpp | 111 + server/src/manager/ComplainManager.h | 43 + server/src/manager/IpListManager.cpp | 87 + server/src/manager/IpListManager.h | 29 + server/src/manager/LetterManager.cpp | 87 + server/src/manager/LetterManager.h | 44 + server/src/manager/SqlDataManager.cpp | 408 + server/src/manager/SqlDataManager.h | 26 + server/src/manager/TokeManager.cpp | 105 + server/src/manager/TokeManager.h | 47 + server/src/music/MusicBotManager.cpp | 383 + server/src/music/MusicBotManager.h | 75 + server/src/music/MusicPlaylist.cpp | 589 ++ server/src/music/MusicPlaylist.h | 155 + server/src/music/PlayablePlaylist.cpp | 155 + server/src/music/PlayablePlaylist.h | 49 + .../pinteraction/ApplicationInteraction.cpp | 101 + .../src/pinteraction/ApplicationInteraction.h | 43 + server/src/server/POWHandler.cpp | 339 + server/src/server/POWHandler.h | 69 + server/src/server/QueryServer.cpp | 383 + server/src/server/QueryServer.h | 115 + server/src/server/VoiceIOManager.cpp | 330 + server/src/server/VoiceIOManager.h | 238 + server/src/server/VoiceServer.cpp | 586 ++ server/src/server/VoiceServer.h | 94 + server/src/server/WebIoManager.cpp | 53 + server/src/server/WebIoManager.h | 31 + server/src/server/WebServer.cpp | 123 + server/src/server/WebServer.h | 45 + server/src/server/file/FileServer.cpp | 618 ++ server/src/server/file/FileServer.h | 153 + server/src/terminal/CommandHandler.cpp | 461 ++ server/src/terminal/CommandHandler.h | 37 + server/src/weblist/TeamSpeakWebClient.cpp | 301 + server/src/weblist/TeamSpeakWebClient.h | 72 + server/src/weblist/WebListManager.cpp | 134 + server/src/weblist/WebListManager.h | 48 + server/thread_concept | 17 + server/todo.md | 79 + server/tomcryptTest.cpp | 24 + server/valrgid_supress.txt | 114 + 252 files changed, 51928 insertions(+), 4 deletions(-) create mode 100644 .git-old create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 MusicBot/CMakeLists.txt create mode 100644 MusicBot/MavStuff.cpp create mode 100644 MusicBot/Protocol.md create mode 100644 MusicBot/main.cpp create mode 100644 MusicBot/src/MusicPlayer.cpp create mode 100644 client/CMakeLists.txt create mode 100644 client/HideString.h create mode 100644 client/changes.md create mode 100644 client/identityHash.cpp create mode 100644 client/main.cpp create mode 100644 client/proto/LicenseKey.proto create mode 100644 client/src/Identity.cpp create mode 100644 client/src/Identity.h create mode 100644 client/src/MultithreadedIdentity.cpp create mode 100644 client/src/TinySHA1.hpp.h create mode 100644 client/src/protocol/Connection.cpp create mode 100644 client/src/protocol/Connection.h create mode 100644 client/src/protocol/ConnectionHandschake.cpp create mode 100644 client/src/protocol/ConnectionPacketHandler.cpp create mode 100644 client/src/protocol/HandshakeNew.cpp create mode 100644 client/src/protocol/socket/FilteredUDPSocket.cpp create mode 100644 client/src/protocol/socket/FilteredUDPSocket.h create mode 100644 client/src/protocol/socket/RawUDPSocket.cpp create mode 100644 client/src/protocol/socket/RawUDPSocket.h create mode 100644 cmake/Modules/FindGLIB.cmake create mode 100644 cmake/Modules/FindLibNice.cmake create mode 100644 cmake/Modules/FindSpdlog.cmake create mode 100644 cmake/Modules/FindUsrSCTP.cmake create mode 100644 flooder/CMakeLists.txt create mode 100644 flooder/main.cpp create mode 100644 flooder/src/PorxiedClientSock5.cpp create mode 100644 flooder/src/ProxiedClient.cpp create mode 100644 flooder/src/ProxiedClient.h create mode 100644 flooder/src/TSClient.cpp create mode 100644 flooder/src/TSClient.h create mode 100644 license/CMakeLists.txt create mode 100644 license/LicenseClientMain.cpp create mode 100644 license/LicenseCreatorCLI.cpp create mode 100644 license/LicenseManager.cpp create mode 100644 license/LicenseServerMain.cpp create mode 100644 license/manager/ServerConnection.cpp create mode 100644 license/manager/ServerConnection.h create mode 100644 license/manager/ServerConnectionExecutor.cpp create mode 100644 license/manager/ServerConnectionHandler.cpp create mode 100644 license/manager/qtHelper.h create mode 100644 license/manager/ui/LicenseGenerator.cpp create mode 100644 license/manager/ui/LicenseGenerator.h create mode 100644 license/manager/ui/LoginWindow.cpp create mode 100644 license/manager/ui/LoginWindow.h create mode 100644 license/manager/ui/Overview.cpp create mode 100644 license/manager/ui/Overview.h create mode 100644 license/manager/ui/UiLicenseInfo.cpp create mode 100644 license/manager/ui/UiLicenseInfo.h create mode 100644 license/manager/ui/licensegenerator.ui create mode 100644 license/manager/ui/licenseinfo.ui create mode 100644 license/manager/ui/loginwindow.ui create mode 100644 license/manager/ui/owerview.ui create mode 100644 license/packets/LicenseManager.proto create mode 100644 license/packets/LicenseRequest.proto create mode 100644 license/server/KeyIdCache.cpp create mode 100644 license/server/LicenseManager.cpp create mode 100644 license/server/LicenseManager.h create mode 100644 license/server/LicenseServer.cpp create mode 100644 license/server/LicenseServer.h create mode 100644 license/server/LicenseServerHandler.cpp create mode 100644 license/server/StatisticManager.cpp create mode 100644 license/server/StatisticManager.h create mode 100644 license/server/UserManager.cpp create mode 100644 license/server/UserManager.h create mode 100644 license/server/WebAPI.cpp create mode 100644 license/server/WebAPI.h create mode 100644 license/shared/License.cpp create mode 100644 license/shared/License.h create mode 100644 license/shared/LicenseRequest.cpp create mode 100644 license/shared/LicenseRequest.h create mode 100644 license/shared/LicenseRequestHandler.cpp create mode 100644 license/shared/crypt.h create mode 100644 server/CMakeLists.txt create mode 100644 server/CreateEnviroment.txt create mode 100644 server/VersionHelper.sh create mode 120000 server/environment/geoloc create mode 120000 server/environment/providers create mode 120000 server/environment/resources create mode 100755 server/flood.sh create mode 100644 server/gui/mainwindow.ui create mode 100644 server/gui/resources/Smoke.png create mode 100644 server/helpers/channel_groups create mode 100644 server/helpers/permgen.cpp create mode 100644 server/helpers/server_groups create mode 100644 server/helpers/server_groups_new create mode 100644 server/lock_concept create mode 100644 server/main.cpp create mode 100755 server/repro/build.sh create mode 100644 server/repro/build_private_key create mode 100644 server/repro/build_public_key create mode 100755 server/repro/deploy_build.sh create mode 120000 server/repro/env/TeaSpeakServer create mode 120000 server/repro/env/certs create mode 120000 server/repro/env/commanddocs create mode 120000 server/repro/env/geoloc create mode 120000 server/repro/env/install_libnice.sh create mode 120000 server/repro/env/install_music.sh create mode 120000 server/repro/env/libs/libDataPipes.so create mode 120000 server/repro/env/libs/libTeaMusic.so create mode 120000 server/repro/env/libs/libcrypto.so create mode 120000 server/repro/env/libs/libjemalloc.so.2 create mode 120000 server/repro/env/libs/libmysqlcppconn.so.7 create mode 120000 server/repro/env/libs/libopus.so.0 create mode 120000 server/repro/env/libs/libsqlite3.so.0 create mode 120000 server/repro/env/libs/libssl.so create mode 120000 server/repro/env/providers create mode 120000 server/repro/env/resources create mode 120000 server/repro/env/tealoop.sh create mode 120000 server/repro/env/teastart.sh create mode 120000 server/repro/env/teastart_autorestart.sh create mode 120000 server/repro/env/teastart_minimal.sh create mode 100755 server/repro/generate_version.sh create mode 100755 server/repro/make_symbol.sh create mode 100755 server/repro/package_server.sh create mode 100755 server/repro/test_docker.sh create mode 100755 server/repro/test_release.sh create mode 100644 server/src/Configuration.cpp create mode 100644 server/src/Configuration.h create mode 100644 server/src/ConnectionStatistics.cpp create mode 100644 server/src/ConnectionStatistics.h create mode 100644 server/src/DatabaseHelper.cpp create mode 100644 server/src/DatabaseHelper.h create mode 100644 server/src/Group.cpp create mode 100644 server/src/Group.h create mode 100644 server/src/InstanceHandler.cpp create mode 100644 server/src/InstanceHandler.h create mode 100644 server/src/InstanceHandlerSetup.cpp create mode 100644 server/src/ServerManager.cpp create mode 100644 server/src/ServerManager.h create mode 100644 server/src/ServerManagerSnapshot.cpp create mode 100644 server/src/ServerManagerSnapshotDeploy.cpp create mode 100644 server/src/ShutdownHelper.cpp create mode 100644 server/src/ShutdownHelper.h create mode 100644 server/src/SignalHandler.cpp create mode 100644 server/src/SignalHandler.h create mode 100644 server/src/TS3ServerClientManager.cpp create mode 100644 server/src/TS3ServerHeartbeat.cpp create mode 100644 server/src/TSServer.cpp create mode 100644 server/src/TSServer.h create mode 100644 server/src/build.cpp create mode 100644 server/src/build.h create mode 100644 server/src/channel/ClientChannelView.cpp create mode 100644 server/src/channel/ClientChannelView.h create mode 100644 server/src/channel/ServerChannel.cpp create mode 100644 server/src/channel/ServerChannel.h create mode 100644 server/src/client/ConnectedClient.cpp create mode 100644 server/src/client/ConnectedClient.h create mode 100644 server/src/client/ConnectedClientCommandHandler.cpp create mode 100644 server/src/client/ConnectedClientNotifyHandler.cpp create mode 100644 server/src/client/ConnectedClientTextCommandHandler.cpp create mode 100644 server/src/client/DataClient.cpp create mode 100644 server/src/client/DataClient.h create mode 100644 server/src/client/InternalClient.cpp create mode 100644 server/src/client/InternalClient.h create mode 100644 server/src/client/SpeakingClient.cpp create mode 100644 server/src/client/SpeakingClient.h create mode 100644 server/src/client/SpeakingClientHandshake.cpp create mode 100644 server/src/client/file/FileClient.cpp create mode 100644 server/src/client/file/FileClient.h create mode 100644 server/src/client/file/FileClientIO.cpp create mode 100644 server/src/client/music/MusicClient.cpp create mode 100644 server/src/client/music/MusicClient.h create mode 100644 server/src/client/music/MusicClientPlayer.cpp create mode 100644 server/src/client/music/MusicQueue.cpp create mode 100644 server/src/client/music/MusicQueue.h create mode 100644 server/src/client/music/Song.cpp create mode 100644 server/src/client/music/Song.h create mode 100644 server/src/client/music/internal_provider/channel_replay/ChannelProvider.cpp create mode 100644 server/src/client/music/internal_provider/channel_replay/ChannelProvider.h create mode 100644 server/src/client/query/QueryClient.cpp create mode 100644 server/src/client/query/QueryClient.h create mode 100644 server/src/client/query/QueryClientCommands.cpp create mode 100644 server/src/client/query/QueryClientNotify.cpp create mode 100644 server/src/client/query/XMacroEventTypes.h create mode 100644 server/src/client/voice/PrecomputedPuzzles.cpp create mode 100644 server/src/client/voice/PrecomputedPuzzles.h create mode 100644 server/src/client/voice/VoiceClient.cpp create mode 100644 server/src/client/voice/VoiceClient.h create mode 100644 server/src/client/voice/VoiceClientCommandHandler.cpp create mode 100644 server/src/client/voice/VoiceClientConnection.cpp create mode 100644 server/src/client/voice/VoiceClientConnection.h create mode 100644 server/src/client/voice/VoiceClientHandschake.cpp create mode 100644 server/src/client/voice/VoiceClientPacketHandler.cpp create mode 100644 server/src/client/voice/VoiceClientView.cpp create mode 100644 server/src/client/web/SampleHandler.cpp create mode 100644 server/src/client/web/SampleHandler.h create mode 100644 server/src/client/web/VoiceBridge.cpp create mode 100644 server/src/client/web/VoiceBridge.h create mode 100644 server/src/client/web/WSWebClient.cpp create mode 100644 server/src/client/web/WebClient.cpp create mode 100644 server/src/client/web/WebClient.h create mode 100644 server/src/geo/GeoLocation.cpp create mode 100644 server/src/geo/GeoLocation.h create mode 100644 server/src/geo/IP2Location.cpp create mode 100644 server/src/geo/VPNBlocker.cpp create mode 100644 server/src/lincense/LicenseHelper.cpp create mode 100644 server/src/lincense/LicenseHelper.h create mode 100644 server/src/lincense/TeamSpeakLicense.cpp create mode 100644 server/src/lincense/TeamSpeakLicense.h create mode 100644 server/src/manager/BanManager.cpp create mode 100644 server/src/manager/BanManager.h create mode 100644 server/src/manager/ComplainManager.cpp create mode 100644 server/src/manager/ComplainManager.h create mode 100644 server/src/manager/IpListManager.cpp create mode 100644 server/src/manager/IpListManager.h create mode 100644 server/src/manager/LetterManager.cpp create mode 100644 server/src/manager/LetterManager.h create mode 100644 server/src/manager/SqlDataManager.cpp create mode 100644 server/src/manager/SqlDataManager.h create mode 100644 server/src/manager/TokeManager.cpp create mode 100644 server/src/manager/TokeManager.h create mode 100644 server/src/music/MusicBotManager.cpp create mode 100644 server/src/music/MusicBotManager.h create mode 100644 server/src/music/MusicPlaylist.cpp create mode 100644 server/src/music/MusicPlaylist.h create mode 100644 server/src/music/PlayablePlaylist.cpp create mode 100644 server/src/music/PlayablePlaylist.h create mode 100644 server/src/pinteraction/ApplicationInteraction.cpp create mode 100644 server/src/pinteraction/ApplicationInteraction.h create mode 100644 server/src/server/POWHandler.cpp create mode 100644 server/src/server/POWHandler.h create mode 100644 server/src/server/QueryServer.cpp create mode 100644 server/src/server/QueryServer.h create mode 100644 server/src/server/VoiceIOManager.cpp create mode 100644 server/src/server/VoiceIOManager.h create mode 100644 server/src/server/VoiceServer.cpp create mode 100644 server/src/server/VoiceServer.h create mode 100644 server/src/server/WebIoManager.cpp create mode 100644 server/src/server/WebIoManager.h create mode 100644 server/src/server/WebServer.cpp create mode 100644 server/src/server/WebServer.h create mode 100644 server/src/server/file/FileServer.cpp create mode 100644 server/src/server/file/FileServer.h create mode 100644 server/src/terminal/CommandHandler.cpp create mode 100644 server/src/terminal/CommandHandler.h create mode 100644 server/src/weblist/TeamSpeakWebClient.cpp create mode 100644 server/src/weblist/TeamSpeakWebClient.h create mode 100644 server/src/weblist/WebListManager.cpp create mode 100644 server/src/weblist/WebListManager.h create mode 100644 server/thread_concept create mode 100644 server/todo.md create mode 100644 server/tomcryptTest.cpp create mode 100644 server/valrgid_supress.txt diff --git a/.git-old b/.git-old new file mode 100644 index 0000000..93cf67c --- /dev/null +++ b/.git-old @@ -0,0 +1 @@ +gitdir: ../.git/modules/TeaSpeak diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..724f615 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Created by .ignore support plugin (hsz.mobi) +**/.idea +**/cmake-build-* +**/build/ +server/environment/* +!server/environment/geoloc +!server/environment/providers +!server/environment/resources +#The remote repro +server/repro/test +MusicBot/enviroment/ +MusicBot/libs +MusicBot/*.opus +server/buildData.txt +server/repro/env/buildVersion.txt +server/repro/TeaSpeak-* +server/repro/unstripped/ +server/repro/build_version.txt +server/gui/ui/ +server/repro/crashes/ +server/repro/symbols/ +license/src/ +server/tmp.txt + +license/environment/**/* +server/src/web/ + +.git-old/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index ad4e5c0..2dc4e7f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,3 @@ -[submodule "TeaSpeak - Music"] - path = music - url = https://github.com/TeaSpeak/TeaMusic-Providers.git - branch = master [submodule "git-teaspeak"] path = git-teaspeak url = https://github.com/TeaSpeak/TeaSpeak.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..f285d78 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,68 @@ +cmake_minimum_required(VERSION 3.6) +#project(TeamSpeak) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17") +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${PROJECT_SOURCE_DIR}/cmake/Modules") +set(TEASPEAK_SERVER ON) + +include_directories(/usr/local/mysql/connector-c++-8.0/include/jdbc/) +#end now +#set(MEMORY_DEBUG_FLAGS " -fsanitize=leak -fsanitize=address -fstack-protector-all ") +#set(MEMORY_DEBUG_FLAGS "-fsanitize=address") + +set(LIBRARY_PATH "${CMAKE_SOURCE_DIR}/../libraries/") +set(CMAKE_PREFIX_PATH "/home/wolverindev/clib/qt/5.6.1/5.6/gcc_64/lib/cmake") +set(LIBEVENT_PATH "${LIBRARY_PATH}/event/build/lib/") + +function(resolve_library VARIABLE FALLBACK PATHS) + set( _PATHS ${PATHS} ${ARGN} ) # Merge them together + + foreach(PATH IN ITEMS ${_PATHS}) + message(STATUS "Try to use path ${PATH} for ${VARIABLE}") + if(EXISTS ${PATH}) + message(STATUS "Setting ${VARIABLE} to ${PATH}") + set(${VARIABLE} ${PATH} PARENT_SCOPE) + return() + endif() + endforeach() + + if(FALLBACK) + message(WARNING "Failed to resolve library path for ${VARIABLE}. Using default ${VARIABLE}") + else() + message(FATAL_ERROR "Failed to find requited library. Variable: ${VARIABLE} Paths: ${_PATHS}") + endif() +endfunction() + +resolve_library(LIBRARY_TOM_MATH OFF "${LIBRARY_PATH}/tommath/build/libtommathStatic.a") +resolve_library(LIBRARY_TOM_CRYPT OFF "${LIBRARY_PATH}/tomcrypt/libtomcrypt.a") +resolve_library(LIBRARY_PATH_BREAKPAD OFF "${LIBRARY_PATH}/breakpad/build/src/client/linux/libbreakpad_client.a") +resolve_library(LIBRARY_PATH_PROTOBUF OFF "${LIBRARY_PATH}/protobuf/build/libprotobuf.a") +resolve_library(LIBRARY_PATH_JDBC OFF "${LIBRARY_PATH}/mysqlconnector/build/jdbc/driver/libmysqlcppconn.so.7" "${LIBRARY_PATH}/mysqlconnector/build/jdbc/libmysqlcppconn.so.7") +resolve_library(LIBRARY_PATH_BORINGSSL_SSL OFF "${LIBRARY_PATH}/boringssl/build/ssl/libssl.so") +resolve_library(LIBRARY_PATH_BORINGSSL_CRYPTO OFF "${LIBRARY_PATH}/boringssl/build/crypto/libcrypto.so") +resolve_library(LIBRARY_PATH_THREAD_POOL OFF "${LIBRARY_PATH}/Thread-Pool/build/libThreadPoolStatic.a") +resolve_library(LIBRARY_PATH_TERMINAL OFF "${LIBRARY_PATH}/CXXTerminal/build/libCXXTerminalStatic.a") +resolve_library(LIBRARY_PATH_VARIBALES OFF "${LIBRARY_PATH}/StringVariable/build/libStringVariablesStatic.a") +resolve_library(LIBRARY_PATH_YAML OFF "${LIBRARY_PATH}/yaml-cpp/build/libyaml-cpp.a") +resolve_library(LIBRARY_PATH_JSON OFF "${LIBRARY_PATH}/jsoncpp/build/src/lib_json/libjsoncpp.a") +resolve_library(LIBRARY_PATH_ED255 OFF "${LIBRARY_PATH}/ed25519/build/libed25519.a") +resolve_library(LIBRARY_PATH_DATA_PIPES OFF "${LIBRARY_PATH}/DataPipes/build/libDataPipes.so" "${LIBRARY_PATH}/DataPipes/cmake-build-release/libDataPipes.so" "${LIBRARY_PATH}/DataPipes/cmake-build-debug/libDataPipes.so") +resolve_library(LIBRARY_PATH_OPUS OFF "${LIBRARY_PATH}/opus/build/.libs/libopus.a") + +#if(EXISTS ${CMAKE_SOURCE_DIR}/../libraries/mysqlconnector/build/libmysqlcppconn8.so.1) +# set(LIBRARY_PATH_MYSQL "${CMAKE_SOURCE_DIR}/../libraries/mysqlconnector/build/libmysqlcppconn8.so.1") +#else() +# set(LIBRARY_PATH_MYSQL "libmysqlcppconn8-static.a") +# message(WARNING "Could not resolve mysql! Using default resolver") +#endif() + +add_definitions(-DINET -DINET6) +add_subdirectory(shared/) +add_subdirectory(client/) +add_subdirectory(server/) +add_subdirectory(license/) +add_subdirectory(flooder/) +add_subdirectory(MusicBot/) +add_subdirectory(music/) \ No newline at end of file diff --git a/MusicBot/CMakeLists.txt b/MusicBot/CMakeLists.txt new file mode 100644 index 0000000..b3391a7 --- /dev/null +++ b/MusicBot/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.6) +project(TeaMusic) + +set(CMAKE_VERBOSE_MAKEFILE ON) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -fpermissive -Wall -Wno-sign-compare -Wno-reorder -static-libgcc -static-libstdc++ -Wl,--enable-new-dtags,--export-dynamic") +set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3") +set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELEASE} -O3") +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/enviroment/) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/libs/) +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +include_directories(../music/include/) +include_directories(../shared/src) +add_definitions(-DLTM_DESC) + +#The basic library +add_library(TeaMusic SHARED src/MusicPlayer.cpp) + +#The test file +add_executable(TeaMusicTest ${MUSIC_SOURCE_FILES} main.cpp) +target_link_libraries(TeaMusicTest ProviderFFMpeg ProviderYT ProviderOpus TeaMusic pthread ThreadPool opus asound opusfile stdc++fs dl mpg123 avformat avcodec avutil TeaSpeak CXXTerminal event jsoncpp) diff --git a/MusicBot/MavStuff.cpp b/MusicBot/MavStuff.cpp new file mode 100644 index 0000000..c1e6fbb --- /dev/null +++ b/MusicBot/MavStuff.cpp @@ -0,0 +1,198 @@ +// +// Created by wolverindev on 14.01.18. +// + +#include +#include +#include +#include +#include +#include + +#define PCM_DEVICE "default" +using namespace std; +using namespace std::chrono; + +typedef struct WAV_HEADER +{ + /* RIFF Chunk Descriptor */ + uint8_t RIFF[4]; // RIFF Header Magic header + uint32_t ChunkSize; // RIFF Chunk Size | All stuff - 8 bytes? + uint8_t WAVE[4]; // WAVE Header + /* "fmt" sub-chunk */ + uint8_t fmt[4]; // FMT header + uint32_t Subchunk1Size; // Size of the fmt chunk + uint16_t AudioFormat; // Audio format 1=PCM,6=mulaw,7=alaw, 257=IBM Mu-Law, 258=IBM A-Law, 259=ADPCM + uint16_t NumOfChan; // Number of channels 1=Mono 2=Sterio + uint32_t SamplesPerSec; // Sampling Frequency in Hz + uint32_t bytesPerSec; // bytes per second + uint16_t blockAlign; // 2=16-bit mono, 4=16-bit stereo + uint16_t bitsPerSample; // Number of bits per sample + /* "data" sub-chunk */ + uint8_t Subchunk2ID[4]; // "data" string + uint32_t Subchunk2Size; // Sampled data length +}; + +int main(int, char**){ + + //youtube-dl --newline -x --audio-format opus --audio-quality 0 -o "vid01.%(ext)s" https://www.youtube.com/watch?v=AlXfbVpDUdo + // run a process and create a streambuf that reads its stdout and stderr + /* + redi::ipstream proc("youtube-dl --help", redi::pstreams::pstdout | redi::pstreams::pstderr); + std::string line; + // read child's stdout + while (std::getline(proc.out(), line)) + std::cout << "stdout: " << line << '\n'; + // read child's stderr + while (std::getline(proc.err(), line)) + std::cout << "stderr: " << line << '\n'; + */ + yt::YTVManager yt(nullptr); + //yt.downloadAudio("AlXfbVpDUdo").waitAndGet(); + cout << "Downloaded" << endl; + + /* + COpusCodec codec(48000, 1); + std::fstream fin ("vAlXfbVpDUdo.opus", std::ios::binary | fstream::in); + std::fstream fout("vAlXfbVpDUdo.raw", std::ios::binary | fstream::out); + + if(!fin) throw std::runtime_error("Could not open input file"); + if(!fout) throw std::runtime_error("Could not open output file"); + + try + { + COpusCodec codec(48000, 1); + + size_t frames = 0; + while(codec.decode_frame(fin, fout)) + { + frames++; + } + + std::cout << "Successfully decoded " << frames << " frames\n"; + } + catch(OpusErrorException const& e) + { + std::cerr << "OpusErrorException: " << e.what() << "\n"; + return 255; + } + */ + + +//Read the header + WAV_HEADER header{}; + std::fstream fin ("vAlXfbVpDUdo.wav", std::ios::binary | fstream::in); + fin.read((char *) &header, sizeof(header)); + + //Read the data + uint16_t bytesPerSample = header.bitsPerSample / 8; //Number of bytes per sample + uint64_t numSamples = header.ChunkSize / bytesPerSample; //How many samples are in the wav file? + + cout << "RIFF header :" << header.RIFF[0] << header.RIFF[1] << header.RIFF[2] << header.RIFF[3] << endl; + cout << "WAVE header :" << header.WAVE[0] << header.WAVE[1] << header.WAVE[2] << header.WAVE[3] << endl; + cout << "FMT :" << header.fmt[0] << header.fmt[1] << header.fmt[2] << header.fmt[3] << endl; + cout << "Data size :" << header.ChunkSize << endl; + + // Display the sampling Rate from the header + cout << "Sampling Rate :" << header.SamplesPerSec << endl; + cout << "Number of bits used :" << header.bitsPerSample << endl; + cout << "Number of channels :" << header.NumOfChan << endl; + cout << "Number of bytes per second :" << header.bytesPerSec << endl; + cout << "Data length :" << header.Subchunk2Size << endl; + cout << "Audio Format :" << header.AudioFormat << endl; + + cout << "Block align :" << header.blockAlign << endl; + cout << "Data string :" << header.Subchunk2ID[0] << header.Subchunk2ID[1] << header.Subchunk2ID[2] << header.Subchunk2ID[3] << endl; + cout << "Samples :" << numSamples << endl; + cout << "Length :" << numSamples / header.SamplesPerSec << endl; + cout << "Header size " << sizeof(header) << endl; + + + unsigned int pcm, tmp, dir; + snd_pcm_t *pcm_handle; + snd_pcm_hw_params_t *params; + snd_pcm_uframes_t frames; + char *buff; + int buff_size, loops; + + int channels = 2; + int rate = 44100; + int seconds = 5; + + /* Open the PCM device in playback mode */ + if (pcm = snd_pcm_open(&pcm_handle, PCM_DEVICE, SND_PCM_STREAM_PLAYBACK, 0) < 0) + printf("ERROR: Can't open \"%s\" PCM device. %s\n", + PCM_DEVICE, snd_strerror(pcm)); + + /* Allocate parameters object and fill it with default values*/ + snd_pcm_hw_params_alloca(¶ms); + + snd_pcm_hw_params_any(pcm_handle, params); + + /* Set parameters */ + if (pcm = snd_pcm_hw_params_set_access(pcm_handle, params, + SND_PCM_ACCESS_RW_INTERLEAVED) < 0) + printf("ERROR: Can't set interleaved mode. %s\n", snd_strerror(pcm)); + + if (pcm = snd_pcm_hw_params_set_format(pcm_handle, params, + SND_PCM_FORMAT_S16_LE) < 0) + printf("ERROR: Can't set format. %s\n", snd_strerror(pcm)); + + if (pcm = snd_pcm_hw_params_set_channels(pcm_handle, params, channels) < 0) + printf("ERROR: Can't set channels number. %s\n", snd_strerror(pcm)); + + if (pcm = snd_pcm_hw_params_set_rate_near(pcm_handle, params, &rate, 0) < 0) + printf("ERROR: Can't set rate. %s\n", snd_strerror(pcm)); + + /* Write parameters */ + if (pcm = snd_pcm_hw_params(pcm_handle, params) < 0) + printf("ERROR: Can't set harware parameters. %s\n", snd_strerror(pcm)); + + /* Resume information */ + printf("PCM name: '%s'\n", snd_pcm_name(pcm_handle)); + + printf("PCM state: %s\n", snd_pcm_state_name(snd_pcm_state(pcm_handle))); + + snd_pcm_hw_params_get_channels(params, &tmp); + printf("channels: %i ", tmp); + + if (tmp == 1) + printf("(mono)\n"); + else if (tmp == 2) + printf("(stereo)\n"); + + snd_pcm_hw_params_get_rate(params, &tmp, 0); + printf("rate: %d bps\n", tmp); + + printf("seconds: %d\n", seconds); + + /* Allocate buffer to hold single period */ + snd_pcm_hw_params_get_period_size(params, &frames, 0); + cout << "perd size: " << frames << endl; + //buff_size = frames * channels * 2 /* 2 -> sample size */; + //buff = (char *) malloc(buff_size); + + snd_pcm_hw_params_get_period_time(params, &tmp, NULL); + + auto last = system_clock::now(); + for (loops = seconds * 1000000 / tmp; loops > 0; loops--) { + if(loops == 25) usleep(5 * 1000 * 1000); + cout << " dur: " << duration_cast(system_clock::now() - last).count() << endl; + last = system_clock::now(); + + int readSize = frames * channels * 2; + char buffer[frames * channels * 2]; + fin.read(buffer, readSize); + + if (pcm = snd_pcm_writei(pcm_handle, buffer, frames) == -EPIPE) { + printf("XRUN.\n"); + snd_pcm_prepare(pcm_handle); + } else if (pcm < 0) { + printf("ERROR. Can't write to PCM device. %s\n", snd_strerror(pcm)); + } + } + + snd_pcm_drain(pcm_handle); + snd_pcm_close(pcm_handle); + return 0; +} \ No newline at end of file diff --git a/MusicBot/Protocol.md b/MusicBot/Protocol.md new file mode 100644 index 0000000..8aa1bad --- /dev/null +++ b/MusicBot/Protocol.md @@ -0,0 +1,249 @@ +# Musik bot websocket protocol +## General structure +Transmitted data is in json format +The json format has the structure as following: +``` +{ + "type": , + "data": [ + { + : value + }, + ... + ] +} +``` +Example: +``` +{ + "type": "showMessage", + "data": [ + { + "message": "A simple info modal", + "type": "info" + }, + { + "message": "A simple error modal", + "type": "error" + } + ] +} +``` +## TODO list +* Music bot queue +* Music bot ts3 access rights (allow other clients to use this music bot etc.) + +## Packet types +### General types +#### Server `showMessage`: +This packet should show up a message modal. +* **[~]** + * `message`: + * `type`: {info|error} + + +#### Server `reqError`: +The server sends this if you applay a invalid request +* **[1]** + * `message`: + * `requestId`: + + +#### Server `disconnect` +I send this packet before i close the comunication +* **[1]** + * `message` + +___ +### Login packets +#### Client `login` +Try login +* **[1]** + * `username` + * `password` + * `requestId` + + +#### Server `notifylogin` +* **[1]** + * `requestId` + * `succeeded`: {0|1} + * `uid` own uid. only set if login failed + * `message` only set if login failed + +#### Client `logout` +* **[1]** + * `requestId` + +#### Server `notifylogout` +Could be send at any time (force logout) +* **[1]** + * `requestId` (empty of not requested) + * `succeeded`: {0|1} + +___ +### Server Management +#### Client `serverlist` +* **[1]** + * `requestId` + +#### Server `notifyserverlist` +Sends when requested or list updated +(Lists online avariable server for the client view) +* **[~]** + * [1] `requestId` (empty of not requested) + * `name` + * `uid` + * `serverId` + * `status`: {online|offline} + * `clientOnline` + * `maxClients` + +#### Server `notifyserverupdate` +Sends when a server changes display properties +* **[1]** + * `serverId` + * `key`: {name|onlineClients|maxClients} + * `value` + +___ +### Channel Management +#### Client `channellist` +Request a channel list +* **[1]** + * `requestId` + * `serverId` + +#### Server `notifychannellist` +The channel response is ordered: +This packet would also be send if the channel tree gets updated +``` +root +- sub 1 + - sub sub 1 + - sub sub 2 +- sub 2 +root 2 +... +``` +* **[~]** + * [1] `requestId` (empty of not requested) + * [1] `serverId` + * `name` + * `channelId` + * `channelParent` + * `channelOrder` + +___ +### Music bot management +#### Client `musicbotlist` +* **[1]** + * `requestId` + * `serverId` + +#### Server `notifymusikmusicbotlist` +* **[~]** + * [1] `requestId` (empty of not requested) + * [1] `serverId` + * `id` + * `connected`: {1|0} + * `name` + * `channelId` + * `ownerUid` (its your own if its matching with our own id) + * `ownerCldbid` + +#### Client `musicbotcreate` +* **[1]** + * `requestId` + * `serverId` + * `name` + * `channelId` + +#### Server `notifymusikbotcreated` +* **[1]** + * `requestId` (empty of not requested) + * `serverId` + * `id` + * `connected`: {1|0} + * `name` + * `channelId` + * `ownerUid` (its your own if its matching with our own id) + * `ownerCldbid` + +#### Client `musicbotdelete` +* **[1]** + * `requestId` + * `serverId` + * `name` + * `channelId` + +#### Server `notifymusikbotdelete` +* **[1]** + * `requestId` (empty of not requested) + * `serverId` + * `id` + +#### Client `musicbotinfo` +Request music bot info +* **[1]** + * `requestId` + * `serverId` + * `id` + +#### Server `notifymusicbotinfo` +* **[1]** + * `requestId` + * `serverId` + * `id` + * `name` + * `connected` + * `phoeticName` + * `channelId` + * `playing` + * `playingInfo`: Empty if noting selected + * `description` + * `textCurrentSong` + +#### Client `musicbotedit` +* **[1]** + * `requestId` + * `serverId` + * `id` + * `key` + * `value` + +#### Server `notifymusicbotedit` +* **[~]** + * [1] `requestId` (empty of not requested) + * [1] `serverId` + * `id` + * `key`: {connected|name|channelId|description|playing|playingInfo} + +#### Client `musicbotplay` +* **[1]** + * `requestId` + * `serverId` + * `id` + * `type`: {yt|file} + * `value` + +#### Server `notifymusicbotplay` +Send only as answer for `musicbotplay` +You would recive the play state update via `notifymusicbotedit` +* **[1]** + * `requestId` + * `succeeded` + * +#### Client `musicbotstop` +* **[1]** + * `requestId` + * `serverId` + * `id` + * `paused`: {1|0} + +#### Server `notifymusicbotstop` +Send only as answer for `musicbotstop` +You would recive the play state update via `notifymusicbotedit` +* **[1]** + * `requestId` + * `succeeded` \ No newline at end of file diff --git a/MusicBot/main.cpp b/MusicBot/main.cpp new file mode 100644 index 0000000..3531c2c --- /dev/null +++ b/MusicBot/main.cpp @@ -0,0 +1,210 @@ +#include +#include "../music/providers/shared/pstream.h" +#include +#include +#include +#include + +extern "C" { + #include + #include + #include +} +#include +#include + +#define PCM_DEVICE "default" +using namespace std; +using namespace std::chrono; +using namespace music; + +void die(const char* message) +{ + fprintf(stderr, "%s\n", message); + exit(1); +} + +int main(int, char**){ + logger::config::logLevel = spdlog::level::trace; + logger::config::terminalLevel = spdlog::level::trace; + //terminal::install(); + + logger::setup(); + logger::updateLogLevels(); + + //youtube-dl -s --dump-json https://www.youtube.com/watch?v=1ifTLj_glhc + //ffmpeg -hide_banner -nostats -i -ac 2 -ar 48000 -f s16le -acodec pcm_s16le pipe:1 + // + + //printf("Length: %d\n", codec_context->frame_size / codec_context->framerate.den / codec_context->channels); //576 | 3 +/* + // To initalize libao for playback + ao_initialize(); + + int driver = ao_default_driver_id(); + + // The format of the decoded PCM samples + ao_sample_format sample_format; + sample_format.bits = 16; + sample_format.channels = 2; + sample_format.rate = 44100; + sample_format.byte_format = AO_FMT_NATIVE; + sample_format.matrix = 0; + + ao_device* device = ao_open_live(driver, &sample_format, NULL); + + AVPacket packet; + int buffer_size = AVCODEC_MAX_AUDIO_FRAME_SIZE; + int8_t buffer[AVCODEC_MAX_AUDIO_FRAME_SIZE]; + + while (1) { + + buffer_size = AVCODEC_MAX_AUDIO_FRAME_SIZE; + + // Read one packet into `packet` + if (av_read_frame(container, &packet) < 0) { + break; // End of lstream. Done decoding. + } + + // Decodes from `packet` into the buffer + if (avcodec_decode_audio3(codec_context, (int16_t*)buffer, &buffer_size, &packet) < 1) { + break; // Error in decoding + } + + // Send the buffer contents to the audio device + ao_play(device, (char*)buffer, buffer_size); + } + + av_close_input_file(container); + + ao_shutdown(); + + fprintf(stdout, "Done playing. Exiting..."); + */ + + music::manager::loadProviders("providers/"); + std::string file = "https://www.youtube.com/watch?v=GVC5adzPpiE"; //https://www.youtube.com/watch?v=eBlg2oX0Z0Q + auto provider = music::manager::resolveProvider("YouTube", file); //test.mp3 + if(!provider) return 0; + cout << "Using provider -> " << provider->providerName << endl; + cout << "Using provider -> " << provider->providerDescription << endl; + if(true) return 0; + /* + auto fut = provider->createPlayer(file); + fut.waitAndGet(nullptr); + if(fut.failed()){ + log::log(log::err, "Could not create: " + fut.errorMegssage()); + } else log::log(log::info, "Got!"); + //VALID + /* + auto player = provider->createPlayer("https://r5---sn-4g5edned.googlevideo.com/videoplayback?lmt=1491636391724572&key=yt6&itag=251&keepalive=yes&signature=8220B0B2D85AFE6A44CAE901A9C5A5D9A2564DA4.59183C41417E91E04552835738ED0AD1592BD05E&source=youtube&clen=4017657&sparams=clen%2Cdur%2Cei%2Cgir%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Ckeepalive%2Clmt%2Cmime%2Cmm%2Cmn%2Cms%2Cmv%2Cpl%2Crequiressl%2Csource%2Cexpire&gir=yes&expire=1516917262&initcwndbps=856250&ei=rv1pWuvYBI-AV7SlqaAE&dur=234.061&mv=m&mt=1516895537&ms=au&requiressl=yes&ip=93.230.21.99&ipbits=0&mn=sn-4g5edned&mm=31&pl=26&mime=audio%2Fwebm&id=o-ABQnAlwQxtPQJTqHMc9SiTD-bGc1YR2yogmPcVUy1Xxp&ratebypass=yes").waitAndGet(nullptr); + /* + auto player = provider->createPlayer("https://r5---sn-4g5edned.googlevideo.com/videoplayback?mn=sn-4g5edned&mm=31&gir=yes&clen=4017657&requiressl=yes&mv=m&mt=1516820571&ms=au&ei=tdhoWtv0Fd5bY1wLY5qVA&lmt=1491636391724572&key=yt6&ip=80.133.238.232&expire=1516842261&dur=234.061&beids=%5B9466594%5D&id=o-AKeOawWpz0BT9Qxuqria6qw1_eTa3is7UXit4BLIC8re&initcwndbps=591250&source=youtube&sparams=clen%2Cdur%2Cei%2Cgir%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Ckeepalive%2Clmt%2Cmime%2Cmm%2Cmn%2Cms%2Cmv%2Cpl%2Crequiressl%2Csource%2Cexpire&ipbits=0&signature=230305B8592A57E939FDBF2D4C19C16DC2FE0D68.AC475B47E4533071F4A6EA96960384398A15D801&pl=26&itag=251&mime=audio%2Fwebm&keepalive=yes&ratebypass=yes").waitAndGet(nullptr); + */ + auto player = provider->createPlayer(file).waitAndGet(nullptr); + if(!player){ + cerr << "Could not load youtube video" << endl; + return -1; + } + if(!player->initialize()){ + log::log(log::err, "Could not inizalisze ffmpeg player -> " + player->error()); + return 1; + } + player->registerEventHandler("main", [player](music::MusicEvent event){ //FIXME weak ptr + log::log(log::info, "Got event " + to_string(event)); + if(event == music::EVENT_ERROR) { + log::log(log::err, "Recived error: " + player->error()); + player->clearError(); + } + }); + player->play(); + player->forward(chrono::minutes(0)); + cout << "Song length " << duration_cast(player->length()).count() << " seconds" << endl; + + + unsigned int pcm, tmp, dir; + snd_pcm_t *pcm_handle; + snd_pcm_hw_params_t *params; + snd_pcm_uframes_t frames; + int loops; + + int channels = 2; + int rate = 48000; + int seconds = duration_cast(player->length()).count() + 1; + seconds = 10000; + + if (pcm = snd_pcm_open(&pcm_handle, PCM_DEVICE, SND_PCM_STREAM_PLAYBACK, 0) < 0) + printf("ERROR: Can't open \"%s\" PCM device. %s\n", + PCM_DEVICE, snd_strerror(pcm)); + + snd_pcm_hw_params_alloca(¶ms); + + snd_pcm_hw_params_any(pcm_handle, params); + + if (pcm = snd_pcm_hw_params_set_access(pcm_handle, params, + SND_PCM_ACCESS_RW_INTERLEAVED) < 0) + printf("ERROR: Can't set interleaved mode. %s\n", snd_strerror(pcm)); + + if (pcm = snd_pcm_hw_params_set_format(pcm_handle, params, + SND_PCM_FORMAT_S16_LE) < 0) + printf("ERROR: Can't set format. %s\n", snd_strerror(pcm)); + + if (pcm = snd_pcm_hw_params_set_channels(pcm_handle, params, channels) < 0) + printf("ERROR: Can't set channels number. %s\n", snd_strerror(pcm)); + + if (pcm = snd_pcm_hw_params_set_rate_near(pcm_handle, params, &rate, 0) < 0) + printf("ERROR: Can't set rate. %s\n", snd_strerror(pcm)); + + if (pcm = snd_pcm_hw_params(pcm_handle, params) < 0) + printf("ERROR: Can't set harware parameters. %s\n", snd_strerror(pcm)); + + printf("PCM name: '%s'\n", snd_pcm_name(pcm_handle)); + printf("PCM state: %s\n", snd_pcm_state_name(snd_pcm_state(pcm_handle))); + + snd_pcm_hw_params_get_channels(params, &tmp); + printf("channels: %i ", tmp); + + if (tmp == 1) + printf("(mono)\n"); + else if (tmp == 2) + printf("(stereo)\n"); + + snd_pcm_hw_params_get_rate(params, &tmp, 0); + printf("rate: %d bps\n", tmp); + + printf("seconds: %d\n", seconds); + + snd_pcm_hw_params_get_period_size(params, &frames, 0); + cout << "perd size: " << frames << endl; + snd_pcm_hw_params_get_period_time(params, &tmp, NULL); + player->preferredSampleCount(frames); + + auto last = system_clock::now(); + for (loops = seconds * 1000000 / tmp; loops > 0; loops--) { + //cout << " dur: " << duration_cast(system_clock::now() - last).count() << endl; + + auto next = player->popNextSegment(); + if(!next) { + log::log(log::info, "END!"); + continue; + } + retry: + if (pcm = snd_pcm_writei(pcm_handle, next->segments, next->segmentLength) == -EPIPE) { + printf("XRUN.\n"); + snd_pcm_prepare(pcm_handle); + goto retry; + } else if (pcm < 0) { + printf("ERROR. Can't write to PCM device. %s\n", snd_strerror(pcm)); + } + //if((system_clock::now() - last) > ::seconds(1)) { + log::log(log::debug, "Time: " + to_string(duration_cast(system_clock::now() - last).count()) + " | " + to_string(duration_cast(player->currentIndex()).count())); + last = system_clock::now(); + //} + //TODO! + } + + snd_pcm_drain(pcm_handle); + snd_pcm_close(pcm_handle); + + return 0; +} \ No newline at end of file diff --git a/MusicBot/src/MusicPlayer.cpp b/MusicBot/src/MusicPlayer.cpp new file mode 100644 index 0000000..9df83a9 --- /dev/null +++ b/MusicBot/src/MusicPlayer.cpp @@ -0,0 +1,115 @@ +#include +#include +#include +#include +#include "MusicPlayer.h" + +using namespace std; +using namespace music; +using namespace music::manager; + +namespace fs = std::experimental::filesystem; + +void log::log(const Level& lvl, const std::string& msg) { + logger::logger(0)->log((spdlog::level::level_enum) lvl, "[Music] " + msg); +} + +void AbstractMusicPlayer::registerEventHandler(const std::string& key, const std::function& function) { + threads::MutexLock lock(this->eventLock); + this->eventHandlers.push_back({key, function}); +} + +void AbstractMusicPlayer::unregisterEventHandler(const std::string& string) { + threads::MutexLock lock(this->eventLock); + for(const auto& entry : this->eventHandlers){ + if(entry.first == string){ + this->eventHandlers.erase(find_if(this->eventHandlers.begin(), this->eventHandlers.end(), [string](const std::pair>& elm){ return elm.first == string; })); + break; + } + } +} + +void AbstractMusicPlayer::fireEvent(MusicEvent event) { + threads::MutexLock lock(this->eventLock); + auto listCopy = this->eventHandlers; //Copy for remove while fire + for(const auto& entry : listCopy) + entry.second(event); +} + +const char* music::stateNames[] = {"uninitialised", "playing", "paused", "stopped"}; + +static threads::Mutex staticLock; +static std::deque> types; + +std::deque> manager::registeredTypes(){ return types; } +void registerType(const std::shared_ptr& provider) { + threads::MutexLock l(staticLock); + types.push_back(provider); +} + +//empty for not set +std::shared_ptr manager::resolveProvider(const std::string& provName, const std::string& str) { + threads::MutexLock l(staticLock); + vector> provs; + for(const auto& prov : types){ + auto p = prov.get(); + if(!str.empty() && prov->acceptString(str)) + provs.push_back(prov); + else if(!provName.empty() && prov->providerName == provName) + provs.push_back(prov); + } + sort(provs.begin(), provs.end(), [str](const std::shared_ptr& a, const std::shared_ptr& b){ + return a->weight(str) > b->weight(str); + }); + return provs.empty() ? nullptr : provs.front(); +} + +typedef std::shared_ptr(*create_provider_fn)(); +void manager::loadProviders(const std::string& path) { + auto dir = fs::u8path(path); + if(!fs::exists(dir)){ + try { + fs::create_directories(dir); + } catch (std::exception& e) {} + return; + } + + deque paths; + for(const auto& entry : fs::directory_iterator(dir)){ + if(!entry.path().has_extension()) continue; + if(entry.path().extension().string() == ".so") + paths.push_back(entry.path()); + } + std::sort(paths.begin(), paths.end(), [](const fs::path& a, const fs::path& b){ return a.filename().string() < b.filename().string(); }); + + int index = 0; + log::log(log::debug, "Provider load order:"); + for(const auto& entry : paths) + log::log(log::debug, "[" + to_string(index++) + "] " + entry.string()); + + for(const auto& entry : paths){ + void* provider = dlopen(entry.string().c_str(), RTLD_NOW); + if(!provider){ + log::log(log::err, string() + "Could not load music provider " + entry.string() + ". Error: " + dlerror()); + continue; + } + auto create_provider = reinterpret_cast(dlsym(provider, "create_provider")); + if(!create_provider){ + log::log(log::err, string() + "Could not find entry point create_provider()@" + entry.string()); + dlclose(provider); + continue; + } + auto mprovider = (*create_provider)(); + if(!mprovider){ + log::log(log::err, string() + "Could not create music provider for " + entry.string()); + dlclose(provider); + continue; + } + log::log(log::info, string() + "Loaded successfully provider " + mprovider->providerName); + types.push_back(mprovider); + } +} + +void manager::register_provider(const std::shared_ptr &provider) { + types.push_back(provider); +} \ No newline at end of file diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt new file mode 100644 index 0000000..d33843f --- /dev/null +++ b/client/CMakeLists.txt @@ -0,0 +1,79 @@ +cmake_minimum_required(VERSION 3.6) +project(TeamSpeak) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -static-libgcc -static-libstdc++ -pthread ${MEMORY_DEBUG_FLAGS}") + +include_directories(../shared/src) +add_definitions(-DLTM_DESC) + +set(SOURCE_FILES + main.cpp + src/protocol/Connection.cpp + src/protocol/ConnectionHandschake.cpp + src/protocol/ConnectionPacketHandler.cpp + + src/protocol/socket/FilteredUDPSocket.cpp + src/protocol/socket/RawUDPSocket.cpp + + src/Identity.cpp + src/MultithreadedIdentity.cpp + src/protocol/HandshakeNew.cpp + + ../shared/src/License.cpp +) + +find_package(Protobuf REQUIRED) +include_directories(${Protobuf_INCLUDE_DIRS}) +include_directories(${CMAKE_CURRENT_BINARY_DIR}) +protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS proto/LicenseKey.proto) + +add_executable(TeamSpeakClient ${SOURCE_FILES} ${PROTO_SRCS}) +target_link_libraries(TeamSpeakClient + ${LIBRARY_PATH_THREAD_POOL} #Static + TeaSpeak #Static + TeaLicenseHelper #Static + TeaMusic #Static + ${LIBRARY_PATH_TERMINAL} #Static + ${LIBRARY_PATH_VARIBALES} + ${LIBRARY_PATH_YAML} + pthread + stdc++fs + ${LIBEVENT_PATH}/libevent.a + ${LIBEVENT_PATH}/libevent_pthreads.a + opus.a + ${LIBRARY_PATH_JSON} + ${LIBRARY_PATH_PROTOBUF} + + DataPipes + #${LIBWEBRTC_LIBRARIES} #ATTENTIAN! WebRTC does not work with crypto! (Already contains a crypto version) + ${LIBRARY_TOM_CRYPT} + ${LIBRARY_TOM_MATH} + + #We're forsed to use boringssl caused by the fact that boringssl is already within webrtc! + + #Require a so + sqlite3 + ${LIBRARY_PATH_ED255} + + ${LIBRARY_PATH_BREAKPAD} + ${LIBRARY_PATH_JDBC} + ${LIBRARY_PATH_PROTOBUF} + + ${LIBRARY_PATH_BORINGSSL_SSL} + ${LIBRARY_PATH_BORINGSSL_CRYPTO} +) + +#strip -s -p -v TeamSpeakHash +add_executable(TeamSpeakHash src/Identity.cpp src/MultithreadedIdentity.cpp identityHash.cpp) +#set_target_properties(TeamSpeakHash PROPERTIES CMAKE_CXX_FLAGS "-o1") + +target_link_libraries(TeamSpeakHash + TeaSpeak + ThreadPoolStatic + pthread + ${TOM_LIBRARIES} + ${LIBRARY_TOM_CRYPT} + ${LIBRARY_TOM_MATH} + ${LIBRARY_PATH_BORINGSSL_SSL} + ${LIBRARY_PATH_BORINGSSL_CRYPTO} +) \ No newline at end of file diff --git a/client/HideString.h b/client/HideString.h new file mode 100644 index 0000000..2acf4f4 --- /dev/null +++ b/client/HideString.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include + +#define CRYPT_MACRO(r, d, i, elem) ( elem ^ ( d - i ) ) + +#define DEFINE_HIDDEN_STRING(NAME, SEED, SEQ)\ +static const char* BOOST_PP_CAT(Get, NAME)()\ +{\ + static char data[] = {\ + BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_FOR_EACH_I(CRYPT_MACRO, SEED, SEQ)),\ + '\0'\ + };\ +\ + static bool isEncrypted = true;\ + if ( isEncrypted )\ + {\ + for (unsigned i = 0; i < ( sizeof(data) / sizeof(data[0]) ) - 1; ++i)\ + {\ + data[i] = CRYPT_MACRO(_, SEED, i, data[i]);\ + }\ +\ + isEncrypted = false;\ + }\ +\ + return data;\ +} \ No newline at end of file diff --git a/client/changes.md b/client/changes.md new file mode 100644 index 0000000..990b822 --- /dev/null +++ b/client/changes.md @@ -0,0 +1,27 @@ +[OUT] clientinitiv alpha=kXZSTZ7qaOhblA== omega=MEwDAgcAAgEgAiA5+ycSoMRRdugndbohYtH7PqS6Q6h3TV1VnGRKoZNMsQIhANn2rszHVipB13LBcY2Zf3APg7HX7ix3WWiaT5hjQrUv ot=1 ip=84.200.62.248 +[ IN] initivexpand2 l=AQCBIiOUkbXLeQ\/6UhNB+8iJnwLYGZJglZoDFSTt+7awWgAJFtlwLxmo8AAAACVUZWFtU3BlYWsgU3lzdGVtcyBHbWJIAADtwskuiFg0FkafvNvS1uwz67b2lEhMnOkAygPvo0waGAAJQrh2HBxqvQAAACRUZWFtU3BlYWsgc3lzdGVtcyBHbWJIAADd85KDiA8UeqB5G9MBtNZ879CYBnfbjc\/OVj0j0xMcqQIJQ7qACi5lgAYAAAAAVGVhbVNwZWFrIFN5c3RlbXMgR21iSAAAitZK9P6JEXiiy+uERaapfw\/I5na7S5M+Se5IoOb\/24kgCV6OnAlfN1w= beta=5cB\/vUHLQ7GbVPNRStMLnZlyo572IFPzdgTTgxJ3zF4JBehh7nLudpdzk1hdbcglx4n3JmqX omega=MEwDAgcAAgEgAiEA83r2Er9vHr5sSEcm9g\/\/rdLjOdTMtwX\/Lkbi02+24FkCIB2P804l26yketcG8WxZQJ1z4ukU0e3SqEzm4pKHzMRY ot=1 proof=MEUCIAgYSvrPo0OKKD8aRmjeKVS1EVF6L\/DXLBT+XFkZlMVPAiEAnSMVOp8DOEMzcE42hna72+aWr+Z4+a76x23hmDJa4Uk= tvd=AQAAAABaQjKEblQQgGczKfB+CSS7IY5qli8LRS9jDltkab8leo6ltwQH82VUU\/WwnpCbazVbnh4iNbcobdxv\/MTX5Y3+6xpqDgEAEa1\/SLpE\/CIg2X7GX9mtDCCvP1KdQGaT\/RKoofPSsTkACRbZcAr4DPAAAAASVGVhbVNwZWFrIFN5c3RlbXMgR21iSAAAPgFaQLsu0h3FTQax+i0qXFXX\/Bn7VcGJH8SE5AOAiuQECVvUYwlenQk= time=1514211836 +[OUT] clientek ek=n4+LesWAJf+8ha6LlF5kWilUbj59qX+zV31asaniK8U= proof=MEQCIC+NxeqTNUQWqHz914BHkFfC3LTF8BkYAYktMB62sET7AiBPmnfN6yoB381g\/GAnZVwMkeCPIjKi9vHUk15yOdT5FQ== +[OUT] clientinit client_nickname=P3nisBaum client_version=3.1.7\s[Build:\s1513163251] client_platform=Windows client_input_hardware=1 client_output_hardware=1 client_default_channel client_default_channel_password client_server_password client_meta_data client_version_sign=tdNngCAZ1ImAf7BxJzO4RXv5nBRsUERsrSOnMKVUFNQg6BS4Bzag0RFgLVzs2DRj19AC8+q5cXgH+5Ms50mTCA== client_key_offset=15848377 client_nickname_phonetic client_default_token client_badges=Overwolf=0 hwid=3036580947951600a7de06cd796bb0f1,7209e96ac1f60ff7772b7301919e0878 +[OUT] clientinit client_nickname=P3nisBaum client_version=3.1.7\s[Build:\s1513163251] client_platform=Windows client_input_hardware=1 client_output_hardware=1 client_default_channel client_default_channel_password client_server_password client_meta_data client_version_sign=tdNngCAZ1ImAf7BxJzO4RXv5nBRsUERsrSOnMKVUFNQg6BS4Bzag0RFgLVzs2DRj19AC8+q5cXgH+5Ms50mTCA== client_key_offset=15848377 client_nickname_phonetic client_default_token client_badges=Overwolf=0 hwid=3036580947951600a7de06cd796bb0f1,7209e96ac1f60ff7772b7301919e0878 +[ IN] error id=521 msg=too\smany\sclones\salready\sconnected + + + +-> ot & ip needs to be set +-> ack needs to be unencripted as look clientinitiv not done + + +initivexpand2 l=AQCBIiOUkbXLeQ\/6UhNB+8iJnwLYGZJglZoDFSTt+7awWgAJFtlwLxmo8AAAACVUZWFtU3BlYWsgU3lzdGVtcyBHbWJIAADtwskuiFg0FkafvNvS1uwz67b2lEhMnOkAygPvo0waGAAJQrh2HBxqvQAAACRUZWFtU3BlYWsgc3lzdGVtcyBHbWJIAADd85KDiA8UeqB5G9MBtNZ879CYBnfbjc\/OVj0j0xMcqQIJQ7qACi5lgAYAAAAAVGVhbVNwZWFrIFN5c3RlbXMgR21iSAAA5Szd\/YkuL5NUF7SeHfvSKi5byOOMvYME0\/WRwlTHK8ogCV6hYglfSiI= beta=W\/D9NTwuZi6L1sbFpA+KiDBMSgl8y6Jy6vFVSGszJy1BPy8zEhNDQ62msazyAgk0CdbY0B+E omega=MEwDAgcAAgEgAiEA83r2Er9vHr5sSEcm9g\/\/rdLjOdTMtwX\/Lkbi02+24FkCIB2P804l26yketcG8WxZQJ1z4ukU0e3SqEzm4pKHzMRY ot=1 proof=MEUCIA3dgrIlXFIjkLrxJ1Yni7OqQYwPing03Xz\/zkQFrmdHAiEA8a0delCktJ21TIrj91QZbA76vtZUL38XVm+P2bR9M7c= tvd=AQAAAABaQjKEblQQgGczKfB+CSS7IY5qli8LRS9jDltkab8leo6ltwQH82VUU\/WwnpCbazVbnh4iNbcobdxv\/MTX5Y3+6xpqDgEAEa1\/SLpE\/CIg2X7GX9mtDCCvP1KdQGaT\/RKoofPSsTkACRbZcAr4DPAAAAASVGVhbVNwZWFrIFN5c3RlbXMgR21iSAAAPgFaQLsu0h3FTQax+i0qXFXX\/Bn7VcGJH8SE5AOAiuQECVvUYwlenQk= time=1514216642 + + +Nr 1 +[OUT] clientinitiv alpha=DBiidVqi8kTYAQ== omega=MEwDAgcAAgEgAiA5+ycSoMRRdugndbohYtH7PqS6Q6h3TV1VnGRKoZNMsQIhANn2rszHVipB13LBcY2Zf3APg7HX7ix3WWiaT5hjQrUv ot=1 ip=84.200.62.248 +[ IN] initivexpand2 l=AQCBIiOUkbXLeQ\/6UhNB+8iJnwLYGZJglZoDFSTt+7awWgAJFtlwLxmo8AAAACVUZWFtU3BlYWsgU3lzdGVtcyBHbWJIAADtwskuiFg0FkafvNvS1uwz67b2lEhMnOkAygPvo0waGAAJQrh2HBxqvQAAACRUZWFtU3BlYWsgc3lzdGVtcyBHbWJIAADd85KDiA8UeqB5G9MBtNZ879CYBnfbjc\/OVj0j0xMcqQIJQ7qACi5lgAYAAAAAVGVhbVNwZWFrIFN5c3RlbXMgR21iSAAALYVev\/rqwinxd2zrGZla8+69t03410Iyo9N6lV7bajMgCV6ioQlfS2E= beta=\/Eg8SA2j45sSVi28kVwBVpxT98G1w0tlenDVYuKy\/7Cn1ZZiNiwu8Z9XhGBxwrzPXrMHPEMJ omega=MEwDAgcAAgEgAiEA83r2Er9vHr5sSEcm9g\/\/rdLjOdTMtwX\/Lkbi02+24FkCIB2P804l26yketcG8WxZQJ1z4ukU0e3SqEzm4pKHzMRY ot=1 proof=MEUCIFFa+q1zdgA0OtXgdgn9gxSOMp7GBvc3vPW0YDTU2+e6AiEA8sjW3XDikxQADPoSaSw7lwL6gfXr6gwO0Hvro3RZBHU= tvd=AQAAAABaQjKEblQQgGczKfB+CSS7IY5qli8LRS9jDltkab8leo6ltwQH82VUU\/WwnpCbazVbnh4iNbcobdxv\/MTX5Y3+6xpqDgEAEa1\/SLpE\/CIg2X7GX9mtDCCvP1KdQGaT\/RKoofPSsTkACRbZcAr4DPAAAAASVGVhbVNwZWFrIFN5c3RlbXMgR21iSAAAPgFaQLsu0h3FTQax+i0qXFXX\/Bn7VcGJH8SE5AOAiuQECVvUYwlenQk= time=1514216961 +[OUT] clientek ek=19doGY4WwwAeW\/zHnmFJTpnE8h9MUJZSYFNpz4aUabs= proof=MEUCIGTwtZ1E+HjDPLqlyTuaHtvWmjYW\/\/tnQgugaC5u1swtAiEA9DN+kuwxuXG80FDzKnICGQYQGZZBySQv8Nhflg\/1FwQ= + + +[OUT] clientinitiv alpha=3Pcn19zISkIB5g== omega=MEwDAgcAAgEgAiA5+ycSoMRRdugndbohYtH7PqS6Q6h3TV1VnGRKoZNMsQIhANn2rszHVipB13LBcY2Zf3APg7HX7ix3WWiaT5hjQrUv ot=1 ip=84.200.62.248 +[ IN] initivexpand2 l=AQCBIiOUkbXLeQ\/6UhNB+8iJnwLYGZJglZoDFSTt+7awWgAJFtlwLxmo8AAAACVUZWFtU3BlYWsgU3lzdGVtcyBHbWJIAADtwskuiFg0FkafvNvS1uwz67b2lEhMnOkAygPvo0waGAAJQrh2HBxqvQAAACRUZWFtU3BlYWsgc3lzdGVtcyBHbWJIAADd85KDiA8UeqB5G9MBtNZ879CYBnfbjc\/OVj0j0xMcqQIJQ7qACi5lgAYAAAAAVGVhbVNwZWFrIFN5c3RlbXMgR21iSAAASpEEQfPJOP8aeecPLtzcu8iYmtqBJn8MWq+8lSQljYcgCV6iyAlfS4g= beta=JLFcgyWwlYNzy5lsTGJKGz6rvmShHrb6YEZfq4TXwTLSiWDGqckf6ywiMOP2MGAc1y1yQsEi omega=MEwDAgcAAgEgAiEA83r2Er9vHr5sSEcm9g\/\/rdLjOdTMtwX\/Lkbi02+24FkCIB2P804l26yketcG8WxZQJ1z4ukU0e3SqEzm4pKHzMRY ot=1 proof=MEYCIQCpKZJCmQCHyC2PLIleZVAl1fSTdnzqcNxhiXxVAHtUHgIhAPvPfziHtiqn36nB7KSVuKV+gMA29OL87bI1HP6CJNw2 tvd=AQAAAABaQjKEblQQgGczKfB+CSS7IY5qli8LRS9jDltkab8leo6ltwQH82VUU\/WwnpCbazVbnh4iNbcobdxv\/MTX5Y3+6xpqDgEAEa1\/SLpE\/CIg2X7GX9mtDCCvP1KdQGaT\/RKoofPSsTkACRbZcAr4DPAAAAASVGVhbVNwZWFrIFN5c3RlbXMgR21iSAAAPgFaQLsu0h3FTQax+i0qXFXX\/Bn7VcGJH8SE5AOAiuQECVvUYwlenQk= time=1514217000 +[OUT] clientek ek=yGZb1Qp0Y\/V7T+rMwcqtzUeOTIGgE6+7gcKxAw2OMPU= proof=MEQCIHdgsGlufstf\/ab1TvvM740Azs56jyGFmo93ijr53k1FAiBHNhWkV9xLUG19jpZGGe86SgCDli7GdSrv61ZGr3jD9g== + +bM5HB+Ox/Wht38dd6/VeB7L76ebcpP9YgKzWAk4lmQ8= \ No newline at end of file diff --git a/client/identityHash.cpp b/client/identityHash.cpp new file mode 100644 index 0000000..dd2d6ab --- /dev/null +++ b/client/identityHash.cpp @@ -0,0 +1,63 @@ +#include +#include +#include +#include "HideString.h" +DEFINE_HIDDEN_STRING(HeaderMessage, 0x44, ('#')('#')('#')('#')('#')('#')('#')('#')('#')('#')('#')('#')('#')('#')('#')('#')('#')(' ')('T')('S')('3')(' ')('H')('a')('s')('h')(' ')('b')('y')(' ')('W')('o')('l')('v')('e')('r')('i')('n')('D')('E')('V')(' ')('#')('#')('#')('#')('#')('#')('#')('#')('#')('#')('#')('#')('#')('#')('#')('#')('#')); + +using namespace ts; +using namespace std; + +inline string crypt(string in){ + stringstream result; + result << "DEFINE_HIDDEN_STRING(EncryptionKey, 0xA5, "; + + for(char c : in) + result << "('" << c << "')"; + + result << ");"; + return result.str(); +} + +int main(int argc, char** argv) { + cout << GetHeaderMessage() << endl; + if(argc < 3) { + cerr << "Invalid arguments. ./TSHash [ = 1.000.000]" << endl; + return 0; + } + + init_LTM(); + if(register_prng(&sprng_desc) == -1) { + cerr << "could not setup prng" << endl; + return EXIT_FAILURE; + } + if (register_cipher(&rijndael_desc) == -1) { + cerr << "could not setup rijndael" << endl; + return EXIT_FAILURE; + } + + int threads = atoi(argv[1]); + int level = atoi(argv[2]); + int blockSize = argc > 3 ? atoi(argv[3]) : 1 * 1000 * 1000; + + cout << "# Creating new identity (Threads: " << threads << " Level: " << level << " Size: " << blockSize << ")" << endl; + auto identity = Identity::createNew(); + cout << "IDENTITY: " << identity->exportIdentity() << endl; + cout << "# Start hashing" << endl; + if(!identity->improveSecurityLevelMultithreaded(level, threads, blockSize, 0, true)) { + cout << "ERROR: Could not improve identity!" << endl; + return 1; + } + cout << "INFO: Found identity!" << endl; + cout << "IDENTITY: " << identity->exportIdentity() << endl; + cout << "LEVEL: " << identity->getSecurityLevel() << endl; + return 0; +} +/* +#IDENTITY: 5513931022VPXfjXSN1qRa2IZvTpS3xs+L7YvcBI2lTfXgOWAkaAQwQVgR8BXtWJ1MKUSh/HGNGNgdVGWYHcCg1BgQie3pValZZB39hVE4HBStcAA5SUz9Tf1JbXVABBVUJUwFmFkJZLlZUfCtaZXFBaUVBZ1poOXlQd09KSzN6ejYxMjVnaGN3bmtSKzRPOXdvZWJObVpaT3gyOTA3ND0= +#LEVEL: 36 + +#IDENTITY: 0VdYzXyIWNlPR991zQZLQoGLDYA4JvCX9GQ3MAYzpBRABWAHZTAmJvNQUAMxEEKGQDJVlnanF1YDdQQn0GewlXVWoAQmVMLX9FVyZaMAI7KlAIeXpPKGMCJ3BTVAJHLGlUHARbBlpqSUNJUURISXY1UXJCYmlBMHlZOVV3dFdRRXovM0lkU1hzNFJvWTkwT1JienA5bVRRPT0= +Current level = 41 at 1233527956186 +Current offset index: 3750576000000 block size: 1000000 + +*/ \ No newline at end of file diff --git a/client/main.cpp b/client/main.cpp new file mode 100644 index 0000000..9281ef0 --- /dev/null +++ b/client/main.cpp @@ -0,0 +1,1176 @@ +#include +#include +#include +#include +#include +#include "src/Identity.h" +#include "src/protocol/Connection.h" +#include +#include +#include +#include +#include +#include +#include + +#define QLZ_COMPRESSION_LEVEL 3 +#include "qlz/QuickLZ.h" + +using namespace std; +using namespace ts; +using namespace license::teamspeak; +void hexout(std::ostream& os, unsigned char c) +{ + unsigned char uc = static_cast(c); + os << std::setw(2) << std::setfill('0') << (unsigned int)uc << ' '; +} + +void hexdump(std::ostream& outs, const std::string& s, size_t line_len = 16) +{ + std::ostringstream os; + const std::string::size_type slen(s.size()); + int i(0); + std::string::size_type pos(0); + const std::streamsize lines(slen / line_len); + const std::streamsize chars(slen % line_len); + std::ios::fmtflags f(os.flags()); + + os << "Length: " << s.length() << "/" << std::hex << "0x" << s.length() << endl; + for(std::streamsize line = 0; line <= lines - (chars == 0 ? 1 : 0); ++line) + { + os << std::hex << setfill('0') << setw(3) << line * line_len << " | "; + for(i = 0; i < line_len; ++i) + { + if(pos < s.length()) + hexout(os, s[pos]); + else os << " "; + pos++; + } + os << " | "; + if(pos - line_len < s.length()){ + auto av = s.substr(pos - line_len); + for(char c : av.substr(0, min(av.length(), line_len))){ + if(isprint(c)) + os << c << " "; + else + os << "." << " "; + } + } + os << '\n'; + } + os.flags(f); + outs << os.str() << endl; +} +//This RSA puzzel crashes the manager +//545333494e49543100658803ffED1eBAACDe66bd212F3B91A0C1dbd65b374F1FCEe353A74C78eB99B5b8B2e0eC7DC9Ce7bCdfcFE97622AbD015B4EBCB3fecc7D23bBfaA2B15DBBBBC1d009DBcd924B06Eb2e0CBCD0AfBbadc2ecbb812C47d5dcAA4BcFdaF73ACe89A106c98f6c0DBeac8e8FeF9d6d836Bd1Cd791f376452Cff581400ed760acB2deAE1Be53E98152AbaBfaC823e3e4FACdaAf69E977C8dE328ECDA5FEE3c327FfCf9228caf8D761aeF7bAdbebe5eAcda87Eba61F09D7AEC9D4EeBa763cFE3c9cE718d37dd4F73CA10ffB4127d15Bee7cF9Edc644b4be06f0F9C4aEb2FBDd9549e215959FaFea57b5B7900000000 + +/* + auto parent = import((char*) root); + fe_neg(parent.X, parent.X); +fe_neg(parent.T, parent.T); +char buf[32]; +ge_p3_tobytes((u_char*) buf, &parent); +return string(buf, 32); + */ +using namespace std::chrono; +inline std::string keyMul(const char* publicKey /* compressed */, char* privateKey /* uncompressed */, bool negate){ + ge_p3 keyA{}; + ge_p2 result{}; + + ge_frombytes_negate_vartime(&keyA, (u_char*) publicKey); + if(negate) { + fe_neg(keyA.X, keyA.X); /* undo negate */ + fe_neg(keyA.T, keyA.T); /* undo negate */ + } + ge_scalarmult_vartime(&result, (u_char*) privateKey, &keyA); + + char buffer[32]; + ge_tobytes((u_char*) buffer, &result); + return string(buffer, 32); +} + +#include "ed25519/sha512.h" +static int consttime_equal(const unsigned char *x, const unsigned char *y) { + unsigned char r = 0; + + r = x[0] ^ y[0]; + #define F(i) r |= x[i] ^ y[i] + F(1); + F(2); + F(3); + F(4); + F(5); + F(6); + F(7); + F(8); + F(9); + F(10); + F(11); + F(12); + F(13); + F(14); + F(15); + F(16); + F(17); + F(18); + F(19); + F(20); + F(21); + F(22); + F(23); + F(24); + F(25); + F(26); + F(27); + F(28); + F(29); + F(30); + F(31); + #undef F + + return !r; +} + +int ed25519_verify(bool do_hash, const unsigned char *signature, const unsigned char *message, size_t message_len, const unsigned char *public_key) { + unsigned char h[64]; + unsigned char checker[32]; + sha512_context hash; + ge_p3 A; + ge_p2 R; + + if (signature[63] & 224) { + return 0; + } + + if (ge_frombytes_negate_vartime(&A, public_key) != 0) { + return 0; + } + + if(do_hash) { + //sha512_init(&hash); + //sha512_update(&hash, signature, 32); + //sha512_update(&hash, public_key, 32); + //sha512_update(&hash, message, message_len); + //sha512_final(&hash, h); + assert(false); + } else { + memcpy(h, message, 64); + } + + sc_reduce(h); + ge_double_scalarmult_vartime(&R, h, &A, signature + 32); + ge_tobytes(checker, &R); + + if (!consttime_equal(checker, signature)) { + return 0; + } + + return 1; +} + +/* + char seed[64]; + + LicenseChain chain; + //E813384CE058BA63F5792D16A093B0C503D2C51FD19A1DC36FD613EFB88E7334 + u_char publicKey[32]; + u_char privateKey[32] = {0xE8, 0x13, 0x38, 0x4C, 0xE0, 0x58, 0xBA, 0x63, 0xF5, 0x79, 0x2D, 0x16, 0xA0, 0x93, 0xB0, 0xC5, 0x03, 0xD2, 0xC5, 0x1F, 0xD1, 0x9A, 0x1D, 0xC3, 0x6F, 0xD6, 0x13, 0xEF, 0xB8, 0x8E, 0x73, 0x34}; + //privateKey[0] &= 248; + //privateKey[31] &= 63; + //privateKey[31] |= 64; + ge_p3 A; + ge_scalarmult_base(&A, privateKey); + ge_p3_tobytes(publicKey, &A); + hexDump((char*) publicKey, 32, 32, 32); + + cout << "A: " << base64::encode(keyMul((char*) chain.generatePublicKey(public_tea_root).data(), (char*) chain.generatePrivateKey((u_char*) privateKey).data(), true)) << endl; + cout << "A: " << base64::encode(keyMul((char*) chain.generatePublicKey((u_char*) publicKey).data(), (char*) chain.generatePrivateKey((u_char*) private_tea_root).data(), true)) << endl; + /* + for(char& c : seed) + c = rand(); + ed25519_create_keypair((u_char*) publicKey, (u_char*) privateKey, (u_char*) seed); + cout << "A: " << base64::encode(keyMul((char*) public_tea_root, privateKey, true)) << endl; + cout << "B: " << base64::encode(keyMul(publicKey, (char*) private_tea_root, true)) << endl; + + chain.addServerEntry(7, "TeaSpeak"); + chain.addEphemeralEntry(); + / + + istringstream is(chain.exportChain()); + auto c2 = LicenseChain::parse(is, error); + + chain.print(); + c2->print(); + cout << "A: " << base64::encode(keyMul((char*) c2->generatePublicKey(public_tea_root).data(), (char*) chain.generatePrivateKey((u_char*) privateKey).data(), true)) << endl; + cout << "A: " << base64::encode(keyMul((char*) c2->generatePublicKey((u_char*) publicKey).data(), (char*) chain.generatePrivateKey((u_char*) private_tea_root).data(), true)) << endl; + cout << "Chain: " << base64::encode(chain.exportChain()) << endl; + */ + +inline std::string parseHex(const std::string& in) { + char buffer[(in.length() + 1) / 2]; + memset(buffer, 0, sizeof(buffer)); + size_t nibble_index = 0; + + for(auto c : in) { + if(c == '\n' || c == ' ') continue; + + uint8_t nibble = 0; + if(c >= 'A' && c <= 'F') + nibble = c - 'A' + 10; + else if(c >= 'a' && c <= 'f') + nibble = c - 'a' + 10; + else if(c >= '0' && c <= '9') + nibble = c - '0'; + + if(nibble_index % 2 == 0) + nibble <<= 4; + else + nibble &= 0xF; + buffer[nibble_index / 2] |= nibble; + nibble_index++; + } + + return string(buffer, (nibble_index + 1) / 2); +} + +bool read_crypted_string(std::string& result, const char* buffer, int length) { + char tmp[264]; + char current; + int index = 0; + + do { + //cout << "Read: " << ((index & 0x1F) - length) << " | " << index << endl; + tmp[index] = length ^ buffer[index] ^ buffer[(index & 0x1F) - length]; + } while(tmp[index++]); + result = string(tmp, index - 1); + return true; +} + +void print_dec_teamspeak() { + auto bin = parseHex("3EF0E405913439B81A2D60D467C04D11\n" + "8E7D6C6CC8DC2B8B8F62FD30F7027393\n" + "29B7A64DC47E7BAD0E6A788E7E881805\n" + "9D373E29D0FC5FAFBC57C8757C8E4CA7\n" + "B44ED70A35E78019D531CBC8299A2E34\n" + "F107D5C124B35F47C62A1F52EA43FF68\n" + "29B14B0F13EDE81BB7AD43C900CD7D41\n" + "AD02A125F7E306917D65E40B3F70CC61\n" + "C35F019E616A71CFC6369FCE3FFA26E5\n" + "1C739028E2FC58812827CBF0051EFC66\n" + "CA8257EE96943F9935D6F23B22C352F0\n" + "F432D2E6A915ED1E889B48BEABAF1613\n" + "E50143BD39F929C1B657F53627DB429E\n" + "874E42B42D8D8D48B999DB24C369EDA4\n" + "7ECBD3C97668FBAA7368C21ABABD3A93\n" + "B9E213EE48DED608F6E7E4585EA86C8D\n" + "66789F00B8AF2982F2D645FE50C08310\n" + "F6FAFC5923638A8264E1532C71A8B319\n" + "4D948F08FE727FF4536325F45E9D8609\n" + "F17176F1587229F848A7B210F97E6EE6\n" + "4B6178A0369B1047C520543DD4DC3AFA\n" + "2F009C270F4FFA5EAB4A33A6534954E6\n" + "971EF2F10D86342CB0142221F96FC249\n" + "1E9C790D04EBB03D8C2328A21D3C1408\n" + "E1ED1B822222E7293B7CCE76CC576EF0\n" + "1105FF7FD79676F6D6862D893BABF16F\n" + "D2AF8A2436CA7765EDD62C39C457EFF9\n" + "6BD6E0E929AB0889EB57B1A4AE413DED\n" + "5152A23AFE7EFBA855E13029C94A9D9E\n" + "5D58A5329E8212F58EC678CD61F1BB61\n" + "C6DAD36233822B35C356FFBC75798F16\n" + "B6B6739DAFE85AE258C3DA11EFF7E203\n" + "51AF150AED16549F8166F9415ADA3C5F\n" + "09F241E66B37A85B506A50938E7AAB0E\n" + "038220175AEE5DFA772BB4474C7663AA\n" + "AB59CB7379F114672F8E2CCE431F8073\n" + "784219DCC22EBD115B9E391B47B605A2\n" + "2F73EC1F142E05C7CA73B10B1D9A391A\n" + "5AF154E46D22AA58003A39FBF64F8832\n" + "3CA603672A9E2D8A075BC4373C0603E1\n" + "EF178B6A29A910737DC577D0560C8423\n" + "667D96C336969E69ED3AE0126ACD33EB\n" + "E80D8A3867A702277BD839D34603846A\n" + "627ED38234818579EE3EFD0F21CD33E6\n" + "EF0F9B2329A1443771DD779E400A8275\n" + "7760B6B44C42A4628B8308AC90C06294\n" + "64FFB17DDBCFDB7A7B9D3C7CD357C350\n" + "B9C560814A54B32C948305AEDEC062C6\n" + "65FBB05D769D8364FB43589C576D6454\n" + "879B63EC5E52B77C899270EA460EDB62\n" + "1A14AC1EB93F65ED4A0F14FFAA5FFFF7\n" + "008453897B03A45A828164E3512E66A1\n" + "AF55CB2A68EA403332833A9B1D5CD56F\n" + "723699957BD9DE3DA767A75B6DC365B9\n" + "FA57C66962E857765E6AA4E609816F6D\n" + "EC732AE3FF1B86226BA6073E76CF7BD5\n" + "1F0F956C7B70C9CB78F9D8130DEA75CD\n" + "D612F9D4CA3FCD26A2FC65949FC62E3F\n" + "C2646B9B12BAF43F21C659E1FA3ED5F8\n" + "E61BAF108ADB41BFEBCAC22B37D34EEA\n" + "A36ECFF6BE07B31DD7CF51A1B7A31E12\n" + "B15D5CBB22E43EDDE94FED263B9E2BA7\n" + "4E52B62B8FC600A09CDA79D96BFBA036\n" + "82C7DB656C837431CA439608B1C52F84\n" + "4148BF6FEB4D9C9374E150509D6C4D17\n" + "A707B7687DEF784094D360F34D40C663\n" + "5D57A61CA43F71F10F091EF3EB4FF4ED\n" + "019E4E8E7244E046909D33866BBFF85D\n" + "D87869ED4570288D3FDC18439232313D\n" + "91C07ED9C02AA062A85B638F71A4AD4D\n" + "D9616BE30675339F35DD7121F4E04E97\n" + "3E3CAA513062CD78DE471696737571C6\n" + "C533C0967CF835F5077FD475E8E71B8E\n" + "2B3BBD16666FD07BC5077AD10410BE67\n" + "CECC5AA1C597208828F6E27A95839237\n" + "6B90637C8C12DF05F78F28C10408F02B\n" + "AF8A5F4EAA3F829659BE9ED37AD42DAC\n" + "A327C9D9D960738D6A2ACC4B9F17C38D\n" + "4658BF20988347AB9DCB2BCE7EFEA735\n" + "82CAD6632FD42738C8569B41B3CB6C85\n" + "404CB46FEB579C8265FA42599D7A5015\n" + "B810A02C38AB0B011CB9AD47E0ED479C\n" + "5A856957967AB9B345D57565BD4C7C24\n" + "8336800C188B2B213C998D67C0CD67BC\n" + "7AA54977B615DAD560A7051D8F2B0D41\n" + "BD02F66021BF4C4047EAB90AA1BD4999\n" + "752CF4F712952778B0086C63D967C247\n" + "1C866F7968CD916A998676E825FF0D75\n" + "D23BFEF20AD155D01915E37ADADA1FF1\n" + "C384368E34AFB6629F92C76F28C4797C\n" + "9113D917A5907BCE194282F9342CCF56\n" + "BFF370D2E9BE11B80288DD05A4A2B70C\n" + "05F45304A838BB3DC5AA5DF43F368E48\n" + "EAFC7DD8E8AC5FA50595CE44B8A4A24F\n" + "57E94E57BD37F53FCCF84FFC37238E49\n" + "F7E767D5E9BC11CCB1696FDD1AB8A032\n" + "96B0FC0CEB57DAC91BE4FAED1050BA1E\n" + "12E925A161DAEA4BA56D6E8908A5AF60\n" + "96BCF549BF48D09B1FEAF0B8104DBD00\n" + "03FB6BAB6ADAFC02AA643C8E01B9B524\n" + "8DA2F62C1CD5C92DB0145D88314956F5\n" + "44ED7A21AF4B0844E3FB0EA3EB47DA17\n" + "DC3359A313DAC761A6150F8A370A4190\n" + "22EFF7148D6428AB093265CA63D95306\n" + "DE7E796ECEC5389EDF7FE433EA137780\n" + "2680CA031FFB66C28B5EE79F87369133\n" + "ABE63C83919D3B2299787E9300D70BE4\n" + "886FCB4205F271D0C249EDBFC6293ED8\n" + "58E4F124EAF8BF0DB50F94AD9E534BA8\n" + "31D8811DBB848C71D82CEEBB24C2C5D4\n" + "24729E6766DB59DC1EBBD271F2BF726A\n" + "8910F9A03C9AA5AD56EC4BD39917B0E8\n" + "F20545A90942E371AF7D8AB25EA0757C\n" + "9C08B0BA32D7A4E24EE40CA04E859B7C\n" + "E34B408463490CA109B93521B2121805\n" + "A0B45EF9F45E85439C704E8F6EA5BB5C\n" + "C36B60A443692C812999020DDB2F5019\n" + "D0DE38AD5A762DD4C03CB54218862517\n" + "53A246E47233B1455D1AF0EF0AFDB55D\n" + "C803CF2740E50ECFDE36EB421C86390F\n" + "42EB5FF13B23B75C541B9EF0091DE168\n" + "9FC55BF8CA8E7F9E2EA7FE6C9880E741\n" + "7373657274696F6E20226D5F74696D65\n" + "725265736F6C7574696F6E20213D2030\n" + "22206661696C6564206174202E2E2F2E\n" + "2E2F2E2E2F2E2E2F732F646570732F74\n" + "65616D737065616B5F7365727665725F\n" + "6C69622F7372632F74735F7365727665\n" + "722F616E7469666C6F6F642E6370703A\n" + "00416E7469466C6F6F64004173736572\n" + "74696F6E20226D5F706F696E74735469\n" + "636B5261746520213D20302220666169\n" + "6C6564206174202E2E2F2E2E2F2E2E2F\n" + "2E2E2F732F646570732F7465616D7370\n" + "65616B5F7365727665725F6C69622F73\n" + "72632F74735F7365727665722F616E74\n" + "69666C6F6F642E6370703A0052656765\n" + "7870206572726F7220666F7220495020\n" + "65787072657373696F6E202200222C20\n" + "6572726F723A200042616E4D616E6167\n" + "657200526567657870206572726F7220\n" + "666F72204E616D652065787072657373\n" + "696F6E20220054686973206E616D6520\n" + "69732000596F7520617265200062616E\n" + "6E6564207065726D616E656E746C792E\n" + "0062616E6E65642074656D706F726172\n" + "696C792E0020526561736F6E3A202200\n" + "0A54727920616761696E20696E20006E\n" + "6F7469667962616E6C697374006C6173\n" + "746E69636B6E616D6500696E766F6B65\n" + "72636C6462696400656E666F7263656D\n" + "656E74730062616E5F696E736572742E\n" + "73716C0062616E5F69700062616E5F6E\n" + "616D650062616E5F7569640062616E5F\n" + "74696D657374616D700062616E5F6C65\n" + "6E6774680062616E5F696E766F6B6572\n" + "5F636C69656E745F69640062616E5F69\n" + "6E766F6B65725F7569640062616E5F69\n" + "6E766F6B65725F6E616D650062616E5F\n" + "726561736F6E0062616E5F6861736800\n" + "62616E5F64656C6574652E73716C0062\n" + "616E5F696400417373657274696F6E20\n" + "226572726F72203D3D204552524F525F\n" + "6F6B22206661696C6564206174202E2E\n" + "2F2E2E2F2E2E2F2E2E2F732F64657073\n" + "2F7465616D737065616B5F7365727665\n" + "725F6C69622F7372632F74735F736572\n" + "7665722F62616E6D616E616765722E63\n" + "70703A0062616E2064656C6574656420\n" + "28657870697265642920726561736F6E\n" + "3D0062616E5F6C6973742E73716C0069\n" + "6E76616C69642062616E206974656D20\n" + "776974682069643A2000000000000000\n" + "4E5374335F5F3132315F5F656D707479\n" + "5F6E6F6E5F6F776E5F73746174654963\n" + "45450000000000000000000000000000\n" + "4E5374335F5F3131355F5F6861735F6F\n" + "6E655F73746174654963454500000000\n" + "4E5374335F5F31365F5F6E6F64654963\n" + "45450000000000000000000000000000\n" + "4E5374335F5F3131315F5F616C746572\n" + "6E617465496345450000000000000000\n" + "4E5374335F5F3131375F5F6F776E735F\n" + "74776F5F737461746573496345450000\n" + "4E5374335F5F3131365F5F6F776E735F\n" + "6F6E655F737461746549634545000000\n" + "4E5374335F5F3131375F5F7265706561\n" + "745F6F6E655F6C6F6F70496345450000\n" + "4E5374335F5F31365F5F6C6F6F704963\n" + "45450000000000000000000000000000\n" + "4E5374335F5F3132365F5F656E645F6D\n" + "61726B65645F73756265787072657373\n" + "696F6E49634545000000000000000000\n" + "4E5374335F5F3132385F5F626567696E\n" + "5F6D61726B65645F7375626578707265\n" + "7373696F6E4963454500000000000000\n" + "4E5374335F5F3131305F5F725F616E63\n" + "686F7249634545000000000000000000\n" + "4E5374335F5F3131305F5F6C5F616E63\n" + "686F7249634545000000000000000000\n" + "4E5374335F5F3131325F5F6D61746368\n" + "5F636861724963454500000000000000\n" + "4E5374335F5F3132305F5F6D61746368\n" + "5F636861725F636F6C6C61746549634E\n" + "535F313272656765785F747261697473\n" + "49634545454500000000000000000000\n" + "4E5374335F5F3131385F5F6D61746368\n" + "5F636861725F696361736549634E535F\n" + "313272656765785F7472616974734963\n" + "45454545000000000000000000000000\n" + "4E5374335F5F3132305F5F627261636B\n" + "65745F65787072657373696F6E49634E\n" + "535F313272656765785F747261697473\n" + "49634545454500000000000000000000\n" + "4E5374335F5F3131315F5F6D61746368\n" + "5F616E79496345450000000000000000\n" + "4E5374335F5F3131305F5F6261636B5F\n" + "72656649634545000000000000000000\n" + "4E5374335F5F3131385F5F6261636B5F\n" + "7265665F636F6C6C61746549634E535F\n" + "313272656765785F7472616974734963\n" + "45454545000000000000000000000000\n" + "4E5374335F5F3131365F5F6261636B5F\n" + "7265665F696361736549634E535F3132\n" + "72656765785F74726169747349634545\n" + "45450000000000000000000000000000\n" + "4E5374335F5F3132335F5F6D61746368\n" + "5F616E795F6275745F6E65776C696E65\n" + "49634545000000000000000000000000\n" + "4E5374335F5F3131315F5F6C6F6F6B61\n" + "6865616449634E535F31327265676578\n" + "5F747261697473496345454545000000\n" + "4E5374335F5F3131355F5F776F72645F\n" + "626F756E6461727949634E535F313272\n" + "656765785F7472616974734963454545\n" + "45000000000000000000000000000000\n" + "4E5374335F5F3131335F5F656D707479\n" + "5F737461746549634545000000000000\n" + "4E5374335F5F3132305F5F7368617265\n" + "645F7074725F706F696E74657249504E\n" + "535F31335F5F656D7074795F73746174\n" + "65496345454E535F313464656661756C\n" + "745F64656C6574654953325F45454E53\n" + "5F39616C6C6F6361746F724953325F45\n" + "45454500000000000000000000000000\n" + "4E5374335F5F31313464656661756C74\n" + "5F64656C657465494E535F31335F5F65\n" + "6D7074795F7374617465496345454545\n" + "00000000000000000000000000000000\n" + "4E5374335F5F3131315F5F656E645F73\n" + "74617465496345450041737365727469\n" + "6F6E20226D5F705669727475616C5365\n" + "7276657220213D205F5F6E756C6C2220\n" + "6661696C6564206174202E2E2F2E2E2F\n" + "2E2E2F2E2E2F732F646570732F746561\n" + "6D737065616B5F7365727665725F6C69\n" + "622F7372632F74735F7365727665722F\n" + "6368616E636C69656E74736361636865\n" + "2E6370703A004368616E436C69656E43\n" + "6163686500417373657274696F6E2022\n" + "6D5F5468726561644944203D3D207374\n" + "643A3A746869735F7468726561643A3A\n" + "6765745F6964282922206661696C6564\n" + "206174202E2E2F2E2E2F2E2E2F2E2E2F\n" + "732F646570732F7465616D737065616B\n" + "5F7365727665725F6C69622F7372632F\n" + "74735F7365727665722F6368616E636C\n" + "69656E747363616368652E6370703A00\n" + "636F6D706C61696E5F696E736572742E\n" + "73716C00636F6D706C61696E5F66726F\n" + "6D5F636C69656E745F696400636F6D70\n" + "6C61696E5F746F5F636C69656E745F69\n" + "6400636F6D706C61696E5F6D65737361\n" + "676500636F6D706C61696E5F74696D65\n" + "7374616D7000636F6D706C61696E5F68\n" + "61736800636F6D706C61696E5F64656C\n" + "6574652E73716C00636F6D706C61696E\n" + "5F64656C6574655F616C6C2E73716C00\n" + "636F6D706C61696E5F64656C6574655F\n" + "7072756E652E73716C00636F6D706C61\n" + "696E5F6765745F62795F736572766572\n" + "69642E73716C00536F6D6520636F6D70\n" + "6C61696E7420656E7472696573206672\n" + "6F6D2074686520646174616261736520\n" + "77657265206E6F74206C6F6164656420\n" + "62656361757365207468657920776572\n" + "65206475706C696361746573206F6620\n" + "616C7265616479206C6F61646564206F\n" + "6E657300436F6D706C61696E4D677200\n" + "6368616E6E656C5F696E736572745F62\n" + "756C6B2E73716C006F72675F6368616E\n" + "6E656C5F6964006368616E6E656C5F69\n" + "6E736572745F62756C6B5F6669787570\n" + "2E73716C006368616E6E656C5F696E73\n" + "6572745F62756C6B5F6D617070696E67\n" + "2E73716C006368616E6E656C5F70726F\n" + "706572746965735F62756C6B5F696E73\n" + "6572742E73716C006368616E6E656C5F\n" + "64656C6574652E73716C006368616E6E\n" + "656C5F696E736572742E73716C006368\n" + "616E6E656C5F7570646174655F706172\n" + "656E7469642E73716C00536572766572\n" + "517565727920477565737400636C6965\n" + "6E745F696E736572742E73716C00636C\n" + "69656E745F696E736572745F62756C6B\n" + "2E73716C006F72675F636C69656E745F\n" + "696400636C69656E745F696E73657274\n" + "5F62756C6B5F6D617070696E672E7371\n" + "6C00636C69656E745F70726F70657274\n" + "6965735F62756C6B5F696E736572742E\n" + "73716C00636C69656E745F6765745F62\n" + "795F7569642E73716C00636C69656E74\n" + "5F6D6F6E74685F75706C6F616400636C\n" + "69656E745F746F74616C5F75706C6F61\n" + "6400636C69656E745F6D6F6E74685F64\n" + "6F776E6C6F616400636C69656E745F74\n" + "6F74616C5F646F776E6C6F616400636C\n" + "69656E745F64656C6574652E73716C00\n" + "636C69656E745F7570646174655F6C6F\n" + "67696E5F696E666F2E73716C00636C69\n" + "656E745F7570646174655F6E616D652E\n" + "73716C00636C69656E745F7570646174\n" + "655F73746174732E73716C00636C6965\n" + "6E745F7570646174655F747261666669\n" + "635F73746174732E73716C006D657373\n" + "6167655F6765745F756E726561645F62\n" + "795F636C69656E7469642E73716C0075\n" + "6E72656164002E736F006C6962007761\n" + "6974696E6720666F7220636F6E6E6563\n" + "74696F6E20617661696C61626C650064\n" + "6174616261736520636F6E6E65637469\n" + "6F6E206E6F7420666F756E6400417373\n" + "657274696F6E20226572726F72203D3D\n" + "204552524F525F6F6B22206661696C65\n" + "64206174202E2E2F2E2E2F2E2E2F2E2E\n" + "2F732F646570732F7465616D73706561\n" + "6B5F7365727665725F6C69622F737263\n" + "2F74735F7365727665722F6461746162\n" + "6173652F64625F64617461626173652E\n" + "6370703A00506C65617365206D616B65\n" + "207375726520796F7520757365207468\n" + "6520737570706C696564207473337365\n" + "727665725F6D696E696D616C5F72756E\n" + "7363726970742E736820746F2072756E\n" + "20746865207365727665722C206F7220\n" + "736574204C445F4C4942524152595F50\n" + "41544820796F757273656C6600756E61\n" + "626C6520746F206C6F61642064617461\n" + "6261736520706C7567696E206C696272\n" + "617279202200222C2068616C74696E67\n" + "21007473336462706C7567696E5F696E\n" + "6974007473336462706C7567696E5F73\n" + "687574646F776E007473336462706C75\n" + "67696E5F636F6E6E6563740074733364\n" + "62706C7567696E5F646973636F6E6E65\n" + "6374007473336462706C7567696E5F65\n" + "786563007473336462706C7567696E5F\n" + "6F70656E007473336462706C7567696E\n" + "5F62756C6B5F696E7365727400747333\n" + "6462706C7567696E5F6765746C617374\n" + "696E736572746964007473336462706C\n" + "7567696E5F6765746D6F646966696564\n" + "726F77636F756E74007473336462706C\n" + "7567696E5F7461626C65657869737473\n" + "007473336462706C7567696E5F766572\n" + "73696F6E007473336462706C7567696E\n" + "5F6E616D65007473336462706C756769\n" + "6E5F73686F72746E616D650074733364\n" + "62706C7567696E5F6170697665727369\n" + "6F6E007473336462706C7567696E5F67\n" + "65746C6173746572726F72004661696C\n" + "656420746F20696D706F727420220022\n" + "2066726F6D202200436F756C64206E6F\n" + "74206C6F616420646174616261736520\n" + "706C7567696E006462506C7567696E20\n" + "6E616D653A20202020006462506C7567\n" + "696E2076657273696F6E3A2000506C75\n" + "67696E206170692076657273696F6E20\n" + "69732000207768696C65200020697320\n" + "72657175697265640064625F65786563\n" + "2829200064625F62756C6B5F696E7365\n" + "72742829200064625F6F70656E282920\n" + "00646174616261736520627573792C20\n" + "77616974696E6720666F722066696E69\n" + "7368696E6720696E646578207461736B\n" + "732C206D61792074616B6520736F6D65\n" + "2074696D65210073657453514C66726F\n" + "6D46696C65282066696C653A00726570\n" + "206669656C6473202F20696E73657274\n" + "20706F696E742073697A65206E6F7420\n" + "73616D6520666F722073716C20616E64\n" + "20726570656174207061727400417373\n" + "657274696F6E2022737472696E675F66\n" + "69656C645F636F756E74203D3D207374\n" + "72696E675F6669656C645F6E616D6573\n" + "2E73697A65282922206661696C656420\n" + "6174202E2E2F2E2E2F2E2E2F2E2E2F73\n" + "2F646570732F7465616D737065616B5F\n" + "7365727665725F6C69622F7372632F74\n" + "735F7365727665722F64617461626173\n" + "652F64625F64617461626173652E6370\n" + "703A00696E76616C696420706172616D\n" + "6574657220666F756E6420005C5B5C5B\n" + "5C5B282E2A3F295C5D5C5D5C5D005C3A\n" + "285B5E5C3A5D2A3F5C2A295C3A003A24\n" + "3031303A003A24303100417373657274\n" + "696F6E2022216D5F73746F726167652E\n" + "656F66282922206661696C6564206174\n" + "202E2E2F2E2E2F2E2E2F2E2E2F732F64\n" + "6570732F7465616D737065616B5F7365\n" + "727665725F6C69622F7372632F74735F\n" + "7365727665722F64617461626173652F\n" + "64625F64617461626173652E6370703A\n" + "00756E6B6E6F776E207461626C652066\n" + "69656C64207265717565737465643A20\n" + "00417373657274696F6E2022636F756E\n" + "74203C3D20363422206661696C656420\n" + "6174202E2E2F2E2E2F2E2E2F2E2E2F73\n" + "2F646570732F7465616D737065616B5F\n" + "7365727665725F6C69622F7372632F74\n" + "735F7365727665722F64617461626173\n" + "652F64625F64617461626173652E6370\n" + "703A00417373657274696F6E20227369\n" + "7A65735B695D203C3D207374643A3A6E\n" + "756D657269635F6C696D6974733C756E\n" + "7369676E656420696E743E3A3A6D6178\n" + "282922206661696C6564206174202E2E\n" + "2F2E2E2F2E2E2F2E2E2F732F64657073\n" + "2F7465616D737065616B5F7365727665\n" + "725F6C69622F7372632F74735F736572\n" + "7665722F64617461626173652F64625F\n" + "64617461626173652E6370703A005C3A\n" + "285B612D7A412D5A302D395F2D5D2A3F\n" + "295C3A005C3A285B612D7A412D5A302D\n" + "395F2D5D2A3F295C2A303F5C3A000000\n" + "3134437573746F6D4461746162617365\n" + "00000000000000000000000000000000\n" + "3134506C7567696E4461746162617365\n" + "0064625F4372656174655461626C6573\n" + "282920756E61626C6520746F206C6F61\n" + "6420696E7374616E6365206465666175\n" + "6C742076616C7565730053514C006372\n" + "656174655F7461626C65732E73716C00\n" + "64625F4372656174655461626C657328\n" + "2920756E61626C6520746F2063726561\n" + "7465207461626C65730064625F437265\n" + "6174655461626C65732829207461626C\n" + "65732063726561746564006465666175\n" + "6C74732E73716C0064625F4372656174\n" + "655461626C6573282920756E61626C65\n" + "20746F20696E73657274206465666175\n" + "6C742076616C7565730064625F437265\n" + "6174655461626C6573282920666F756E\n" + "6420696E76616C696420646174616261\n" + "73652076657273696F6E0064625F4372\n" + "656174655461626C6573282920756E61\n" + "626C6520746F207361766520696E7374\n" + "616E63652064656661756C742076616C\n" + "7565730064625F437265617465546162\n" + "6C6573282920756E61626C6520746F20\n" + "696E7365727420646174616261736520\n" + "76657273696F6E0064726F705F746162\n" + "6C65732E73716C0064625F4372656174\n" + "655461626C65732829207461626C6573\n" + "2064726F7065640064625F4372656174\n" + "655461626C6573282920756E61626C65\n" + "20746F2064726F70207461626C657300\n" + "7570646174655F64617461626173655F\n" + "76657273696F6E2E73716C0043524954\n" + "4943414C206572726F72207768696C65\n" + "207570646174696E6720646174616261\n" + "73652076657273696F6E007570646174\n" + "655F0064617461626173652075706461\n" + "746564207375636365737366756C6C79\n" + "20746F207265766973696F6E3A200075\n" + "70646174655461626C6573282920756E\n" + "61626C6520746F207570646174652064\n" + "617461626173652076657273696F6E00\n" + "64617461626173652075706461746520\n" + "746F207265766973696F6E3A20006375\n" + "73746F6D5F696E736572742E73716C00\n" + "637573746F6D5F64656C6574655F6279\n" + "5F636C69656E7469642E73716C00636C\n" + "69656E745F64656C6574655F7072756E\n" + "652E73716C0067726F7570735F736572\n" + "7665720067726F7570735F6368616E6E\n" + "656C007065726D5F696E736572742E73\n" + "716C007065726D5F6964007065726D5F\n" + "76616C7565007065726D5F6E65676174\n" + "6564007065726D5F736B697000706572\n" + "6D5F64656C6574655F62795F67726F75\n" + "7069642E73716C007065726D5F64656C\n" + "6574655F62795F7065726D69642E7371\n" + "6C007065726D5F696E736572745F6275\n" + "6C6B2E73716C0067726F75705F6D656D\n" + "6265725F696E736572745F62756C6B2E\n" + "73716C0067726F75705F696E73657274\n" + "2E73716C0067726F75705F696E736572\n" + "745F62756C6B5F736E617073686F742E\n" + "73716C006F72675F67726F75705F6964\n" + "0067726F75705F696E736572745F6275\n" + "6C6B5F736E617073686F745F6765745F\n" + "6D617070696E672E73716C0067726F75\n" + "705F64656C6574652E73716C00746162\n" + "6C6567726F7570007461626C65706572\n" + "6D73007461626C656D656D6265727300\n" + "67726F75705F72656E616D652E73716C\n" + "0067726F7570735F6765745F62795F73\n" + "657276657269645F747970652E73716C\n" + "0067726F75705F69645F6765745F6279\n" + "5F6E616D652E73716C007065726D5F75\n" + "70646174655F6765745F67726F757073\n" + "2E73716C006661696C656420746F2075\n" + "7064617465207065726D697373696F6E\n" + "732C2068616C74656421007065726D5F\n" + "72656E616D652E73716C006F6C645F70\n" + "65726D5F6E616D65006E65775F706572\n" + "6D5F6E616D6500756E61626C6520746F\n" + "2072656E616D65207065726D69737369\n" + "6F6E2C2068616C74656421007065726D\n" + "5F636F70795F64656661756C745F7065\n" + "726D697373696F6E732E73716C00756E\n" + "61626C6520746F20636F707920646566\n" + "61756C74207065726D697373696F6E73\n" + "2C2068616C74656421006661696C6564\n" + "20746F20726574726965766520766972\n" + "7475616C736572766572732C2068616C\n" + "74656421007065726D5F736572766572\n" + "5F67726F757000695F636C69656E745F\n" + "6D6F646966795F706F77657200695F6E\n" + "65656465645F6D6F646966795F706F77\n" + "65725F636C69656E745F6D6F64696679\n" + "5F706F77657200695F636C69656E745F\n" + "6E65656465645F6D6F646966795F706F\n" + "77657200695F6E65656465645F6D6F64\n" + "6966795F706F7765725F636C69656E74\n" + "5F6E65656465645F6D6F646966795F70\n" + "6F776572007570646174656420706572\n" + "6D697373696F6E7320746F2076657273\n" + "696F6E2031007065726D5F6368616E6E\n" + "656C5F67726F75707300757064617465\n" + "64207065726D697373696F6E7320746F\n" + "2076657273696F6E2032007570646174\n" + "6564207065726D697373696F6E732074\n" + "6F2076657273696F6E20330075706461\n" + "746564207065726D697373696F6E7320\n" + "746F2076657273696F6E203400757064\n" + "61746564207065726D697373696F6E73\n" + "20746F2076657273696F6E2035007570\n" + "6461746564207065726D697373696F6E\n" + "7320746F2076657273696F6E20360075\n" + "706461746564207065726D697373696F\n" + "6E7320746F2076657273696F6E203700\n" + "695F6368616E6E656C5F6D6F64696679\n" + "5F6D616B655F636F6465635F656E6372\n" + "79707465640075706461746564207065\n" + "726D697373696F6E7320746F20766572\n" + "73696F6E203800757064617465642070\n" + "65726D697373696F6E7320746F207665\n" + "7273696F6E2039007570646174656420\n" + "7065726D697373696F6E7320746F2076\n" + "657273696F6E20313000695F636C6965\n" + "6E745F6D61785F636C6F6E657300695F\n" + "6E65656465645F6D6F646966795F706F\n" + "7765725F636C69656E745F6D61785F63\n" + "6C6F6E65730075706461746564207065\n" + "726D697373696F6E7320746F20766572\n" + "73696F6E203131007570646174655F70\n" + "65726D697373696F6E735F31322E7371\n" + "6C006661696C656420746F2075706772\n" + "616465207065726D697373696F6E732C\n" + "2068616C746564210075706461746564\n" + "207065726D697373696F6E7320746F20\n" + "76657273696F6E203132007570646174\n" + "6564207065726D697373696F6E732074\n" + "6F2076657273696F6E20313300757064\n" + "61746564207065726D697373696F6E73\n" + "20746F2076657273696F6E2031340075\n" + "706461746564207065726D697373696F\n" + "6E7320746F2076657273696F6E203135\n" + "0075706461746564207065726D697373\n" + "696F6E7320746F2076657273696F6E20\n" + "31360075706461746564207065726D69\n" + "7373696F6E7320746F2076657273696F\n" + "6E20313700625F7669727475616C7365\n" + "727665725F6D6F646966795F64656661\n" + "756C745F74656D705F6368616E6E656C\n" + "5F64656C6574655F64656C617900695F\n" + "6E65656465645F6D6F646966795F706F\n" + "7765725F7669727475616C7365727665\n" + "725F6D6F646966795F64656661756C74\n" + "5F74656D705F6368616E6E656C5F6465\n" + "6C6574655F64656C617900695F636861\n" + "6E6E656C5F6372656174655F6D6F6469\n" + "66795F74656D705F64656C6574655F64\n" + "656C617900695F6E65656465645F6D6F\n" + "646966795F706F7765725F6368616E6E\n" + "656C5F6372656174655F6D6F64696679\n" + "5F74656D705F64656C6574655F64656C\n" + "61790075706461746564207065726D69\n" + "7373696F6E7320746F2076657273696F\n" + "6E203138007570646174656420706572\n" + "6D697373696F6E7320746F2076657273\n" + "696F6E20313900757064617465506572\n" + "6D697373696F6E73282920756E61626C\n" + "6520746F20696E736572742064617461\n" + "626173652076657273696F6E00736572\n" + "7665725F696E736572742E73716C0073\n" + "65727665725F64656C6574652E73716C\n" + "0062696E64696E67735F6C6973742E73\n" + "716C007365727665725F757064617465\n" + "5F747261666669635F73746174732E73\n" + "716C007365727665725F757064617465\n" + "5F706F72742E73716C00736572766572\n" + "5F7570646174655F6175746F73746172\n" + "742E73716C007365727665725F676574\n" + "5F6279706F72742E73716C0073657276\n" + "65725F7570646174655F6D616368696E\n" + "655F69642E73716C007265766F636174\n" + "696F6E735F696E736572745F62756C6B\n" + "2E73716C007265766F636174696F6E5F\n" + "74797065007265766F636174696F6E5F\n" + "65787069726174696F6E007265766F63\n" + "6174696F6E5F6B6579007265766F6361\n" + "74696F6E735F6765746C6973742E7371\n" + "6C00496E76616C6964207265766F6361\n" + "74696F6E206B657920007265766F6361\n" + "74696F6E735F64656C6574652E73716C\n" + "003B20726561736F6E3A200046696C65\n" + "4D616E6167657200436F756C64206E6F\n" + "742062696E642066696C657472616E73\n" + "666572206C697374656E20706F727420\n" + "6F6E2064656661756C74206164647265\n" + "737365730066696C657472616E736665\n" + "722062696E64206661696C6564206F6E\n" + "20004572726F72207768696C65206765\n" + "7474696E67206C697374656E696E6720\n" + "6164647265737365732E200041737365\n" + "7274696F6E20226D5F62616E64776964\n" + "74685F6C696D69745F73657276657273\n" + "2E66696E64287365727665725F696429\n" + "203D3D206D5F62616E6477696474685F\n" + "6C696D69745F736572766572732E656E\n" + "64282922206661696C6564206174202E\n" + "2E2F2E2E2F2E2E2F2E2E2F732F646570\n" + "732F7465616D737065616B5F73657276\n" + "65725F6C69622F7372632F74735F7365\n" + "727665722F66696C657472616E736665\n" + "722F73657276657266696C656D616E61\n" + "6765722E6370703A0041737365727469\n" + "6F6E2022697420213D206D5F62616E64\n" + "77696474685F6C696D69745F73657276\n" + "6572732E656E64282922206661696C65\n" + "64206174202E2E2F2E2E2F2E2E2F2E2E\n" + "2F732F646570732F7465616D73706561\n" + "6B5F7365727665725F6C69622F737263\n" + "2F74735F7365727665722F66696C6574\n" + "72616E736665722F7365727665726669\n" + "6C656D616E616765722E6370703A0045\n" + "72726F7220616363657074696E672066\n" + "696C657472616E736665722000417373\n" + "657274696F6E202262775F7365727665\n" + "7220213D206D5F62616E647769647468\n" + "5F6C696D69745F736572766572732E65\n" + "6E64282922206661696C656420617420\n" + "2E2E2F2E2E2F2E2E2F2E2E2F732F6465\n" + "70732F7465616D737065616B5F736572\n" + "7665725F6C69622F7372632F74735F73\n" + "65727665722F66696C657472616E7366\n" + "65722F73657276657266696C656D616E\n" + "616765722E6370703A00417373657274\n" + "696F6E202262775F636C69656E742021\n" + "3D2062775F7365727665722D3E736563\n" + "6F6E642E6D5F636C69656E745F62616E\n" + "647769647468732E656E642829222066\n" + "61696C6564206174202E2E2F2E2E2F2E\n" + "2E2F2E2E2F732F646570732F7465616D\n" + "737065616B5F7365727665725F6C6962\n" + "2F7372632F74735F7365727665722F66\n" + "696C657472616E736665722F73657276\n" + "657266696C656D616E616765722E6370\n" + "703A0046696C654D616E616765722054\n" + "696D657220657870697265735F66726F\n" + "6D5F6E6F772072657475726E65642065\n" + "72726F723A2000747269656420746F20\n" + "72656D6F766520697020746861742077\n" + "6173206E6F7420696E206C6973742120\n" + "00636C6F7365006C6F63616C5F656E64\n" + "706F696E740000000000000000000000\n" + "1E000000000000001E00000000000000\n" + "0A00000000000000C800000000000000\n" + "F4010000000000000000000000000000\n" + "25000000000000000000000000000000\n" + "00000000000000000000000000000000\n" + "0000000000000080FFFFFFFFFFFFFF7F\n" + "0100000000000080FDFFFFFFFFFFFF7F"); + cout << "Bin length: " << bin.length() << endl; + //0x33E 0x43C + //cout << "Str: " << read_crypted_string(bin.data() + 0x3BA, 0x3BA) << endl; + //cout << "Str: " << read_crypted_string(bin.data() + 0x3DF, 0x3DF) << endl; + //cout << "Str: " << read_crypted_string(bin.data() + 0x405, 0x405) << endl; => virtual server id + //cout << "Str: " << read_crypted_string(bin.data() + 0x43C, 0x43C) << endl; => is duplicated in same instance, shutting down! + + for(int index = 0x3E; index < bin.length() + 1; ) { + string str; + read_crypted_string(str, bin.data() + index, index); + cout << "Hex: 0x" << hex << setfill('0') << setw(3) << index << " => " << str << endl; + index += str.length() + 1; + } +} + +int main(int argc, char** argv){ + threads::timer t(""); + + srand(system_clock::now().time_since_epoch().count()); + init_LTM(); + if(register_prng(&sprng_desc) == -1) { + cerr << "could not setup prng" << endl; + return EXIT_FAILURE; + } + if (register_cipher(&rijndael_desc) == -1) { + cerr << "could not setup rijndael" << endl; + return EXIT_FAILURE; + } + string error; + + + if(false) { + auto base64_tvd = base64::decode("AQCvbHFTQDY/terPeilrp/ECU9xCH5U3xC92lYTNaY/0KQAJFueAazbsgAAAACVUZWFtU3BlYWsgU3lzdGVtcyBHbWJIAADmqdzhKVTai5ZX3LMCOmhH4wOWa7Jrb27PTN6XJSvOaAAKcspvMINAZwAAACRUZWFtU3BlYWsgc3lzdGVtcyBHbWJIAADRNBxg9GPfGFpwf0akzRQs4B/VhIvD+cKAC7vzRAys/AIKfC4AC27CAAYAAAAAW09EQU5JIEHDhyBUQUtJTF0gVHMzLkFtYWNpT2xtYXlhblRheWZhLkNPTSB8IFdFQkRpeW8uQ09NAA=="); + istringstream s(base64_tvd); + auto result = LicenseChain::parse(s, error); + if(!result) { + cerr << error << endl; + return 0; + } + + result->print(); + return 0; + } + + + cout << base64::encode(license::teamspeak::Anonymous::chain->exportChain()) << endl; + cout << base64::encode(reinterpret_cast(license::teamspeak::Anonymous::root_key), 32) << endl; + + if(false) { + auto key_plk = license::teamspeak::Anonymous::chain->generatePublicKey(); + auto key_prv = license::teamspeak::Anonymous::chain->generatePrivateKey((u_char*) license::teamspeak::Anonymous::root_key, license::teamspeak::Anonymous::root_index); + + u_char sign[64]; + string message = "Hello World"; + ed25519_sign(sign, (u_char*) message.data(), message.length(), (u_char*) key_plk.data(), (u_char*) key_prv.data()); + assert(ed25519_verify(sign, (const u_char*) message.data(), message.length(), (u_char*) key_plk.data())); + } +#if false + auto base64_license = "Co4CCsQBAQCvbHFTQDY/terPeilrp/ECU9xCH5U3xC92lYTNaY/0KQAJFueAazbsgAAAACVUZWFtU3BlYWsgU3lzdGVtcyBHbWJIAABVa/g1Aa+EhqDv4a+FKZ+nhnzgNNoG5LZZDMbWP1rUJwAJjrHNL59A2wAAACRUZWFtU3BlYWsgc3lzdGVtcyBHbWJIAAAY8GKBKoLJ2NJT15WG1E3GQ0vl0Ju29Oq3KqYAeEH9GQIJmBqACpU6gAQAAAIAVG9tIFdlYmVyABIgwLAWtDC3hSt8KzSBEksYyaYXxrIybtV207ImV+ugSWkYgAQgCioeVGVhbVNwZWFrIDMgTm9uLVByb2ZpdCBMaWNlbnNlErUBAQCvbHFTQDY/terPeilrp/ECU9xCH5U3xC92lYTNaY/0KQAJFueAazbsgAAAACVUZWFtU3BlYWsgU3lzdGVtcyBHbWJIAABVa/g1Aa+EhqDv4a+FKZ+nhnzgNNoG5LZZDMbWP1rUJwAJjrHNL59A2wAAACRUZWFtU3BlYWsgc3lzdGVtcyBHbWJIAACWjeLEq44YFFAhN9aie2W8g7q3LO2UN/FiHtoGxOoiKAUJjrHNL59A2xpARpdAlulkIKm793JIeJcyxYvvc/tdj4Bx45qcLMISR8qdAOjtgTAV2fWbkdjx0roelL6EAubc3HPij+5Xny1UDw=="; + ts::proto::license::teamspeak::License license; + license.ParseFromString(base64::decode(base64_license)); + + auto chain_sign = LicenseChain::parse(license.sign_chain(), error); + auto chain_license = LicenseChain::parse(license.license().chain(), error); + + { + auto key = chain_sign->generatePublicKey(); + auto signature = license.license_sign(); + auto message = license.license().SerializeAsString(); + + assert(ed25519_verify((const u_char*) signature.data(), (const u_char*) message.data(), message.length(), (u_char*) key.data())); + } + { + auto key_plk = chain_license->generatePublicKey(); + auto key_prv = chain_license->generatePrivateKey((u_char*) license.license().root().data(), 3); + + u_char sign[64]; + string message = "Hello World"; + ed25519_sign(sign, (u_char*) message.data(), message.length(), (u_char*) key_plk.data(), (u_char*) key_prv.data()); + assert(ed25519_verify(sign, (const u_char*) message.data(), message.length(), (u_char*) key_plk.data())); + } + chain_sign->print(); + chain_license->print(); + + cout << "version:0" << endl; + cout << "chain:" << base64::encode(chain_license->exportChain()) << endl; + cout << "root_key_prv:" << base64::encode(license.license().root()) << endl; + cout << "root_key_pbl:" << base64::encode((const char*) public_root, 32) << endl; + cout << "root_prv_index:" << to_string(3) << endl; +#endif + //return 0; + + /** + * DONT DELET! + */ + //Last checked offset was 1591824411998 + //Level 39 + //ts::Identity* id = new ts::Identity("4161411998Vginro7hE2a4haFhxRR5+Q1aknrR8Nn1iYAAUZldWVn8XZFVVUnUOVBFeMQJ9HHZaOVh3fEhLShwdXHkie2lza1gDWQltMwhROBNeVTQ2GS5EeFNbA2dBE3QvXU9jJ0VKMXd4YkpxU0tFQUlnYVZaeVVTTTYzSldSLzBRT3Nqd3hSelJBYmRpZ2xsaFhsVTF5L3hVYWgxWT0="); + + //Last checked offset was 1330241596003 + //Level 48 535525596003 + //ts::Identity* id = new ts::Identity("535525596003VMXsJbBqTW70w6dnQUt/X/CiIeth9DEVcVEVUUxBjXUY9RGxSeVN+OhYDHjJSFRoCJHIDd3FjXCgSdH0Gf3RhBlJ0SGYbIEBCVzNcAAITKD9yY30aAgBqOnQIQmVdVQB5L3NVWnZkQUNJUUR2dG5ScnNYdHVWZkFhem9OYVNxTGhjZ2ZwVXhVelFpcGMrSDFSbkIrL3dRPT0="); + + /* + for(int i = 0; ltc_ecc_sets[i].size != 0; i++){ + cout << " -> " << ltc_ecc_sets[i].name << endl; + } + int targetLevel = 20; + if(argc > 1){ + targetLevel = atoi(argv[1]); + } + + cout << "Try to find security level " << targetLevel << endl; + ts::Identity* id = new ts::Identity("30895417VvPSPlSzA2mTBpjn7RrJYEq/PmZcNAmhDCXkVdStbXUU2bVxBQ0IHNVAGNTR/CXplN2Z/AwRASSFTQn0GV3NTGX18dXENB2laLQJHKVFUKgl/VHRgD21LUlAHVk18FQcEMGpAXTBxUUNJSEtnTCtOcXJhZDRUSTlKNk04clZXMHkxQjcwWnNsanNsbldOOWo5cjBObA=="); + id->improveSecurityLevelMultithreaded(targetLevel, 16, 1000000, 10 * 1000 * 1000U, true); + cout << "New string: " << id->exportIdentity() << endl; + + if(true) return false; + */ + + /* + string error; + //auto stream = stringstream(base64::decode("AQCvbHFTQDY/terPeilrp/ECU9xCH5U3xC92lYTNaY/0KQAJFueAazbsgAAAACVUZWFtU3BlYWsgU3lzdGVtcyBHbWJIAABhl9gwla/UJp2Eszst9TRVXO/PeE6a6d+CTI6Pg7OEVgAJc5CrL4Nh8gAAACRUZWFtU3BlYWsgc3lzdGVtcyBHbWJIAACvTQIgpv6zmLZq3znh7ygmOSokGFkFjz4bTigrOnetrgIJdIIACdS/gAYAAAAAU29zc2VuU3lzdGVtcy5iaWQAADY7+uV1CQ1niOvYSdGzsu83kPTNWijovr3B78eHGeePIAm98vQJvpu0")); + auto stream = stringstream(base64::decode("AQA1hUFJiiSs0wFXkYuPUJVcDa6XCrZTcsvkB0Ffzz4CmwIITRXgCqeTYAcAAAAgQW5vbnltb3VzAAC4R+5mos+UQ/KCbkpQLMI5WRp4wkQu8e5PZY4zU+/FlyAJwaE8CcJJ/A==")); + auto chain = LicenseChain::parse(stream, error); + if(!chain) { + cerr << "Could not parse: " << error << endl; + return 0; + } + chain->print(); + cout << chain->exportChain() << endl; + + auto key = chain->generateKey(); + cout << " -> " << base64::encode(key) << endl; + cout << hex; + for(const auto& c : key) + cout << " " << (uint32_t) (uint8_t) c << endl; + if(true) return 0; + */ + + ts::Identity* identity; + //identity = new ts::Identity("30895417VvPSPlSzA2mTBpjn7RrJYEq/PmZcNAmhDCXkVdStbXUU2bVxBQ0IHNVAGNTR/CXplN2Z/AwRASSFTQn0GV3NTGX18dXENB2laLQJHKVFUKgl/VHRgD21LUlAHVk18FQcEMGpAXTBxUUNJSEtnTCtOcXJhZDRUSTlKNk04clZXMHkxQjcwWnNsanNsbldOOWo5cjBObA=="); + identity = new ts::Identity("4161411998Vginro7hE2a4haFhxRR5+Q1aknrR8Nn1iYAAUZldWVn8XZFVVUnUOVBFeMQJ9HHZaOVh3fEhLShwdXHkie2lza1gDWQltMwhROBNeVTQ2GS5EeFNbA2dBE3QvXU9jJ0VKMXd4YkpxU0tFQUlnYVZaeVVTTTYzSldSLzBRT3Nqd3hSelJBYmRpZ2xsaFhsVTF5L3hVYWgxWT0="); + cout << "Private key: " << identity->privateKey() << endl; + cout << "Public key: " << identity->publicKey() << endl; + + { + auto hash = digest::sha256("Hello World"); + auto sign_rfc = base64::decode("kjEYFcZ4iDxosbhi7bV04nRu0c0JzEIKAACSpLo9G1cJX60Ta46QpoPXTpMg9E8z5pHeZXPVUx75zmWE/LQTuQ=="); + int state; + auto result = ecc_verify_hash_rfc7518((u_char*) sign_rfc.data(), sign_rfc.length(), (u_char*) hash.data(), hash.length(), &state, identity->getKeyPair()); + __asm__("nop"); + } + + //identity = ts::Identity::createNew(); + //identity->improveSecurityLevel(9); + /* + identity = ts::Identity::createNew(); + cout << "Improve" << endl; + identity->improveSecurityLevelMultithreaded(24, 12); + cout << "Got level: " << identity->getSecurityLevel() << " at " << identity->lastValidKeyOffset() << " - " << identity->privateKey() << " - " << identity->publicKey() << endl; + return 0; + */ + + auto lastConnect = time_point(); + for(int i = 0; i < 1; i++){ + //identity = ts::Identity::createNew(); + //identity->improveSecurityLevel(9); + + //identity = new ts::Identity("4161411998Vginro7hE2a4haFhxRR5+Q1aknrR8Nn1iYAAUZldWVn8XZFVVUnUOVBFeMQJ9HHZaOVh3fEhLShwdXHkie2lza1gDWQltMwhROBNeVTQ2GS5EeFNbA2dBE3QvXU9jJ0VKMXd4YkpxU0tFQUlnYVZaeVVTTTYzSldSLzBRT3Nqd3hSelJBYmRpZ2xsaFhsVTF5L3hVYWgxWT0="); + //identity = new ts::Identity("8008VgvLYFzLLe4lE6oTKyMBPViPHzJVdCGVqAGc7VQQJXkcIRUZcCkFzMDJmHCYBMF1kWmdzRwlQBy4PT3Eie2lzVmUCd39bD3RnNC9gAyRQFh14Z0YGEHJHDg4nGkceNgdWBH9RAE1ZTjlqUUlnV1hUck5vZVdUQ21sNkVYeDNMUm80M0pMU0ZDdkRzWUorR0VkeDE3ZDJLST0="); + //identity = identity->createNew(); + //cout << "Improve" << endl; + //identity->improveSecurityLevelMultithreaded(10, 12); + //cout << "Using identity: " << identity->exportIdentity() << endl; + auto connection = new ts::connection::ServerConnection(); + + while(system_clock::now() - lastConnect < milliseconds(250)) usleep(1000); + lastConnect = system_clock::now(); + + string host = + "127.0.0.1" + //"51.255.133.2" + //"79.133.54.198" + //"51.255.133.6" + //"79.133.54.207" //GommeHD.net + //"79.133.54.202" //Rewi + //"79.133.54.210" //minesucht.net:9988 + //"91.134.112.225" //The Beast (NOT 3.1!) + //"79.133.54.198" //Grifer games:9180 + //"52.63.60.28" + //"87.106.252.164" + //"5.1.80.215" //Twerion + //"212.114.60.36" //Domies ts + //"54.36.90.72" + //"87.106.252.164" + //"84.200.62.248" + //"79.133.54.198" //Durchrasten.de:9122 + "" + ; + string port = "9987"; + if(argc > 2) { + host = argv[1]; + port = argv[2]; + } + + threads::Thread([connection, host, port, identity]() { + string error; + cout << host << ":" << port << endl; + if(!connection->connect(host, port, identity)){ + cerr << "Cant connect!" << endl; + goto sleep; + } + + if(!connection->handshake(error)){ + cerr << "Cant handschake: " << error << endl; + goto sleep; + } + + threads::self::sleep_for(seconds(10 + rand() % 10)); + sleep:; + //connection->disconnect(); + //delete connection; + }).detach(); + //connection->disconnect(); + usleep(1000 * 1000); + } + cout << "Done!" << endl; + + while(true) usleep(1000 * 2000); +} \ No newline at end of file diff --git a/client/proto/LicenseKey.proto b/client/proto/LicenseKey.proto new file mode 100644 index 0000000..e4f83de --- /dev/null +++ b/client/proto/LicenseKey.proto @@ -0,0 +1,17 @@ +syntax = "proto2"; + +package ts.proto.license.teamspeak; + +message LicenseData { //_ZTIN3com10teamspeak310accounting8protobuf19Signed_License_InfoE => Signed_License_Info + required bytes chain = 1; + required bytes root = 2; + required int32 slots = 3; + required int32 servers = 4; + required string type = 5; +} + +message License { + required LicenseData license = 1; + required bytes sign_chain = 2; + required bytes license_sign = 3; +} diff --git a/client/src/Identity.cpp b/client/src/Identity.cpp new file mode 100644 index 0000000..caf3ff9 --- /dev/null +++ b/client/src/Identity.cpp @@ -0,0 +1,218 @@ +// +// Created by wolverindev on 07.10.17. +// + +#include +#include +#include "misc/base64.h" +#include "Identity.h" + +#define ECC_TYPE_INDEX 5 + +using namespace std; + +static const char *TSKEY = + "b9dfaa7bee6ac57ac7b65f1094a1c155" + "e747327bc2fe5d51c512023fe54a2802" + "01004e90ad1daaae1075d53b7d571c30" + "e063b5a62a4a017bb394833aa0983e6e"; + +static int obfuscateInplace(char *data, uint32_t length) { + int dataSize = min((uint32_t) 100, length); + for (int i = 0; i < dataSize; i++) { + data[i] ^= TSKEY[i]; + } + + char hash[20]; + hash_state ctx; + if (sha1_init(&ctx) != CRYPT_OK) + { return -1; } + if (sha1_process(&ctx, (uint8_t*)data + 20, strlen(data + 20)) != CRYPT_OK) + { return -1; } + if (sha1_done(&ctx, (uint8_t*)hash) != CRYPT_OK) + { return -1; } + + for (int i = 0; i < 20; i++) { + data[i] ^= hash[i]; + } + + return 0; +} + +static int deObfuscateInplace(char *data, uint32_t length) { + char hash[20]; + hash_state ctx; + if (sha1_init(&ctx) != CRYPT_OK) + { return -1; } + if (sha1_process(&ctx, (uint8_t*)data + 20, strlen(data + 20)) != CRYPT_OK) + { return -1; } + if (sha1_done(&ctx, (uint8_t*)hash) != CRYPT_OK) + { return -1; } + + for (int i = 0; i < 20; i++) { + data[i] ^= hash[i]; + } + + int dataSize = min((uint32_t) 100, length); + for (int i = 0; i < dataSize; i++) { + data[i] ^= TSKEY[i]; + } + return 0; +} + +namespace ts { + Identity* Identity::createNew() { + auto result = new Identity(); + + prng_state rndState{}; + memset(&rndState, 0, sizeof(prng_state)); + int err; + + result->keyPair = new ecc_key; + + cout << " -> " << find_prng("sprng") << endl; + if((err = ecc_make_key_ex(&rndState, find_prng("sprng"), result->keyPair, <c_ecc_sets[ECC_TYPE_INDEX])) != CRYPT_OK) { + cerr << "Cant create a new identity (Keygen)" << endl; + cerr << "Message: " << error_to_string(err) << endl; + delete result; + return nullptr; + } + + return result; + } + + Identity::Identity(std::string asnStruct, int64_t keyOffset, int64_t lastCheckedOffset) { + this->keyOffset = keyOffset; + this->lastCheckedOffset = lastCheckedOffset; + importKey(asnStruct); + } + + Identity::Identity(std::string data) : Identity() { + int vindex = data.find('V'); + assert(vindex > 0); + + auto slevel = data.substr(0, vindex); + assert(slevel.find_first_not_of("0123456789") == std::string::npos); + this->keyOffset = stol(slevel); + + data = data.substr(vindex + 1); + data = base64::decode(data); + if(deObfuscateInplace((char *) data.data(), data.length()) < 0) { + cerr << "Cand decript identitry data" << endl; + return; + } + importKey(base64::decode(data)); + } + + Identity::Identity() { + this->keyOffset = 0; + this->lastCheckedOffset = 0; + this->keyPair = nullptr; + } + + Identity::~Identity() { + delete this->keyPair; + this->keyPair = nullptr; + } + + void Identity::importKey(std::string asnStruct) { + this->keyPair = new ecc_key; + int err; + if((err = ecc_import_ex((const unsigned char *) asnStruct.data(), asnStruct.length(), this->keyPair, <c_ecc_sets[ECC_TYPE_INDEX])) != CRYPT_OK){ + delete this->keyPair; + this->keyPair = nullptr; + + cerr << "Cant import identity from asn structure" << endl; + cerr << "Message: " << error_to_string(err) << endl; + return; + } + } + + std::string Identity::exportIdentity() { + string data = privateKey(); + obfuscateInplace((char *) data.data(), data.length()); + return to_string(this->lastValidKeyOffset()) + "V" + base64_encode(data); + } + + std::string Identity::publicKey() { + assert(this->keyPair); + + size_t bufferLength = 1028; + char buffer[bufferLength]; + ecc_export((unsigned char *) buffer, &bufferLength, PK_PUBLIC, this->keyPair); + + return base64_encode(string(buffer, bufferLength)); + } + + std::string Identity::privateKey() { + assert(this->keyPair); + + size_t bufferLength = 1028; + char buffer[bufferLength]; + ecc_export((unsigned char *) buffer, &bufferLength, PK_PRIVATE, this->keyPair); + + return base64_encode(string(buffer, bufferLength)); + } + + ecc_key& Identity::getPrivateKey() { + return *keyPair; + } + + #define MaxUlongString 20 + + bool Identity::improveSecurityLevel(int target) { + auto publicKey = this->publicKey(); + char hashBuffer[publicKey.length() + MaxUlongString]; + memcpy(hashBuffer, publicKey.data(), publicKey.length()); + + this->lastCheckedOffset = max(this->lastCheckedOffset, this->keyOffset); + int best = getSecurityLevel(hashBuffer, publicKey.length(), this->lastCheckedOffset); + while(true){ + if(best >= target) return true; + + int currentLevel = getSecurityLevel(hashBuffer, publicKey.length(), this->lastCheckedOffset); + if(currentLevel >= best){ + this->keyOffset = this->lastCheckedOffset; + best = currentLevel; + } + this->lastCheckedOffset++; + } + } + + int Identity::getSecurityLevel() { + auto length = publicKey().length(); + char hashBuffer[length + MaxUlongString]; + + auto publicKey = this->publicKey(); + memcpy(hashBuffer, publicKey.data(), publicKey.length()); + + return getSecurityLevel(hashBuffer, publicKey.length(), this->keyOffset); + } + + int Identity::getSecurityLevel(char *hashBuffer, size_t keyLength, int64_t offset) { + char numBuffer[MaxUlongString]; + int numLen = 0; + do { + numBuffer[numLen] = '0' + (offset % 10); + offset /= 10; + numLen++; + } while(offset > 0); + for(int i = 0; i < numLen; i++) + hashBuffer[keyLength + i] = numBuffer[numLen - (i + 1)]; + + char shaBuffer[SHA_DIGEST_LENGTH]; + SHA1((const unsigned char *) hashBuffer, keyLength + numLen, (unsigned char *) shaBuffer); + + //Leading zero bits + register int zeroBits = 0; + register int i; + for(i = 0; i < SHA_DIGEST_LENGTH; i++) + if(shaBuffer[i] == 0) zeroBits += 8; + else break; + if(i < SHA_DIGEST_LENGTH) + for(int bit = 0; bit < 8; bit++) + if((shaBuffer[i] & (1 << bit)) == 0) zeroBits++; + else break; + return zeroBits; + } +} \ No newline at end of file diff --git a/client/src/Identity.h b/client/src/Identity.h new file mode 100644 index 0000000..a6eb2e0 --- /dev/null +++ b/client/src/Identity.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +namespace ts { + class Identity { + public: + static Identity* createNew(); + + explicit Identity(std::string data); + Identity(std::string asnStruct,int64_t keyOffset,int64_t lastCheckedOffset); + ~Identity(); + + bool valid(){ return keyPair != nullptr; } + + std::string publicKey(); + std::string privateKey(); + std::string exportIdentity(); + + ecc_key* getKeyPair(){ + return keyPair; + } + + ecc_key& getPrivateKey(); + + bool improveSecurityLevel(int target); + bool improveSecurityLevelMultithreaded(int target, int nthread = 4, size_t nblockSize = 1000, size_t baseOffset = 0, bool verbose = false); + + int getSecurityLevel(); + + int64_t lastValidKeyOffset(){ return keyOffset; } + int64_t lastTestedKeyOffset(){ return lastCheckedOffset; } + private: + Identity(); + + int getSecurityLevel(char* hasBuffer, size_t keyLength, int64_t offset); + void importKey(std::string asn1); + + ecc_key* keyPair = nullptr; + size_t keyOffset; + size_t lastCheckedOffset; + }; +} \ No newline at end of file diff --git a/client/src/MultithreadedIdentity.cpp b/client/src/MultithreadedIdentity.cpp new file mode 100644 index 0000000..c9b8136 --- /dev/null +++ b/client/src/MultithreadedIdentity.cpp @@ -0,0 +1,189 @@ +// +// Created by wolverindev on 14.10.17. +// + +#include +#include +#include +#include +#include +#include +#include "misc/base64.h" +#include "Identity.h" + +#define ECC_TYPE_INDEX 5 + +#define USE_OPENSSL_SHA1 +using namespace std::chrono; +using namespace std; +namespace ts { + + inline int calculateSecutityLevel(uint8_t* hashBuffer, uint8_t* bufferToHash, int length){ + register int zeroBits = 0; + register int bit; + register int i; + + SHA1(bufferToHash, length, hashBuffer); + + //Leading zero bits + for(i = 0; i < SHA_DIGEST_LENGTH; i++) + if(hashBuffer[i] == 0) zeroBits += 8; + else break; + if(i < SHA_DIGEST_LENGTH) + for(bit = 0; bit < 8; bit++) + if((hashBuffer[i] & (1 << bit)) == 0) zeroBits++; + else break; + + return zeroBits; + } + + inline void increaseNumBuffer(char* buffer, int numOffset, int* length){ + int index = *length - 1; + + while(true){ + if(buffer[index] == '9'){ + if(index - 1 < numOffset) { + buffer[index] = '1'; + buffer[*length] = '0'; + *length += 1; + return; + } + buffer[index--] = '0'; + } else { + buffer[index] += 1; + return; + } + } + } + +#define MaxUlongString 20 + bool Identity::improveSecurityLevelMultithreaded(int target, int nthread, size_t nblockSize, size_t baseOffset, bool verbose) { + if(this->getSecurityLevel() >= target) + return false; + + vector threads; + + auto publicKey = this->publicKey(); + + size_t currentBlockIndex = max(max(this->lastCheckedOffset, this->keyOffset), baseOffset); + threads::Mutex blockIndexLock; + + auto activeDigging = new bool(true); + threads::Thread* thread; + for(int threadId = 0; threadId < nthread; threadId++){ + thread = new threads::Thread([&](){ + size_t offset = 0; + size_t endOffset = 0; + size_t bestOffset = 0; + + char shaHashBuffer[SHA_DIGEST_LENGTH]; + char hashBuffer[publicKey.length() + MaxUlongString]; + memcpy(hashBuffer, publicKey.data(), publicKey.length()); + +#ifndef SLOW + //Setup some stuff + size_t pubKeyLength = publicKey.length(); + +#endif + while(*activeDigging){ + //Get next range + blockIndexLock.lock(); + offset = currentBlockIndex; + endOffset = currentBlockIndex + nblockSize; + currentBlockIndex += nblockSize; + blockIndexLock.unlock(); + + int bestLevel = this->getSecurityLevel(hashBuffer, publicKey.length(), this->keyOffset); +#ifdef SLOW + while(offset < endOffset){ + int currentLevel = getSecurityLevel(hashBuffer, publicKey.length(), offset); + if(currentLevel >= best){ + bestOffset = offset; + best = currentLevel; + } + offset++; + } + blockIndexLock.lock(); + if(best > this->getSecurityLevel()){ + if(verbose) cout << "Improved -> " << best << "/" << target << endl; + this->lastCheckedOffset = bestOffset; + this->keyOffset = bestOffset; + cout << "Got level: " << best << endl; + + if(best >= target){ + cout << "Done!" << endl; + *activeDigging = false; + } + } + blockIndexLock.unlock(); + if(verbose) cout << "round done: highest -> " << best << endl; +#else + + string strStartOffset = to_string(offset); + ssize_t roundsLeft = nblockSize; + + auto hashBufferLength = static_cast(publicKey.length() + strStartOffset.length()); + memcpy(&hashBuffer[pubKeyLength], strStartOffset.c_str(), strStartOffset.length()); + + while(roundsLeft-- >= 0){ + auto level = calculateSecutityLevel(reinterpret_cast(shaHashBuffer), reinterpret_cast(hashBuffer), hashBufferLength); + if(level > bestLevel){ + { + threads::MutexLock l(blockIndexLock); + + auto strOffset = string(&hashBuffer[pubKeyLength], hashBufferLength - pubKeyLength); + bestOffset = stoull(strOffset); + auto got = this->getSecurityLevel(); + if(got >= level) { + cout << "Already got bedder level! (" << got << ")" << endl; + bestLevel = got; + continue; + } + + if(verbose) cout << "Improved -> " << level << "/" << target << " [" << strOffset << "]" << endl; + this->lastCheckedOffset = bestOffset; + this->keyOffset = bestOffset; + bestLevel = level; + + if(bestLevel >= target){ + cout << "Done! (" << bestLevel << ")" << endl; + *activeDigging = false; + } + } + } + increaseNumBuffer(hashBuffer, pubKeyLength + 1, &hashBufferLength); + } +#endif + } + }); + thread->name("IdentityHashDigger #" + to_string(threadId)); + threads.push_back(thread); + } + + if(verbose){ + thread = new threads::Thread([&](){ + auto lastIndex = currentBlockIndex; + while(*activeDigging){ + for(int count = 0; count < 1000 && *activeDigging; count++) + usleep(1000); + + blockIndexLock.lock(); + cout << "Current level = " << this->getSecurityLevel() << " at " << this->keyOffset << endl; + cout << "Current offset index: " << currentBlockIndex << " block size: " << nblockSize << endl; + cout << "speed: " << (currentBlockIndex - lastIndex) / 1000 / 1000.f << " Mio. attemps/sec" << endl; + lastIndex = currentBlockIndex; + blockIndexLock.unlock(); + } + }); + thread->name("Status printer"); + threads.push_back(thread); + } + + for(auto elm : threads){ + if(elm->state() == threads::ThreadState::RUNNING) + elm->join(); + delete elm; + } + return true; + } +} \ No newline at end of file diff --git a/client/src/TinySHA1.hpp.h b/client/src/TinySHA1.hpp.h new file mode 100644 index 0000000..a53decd --- /dev/null +++ b/client/src/TinySHA1.hpp.h @@ -0,0 +1,196 @@ +/* + * + * TinySHA1 - a header only implementation of the SHA1 algorithm in C++. Based + * on the implementation in boost::uuid::details. + * + * SHA1 Wikipedia Page: http://en.wikipedia.org/wiki/SHA-1 + * + * Copyright (c) 2012-22 SAURAV MOHAPATRA + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifndef _TINY_SHA1_HPP_ +#define _TINY_SHA1_HPP_ +#include +#include +#include +#include +namespace sha1 +{ + class SHA1 + { + public: + typedef uint32_t digest32_t[5]; + typedef uint8_t digest8_t[20]; + inline static uint32_t LeftRotate(uint32_t value, size_t count) { + return (value << count) ^ (value >> (32-count)); + } + SHA1(){ reset(); } + virtual ~SHA1() {} + SHA1(const SHA1& s) { *this = s; } + const SHA1& operator = (const SHA1& s) { + memcpy(m_digest, s.m_digest, 5 * sizeof(uint32_t)); + memcpy(m_block, s.m_block, 64); + m_blockByteIndex = s.m_blockByteIndex; + m_byteCount = s.m_byteCount; + return *this; + } + SHA1& reset() { + m_digest[0] = 0x67452301; + m_digest[1] = 0xEFCDAB89; + m_digest[2] = 0x98BADCFE; + m_digest[3] = 0x10325476; + m_digest[4] = 0xC3D2E1F0; + m_blockByteIndex = 0; + m_byteCount = 0; + return *this; + } + SHA1& processByte(uint8_t octet) { + this->m_block[this->m_blockByteIndex++] = octet; + ++this->m_byteCount; + if(m_blockByteIndex == 64) { + this->m_blockByteIndex = 0; + processBlock(); + } + return *this; + } + SHA1& processBlock(const void* const start, const void* const end) { + const uint8_t* begin = static_cast(start); + const uint8_t* finish = static_cast(end); + while(begin != finish) { + processByte(*begin); + begin++; + } + return *this; + } + SHA1& processBytes(const void* const data, size_t len) { + const uint8_t* block = static_cast(data); + processBlock(block, block + len); + return *this; + } + const uint32_t* getDigest(digest32_t digest) { + size_t bitCount = this->m_byteCount * 8; + processByte(0x80); + if (this->m_blockByteIndex > 56) { + while (m_blockByteIndex != 0) { + processByte(0); + } + while (m_blockByteIndex < 56) { + processByte(0); + } + } else { + while (m_blockByteIndex < 56) { + processByte(0); + } + } + processByte(0); + processByte(0); + processByte(0); + processByte(0); + processByte( static_cast((bitCount>>24) & 0xFF)); + processByte( static_cast((bitCount>>16) & 0xFF)); + processByte( static_cast((bitCount>>8 ) & 0xFF)); + processByte( static_cast((bitCount) & 0xFF)); + + memcpy(digest, m_digest, 5 * sizeof(uint32_t)); + return digest; + } + const uint8_t* getDigestBytes(digest8_t digest) { + digest32_t d32; + getDigest(d32); + size_t di = 0; + digest[di++] = ((d32[0] >> 24) & 0xFF); + digest[di++] = ((d32[0] >> 16) & 0xFF); + digest[di++] = ((d32[0] >> 8) & 0xFF); + digest[di++] = ((d32[0]) & 0xFF); + + digest[di++] = ((d32[1] >> 24) & 0xFF); + digest[di++] = ((d32[1] >> 16) & 0xFF); + digest[di++] = ((d32[1] >> 8) & 0xFF); + digest[di++] = ((d32[1]) & 0xFF); + + digest[di++] = ((d32[2] >> 24) & 0xFF); + digest[di++] = ((d32[2] >> 16) & 0xFF); + digest[di++] = ((d32[2] >> 8) & 0xFF); + digest[di++] = ((d32[2]) & 0xFF); + + digest[di++] = ((d32[3] >> 24) & 0xFF); + digest[di++] = ((d32[3] >> 16) & 0xFF); + digest[di++] = ((d32[3] >> 8) & 0xFF); + digest[di++] = ((d32[3]) & 0xFF); + + digest[di++] = ((d32[4] >> 24) & 0xFF); + digest[di++] = ((d32[4] >> 16) & 0xFF); + digest[di++] = ((d32[4] >> 8) & 0xFF); + digest[di++] = ((d32[4]) & 0xFF); + return digest; + } + + protected: + void processBlock() { + uint32_t w[80]; + for (size_t i = 0; i < 16; i++) { + w[i] = (m_block[i*4 + 0] << 24); + w[i] |= (m_block[i*4 + 1] << 16); + w[i] |= (m_block[i*4 + 2] << 8); + w[i] |= (m_block[i*4 + 3]); + } + for (size_t i = 16; i < 80; i++) { + w[i] = LeftRotate((w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16]), 1); + } + + uint32_t a = m_digest[0]; + uint32_t b = m_digest[1]; + uint32_t c = m_digest[2]; + uint32_t d = m_digest[3]; + uint32_t e = m_digest[4]; + + for (std::size_t i=0; i<80; ++i) { + uint32_t f = 0; + uint32_t k = 0; + + if (i<20) { + f = (b & c) | (~b & d); + k = 0x5A827999; + } else if (i<40) { + f = b ^ c ^ d; + k = 0x6ED9EBA1; + } else if (i<60) { + f = (b & c) | (b & d) | (c & d); + k = 0x8F1BBCDC; + } else { + f = b ^ c ^ d; + k = 0xCA62C1D6; + } + uint32_t temp = LeftRotate(a, 5) + f + e + k + w[i]; + e = d; + d = c; + c = LeftRotate(b, 30); + b = a; + a = temp; + } + + m_digest[0] += a; + m_digest[1] += b; + m_digest[2] += c; + m_digest[3] += d; + m_digest[4] += e; + } + private: + digest32_t m_digest; + uint8_t m_block[64]; + size_t m_blockByteIndex; + size_t m_byteCount; + }; +} +#endif \ No newline at end of file diff --git a/client/src/protocol/Connection.cpp b/client/src/protocol/Connection.cpp new file mode 100644 index 0000000..6945b0b --- /dev/null +++ b/client/src/protocol/Connection.cpp @@ -0,0 +1,454 @@ +// +// Created by wolverindev on 07.10.17. +// + +#include +#include "Connection.h" +#include "misc/base64.h" +#include + +#include +#include +#include +#include +#include + +using namespace std; +using namespace ts; +using namespace ts::connection; +using namespace ts::protocol; + +ServerConnection::ServerConnection() { + cryptionHandler = new CryptionHandler(); + cryptionHandler->reset(); + + compressionHandler = new CompressionHandler(); + + readQueue = (buffer::SortedBufferQueue **) malloc(16 * sizeof(void*)); + for(int i = 0; i < 16; i++) { + auto type = ts::protocol::PacketTypeInfo::fromid(i); + if(type != PacketTypeInfo::Undefined){ + readQueue[i] = new buffer::SortedBufferQueue(ts::protocol::PacketTypeInfo::fromid(i), PacketTypeInfo::Command != type); //Ignore command low + } else { + readQueue[i] = nullptr; + } + } +} + +ServerConnection::~ServerConnection() { + for(int i = 0; i < 16; i++) + if(readQueue[i]) + delete readQueue[i]; + free(readQueue); + this->rwThread->join(); +} + +static int sourcePort = 50000; + +void ServerConnection::disconnect() { + //this->rwThread->cancel(); + //this->handleThread->cancel(); + this->connected = false; + if(this->socket) this->socket->close(); +} + +bool ServerConnection::connect(std::string host, std::string port, Identity *identity) { + this->clientIdentity = identity; + + memset(&remoteAddress, 0, sizeof(remoteAddress)); + remoteAddress.sin_family = AF_INET; + remoteAddress.sin_port = htons((uint16_t) std::stoi(port)); + remoteAddress.sin_addr.s_addr = inet_addr(host.c_str()); +#ifdef NoQt + /* + this->socketfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + int allow = 1; + setsockopt(this->socketfd, SOL_SOCKET, SO_REUSEADDR, &allow, sizeof(int)); + + memset(&localAddress, 0, sizeof(localAddress)); + localAddress.sin_family = AF_INET; + localAddress.sin_addr.s_addr = htonl (INADDR_ANY); + localAddress.sin_port = htons (sourcePort++); + ::connect(this->socketfd, (const sockaddr *) &remoteHost, sizeof(this->remoteAddress)); + //bind(this->socketfd, (struct sockaddr *) &localAddress, sizeof(localAddress)); + + */ + this->socket = new UdpSocket; + if(!this->socket->setup(&remoteAddress)){ + cerr << "Invalid socket setup" << endl; + } +#else + +#endif + this->rwThread = new threads::Thread(THREAD_SAVE_OPERATIONS, [&]() { +#ifndef NoQt + this->qtSocket = new QUdpSocket(); + + QObject::connect(qtSocket,SIGNAL(bytesWritten(qint64)),this,SLOT(bytesWritten(qint64))); + QObject::connect(this->qtSocket, SIGNAL(readyRead()), this, SLOT(attempDatagramRead())); + this->qtSocket->bind(QHostAddress::Any, 23111); + + this->socketfd = qtSocket->socketDescriptor(); + cout << "Sock fd: " << this->socketfd << endl; +#endif + /* + auto cthread = QThread::currentThread(); + cout << "ex" << endl; + runOnThread(qtSocket->thread(), [&](){ + cout << "try" << endl; + qtSocket->moveToThread(cthread); + cout << "Moved" << endl; + }); + cout << "Start rw" << endl; + */ + + this->rwExecutor(); + }); + + /* + this->handleThread = new threads::Thread([&]() { + this->handleExecutor(); + }); + */ + return true; +} + +#ifndef NoQt +void ServerConnection::bytesWritten(qint64 b) { + cout << "written " << b << endl; +} + +void ServerConnection::attempDatagramRead() { + cout << "Data " << endl; +} +#endif + +//this->socket->socketDescriptor() +void ServerConnection::rwExecutor() { + pollfd pollData = {this->socket->getSocketDescriptor(), POLLRDHUP | POLLIN | POLLOUT, 0}; + buffer::RawBuffer readBuffer(512); + std::shared_ptr readedPacket; + while (socket->getSocketDescriptor() > 0 && this->connected) { + int rfds = poll(&pollData, 1, -1); + bool select = false; + if(rfds == 0) { + usleep(5 * 1000);; + continue; + } else if(rfds < 0) { + break; + } + if (pollData.revents & POLLRDHUP || pollData.revents & POLLHUP) { + select = 1; + cerr << "Connection hang up!" << endl; + return; + } + + if (pollData.revents & POLLIN) { + select = 1; + ssize_t readedBytes = -1; +#ifdef NoQt + readedBytes = socket->read(readBuffer.buffer, readBuffer.length); + #ifdef DEBUG + cout << "Read bytes (" << readedBytes << ")" << endl; + #endif +#else + QHostAddress senderAddr; + quint16 senderPort; + readedBytes = this->qtSocket->readDatagram(readBuffer.buffer, readBuffer.length, &senderAddr, &senderPort); +#endif + if (readedBytes < 0) { + cout << "fatal read error: " << errno << "/" << strerror(errno) << endl; + return; + } + + readedPacket = std::make_shared(pipes::buffer_view(readBuffer.buffer, readedBytes)); + if(!preProgressPacket(readedPacket)){ + cerr << "Invalid packet preprocess!" << endl; + readedPacket = nullptr; + goto exitRead; + } + + if(readedPacket->type().type() < 0 || readedPacket->type().type() > 16){ + cerr << "Invalid packet id!" << endl; + readedPacket = nullptr; + goto exitRead; + } + //Deserelize packet + this->bufferQueueLock.lock(); + if(!this->readQueue[readedPacket->type().type()]->push_pack(readedPacket)){ + //TODO error handling + cout << "pkId: " << be2le16((char*) readedPacket->data().data_ptr()) << " -> " << readedPacket->type().name() << endl; + } + this->bufferQueueLock.unlock(); + while(this->handleNextPacket()); + exitRead:; + } + + if (pollData.revents & POLLOUT) { + this->bufferQueueLock.lock(); + if (!this->writeQueue.empty()) { + select = 1; + buffer::RawBuffer buffer = this->writeQueue.front(); + +#ifdef NoQt + auto res = this->socket->write(buffer.buffer, buffer.length); + if (res == -1) { + cout << "having write error: " << errno << "/" << strerror(errno) << " -> " << buffer.length << endl; + } + //cout << string() + "Write: " + PacketType::fromid(buffer.type()).name() << endl; +#else + this->qtSocket->writeDatagram(buffer.buffer, buffer.length, QHostAddress("localhost"), htons(this->remoteAddress.sin_port)); +#endif + + /* + if(!PacketTypeInfo::fromid(buffer.type()).requireAcknowledge()){ //Than we need a ack! + //Wait for acknowlage + this->acknowlageQueueLock.lock(); + this->acknowlageQueue.push_back(buffer); + this->acknowlageQueueLock.unlock(); + } + */ + + this->writeQueue.pop_front(); + } + this->bufferQueueLock.unlock(); + } + if (!select) { + usleep(5 * 10000); + continue; + } + } + cerr << "rw loop broken!" << endl; +} + +bool ServerConnection::preProgressPacket(std::shared_ptr packet){ + packet->setEncrypted(!packet->hasFlag(PacketFlag::Unencrypted)); + packet->setCompressed(packet->hasFlag(PacketFlag::Compressed)); + packet->setFragmentedEntry(packet->hasFlag(PacketFlag::Fragmented)); + + if(packet->type() == PacketTypeInfo::Init1){ + + } + if (packet->isEncrypted()) { + string error = "success"; + if (!cryptionHandler->progressPacketIn(packet.get(), error, false)) { + cerr << "Cant decript packet! Message: " << error << endl; + cerr << "Dropping it!" << endl; + return false; + } + } + +#ifdef DEBUG + cout << "[IN] Packet type -> " << packet->type().name() << " flags " << packet->flags() << " Length: " << packet->data().length() << endl; +#endif + if(packet->type() == PacketTypeInfo::Command || packet->type() == PacketTypeInfo::CommandLow){ //needs an acknowledge + sendAcknowledge(packet->packetId(), packet->type() == PacketTypeInfo::CommandLow); + } + return true; +} + +//TODO right packet recive order! +void ServerConnection::handleExecutor() { + shared_ptr packet = nullptr; + string error = "success"; + while(this->connected){ + while(this->handleNextPacket()); + usleep(10 * 1000); + } +} + +bool ServerConnection::handleNextPacket() { + shared_ptr packet = nullptr; + string error = "success"; + + if(this->autoHandle){ + handleQueueLock.lock(); + if(!this->handleQueue.empty()) { + packet = this->handleQueue.front(); + this->handleQueue.pop_front(); + } + handleQueueLock.unlock(); + + if(packet){ + if(packet->type() == PacketTypeInfo::Ack || packet->type() == PacketTypeInfo::AckLow){ + handlePacketAck(packet); + } else if(packet->type() == PacketTypeInfo::Command || packet->type() == PacketTypeInfo::CommandLow){ + handlePacketCommand(packet); + } else if(packet->type() == PacketTypeInfo::Ping || packet->type() == PacketTypeInfo::Pong){ + handlePacketPing(packet); + } else if(packet->type() == PacketTypeInfo::Voice || packet->type() == PacketTypeInfo::VoiceWhisper){ + handlePacketVoice(packet); + } + return true; + } + } + + for(int index = 0; index < 16; index++){ + if(this->readQueue[index]) { + if(this->readQueue[index]->available() > 0){ + auto npacket = this->readQueue[index]->peekNext(0); + packet = make_shared(npacket->buffer()); + packet->setEncrypted(npacket->isEncrypted()); + packet->setCompressed(npacket->isCompressed()); + packet->setFragmentedEntry(npacket->isFragmentEntry()); + break; + } + } + + } + if(!packet) return false; + if(packet->isFragmentEntry()){ + packet->setFragmentedEntry(false); + int deltaPacketIndex = 0; + while(this->connected){ + std::shared_ptr nextElm = this->readQueue[packet->type().type()]->peekNext(++deltaPacketIndex); + if(!nextElm) + return false; + + if(!nextElm) { + cerr << "Dropped fragment?" << endl; + packet = nullptr; + break; + } + packet->append_data({nextElm->data()}); + if(nextElm->hasFlag(protocol::PacketFlag::Fragmented)) break; //Tail end + nextElm = nullptr; + } + this->readQueue[packet->type().type()]->pop_packets(deltaPacketIndex); + } + this->readQueue[packet->type().type()]->pop_packets(1); + + if(packet->type() != PacketTypeInfo::Init1 && !this->compressionHandler->progressPacketIn(packet.get(), error)){ + cerr << "Cant decompress packet! (" << error << ")" << endl; + packet = nullptr; + return true; + } + +#if defined(DEBUG_PACKET_LOG) + cout << "Parsed packet " << packet->type().name() << " with id " << packet->packetId() << ". Data:" << endl; + hexDump((void *) packet->data().data_ptr(), packet->data().length(), 16, 8, [](std::string line) { + cout << "[IN] " << line << endl; + }); +#endif + handleQueueLock.lock(); + this->handleQueue.push_back(packet); + handleQueueLock.unlock(); + return true; +} + +using namespace std::chrono; +std::shared_ptr ServerConnection::readNextPacket(bool block) { + auto start = system_clock::now(); + attempGet: + if(system_clock::now() - start > seconds(5)) return nullptr; + + this->handleQueueLock.lock(); + if (this->handleQueue.empty()) { + this->handleQueueLock.unlock(); + if (!block) return nullptr; + usleep(5 * 1000); + goto attempGet; + } + + std::shared_ptr packet = std::move(this->handleQueue.front()); + this->handleQueue.pop_front(); + this->handleQueueLock.unlock(); + return packet; +} + +bool ServerConnection::setupSharedSecret(std::string alpha, std::string beta, std::string sharedKey, std::string &error) { + return this->cryptionHandler->setupSharedSecret(alpha, beta, sharedKey, error); +} + +//Packet splitting not working correctly! (On clientinit dosnt wait for the second) +void ServerConnection::sendPacket(ts::protocol::ClientPacket &packet) { + int maxDataLength = 500 - packet.header().length(); + + if(packet.data().length() > maxDataLength){ + string error; +/* + packet.enableFlag(PacketFlag::Compressed); + if(!this->compressionHandler->progressPacketOut(&packet, error)){ + cerr << "Compress error!" << endl; + return; + } + packet.enableFlag(PacketFlag::Compressed); +*/ + if(packet.data().length() > maxDataLength){ + std::vector> siblings; + siblings.reserve(8); + + + { //Split packets + auto buffer = packet.data(); + + const auto max_length = packet.type().max_length(); + while(buffer.length() > max_length * 2) { + siblings.push_back(make_shared(packet.type(), buffer.view(0, max_length))); + buffer = buffer.range(max_length); + } + + if(buffer.length() > max_length) { //Divide rest by 2 + siblings.push_back(make_shared(packet.type(), buffer.view(0, buffer.length() / 2))); + buffer = buffer.range(buffer.length() / 2); + } + siblings.push_back(make_shared(packet.type(), buffer)); + + for(const auto& frag : siblings) { + frag->setFragmentedEntry(true); + frag->enableFlag(PacketFlag::NewProtocol); + } + } + + for(const auto& entry : siblings) + this->sendPacket(*entry); + return; + } + } + + if(!packet.memory_state.id_branded) + packet.applyPacketId(idManager); + packet.clientId(this->clientId); + + string error = "success"; + if (!this->cryptionHandler->progressPacketOut(&packet, error, false)) { + cerr << "Invalid crypt -> " << error << endl; + return; + } + + buffer::RawBuffer buffer(packet.buffer().length()); + + memcpy(&buffer.buffer[0], packet.buffer().data_ptr(), packet.buffer().length()); + + this->bufferQueueLock.lock(); + this->writeQueue.push_back(buffer); +#if defined(DEBUG_PACKET_LOG) + cout << "Send packet " << packet.type().name() << " fragmented -> " << packet.isFragmentEntry() << " length " << packet.data().length() << " flags " << packet.flags() << " ID: " << packet.packetId() << endl; + hexDump(buffer.buffer, buffer.length, buffer.length, buffer.length); +#endif + this->bufferQueueLock.unlock(); +} + +void ServerConnection::sendCommand(ts::Command command, bool low) { + auto data = command.build(); + protocol::ClientPacket pkt(low ? protocol::PacketTypeInfo::CommandLow : protocol::PacketTypeInfo::Command, pipes::buffer_view{(void*) data.data(), data.length()}); +#ifdef DEBUG + cout << "[Client -> Server][" << pkt.type().name() << "] " << pkt.data() << endl; +#endif + if(!low) pkt.enableFlag(PacketFlag::NewProtocol); + sendPacket(pkt); +} + +void ServerConnection::sendAcknowledge(uint16_t packetId, bool low) { + if(breakAck) return; + char buffer[2]; + le2be16(packetId, buffer); + protocol::ClientPacket pkt(low ? protocol::PacketTypeInfo::AckLow : protocol::PacketTypeInfo::Ack, pipes::buffer_view(buffer, 2)); +#ifdef DEBUG + cout << "Sending packet acknowledge for " << packetId << " (Encrypt: " << encriptAck << ")" << endl; +#endif + if(!encriptAck) + pkt.enableFlag(PacketFlag::Unencrypted); + if(!low) pkt.toggle(protocol::PacketFlag::NewProtocol, true); + sendPacket(pkt); +} \ No newline at end of file diff --git a/client/src/protocol/Connection.h b/client/src/protocol/Connection.h new file mode 100644 index 0000000..519f396 --- /dev/null +++ b/client/src/protocol/Connection.h @@ -0,0 +1,139 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NoQt +#ifndef NoQt + #include +#endif + +#define DEBUG_PACKET_LOG +//#define LOG_CMD +namespace ts { + namespace connection { + namespace ConnectionState { + enum ConnectionState { + UNCONNECTED, + LLHANDSCHAKE, + HANDSCHAKE, + CONNECTED, + DISCONNECTED + }; + } + + class ServerConnection +#ifndef NoQt +: public QObject { + Q_OBJECT +#else +{ +#endif + public: + ServerConnection(); + ~ServerConnection(); + + bool connect(std::string host, std::string port, Identity* identity); + void disconnect(); + + ConnectionState::ConnectionState getConnectionState(){ return cstate; } + + bool handshake(std::string &errorMessage); + bool handshakeNew(Command &cmd, const std::string& alpha, std::string &errorMessage); + + void sendPacket(ts::protocol::ClientPacket& packet); + void sendCommand(ts::Command command, bool low = false); + void sendAcknowledge(uint16_t packetId, bool low = false); + + std::shared_ptr readNextPacket(bool block = true); + + uint16_t getClientId(){ + return this->clientId; + } + + void setClientId(uint16_t id){ + this->clientId = id; + } + +#ifndef NoQt + public slots: + void attempDatagramRead(); + void bytesWritten(qint64); +#endif + private: + bool encriptAck = false; + + bool preProgressPacket(std::shared_ptr packet); + + void rwExecutor(); + void handleExecutor(); + bool handleNextPacket(); + + bool setupSharedSecret(std::string alpha, std::string beta, std::string sharedKey, std::string& error); + + + void handlePacketPing(std::shared_ptr packet); + void handlePacketCommand(std::shared_ptr packet); + void handlePacketAck(std::shared_ptr packet); + void handlePacketVoice(std::shared_ptr packet); + + bool connected = true; + + sockaddr_in remoteAddress; + sockaddr_in localAddress; + + + UdpSocket* socket; + threads::Thread* rwThread = nullptr; + std::deque writeQueue; +#ifndef NoQt + QUdpSocket* qtSocket = nullptr; +#endif + + threads::Mutex bufferQueueLock; + buffer::SortedBufferQueue** readQueue = nullptr; + + //std::deque readQueue; + + std::deque acknowlageQueue; + threads::Mutex acknowlageQueueLock; + + protocol::PacketIdManager idManager; + threads::Thread* handleThread = nullptr; + std::list> handleQueue; //Parsed packets + threads::Mutex handleQueueLock; + bool autoHandle = false; + + ts::connection::CryptionHandler* cryptionHandler = nullptr; + ts::connection::CompressionHandler* compressionHandler = nullptr; + + std::string remoteHost; + uint16_t remotePort; + + Identity* clientIdentity; + + ConnectionState::ConnectionState cstate = ConnectionState::UNCONNECTED; + + + bool breakAck = false; + /** + * TS3 Client data + */ + + uint16_t clientId = 0; + + std::deque channels; + }; + } +} \ No newline at end of file diff --git a/client/src/protocol/ConnectionHandschake.cpp b/client/src/protocol/ConnectionHandschake.cpp new file mode 100644 index 0000000..d94de30 --- /dev/null +++ b/client/src/protocol/ConnectionHandschake.cpp @@ -0,0 +1,361 @@ +// +// Created by wolverindev on 08.10.17. +// + +#include +#include +#include +#include "Connection.h" +#include "misc/base64.h" +#include "misc/endianness.h" + +using namespace std; +using namespace std::chrono; +using namespace ts; +using namespace ts::connection; +using namespace ts::protocol; + +const int InitVersionLength = 4; +const uint8_t InitVersion[InitVersionLength] = {0x09, 0x83, 0x8C, 0xCF}; + +/** + * Maybe memset to 0 for security? + */ +#define RESET_DATA \ +bufferIndex = 0; \ +delete pkt; \ +pkt = nullptr; + + +inline ClientPacket *solvePuzzle(shared_ptr response, Identity *, std::string &); + +inline std::string toString(mp_int* num){ + char buffer[2048]; + memset(buffer, 0, 2048); + auto len = mp_todecimal(num, buffer); + return string(buffer); +} + + +extern void hexdump(std::ostream& outs, const std::string& s, size_t line_len = 16); +bool ServerConnection::handshake(std::string &errorMessage) { + //setup the init mac + /** + * Low level + */ + ts::protocol::ClientPacket *pkt; + shared_ptr response; + int maxBufferSize = 512; + size_t bufferIndex = 0; + uint8_t buffer[maxBufferSize]; + memset(buffer, 0, maxBufferSize); + int err = 0; + string error = "success"; + + beginCoocie: + memcpy(buffer, InitVersion, InitVersionLength); + bufferIndex += InitVersionLength; + buffer[bufferIndex++] = 0x00; //Login state + + auto millis = duration_cast(system_clock::now().time_since_epoch()).count(); + memcpy(&buffer[bufferIndex], &millis, 4); + bufferIndex += 4; + //generate the alpha key + for (int i = 0; i < 4; i++) buffer[bufferIndex++] = (uint8_t) std::rand(); + bufferIndex += 8; //Reserved bytes + + pkt = new ts::protocol::ClientPacket(ts::protocol::PacketTypeInfo::Init1, pipes::buffer_view((void *) buffer, bufferIndex)); + pkt->clientId(0); + pkt->toggle(ts::protocol::PacketFlag::Unencrypted, true); + pkt->applyPacketId(101, 0); + this->sendPacket(*pkt); + RESET_DATA; + + response = readNextPacket(); + if (!response) { + errorMessage = "could not get a valid response!"; + return false; + } + if (response->type() != protocol::PacketTypeInfo::Init1) { + errorMessage = "invalid response type. Got: " + response->type().name(); + return false; + } + if (response->data()[0] != 1) { + errorMessage = "iInvalid requested login type (" + to_string((int) response->data()[0]) + " == 1)"; + return false; + } + + //the second request of the manager + memcpy(buffer, InitVersion, InitVersionLength); + bufferIndex += InitVersionLength; + buffer[bufferIndex++] = 0x02; //Login state + if(response) + memcpy(&buffer[bufferIndex], response->data().string().substr(1, 16).data(), 16); + bufferIndex += 16; //Servers 16 bytes + if(response) + memcpy(&buffer[bufferIndex], response->data().string().substr(17, 4).data(), 4); + bufferIndex += 4; //My own 16 bytes, reversed + + pkt = new ts::protocol::ClientPacket(ts::protocol::PacketTypeInfo::Init1, pipes::buffer_view((void *) buffer, bufferIndex)); + pkt->clientId(0); + pkt->toggle(ts::protocol::PacketFlag::Unencrypted, true); + pkt->applyPacketId(101, 0); + this->sendPacket(*pkt); + RESET_DATA; + + //We got the RSA challenge + response = readNextPacket(); + if (!response) { + errorMessage = "could not get a valid response!"; + return false; + } + if (response->type() != protocol::PacketTypeInfo::Init1) { + errorMessage = "invalid response type"; + return false; + } + if (response->data()[0] != 3) { + if(response->data()[0] == 127) { + cout << "COOCIE RESET!" << endl; + goto beginCoocie; + } + hexdump(cout, response->data().string()); + errorMessage = "Invalid requested login type (" + to_string((int) response->data()[0]) + " == 3 | unencripted -> " + (response->hasFlag(PacketFlag::Unencrypted) ? "true" : "false") + ")"; + return false; + } + + //Generate puzzel response + std::string alpha; + pkt = solvePuzzle(response, this->clientIdentity, alpha); + pkt->applyPacketId(101, 0); + this->sendPacket(*pkt); + RESET_DATA; + + cout << "manager init done" << endl; + this->encriptAck = true; + + response = readNextPacket(); + if (!response) { + errorMessage = "could not get a valid response!"; + return false; + } + if (response->type() != protocol::PacketTypeInfo::Command) { + errorMessage = "invalid response type: " + response->type().name(); + return false; + } + + + auto command = response->asCommand(); + if (command.getCommand().compare("initivexpand") != 0) { + // errorMessage = "invalid response command. Got: " + command.getCommand() + " Expected: initivexpand"; + return this->handshakeNew(command, alpha, errorMessage); + } + + //std::string alpha = base64::decode(command[0]["alpha"]); + std::string beta = base64::decode(command[0]["beta"]); + std::string omega = base64::decode(command[0]["omega"]); //Remotes public key + cout << "RESPONSE! -> " << command.build() << endl; + //Read public key + ecc_key remotePublicKey{}; + if ((err = ecc_import((const unsigned char *) omega.data(), omega.length(), &remotePublicKey)) != CRYPT_OK) { + errorMessage = "ecc_import(...) returned " + to_string(err) + "/" + error_to_string(err); + return false; + } + + if(strcmp(remotePublicKey.dp->name, "ECC-256") != 0){ + errorMessage = "invalid imported public key! Curve found " + string(remotePublicKey.dp->name); + return false; + } + + size_t sharedSecretLength = 32; + char sharedSecret[sharedSecretLength]; + + if ((err = ecc_shared_secret(clientIdentity->getKeyPair(), &remotePublicKey, (unsigned char *) sharedSecret, &sharedSecretLength)) != CRYPT_OK) { + errorMessage = "ecc_shared_secret(...) returned " + to_string(err) + "/" + error_to_string(err); + return false; + } + + if (!setupSharedSecret(alpha, beta, string(sharedSecret, sharedSecretLength), error)) { + errorMessage = "setupSharedSecret(...) failed: " + error; + return false; + } + + //this->readQueue[PacketType::Command.type()]->reset(); + + //TS 3.1 + /* + response = readNextPacket(); + if (!response) { + errorMessage = "could not get a valid response!"; + return false; + } + if (response->type() != protocol::PacketType::Command) { + errorMessage = "invalid response type: " + response->type().name(); + return false; + } + command = response->asCommand(); + cout << "Having initiv2 -> " << response->data() << endl; + */ + + this->idManager.nextPacketId(PacketTypeInfo::Command); + Command clientinit("clientinit"); + //94ec66de-5940-4e38-b002-970df0cf6c94,62444179-0d99-42ba-a45c-c6b1557d079a,d95f9901-c42d-4bac-8849-7164fd9e2310 + //clientinit["client_badges"] = "badges=450f81c1-ab41-4211-a338-222fa94ed157,c9e97536-5a2d-4c8e-a135-af404587a472,94ec66de-5940-4e38-b002-970df0cf6c94"; //,62444179-0d99-42ba-a45c-c6b1557d079a + clientinit["client_nickname"] = "Wolf C++ XXXX"; + clientinit["client_version"] = "3.1 [Build: 1471417187]"; + clientinit["client_platform"] = "Windows"; + clientinit["client_version_sign"] = "Vr9F7kbVorcrkV5b/Iw+feH9qmDGvfsW8tpa737zhc1fDpK5uaEo6M5l2DzgaGqqOr3GKl5A7PF9Sj6eTM26Aw=="; + clientinit["client_input_hardware"] = true; + clientinit["client_output_hardware"] = true; + clientinit["client_default_channel"] = ""; + clientinit["client_default_channel_password"] = ""; + + string password; + if(!password.empty()){ + char passwordBuffer[SHA_DIGEST_LENGTH]; + SHA1((const unsigned char *) password.data(), password.length(), (unsigned char *) passwordBuffer); + password = base64_encode(string(passwordBuffer, SHA_DIGEST_LENGTH)); + } + clientinit["client_server_password"] = password; + clientinit["client_meta_data"] = ""; + clientinit["client_key_offset"] = this->clientIdentity->lastValidKeyOffset(); + clientinit["client_nickname_phonetic"] = ""; + clientinit["client_default_token"] = ""; + clientinit["hwid"] = "123,456123123123"; + sendCommand(clientinit); + + while(true){ + response = readNextPacket(); + if (!response) { + errorMessage = "could not get a valid response!"; + return false; + } + if(response->type() == PacketTypeInfo::Ack) continue; + break; + } + + //TODO check ack id + + if (!response) { + errorMessage = "could not get a valid response!"; + return false; + } + if (response->type() != protocol::PacketTypeInfo::Command) { + errorMessage = "invalid response type: " + response->type().name(); + return false; + } + if(response->asCommand().getCommand() == "initserver"){ //Got success + this->handleQueueLock.lock(); + this->handleQueue.push_front(response); + this->handleQueueLock.unlock(); + + this->setClientId(response->asCommand()["aclid"]); + + this->autoHandle = true; + cout << "Successfull connected!" << endl; + + /* + std::thread([&](){ + usleep(1000 * 1000); + cout << " -> send extra command" << endl; + //this->sendCommand(Command("channelsubscribeall return_code=1:i")); + + while(true){ + //this->sendCommand(Command("getconnectioninfo clid=320 return_code=1:112")); + //this->sendCommand(Command("ftgetfilelist cid=0 cpw path=\\/icons return_code=1:z0")); + this->sendCommand(Command("servergrouppermlist sgid=6 return_code=1:112")); + usleep(10 * 1000 * 1000); + } + //Command cmd("channelgetdescription cid=1 return_code=1:3o"); + //Command cmd("clientupdate client_nickname=WolverinDEV22 return_code=__1_"); + //Command cmd("clientdisconnect reasonid=8 reasonmsg=leaving"); + //this->sendCommand(Command("permissionlist return_code=__1_")); + //this->sendCommand(Command("clientgetvariables clid=" + to_string(this->clientId))); + }).detach(); + */ + + + return true; + } + cout << "Invalid connect: " << response->data() << endl; + //TODO error handling + return true; +} + +inline ClientPacket* solvePuzzle(shared_ptr response, Identity *identity, std::string &alpha) { + uint32_t puzzelLength = be2le32(&((char*) response->data().data_ptr())[1 + 128]); //1 for the first byte (the state byte) + + auto buffer = (char*) response->data().data_ptr(); + + char alphaBuffer[10]; + for (int index = 0; index < 10; index++) + alphaBuffer[index] = 0; //rand(); + alpha = string(alphaBuffer, 10); + + //Generating command + auto pkey = identity->publicKey(); + ts::Command command("clientinitiv", { + {"alpha", base64_encode(alphaBuffer, 10)}, + {"omega", pkey}, + {"ip", ""}, + {"ot", 1} //Required by 3.1 + }); + std::string cmd = command.build(); + + //Sloving puzzel + mp_int x{}; + mp_int n{}; + mp_int result{}; + + //mp_init_multi(&x, &n, &result); + mp_init(&x); + mp_init(&n); + mp_init(&result); + + char numBuffer[2048]; + mp_read_unsigned_bin(&x, (const unsigned char *) &response->data()[1], 64); //One offset + mp_read_unsigned_bin(&n, (const unsigned char *) &response->data()[1 + 64], 64); //1 + 64 offset + + cout << "X: " << toString(&x) << endl; + cout << "N: " << toString(&n) << endl; + cout << "Length: " << puzzelLength << endl; + + mp_int exp{}; + mp_init(&exp); + mp_2expt(&exp, puzzelLength); + + + //x ** (2 ** puzzelLength) mod n + int err = 0; + if ((err = mp_exptmod(&x, &exp, &n, &result)) != CRYPT_OK) { + cerr << "Invalid crypt: " << err << "/" << error_to_string(err) << endl; + } + + int resultBufferLength = mp_unsigned_bin_size(&result); + char resultBuffer[resultBufferLength]; + mp_to_unsigned_bin(&result, (unsigned char *) resultBuffer); + + + //mp_clear_multi(&x, &n, &exp, &result); + mp_clear(&x); + mp_clear(&n); + mp_clear(&exp); + mp_clear(&result); + + size_t packetBufferLength = InitVersionLength + 1 + 232 + 64 + cmd.length(); + char packetBuffer[packetBufferLength]; + memset(packetBuffer, 0, packetBufferLength); + + memcpy(packetBuffer, InitVersion, InitVersionLength); + packetBuffer[InitVersionLength] = 0x04; + + //Copy old data + memcpy(&packetBuffer[InitVersionLength + 1], &response->data()[1], 232); + memcpy(&packetBuffer[InitVersionLength + 1 + 232 + (64 - resultBufferLength)], resultBuffer, resultBufferLength); + memcpy(&packetBuffer[InitVersionLength + 1 + 232 + 64], cmd.data(), cmd.length()); + + cout << "sending puzzel sulution" << endl; + auto pkt = new ts::protocol::ClientPacket(ts::protocol::PacketTypeInfo::Init1, pipes::buffer_view((void *) packetBuffer, packetBufferLength)); + pkt->clientId(0); + pkt->toggle(ts::protocol::PacketFlag::Unencrypted, true); + return pkt; +} \ No newline at end of file diff --git a/client/src/protocol/ConnectionPacketHandler.cpp b/client/src/protocol/ConnectionPacketHandler.cpp new file mode 100644 index 0000000..2012015 --- /dev/null +++ b/client/src/protocol/ConnectionPacketHandler.cpp @@ -0,0 +1,158 @@ +#include +#include +#include "Connection.h" +#include "misc/base64.h" +#include "misc/endianness.h" + +using namespace std; +using namespace ts; +using namespace ts::connection; +using namespace ts::protocol; + +//notifystatusfiletransfer clientftfid=4093 status=2063 msg=lost\sfile\stransfer\sconnection size=16384 + +extern void hexdump(std::ostream& outs, const std::string& s, size_t line_len = 16); +inline void downloadStuff(std::string key, uint16_t port, uint64_t size){ + threads::Thread([key, port, size](){ + int socketId = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + assert(socketId > 1); + + sockaddr_in server; + server.sin_addr.s_addr = inet_addr("127.0.0.1"); + server.sin_family = AF_INET; + server.sin_port = htons( port ); + + //Connect to remote server + if (connect(socketId , (struct sockaddr *)&server , sizeof(server)) < 0) + { + perror("connect failed. Error"); + return; + } + + uint32_t readed = 0; + assert(send(socketId, key.data(), key.length(), 0) > 0); + + while(readed < size + 3){ + char buffer[size]; + auto readedBytes = recv(socketId, buffer, size - readed, MSG_DONTWAIT); + if(readedBytes < 0) { + //cerr << "Invalid ft read" << endl; + continue; + } + if(readedBytes == 0){ + continue; + } + hexdump(cout, string(buffer, readedBytes)); + readed += readedBytes; + } + cout << "File downloaded!" << endl; + }).detach(); +} + +void ServerConnection::handlePacketAck(std::shared_ptr packet) { + auto packetId = be2le16((const char*) packet->data().data_ptr()); +#if defined(DEBUG_PACKET_LOG) || defined(LOG_ACK) + cout << "Got ack for " << packetId << endl; +#endif +} + +void ServerConnection::handlePacketCommand(std::shared_ptr packet) { + auto command = packet->asCommand(); +#if defined(DEBUG_PACKET_LOG) || defined(LOG_CMD) + cout << "[Server -> Client][" << packet->type().name() << "] " << packet->data() << endl; +#endif + if (command.getCommand().compare("notifyconnectioninforequest") == 0) { //TODO + cout << "Send response" << endl; + Command cmd( + string("setconnectioninfo"), { + {"connection_ping", 10000000}, + {"connection_ping_deviation", 10000000}, + {"connection_packets_sent_speech", 0}, + {"connection_packets_sent_keepalive", 0}, + {"connection_packets_sent_control", rand()}, + {"connection_bytes_sent_speech", 0}, + {"connection_bytes_sent_keepalive", 0}, + {"connection_bytes_sent_control", 0}, + {"connection_packets_received_speech", 0}, + {"connection_packets_received_keepalive", 0}, + {"connection_packets_received_control", 0}, + {"connection_bytes_received_speech", 0}, + {"connection_bytes_received_keepalive", 0}, + {"connection_bytes_received_control", 0}, + {"connection_server2client_packetloss_speech", 10000000}, + {"connection_server2client_packetloss_keepalive", 10000000}, + {"connection_server2client_packetloss_control", 10000000}, + {"connection_server2client_packetloss_total", 10000000}, + {"connection_bandwidth_sent_last_second_speech", 0}, + {"connection_bandwidth_sent_last_second_keepalive", 0}, + {"connection_bandwidth_sent_last_second_control", 0}, + {"connection_bandwidth_sent_last_minute_speech", 0}, + {"connection_bandwidth_sent_last_minute_keepalive", 0}, + {"connection_bandwidth_sent_last_minute_control", 0}, + {"connection_bandwidth_received_last_second_speech", 0}, + {"connection_bandwidth_received_last_second_keepalive", 0}, + {"connection_bandwidth_received_last_second_control", 0}, + {"connection_bandwidth_received_last_minute_speech", 0}, + {"connection_bandwidth_received_last_minute_keepalive", 0}, + {"connection_bandwidth_received_last_minute_control", 0} + } + ); + sendCommand(cmd, true); + } else if (command.command() == "notifyserverupdated") { +#if defined(DEBUG_PACKET_LOG) || defined(LOG_CMD) + cout << "notifyserverupdated -> " << endl; + cout << "Last data: " << packet->data().string().substr(packet->data().length() - 10) << endl; +#endif + } else if (command.command() == "notifystartdownload") { + cout << "Client download: " << command.build() << endl; + auto port = command["port"].as(); + auto key = command["ftkey"].string(); + auto size = command["size"].as(); + downloadStuff(key, port, size); + } else if (command.command() == "channellist") { + cout << "Breaking ack" << endl; + for (int index = 0; index < command.bulkCount(); index++) { + this->channels.push_back(command[index]["cid"].as()); + } + } +} +void ServerConnection::handlePacketVoice(std::shared_ptr packet) {} + +static int pingIndex = 0; +void ServerConnection::handlePacketPing(std::shared_ptr packet) { + if(packet->type() == PacketTypeInfo::Pong){ + //cout << "[PING] gota " << be2le16(packet->data().data()) << endl; + return; + } + + char buffer[2]; + le2be16(packet->packetId(), buffer); + + ClientPacket pkt(PacketTypeInfo::Pong, pipes::buffer_view{buffer, 2}); + pkt.enableFlag(PacketFlag::Unencrypted); + sendPacket(pkt); + + ClientPacket ping(PacketTypeInfo::Ping, pipes::buffer_view{buffer, 0}); + ping.enableFlag(PacketFlag::Unencrypted); + sendPacket(ping); + //cout << "[PING] Reqe " << ping.packetId() << endl; + + //cout << "[PONG] Send " << packet->packetId() << endl; + + if(this->clientId > 0 && this->channels.size() > 0) { + Command command("clientmove"); + command["clid"] = this->clientId; + + auto idx = rand() % this->channels.size(); + command["cid"] = this->channels[idx]; + this->sendCommand(command); + + std::thread([&] { + threads::self::sleep_for(chrono::seconds(1)); + + Command cmd("channelcreate"); + cmd["channel_name"] = to_string(rand()) + "_" + to_string(rand()); + //this->sendCommand(cmd); + }).detach(); + } +} \ No newline at end of file diff --git a/client/src/protocol/HandshakeNew.cpp b/client/src/protocol/HandshakeNew.cpp new file mode 100644 index 0000000..cfeba8b --- /dev/null +++ b/client/src/protocol/HandshakeNew.cpp @@ -0,0 +1,154 @@ +#include +#include +#include +#include +#include "Connection.h" +#include "License.h" +#include + +using namespace std; +using namespace std::chrono; +using namespace ts; +using namespace license::teamspeak; +using namespace ts::connection; +using namespace ts::protocol; + + +int __ed_sha512_init(sha512_context* ctx) { + //assert(!ctx->context); + + ctx->context = new hash_state{}; + return sha512_init((hash_state*) ctx->context) == CRYPT_OK; +} + +int __ed_sha512_final(sha512_context* ctx, unsigned char *out) { + assert(ctx->context); + + auto result = sha512_done((hash_state*) ctx->context, out) == CRYPT_OK; + delete (hash_state*) ctx->context; + return result; +} +int __ed_sha512_update(sha512_context* ctx, const unsigned char *msg, size_t len) { + assert(ctx->context); + return sha512_process((hash_state*) ctx->context, msg, len) == CRYPT_OK; +} + +static sha512_functions __ed_sha512_functions { + __ed_sha512_init, + __ed_sha512_final, + __ed_sha512_update +}; + + +bool ServerConnection::handshakeNew(Command &initivexpand2, const std::string& alpha, std::string &errorMessage) { + if(&__ed_sha512_functions != &_ed_sha512_functions) + _ed_sha512_functions = __ed_sha512_functions; + + cout << initivexpand2.build() << endl; + u_char seed[32 * 2]; + u_char clientPrivateKey[32]; + u_char clientPublicKey[32]; + ed25519_create_keypair(clientPublicKey, clientPrivateKey, seed); + cout << "Client key: " << base64::encode((char*) clientPrivateKey, 32) << endl; + cout << "Privet key:" << endl; + hexDump(clientPrivateKey, 32); + cout << "Public key:" << endl; + hexDump(clientPublicKey, 32); + auto license = base64::decode(initivexpand2["l"]); + + auto licensestream = stringstream(license); + auto chain = LicenseChain::parse(licensestream, errorMessage); + if(!chain) return false; + chain->print(); + + unique_ptr serverPublic(new ecc_key{}); + auto omega = base64::decode(initivexpand2["omega"]); + ecc_import((u_char*) omega.data(), omega.length(), serverPublic.get()); + + //7B 1E AC 02 CE 77 35 0E EF C4 5C 1C F7 54 04 87 A9 A7 64 A7 8F 04 F7 53 58 64 84 D7 0A 97 F2 63 + //[0x7b, 0x1e, 0xac, 0x2, 0xce, 0x77, 0x35, 0xe, 0xef, 0xc4, 0x5c, 0x1c, 0xf7, 0x54, 0x4, 0x87, 0xa9, 0xa7, 0x64, 0xa7, 0x8f, 0x4, 0xf7, 0x53, 0x58, 0x64, 0x84, 0xd7, 0xa, 0x97, 0xf2, 0xe3] + //License signed from server :) + auto licenseHash = digest::sha256(license); + auto licenseSign = base64::decode(initivexpand2["proof"]); + + int state; + assert(ecc_verify_hash((u_char*) licenseSign.c_str(), licenseSign.length(), (u_char*) licenseHash.c_str(), licenseHash.length(), &state, serverPublic.get()) == CRYPT_OK); + cout << "State: " << state << endl; + assert(state == 1); + + //EK! + this->idManager.nextPacketId(PacketTypeInfo::Command); + cout << this->idManager.currentPacketId(PacketTypeInfo::Command) << endl; + Command clientek("clientek"); + clientek["ek"] = base64::encode((char*) clientPublicKey, 32); + auto rawProof = string((char*) clientPublicKey, 32) + base64::decode(initivexpand2["beta"]); + cout << " -> " << rawProof.length() << endl; + size_t signBufferLength = 120; + char signBuffer[signBufferLength]; + prng_state prngState{}; + memset(&prngState, 0, sizeof(prngState)); + + cout << "Data: " << base64::encode(rawProof) << endl; + cout << "KEY: " << this->clientIdentity->privateKey() << endl; + rawProof = digest::sha256(rawProof); + assert(ecc_sign_hash((u_char*) rawProof.data(), rawProof.length(), (u_char*) signBuffer, &signBufferLength, &prngState, find_prng("sprng"), this->clientIdentity->getKeyPair()) == CRYPT_OK); + cout << "ecc_sign_hash() -> " << base64::encode(signBuffer, signBufferLength) << endl; + clientek["proof"] = base64::encode(signBuffer, signBufferLength); + this->sendCommand(clientek, false); + + //TODO magic stuff + shared_ptr response; + response = readNextPacket(); + if (!response) { + errorMessage = "could not get a valid response!"; + return false; + } + cout << "Type: " << response->type().name() << endl; + cout << "ID: " << (int) response->data()[0] << " " << (int) response->data()[1] << endl; + + + LicensePublicKey serverroot; + memcpy(serverroot, public_root, 32); + if(initivexpand2[0].has("root")) { + cout << "Cot costume server root!" << endl; + auto root = base64::decode(initivexpand2["root"]); + memcpy(serverroot, root.data(), 32); + } + cout << "Public root key: " << endl; + for(const auto& e : serverroot) + cout << hex << "0x" << (int) (uint8_t) e << " " << endl; + + string sharedData; + this->cryptionHandler->setupSharedSecretNew(alpha, base64::decode(initivexpand2["beta"]), (char*) clientPrivateKey, (char*) chain->generatePublicKey(serverroot).data()); + //this->cryptionHandler->setupSharedSecretNew(alpha, base64::decode(initivexpand2["beta"]), (char*) clientPrivateKey, (char*) public_tea_root); + + threads::self::sleep_for(milliseconds(250)); + Command clientinit("clientinit"); + //94ec66de-5940-4e38-b002-970df0cf6c94,62444179-0d99-42ba-a45c-c6b1557d079a,d95f9901-c42d-4bac-8849-7164fd9e2310 + //clientinit["client_badges"] = "badges=450f81c1-ab41-4211-a338-222fa94ed157,c9e97536-5a2d-4c8e-a135-af404587a472,94ec66de-5940-4e38-b002-970df0cf6c94"; //,62444179-0d99-42ba-a45c-c6b1557d079a + clientinit["client_nickname"] = "Wolf C++ XX"; + clientinit["client_version"] = "3.1.8 [Build: 1516614607]"; + clientinit["client_platform"] = "Windows"; + clientinit["client_version_sign"] = "gDEgQf/BiOQZdAheKccM1XWcMUj2OUQqt75oFuvF2c0MQMXyv88cZQdUuckKbcBRp7RpmLInto4PIgd7mPO7BQ=="; + clientinit["client_input_hardware"] = true; + clientinit["client_output_hardware"] = true; + clientinit["client_default_channel"] = ""; + clientinit["client_default_channel_password"] = ""; + clientinit["client_server_password"] = ""; + clientinit["client_meta_data"] = ""; + clientinit["client_key_offset"] = this->clientIdentity->lastValidKeyOffset(); + clientinit["client_nickname_phonetic"] = ""; + clientinit["client_default_token"] = ""; + clientinit["hwid"] = "123,456123123123"; + sendCommand(clientinit); + + this->autoHandle = true; + /* + response = readNextPacket(); + if (!response) { + errorMessage = "could not get a valid response!"; + return false; + } + */ + return true; +} \ No newline at end of file diff --git a/client/src/protocol/socket/FilteredUDPSocket.cpp b/client/src/protocol/socket/FilteredUDPSocket.cpp new file mode 100644 index 0000000..ae35771 --- /dev/null +++ b/client/src/protocol/socket/FilteredUDPSocket.cpp @@ -0,0 +1,115 @@ +// +// Created by root on 13.10.17. +// + +#include "FilteredUDPSocket.h" + +#include +#include +#include +#include //Provides declarations for udp header +#include //Provides declarations for ip header +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace std::chrono; +using namespace ts::connection; + +FilteredUdpSocket::FilteredUdpSocket() {} +FilteredUdpSocket::~FilteredUdpSocket() {} + + +bool FilteredUdpSocket::setup(sockaddr_in * remoteAddr) { + srand(system_clock::now().time_since_epoch().count()); // should only be called once + int r = (lrand48() % (50 * 1000)) + 1000; // returns a pseudo-random integer between 0 and RAND_MAX + + + this->remoteAdress = new sockaddr_in; + memcpy(this->remoteAdress, remoteAddr, sizeof(sockaddr_in)); + + this->socketDescriptor = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if(this->socketDescriptor < 0){ + cerr << "Invalid socket create: " << errno << " - " << this->socketDescriptor << " -> " << strerror(errno) << endl; + } + int allow = 1; + setsockopt(this->socketDescriptor, SOL_SOCKET, SO_REUSEADDR, &allow, sizeof(int)); + + this->localAdress = new sockaddr_in; + memset((char *) this->localAdress, 0, sizeof(sockaddr_in)); + localAdress->sin_family = AF_INET; + localAdress->sin_addr.s_addr = htonl(INADDR_ANY); + localAdress->sin_port = htons(r); + + +/* sudo tcpdump -q udp port 256 -dd */ + struct sock_filter code[ ] = { + { 0x28, 0, 0, 0x0000000c }, + { 0x15, 0, 8, 0x000086dd }, + { 0x30, 0, 0, 0x00000014 }, + { 0x15, 2, 0, 0x00000084 }, + { 0x15, 1, 0, 0x00000006 }, + { 0x15, 0, 17, 0x00000011 }, + { 0x28, 0, 0, 0x00000036 }, + { 0x15, 14, 0, 0x00002fbd }, + { 0x28, 0, 0, 0x00000038 }, + { 0x15, 12, 13, 0x00002fbd }, + { 0x15, 0, 12, 0x00000800 }, + { 0x30, 0, 0, 0x00000017 }, + { 0x15, 2, 0, 0x00000084 }, + { 0x15, 1, 0, 0x00000006 }, + { 0x15, 0, 8, 0x00000011 }, + { 0x28, 0, 0, 0x00000014 }, + { 0x45, 6, 0, 0x00001fff }, + { 0xb1, 0, 0, 0x0000000e }, + { 0x48, 0, 0, 0x0000000e }, + { 0x15, 2, 0, 0x00002fbd }, + { 0x48, 0, 0, 0x00000010 }, + { 0x15, 0, 1, 0x00002fbd }, + { 0x6, 0, 0, 0x00040000 }, + { 0x6, 0, 0, 0x00000000 }, + + }; + + struct sock_fprog bpf = { + .len = sizeof(code) / sizeof(*code), + .filter = code, + }; + + //auto response = setsockopt(this->socketDescriptor, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf)); + //if (response < 0) cerr << "Invalid attach!" << endl; + /* ... bail out ... */ + + + /* + * + if(connect(this->socketDescriptor, (const sockaddr *) remoteAddr, sizeof(sockaddr_in)) < 0){ + cerr << "Invalid connect" << endl; + } + */ + if(bind(this->socketDescriptor, (const sockaddr *) localAdress, sizeof(sockaddr_in)) < 0) cout << "XXX" << endl; + + return true; +} + +void FilteredUdpSocket::close() { + if(this->socketDescriptor > 0) { + shutdown(this->socketDescriptor, SHUT_RDWR); + ::close(this->socketDescriptor); + this->socketDescriptor = 0; + } +} + +ssize_t FilteredUdpSocket::write(const char *buffer, size_t size) { + return sendto(this->socketDescriptor, buffer, size, 0, (const sockaddr *) this->remoteAdress, sizeof(sockaddr_in)); +} + +ssize_t FilteredUdpSocket::read(char *buffer, size_t size) { + return recv(this->socketDescriptor, (void *) buffer, size, 0); +} \ No newline at end of file diff --git a/client/src/protocol/socket/FilteredUDPSocket.h b/client/src/protocol/socket/FilteredUDPSocket.h new file mode 100644 index 0000000..2b68b49 --- /dev/null +++ b/client/src/protocol/socket/FilteredUDPSocket.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace ts { + namespace connection { + class FilteredUdpSocket { + public: + FilteredUdpSocket(); + ~FilteredUdpSocket(); + + bool setup(sockaddr_in*); + void close(); + + ssize_t read(char* buffer, size_t size); + ssize_t write(const char* buffer, size_t size); + + int getSocketDescriptor(){ return socketDescriptor; } + private: + int socketDescriptor; + sockaddr_in* remoteAdress = nullptr; + sockaddr_in* localAdress = nullptr; + }; + + typedef FilteredUdpSocket UdpSocket; + } +} \ No newline at end of file diff --git a/client/src/protocol/socket/RawUDPSocket.cpp b/client/src/protocol/socket/RawUDPSocket.cpp new file mode 100644 index 0000000..ce9504d --- /dev/null +++ b/client/src/protocol/socket/RawUDPSocket.cpp @@ -0,0 +1,151 @@ +// +// Created by wolverindev on 12.10.17. +// + +#include +#include +#include +#include //Provides declarations for udp header +#include //Provides declarations for ip header +#include +#include "RawUDPSocket.h" + +using namespace std; +using namespace ts::connection; + +/* + 96 bit (12 bytes) pseudo header needed for udp header checksum calculation +*/ +struct pseudo_header +{ + u_int32_t source_address; + u_int32_t dest_address; + u_int8_t placeholder; + u_int8_t protocol; + u_int16_t udp_length; +}; + +RawUdpSocket::RawUdpSocket() {} + +RawUdpSocket::~RawUdpSocket() {} + +uint16_t RawUdpSocket::buildCheckSum(uint16_t* buffer, size_t size) { + register long sum; + unsigned short oddbyte; + register short answer; + + sum = 0; + while (size > 1) { + sum += *buffer++; + size -= 2; + } + if (size == 1) { + oddbyte = 0; + *((u_char *) &oddbyte) = *(u_char *) buffer; + sum += oddbyte; + } + + sum = (sum >> 16) + (sum & 0xffff); + sum = sum + (sum >> 16); + answer = (short) ~sum; + + return (answer); +} + +int RawUdpSocket::read(char *buffer, size_t size) {} + +int RawUdpSocket::write(const char *buffer, size_t size) { + char datagram[4096]; + memset(datagram, 0, 4096); + memcpy(&datagram[sizeof(iphdr) + sizeof(udphdr)], buffer, size); + + //IP header + struct iphdr *iph = (struct iphdr *) datagram; + //UDP header + struct udphdr *udph = (struct udphdr *) (datagram + sizeof(struct ip)); + + //Setup the ip header + iph->ihl = 5; + iph->version = 4; + iph->tos = 0; + iph->tot_len = sizeof(iphdr) + sizeof(udphdr) + size; //Total length + iph->id = htonl(12); //TODO increase + iph->frag_off = 0; + iph->ttl = 255; //Max 255 hops maybe change it to default 64 + iph->protocol = IPPROTO_UDP; + iph->saddr = localAdress->sin_addr.s_addr; + iph->daddr = remoteAdress->sin_addr.s_addr; + iph->check = this->buildCheckSum ((unsigned short *) datagram, iph->tot_len); + + udph->source = localAdress->sin_port; + udph->dest = remoteAdress->sin_port; + udph->len = htons(8 + size); + + size_t csumLength = sizeof(struct pseudo_header) + sizeof(struct udphdr) + size; + char csumData[csumLength]; + + pseudo_header psh; + //Now the UDP checksum using the pseudo header + psh.source_address = localAdress->sin_addr.s_addr; + psh.dest_address = remoteAdress->sin_addr.s_addr; + psh.placeholder = 0; + psh.protocol = IPPROTO_UDP; + psh.udp_length = htons(sizeof(struct udphdr) + size); + memcpy(csumData , (char*) &psh , sizeof (struct pseudo_header)); + memcpy(csumData + sizeof(struct pseudo_header) , udph , sizeof(struct udphdr) + size); + + udph->check = buildCheckSum((uint16_t *) csumData, csumLength); + + auto written = sendto(this->socketDescriptor, datagram, iph->tot_len, 0, (struct sockaddr *) this->remoteAdress, sizeof(sockaddr)); + if(written != iph->tot_len){ + cerr << "Invalid write: " << written << endl; + return -1; + } + cout << "Write: " << written << endl; + return size; +} + +bool RawUdpSocket::setup(sockaddr_in *remoteAdress) { + this->remoteAdress = new sockaddr_in; + memcpy(this->remoteAdress, remoteAdress, sizeof(sockaddr_in)); + this->socketDescriptor = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); + if(this->socketDescriptor < 0){ + if(this->socketDescriptor == EPERM){ + cerr << "Invalid permission. Dont have permission to create a new RAW socket!"; + return false; + } + cerr << "Invalid socket create: " << errno << " - " << this->socketDescriptor << " -> " << strerror(errno) << endl; + } + + //get local addr + this->localAdress = new sockaddr_in; + struct ifaddrs *ifAddrStruct = NULL; + getifaddrs(&ifAddrStruct); + + for (ifaddrs* ifa = ifAddrStruct; ifa != NULL; ifa = ifa->ifa_next) { + if (!ifa->ifa_addr) { + continue; + } + if (ifa->ifa_addr->sa_family == AF_INET) { // check it is IP4 + // is a valid IP4 Address + memcpy(this->localAdress, ifa->ifa_addr, sizeof(sockaddr_in)); + /* + tmpAddrPtr=&((struct sockaddr_in *)ifa->ifa_addr)->sin_addr; + char addressBuffer[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, tmpAddrPtr, addressBuffer, INET_ADDRSTRLEN); + */ + } + /* + else if (ifa->ifa_addr->sa_family == AF_INET6) { // check it is IP6 + // is a valid IP6 Address + tmpAddrPtr=&((struct sockaddr_in6 *)ifa->ifa_addr)->sin6_addr; + char addressBuffer[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, tmpAddrPtr, addressBuffer, INET6_ADDRSTRLEN); + } + */ + } + if (ifAddrStruct != NULL) freeifaddrs(ifAddrStruct); + this->localAdress->sin_port = 1232; + + return true; +} \ No newline at end of file diff --git a/client/src/protocol/socket/RawUDPSocket.h b/client/src/protocol/socket/RawUDPSocket.h new file mode 100644 index 0000000..46e6faf --- /dev/null +++ b/client/src/protocol/socket/RawUDPSocket.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +namespace ts { + namespace connection { + class RawUdpSocket { + public: + RawUdpSocket(); + ~RawUdpSocket(); + + bool setup(sockaddr_in*); + + int read(char* buffer, size_t size); + int write(const char* buffer, size_t size); + uint16_t buildCheckSum(uint16_t* buffer, size_t size); + + int getSocketDescriptor(){ return socketDescriptor; } + private: + int socketDescriptor; + sockaddr_in* remoteAdress = nullptr; + sockaddr_in* localAdress = nullptr; + }; + + + typedef RawUdpSocket UdpSocket; + } +} \ No newline at end of file diff --git a/cmake/Modules/FindGLIB.cmake b/cmake/Modules/FindGLIB.cmake new file mode 100644 index 0000000..93d3ec5 --- /dev/null +++ b/cmake/Modules/FindGLIB.cmake @@ -0,0 +1,122 @@ +# - Try to find Glib and its components (gio, gobject etc) +# Once done, this will define +# +# GLIB_FOUND - system has Glib +# GLIB_INCLUDE_DIRS - the Glib include directories +# GLIB_LIBRARIES - link these to use Glib +# +# Optionally, the COMPONENTS keyword can be passed to find_package() +# and Glib components can be looked for. Currently, the following +# components can be used, and they define the following variables if +# found: +# +# gio: GLIB_GIO_LIBRARIES +# gobject: GLIB_GOBJECT_LIBRARIES +# gmodule: GLIB_GMODULE_LIBRARIES +# gthread: GLIB_GTHREAD_LIBRARIES +# +# Note that the respective _INCLUDE_DIR variables are not set, since +# all headers are in the same directory as GLIB_INCLUDE_DIRS. +# +# Copyright (C) 2012 Raphael Kubo da Costa +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND ITS CONTRIBUTORS ``AS +# IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR ITS +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +find_package(PkgConfig) +pkg_check_modules(PC_GLIB QUIET glib-2.0) + +find_library(GLIB_LIBRARIES + NAMES glib-2.0 + HINTS ${PC_GLIB_LIBDIR} + ${PC_GLIB_LIBRARY_DIRS} +) + +# Files in glib's main include path may include glibconfig.h, which, +# for some odd reason, is normally in $LIBDIR/glib-2.0/include. +get_filename_component(_GLIB_LIBRARY_DIR ${GLIB_LIBRARIES} PATH) +find_path(GLIBCONFIG_INCLUDE_DIR + NAMES glibconfig.h + HINTS ${PC_LIBDIR} ${PC_LIBRARY_DIRS} ${_GLIB_LIBRARY_DIR} + ${PC_GLIB_INCLUDEDIR} ${PC_GLIB_INCLUDE_DIRS} + PATH_SUFFIXES glib-2.0/include +) + +find_path(GLIB_INCLUDE_DIR + NAMES glib.h + HINTS ${PC_GLIB_INCLUDEDIR} + ${PC_GLIB_INCLUDE_DIRS} + PATH_SUFFIXES glib-2.0 +) + +set(GLIB_INCLUDE_DIRS ${GLIB_INCLUDE_DIR} ${GLIBCONFIG_INCLUDE_DIR}) + +# Version detection +if (EXISTS "${GLIBCONFIG_INCLUDE_DIR}/glibconfig.h") + file(READ "${GLIBCONFIG_INCLUDE_DIR}/glibconfig.h" GLIBCONFIG_H_CONTENTS) + string(REGEX MATCH "#define GLIB_MAJOR_VERSION ([0-9]+)" _dummy "${GLIBCONFIG_H_CONTENTS}") + set(GLIB_VERSION_MAJOR "${CMAKE_MATCH_1}") + string(REGEX MATCH "#define GLIB_MINOR_VERSION ([0-9]+)" _dummy "${GLIBCONFIG_H_CONTENTS}") + set(GLIB_VERSION_MINOR "${CMAKE_MATCH_1}") + string(REGEX MATCH "#define GLIB_MICRO_VERSION ([0-9]+)" _dummy "${GLIBCONFIG_H_CONTENTS}") + set(GLIB_VERSION_MICRO "${CMAKE_MATCH_1}") + set(GLIB_VERSION "${GLIB_VERSION_MAJOR}.${GLIB_VERSION_MINOR}.${GLIB_VERSION_MICRO}") +endif () + +# Additional Glib components. We only look for libraries, as not all of them +# have corresponding headers and all headers are installed alongside the main +# glib ones. +foreach (_component ${GLIB_FIND_COMPONENTS}) + if (${_component} STREQUAL "gio") + find_library(GLIB_GIO_LIBRARIES NAMES gio-2.0 HINTS ${_GLIB_LIBRARY_DIR}) + set(ADDITIONAL_REQUIRED_VARS ${ADDITIONAL_REQUIRED_VARS} GLIB_GIO_LIBRARIES) + elseif (${_component} STREQUAL "gobject") + find_library(GLIB_GOBJECT_LIBRARIES NAMES gobject-2.0 HINTS ${_GLIB_LIBRARY_DIR}) + set(ADDITIONAL_REQUIRED_VARS ${ADDITIONAL_REQUIRED_VARS} GLIB_GOBJECT_LIBRARIES) + elseif (${_component} STREQUAL "gmodule") + find_library(GLIB_GMODULE_LIBRARIES NAMES gmodule-2.0 HINTS ${_GLIB_LIBRARY_DIR}) + set(ADDITIONAL_REQUIRED_VARS ${ADDITIONAL_REQUIRED_VARS} GLIB_GMODULE_LIBRARIES) + elseif (${_component} STREQUAL "gthread") + find_library(GLIB_GTHREAD_LIBRARIES NAMES gthread-2.0 HINTS ${_GLIB_LIBRARY_DIR}) + set(ADDITIONAL_REQUIRED_VARS ${ADDITIONAL_REQUIRED_VARS} GLIB_GTHREAD_LIBRARIES) + elseif (${_component} STREQUAL "gio-unix") + # gio-unix is compiled as part of the gio library, but the include paths + # are separate from the shared glib ones. Since this is currently only used + # by WebKitGTK+ we don't go to extraordinary measures beyond pkg-config. + pkg_check_modules(GIO_UNIX QUIET gio-unix-2.0) + endif () +endforeach () + +include(FindPackageHandleStandardArgs) +FIND_PACKAGE_HANDLE_STANDARD_ARGS(GLIB REQUIRED_VARS GLIB_INCLUDE_DIRS GLIB_LIBRARIES ${ADDITIONAL_REQUIRED_VARS} + VERSION_VAR GLIB_VERSION) + +mark_as_advanced( + GLIBCONFIG_INCLUDE_DIR + GLIB_GIO_LIBRARIES + GLIB_GIO_UNIX_LIBRARIES + GLIB_GMODULE_LIBRARIES + GLIB_GOBJECT_LIBRARIES + GLIB_GTHREAD_LIBRARIES + GLIB_INCLUDE_DIR + GLIB_INCLUDE_DIRS + GLIB_LIBRARIES +) diff --git a/cmake/Modules/FindLibNice.cmake b/cmake/Modules/FindLibNice.cmake new file mode 100644 index 0000000..4e1edf1 --- /dev/null +++ b/cmake/Modules/FindLibNice.cmake @@ -0,0 +1,34 @@ +if (NOT TARGET LibNice::LibNice) + find_package(PkgConfig) + pkg_check_modules(PC_LIBNICE nice) + set(LIBNICE_DEFINITIONS ${PC_LIBNICE_CFLAGS_OTHER}) + + find_path(LIBNICE_INCLUDE_DIR nice/agent.h + HINTS ${PC_LIBNICE_INCLUDEDIR} ${PC_LIBNICE_INCLUDE_DIRS} + PATH_SUFFICES libnice) + find_library(LIBNICE_LIBRARY NAMES nice libnice + HINTS ${PC_LIBNICE_LIBDIR} ${PC_LIBNICE_LIBRARY_DIRS}) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(Libnice DEFAULT_MSG + LIBNICE_LIBRARY LIBNICE_INCLUDE_DIR) + mark_as_advanced(LIBNICE_INCLUDE_DIR LIBNICE_LIBRARY) + + set(LIBNICE_LIBRARIES ${LIBNICE_LIBRARY}) + set(LIBNICE_INCLUDE_DIRS ${LIBNICE_INCLUDE_DIR}) + + find_package(GLIB REQUIRED COMPONENTS gio gobject gmodule gthread) + + list(APPEND LIBNICE_INCLUDE_DIRS ${GLIB_INCLUDE_DIRS}) + list(APPEND LIBNICE_LIBRARIES ${GLIB_GOBJECT_LIBRARIES} ${GLIB_LIBRARIES}) + + if (LIBNICE_FOUND) + add_library(LibNice::LibNice UNKNOWN IMPORTED) + set_target_properties(LibNice::LibNice PROPERTIES + IMPORTED_LOCATION "${LIBNICE_LIBRARY}" + INTERFACE_COMPILE_DEFINITIONS "_REENTRANT" + INTERFACE_INCLUDE_DIRECTORIES "${LIBNICE_INCLUDE_DIRS}" + INTERFACE_LINK_LIBRARIES "${LIBNICE_LIBRARIES}" + IMPORTED_LINK_INTERFACE_LANGUAGES "C") + endif () +endif () diff --git a/cmake/Modules/FindSpdlog.cmake b/cmake/Modules/FindSpdlog.cmake new file mode 100644 index 0000000..7db9ed8 --- /dev/null +++ b/cmake/Modules/FindSpdlog.cmake @@ -0,0 +1,8 @@ +if (NOT TARGET Gabime::Spdlog) + include(FindPackageHandleStandardArgs) + find_path(SPDLOG_INCLUDE_DIR NAMES spdlog/spdlog.h) + find_package_handle_standard_args(Spdlog DEFAULT_MSG SPDLOG_INCLUDE_DIR) + add_library(spdlog INTERFACE) + target_include_directories(spdlog INTERFACE ${SPDLOG_INCLUDE_DIR}) + add_library(Gabime::Spdlog ALIAS spdlog) +endif () diff --git a/cmake/Modules/FindUsrSCTP.cmake b/cmake/Modules/FindUsrSCTP.cmake new file mode 100644 index 0000000..f5ed9c7 --- /dev/null +++ b/cmake/Modules/FindUsrSCTP.cmake @@ -0,0 +1,24 @@ +# Simple libnice cmake find + +if (NOT TARGET SctpLab::UsrSCTP) + set(USRSCTP_DEFINITIONS INET INET6) + find_path(USRSCTP_INCLUDE_DIR usrsctp.h PATH_SUFFICES usrsctp) + find_library(USRSCTP_LIBRARY NAMES usrsctp libusrsctp) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(Usrsctp DEFAULT_MSG USRSCTP_LIBRARY USRSCTP_INCLUDE_DIR) + + mark_as_advanced(USRSCTP_INCLUDE_DIR USRSCTP_LIBRARY) + + set(USRSCTP_LIBRARIES ${USRSCTP_LIBRARY}) + set(USRSCTP_INCLUDE_DIRS ${USRSCTP_INCLUDE_DIR}) + + if (USRSCTP_FOUND) + add_library(SctpLab::UsrSCTP UNKNOWN IMPORTED) + set_target_properties(SctpLab::UsrSCTP PROPERTIES + IMPORTED_LOCATION "${USRSCTP_LIBRARY}" + INTERFACE_COMPILE_DEFINITIONS "${USRSCTP_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${USRSCTP_INCLUDE_DIRS}" + IMPORTED_LINK_INTERFACE_LANGUAGES "C") + endif () +endif () diff --git a/flooder/CMakeLists.txt b/flooder/CMakeLists.txt new file mode 100644 index 0000000..4c8d479 --- /dev/null +++ b/flooder/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.6) +project(TeamSpeak) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") + +include_directories(../shared/src) +add_definitions(-DLTM_DESC) + +set(SOURCE_FILES + main.cpp + src/ProxiedClient.cpp + src/PorxiedClientSock5.cpp + src/TSClient.cpp +) + +add_executable(TeamSpeakFloodClient ${SOURCE_FILES}) +target_link_libraries(TeamSpeakFloodClient + TeaSpeak + pthread ThreadPool + ${TOM_LIBRARIES} + crypto + event + event_pthreads + /usr/local/lib/libjsoncpp.so +) \ No newline at end of file diff --git a/flooder/main.cpp b/flooder/main.cpp new file mode 100644 index 0000000..37983b7 --- /dev/null +++ b/flooder/main.cpp @@ -0,0 +1,81 @@ +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace ts::flood; + +void hexout(std::ostream& os, unsigned char c) +{ + unsigned char uc = static_cast(c); + os << std::setw(2) << std::setfill('0') << (unsigned int)uc << ' '; +} + +void hexdump(std::ostream& outs, const std::string& s, size_t line_len = 16) +{ + std::ostringstream os; + const std::string::size_type slen(s.size()); + int i(0); + std::string::size_type pos(0); + const std::streamsize lines(slen / line_len); + const std::streamsize chars(slen % line_len); + std::ios::fmtflags f(os.flags()); + + os << "Length: " << s.length() << "/" << std::hex << "0x" << s.length() << endl; + for(std::streamsize line = 0; line <= lines - (chars == 0 ? 1 : 0); ++line) + { + os << std::hex << setfill('0') << setw(3) << line * line_len << " | "; + for(i = 0; i < line_len; ++i) + { + if(pos < s.length()) + hexout(os, s[pos]); + else os << " "; + pos++; + } + os << " | "; + if(pos - line_len < s.length()){ + auto av = s.substr(pos - line_len); + for(char c : av.substr(0, min(av.length(), line_len))){ + if(isprint(c)) + os << c << " "; + else + os << "." << " "; + } + } + os << '\n'; + } + os.flags(f); + outs << os.str() << endl; +} + +int main(int argc, char** argv){ + sockaddr_in remoteAddress{}, proxyAddress{}; + memset(&remoteAddress, 0, sizeof(remoteAddress)); + memset(&proxyAddress, 0, sizeof(proxyAddress)); + + + proxyAddress.sin_family = AF_INET; + proxyAddress.sin_port = htons(1085); + proxyAddress.sin_addr.s_addr = inet_addr("185.89.100.17"); + + /* + proxyAddress.sin_family = AF_INET; + proxyAddress.sin_port = htons(1080); + proxyAddress.sin_addr.s_addr = inet_addr("54.38.22.7"); + */ + + remoteAddress.sin_family = AF_INET; + remoteAddress.sin_port = htons(1100); + remoteAddress.sin_addr.s_addr = inet_addr("87.106.252.164"); + + assert(evthread_use_pthreads() == 0); + event_base* evBase = event_base_new(); + + ProxiedClient client(evBase, proxyAddress, remoteAddress); + client.connect(); + + event_base_dispatch(evBase); +} \ No newline at end of file diff --git a/flooder/src/PorxiedClientSock5.cpp b/flooder/src/PorxiedClientSock5.cpp new file mode 100644 index 0000000..cd4c163 --- /dev/null +++ b/flooder/src/PorxiedClientSock5.cpp @@ -0,0 +1,111 @@ +#include +#include +#include +#include "ProxiedClient.h" + +using namespace std; +using namespace std::chrono; +using namespace ts::flood; + +#define CERROR(msg) \ +do { \ + cerr << msg << endl;\ + this->disconnect();\ + return;\ +} while(0) + +static int port = 10000; +static threads::Mutex portLock; +void ProxiedClient::handleProxyMessage(const std::string &msg) { + if(this->state == PROXY_INIT_METHODS){ + if(msg[0] != 0x05) CERROR("Invalid proxy version response (methode exchange)"); + if(msg[1] != 0x00) CERROR("Invalid respond methode"); + this->state = PROXY_INIT_CONNECTION; + + char buffer[128]; + int index = 0; + buffer[index++] = 0x05; //Version + buffer[index++] = 0x03; //Udp weiterleitung + buffer[index++] = 0x00; //Resv + buffer[index++] = 0x01; //Addr type = IPv4 + + auto addr = IPv4{this->remoteAddr->sin_addr.s_addr}; + buffer[index++] = addr._1; + buffer[index++] = addr._2; + buffer[index++] = addr._3; + buffer[index++] = addr._4; + + buffer[index++] = (ntohs(this->remoteAddr->sin_port) >> 8) & 0xFF; + buffer[index++] = (ntohs(this->remoteAddr->sin_port) >> 0) & 0xFF; + this->sendMessage(string(buffer, index)); + } else if(this->state = PROXY_INIT_CONNECTION){ + cout << "res!" << endl; + + int index = 0; + if(msg[index++] != 0x05) CERROR("Invalid proxy version response (connection request)"); + if(msg[index++] != 0x00) CERROR("Could not create connection (" + to_string((int) msg[1]) + ")"); + if(msg[index++] != 0x00) CERROR("Invalid proxy rsv response (connection request)"); + if(msg[index++] != 0x01) CERROR("Invalid proxy ip response type"); + + auto rAddr = IPv4{}; + rAddr._1 = msg[index++]; + rAddr._2 = msg[index++]; + rAddr._3 = msg[index++]; + rAddr._4 = msg[index++]; + + uint16_t pHigh = ((uint16_t) msg[index++]) & 0xFF; + uint16_t pLow = ((uint16_t) msg[index++]) & 0xFF; + uint16_t rPort = (pHigh << 8) | pLow; + cout << "Got udp relay " << rAddr.string() << ":" << rPort << endl; + + //Delete old connection + //shutdown(this->fileDescriptor, SHUT_RDWR); + + event_del(this->wEvent); + event_del(this->rEvent); + this->fileDescriptor = 0; + + //Setup relay + this->relayAddr = new sockaddr_in{}; + memset(relayAddr, 0, sizeof(*relayAddr)); + relayAddr->sin_family = AF_INET; + relayAddr->sin_port = htons(rPort); + relayAddr->sin_addr.s_addr = rAddr.addr; + cout << "Relay addr: " << inet_ntoa(relayAddr->sin_addr) << ":" << ntohs(relayAddr->sin_port) << endl; + + this->localAddr = new sockaddr_in{}; + memset(localAddr, 0, sizeof(*localAddr)); + localAddr->sin_family = AF_INET; + { + lock_guard l(portLock); + localAddr->sin_port = this->remoteAddr->sin_port; // = htons(10000 + (port++ % 30000)); + } + localAddr->sin_addr.s_addr = htonl(INADDR_ANY); + + this->fileDescriptor = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + cout << "fd -> " << this->fileDescriptor << endl; + int allow = 1; + if(setsockopt(this->fileDescriptor, SOL_SOCKET, SO_REUSEADDR, &allow, sizeof(int)) < 0) CERROR("Could not enable reuse addr"); + if(bind(this->fileDescriptor, reinterpret_cast(this->localAddr), sizeof(*this->localAddr)) < 0) CERROR("Could nto bind to relay"); + cout << "Bind on " << inet_ntoa(this->localAddr->sin_addr) << ":" << ntohs(this->localAddr->sin_port) << endl; + this->state = PROXY_CONNECTED; + + this->rEvent = event_new(this->evBase, this->fileDescriptor, EV_READ | EV_PERSIST, ProxiedClient::handleEventRead, this); + this->wEvent = event_new(this->evBase, this->fileDescriptor, EV_WRITE, ProxiedClient::handleEventWrite, this); + event_add(rEvent, nullptr); + + threads::Thread([&](){ + threads::self::sleep_for(seconds(1)); + this->proxyInizalisized(); + }); + } +} + +void ProxiedClient::requestProxyConnection() { + char buffer[3]; + buffer[0] = 0x05; //Version + buffer[1] = 1; //One methode + buffer[2] = 0x00; //No auth required + + this->sendMessage(string(buffer, 3)); +} \ No newline at end of file diff --git a/flooder/src/ProxiedClient.cpp b/flooder/src/ProxiedClient.cpp new file mode 100644 index 0000000..52170cc --- /dev/null +++ b/flooder/src/ProxiedClient.cpp @@ -0,0 +1,175 @@ +#include +#include +#include +#include "ProxiedClient.h" +#include "TSClient.h" + +using namespace std; +using namespace ts::flood; + +ProxiedClient::ProxiedClient(event_base* base, const sockaddr_in &proxyAddr, const sockaddr_in &remoteAddr) : evBase(base) { + this->proxyAddr = new sockaddr_in{}; + this->remoteAddr = new sockaddr_in{}; + + memcpy(this->proxyAddr, &proxyAddr, sizeof(proxyAddr)); + memcpy(this->remoteAddr, &remoteAddr, sizeof(proxyAddr)); + + this->client = new TSClient(this); +} + +ProxiedClient::~ProxiedClient() { + delete this->proxyAddr; + delete this->remoteAddr; +} + +#define CERR(msg) \ +do { \ + cerr << "Could not connect: " << msg << "(" << errno << "/" << strerror(errno) << ")" << endl; \ + return false; \ +} while(0) + +#if defined(TCP_CORK) && !defined(TCP_NOPUSH) + #define TCP_NOPUSH TCP_CORK +#endif + +static int enabled = 1; +static int disabled = 0; + +bool ProxiedClient::connect() { + assert(this->state == ProxyState::PROXY_UNCONNECTED); + TAILQ_INIT(&this->writeQueue); + + this->fileDescriptor = socket(AF_INET, SOCK_STREAM, 0); + if(this->fileDescriptor < 0) CERR("Socket setup failed"); + if(::connect(this->fileDescriptor, reinterpret_cast(this->proxyAddr), sizeof(*this->proxyAddr)) < 0) CERR("connect() failed"); + if(setsockopt(this->fileDescriptor, SOL_SOCKET, SO_REUSEADDR, &enabled, sizeof(enabled)) < 0) CERR("could not set reuse addr"); + if(setsockopt(this->fileDescriptor, IPPROTO_TCP, TCP_NOPUSH, &disabled, sizeof(disabled)) < 0) CERR("could not set no push"); + + cout << "Connected to " << inet_ntoa(this->proxyAddr->sin_addr) << endl; + + this->rEvent = event_new(this->evBase, this->fileDescriptor, EV_READ | EV_PERSIST, ProxiedClient::handleEventRead, this); + this->wEvent = event_new(this->evBase, this->fileDescriptor, EV_WRITE, ProxiedClient::handleEventWrite, this); + + event_add(rEvent, nullptr); + this->state = ProxyState::PROXY_INIT_METHODS; + + this->requestProxyConnection(); + return true; +} + +void ProxiedClient::disconnect() { + this->closeConnection(); +} + +void ProxiedClient::closeConnection() { + { + lock_guard lock(this->stateLock); + if(this->state == PROXY_UNCONNECTED) return; + this->state = PROXY_UNCONNECTED; + } + + event_del(this->wEvent); + event_del(this->rEvent); + + this->wEvent = nullptr; + this->rEvent = nullptr; +} + +void ProxiedClient::sendMessage(const std::string& message) { + buffer::RawBuffer* buffer; + if(this->state != PROXY_CONNECTED){ + buffer = new buffer::RawBuffer{message.length()}; + memcpy(buffer->buffer, message.data(), message.length()); + } else { + cout << "Send " << message.length() << " bytes with relay" << endl; + int relayHeaderLength = 2 + 1 + 1 + 4 + 2; + buffer = new buffer::RawBuffer{relayHeaderLength + message.length()}; + buffer->index = 0; + char preBuffer[relayHeaderLength]; + preBuffer[0] = 0x00; + preBuffer[1] = 0x00; + preBuffer[2] = 0x00; + preBuffer[3] = 0x01; + + IPv4 addr{this->relayAddr->sin_addr.s_addr}; + preBuffer[4] = addr._1; + preBuffer[5] = addr._2; + preBuffer[6] = addr._3; + preBuffer[7] = addr._4; + + preBuffer[8] = (ntohs(this->relayAddr->sin_port) >> 8) & 0xFF; + preBuffer[9] = (ntohs(this->relayAddr->sin_port) >> 0) & 0xFF; + //memset(&preBuffer[4], 0, 6); + + memcpy(&buffer->buffer[0], preBuffer, relayHeaderLength); + memcpy(&buffer->buffer[relayHeaderLength], message.data(), message.length()); + } + + { + lock_guard lock(this->queueLock); + TAILQ_INSERT_TAIL(&this->writeQueue, buffer, tail); + } + event_add(this->wEvent, nullptr); +} + +void ProxiedClient::handleMessage(const std::string &message) { + if(this->state == PROXY_UNCONNECTED) return; + if(this->state == PROXY_CONNECTED) return; //TODO + this->handleProxyMessage(message); +} + +extern void hexdump(std::ostream& outs, const std::string& s, size_t line_len = 16); +void ProxiedClient::handleEventWrite(int fd, short, void* ptrClient) { + auto* client = static_cast(ptrClient); + + buffer::RawBuffer* buffer = nullptr; + { + lock_guard lock(client->queueLock); + buffer = TAILQ_FIRST(&client->writeQueue); + if(!buffer) return; + + ssize_t writtenBytes = 0; + if(client->state == PROXY_CONNECTED){ + cout << "Write bytes to relay - " << fd << " - " << inet_ntoa(client->relayAddr->sin_addr) << ":" << ntohs(client->relayAddr->sin_port) << endl; + hexdump(cout, string((const char*) buffer->buffer, buffer->length)); + writtenBytes = sendto(fd, buffer->buffer, buffer->length, 0, (const sockaddr *) client->relayAddr, sizeof(*client->relayAddr)); + } else + writtenBytes = send(fd, &buffer->buffer[buffer->index], buffer->length - buffer->index, 0); + buffer->index += writtenBytes; + cout << "Written: " << writtenBytes << endl; + + if(buffer->index >= buffer->length || client->state == PROXY_CONNECTED) { + TAILQ_REMOVE(&client->writeQueue, buffer, tail); + delete buffer; + } + if(!TAILQ_EMPTY(&client->writeQueue)) + event_add(client->wEvent, nullptr); + } +} + +void ProxiedClient::handleEventRead(int fd, short, void* ptrClient) { + auto* client = static_cast(ptrClient); + + char buffer[1024]; + sockaddr_in remoteAddr{}; + socklen_t remoteAddrSize = sizeof(remoteAddr); + auto read = recvfrom(fd, buffer, 1024, MSG_DONTWAIT, reinterpret_cast(&remoteAddr), &remoteAddrSize); + cout << "Read " << read << " bytes" << endl; + if(read < 0){ + if(errno == EWOULDBLOCK) return; + cerr << "Invalid read: " << errno << "/" << strerror(errno) << endl; + client->disconnect(); + return; + } else if(read == 0){ + cerr << "Client hangs up!" << endl; + client->closeConnection(); + return; + } + + hexdump(cout, string(buffer, read)); + client->handleMessage(string(buffer, read)); +} + +void ProxiedClient::proxyInizalisized() { + this->client->startConnect(); +} \ No newline at end of file diff --git a/flooder/src/ProxiedClient.h b/flooder/src/ProxiedClient.h new file mode 100644 index 0000000..041abcc --- /dev/null +++ b/flooder/src/ProxiedClient.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace ts { + namespace flood { + union IPv4 { + uint32_t addr; + struct __attribute__ ((__packed__)) { + uint8_t _1; + uint8_t _2; + uint8_t _3; + uint8_t _4; + }; + + inline std::string string(){ + std::stringstream ss; + ss << (int) _1 << "." << (int) _2 << "." << (int) _3 << "." << (int) _4; + return ss.str(); + } + }; + + class TSClient; + enum ProxyState { + PROXY_UNCONNECTED, + PROXY_INIT_METHODS, + PROXY_INIT_CONNECTION, + PROXY_CONNECTED + }; + + class ProxiedClient { + public: + ProxiedClient(event_base*,const sockaddr_in& proxyAddr, const sockaddr_in& remoteAddr); + ~ProxiedClient(); + + bool connect(); + void disconnect(); + void closeConnection(); + + void handleMessage(const std::string& message); + void sendMessage(const std::string &); + private: + static void handleEventRead(int, short, void*); + static void handleEventWrite(int, short, void*); + + void requestProxyConnection(); + void handleProxyMessage(const std::string &); + + void proxyInizalisized(); + + event_base* evBase = nullptr; + event* rEvent = nullptr; + event* wEvent = nullptr; + int fileDescriptor; + ProxyState state = ProxyState::PROXY_UNCONNECTED; + threads::Mutex stateLock; + sockaddr_in* proxyAddr = nullptr; + sockaddr_in* relayAddr = nullptr; + sockaddr_in* localAddr = nullptr; + sockaddr_in* remoteAddr = nullptr; + + TAILQ_HEAD(, buffer::RawBuffer) writeQueue; + threads::Mutex queueLock; + + TSClient* client = nullptr; + }; + } +} \ No newline at end of file diff --git a/flooder/src/TSClient.cpp b/flooder/src/TSClient.cpp new file mode 100644 index 0000000..4205f6c --- /dev/null +++ b/flooder/src/TSClient.cpp @@ -0,0 +1,108 @@ +// +// Created by wolverindev on 06.01.18. +// + +#include "TSClient.h" + +using namespace std; +using namespace std::chrono; +using namespace ts::flood; +using namespace ts::protocol; + +TSClient::TSClient(ProxiedClient* con) : connection(con) { + this->cryptionHandler = new connection::CryptionHandler(); + this->cryptionHandler->reset(); +} + +TSClient::~TSClient() {} + +void TSClient::handleMessageRead(const std::string& message) { + if(message.length() < MAC_SIZE + SERVER_HEADER_SIZE) { + cerr << "Invalid pkt length!" << endl; + return; + } + + shared_ptr packet = make_shared(message); + cout << "Having packet " << packet->type().name() << endl; +} + +void TSClient::sendPacket(ts::protocol::ClientPacket &packet, int32_t packetId) { + size_t maxDataLength = 500 - packet.header().length(); + + if(packet.data().length() > maxDataLength){ + cout << "Split packet" << endl; + string error; + + + /* + if(!compressPacket(&packet, error)){ + cerr << "Compress error!" << endl; + return; + } + packet.enableFlag(PacketFlag::Compressed); + */ + if(packet.data().length() > maxDataLength){ + std::vector siblings; + + ClientPacket* root = new ClientPacket(packet.type(), packet.flagMask(), packet.data()); + root->enableFlag(PacketFlag::Fragmented); + //assert(root->hasFlag(PacketFlag::Compressed)); + siblings.push_back(root); + + //Max len - mac - header + while(siblings.back()->data().length() > maxDataLength){ + auto overhead = siblings.back()->data().substr(maxDataLength); + siblings.back()->data(siblings.back()->data().substr(0, maxDataLength)); + + ClientPacket* sib = new ClientPacket(packet.type(), packet.flagMask(), overhead); + sib->toggle(PacketFlag::Fragmented, false); + siblings.push_back(sib); + } + siblings.back()->enableFlag(PacketFlag::Fragmented); + for(auto elm : siblings){ + sendPacket(*elm); + delete elm; + } + return; + } + } + + if (packetId == -1) + packet.applyPacketId(pktIdManager); + else packet.applyPacketId((uint16_t) packetId, 0); + packet.clientId(this->clientId); + + string error = "success"; + if (!this->cryptionHandler->progressPacketOut(&packet, error)) { + cerr << "Invalid crypt -> " << error << endl; + return; + } + + this->connection->sendMessage(packet.mac() + packet.header() + packet.data()); +} + +const int InitVersionLength = 4; +const uint8_t InitVersion[InitVersionLength] = {0x06, 0x3b, 0xEC, 0xE9}; + +void TSClient::startConnect() { + int maxBufferSize = 512; + size_t bufferIndex = 0; + uint8_t buffer[maxBufferSize]; + string error = "success"; + + memcpy(buffer, InitVersion, InitVersionLength); + bufferIndex += InitVersionLength; + buffer[bufferIndex++] = 0x00; //Login state + + int64_t millis = duration_cast(system_clock::now().time_since_epoch()).count(); + memcpy(&buffer[5], &millis, 4); + bufferIndex += 4; + //generate the alpha key + for (int i = 0; i < 4; i++) buffer[bufferIndex++] = (uint8_t) std::rand(); + bufferIndex += 8; //Reserved bytes + + ClientPacket pkt(ts::protocol::PacketTypeInfo::Init1, string((char *) buffer, bufferIndex)); + pkt.clientId(0); + pkt.toggle(ts::protocol::PacketFlag::Unencrypted, true); + this->sendPacket(pkt, 101); +} \ No newline at end of file diff --git a/flooder/src/TSClient.h b/flooder/src/TSClient.h new file mode 100644 index 0000000..df3001f --- /dev/null +++ b/flooder/src/TSClient.h @@ -0,0 +1,36 @@ +#pragma once + +#include "ProxiedClient.h" +#include +#include +#include + +namespace ts { + namespace flood { + enum TSClientConnectionState { + TSC_UNCONNECTED, + TSC_PRE, + TSC_RSA, + TSC_HIGH, + TSC_CONNECTED + }; + + class TSClient { + public: + TSClient(ProxiedClient*); + ~TSClient(); + + void startConnect(); + void handleMessageRead(const std::string&); + + void sendPacket(ts::protocol::ClientPacket &packet, int32_t packetId = -1); + private: + ProxiedClient* connection; + TSClientConnectionState state = TSClientConnectionState::TSC_UNCONNECTED; + protocol::PacketIdManager pktIdManager; + ts::connection::CryptionHandler* cryptionHandler = nullptr; + + ClientId clientId = 0; + }; + } +} \ No newline at end of file diff --git a/license/CMakeLists.txt b/license/CMakeLists.txt new file mode 100644 index 0000000..aa1fb3a --- /dev/null +++ b/license/CMakeLists.txt @@ -0,0 +1,162 @@ +cmake_minimum_required(VERSION 3.6) +project(TeaSpeakLicence) + +set(CMAKE_VERBOSE_MAKEFILE ON) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -fpermissive -Wall -Wno-sign-compare -Wno-reorder -static-libgcc -static-libstdc++") +set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3") +set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELEASE} -O3") +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/environment/) + +#disable for debug +#add_definitions(-DRELEASE_MODE) + +include_directories(../shared/src) +add_definitions(-DLTM_DESC) + +set(LICENCE_SOURCE_FILES + shared/LicenseRequest.cpp + shared/LicenseRequestHandler.cpp + shared/License.cpp + ../shared/src/log/LogUtils.cpp +) + +#Protobuf +find_package(Protobuf REQUIRED) +include_directories(${Protobuf_INCLUDE_DIRS}) +include_directories(${CMAKE_CURRENT_BINARY_DIR}) +protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS packets/LicenseRequest.proto packets/LicenseManager.proto) + +#The actual librarie +add_library(TeaLicenseHelper STATIC ${LICENCE_SOURCE_FILES} ${PROTO_SRCS} ${PROTO_HDRS}) + + +#The license server +add_executable(TeaLicenseServer ${LICENCE_SOURCE_FILES} ${PROTO_SRCS} ${PROTO_HDRS} + server/KeyIdCache.cpp + server/LicenseServer.cpp + server/LicenseServerHandler.cpp + server/LicenseManager.cpp + LicenseServerMain.cpp + server/WebAPI.cpp + server/StatisticManager.cpp + server/UserManager.cpp +) + +target_link_libraries(TeaLicenseServer + TeaSpeak #Static + ${LIBRARY_PATH_DATA_PIPES} + ${LIBRARY_PATH_TERMINAL} #Static + ${LIBRARY_PATH_THREAD_POOL} #Static + ${PROJECT_SOURCE_DIR}/../../libraries/event/build/lib/libevent.a + ${PROJECT_SOURCE_DIR}/../../libraries/event/build/lib/libevent_pthreads.a + pthread + ${LIBRARY_PATH_BORINGSSL_SSL} + ${LIBRARY_PATH_BORINGSSL_CRYPTO} + ${LIBRARY_PATH_VARIBALES} + ${LIBRARY_PATH_BREAKPAD} + ${LIBRARY_PATH_PROTOBUF} + + ${LIBRARY_TOM_MATH} + ${LIBRARY_TOM_CRYPT} + + ${LIBRARY_PATH_BREAKPAD} + ${TOM_LIBRARIES} + ${LIBRARY_PATH_JDBC} + jsoncpp.a + stdc++fs.a +) + +#The test license client +add_executable(TeaLicenseClient + LicenseClientMain.cpp + ${LICENCE_SOURCE_FILES} ${PROTO_SRCS} ${PROTO_HDRS} +) +target_link_libraries(TeaLicenseClient + ${LIBRARY_PATH_DATA_PIPES} + ${LIBRARY_PATH_TERMINAL} #Static + ${LIBRARY_PATH_THREAD_POOL} #Static + ${PROJECT_SOURCE_DIR}/../../libraries/event/build/lib/libevent.a + ${PROJECT_SOURCE_DIR}/../../libraries/event/build/lib/libevent_pthreads.a + pthread + ${LIBRARY_PATH_VARIBALES} + ${LIBRARY_PATH_BREAKPAD} + ${LIBRARY_PATH_PROTOBUF} + + ${LIBRARY_TOM_MATH} + ${LIBRARY_TOM_CRYPT} + stdc++fs.a + + ${LIBRARY_PATH_BORINGSSL_SSL} + ${LIBRARY_PATH_BORINGSSL_CRYPTO} + ${LIBRARY_PATH_BREAKPAD} + ${TOM_LIBRARIES} + ${LIBRARY_PATH_JDBC} + TeaSpeak #Static + jsoncpp.a +) + +#The license manager +if(NOT DISABLE_QT) + find_package(Qt5Widgets) + include_directories(${Qt5Widgets_INCLUDE_DIRS}) + add_definitions(${Qt5Widgets_DEFINITIONS}) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${Qt5Widgets_EXECUTABLE_COMPILE_FLAGS}") + + set(UI_FILES manager/ui/licensegenerator.ui manager/ui/loginwindow.ui manager/ui/owerview.ui manager/ui/licenseinfo.ui) + set(DESIGN_HEADER_FILES manager/ui/LicenseGenerator.h manager/ui/LoginWindow.h manager/ui/Overview.h manager/ui/UiLicenseInfo.h) + + qt5_wrap_ui(UI_WARPED_FILES ${UI_FILES}) + qt5_wrap_cpp(UI_HEADER_FILES ${DESIGN_HEADER_FILES}) + + add_executable(LicenseManager + ${LICENCE_SOURCE_FILES} + LicenseManager.cpp + manager/ui/LicenseGenerator.cpp + manager/ui/LoginWindow.cpp + manager/ui/Overview.cpp + manager/ui/UiLicenseInfo.cpp + manager/ServerConnection.cpp + manager/ServerConnectionHandler.cpp + manager/ServerConnectionExecutor.cpp + ${UI_WARPED_FILES} ${UI_HEADER_FILES} ${PROTO_SRCS} ${PROTO_HDRS}) + target_link_libraries(LicenseManager Qt5::Widgets Qt5::Core Qt5::Gui) + target_link_libraries(LicenseManager + ${LIBRARY_PATH_THREAD_POOL} #Static + TeaSpeak #Static + ${LIBRARY_PATH_TERMINAL} #Static + ${LIBRARY_PATH_BORINGSSL_CRYPTO} + ${PROJECT_SOURCE_DIR}/../../libraries/event/build/lib/libevent.a + ${PROJECT_SOURCE_DIR}/../../libraries/event/build/lib/libevent_pthreads.a + pthread + ${LIBRARY_PATH_VARIBALES} + ${LIBRARY_PATH_BREAKPAD} + ${LIBRARY_PATH_PROTOBUF} + + ${LIBRARY_TOM_MATH} + ${LIBRARY_TOM_CRYPT} + stdc++fs + jsoncpp.a + ${LIBRARY_PATH_DATA_PIPES} + ${LIBRARY_PATH_BORINGSSL_SSL} + ${LIBRARY_PATH_BORINGSSL_CRYPTO} + ) +endif() + +add_executable(LicenseCLI + LicenseCreatorCLI.cpp + manager/ServerConnection.cpp + manager/ServerConnectionExecutor.cpp + manager/ServerConnectionHandler.cpp + shared/License.cpp + ${PROTO_SRCS} ${PROTO_HDRS} +) + +target_link_libraries(LicenseCLI + ${LIBRARY_PATH_THREAD_POOL} #Static + ${PROJECT_SOURCE_DIR}/../../libraries/event/build/lib/libevent.a + ${PROJECT_SOURCE_DIR}/../../libraries/event/build/lib/libevent_pthreads.a + ${LIBRARY_PATH_PROTOBUF} + ${LIBRARY_TOM_MATH} #Static + ${LIBRARY_TOM_CRYPT} #Static + pthread +) \ No newline at end of file diff --git a/license/LicenseClientMain.cpp b/license/LicenseClientMain.cpp new file mode 100644 index 0000000..de3a373 --- /dev/null +++ b/license/LicenseClientMain.cpp @@ -0,0 +1,87 @@ +#include +#include +#include +#include + +using namespace std; +using namespace std::chrono; +using namespace license; + +/* + struct LicenseInfo { + LicenseType type; + std::string username; + std::string first_name; + std::string last_name; + std::string email; + std::chrono::system_clock::time_point start; + std::chrono::system_clock::time_point end; + std::chrono::system_clock::time_point creation; + + inline bool isValid() { return (end.time_since_epoch().count() == 0 || std::chrono::system_clock::now() < this->end); } + }; + */ +int main(int ac, char** av){ + auto state = evthread_use_pthreads(); + assert(state == 0); + + srand(system_clock::now().time_since_epoch().count()); + cout << "Generating new license" << endl; + + std::string name = "WolverinDEV"; + auto raw_license = createLocalLicence(LicenseType::PREMIUM, system_clock::now() - chrono::hours((int) (24 * 30.5 * 3)), "WolverinDEV"); + auto license = readLocalLicence(raw_license, error); + assert(license); + + + + sockaddr_in serv_addr{}; + serv_addr.sin_family = AF_INET; + + serv_addr.sin_addr.s_addr = ((in_addr*) gethostbyname("localhost")->h_addr)->s_addr; + serv_addr.sin_port = htons(27786); + + + /* + * struct LicenseRequestData { + std::shared_ptr license; + + int64_t speach_total; + int64_t speach_dead; + int64_t speach_online; + int64_t speach_varianz; + + int64_t client_online; + int64_t bots_online; + int64_t queries_online; + int64_t servers_online; + }; + */ + auto data = make_shared(); + data->license = license; + data->info = make_shared(); + while(true) { + LicenceRequest request(data, serv_addr); + try { + cout << "Requesting license" << endl; + auto info = request.requestInfo().waitAndGet(nullptr); + if(!info) { + cout << "Invalid result! Error: " << (request.exception() ? "yes => " + string(request.exception()->what()) : "no") << endl; + throw *request.exception(); + } + cout << "Got result!" << endl; + cout << "Valid: " << info->license_valid << endl; + if(info->license) { + cout << "License:" << endl; + cout << " Type: " << info->license->type << endl; + cout << " User name: " << info->license->username << endl; + cout << " First name: " << info->license->first_name << endl; + cout << " Last name: " << info->license->last_name << endl; + cout << " EMail: " << info->license->email << endl; + } else cout << "License: none"; + } catch (const std::exception& ex){ + cerr << "Could not load info after throwing: " << endl << ex.what() << endl; + } + } + return 0; +} \ No newline at end of file diff --git a/license/LicenseCreatorCLI.cpp b/license/LicenseCreatorCLI.cpp new file mode 100644 index 0000000..a483f0a --- /dev/null +++ b/license/LicenseCreatorCLI.cpp @@ -0,0 +1,138 @@ +#include +#include +#include +#include + +#include "manager/ServerConnection.h" + +using namespace std; +using namespace license; +using namespace license::manager; +using namespace std::chrono; + +std::string url_decode(const std::string& source) { + std::string ret; + char ch; + size_t i; + int ii; + for (i=0; i < source.length(); i++) { + if ((int) source[i] == 37) { + sscanf(source.substr(i + 1, 2).c_str(), "%x", &ii); + ch = static_cast(ii); + ret += ch; + i = i + 2; + } else { + ret += source[i]; + } + } + return ret; +} + +class CLIParser { + public: + CLIParser(int &argc, char **argv) { + for (int i = 1; i < argc; ++i) + this->tokens.emplace_back(argv[i]); + } + + /// @author iain + const std::string& get_option(const std::vector &options) const { + for(const auto& option : options) { + std::vector::const_iterator itr; + itr = std::find(this->tokens.begin(), this->tokens.end(), option); + if (itr != this->tokens.end() && ++itr != this->tokens.end()) + return *itr; + } + + static const std::string empty_string; + return empty_string; + } + + /// @author iain + bool option_exists(const std::vector &options, bool allow_empty = true) const { + for(const auto& option : options) { + auto index = std::find(this->tokens.begin(), this->tokens.end(), option); + if(index != this->tokens.end()) + if(allow_empty || !index->empty()) + return true; + } + return false; + } + + private: + std::vector tokens; +}; + +#define REQ_CMD(variable, message, ...) \ +if(!options.option_exists({__VA_ARGS__}, false)) { \ + cerr << "missing user" << endl; \ + return 1; \ +} \ +auto variable = url_decode(options.get_option({__VA_ARGS__})) \ + +#define NO_OPEN_SSL +#include +#include +int main(int argc, char **argv) { + CLIParser options(argc, argv); + + REQ_CMD(user, "missing user", "-u", "--user"); + REQ_CMD(first_name, "missing first name", "-n", "--first_name"); + REQ_CMD(last_name, "missing last name", "-s", "--last_name"); + REQ_CMD(begin, "missing begin timestamp", "-b", "--begin"); + REQ_CMD(end, "missing end timestamp", "-e", "--end"); + REQ_CMD(email, "missing email", "-m", "--email"); + REQ_CMD(license_type, "missing license_type", "-l", "--license-type"); + + REQ_CMD(auth_user, "missing authentification user", "-au", "--auth-user"); + REQ_CMD(auth_pass, "missing authentification user", "-ap", "--auth-pass"); + + REQ_CMD(server_host, "missing server host", "-h", "--server-host"); + REQ_CMD(server_port, "missing server port", "-p", "--server-port"); + + auto state = evthread_use_pthreads(); + if(state != 0) { + cerr << "failed to setup pthreads" << endl; + return 1; + } + + ServerConnection connection; + connection.verbose = false; + try { + auto future = connection.connect(server_host, (uint16_t) stol(server_port)); + if(!future.waitAndGet(false, system_clock::now() + seconds(5))) { + cerr << "failed to connect (" << future.errorMegssage() << ")" << endl; + return 1; + } + } catch(std::exception& ex) { + cerr << "invalid port" << endl; + return 1; + } + + { + auto future = connection.login(auth_user, auth_pass); + if(!future.waitAndGet(false, system_clock::now() + seconds(5))) { + cerr << "failed to athentificate (" << future.errorMegssage() << ")" << endl; + return 1; + } + }; + + try { + system_clock::time_point timestamp_begin = system_clock::time_point() + seconds(stoll(begin)); + system_clock::time_point timestamp_end = system_clock::time_point() + seconds(stoll(end)); + auto future = connection.registerLicense(first_name, last_name, user, email, (LicenseType) stoll(license_type), timestamp_end, timestamp_begin); + auto result = future.waitAndGet({nullptr, nullptr}, system_clock::now() + seconds(5)); + if(!result.first || !result.second) { + cerr << "failed to create license! (" << future.errorMegssage() << ")" << endl; + return 1; + } + + auto license_key = license::exportLocalLicense(result.first); + cout << license_key << endl; + } catch(std::exception& ex) { + cerr << "invalid timestamps or license type" << endl; + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/license/LicenseManager.cpp b/license/LicenseManager.cpp new file mode 100644 index 0000000..a80e44d --- /dev/null +++ b/license/LicenseManager.cpp @@ -0,0 +1,97 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace std::chrono; +using namespace license; +using namespace license::manager; + +ServerConnection* connection = nullptr; +QWidget* current_widget = nullptr; + +std::string string_to_hex(const std::string& input) +{ + static const char* const lut = "0123456789ABCDEF"; + size_t len = input.length(); + + std::string output; + output.reserve(2 * len); + for (size_t i = 0; i < len; ++i) + { + const unsigned char c = input[i]; + output.push_back(lut[c >> 4]); + output.push_back(lut[c & 15]); + } + return output; +} + +int main(int ac, char** av){ + evthread_use_pthreads(); + + + + auto data = "AQBUAd26AAAAACnNuqvy++NGfMJU+Bvo5412Wi5jM5/JmmYyDbcxWKNaJV0FF1jpXtSrss3Gm7RUEQ6CdEEhPdyHVgHdugAAAAAphOsSMO1976JoEaTLru5boYKp2A62gUVdGvLZYqrDjBnsiumfxPlxfF/PYai/Khvth7FgDzDc7WIBBTPyOV8uw4Db2ZmOkA4dpjGWfY2L6QJaRyjh9XySDL+c6/eoKOv75Wbu1FO1mr83jr8H9pWoCalwm7SEPngSdYa3ZOa+Q7c="; + string error; + auto license = license::readLocalLicence(data, error); + cout << "Key: " << base64::encode(license->key()) << endl; + cout << "Key: " << string_to_hex(license->key()) << endl; + return false; + + connection = new ServerConnection(); + if(!connection->connect("mcgalaxy.de", 27786).waitAndGet(false)) { + cerr << "Failed to connect!" << endl; + return 2; + } + auto login = connection->login("WolverinDEV", "HelloWorld").waitAndGet(false); + if(!login) { + cerr << "Failed to login!" << endl; + return 1; + } + /* + cout << "Generating new license" << endl; + + std::string name = "Hyp3rX (timo-games@gmx.de )"; + auto licenses = createLocalLicence(LicenseType::PREMIUM, system_clock::now() + chrono::hours((int) (24 * 30.5 * 3))/* system_clock::time_point(), name); + cout << name << " " << licenses << endl; + + readLocalLicence(licenses, name); + cout << name << endl; + return 0; + string error = ""; + auto binary = readLocalLicence(licenses, error); + if(!binary){ + cerr << "Could not read local license " << error << endl; + return -1; + } + + struct sockaddr_in serv_addr; + serv_addr.sin_family = AF_INET; + + serv_addr.sin_addr.s_addr = ((in_addr*) gethostbyname("0.0.0.0")->h_addr)->s_addr; + serv_addr.sin_port = htons(27786); + LicenceRequest request(binary, serv_addr); + + try { + auto info = request.requestInfo().waitAndGet(nullptr); + cout << "Got info -> " << info->owner << "|" << info->licenseInfo << " -> " << info->valid << endl; + } catch (const std::exception& ex){ + cerr << "Could not load info after throwing " << ex.what() << endl; + cerr << "Exception: " << request.exception()->what() << endl; + } + return 0; + */ + + QApplication app(ac, av); + //license::ui::LoginWindow gen; + license::ui::Overview gen; + gen.show(); + return app.exec(); +} \ No newline at end of file diff --git a/license/LicenseServerMain.cpp b/license/LicenseServerMain.cpp new file mode 100644 index 0000000..ceb6baa --- /dev/null +++ b/license/LicenseServerMain.cpp @@ -0,0 +1,240 @@ +#include +#include +#include +#include +#include +#include +#include +#include "server/WebAPI.h" +#include "server/StatisticManager.h" +#include +#include "server/UserManager.h" + +#include +#include + +using namespace std; +using namespace std::chrono; +using namespace license; + +/* + * Requests/license: SELECT `tmp`.`keyId`, `tmp`.`key`, `tmp`.`ip`, `tmp`.`count`, `license_info`.`username`, `tmp`.`type` FROM (SELECT DISTINCT `license_request`.`keyId`, `key`, `license_request`.`ip`, COUNT(`license_request`.`keyId`) AS `count`, `license`.`type` FROM `license_request` INNER JOIN `license` ON `license`.`keyId` = `license_request`.`keyId` GROUP BY (`license_request`.`ip`)) AS `tmp` INNER JOIN `license_info` ON `license_info`.`keyId` = `tmp`.`keyId` + * Different IP's: SELECT `tmp`.`keyId`, `license_info`.`username`, COUNT(`ip`) FROM (SELECT DISTINCT `ip`, `keyId` FROM `license_request`) AS `tmp` LEFT JOIN `license_info` ON `license_info`.`keyId` = `tmp`.`keyId` GROUP BY (`tmp`.`keyId`) + * + * Requests (license) / ip: SELECT DISTINCT `ip`, COUNT(`ip`) FROM `license_request` WHERE `keyId` = ? GROUP BY `ip` + * + * SELECT DISTINCT(`ip`), `keyId` FROM `license_request` WHERE `timestamp` > (UNIX_TIMESTAMP() - 2 * 60 * 60) * 1000 + * + * SELECT DISTINCT(`ip`), `license_request`.`keyId`, `license_info`.`username` FROM `license_request` LEFT JOIN `license_info` ON `license_info`.`keyId` = `license_request`.`keyId` WHERE `timestamp` > (UNIX_TIMESTAMP() - 2 * 60 * 60) * 1000 + * + * + * Online clients: SELECT SUM(`clients`) FROM (SELECT DISTINCT(`ip`), `clients` FROM `history_online` WHERE `timestamp` > (UNIX_TIMESTAMP() - 60 * 60 * 2) * 1000) AS `a` + * Online bots: SELECT SUM(`clients`) FROM (SELECT DISTINCT(`ip`), `clients` FROM `history_online` WHERE `timestamp` > (UNIX_TIMESTAMP() - 60 * 60 * 2) * 1000) AS `a` + * Online VS Server: SELECT SUM(`music`) FROM (SELECT DISTINCT(`ip`), `music` FROM `history_online` WHERE `timestamp` > (UNIX_TIMESTAMP() - 60 * 60 * 2) * 1000) AS `a` + */ +bool handle_command(string& line); + +shared_ptr license_manager; +shared_ptr statistic_manager; +shared_ptr ssl_manager; +shared_ptr web_server; +shared_ptr license_server; +shared_ptr user_manager; + +int main(int argc, char** argv) { + if(argc < 2) { + cerr << "Invalid arguments! Need MySQL connection" << endl; + return 0; + } + + evthread_use_pthreads(); + http::decode_url("xxx"); + srand(system_clock::now().time_since_epoch().count()); + terminal::install(); + if(!terminal::active()){ cerr << "could not setup terminal!" << endl; return -1; } + + auto config = std::make_shared(); + config->vs_group_size = 0; + config->logfileLevel = spdlog::level::trace; + config->terminalLevel = spdlog::level::trace; + config->logPath = "logs/log_${time}(%Y-%m-%d_%H:%M:%S).log"; + logger::setup(config); + + string error; + sql::SqlManager* database = new sql::mysql::MySQLManager(); + bool db_connected = true; + ((sql::mysql::MySQLManager*) database)->listener_disconnected = [&](bool wanted){ + if(wanted) return; + logCritical("Lost connection to MySQL server!"); + logCritical("Stopping server!"); + db_connected = false; + }; + + //mysql://localhost:3306/license?userName=root&password=markus + logMessage(LOG_GENERAL, "Connecting to {}", argv[1]); + auto connect_result = database->connect(string(argv[1])); + if(!connect_result) { + logError("Could not connect to mysql server! (" + connect_result.fmtStr() + ")"); + return 0; + } +#if false + sql::command(database, "INSERT INTO license (`key`, type, deleted, issuer) VALUES ('0020', 1, 1, 'Test'); ").execute(); + cout << sql::command(database, "SELECT LAST_INSERT_ID();").query([](void*, int length, string* values, string* names) { + for(int i = 0; i < length; i++) + cout << names[i] << " -> " << values[i] << endl; + return 0; + }, (void*) nullptr) << endl; +#endif + + license_manager = make_shared(database); + if(!license_manager->setup(error)) { + logError("Could not start license manager! (" +error + ")"); + return 0; + } + + + statistic_manager = make_shared(license_manager); + + /* + { + auto _now = system_clock::now(); + auto statistics = license_manager->list_statistics_user(_now - hours(24) * 32 * 4, _now, duration_cast(hours(2))); + cout << "Date,Instances,Servers,Clients,Web Clients,Queries,Music Bots" << endl; + for(const auto& entry : statistics) { + auto time = system_clock::to_time_t(entry->timestamp); + tm* localtm = localtime(&time); + string string_time = asctime(localtm); + string_time = string_time.substr(0, string_time.length() - 1); + //cout << "[" << string_time << "] Users online: " << entry->clients_online << " | Instances: " << entry->instance_online << endl; + cout << string_time << "," << entry->instance_online << "," << entry->servers_online << "," << entry->clients_online << "," << entry->web_clients_online << "," << entry->queries_online << "," << entry->bots_online << endl; + } + } + return 0; + { + auto _now = system_clock::now(); + auto statistics = license_manager->list_statistics_version(_now - hours(24) * 9, _now, duration_cast(hours(1))); + std::deque versions; + const auto version_name = [](const std::string& key) { + auto space = key.find(' '); + return key.substr(0, space); + }; + + for(const auto& entry : statistics) { + for(const auto& version : entry->versions) { + const auto name = version_name(version.first); + if(name.empty()) { + continue; + } + auto it = find(versions.begin(), versions.end(), name); + if(it == versions.end()) + versions.push_back(name); + } + } + sort(versions.begin(), versions.end(), [](const std::string& a, const std::string& b) { + const auto index_a = a.find_last_of('.'); + const auto index_b = b.find_last_of('.'); + const auto length_a = a.find('-', index_a) - index_a - 1; + const auto length_b = b.find('-', index_b) - index_b - 1; + + const auto patch_a = stoll(a.substr(index_a + 1, length_a)); + const auto patch_b = stoll(b.substr(index_b + 1, length_b)); + return patch_a > patch_b; + }); + + cout << "Date"; + for(auto it = versions.begin(); it != versions.end(); it++) + cout << "," << *it; + cout << endl; + + for(const auto& entry : statistics) { + auto time = system_clock::to_time_t(entry->timestamp); + tm* localtm = localtime(&time); + string string_time = asctime(localtm); + string_time = string_time.substr(0, string_time.length() - 1); + + map version_count; + for(const auto& version : entry->versions) { + const auto name = version_name(version.first); + version_count[name] += version.second; + } + + cout << string_time; + for(const auto& name : versions) { + cout << "," << version_count[name]; + } + cout << endl; + } + } + */ + + ssl_manager = make_shared(); + { + string key_file = "certificates/web_stats_prv.pem"; + string cert_file = "certificates/web_stats_crt.pem"; + if(!ssl_manager->initializeContext("web_stats", key_file, cert_file, error, false, make_shared(ts::ssl::SSLGenerator{ + .subjects = {}, + .issues = {{"O", "TeaSpeak"}, {"OU", "License server"}, {"creator", "WolverinDEV"}} + }))) { + logCritical(LOG_LICENSE_WEB, "Failed to initialize ssl certificate! Stopping server."); + } + } + + { + web_server = make_shared(license_manager, statistic_manager); + logMessage("Starting web server on [:::]:27788"); + if(!web_server->start(error, 27788, ssl_manager->getContext("web_stats"))) { + logError(LOG_GENERAL, "Failed to start web statistics server!"); + return 0; + } + } + + { + user_manager = make_shared(database); + } + { + logMessage("Starting license server on [:::]:27786"); + struct sockaddr_in listen_addr{}; + memset(&listen_addr, 0, sizeof(listen_addr)); + listen_addr.sin_family = AF_INET; + listen_addr.sin_addr.s_addr = INADDR_ANY; + listen_addr.sin_port = htons(27786); + + license_server = make_shared(listen_addr, license_manager, statistic_manager, web_server, user_manager); + license_server->startServer(); + } + + while(db_connected && web_server->running() && license_server->isRunning()) { + auto line = terminal::instance()->readLine("§a> §f"); + if(line.empty()){ + usleep(500); + continue; + } + if(!handle_command(line)) { + terminal::instance()->writeMessage("§aStopping server..."); + break; + } + } + + web_server->stop(); + license_server->stopServer(); + if(database) database->disconnect(); + + logger::uninstall(); + terminal::uninstall(); + return 0; +} + +bool handle_command(string& line) { + if(line == "end" || line == "stop") return false; //Exit loop + + if(line == "info web") { + logMessage(LOG_LICENSE_WEB, "Currently online clients:"); + auto clients = web_server->get_clients(); + for(const auto& client : clients) + logMessage(LOG_LICENSE_WEB, " - {}", client->client_prefix()); + logMessage(LOG_LICENSE_WEB, " {} clients are currently connected!", clients.size()); + return true; + } + logError("Invalid command: " + line); + return true; +} \ No newline at end of file diff --git a/license/manager/ServerConnection.cpp b/license/manager/ServerConnection.cpp new file mode 100644 index 0000000..3429e10 --- /dev/null +++ b/license/manager/ServerConnection.cpp @@ -0,0 +1,225 @@ +// +// Created by wolverindev on 08.05.18. +// + +#include +#include +#include +#include +#include +#include "ServerConnection.h" + +using namespace std; +using namespace std::chrono; +using namespace license; +using namespace license::manager; + +ServerConnection::ServerConnection() {} +ServerConnection::~ServerConnection() { + this->disconnect("deallocation"); + if(this->network.flush_thread) + this->network.flush_thread->join(); +} + +#define CERR(message) \ +do { \ + FLERROR(this->listener.future_connect, message); \ + return; \ +} while(0) + +threads::Future ServerConnection::connect(const std::string &host, uint16_t port) { + this->listener.future_connect = std::make_unique>(); + this->network.state = ConnectionState::CONNECTING; + + threads::Thread([&, host, port](){ + this->network.address_remote.sin_family = AF_INET; + { + auto address = gethostbyname(host.c_str()); + if(!address) + CERR("invalid address"); + + auto inet_address = (in_addr*) address->h_addr; + if(!inet_address) + CERR("invalid address (2)"); + this->network.address_remote.sin_addr.s_addr = inet_address->s_addr; + } + this->network.address_remote.sin_port = htons(port); //27786 + + this->network.file_descriptor = socket(AF_INET, SOCK_STREAM, 0); + if(this->network.file_descriptor < 0) CERR("Socket setup failed"); + if(::connect(this->network.file_descriptor, reinterpret_cast(&this->network.address_remote), sizeof(this->network.address_remote)) < 0) CERR("connect() failed (" + to_string(errno) + " | " + strerror(errno) + ")"); + int enabled = 1, disabled = 0; + if(setsockopt(this->network.file_descriptor, SOL_SOCKET, SO_REUSEADDR, &enabled, sizeof(enabled)) < 0) CERR("could not set reuse addr"); + if(setsockopt(this->network.file_descriptor, IPPROTO_TCP, TCP_CORK, &disabled, sizeof(disabled)) < 0) CERR("could not set no push"); + + this->network.event_base = event_base_new(); + this->network.event_read = event_new(this->network.event_base, this->network.file_descriptor, EV_READ | EV_PERSIST, ServerConnection::handleEventRead, this); + this->network.event_write = event_new(this->network.event_base, this->network.file_descriptor, EV_WRITE, ServerConnection::handleEventWrite, this); + event_add(this->network.event_read, nullptr); + + this->network.event_base_dispatch = new threads::Thread(THREAD_SAVE_OPERATIONS, [&](){ + event_base_dispatch(this->network.event_base); + if(this->verbose) + cout << "ev ended!" << endl; + }); + this->network.state = ConnectionState::CONNECTED; + this->protocol.state = protocol::HANDSCAKE; + this->protocol.ping_thread = thread([&]{ + while(true) { + { + unique_lock lock(this->protocol.ping_lock); + this->protocol.ping_notify.wait_until(lock, system_clock::now() + seconds(30), [&]{ + return this->network.state != ConnectionState::CONNECTED; + }); + + if(this->network.state != ConnectionState::CONNECTED) return; + } + + this->ping(); + } + }); + uint8_t handshakeBuffer[5]; + handshakeBuffer[0] = 0xC0; + handshakeBuffer[1] = 0xFF; + handshakeBuffer[2] = 0xEE; + handshakeBuffer[3] = LICENSE_PROT_VERSION; + handshakeBuffer[4] = 1; //Im a manager + this->sendPacket(protocol::packet{protocol::PACKET_CLIENT_HANDSHAKE, string((const char*) handshakeBuffer, 5)}); //Initialise packet + }).detach(); + + return *this->listener.future_connect; +} + +void ServerConnection::disconnect(const std::string& reason) { + this->network.state = ConnectionState::DISCONNECTING; + this->local_disconnect_message = reason; + //TODO + this->closeConnection(); +} + +#define F_ERROR_DISCONNECT(name) \ +FLERROR(name, "connection closed locally" + (this->local_disconnect_message.empty() ? "" : " (" + this->local_disconnect_message + ")")) + +void ServerConnection::closeConnection() { + if(this->network.state == ConnectionState::UNCONNECTED) return; + this->network.state = ConnectionState::DISCONNECTING; + + if(this->network.event_base_dispatch && *this->network.event_base_dispatch == threads::self::id()) { + this->network.flush_thread = new threads::Thread(THREAD_SAVE_OPERATIONS, [&](){ this->closeConnection(); }); + return; + } + + if(this->network.event_write) { + event_del(this->network.event_write); + event_free(this->network.event_write); + this->network.event_write = nullptr; + } + if(this->network.event_read) { + event_del(this->network.event_read); + event_free(this->network.event_read); + this->network.event_read = nullptr; + } + if(this->network.event_base) { + event_base_loopbreak(this->network.event_base); + if(event_base_loopexit(this->network.event_base, nullptr) < 0) { + if(this->verbose) + cerr << "could not stop event loop!" << endl; + } + } + if(this->network.event_base_dispatch) { + this->network.event_base_dispatch->join(); + delete this->network.event_base_dispatch; + this->network.event_base_dispatch = nullptr; + } + if(this->network.event_base) { + event_base_free(this->network.event_base); + this->network.event_base = nullptr; + } + + if(this->network.file_descriptor > 0) { + shutdown(this->network.file_descriptor, SHUT_RDWR); + this->network.file_descriptor = 0; + } + this->network.state = ConnectionState::UNCONNECTED; + + { + this->protocol.ping_notify.notify_all(); + if(this->protocol.ping_thread.joinable()) + this->protocol.ping_thread.join(); + } + + F_ERROR_DISCONNECT(this->listener.future_register); + F_ERROR_DISCONNECT(this->listener.future_connect); + F_ERROR_DISCONNECT(this->listener.future_delete); + F_ERROR_DISCONNECT(this->listener.future_list); + F_ERROR_DISCONNECT(this->listener.future_login); +} + +void ServerConnection::handleEventRead(int fd, short, void* _connection) { + auto connection = (ServerConnection*) _connection; + + char buffer[1024]; + auto read = recv(fd, buffer, 1024, SOCK_NONBLOCK); + if(read < 0) { + if(connection->verbose) + cout << "Invalid read: " << strerror(errno) << endl; + connection->local_disconnect_message = "invalid read"; + connection->closeConnection(); + return; + } + if(read > 0) { + if(connection->verbose) + cout << "Read: " << read << endl; + connection->local_disconnect_message = "invalid read"; + connection->handleMessage(string(buffer, read)); + } +} + +void ServerConnection::handleEventWrite(int fd, short, void* _connection) { + auto connection = (ServerConnection*) _connection; + + threads::MutexLock lock(connection->network.queue_lock); + auto& queue = connection->network.queue_write; + if(queue.empty()) return; + auto message = queue.front(); + queue.pop_front(); + + auto wrote = send(fd, message.data(), message.length(), 0); + if(wrote < 0) { + if(connection->verbose) + cout << "Invalid write: " << strerror(errno) << endl; + connection->local_disconnect_message = "invalid write"; + connection->closeConnection(); + return; + } + if(connection->verbose) + cout << "Wrote: " << wrote << endl; + + if(wrote < message.length()) { + queue.push_front(message.substr(wrote)); + event_add(connection->network.event_write, nullptr); + } +} + +void ServerConnection::sendPacket(const protocol::packet& packet) { + if(this->network.state == ConnectionState::UNCONNECTED || this->network.state == ConnectionState::DISCONNECTING) { + if(this->verbose) + cout << "Tried to send a packet to an unconnected remote!" << endl; + return; + } + packet.prepare(); + + string buffer; + buffer.resize(packet.data.length() + sizeof(packet.header)); + memcpy((void*) buffer.data(), &packet.header, sizeof(packet.header)); + memcpy((void*) &buffer.data()[sizeof(packet.header)], packet.data.data(), packet.data.length()); + + if(!this->protocol.crypt_key.empty()) + xorBuffer(&buffer[sizeof(packet.header)], packet.data.length(), this->protocol.crypt_key.data(), this->protocol.crypt_key.length()); + + { + threads::MutexLock lock(this->network.queue_lock); + this->network.queue_write.push_back(buffer); + } + event_add(this->network.event_write, nullptr); +} \ No newline at end of file diff --git a/license/manager/ServerConnection.h b/license/manager/ServerConnection.h new file mode 100644 index 0000000..4d0699a --- /dev/null +++ b/license/manager/ServerConnection.h @@ -0,0 +1,113 @@ +#pragma once + +#include +#include +#include +#include +#include "shared/License.h" + +#define FLSUCCESS(listener, object) \ +do { \ + auto l = listener.release(); \ + if(l) \ + l->executionSucceed(object); \ + delete l; \ +} while(0) + +#define FLERROR(listener, object) \ +do { \ + auto l = listener.release(); \ + if(l) \ + l->executionFailed(object); \ + delete l; \ +} while(0) + + +namespace license { + namespace manager { + enum ConnectionState { + UNCONNECTED, + CONNECTING, + CONNECTED, + DISCONNECTING + }; + class ServerConnection { + public: + ServerConnection(); + ~ServerConnection(); + + threads::Future connect(const std::string& host, uint16_t port); + void disconnect(const std::string&); + + void ping(); + + threads::Future login(const std::string&, const std::string&); + threads::Future, std::shared_ptr>> registerLicense( + const std::string& first_name, + const std::string& last_name, + const std::string& username, + const std::string& email, + license::LicenseType type, + const std::chrono::system_clock::time_point& end, + const std::chrono::system_clock::time_point& begin = std::chrono::system_clock::now() + ); + + threads::Future>> list(int offset, int count); + threads::Future deleteLicense(const std::string& key, bool full = false); + + bool verbose = true; + private: + struct { + ConnectionState state = ConnectionState::UNCONNECTED; + sockaddr_in address_remote; + int file_descriptor = 0; + + event* event_read = nullptr; + event* event_write = nullptr; + struct event_base* event_base = nullptr; + threads::Thread* event_base_dispatch = nullptr; + + threads::Thread* flush_thread = nullptr; + + threads::Mutex queue_lock; + std::deque queue_write; + + std::unique_ptr current_packet; + } network; + + + struct { + protocol::RequestState state; + std::string crypt_key = ""; + + std::mutex ping_lock; + std::condition_variable ping_notify; + std::thread ping_thread; + } protocol; + + struct { + std::unique_ptr> future_connect; + std::unique_ptr> future_login; + std::unique_ptr, std::shared_ptr>>> future_register; + std::unique_ptr>>> future_list; + std::unique_ptr> future_delete; + } listener; + + std::string local_disconnect_message; + + static void handleEventRead(int, short, void*); + static void handleEventWrite(int, short, void*); + + void closeConnection(); + void sendPacket(const protocol::packet&); + void handleMessage(const std::string&); + + void handlePacketDisconnect(const std::string&); + void handlePacketHandshake(const std::string&); + void handlePacketAuthResponse(const std::string&); + void handlePacketCreateResponse(const std::string&); + void handlePacketListResponse(const std::string&); + void handlePacketDeleteResponse(const std::string&); + }; + } +} \ No newline at end of file diff --git a/license/manager/ServerConnectionExecutor.cpp b/license/manager/ServerConnectionExecutor.cpp new file mode 100644 index 0000000..067e194 --- /dev/null +++ b/license/manager/ServerConnectionExecutor.cpp @@ -0,0 +1,93 @@ +// +// Created by wolverindev on 08.05.18. +// + +#include +#include +#include +#include +#include +#include "ServerConnection.h" + +using namespace std; +using namespace std::chrono; +using namespace license; +using namespace license::manager; + +threads::Future ServerConnection::login(const std::string& username, const std::string& password) { + this->listener.future_login = unique_ptr>(new threads::Future()); + + ts::proto::license::AuthorizationRequest request; + request.set_username(username); + request.set_password(password); + this->sendPacket({protocol::PACKET_CLIENT_AUTH_REQUEST, request}); + + if(this->network.state != ConnectionState::CONNECTED) + this->listener.future_login->executionFailed("not connected"); + + return *this->listener.future_login; +} + +threads::Future, std::shared_ptr>> ServerConnection::registerLicense( + const std::string &first_name, + const std::string &last_name, + const std::string &username, + const std::string &email, + license::LicenseType type, + const std::chrono::system_clock::time_point& end, + const std::chrono::system_clock::time_point& start +) { + + this->listener.future_register = std::make_unique, std::shared_ptr>>>(); + + ts::proto::license::LicenseCreateRequest request; + request.set_issuer_first_name(first_name); + request.set_issuer_last_name(last_name); + request.set_issuer_username(username); + request.set_issuer_email(email); + request.set_type(type); + request.set_begin(duration_cast(start.time_since_epoch()).count()); + request.set_end(duration_cast(end.time_since_epoch()).count()); + this->sendPacket({protocol::PACKET_CLIENT_LICENSE_CREATE_REQUEST, request}); + + if(this->network.state != ConnectionState::CONNECTED) + this->listener.future_register->executionFailed("not connected"); + + return *this->listener.future_register; +} + +threads::Future>> ServerConnection::list(int offset, + int count) { + this->listener.future_list = std::make_unique>>>(); + + ts::proto::license::LicenseListRequest request; + request.set_offset(offset); + request.set_count(count); + this->sendPacket({protocol::PACKET_CLIENT_LIST_REQUEST, request}); + + if(this->network.state != ConnectionState::CONNECTED) + this->listener.future_register->executionFailed("not connected"); + + return *this->listener.future_list; +} + +threads::Future ServerConnection::deleteLicense(const std::string &key, bool full) { + this->listener.future_delete = make_unique>(); + + ts::proto::license::LicenseDeleteRequest request; + request.set_key(key); + request.set_full(full); + this->sendPacket({protocol::PACKET_CLIENT_DELETE_REQUEST, request}); + + if(this->network.state != ConnectionState::CONNECTED) + this->listener.future_register->executionFailed("not connected"); + + return *this->listener.future_delete; +} + +void ServerConnection::ping() { + this->list(0, 1); + return; //FIXME + cout << "Sending ping" << endl; + this->sendPacket({protocol::PACKET_PING, nullptr}); +} \ No newline at end of file diff --git a/license/manager/ServerConnectionHandler.cpp b/license/manager/ServerConnectionHandler.cpp new file mode 100644 index 0000000..7103a59 --- /dev/null +++ b/license/manager/ServerConnectionHandler.cpp @@ -0,0 +1,180 @@ +#include +#include +#include +#include +#include +#include +#include +#include "ServerConnection.h" + +using namespace std; +using namespace std::chrono; +using namespace license; +using namespace license::manager; + +#define LERROR(message) \ +do { \ + FLERROR(this->listener.future_connect, message); \ + return; \ +} while(0) + + +void ServerConnection::handleMessage(const std::string& message) { + auto& packet = this->network.current_packet; + if(packet) { + auto left = packet->header.length - packet->data.size(); + if(left >= message.length()) { + packet->data += message; + } else { + packet->data += message.substr(0, left); + if(this->verbose) + cerr << "Dropping overhead! FIXME!" << endl; + } + } else { + if(message.length() < sizeof(protocol::packet::header)) { + if(this->verbose) + cout << "Invalid packet header size!" << endl; + return; + } + + packet = std::make_unique(protocol::PACKET_DISCONNECT, ""); + memcpy(packet.get(), message.data(), sizeof(protocol::packet::header)); + packet->data = message.substr(sizeof(protocol::packet::header)); + } + if(packet->data.length() < packet->header.length) return; + + if(!this->protocol.crypt_key.empty()) + xorBuffer((char*) packet->data.data(), packet->data.length(), this->protocol.crypt_key.data(), this->protocol.crypt_key.length()); + + switch (packet->header.packetId) { + case protocol::PACKET_SERVER_HANDSHAKE: + this->handlePacketHandshake(packet->data); + break; + case protocol::PACKET_SERVER_AUTH_RESPONSE: + this->handlePacketAuthResponse(packet->data); + break; + case protocol::PACKET_DISCONNECT: + this->handlePacketDisconnect(packet->data); + break; + case protocol::PACKET_SERVER_LICENSE_CREATE_RESPONSE: + this->handlePacketCreateResponse(packet->data); + break; + case protocol::PACKET_SERVER_LIST_RESPONSE: + this->handlePacketListResponse(packet->data); + break; + case protocol::PACKET_CLIENT_DELETE_RESPONSE: + this->handlePacketDeleteResponse(packet->data); + break; + default: + if(this->verbose) + cout << "Invalid packet type: " << packet->header.packetId << endl; + } + packet.reset(); +} + +void ServerConnection::handlePacketDisconnect(const std::string& message) { + if(this->verbose) + cout << "Got disconnect: " << message << endl; + this->closeConnection(); +} + +void ServerConnection::handlePacketHandshake(const std::string& data) { + if(this->protocol.state != protocol::HANDSCAKE) LERROR("Protocol state mismatch"); + if(data.length() < 3) LERROR("Invalid packet size"); + + if((uint8_t) data[0] != 0xAF || (uint8_t) data[1] != 0xFE) LERROR("Invalid handshake"); + if((uint8_t) data[2] != LICENSE_PROT_VERSION) LERROR("Invalid license protocol version. Please update TeaSpeak!"); + + auto key_length = be2le16(data.data(), 3); + if(data.length() < key_length + 3) LERROR("Invalid packet size"); + this->protocol.crypt_key = data.substr(5, key_length); + + FLSUCCESS(this->listener.future_connect, true); + this->protocol.state = protocol::MANAGER_AUTHORIZATION; +} + +void ServerConnection::handlePacketAuthResponse(const std::string& data) { + if(this->protocol.state != protocol::MANAGER_AUTHORIZATION) { + FLERROR(this->listener.future_login, "Invalid state"); + return; + } + + ts::proto::license::AuthorizationResponse pkt; + if(!pkt.ParseFromString(data)) { + FLERROR(this->listener.future_login, "Invalid response"); + return; + } + + if(pkt.has_success() && pkt.success()) { + FLSUCCESS(this->listener.future_login, true); + } else { + FLERROR(this->listener.future_login, pkt.has_message() ? "Login error: " + pkt.message() : "invalid login"); + } +} + +void ServerConnection::handlePacketCreateResponse(const std::string& data) { + ts::proto::license::LicenseCreateResponse pkt; + if(!pkt.ParseFromString(data)) { + FLERROR(this->listener.future_register, "Invalid response"); + return; + } + + if(!pkt.has_error()) { + auto info = make_shared(); + info->first_name = pkt.license().first_name(); + info->last_name = pkt.license().last_name(); + info->username = pkt.license().username(); + info->email = pkt.license().email(); + info->type = static_cast(pkt.license().type()); + info->start = system_clock::time_point() + milliseconds(pkt.license().begin()); + info->creation = system_clock::time_point() + milliseconds(pkt.license().created()); + info->end = system_clock::time_point() + milliseconds(pkt.license().end()); + + string error; + auto license = license::readLocalLicence(pkt.exported_key(), error); + if(!license) FLERROR(this->listener.future_register, "Failed to read license!"); + + if(this->listener.future_register) { + auto l = this->listener.future_register.release(); + l->executionSucceed({license, info}); + delete l; + } + } else { + FLERROR(this->listener.future_register, pkt.error()); + } +} + +void ServerConnection::handlePacketListResponse(const std::string& data) { + ts::proto::license::LicenseListResponse pkt; + if(!pkt.ParseFromString(data)) { + FLERROR(this->listener.future_list, "Invalid response"); + return; + } + + std::map> response; + for(const auto& entry : pkt.entries()) { + auto info = make_shared(); + info->first_name = entry.first_name(); + info->last_name = entry.last_name(); + info->username = entry.username(); + info->email = entry.email(); + info->type = static_cast(entry.type()); + info->start = system_clock::time_point() + milliseconds(entry.begin()); + info->creation = system_clock::time_point() + milliseconds(entry.created()); + info->end = system_clock::time_point() + milliseconds(entry.end()); + + response[entry.key()] = info; + } + + FLSUCCESS(this->listener.future_list, response); +} + +void ServerConnection::handlePacketDeleteResponse(const std::string& data) { + ts::proto::license::LicenseDeleteResponse pkt; + if(!pkt.ParseFromString(data)) { + FLERROR(this->listener.future_list, "Invalid response"); + return; + } + + FLSUCCESS(this->listener.future_delete, pkt.succeed()); +} \ No newline at end of file diff --git a/license/manager/qtHelper.h b/license/manager/qtHelper.h new file mode 100644 index 0000000..290b293 --- /dev/null +++ b/license/manager/qtHelper.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +template +inline void runOnThread(QThread *qThread, Func &&func) +{ + if(qThread == QThread::currentThread()){ + func(); + return; + } + + QTimer *t = new QTimer(); + t->moveToThread(qThread); + t->setSingleShot(true); + QObject::connect(t, &QTimer::timeout, [=]() + { + func(); + t->deleteLater(); + }); + QMetaObject::invokeMethod(t, "start", Qt::QueuedConnection, Q_ARG(int, 0)); +} \ No newline at end of file diff --git a/license/manager/ui/LicenseGenerator.cpp b/license/manager/ui/LicenseGenerator.cpp new file mode 100644 index 0000000..0a3ba03 --- /dev/null +++ b/license/manager/ui/LicenseGenerator.cpp @@ -0,0 +1,193 @@ +#include +#include +#include +#include "LicenseGenerator.h" +#include "manager/ServerConnection.h" + +using namespace std; +using namespace std::chrono; +using namespace license; +using namespace license::manager; +using namespace license::ui; + +enum TimeType : uint8_t { + PERM, + YEARS_1, + MONTHS_6, + MONTHS_3, + MONTHS_1, + COSTUME +}; + +LicenseGenerator::LicenseGenerator(QWidget* owner) : QMainWindow(owner) { + ui.setupUi(this); + + ui.licenseType->addItem("Demo", qVariantFromValue((uint8_t) license::DEMO)); + ui.licenseType->addItem("Premium", qVariantFromValue((uint8_t) license::PREMIUM)); + ui.licenseType->addItem("Hoster", qVariantFromValue((uint8_t) license::HOSTER)); + ui.licenseType->addItem("Private", qVariantFromValue((uint8_t) license::PRIVATE)); + ui.licenseType->setCurrentIndex(ui.licenseType->findData(qVariantFromValue((uint8_t) license::PREMIUM))); + + ui.datePickerType->addItem("Permanent", qVariantFromValue((uint8_t) TimeType::PERM)); + ui.datePickerType->addItem("1 Year", qVariantFromValue((uint8_t) TimeType::YEARS_1)); + ui.datePickerType->addItem("6 Months", qVariantFromValue((uint8_t) TimeType::MONTHS_6)); + ui.datePickerType->addItem("3 Months", qVariantFromValue((uint8_t) TimeType::MONTHS_3)); + ui.datePickerType->addItem("1 Month", qVariantFromValue((uint8_t) TimeType::MONTHS_1)); + ui.datePickerType->addItem("Costume", qVariantFromValue((uint8_t) TimeType::COSTUME)); + + QObject::connect(ui.datePickerType, SIGNAL(currentIndexChanged(int)), this, SLOT(handleTimeTypeChanged(int))); + ui.datePickerType->setCurrentIndex(ui.datePickerType->findData(qVariantFromValue((uint8_t) TimeType::MONTHS_3))); + + //QObject::connect(ui.generateLicense, SIGNAL(clicked()), this, SLOT(handleGenerateLicense())); + QObject::connect(ui.registerLicense, SIGNAL(clicked()), this, SLOT(handleRegisterLicense())); + QObject::connect(ui.username, SIGNAL(textChanged(const QString&)), this, SLOT(handleInformationChanged())); + QObject::connect(ui.email, SIGNAL(textChanged(const QString&)), this, SLOT(handleInformationChanged())); + QObject::connect(ui.name_last, SIGNAL(textChanged(const QString&)), this, SLOT(handleInformationChanged())); + QObject::connect(ui.name_first, SIGNAL(textChanged(const QString&)), this, SLOT(handleInformationChanged())); + + this->handleInformationChanged(); + this->setAttribute(Qt::WA_DeleteOnClose); +} + +LicenseGenerator::~LicenseGenerator() {} + +void LicenseGenerator::handleTimeTypeChanged(int type) { + if(type == TimeType::COSTUME) { + ui.datePicker->setEnabled(true); + ui.datePicker->setDateTime(QDateTime::currentDateTimeUtc()); + } else { + ui.datePicker->setEnabled(false); + auto current = system_clock::now(); + switch (type){ + case TimeType::YEARS_1: + current += hours(24 * 30 * 12); + break; + case TimeType::MONTHS_6: + current += hours(24 * 30 * 6); + break; + case TimeType::MONTHS_3: + current += hours(24 * 30 * 3); + break; + case TimeType::MONTHS_1: + current += hours(24 * 30 * 1); + break; + case TimeType::PERM: + current = system_clock::time_point(); + break; + default: + break; + } + ui.datePicker->setDateTime(QDateTime::fromMSecsSinceEpoch(duration_cast(current.time_since_epoch()).count())); + } +} + +#define BACKGROUND(var, color) \ +do { \ + QPalette pal = (var)->palette(); \ + pal.setColor(QPalette::ColorRole::Base, color); \ + (var)->setPalette(pal); \ +} while(false) + +bool LicenseGenerator::validInput() { + bool error = false; + if(ui.username->text().isEmpty()) { + BACKGROUND(ui.username, QColor(255, 0, 0)); + error |= true; + } else + BACKGROUND(ui.username, QColor(255, 255, 255)); + + if(ui.email->text().isEmpty()) { + BACKGROUND(ui.email, QColor(255, 0, 0)); + error |= true; + } else BACKGROUND(ui.email, QColor(255, 255, 255)); + + if(ui.name_last->text().isEmpty()) { + BACKGROUND(ui.name_last, QColor(255, 0, 0)); + error |= true; + } else BACKGROUND(ui.name_last, QColor(255, 255, 255)); + + if(ui.name_first->text().isEmpty()) { + BACKGROUND(ui.name_first, QColor(255, 0, 0)); + error |= true; + } else BACKGROUND(ui.name_first, QColor(255, 255, 255)); + return !error; +} + +void LicenseGenerator::handleInformationChanged() { + std::string info = ui.username->text().toStdString() + "(" + ui.email->text().toStdString() + ")"; + + QPalette pal = this->ui.character_counter->palette(); + if(info.length() >= 64) { + this->ui.character_counter->setText(QString::fromStdString("Input is " + to_string(info.length() - 63) + " characters to long!")); + pal.setColor(QPalette::ColorRole::Foreground, QColor(0xFF, 0, 0)); + } else { + this->ui.character_counter->setText(QString::fromStdString(to_string(64 - info.length()) + " characters left")); + pal.setColor(QPalette::ColorRole::Foreground, QColor(0, 0, 0)); + } + this->ui.character_counter->setPalette(pal); +} + +void LicenseGenerator::handleGenerateLicense() { + if(!validInput()) { + QMessageBox::warning(this, "Invalid arguments", "Please check your provided arguments"); + return; + } + + std::string info = ui.username->text().toStdString() + "(" + ui.email->text().toStdString() + ")"; + if(info.length() >= 64) { + QMessageBox::warning(this, "Invalid arguments", "Username + E-Mail are too long!"); + return; + } + + system_clock::time_point duration; + duration += milliseconds(ui.datePicker->dateTime().toMSecsSinceEpoch()); + + if(duration.time_since_epoch().count() != 0 && system_clock::now() > duration) { + auto res = QMessageBox::warning(this, "Invalid arguments", "Invalid end time. Are you sure you want to create this license?", QMessageBox::Ok | QMessageBox::Abort); + if(res != QMessageBox::Ok) return; + } + + auto ltType = (license::LicenseType) ui.licenseType->itemData(ui.licenseType->currentIndex()).toInt(); + + auto license = license::createLocalLicence(ltType, duration, info); + ui.license->setText(QString::fromStdString(license)); + QMessageBox::information(this, "License", "License successfully generated!"); +} + +extern ServerConnection* connection; +void LicenseGenerator::handleRegisterLicense() { + if(!validInput()) { + QMessageBox::warning(this, "Invalid arguments", "Please check your provided arguments"); + return; + } + + std::string info = ui.username->text().toStdString() + "(" + ui.email->text().toStdString() + ")"; + if(info.length() >= 64) { + QMessageBox::warning(this, "Invalid arguments", "Username + E-Mail are too long!"); + return; + } + + system_clock::time_point duration; + duration += milliseconds(ui.datePicker->dateTime().toMSecsSinceEpoch()); + + if(duration.time_since_epoch().count() != 0 && system_clock::now() > duration) { + auto res = QMessageBox::warning(this, "Invalid arguments", "Invalid end time. Are you sure you want to create this license?", QMessageBox::Ok | QMessageBox::Abort); + if(res != QMessageBox::Ok) return; + } + + auto ltType = (license::LicenseType) ui.licenseType->itemData(ui.licenseType->currentIndex()).toInt(); + auto result = connection->registerLicense(ui.name_first->text().toStdString(), ui.name_last->text().toStdString(), ui.username->text().toStdString(), ui.email->text().toStdString(), ltType, duration); + result.waitAndGetLater([&, result](std::pair, std::shared_ptr>* response) { + if(result.state() == threads::FutureState::FAILED || !response) { + runOnThread(this->thread(), [&, result]{ + QMessageBox::warning(this, "Creation failed", QString::fromStdString("Failed to create license (" + result.errorMegssage() + ")")); + return; + }); + } else { + runOnThread(this->thread(), [&, result, response]{ + ui.license->setText(QString::fromStdString(license::exportLocalLicense(response->first))); + QMessageBox::information(this, "License", "License successfully generated and registered!"); + }); + } + }); +} \ No newline at end of file diff --git a/license/manager/ui/LicenseGenerator.h b/license/manager/ui/LicenseGenerator.h new file mode 100644 index 0000000..49f440c --- /dev/null +++ b/license/manager/ui/LicenseGenerator.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +namespace license { + namespace ui { + class LicenseGenerator : public QMainWindow { + Q_OBJECT; + public: + LicenseGenerator(QWidget*); + ~LicenseGenerator(); + + private slots: + void handleTimeTypeChanged(int); + void handleGenerateLicense(); + void handleRegisterLicense(); + void handleInformationChanged(); + private: + Ui::LicenseGenerator ui; + + bool validInput(); + }; + } +} \ No newline at end of file diff --git a/license/manager/ui/LoginWindow.cpp b/license/manager/ui/LoginWindow.cpp new file mode 100644 index 0000000..fda911b --- /dev/null +++ b/license/manager/ui/LoginWindow.cpp @@ -0,0 +1,68 @@ +#include +#include "LicenseGenerator.h" +#include +#include +#include "LoginWindow.h" +#include "manager/qtHelper.h" +#include "Overview.h" + +using namespace license; +using namespace license::manager; +using namespace license::ui; +using namespace std; +using namespace std::chrono; + +LoginWindow::LoginWindow() : QDialog(nullptr) { + this->ui.setupUi(this); + this->setFixedSize(this->size()); + + QObject::connect(ui.btn_login, SIGNAL(clicked()), this, SLOT(onClickConnect())); + //this->setAttribute(Qt::WA_DeleteOnClose); +} + +LoginWindow::~LoginWindow() { } + +extern ServerConnection* connection; +extern QWidget* current_widget; +void LoginWindow::onClickConnect() { + progress = unique_ptr(new QProgressDialog(this)); + progress->setMinimum(0); + progress->setMaximum(2); + progress->setLabelText("Connecting..."); + progress->show(); + + auto future = connection->connect("0.0.0.0", 27786); + future.waitAndGetLater([&, future](bool* flag) { + runOnThread(this->thread(), [&, future, flag](){ + if(!flag || !*flag) { + if(future.state() == threads::FutureState::WORKING) + QMessageBox::critical(this, "Connect error", "Could not connect to remote!
Error: Connect timeout"); + else + QMessageBox::critical(this, "Connect error", QString::fromStdString("Could not connect to remote!
Error: " + future.errorMegssage())); + progress.reset(); + connection->disconnect("timeout"); + } else { + progress->setValue(1); + progress->setLabelText("Logging in"); + auto login_future = connection->login(this->ui.field_user->text().toStdString(), this->ui.field_password->text().toStdString()); + login_future.waitAndGetLater([&, login_future](bool* flag){ + runOnThread(this->thread(), [&, login_future, flag](){ + if(!flag || !*flag) { + if(future.state() == threads::FutureState::WORKING) + QMessageBox::critical(this, "Login error", "Could not login!
Error: Login timeout"); + else + QMessageBox::critical(this, "Login error", QString::fromStdString("Could not login!
Error: " + future.errorMegssage())); + progress.reset(); + connection->disconnect("timeout"); + } else { + current_widget = new Overview(); + current_widget->show(); + progress.reset(); + this->close(); + } + }); + }, system_clock::now() + seconds(5)); + } + }); + }, system_clock::now() + seconds(5)); +}; \ No newline at end of file diff --git a/license/manager/ui/LoginWindow.h b/license/manager/ui/LoginWindow.h new file mode 100644 index 0000000..d467d84 --- /dev/null +++ b/license/manager/ui/LoginWindow.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace license { + namespace ui { + class LoginWindow : public QDialog { + Q_OBJECT; + public: + LoginWindow(); + virtual ~LoginWindow(); + + private slots: + void onClickConnect(); + private: + std::unique_ptr progress; + Ui::LoginWindow ui; + }; + } +} \ No newline at end of file diff --git a/license/manager/ui/Overview.cpp b/license/manager/ui/Overview.cpp new file mode 100644 index 0000000..dd84d80 --- /dev/null +++ b/license/manager/ui/Overview.cpp @@ -0,0 +1,173 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "LicenseGenerator.h" +#include "Overview.h" +#include "UiLicenseInfo.h" + +using namespace license; +using namespace license::manager; +using namespace license::ui; +using namespace std; +using namespace std::chrono; + +extern ServerConnection* connection; +Overview::Overview() { + this->ui.setupUi(this); + + this->ui.licenses->setColumnCount(RowEntry::ENDMARKER); + this->ui.licenses->setHorizontalHeaderItem(RowEntry::ID, new QTableWidgetItem("ID")); + this->ui.licenses->setHorizontalHeaderItem(RowEntry::USERNAME, new QTableWidgetItem("Username")); + this->ui.licenses->setHorizontalHeaderItem(RowEntry::NAME, new QTableWidgetItem("Name")); + this->ui.licenses->setHorizontalHeaderItem(RowEntry::END, new QTableWidgetItem("End timestamp")); + this->ui.licenses->setHorizontalHeaderItem(RowEntry::ACTIVE, new QTableWidgetItem("Active")); + this->ui.licenses->setHorizontalHeaderItem(RowEntry::ACTION_EDIT, new QTableWidgetItem("")); + this->ui.licenses->setHorizontalHeaderItem(RowEntry::ACTION_DELETE, new QTableWidgetItem("")); + + this->ui.licenses->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); + this->ui.licenses->verticalHeader()->hide(); + this->ui.licenses->setSelectionBehavior(QAbstractItemView::SelectRows); + //this->ui.licenses->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); + + QObject::connect(this->ui.btn_create_license, SIGNAL(clicked()), this, SLOT(clickedNewLicense())); + QObject::connect(this->ui.btn_refresh, SIGNAL(clicked()), this, SLOT(btn_refresh_clicked())); + + if(!connection) { + auto test = make_shared(); + test->type = LicenseType::PREMIUM; + test->email = "test@test.de"; + test->first_name = "Markus"; + test->last_name = "Hadenfeldt"; + test->username = "WolverinDEV"; + test->end = system_clock::now() + minutes(60); + test->creation = system_clock::now(); + test->start = system_clock::now(); + addLicenseEntry("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", test); + } else { + this->refresh_license(); + } +} + +Overview::~Overview() {} + +#define M(var) \ +var = new QTableWidgetItem(); \ +var->setData(Qt::UserRole, qVariantFromValue(this)); \ +var->setFlags(var->flags() & ~Qt::ItemIsEditable) + +RowEntry::RowEntry(const shared_ptr &license, const string &licenseId) : license(license), licenseId(licenseId) { + M(widget_id); + M(widget_username); + M(widget_name); + M(widget_end_timestamp); + M(widget_active); + + widget_id->setText(QString::fromStdString(base64::encode(licenseId))); + widget_username->setText(QString::fromStdString(license->username)); + widget_name->setText(QString::fromStdString(license->first_name + " " + license->last_name)); + + { + if(license->end.time_since_epoch() > hours(1)) { + char buffer[128]; + auto tm = chrono::system_clock::to_time_t(license->end); + strftime(buffer, sizeof(buffer), "%Y-%m-%d.%X", localtime(&tm)); + widget_end_timestamp->setText(buffer); + } else { + widget_end_timestamp->setText("never"); + } + } + + widget_active->setTextColor(license->isValid() ? Qt::green : Qt::red); + widget_active->setText(QString::fromStdString(license->isValid() ? "yes" : "no")); + + this->button_delete = new QPushButton(); + this->button_edit = new QPushButton(); + this->button_delete->setIcon(QApplication::style()->standardIcon(QStyle::SP_BrowserStop)); + this->button_edit->setIcon(QApplication::style()->standardIcon(QStyle::SP_DialogOpenButton)); + + QObject::connect(this->button_edit, SIGNAL(clicked()), this, SLOT(btn_edit_clicked())); + QObject::connect(this->button_delete, SIGNAL(clicked()), this, SLOT(btn_delete_clicked())); +} + +RowEntry::~RowEntry() {} + +void RowEntry::btn_delete_clicked() { + auto message = "Do you really want to delete this license?
" + "Name: " + this->license->first_name + " " + this->license->last_name + "
" + "Username: " + this->license->username; + if(QMessageBox::question(this->handle, "Are you sure?", QString::fromStdString(message)) == QMessageBox::Yes) { + bool full = QMessageBox::question(this->handle, "You want a full delete?", "Do you even want to erase the license from the database?") == QMessageBox::Yes; + connection->deleteLicense(this->licenseId, full).waitAndGetLater([&](bool flag) { + if(!flag) + runOnThread(this->thread(), [&]{ + QMessageBox::warning(this->handle, "Delete error", "Failed to delete license!"); + }); + else + this->handle->refresh_license(); + }, false); + } +} + +void RowEntry::btn_edit_clicked() { + auto info = new ui::UiLicenseInfo(this->license, this->licenseId, this->handle); + info->show(); +} + +void Overview::addLicenseEntry(std::string licenseId, shared_ptr info) { + if(this->thread() != QThread::currentThread()) { + runOnThread(this->thread(), [&, licenseId, info]{ + this->addLicenseEntry(licenseId, info); + }); + return; + } + auto entry = make_shared(info, licenseId); + entry->handle = this; + + int row = this->ui.licenses->rowCount(); + this->ui.licenses->insertRow(row); + this->ui.licenses->setItem(row, RowEntry::ID, entry->widget_id); + this->ui.licenses->setItem(row, RowEntry::USERNAME, entry->widget_username); + this->ui.licenses->setItem(row, RowEntry::NAME, entry->widget_name); + this->ui.licenses->setItem(row, RowEntry::END, entry->widget_end_timestamp); + this->ui.licenses->setItem(row, RowEntry::ACTIVE, entry->widget_active); + + + this->ui.licenses->setIndexWidget(this->ui.licenses->model()->index(row, RowEntry::ACTION_EDIT), entry->button_edit); + this->ui.licenses->setIndexWidget(this->ui.licenses->model()->index(row, RowEntry::ACTION_DELETE), entry->button_delete); + + this->entries.push_back(entry); +} + +void Overview::clickedNewLicense() { + auto instance = new LicenseGenerator(this); + instance->show(); +} + +void Overview::refresh_license() { + this->btn_refresh_clicked(); +} + +void Overview::btn_refresh_clicked() { + runOnThread(this->thread(), [&]{ + while(this->ui.licenses->rowCount() > 0) + this->ui.licenses->removeRow(0); + this->entries.clear(); + + auto fut = connection->list(0, 0); + fut.waitAndGetLater([&, fut](std::map> response) { + if(!fut.succeeded()) { + runOnThread(this->thread(), [&, fut](){ + QMessageBox::warning(this, "Failed to load data", "Failed to load data:
" + QString::fromStdString(fut.errorMegssage())); + }); + } else { + for(const auto& entry : response) + this->addLicenseEntry(entry.first, entry.second); + } + }, {}); + }); +} \ No newline at end of file diff --git a/license/manager/ui/Overview.h b/license/manager/ui/Overview.h new file mode 100644 index 0000000..d8db7af --- /dev/null +++ b/license/manager/ui/Overview.h @@ -0,0 +1,69 @@ +#pragma once + +#include "shared/License.h" +#include +#include +#include +#include +#include +#include +#include + +namespace license { + namespace ui { + class Overview; + struct RowEntry : public QObject { + Q_OBJECT; + public: + enum Row { + ID, + USERNAME, + NAME, + END, + ACTIVE, + + ACTION_EDIT, + ACTION_DELETE, + + ENDMARKER + }; + + Overview* handle; + + QTableWidgetItem* widget_id; + QTableWidgetItem* widget_username; + QTableWidgetItem* widget_name; + QTableWidgetItem* widget_end_timestamp; + QTableWidgetItem* widget_active; + + QPushButton* button_edit; + QPushButton* button_delete; + + std::shared_ptr license; + std::string licenseId; + + RowEntry(const std::shared_ptr &license, const std::string &licenseId); + ~RowEntry(); + private slots: + void btn_edit_clicked(); + void btn_delete_clicked(); + }; + + class Overview : public QMainWindow { + Q_OBJECT; + public: + Overview(); + virtual ~Overview(); + + void refresh_license(); + private slots: + void clickedNewLicense(); + void btn_refresh_clicked(); + private: + Ui::Overview ui; + std::deque> entries; + + void addLicenseEntry(std::string, std::shared_ptr); + }; + } +} \ No newline at end of file diff --git a/license/manager/ui/UiLicenseInfo.cpp b/license/manager/ui/UiLicenseInfo.cpp new file mode 100644 index 0000000..ab9a580 --- /dev/null +++ b/license/manager/ui/UiLicenseInfo.cpp @@ -0,0 +1,112 @@ +#include +#include + +#include "manager/ServerConnection.h" +#include "UiLicenseInfo.h" +#include "LoginWindow.h" +#include "manager/qtHelper.h" +#include "Overview.h" + +using namespace license; +using namespace license::manager; +using namespace license::ui; +using namespace std; +using namespace std::chrono; + +#define q(str) QString::fromStdString(str) + +UiLicenseInfo::UiLicenseInfo(const std::shared_ptr &info, const std::string &key, QWidget* h) : QDialog(h), key(key), info(info) { + this->ui.setupUi(this); + this->set_editable(this->flag_editable); + + this->ui.edit_name_first->setText(q(info->first_name)); + this->ui.edit_name_last->setText(q(info->last_name)); + this->ui.edit_email->setText(q(info->email)); + this->ui.edit_username->setText(q(info->username)); + + this->ui.edit_time_created->setSpecialValueText("never"); + this->ui.edit_time_begin->setSpecialValueText("never"); + this->ui.edit_time_end->setSpecialValueText("never"); + this->ui.edit_time_created->setDateTime(QDateTime::fromMSecsSinceEpoch(duration_cast(this->info->creation.time_since_epoch()).count())); + this->ui.edit_time_begin->setDateTime(QDateTime::fromMSecsSinceEpoch(duration_cast(this->info->start.time_since_epoch()).count())); + this->ui.edit_time_end->setDateTime(QDateTime::fromMSecsSinceEpoch(duration_cast(this->info->end.time_since_epoch()).count())); + + this->update_length(); + + QObject::connect(this->ui.btn_edit, SIGNAL(clicked()), this, SLOT(btn_edit_clicked())); +} + +UiLicenseInfo::~UiLicenseInfo() {} + +//The VA ars are for my ide +#define M(var, ...) \ +do { \ + (var)->setFrame(flag); \ + (var)->setReadOnly(!flag); \ + QPalette p = (var)->palette(); \ + p.setColor(QPalette::Base, QColor(255,255,255,flag ? 255 : 0)); \ + (var)->setPalette(p); \ +} while(0) + +void UiLicenseInfo::set_editable(bool flag) { + M(this->ui.edit_email); + M(this->ui.edit_name_first); + M(this->ui.edit_name_last); + M(this->ui.edit_username); + M(this->ui.edit_time_created); + M(this->ui.edit_time_begin); + M(this->ui.edit_time_end); + + if(flag) { + this->ui.edit_time_created->setButtonSymbols(QAbstractSpinBox::UpDownArrows); + this->ui.edit_time_begin->setButtonSymbols(QAbstractSpinBox::UpDownArrows); + this->ui.edit_time_end->setButtonSymbols(QAbstractSpinBox::UpDownArrows); + } else { + this->ui.edit_time_created->setButtonSymbols(QAbstractSpinBox::NoButtons); + this->ui.edit_time_begin->setButtonSymbols(QAbstractSpinBox::NoButtons); + this->ui.edit_time_end->setButtonSymbols(QAbstractSpinBox::NoButtons); + } +} + +#undef M + +void UiLicenseInfo::btn_edit_clicked() { + this->set_editable(this->flag_editable ^= 1); +} + +using days = std::chrono::duration, std::chrono::hours::period>>; +using years = std::chrono::duration, days::period>>; + +//The VA ars are for my ide +#define M(unit, name, ...) \ +do { \ + auto num = duration_cast(length); \ + if(num.count() > 0) { \ + result += " " + to_string(num.count()) + " " + name; \ + length -= num; \ + } \ +} while (0) + +void UiLicenseInfo::update_length() { + if(this->info->end.time_since_epoch().count() == 0) + this->ui.text_length->setText("unlimited"); + else { + if(this->info->end < this->info->start) { + this->ui.text_length->setText("error"); + } else { + auto length = this->info->end - this->info->start; + length += seconds(1); + + string result; + M(years, "years"); + M(days, "days"); + M(hours, "hours"); + M(minutes, "minutes"); + M(seconds, "seconds"); + if(!result.empty()) + result = result.substr(1); + + this->ui.text_length->setText(q(result)); + } + } +} \ No newline at end of file diff --git a/license/manager/ui/UiLicenseInfo.h b/license/manager/ui/UiLicenseInfo.h new file mode 100644 index 0000000..db6c59b --- /dev/null +++ b/license/manager/ui/UiLicenseInfo.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include +#include + +namespace license { + namespace ui { + class UiLicenseInfo : public QDialog { + Q_OBJECT; + public: + UiLicenseInfo(const std::shared_ptr& info, const std::string& key, QWidget*); + ~UiLicenseInfo(); + + private slots: + void btn_edit_clicked(); + + private: + std::string key; + std::shared_ptr info; + Ui::LicenseInfo ui; + + bool flag_editable = false; + void set_editable(bool); + void update_length(); + }; + } +} \ No newline at end of file diff --git a/license/manager/ui/licensegenerator.ui b/license/manager/ui/licensegenerator.ui new file mode 100644 index 0000000..ea5d9e5 --- /dev/null +++ b/license/manager/ui/licensegenerator.ui @@ -0,0 +1,351 @@ + + + LicenseGenerator + + + Qt::NonModal + + + + 0 + 0 + 681 + 444 + + + + + Ubuntu Mono + 12 + + + + TeaSpeak license generator + + + QTabWidget::Triangular + + + + + + + + + + 200 + 20 + + + + + 12 + + + + Qt::RightToLeft + + + License + + + + + + + true + + + + 0 + 0 + + + + + 100 + 130 + + + + + 16777215 + 300 + + + + Qt::RightToLeft + + + asdasdasd + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><title>asdasdasd</title><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Ubuntu Mono'; font-size:12pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Ubuntu'; font-size:11pt;"><br /></p></body></html> + + + Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + generated license + + + + + + + Qt::Vertical + + + QSizePolicy::MinimumExpanding + + + + 20 + 0 + + + + + + + + + + Register License + + + + + + + + + + 0 + 0 + + + + License Information + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + First Name: + + + + + + + + 600 + 16777215 + + + + + + + + Last Name: + + + + + + + + 600 + 16777215 + + + + + + + + + 200 + 16777215 + + + + + 12 + + + + Username: + + + + + + + + 600 + 16777215 + + + + + + + + + 12 + + + + E-Mail: + + + + + + + + 600 + 16777215 + + + + + + + + + 200 + 16777215 + + + + Qt::LeftToRight + + + 30 Characters left + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + QLayout::SetDefaultConstraint + + + QFormLayout::AllNonFixedFieldsGrow + + + + + + 12 + + + + Duration + + + + + + + + 200 + 16777215 + + + + + + + + + 12 + + + + License Type: + + + + + + + + 200 + 16777215 + + + + + + + + + 200 + 16777215 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + 0 + 0 + 681 + 24 + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + diff --git a/license/manager/ui/licenseinfo.ui b/license/manager/ui/licenseinfo.ui new file mode 100644 index 0000000..e4b7ea5 --- /dev/null +++ b/license/manager/ui/licenseinfo.ui @@ -0,0 +1,242 @@ + + + LicenseInfo + + + + 0 + 0 + 782 + 541 + + + + + 0 + 0 + + + + + 400 + 0 + + + + Form + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + enable edit + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + First Name + + + + + + + true + + + + + + + Last Name + + + + + + + true + + + + + + + E-Mail + + + + + + + true + + + + + + + Username + + + + + + + + + true + + + + + + + Forum Profile + + + false + + + false + + + false + + + false + + + false + + + + + + + + + Created + + + + + + + true + + + false + + + + + + + License begin + + + + + + + + + + License end + + + + + + + Status + + + + + + + TextLabel + + + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + 200 + 0 + + + + + 1200 + 16777215 + + + + 1 Tag 20 Stunden + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + diff --git a/license/manager/ui/loginwindow.ui b/license/manager/ui/loginwindow.ui new file mode 100644 index 0000000..b6bea07 --- /dev/null +++ b/license/manager/ui/loginwindow.ui @@ -0,0 +1,92 @@ + + + LoginWindow + + + + 0 + 0 + 330 + 105 + + + + Login + + + + + + + + <html><head/><body><p><span style=" font-size:14pt;">Username:</span></p></body></html> + + + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-size:14pt;">Password:</span></p></body></html> + + + + + + + + + + + 200 + 0 + + + + + + + + + + + + + 0 + 0 + + + + + 200 + 0 + + + + + 800 + 16777215 + + + + + 12 + + + + login + + + + + + + + + + diff --git a/license/manager/ui/owerview.ui b/license/manager/ui/owerview.ui new file mode 100644 index 0000000..d291ef2 --- /dev/null +++ b/license/manager/ui/owerview.ui @@ -0,0 +1,151 @@ + + + Overview + + + + 0 + 0 + 1219 + 554 + + + + Overview + + + + + + + + + + 150 + 0 + + + + Refresh + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + Qt::RightToLeft + + + Create license + + + + + + + + + + + + + + Last side + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 12 + + + + 1/2 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Next side + + + + + + + + + + + 0 + 0 + 1219 + 19 + + + + + Disconnect + + + + + + + + + diff --git a/license/packets/LicenseManager.proto b/license/packets/LicenseManager.proto new file mode 100644 index 0000000..83a91fb --- /dev/null +++ b/license/packets/LicenseManager.proto @@ -0,0 +1,54 @@ +syntax = "proto2"; +package ts.proto.license; + +import "LicenseRequest.proto"; + +//Requesting stuff +message LicenseListRequest { + optional int64 offset = 1; + optional int64 count = 2; +} +message LicenseListResponse { + repeated LicenseInfo entries = 1; + required bool end = 2; +} + +//Deleting stuff +message LicenseDeleteRequest { + required bytes key = 1; + required bool full = 2; +} +message LicenseDeleteResponse { + required bool succeed = 1; +} + +//License request +message LicenseCreateRequest { + required string issuer_username = 1; + required string issuer_first_name = 2; + required string issuer_last_name = 3; + required string issuer_email = 4; + + required int64 type = 5; + required int64 begin = 6; + required int64 end = 7; +} +message LicenseCreateResponse { + oneof result { + string error = 2; + LicenseInfo license = 3; + } + optional string exported_key = 4; +} + +//Auth +message AuthorizationRequest { + required string username = 1; + required string password = 2; +} +message AuthorizationResponse { + oneof result { + string message = 1; + bool success = 2; + } +} \ No newline at end of file diff --git a/license/packets/LicenseRequest.proto b/license/packets/LicenseRequest.proto new file mode 100644 index 0000000..965f9b4 --- /dev/null +++ b/license/packets/LicenseRequest.proto @@ -0,0 +1,72 @@ +syntax = "proto2"; + +package ts.proto.license; + +enum BlacklistState { + VALID = 0; + SUSPICIOUS = 1; + BLACKLISTED = 2; +} + +message Blacklist { + required BlacklistState state = 1; + optional string reason = 2; +} + + +message LicenseInfo { + required bytes key = 1; + required string username = 2; + required string first_name = 3; + required string last_name = 4; + required string email = 5; + required int32 type = 6; + required int64 begin = 7; + required int64 end = 8; + required int64 created = 9; + + reserved + 10, //music_limit + 11; //speach_limit +} + +message ServerInfo { + required string version = 1; + required string uname = 2; + required int64 timestamp = 3; + optional string unique_id = 4; +} + +message ServerValidation { + required bool licensed = 1; + required bool license_info = 2; + optional bytes license = 3; + optional ServerInfo info = 4; //Change somewhen to required but its currently for lagacy support +} + +message LicenseResponse { + required bool valid = 1; + required Blacklist blacklist = 2; + optional LicenseInfo license_info = 3; //Only availible when ServerValidation::license_info = true +} + +message PropertyUpdateRequest { + required int64 speach_total = 1; + required int64 speach_dead = 2; + required int64 speach_online = 3; + required int64 speach_varianz = 4; + + required int64 clients_online = 7; + required int64 web_clients_online = 8; + required int64 bots_online = 9; + required int64 queries_online = 10; + required int64 servers_online = 11; +} + +message PropertyUpdateResponse { + required bool accepted = 1; + required int64 speach_total_remote = 2; + required int64 speach_varianz_corrector = 3; + + optional bool reset_speach = 4; +} \ No newline at end of file diff --git a/license/server/KeyIdCache.cpp b/license/server/KeyIdCache.cpp new file mode 100644 index 0000000..f148af9 --- /dev/null +++ b/license/server/KeyIdCache.cpp @@ -0,0 +1,67 @@ +#include "log/LogUtils.h" +#include "LicenseManager.h" + +using namespace license; +using namespace license::server; +using namespace std; +using namespace std::chrono; +using KeyIdCache = LicenseManager::KeyIdCache; + +KeyIdCache::KeyIdCache(license::server::LicenseManager *handle) : handle(handle) {} + +std::string KeyIdCache::getKey(size_t keyId) { + { + threads::MutexLock lock(this->entry_lock); + + for(const auto& entry : this->entries) + if(entry->keyId == keyId) return entry->key; + } + + sql::command(this->handle->database, "SELECT `key`, `keyId` FROM `license` WHERE `keyId` = :key", variable{":key", keyId}) + .query(&KeyIdCache::insert_entry, this); + { + threads::MutexLock lock(this->entry_lock); + + for(const auto& entry : this->entries) + if(entry->keyId == keyId) return entry->key; + return ""; //Key not found! + } +} + +size_t KeyIdCache::getKeyId(const std::string &key) { + { + threads::MutexLock lock(this->entry_lock); + + for(const auto& entry : this->entries) + if(entry->key == key) return entry->keyId; + } + + auto result = sql::command(this->handle->database, "SELECT `key`, `keyId` FROM `license` WHERE `key` = :key", variable{":key", key}) + .query(&KeyIdCache::insert_entry, this); + if(!result) + logError(LOG_GENERAL, "Failed to query key id for license. Query resulted in {}", result.fmtStr()); + { + threads::MutexLock lock(this->entry_lock); + + for(const auto& entry : this->entries) + if(entry->key == key) + return entry->keyId; + + return 0; //Key not found! + } +} + +int KeyIdCache::insert_entry(int length, std::string *value, std::string *names) { + string key; + size_t keyId = 0; + for(int index = 0; index < length; index++) + if(names[index] == "key") + key = value[index]; + else if(names[index] == "keyId") + keyId = stoll(value[index]); + { + threads::MutexLock lock(this->entry_lock); + this->entries.push_back(new KeyIdCache::CacheEntry{key, keyId, system_clock::now()}); + } + return 0; +} \ No newline at end of file diff --git a/license/server/LicenseManager.cpp b/license/server/LicenseManager.cpp new file mode 100644 index 0000000..4f95606 --- /dev/null +++ b/license/server/LicenseManager.cpp @@ -0,0 +1,463 @@ +#include "LicenseManager.h" +#include + +using namespace std; +using namespace std::chrono; +using namespace license; +using namespace license::server; +using namespace sql; + +LicenseManager::LicenseManager(sql::SqlManager* database) : database(database){ + this->id_cache = make_shared(this); +} +LicenseManager::~LicenseManager() { } + +/* + LicenseType type; + std::string username; + std::string first_name; + std::string last_name; + std::string email; + std::chrono::system_clock::time_point start; + std::chrono::system_clock::time_point end; + std::chrono::system_clock::time_point creation; + */ + +#define CTBL(cmd) \ +res = command(this->database, cmd).execute(); \ + if(!res) { \ + error = "Could not setup tables! (" + res.fmtStr() + ")"; \ + return false; \ +} +#define CIDX(cmd) \ +res = command(this->database, cmd).execute(); \ + if(!res && res.msg().find("Duplicate") == string::npos && res.msg().find("exist") == string::npos) { \ + error = "Could not setup indexes! (" + res.fmtStr() + ")"; \ + return false; \ +} + +#define SET_VERSION(ver) \ +version = ver; \ +sql::command(this->database, "UPDATE `general` SET `value` = :version WHERE `key` = 'version'", variable{":version", version}).execute(); + +bool LicenseManager::setup(std::string& error) { + result res; + int version = -1; + sql::command(this->database, "SELECT `value` FROM `general` WHERE `key` = 'version'").query([](int* version, int lnegth, string* values, string* names) { + *version = stoll(values[0]); + return 0; + }, &version); + + + switch (version) { + case -1: + CTBL("CREATE TABLE IF NOT EXISTS `license` (`key` BINARY(64) NOT NULL PRIMARY KEY, `type` INT, `deleted` BOOL, `issuer` TEXT)"); + CTBL("CREATE TABLE IF NOT EXISTS `license_info` (`key` BINARY(64) NOT NULL PRIMARY KEY, `username` VARCHAR(128), `first_name` TEXT, `last_name` TEXT, `email` TEXT, `begin` BIGINT, `end` BIGINT, `generated` BIGINT)"); + CTBL("CREATE TABLE IF NOT EXISTS `license_request` (`key` BINARY(64) NOT NULL, `ip` TEXT, `timestamp` BIGINT, `result` INT)"); + CTBL("CREATE TABLE IF NOT EXISTS `general` (`key` VARCHAR(64), `value` TEXT)"); + CTBL("INSERT INTO `general` (`key`, `value`) VALUES ('version', '0');"); + + SET_VERSION(0); + case 0: + logMessage("Updating database! To version 1"); + CTBL("CREATE TABLE IF NOT EXISTS `history_speach` (`keyId` INT, `timestamp` BIGINT, `total` BIGINT, `dead` BIGINT, `online` BIGINT, `varianz` BIGINT)"); + CTBL("CREATE TABLE IF NOT EXISTS `history_online` (`keyId` INT, `timestamp` BIGINT, `server` INT, `clients` INT, `web` INT, `music` INT, `queries` INT)"); + + CIDX("CREATE INDEX `license_key` ON `license` (`key`)"); + CIDX("CREATE INDEX `license_info_key` ON `license_info` (`key`)"); + + CTBL("ALTER TABLE `license` DROP PRIMARY KEY, ADD `keyId` INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST;"); + CTBL("ALTER TABLE `license_info` DROP PRIMARY KEY, ADD `keyId` INT NOT NULL PRIMARY KEY AUTO_INCREMENT FIRST;"); + //Fixing key id's + CTBL("UPDATE `license_info` LEFT JOIN `license` ON `license`.`key` = `license_info`.`key` SET `license_info`.`keyId` = `license`.`keyId`"); + //Deleting the key blob and removing auto + CTBL("ALTER TABLE `license_info` CHANGE `keyId` `keyId` INT NOT NULL UNIQUE, DROP COLUMN `key`"); + + //Fixing request table + CTBL("ALTER TABLE license_request ADD COLUMN `keyId` INT(0) NOT NULL;"); + CTBL("UPDATE license_request LEFT JOIN license ON license.key = license_request.key SET license_request.keyId = license.keyId;"); + CTBL("ALTER TABLE license_request DROP COLUMN `key`;"); + + //IDK why but i failed stuff :D + CTBL("ALTER TABLE license DROP PRIMARY KEY, CHANGE `keyId` `keyId` INT NOT NULL AUTO_INCREMENT PRIMARY KEY"); + SET_VERSION(1); + + case 1: + CTBL("ALTER TABLE history_online ADD COLUMN `ip` TEXT AFTER `keyId`;"); + CTBL("ALTER TABLE history_speach ADD COLUMN `ip` TEXT AFTER `keyId`;"); + SET_VERSION(2); + + case 2: + CTBL("CREATE TABLE `history_version` (`keyId` INT, `ip` VARCHAR(64), `timestamp` BIGINT, `version` VARCHAR(126));"); + SET_VERSION(3); + + case 3: + /* + CTBL("CREATE TABLE `save_history_online` AS SELECT * FROM `history_online`;"); + CTBL("CREATE TABLE `save_history_speach` AS SELECT * FROM `history_speach`;"); + CTBL("CREATE TABLE `save_history_version` AS SELECT * FROM `history_version`;"); + */ + + CTBL("ALTER TABLE `history_online` CHANGE `ip` `unique_id` VARCHAR(64) NOT NULL;"); + CTBL("ALTER TABLE `history_speach` CHANGE `ip` `unique_id` VARCHAR(64) NOT NULL;"); + CTBL("ALTER TABLE `history_version` CHANGE `ip` `unique_id` VARCHAR(64) NOT NULL;"); + CTBL("ALTER TABLE `license_request` ADD `unique_id` VARCHAR(64) NOT NULL AFTER `ip`;"); + SET_VERSION(4); + + case 4: + CTBL("CREATE TABLE IF NOT EXISTS `users` (`username` VARCHAR(64) NOT NULL PRIMARY KEY, `password_hash` VARCHAR(128), `status` INT, `owner` VARCHAR(64))"); + + default:; + } + return true; +} + +bool LicenseManager::registerLicense(const std::string& key, const shared_ptr& info, const std::string& issuer) { + result res; + res = command(this->database, "INSERT INTO `license` (`key`, `type`, `deleted`, `issuer`) VALUES (:key, :type, :deleted, :issuer)", variable{":key", key}, variable{":type", (uint32_t) info->type}, variable{":deleted", false}, variable{":issuer", issuer}).execute(); + if(!res) { + logError("Could not register new license (" + res.fmtStr() + ")"); + return false; + } + auto keyId = this->id_cache->getKeyId(key); + if(keyId == 0) return false; + + res = command(this->database, "INSERT INTO `license_info` (`keyId`, `username`, `first_name`, `last_name`, `email`, `begin`, `end`, `generated`) VALUES (:key, :username, :first_name, :last_name, :email, :begin, :end, :generated)", + variable{":key", keyId}, + variable{":username", info->username}, + variable{":first_name", info->first_name}, + variable{":last_name", info->last_name}, + variable{":email", info->email}, + variable{":generated", duration_cast(info->creation.time_since_epoch()).count()}, + variable{":begin", duration_cast(info->start.time_since_epoch()).count()}, + variable{":end", duration_cast(info->end.time_since_epoch()).count()} + ).execute(); + if(!res) { + logError("Could not register new license info (" + res.fmtStr() + ")"); + return false; + } + return true; +} + +bool LicenseManager::deleteLicense(const std::string& key, bool full) { + if(full) { + auto keyId = this->id_cache->getKeyId(key); + if(keyId == 0) return false; //Never exists + + auto res = command(this->database, "DELETE FROM `license` WHERE `key` = :key", variable{":key", key}).execute(); + if(!res) logError("Could not delete license (" + res.fmtStr() + ")"); + res = command(this->database, "DELETE FROM `license_info` WHERE `keyId` = :key", variable{":keyId", keyId}).execute(); + if(!res) logError("Could not delete license (" + res.fmtStr() + ")"); + return !!res; + } else { + auto res = command(this->database, "UPDATE `license` SET `deleted` = :true WHERE `key` = :key", variable{":true", true}, variable{":key", key}).execute(); + if(!res) logError("Could not delete license (" + res.fmtStr() + ")"); + return !!res; + } +} + +bool LicenseManager::validLicenseKey(const std::string& key) { + bool valid = false; + auto res = command(this->database, "SELECT * FROM `license` WHERE `key` = :key AND `deleted` = :false LIMIT 1", variable{":false", false}, variable{":key", key}).query([](bool* flag, int, char**, char**) { + *flag = true; + return 0; + }, &valid); + if(!res) logError("Could not validate license (" + res.fmtStr() + ")"); + return !!res && valid; +} + +inline std::map> query_license(SqlManager* mgr, std::string key, int offset, int length) { + std::map> result; + + auto query = string() + "SELECT `key`, `username`, `first_name`, `last_name`, `email`, `begin`, `end`, `generated`, `deleted` FROM `license_info` INNER JOIN `license` ON `license_info`.`keyId` = `license`.`keyId`"; + if(!key.empty()) + query += "WHERE `key` = :key"; + else + query += "WHERE `deleted` = :false"; + + if(offset > 0 || length > 0) + query += " LIMIT " + to_string(offset) + ", " + to_string(length); + + logTrace(LOG_GENERAL, "Executing query {}", query); + auto res = command(mgr, query, variable{":key", key}, variable{":false", false}).query([](std::map>* list, int length, std::string* values, std::string* names) { + auto info = make_shared(); + info->deleted = false; + string k; + for(int index = 0; index < length; index++) { + try { + if(names[index] == "username") + info->username = values[index]; + else if(names[index] == "first_name") + info->first_name = values[index]; + else if(names[index] == "key") + k = values[index]; + else if(names[index] == "last_name") + info->last_name = values[index]; + else if(names[index] == "email") + info->email = values[index]; + else if(names[index] == "begin") + info->start = system_clock::time_point() + milliseconds(stoll(values[index])); + else if(names[index] == "end") + info->end = system_clock::time_point() + milliseconds(stoll(values[index])); + else if(names[index] == "generated") + info->creation = system_clock::time_point() + milliseconds(stoll(values[index])); + else if(names[index] == "deleted") + info->deleted = values[index] == "1" || values[index] == "true"; + else + logError(LOG_GENERAL, "Unknown field {}", names[index]); + } catch (std::exception& ex) { + logError(LOG_GENERAL, "Failed to parse field {} ({}). Message: {}", names[index], values[index], ex.what()); + return 1; + } + } + (*list)[k] = info; + return 0; + }, &result); + logTrace(LOG_GENERAL, "Query returned {} results", result.size()); + if(!res) logError("Could not query license (" + res.fmtStr() + ")"); + return result; +} + +std::shared_ptr LicenseManager::licenseInfo(const std::string& key) { + auto result = query_license(this->database, key, 0, 0); + if(result.empty()) return nullptr; + return result.begin()->second; +} + +std::map> LicenseManager::listLicenses(int offset, int limit) { + return query_license(this->database, "", offset, limit); +} + +bool LicenseManager::logRequest(const std::string& key, const std::string& unique_id, const std::string& ip, const std::string& version, int state) { + result res; + + auto keyId = this->id_cache->getKeyId(key); + if(keyId == 0) { + logError(LOG_GENERAL, "Failed to log license request (could not resolve key id)"); + return false; + } + + auto timestamp = duration_cast(system_clock::now().time_since_epoch()).count(); + //SELECT * FROM `license_info` WHERE `keyId` IN (SELECT `keyId` FROM `license` WHERE `key` = '000') + res = command(this->database, "INSERT INTO `license_request` (`keyId`, `ip`, `unique_id`, `timestamp`, `result`) VALUES (:key, :ip, :unique_id, :timestamp, :result)", + variable{":key", keyId}, + variable{":ip", ip}, + variable{":timestamp", timestamp}, + variable{":unique_id", unique_id}, + variable{":result", state}).execute(); + if(!res) { + logError("Could not log license validation (" + res.fmtStr() + ")"); + return false; + } + + { //Log version + res = command(this->database, "INSERT INTO `history_version`(`keyId`, `unique_id`, `timestamp`, `version`) VALUES (:key, :unique_id, :time, :version)", + variable{":key", keyId}, + variable{":time", timestamp}, + variable{":unique_id", unique_id}, + variable{":version", version} + ).execute(); + if(!res) + logError("Could not log license version statistic (" + res.fmtStr() + ")"); + res = {}; + } + return true; +} + +bool LicenseManager::logStatistic(const std::string &key, const std::string& unique_id, const std::string &ip, + const ts::proto::license::PropertyUpdateRequest &data) { + result res; + + auto keyId = this->id_cache->getKeyId(key); + if(keyId == 0) return false; + + auto time = duration_cast(system_clock::now().time_since_epoch()).count(); + { //Log online + res = command(this->database, "INSERT INTO `history_online`(`keyId`, `unique_id`, `timestamp`, `server`, `clients`, `web`, `music`, `queries`) VALUES (:key, :unique_id, :time, :server, :client, :web, :music, :query)", + variable{":key", keyId}, + variable{":unique_id", unique_id}, + variable{":time", time}, + variable{":server", data.servers_online()}, + variable{":client", data.clients_online()}, + variable{":web", data.web_clients_online()}, + variable{":music", data.bots_online()}, + variable{":query", data.queries_online()} + ).execute(); + if(!res) + logError("Could not log license statistics (" + res.fmtStr() + ")"); + res = {}; + } + //SELECT * FROM `license_info` WHERE `keyId` IN (SELECT `keyId` FROM `license` WHERE `key` = '000') + { + res = command(this->database, "INSERT INTO `history_speach`(`keyId`, `unique_id`, `timestamp`, `total`, `dead`, `online`, `varianz`) VALUES (:key, :unique_id, :time, :total, :dead, :online, :varianz)", + variable{":key", keyId}, + variable{":unique_id", unique_id}, + variable{":time", time}, + variable{":total", data.speach_total()}, + variable{":dead", data.speach_dead()}, + variable{":online", data.speach_online()}, + variable{":varianz", data.speach_varianz()} + ).execute(); + if(!res) + logError("Could not log license statistics (" + res.fmtStr() + ")"); + res = {}; + } + return true; +} + +std::deque> LicenseManager::list_statistics_user(const system_clock::time_point &begin, const system_clock::time_point &end, const milliseconds &interval) { + map>> server_statistics; + auto timeout = hours(2) + minutes(10); //TODO timeout configurable? + + auto state = command(this->database,"SELECT * FROM `history_online` WHERE `timestamp` >= :timestamp_start AND `timestamp` <= :timestamp_end ORDER BY `timestamp` ASC", + variable{":timestamp_start", duration_cast(begin.time_since_epoch() - timeout).count()}, + variable{":timestamp_end", duration_cast(end.time_since_epoch()).count()} + ).query([&server_statistics](int columns, std::string* values, std::string* names){ + size_t key_id = 0; + std::string unique_id; + auto info = make_unique(); + for(int index = 0; index < columns; index++) { + try { + if(names[index] == "keyId") + key_id = stoull(values[index]); + else if(names[index] == "unique_id") + unique_id = values[index]; + else if(names[index] == "timestamp") + info->timestamp = system_clock::time_point() + milliseconds(stoll(values[index])); + else if(names[index] == "server") + info->servers_online = stoull(values[index]); + else if(names[index] == "clients") + info->clients_online = stoull(values[index]); + else if(names[index] == "web") + info->web_clients_online = stoull(values[index]); + else if(names[index] == "music") + info->bots_online = stoull(values[index]); + else if(names[index] == "queries") + info->queries_online = stoull(values[index]); + } catch (std::exception& ex) { + logError("Failed to parse column " + names[index] + " => " + ex.what() + " (Value: " + values[index] + ")"); + return 0; + } + } + if(key_id == 0) return 0; + + server_statistics[to_string(key_id) + "_" + unique_id].push_back(std::move(info)); + return 0; + }); + + if(!state) { + logError("Could not read license statistics (" + state.fmtStr() + ")"); + return {}; + } + + std::deque> result; + system_clock::time_point current_timestamp = begin; + while(current_timestamp <= end) { + auto info = make_unique(); + info->timestamp = current_timestamp; + + for(auto& server : server_statistics) { + while(!server.second.empty()) { + auto& first = *server.second.begin(); + if(first->timestamp > current_timestamp) break; //Entry within the future + if(first->timestamp + timeout < current_timestamp) { //Entry within the past + server.second.pop_front(); + continue; + } + if(server.second.size() > 1) { + auto& second = *(server.second.begin() + 1); + if(second->timestamp <= current_timestamp) { + server.second.pop_front(); //The next entry is more up 2 date + continue; + } + } + + info->instance_online++; + info->queries_online += first->queries_online; + info->bots_online += first->bots_online; + info->web_clients_online += first->web_clients_online; + info->clients_online += first->clients_online; + info->servers_online += first->servers_online; + break; + } + } + + result.push_back(std::move(info)); + current_timestamp += interval; + } + + return result; +} + +std::deque> LicenseManager::list_statistics_version(const std::chrono::system_clock::time_point &begin, const std::chrono::system_clock::time_point &end, const std::chrono::milliseconds &interval) { + map>> server_statistics; + auto timeout = hours(2) + minutes(10); //TODO timeout configurable? + + auto state = command(this->database,"SELECT * FROM `history_version` WHERE `timestamp` >= :timestamp_start AND `timestamp` <= :timestamp_end ORDER BY `timestamp` ASC", + variable{":timestamp_start", duration_cast(begin.time_since_epoch() - timeout).count()}, + variable{":timestamp_end", duration_cast(end.time_since_epoch()).count()} + ).query([&server_statistics](int columns, std::string* values, std::string* names){ + size_t key_id = 0; + std::string unique_id; + auto info = make_unique(); + for(int index = 0; index < columns; index++) { + try { + if(names[index] == "keyId") + key_id = stoull(values[index]); + else if(names[index] == "unique_id") + unique_id = values[index]; + else if(names[index] == "timestamp") + info->timestamp = system_clock::time_point() + milliseconds(stoll(values[index])); + else if(names[index] == "version") + info->versions[values[index]] = 1; + } catch (std::exception& ex) { + logError("Failed to parse column " + names[index] + " => " + ex.what() + " (Value: " + values[index] + ")"); + return 0; + } + } + if(key_id == 0) return 0; + + server_statistics[to_string(key_id) + "_" + unique_id].push_back(std::move(info)); + return 0; + }); + + if(!state) { + logError("Could not read license statistics (" + state.fmtStr() + ")"); + return {}; + } + + std::deque> result; + system_clock::time_point current_timestamp = begin; + while(current_timestamp <= end) { + auto info = make_unique(); + info->timestamp = current_timestamp; + + for(auto& server : server_statistics) { + while(!server.second.empty()) { + auto& first = *server.second.begin(); + if(first->timestamp > current_timestamp) break; //Entry within the future + if(first->timestamp + timeout < current_timestamp) { //Entry within the past + server.second.pop_front(); + continue; + } + if(server.second.size() > 1) { + auto& second = *(server.second.begin() + 1); + if(second->timestamp <= current_timestamp) { + server.second.pop_front(); //The next entry is more up 2 date + continue; + } + } + + for(const auto& entry : first->versions) + info->versions[entry.first] += entry.second; + + break; + } + } + + result.push_back(std::move(info)); + current_timestamp += interval; + } + + return result; +} \ No newline at end of file diff --git a/license/server/LicenseManager.h b/license/server/LicenseManager.h new file mode 100644 index 0000000..633c914 --- /dev/null +++ b/license/server/LicenseManager.h @@ -0,0 +1,80 @@ +#pragma once + +#include +#include +#include + +namespace license { + namespace server { + class LicenseManager { + public: + class KeyIdCache { + struct CacheEntry { + std::string key; + size_t keyId; + std::chrono::system_clock::time_point age; + }; + + public: + KeyIdCache(LicenseManager*); + + void cache_keys(const std::deque& /* keys */); + std::string getKey(size_t keyId); + size_t getKeyId(const std::string&); + private: + int insert_entry(int, std::string*, std::string*); + + LicenseManager* handle; + threads::Mutex entry_lock; + threads::Mutex entry_update_lock; + std::deque entries; + }; + struct UserStatistics { + std::chrono::system_clock::time_point timestamp; + uint64_t clients_online; + uint64_t web_clients_online; + uint64_t bots_online; + uint64_t queries_online; + uint64_t servers_online; + }; + + struct ExtendedUserStatistics : public UserStatistics{ + std::string ip_address; + }; + + struct GlobalUserStatistics : public UserStatistics { + uint64_t instance_online; + }; + + struct GlobalVersionsStatistic { + std::chrono::system_clock::time_point timestamp; + std::map versions; + }; + public: + LicenseManager(sql::SqlManager* database); + ~LicenseManager(); + + bool setup(std::string&); + + bool validLicenseKey(const std::string& /* key */); + std::shared_ptr licenseInfo(const std::string&); + + std::map> listLicenses(int offset = 0, int limit = -1); + + bool registerLicense(const std::string& /* key */, const std::shared_ptr& /* info */, const std::string& /* issuer */); + bool updateLicenseInfo(const std::string&, const std::shared_ptr&); + bool deleteLicense(const std::string& /* key */, bool /* full */ = false); + + bool logRequest(const std::string& /* key */, const std::string& /* unique_id */, const std::string& /* ip */, const std::string& /* version */, int /* result */); + bool logStatistic(const std::string& /* key */, const std::string& /* unique_id */, const std::string& /* ip */, const ts::proto::license::PropertyUpdateRequest&); + //std::deque> list_statistics_user(const std::string& key, const std::chrono::system_clock::time_point& begin, const std::chrono::system_clock::time_point& end); + std::deque> list_statistics_user(const std::chrono::system_clock::time_point& begin, const std::chrono::system_clock::time_point& end, const std::chrono::milliseconds& /* interval */); + std::deque> list_statistics_version(const std::chrono::system_clock::time_point& begin, const std::chrono::system_clock::time_point& end, const std::chrono::milliseconds& /* interval */); + + inline sql::SqlManager* sql() { return this->database; } + private: + std::shared_ptr id_cache; + sql::SqlManager* database; + }; + } +} \ No newline at end of file diff --git a/license/server/LicenseServer.cpp b/license/server/LicenseServer.cpp new file mode 100644 index 0000000..dd99cd2 --- /dev/null +++ b/license/server/LicenseServer.cpp @@ -0,0 +1,374 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "LicenseServer.h" +#include "crypt.h" +#include "UserManager.h" + +using namespace std; +using namespace std::chrono; +using namespace license; +using namespace ts; + +LicenseServer::LicenseServer(const sockaddr_in& addr, + const std::shared_ptr& manager, + const shared_ptr &stats, + const shared_ptr &wstats, + const std::shared_ptr& user_manager) : manager(manager), statistics(stats), web_statistics(wstats), user_manager(user_manager) { + this->localAddr = new sockaddr_in{}; + memcpy(this->localAddr, &addr, sizeof(addr)); +} + +LicenseServer::~LicenseServer() { + this->stopServer(); + delete this->localAddr; +} + +#define SFAIL(message) \ +do { \ + logError(lstream << (message) << " Message: " << errno << "/" << strerror(errno)); \ + this->stopServer(); \ + return false; \ +} while(0) + +static int enabled = 1; +static int disabled = 0; +bool LicenseServer::startServer() { + { + lock_guard lock(this->lock); + if(this->running) return false; + this->running = true; + } + + fileDescriptor = socket(AF_INET, SOCK_STREAM, 0); + if (fileDescriptor < 0) SFAIL("Could not create new socket"); + + if(setsockopt(this->fileDescriptor, SOL_SOCKET, SO_REUSEADDR, &enabled, sizeof(enabled)) < 0) SFAIL("could not set reuse address"); + if(setsockopt(this->fileDescriptor, IPPROTO_TCP, TCP_CORK, &disabled, sizeof(disabled)) < 0) SFAIL("could not set no push"); + if(bind(this->fileDescriptor, (struct sockaddr *) this->localAddr, sizeof(sockaddr_in)) < 0) SFAIL("Could not bind socket on " + string(inet_ntoa(this->localAddr->sin_addr))); + + if(listen(this->fileDescriptor, 32) < 0) SFAIL("Could not listen on socket"); + + this->evBase = event_base_new(); + this->acceptEvent = event_new(this->evBase, this->fileDescriptor, EV_READ | EV_PERSIST, LicenseServer::handleEventAccept, this); + this->client_cleanup = evtimer_new(this->evBase, LicenseServer::handleEventCleanup, this); + + event_add(this->acceptEvent, nullptr); + { + timeval now{1, 0}; + evtimer_add(this->client_cleanup, &now); + } + evBaseDispatch = new threads::Thread(THREAD_SAVE_OPERATIONS, [&](){ + signal(SIGPIPE, SIG_IGN); + event_base_dispatch(this->evBase); + }); + return true; +} + +void LicenseServer::stopServer() { + { + lock_guard lock(this->lock); + if(!this->running) return; + this->running = false; + } + for(const auto& client : this->getClients()) + this->closeConnection(client); + + if(this->acceptEvent) { + event_del(this->acceptEvent); + event_free(this->acceptEvent); + } + this->acceptEvent = nullptr; + + if(this->client_cleanup) { + event_del_block(this->client_cleanup); + event_free(this->client_cleanup); + this->client_cleanup = nullptr; + } + + if(this->evBase) + event_base_loopbreak(this->evBase); + + if(this->evBaseDispatch) + this->evBaseDispatch->join(); + delete this->evBaseDispatch; + this->evBaseDispatch = nullptr; + if(this->evBase) { + event_base_loopbreak(this->evBase); /* again for some reason */ + event_base_free(this->evBase); + } + this->evBase = nullptr; + + if(this->fileDescriptor != 0) { + shutdown(this->fileDescriptor, SHUT_RDWR); + close(this->fileDescriptor); + this->fileDescriptor = 0; + } +} + +void LicenseServer::handleEventCleanup(int, short, void* ptrServer) { + auto server = static_cast(ptrServer); + + server->cleanup_clients(); + timeval next{1, 0}; + + if(server->client_cleanup) + event_add(server->client_cleanup, &next); +} + +//Basic IO +void LicenseServer::handleEventWrite(int fd, short, void* ptrServer) { + auto server = static_cast(ptrServer); + auto client = server->findClient(fd); + if(!client) return; + buffer::RawBuffer* buffer = nullptr; + { + threads::MutexLock lock(client->network.lock); + buffer = TAILQ_FIRST(&client->network.writeQueue); + if(!buffer) return; + + auto writtenBytes = send(fd, &buffer->buffer[buffer->index], buffer->length - buffer->index, 0); + buffer->index += writtenBytes; + + if(buffer->index >= buffer->length) { + TAILQ_REMOVE(&client->network.writeQueue, buffer, tail); + delete buffer; + } + if(!TAILQ_EMPTY(&client->network.writeQueue)) + event_add(client->network.writeEvent, nullptr); + } +} + +void ConnectedClient::sendPacket(const protocol::packet& packet) { + packet.prepare(); + + auto buffer = new buffer::RawBuffer(packet.data.length() + sizeof(packet.header)); + memcpy(buffer->buffer, &packet.header, sizeof(packet.header)); + memcpy(&buffer->buffer[sizeof(packet.header)], packet.data.data(), packet.data.length()); + + if(!this->protocol.cryptKey.empty()) + xorBuffer(&buffer->buffer[sizeof(packet.header)], packet.data.length(), this->protocol.cryptKey.data(), this->protocol.cryptKey.length()); + + { + threads::MutexLock lock(this->network.lock); + TAILQ_INSERT_TAIL(&this->network.writeQueue, buffer, tail); + } + event_add(this->network.writeEvent, nullptr); +} + +void ConnectedClient::init() { + protocol.last_read = std::chrono::system_clock::now(); + TAILQ_INIT(&network.writeQueue); +} + +void ConnectedClient::uninit() { + { + threads::MutexLock lock(this->network.lock); + ts::buffer::RawBuffer* buffer; + while ((buffer = TAILQ_FIRST(&this->network.writeQueue))) { + TAILQ_REMOVE(&this->network.writeQueue, buffer, tail); + delete buffer; + } + } + if(network.fileDescriptor > 0) { + shutdown(this->network.fileDescriptor, SHUT_RDWR); + close(this->network.fileDescriptor); + network.fileDescriptor = 0; + } + if(this->network.readEvent) event_del(this->network.readEvent); + this->network.readEvent = nullptr; + + if(this->network.writeEvent) event_del(this->network.writeEvent); + this->network.writeEvent = nullptr; +} + +void LicenseServer::handleEventRead(int fd, short, void* ptrServer) { + auto server = static_cast(ptrServer); + auto client = server->findClient(fd); + if(!client) return; + + char buffer[1024]; + sockaddr_in remoteAddress{}; + socklen_t remoteAddressSize = sizeof(remoteAddress); + auto read = recvfrom(fd, buffer, 1024, 0, reinterpret_cast(&remoteAddress), &remoteAddressSize); + + if(read < 0){ + if(errno == EWOULDBLOCK) return; + logError(LOG_LICENSE_CONTROLL, "Invalid read. Disconnecting remote manager. Message: {}/{}", errno, strerror(errno)); + event_del_noblock(client->network.readEvent); + server->closeConnection(client); + return; + } else if(read == 0) { + logError(LOG_LICENSE_CONTROLL, "Invalid read. Disconnecting remote client"); + event_del_noblock(client->network.readEvent); + server->closeConnection(client); + return; + } + + client->protocol.last_read = std::chrono::system_clock::now(); + server->handleMessage(client, string(buffer, read)); +} + +void LicenseServer::handleEventAccept(int fd, short, void* ptrServer) { + auto server = static_cast(ptrServer); + + auto client = make_shared(); + client->init(); + + socklen_t client_len = sizeof(client->network.remoteAddr); + + client->network.fileDescriptor = accept(fd, (struct sockaddr *)&client->network.remoteAddr, &client_len); + if(setsockopt(client->network.fileDescriptor, SOL_SOCKET, SO_REUSEADDR, &enabled, sizeof(enabled)) < 0);// CERR("could not set reuse addr"); + if(setsockopt(client->network.fileDescriptor, IPPROTO_TCP, TCP_CORK, &disabled, sizeof(disabled)) < 0);// CERR("could not set no push"); + + if (client->network.fileDescriptor < 0) { + logCritical("Could not accept new client! (" + to_string(client->network.fileDescriptor) + "|" + to_string(errno) + "|" + strerror(errno) + ")"); + return; + } + + client->protocol.state = protocol::HANDSCAKE; + { + lock_guard lock(server->lock); + server->currentClients.push_back(client); + } + + client->network.readEvent = event_new(server->evBase, client->network.fileDescriptor, EV_READ | EV_PERSIST, LicenseServer::handleEventRead, server); + client->network.writeEvent = event_new(server->evBase, client->network.fileDescriptor, EV_WRITE, LicenseServer::handleEventWrite, server); + event_add(client->network.readEvent, nullptr); + + logMessage(lstream << "Got new client from " << inet_ntoa(client->network.remoteAddr.sin_addr)); +} + +void LicenseServer::disconnectClient(const std::shared_ptr& client, const std::string &reason) { + client->sendPacket({protocol::PACKET_DISCONNECT, reason}); +} + +void LicenseServer::closeConnection(const std::shared_ptr &client, bool blocking) { + if(this->evBaseDispatch && threads::self::id() == *this->evBaseDispatch) { + std::thread(std::bind(&LicenseServer::closeConnection, this, client, true)).detach(); + return; + } + + { + + unique_lock lock(client->network.lock); + if(!TAILQ_EMPTY(&client->network.writeQueue)) { + lock.unlock(); + + if(!blocking) { + std::thread(std::bind(&LicenseServer::closeConnection, this, client, true)).detach(); + return; + } + auto start = system_clock::now(); + while(system_clock::now() - start < seconds(5)){ + { + lock.lock(); + if(TAILQ_EMPTY(&client->network.writeQueue)) break; + lock.unlock(); + } + threads::self::sleep_for(milliseconds(5)); + } + } + } + this->unregisterClient(client); +} + +void LicenseServer::unregisterClient(const std::shared_ptr &client) { + { + lock_guard lock(this->lock); + + auto it = find(this->currentClients.begin(), this->currentClients.end(), client); + if(it != this->currentClients.end()) + this->currentClients.erase(it); + } + + client->protocol.state = protocol::UNCONNECTED; + client->uninit(); +} + +void LicenseServer::cleanup_clients() { + unique_lock lock(this->lock); + auto clients = this->currentClients; + lock.unlock(); + + for(const auto& client : clients) { + if(client->protocol.last_read + minutes(1) < system_clock::now()) { + if(client->protocol.state != protocol::DISCONNECTING && client->protocol.state != protocol::UNCONNECTED) { + this->disconnectClient(client, "timeout"); + this->closeConnection(client); + client->protocol.state = protocol::DISCONNECTING; + } else { + auto it = find(this->currentClients.begin(), this->currentClients.end(), client); + if(it != this->currentClients.end()) + this->currentClients.erase(it); + } + } + } + debugMessage("Client's cleaned up"); +} + +std::shared_ptr LicenseServer::findClient(int fileDescriptor) { + lock_guard lock(this->lock); + for(const auto& cl : this->currentClients) + if(cl->network.fileDescriptor == fileDescriptor) + return cl; + return nullptr; +} + +#define ERR(message) \ +do { \ + logError(lstream << message); \ + this->closeConnection(client); \ + return; \ +} while(0) + +void LicenseServer::handleMessage(shared_ptr& client, const std::string& message) { + if(message.length() < sizeof(protocol::packet::header)) ERR("A client tried to send a invalid packet (too small header)"); + + protocol::packet packet{protocol::PACKET_DISCONNECT, ""}; + memcpy(&packet.header, message.data(), sizeof(protocol::packet::header)); + packet.data = message.substr(sizeof(protocol::packet::header), packet.header.length); + + if(!client->protocol.cryptKey.empty()) + xorBuffer((char*) packet.data.data(), packet.data.length(), client->protocol.cryptKey.data(), client->protocol.cryptKey.length()); + + bool success = false; + string error; + try { + if(packet.header.packetId == protocol::PACKET_CLIENT_HANDSHAKE) { + success = this->handleHandshake(client, packet, error); + } else if(packet.header.packetId == protocol::PACKET_DISCONNECT) { + success = this->handleDisconnect(client, packet, error); + } else if(packet.header.packetId == protocol::PACKET_CLIENT_SERVER_VALIDATION) { + success = this->handleServerValidation(client, packet, error); + } else if(packet.header.packetId == protocol::PACKET_CLIENT_PROPERTY_ADJUSTMENT) { + success = this->handlePacketPropertyUpdate(client, packet, error); + } else if(packet.header.packetId == protocol::PACKET_CLIENT_AUTH_REQUEST) { + success = this->handlePacketAuth(client, packet, error); + } else if(packet.header.packetId == protocol::PACKET_CLIENT_LICENSE_CREATE_REQUEST) { + success = this->handlePacketLicenseCreate(client, packet, error); + } else if(packet.header.packetId == protocol::PACKET_CLIENT_LIST_REQUEST) { + success = this->handlePacketLicenseList(client, packet, error); + } else if(packet.header.packetId == protocol::PACKET_CLIENT_DELETE_REQUEST) { + success = this->handlePacketLicenseDelete(client, packet, error); + } else if(packet.header.packetId == protocol::PACKET_PING) { + /* nothing todo */ + } else error = "Invalid packet id!"; + } catch (std::exception& ex) { + success = false; + error = "Caught exception while handle packet: " + string(ex.what()); + } + + if(!success) { + logError("[CLIENT][" + client->address() + "] Failed to handle packet. message: " + error); + this->disconnectClient(client, error); + } +} \ No newline at end of file diff --git a/license/server/LicenseServer.h b/license/server/LicenseServer.h new file mode 100644 index 0000000..191b3c9 --- /dev/null +++ b/license/server/LicenseServer.h @@ -0,0 +1,115 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "LicenseManager.h" + +namespace license { + namespace web { + class WebStatistics; + } + namespace stats { + class StatisticManager; + } + class UserManager; + + enum ClientType { + SERVER, + MANAGER + }; + struct ConnectedClient { + public: + struct { + sockaddr_in remoteAddr; + int fileDescriptor = 0; + event* readEvent = nullptr; + event* writeEvent = nullptr; + + threads::Mutex lock; + TAILQ_HEAD(, ts::buffer::RawBuffer) writeQueue; + } network; + + struct { + protocol::RequestState state = protocol::UNCONNECTED; + std::chrono::system_clock::time_point last_read; + std::string cryptKey = ""; + } protocol; + + ClientType type = ClientType::SERVER; + std::string username; + std::string key; + std::string unique_identifier; + + void init(); + void uninit(); + void sendPacket(const protocol::packet&); + + inline std::string address() { return inet_ntoa(network.remoteAddr.sin_addr); } + }; + + class LicenseServer { + public: + explicit LicenseServer(const sockaddr_in&, const std::shared_ptr&, const std::shared_ptr& /* stats */, const std::shared_ptr& /* web stats */, const std::shared_ptr& /* user manager */); + ~LicenseServer(); + + bool startServer(); + bool isRunning(){ return this->running; } + void stopServer(); + + std::shared_ptr findClient(int fileDescriptor); + void disconnectClient(const std::shared_ptr&, const std::string& reason); + void closeConnection(const std::shared_ptr&, bool blocking = false); + + std::deque> getClients() { + std::lock_guard lock(this->lock); + return currentClients; + } + private: + void unregisterClient(const std::shared_ptr&); + void cleanup_clients(); + + std::shared_ptr web_statistics; + std::shared_ptr statistics; + std::shared_ptr manager; + std::shared_ptr user_manager; + + bool running = false; + + std::mutex lock; + std::deque> currentClients; + + sockaddr_in* localAddr = nullptr; + int fileDescriptor = 0; + event_base* evBase = nullptr; + event* acceptEvent = nullptr; + event* client_cleanup = nullptr; + + threads::Thread* evBaseDispatch = nullptr; + + static void handleEventCleanup(int, short, void*); + static void handleEventAccept(int, short, void*); + static void handleEventRead(int, short, void*); + static void handleEventWrite(int, short, void*); + + void handleMessage(std::shared_ptr&, const std::string&); + + bool handleDisconnect(std::shared_ptr&, protocol::packet&, std::string& error); + bool handleHandshake(std::shared_ptr&, protocol::packet&, std::string& error); + bool handleServerValidation(std::shared_ptr &, protocol::packet &, std::string &error); + bool handlePacketPropertyUpdate(std::shared_ptr &, protocol::packet &, std::string &error); + + bool handlePacketAuth(std::shared_ptr &, protocol::packet &, std::string &error); + bool handlePacketLicenseCreate(std::shared_ptr &, protocol::packet &, std::string &error); + bool handlePacketLicenseList(std::shared_ptr &, protocol::packet &, std::string &error); + bool handlePacketLicenseDelete(std::shared_ptr &, protocol::packet &, std::string &error); + }; +} \ No newline at end of file diff --git a/license/server/LicenseServerHandler.cpp b/license/server/LicenseServerHandler.cpp new file mode 100644 index 0000000..9c816ed --- /dev/null +++ b/license/server/LicenseServerHandler.cpp @@ -0,0 +1,338 @@ +#include +#include +#include +#include +#include +#include "LicenseRequest.pb.h" +#include "shared/License.h" +#include "LicenseServer.h" +#include "WebAPI.h" +#include "StatisticManager.h" +#include "UserManager.h" + +using namespace std; +using namespace std::chrono; +using namespace license; +using namespace ts; + + +inline void generate(char* buffer, size_t length){ + for(int index = 0; index < length; index++) + buffer[index] = rand(); +} + +#define TEST_PROTOCOL_STATE(expected) \ +if(client->protocol.state != protocol::expected) { \ + error = "invalid protocol state"; \ + return false; \ +} + +#define ENSURE_PACKET_SIZE(expected) \ +if(packet.data.length() < (expected)) { \ + error = "too small packet!"; \ + return false; \ +} + +#define PARSE_PROTO(class, var) \ +ts::proto::license::class var; \ +if(!var.ParseFromString(packet.data)) { \ + error = "invalid data!"; \ + return false; \ +} + +#define CRYPT_KEY_LENGTH 32 +bool LicenseServer::handleHandshake(shared_ptr& client, protocol::packet& packet, std::string &error) { + TEST_PROTOCOL_STATE(HANDSCAKE); + ENSURE_PACKET_SIZE(4); + if((uint8_t) packet.data[0] != 0xC0 || (uint8_t) packet.data[1] != 0xFF || (uint8_t) packet.data[2] != 0xEE) { + error = "invalid magic!"; + return false; + } + if((uint8_t) packet.data[3] != LICENSE_PROT_VERSION) { + error = "invalid version!"; + return false; + } + + bool manager = false; + if(packet.data.length() >= 4 && (uint8_t) packet.data[4] == 1) { + manager = true; + //Its a manager! + } + + char buffer_cryptkey[CRYPT_KEY_LENGTH]; + generate(buffer_cryptkey, CRYPT_KEY_LENGTH); + + uint8_t buffer[128]; + size_t buffer_index = 0; + buffer[buffer_index++] = 0xAF; + buffer[buffer_index++] = 0xFE; + buffer[buffer_index++] = LICENSE_PROT_VERSION; + le2be16(CRYPT_KEY_LENGTH, buffer, buffer_index, &buffer_index); + memcpy(&buffer[buffer_index], buffer_cryptkey, CRYPT_KEY_LENGTH); + buffer_index += CRYPT_KEY_LENGTH; + if(manager) + buffer[buffer_index++] = 0x01; //Manager accepted + + client->sendPacket({protocol::PACKET_SERVER_HANDSHAKE, string((char*) buffer, buffer_index)}); + client->protocol.cryptKey = string(buffer_cryptkey, 32); + + if(manager) { + client->protocol.state = protocol::MANAGER_AUTHORIZATION; + client->type = ClientType::MANAGER; + } else { + client->protocol.state = protocol::SERVER_VALIDATION; + client->type = ClientType::SERVER; + } + + return true; +} + +bool LicenseServer::handleDisconnect(shared_ptr& client, protocol::packet& packet, std::string &error) { + logMessage("[CLIENT][" + client->address() + "] Remote disconnect. Reason: " + packet.data); + this->closeConnection(client); + return true; +} + +inline void fill_info(proto::license::LicenseInfo* proto, const shared_ptr& info, const std::string& key) { + proto->set_key(key); + proto->set_username(info->username); + proto->set_first_name(info->first_name); + proto->set_last_name(info->last_name); + proto->set_email(info->email); + proto->set_type(info->type); + proto->set_created(duration_cast(info->creation.time_since_epoch()).count()); + proto->set_begin(duration_cast(info->start.time_since_epoch()).count()); + proto->set_end(duration_cast(info->end.time_since_epoch()).count()); +} + +std::string string_to_hex(const std::string& input) +{ + static const char* const lut = "0123456789ABCDEF"; + size_t len = input.length(); + + std::string output; + output.reserve(2 * len); + for (size_t i = 0; i < len; ++i) + { + const unsigned char c = input[i]; + output.push_back(lut[c >> 4]); + output.push_back(lut[c & 15]); + } + return output; +} + +bool LicenseServer::handleServerValidation(shared_ptr &client, protocol::packet &packet, std::string &error) { + TEST_PROTOCOL_STATE(SERVER_VALIDATION); + PARSE_PROTO(ServerValidation, pkt); + + shared_ptr remoteLicense = nullptr; + if(pkt.licensed() && !pkt.has_license()) { + //TODO shutdown server + } + if(!pkt.has_info()) { + error = "invalid data or missing data"; + return false; + } + if(pkt.has_license()){ //Client has license + remoteLicense = readLocalLicence(pkt.license(), error); + if(!remoteLicense) { + error = "Could not read remote key: " + error; + return false; + }; + logMessage(LOG_GENERAL, "[CLIENT][{}] Got remote license. Registered to {}. Key: {} (0x{})", client->address(), remoteLicense->owner(), base64::encode(remoteLicense->key()), string_to_hex(remoteLicense->key())); + client->key = remoteLicense->key(); + } else { } + if(pkt.licensed() && !pkt.has_license_info()) { + error = "Invalid content!"; + return false; + } + + logMessage(LOG_GENERAL, "[CLIENT][{}] Got some server information. TeaSpeak-Version: {} uname: {}", client->address(), pkt.info().version(), pkt.info().uname()); + ts::proto::license::LicenseResponse response; + + //Forces + client->unique_identifier = pkt.info().has_unique_id() ? pkt.info().unique_id() : client->address(); + if(remoteLicense && pkt.licensed()) { + auto info = this->manager->licenseInfo(remoteLicense->key()); + if(!info) { + response.set_valid(false); + + /* + logMessage(LOG_GENERAL, "[CLIENT][{}] Unknown license! Adding it to database!", client->address()); + auto db_info = make_shared(); + db_info->start = system_clock::now(); + db_info->end = remoteLicense->end(); + db_info->last_name = "unknown"; + db_info->first_name = "unknown"; + db_info->username = remoteLicense->owner(); + db_info->email = "unknonw@unknown"; + db_info->creation = system_clock::now(); + db_info->type = remoteLicense->data.type; + this->manager->registerLicense(remoteLicense->key(), db_info, "teaforo-fix"); + info = this->manager->licenseInfo(remoteLicense->key()); + if(!info) { + error = "could not insert key!"; + return false; + } + */ + + logMessage(LOG_GENERAL, "[CLIENT][{}] Remote license hasn't been found in database. Shutting down server!", client->address()); + } else { + if(info->deleted) { + response.set_valid(false); + logMessage(LOG_GENERAL, "[CLIENT][{}] Remote license has been deleted! Shutting down server!", client->address()); + } else { + fill_info(response.mutable_license_info(), info, remoteLicense->data.licenceKey); + response.set_valid(info->isValid()); + } + } + this->manager->logRequest(remoteLicense->key(), client->unique_identifier, client->address(), pkt.info().version(), response.valid()); + } else { + response.set_valid(true); + } + + response.mutable_blacklist()->set_state(ts::proto::license::VALID); + client->sendPacket(protocol::packet{protocol::PACKET_SERVER_VALIDATION_RESPONSE, response}); + client->protocol.state = protocol::PROPERTY_ADJUSTMENT; + return true; +} + +bool LicenseServer::handlePacketPropertyUpdate(shared_ptr &client, protocol::packet &packet, std::string &error) { + TEST_PROTOCOL_STATE(PROPERTY_ADJUSTMENT); + PARSE_PROTO(PropertyUpdateRequest, pkt); + + logMessage("[CLIENT][" + client->address() + "] Got server statistics:"); + logMessage("[CLIENT][" + client->address() + "] Spoken total : " + to_string(pkt.speach_total())); + logMessage("[CLIENT][" + client->address() + "] Spoken dead : " + to_string(pkt.speach_dead())); + logMessage("[CLIENT][" + client->address() + "] Spoken online : " + to_string(pkt.speach_online())); + logMessage("[CLIENT][" + client->address() + "] Spoken varianz : " + to_string(pkt.speach_varianz())); + logMessage("[CLIENT][" + client->address() + "] -------------------------------"); + logMessage("[CLIENT][" + client->address() + "] Users online : " + to_string(pkt.clients_online())); + logMessage("[CLIENT][" + client->address() + "] Web Users online : " + to_string(pkt.web_clients_online())); + logMessage("[CLIENT][" + client->address() + "] Queries online : " + to_string(pkt.queries_online())); + logMessage("[CLIENT][" + client->address() + "] Bots online : " + to_string(pkt.bots_online())); + logMessage("[CLIENT][" + client->address() + "] Servers : " + to_string(pkt.servers_online())); + this->manager->logStatistic(client->key, client->unique_identifier, client->address(), pkt); + //TODO test stuff! + + ts::proto::license::PropertyUpdateResponse response; + response.set_accepted(true); + response.set_reset_speach(pkt.speach_total() < 0); + response.set_speach_total_remote(pkt.speach_total()); + response.set_speach_varianz_corrector(0); + client->sendPacket(protocol::packet{protocol::PACKET_SERVER_PROPERTY_ADJUSTMENT, response}); + this->disconnectClient(client, "finished"); + + if(this->statistics) + this->statistics->reset_cache_general(); + if(this->web_statistics) + this->web_statistics->async_broadcast_notify_general_update(); + return true; +} + +bool LicenseServer::handlePacketAuth(shared_ptr &client, protocol::packet &packet, std::string &error) { + TEST_PROTOCOL_STATE(MANAGER_AUTHORIZATION); + PARSE_PROTO(AuthorizationRequest, pkt); + + logMessage("[MANAGER][" + client->address() + "] Got login. User: " + pkt.username() + " Password: " + pkt.password()); + + ts::proto::license::AuthorizationResponse response; + response.set_success(false); + if(this->user_manager) { + auto user_account = this->user_manager->find_user(pkt.username()); + if(user_account) { + if(user_account->verify_password(pkt.password())) { + switch(user_account->status()) { + case User::Status::ACTIVE: + response.set_success(true); + break; + case User::Status::BANNED: + response.set_message("you have been banned"); + break; + case User::Status::DISABLED: + response.set_message("you have been disabled"); + break; + default: + response.set_message("Your account hasn't been activated"); + break; + } + } + } + } else { + response.set_success(pkt.password() == "HelloWorld"); + } + if(!response.has_message() && !response.success()) + response.set_message("username or password mismatch"); + + if(response.success()) { + logMessage("[MANAGER][" + client->address() + "] Got succeeded user login. User: " + pkt.username() + " Password: " + pkt.password()); + client->username = pkt.username(); + client->protocol.state = protocol::MANAGER_CONNECTED; + } else + logMessage("[MANAGER][" + client->address() + "] Got failed user login. User: " + pkt.username() + " Password: " + pkt.password()); + + client->sendPacket(protocol::packet{protocol::PACKET_SERVER_AUTH_RESPONSE, response}); + return true; +} + +bool LicenseServer::handlePacketLicenseCreate(shared_ptr &client, protocol::packet &packet, std::string &error) { + TEST_PROTOCOL_STATE(MANAGER_CONNECTED); + PARSE_PROTO(LicenseCreateRequest, pkt); + + logMessage(LOG_GENERAL, "[MANAGER][" + client->address() + "] Register new license to {} {} ({}). E-Mail: {}", pkt.issuer_first_name(), pkt.issuer_last_name(), pkt.issuer_username(), pkt.issuer_email()); + + ts::proto::license::LicenseCreateResponse response; + + auto db_info = make_shared(); + db_info->start = system_clock::time_point() + milliseconds(pkt.begin()); + db_info->end = system_clock::time_point() + milliseconds(pkt.end()); + db_info->last_name = pkt.issuer_last_name(); + db_info->first_name = pkt.issuer_first_name(); + db_info->username = pkt.issuer_username(); + db_info->email = pkt.issuer_email(); + db_info->creation = system_clock::now(); + db_info->type = static_cast(pkt.type()); + + auto license = license::createLocalLicence(db_info->type, db_info->end, db_info->first_name + db_info->last_name); + auto parsed_license = license::readLocalLicence(license, error); + if(!parsed_license) { + response.set_error("failed to register license (parse)"); + } else { + + if(!this->manager->registerLicense(parsed_license->key(), db_info, client->username)) { + response.set_error("failed to register license"); + } else { + fill_info(response.mutable_license(), db_info, parsed_license->key()); + response.set_exported_key(license); + } + } + client->sendPacket(protocol::packet{protocol::PACKET_SERVER_LICENSE_CREATE_RESPONSE, response}); + return true; +} + +bool LicenseServer::handlePacketLicenseList(shared_ptr &client, protocol::packet &packet, std::string &error) { + TEST_PROTOCOL_STATE(MANAGER_CONNECTED); + PARSE_PROTO(LicenseListRequest, pkt); + + proto::license::LicenseListResponse response; + response.set_end(false); + + for(const auto& info : this->manager->listLicenses(pkt.offset(), pkt.count())) { + auto entry = response.add_entries(); + fill_info(entry, info.second, info.first); + } + client->sendPacket(protocol::packet{protocol::PACKET_SERVER_LIST_RESPONSE, response}); + + return true; +} + +bool LicenseServer::handlePacketLicenseDelete(shared_ptr &client, protocol::packet &packet, std::string &error) { + TEST_PROTOCOL_STATE(MANAGER_CONNECTED); + PARSE_PROTO(LicenseDeleteRequest, pkt); + + proto::license::LicenseDeleteResponse response; + response.set_succeed(this->manager->deleteLicense(pkt.key(), pkt.full())); + client->sendPacket(protocol::packet{protocol::PACKET_CLIENT_DELETE_RESPONSE, response}); + + return true; +} \ No newline at end of file diff --git a/license/server/StatisticManager.cpp b/license/server/StatisticManager.cpp new file mode 100644 index 0000000..5e813e5 --- /dev/null +++ b/license/server/StatisticManager.cpp @@ -0,0 +1,201 @@ +// +// Created by wolverindev on 04.09.18. +// + +#include +#include +#include "StatisticManager.h" +#include "LicenseManager.h" + +using namespace std; +using namespace std::chrono; +using namespace license; +using namespace license::server; +using namespace license::stats; + +std::chrono::milliseconds HistoryStatistics::time_period(license::stats::HistoryStatistics::HistoryType type) { + switch (type) { + case HistoryType::LAST_DAY: + case HistoryType::DAY_YESTERDAY: + case HistoryType::DAY_7DAYS_AGO: + return minutes(15); + case HistoryType::LAST_WEEK: + return hours(1); + case HistoryType::LAST_MONTH: + case HistoryType::LAST_HALF_YEAR: + default: + return hours(2); + } +} + +std::chrono::milliseconds HistoryStatistics::cache_timeout(license::stats::HistoryStatistics::HistoryType type) { + switch (type) { + case HistoryType::LAST_DAY: + case HistoryType::DAY_YESTERDAY: + case HistoryType::DAY_7DAYS_AGO: + return minutes(15); + case HistoryType::LAST_WEEK: + return hours(1); + case HistoryType::LAST_MONTH: + return hours(2); + + case HistoryType::LAST_HALF_YEAR: + default: + return hours(8); + } +} + +std::chrono::milliseconds HistoryStatistics::type_duration(license::stats::HistoryStatistics::HistoryType type) { + switch (type) { + case HistoryType::LAST_DAY: + case HistoryType::DAY_YESTERDAY: + case HistoryType::DAY_7DAYS_AGO: + return hours(24); + case HistoryType::LAST_WEEK: + return hours(24) * 7; + case HistoryType::LAST_MONTH: + return hours(24) * 32; + case HistoryType::LAST_HALF_YEAR: + return hours(24) * 32 * 6; + default: + return hours(24); + } +} + +system_clock::time_point HistoryStatistics::align_type(license::stats::HistoryStatistics::HistoryType type, const std::chrono::system_clock::time_point &tp) { + switch (type) { + case HistoryType::LAST_DAY: + case HistoryType::DAY_YESTERDAY: + case HistoryType::DAY_7DAYS_AGO: + return system_clock::time_point() + minutes(duration_cast(tp.time_since_epoch()).count()); + case HistoryType::LAST_WEEK: + case HistoryType::LAST_MONTH: + case HistoryType::LAST_HALF_YEAR: + default: + return system_clock::time_point() + hours(duration_cast(tp.time_since_epoch()).count()); + } +} + +StatisticManager::StatisticManager(const std::shared_ptr &manager) : license_manager(manager) {} +StatisticManager::~StatisticManager() {} + +struct GeneralStatisticEntry { + std::chrono::system_clock::time_point age; + string unique_id = ""; + uint64_t key_id = 0; + uint64_t servers = 0; + uint64_t clients = 0; + uint64_t bots = 0; +}; + +void StatisticManager::reset_cache_general() { + lock_guard lock(this->_general_statistics_lock); + this->_general_statistics = nullptr; +} + +void parse_general_entry(deque>& entries, bool unique, int length, string* values, string* names) { + auto entry = make_unique(); + for(int index = 0; index < length; index++) { + if(names[index] == "keyId") { + entry->key_id = stoull(values[index]); + } else if(names[index] == "timestamp") { + entry->age = system_clock::time_point() + milliseconds(stoll(values[index])); + } else if(names[index] == "server") { + entry->servers = stoull(values[index]); + } else if(names[index] == "clients") { + entry->clients = stoull(values[index]); + } else if(names[index] == "music") { + entry->bots = stoull(values[index]); + } else if(names[index] == "unique_id") + entry->unique_id = values[index]; + } + + if(unique) { + for(auto& e : entries) { + if(e->key_id == entry->key_id && e->unique_id == entry->unique_id) { + if(e->age < entry->age) { + entries.erase(find(entries.begin(), entries.end(), e)); + break; + } else { + return; + } + } + } + } + entries.push_back(std::move(entry)); +} + +std::shared_ptr StatisticManager::general_statistics() { + unique_lock lock(this->_general_statistics_lock); + if(this->_general_statistics && system_clock::now() < this->_general_statistics->age + seconds(300)) return this->_general_statistics; + + lock.unlock(); + unique_lock create_lock(this->_general_statistics_generate_lock); + lock.lock(); + if(this->_general_statistics && system_clock::now() < this->_general_statistics->age + seconds(300)) return this->_general_statistics; + lock.unlock(); + + deque> entries; + + auto result = sql::command(this->license_manager->sql(), "SELECT `keyId`, `unique_id`, `timestamp`,`server`,`clients`,`music` FROM `history_online` WHERE `timestamp` > :time ORDER BY `timestamp` ASC", + variable{":time", duration_cast(system_clock::now().time_since_epoch() - hours(2) - minutes(10)).count()}) //10min as buffer + .query(std::function(parse_general_entry), entries, true); + + auto stats = make_shared(); + for(auto& entry : entries) { + stats->bots += entry->bots; + stats->clients += entry->clients; + stats->servers += entry->servers; + stats->instances++; + } + stats->age = system_clock::now(); + + lock.lock(); + this->_general_statistics = stats; + lock.unlock(); + create_lock.unlock(); + return stats; +} + +std::shared_ptr StatisticManager::history(license::stats::HistoryStatistics::HistoryType type) { + lock_guard lock(this->_history_locks[type]); + auto current_time = system_clock::now(); + auto& entry = this->_history[type]; + + if(entry && entry->evaluated + HistoryStatistics::cache_timeout(type) > current_time) + return entry; + + entry = make_shared(); + + entry->period = HistoryStatistics::time_period(type); + entry->begin = HistoryStatistics::align_type(type, current_time - HistoryStatistics::type_duration(type)); + entry->end = HistoryStatistics::align_type(type, current_time); + + if(type == HistoryStatistics::DAY_YESTERDAY || type == HistoryStatistics::DAY_7DAYS_AGO) { + auto& reference = this->_history[HistoryStatistics::LAST_DAY]; + if(reference) { + entry->begin = reference->begin; + entry->end = reference->end; + entry->evaluated = reference->evaluated; + } + if(type == HistoryStatistics::DAY_YESTERDAY) { + entry->begin -= hours(24); + entry->end -= hours(24); + } else if(type == HistoryStatistics::DAY_7DAYS_AGO) { + entry->begin -= hours(24) * 7; + entry->end -= hours(24) * 7; + } + } + + auto statistics = this->license_manager->list_statistics_user(entry->begin, entry->end, entry->period); + + entry->statistics = std::move(statistics); + if(entry->evaluated.time_since_epoch().count() == 0) + entry->evaluated = current_time; + + if(type == HistoryStatistics::LAST_DAY) { + this->history(HistoryStatistics::DAY_YESTERDAY); + this->history(HistoryStatistics::DAY_7DAYS_AGO); + } + return entry; +} \ No newline at end of file diff --git a/license/server/StatisticManager.h b/license/server/StatisticManager.h new file mode 100644 index 0000000..192c712 --- /dev/null +++ b/license/server/StatisticManager.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include "LicenseManager.h" + +namespace license { + namespace stats { + struct GeneralStatistics { + std::chrono::system_clock::time_point age; + + uint64_t instances = 0; + uint64_t servers = 0; + uint64_t clients = 0; + uint64_t bots = 0; + }; + + struct HistoryStatistics { + enum HistoryType { + LAST_DAY, + DAY_YESTERDAY, + DAY_7DAYS_AGO, + LAST_WEEK, + LAST_MONTH, + LAST_HALF_YEAR + }; + static std::chrono::system_clock::time_point align_type(HistoryType type, const std::chrono::system_clock::time_point&); + static std::chrono::milliseconds time_period(HistoryType type); + static std::chrono::milliseconds cache_timeout(HistoryType type); + static std::chrono::milliseconds type_duration(HistoryType type); + + std::chrono::system_clock::time_point evaluated; + std::chrono::system_clock::time_point begin; + std::chrono::system_clock::time_point end; + std::chrono::milliseconds period; + HistoryType type; + + std::deque> statistics; + }; + + class StatisticManager { + public: + explicit StatisticManager(const std::shared_ptr& /* manager */); + virtual ~StatisticManager(); + + void reset_cache_general(); + std::shared_ptr general_statistics(); + std::shared_ptr history(HistoryStatistics::HistoryType); + private: + std::shared_ptr license_manager; + + std::recursive_mutex _general_statistics_lock; + std::recursive_mutex _general_statistics_generate_lock; + std::shared_ptr _general_statistics; + + std::map _history_locks; + std::map> _history; + }; + } +} \ No newline at end of file diff --git a/license/server/UserManager.cpp b/license/server/UserManager.cpp new file mode 100644 index 0000000..2df32d4 --- /dev/null +++ b/license/server/UserManager.cpp @@ -0,0 +1,64 @@ +#include "UserManager.h" +#include +#include +#include + +using namespace license; +using namespace std; + +UserManager::UserManager(sql::SqlManager *db) : database(db) {} +UserManager::~UserManager() {} + +std::shared_ptr UserManager::find_user(const std::string &username) { + unique_lock lock(this->loaded_user_lock); + for(const auto& user : this->loaded_user) + if(user->username() == username) + return user; + + lock.unlock(); + lock_guard load_lock(this->load_user_lock); + lock.lock(); + for(const auto& user : this->loaded_user) + if(user->username() == username) + return user; + lock.unlock(); + + std::shared_ptr user; + sql::command(this->database, "SELECT `username`, `password_hash`, `status`, `owner` FROM `users` WHERE `username` = :username", variable{":username", username}).query([&](int length, string* values, string* key) { + string username, password_hash, owner; + int status; + + for(int index = 0; index < length; index++) { + try { + + if(key[index] == "username") + username = values[index]; + else if(key[index] == "password_hash") + password_hash = values[index]; + else if(key[index] == "status") + status = (User::Status::value) stoll(values[index]); + else if(key[index] == "owner") + owner = values[index]; + } catch(std::exception& ex) { + logError(LOG_LICENSE_CONTROLL, "Failed to load user {}. Failed to parse field {} ({})", username, key[index], values[index]); + return; + } + } + + user = make_shared(this, username, password_hash, status); + user->_owner = owner; + + lock.lock(); + this->loaded_user.push_back(user); + lock.unlock(); + }); + + return user; +} + +User::User(license::UserManager *handle, const std::string &name, const std::string &password, license::User::Status::value status) : handle(handle), _username(name), _password_hash(password), _status(status) {} + +bool User::verify_password(const std::string &password) { + auto hashed_password = base64::encode(digest::sha1(password)); + return this->_password_hash == hashed_password; +} \ No newline at end of file diff --git a/license/server/UserManager.h b/license/server/UserManager.h new file mode 100644 index 0000000..ae7569f --- /dev/null +++ b/license/server/UserManager.h @@ -0,0 +1,68 @@ +#pragma once + +#include + +namespace license { + class UserManager; + class User; + + class UserPermissions { + friend class User; + public: + struct Permission { + static constexpr auto LICENSE_CREATE = "license_create"; + static constexpr auto LICENSE_CREATE_FROM_TEMPLATE = "license_create_from_template"; + }; + private: + User* handle; + }; + + class User { + friend class UserManager; + public: + struct Status { + enum value { + ACTIVE, + DISABLED, + BANNED + }; + }; + + User(UserManager* /* manager */, const std::string& /* username */,const std::string& /* password hash */, Status::value /* status */); + + double balance(); + inline std::string username() { return this->_username; } + inline std::string password_hash() { return this->_password_hash; } + inline Status::value status() { return this->_status; } + bool verify_password(const std::string& /* password */); + private: + UserManager* handle; + + Status::value _status; + std::string _username; + std::string _password_hash; + std::string _owner; + double _balance = 0; + }; + + class UserOffer { + public: + + private: + + }; + + class UserManager { + public: + explicit UserManager(sql::SqlManager* /* database */); + virtual ~UserManager(); + + std::shared_ptr find_user(const std::string& /* name */); + private: + std::mutex load_user_lock; + std::mutex loaded_user_lock; + std::deque> loaded_user; + + sql::SqlManager* database; + }; +} \ No newline at end of file diff --git a/license/server/WebAPI.cpp b/license/server/WebAPI.cpp new file mode 100644 index 0000000..eab4597 --- /dev/null +++ b/license/server/WebAPI.cpp @@ -0,0 +1,562 @@ +// +// Created by wolverindev on 04.09.18. +// + +#include +#include +#include +#include +#include +#include "StatisticManager.h" +#include "WebAPI.h" + +using namespace license; +using namespace license::server; +using namespace license::web; +using namespace ts::ssl; +using namespace std; +using namespace std::chrono; + +WebStatistics::WebStatistics(const shared_ptr &manager, const std::shared_ptr& stats) : license_manager(manager), statistics_manager(stats) {} +WebStatistics::~WebStatistics() {} + +#define SFAIL(message) \ +do { \ + error = message; \ + this->stop(); \ + return false; \ +} while(0) + +static int enabled = 1; +static int disabled = 0; +bool WebStatistics::start(std::string &error, uint16_t port, const std::shared_ptr &ssl) { + { + std::lock_guard lock(this->running_lock); + if(this->_running) return false; + this->_running = true; + } + this->ssl = ssl; + + { + this->socket.local_address = make_unique(); + memset(this->socket.local_address.get(), 0, sizeof(sockaddr_in)); + this->socket.local_address->sin_family = AF_INET; + this->socket.local_address->sin_addr.s_addr = INADDR_ANY; + this->socket.local_address->sin_port = htons(port); + } + + this->socket.file_descriptor = ::socket(AF_INET, SOCK_STREAM, 0); + if (this->socket.file_descriptor < 0) SFAIL("Could not create new socket"); + + if(setsockopt(this->socket.file_descriptor, SOL_SOCKET, SO_REUSEADDR, &enabled, sizeof(enabled)) < 0) SFAIL("could not set reuse address"); + if(setsockopt(this->socket.file_descriptor, IPPROTO_TCP, TCP_CORK, &disabled, sizeof(disabled)) < 0) SFAIL("could not set no push"); + if(bind(this->socket.file_descriptor, (struct sockaddr *) this->socket.local_address.get(), sizeof(sockaddr_in)) < 0) SFAIL("Could not bind socket on " + string(inet_ntoa(this->socket.local_address->sin_addr))); + + if(listen(this->socket.file_descriptor, 32) < 0) SFAIL("Could not listen on socket"); + + this->socket.event_base = event_base_new(); + this->socket.event_accept = event_new(this->socket.event_base, this->socket.file_descriptor, EV_READ | EV_PERSIST, WebStatistics::handleEventAccept, this); + event_add(this->socket.event_accept, nullptr); + + this->socket.event_base_dispatch = make_unique(THREAD_SAVE_OPERATIONS, [&](){ + signal(SIGABRT, SIG_IGN); + event_base_dispatch(this->socket.event_base); + }); + + return true; +} + +void WebStatistics::stop() { + { + std::lock_guard lock(this->running_lock); + if(!this->_running) return; + this->_running = false; + } + for(const auto& client : this->get_clients()) + this->close_connection(client); + + if(this->socket.event_accept) { + event_del(this->socket.event_accept); + event_free(this->socket.event_accept); + } + this->socket.event_accept = nullptr; + + if(this->socket.event_base) + event_base_loopbreak(this->socket.event_base); + + if(this->socket.event_base_dispatch) + this->socket.event_base_dispatch->join(seconds(5)); + this->socket.event_base_dispatch = nullptr; + + if(this->socket.event_base) { + event_base_free(this->socket.event_base); + this->socket.event_base = nullptr; + } + + if(this->socket.file_descriptor != 0) { + shutdown(this->socket.file_descriptor, SHUT_RDWR); + close(this->socket.file_descriptor); + this->socket.file_descriptor = 0; + } +} + +void WebStatistics::initialize_client(const std::shared_ptr &client) { + weak_ptr weak_client = client; + + auto send_message = [&](const std::shared_ptr& client, const pipes::buffer_view& message) { + { + std::lock_guard lock(client->execute_lock); + client->buffer_write.push_back(message.string()); + } + + if(client->event_write) + event_add(client->event_write, nullptr); + }; + + { //WebSocket and SSL setup + + client->pipe_websocket = make_unique(); + + client->pipe_websocket->direct_process(pipes::PROCESS_DIRECTION_IN, true); + client->pipe_websocket->direct_process(pipes::PROCESS_DIRECTION_OUT, true); + + client->pipe_websocket->callback_error([&, weak_client](int code, const std::string &reason) { + auto _client = weak_client.lock(); + if(!_client) return; + + logError(LOG_LICENSE_WEB, "[{}][WS] Catched an error. code: {} reason: {}.", _client->client_prefix(), code, reason); + logError(LOG_LICENSE_WEB, "[{}][WS] Disconnecting client", _client->client_prefix()); + if(_client->pipe_websocket && _client->pipe_websocket->getState() == pipes::CONNECTED) _client->pipe_websocket->disconnect(1100, "Catched a server sided error"); + else this->close_connection(_client); + }); + client->pipe_websocket->callback_write([weak_client, send_message](const pipes::buffer_view& message) { + auto _client = weak_client.lock(); + if(!_client) return; + + if(_client->pipe_ssl) + _client->pipe_ssl->send(message); + else send_message(_client, message); + }); + client->pipe_websocket->callback_data([&, weak_client](const pipes::WSMessage& message) { + auto _client = weak_client.lock(); + if(!_client) return; + + this->handle_message(_client, message); //TODO if return false error handling! + }); + client->pipe_websocket->on_connect = [&, weak_client] { + auto _client = weak_client.lock(); + if(!_client) return; + + logMessage(LOG_LICENSE_WEB, "[{}] WebSocket handshake completed!", _client->client_prefix()); + }; + client->pipe_websocket->on_disconnect = [&, weak_client](const std::string& reason) { + auto _client = weak_client.lock(); + if(!_client) return; + + logMessage(LOG_LICENSE_WEB, "[{}] Remote connection disconnected ({} | {})", _client->client_prefix(), reason.length() >= 2 ? be2le16(reason.data()) : -1, reason.length() > 2 ? reason.substr(2) : ""); + this->close_connection(_client); + }; + client->pipe_websocket->callback_invalid_request = [&, weak_client](const http::HttpRequest& request, http::HttpResponse& response) { + auto _client = weak_client.lock(); + if(!_client) return; + + auto lmethod = request.method; + transform(lmethod.begin(), lmethod.end(), lmethod.begin(), ::tolower); + if(lmethod == "get" && !request.parameters["type"].empty()) + this->handle_request(_client, request, response); + }; + + client->pipe_websocket->initialize(); + + //FIXME Setup ssl + } + + { + client->pipe_ssl = make_unique(); + + client->pipe_ssl->direct_process(pipes::PROCESS_DIRECTION_IN, true); + client->pipe_ssl->direct_process(pipes::PROCESS_DIRECTION_OUT, true); + + client->pipe_ssl->callback_error([&, weak_client](int code, const std::string &reason) { + auto _client = weak_client.lock(); + if(!_client) return; + + logError(LOG_LICENSE_WEB, "[{}][SSL] Catched an error. code: {} reason: {}.", _client->client_prefix(), code, reason); + logError(LOG_LICENSE_WEB, "[{}][SSL] Disconnecting client", _client->client_prefix()); + if(_client->pipe_websocket && _client->pipe_websocket->getState() == pipes::CONNECTED) _client->pipe_websocket->disconnect(1100, "Catched a server sided error (SSL)"); + else this->close_connection(_client); + }); + client->pipe_ssl->callback_write([weak_client, send_message](const pipes::buffer_view& message) { + auto _client = weak_client.lock(); + if(!_client) return; + + send_message(_client, message); + }); + client->pipe_ssl->callback_data([&, weak_client](const pipes::buffer_view& message) { + auto _client = weak_client.lock(); + if(!_client) return; + + if(_client->pipe_websocket) _client->pipe_websocket->process_incoming_data(message); + }); + + { + auto options = make_shared(); + options->type = pipes::SSL::SERVER; + options->context_method = TLS_method(); + options->free_unused_keypairs = false; /* we dont want our keys get removed */ + + options->default_keypair({this->ssl->privateKey, this->ssl->certificate}); + if(!client->pipe_ssl->initialize(options)) { + logError(LOG_LICENSE_WEB, "[{}][SSL] Failed to setup ssl! Disconnecting client", client->client_prefix()); + this->close_connection(client); + } + } + } +} + +void WebStatistics::handleEventAccept(int fd, short, void *ptr_server) { + auto server = (WebStatistics*) ptr_server; + + auto client = make_shared(); + + { //Network accept + auto address = make_unique(); + auto address_length = (socklen_t) sizeof(*address); + + client->file_descriptor = accept(fd, (struct sockaddr *) address.get(), &address_length); + if (client->file_descriptor < 0) { + logCritical(LOG_LICENSE_WEB, "Failed to accept new client. ({} | {}/{})", client->file_descriptor, errno, strerror(errno)); + return; + } + if(setsockopt(client->file_descriptor, SOL_SOCKET, SO_REUSEADDR, &enabled, sizeof(enabled)) < 0);// CERR("could not set reuse addr"); + if(setsockopt(client->file_descriptor, IPPROTO_TCP, TCP_CORK, &disabled, sizeof(disabled)) < 0);// CERR("could not set no push"); + client->peer_address = std::move(address); + } + server->initialize_client(client); + + { //Client registration + std::lock_guard lock(server->clients_lock); + server->clients.push_back(client); + } + + { //IO Init + client->event_read = event_new(server->socket.event_base, client->file_descriptor, EV_READ | EV_PERSIST, WebStatistics::handleEventRead, server); + client->event_write = event_new(server->socket.event_base, client->file_descriptor, EV_WRITE, WebStatistics::handleEventWrite, server); + event_add(client->event_read, nullptr); + } + + logMessage(LOG_LICENSE_WEB, "Accepted new client from {}", net::to_string(client->peer_address->sin_addr)); +} + +void WebStatistics::handleEventRead(int file_descriptor, short, void* ptr_server) { + auto server = (WebStatistics*) ptr_server; + auto client = server->find_client_by_fd(file_descriptor); + if(!client || client->file_descriptor == 0) { + //TODO error + return; + } + + pipes::buffer buffer(1024); + + sockaddr_in remote_address{}; + socklen_t remote_address_size = sizeof(remote_address); + auto read = recvfrom(file_descriptor, buffer.data_ptr(), buffer.length(), 0, reinterpret_cast(&remote_address), &remote_address_size); + + if(read < 0){ + if(errno == EWOULDBLOCK) return; + logError(LOG_LICENSE_WEB, "[{}] Invalid read: {}/{}. Closing connection.", client->client_prefix(), errno, strerror(errno)); + if(client->event_read) + event_del_noblock(client->event_read); + server->close_connection(client); + return; + } else if(read == 0) { + debugMessage(LOG_LICENSE_WEB, "[{}] Invalid read (eof). Closing connection", client->client_prefix()); + if(client->event_read) + event_del_noblock(client->event_read); + server->close_connection(client); + return; + } + + buffer.resize(read); + + lock_guard lock(client->execute_lock); + if(client->file_descriptor == 0) return; + + if(client->pipe_ssl) { + client->pipe_ssl->process_incoming_data(buffer); + } else if(client->pipe_websocket) { + client->pipe_websocket->process_incoming_data(buffer); + } + else; //TODO error handling +} + +void WebStatistics::handleEventWrite(int file_descriptor, short, void* ptr_server) { + auto server = (WebStatistics*) ptr_server; + auto client = server->find_client_by_fd(file_descriptor); + if(!client) { + //TODO error + return; + } + + + std::lock_guard lock(client->execute_lock); + if(client->buffer_write.empty()) return; + auto& buffer = client->buffer_write.front(); + + auto written = send(file_descriptor, buffer.data(), buffer.length(), MSG_DONTWAIT | MSG_NOSIGNAL); + if(written < 0){ + if(errno == EWOULDBLOCK) return; + logError(LOG_LICENSE_WEB, "[{}] Invalid write: {}/{}. Closing connection.", client->client_prefix(), errno, strerror(errno)); + server->close_connection(client); + return; + } else if(written == 0) { + logError(LOG_LICENSE_WEB, "[{}] Invalid write (eof). Closing connection", client->client_prefix()); + server->close_connection(client); + return; + } + + if(written >= buffer.length()) + client->buffer_write.pop_front(); + else buffer = buffer.substr(written); + + if(!client->buffer_write.empty()) event_add(client->event_write, nullptr); +} + +void WebStatistics::close_connection(const std::shared_ptr &client) { + if(this->socket.event_base_dispatch && *this->socket.event_base_dispatch == pthread_self()) { + std::thread(bind(&WebStatistics::close_connection, this, client)).detach(); + return; + } + + { + std::lock_guard lock(this->clients_lock); + auto entry = find(this->clients.begin(), this->clients.end(), client); + if(entry != this->clients.end()) + this->clients.erase(entry); + else; //TODO Error handling? + } + + std::lock_guard lock(client->execute_lock); + if(client->event_read) { + event_del(client->event_read); + event_free(client->event_read); + client->event_read = nullptr; + } + if(client->event_write) { + event_del(client->event_write); + event_free(client->event_write); + client->event_write = nullptr; + } + if(client->file_descriptor > 0) { + if(shutdown(client->file_descriptor, SHUT_RDWR) < 0); //TODO error handling + if(close(client->file_descriptor) < 0); //TODO error handling + client->file_descriptor = 0; + } + + if(client->pipe_websocket) + client->pipe_websocket = nullptr; + if(client->pipe_ssl) { + client->pipe_ssl->finalize(); + client->pipe_ssl = nullptr; + } + logMessage(LOG_LICENSE_WEB, "[{}] Connection closed", client->client_prefix()); +} + +std::shared_ptr WebStatistics::find_client_by_fd(int file_descriptor) { + std::lock_guard lock(this->clients_lock); + for(const auto& client : this->clients) + if(client->file_descriptor == file_descriptor) return client; + return nullptr; +} + +#define HERR(message, ...) \ +do {\ + logError(LOG_LICENSE_WEB, "[{}] " message, client->client_prefix(), ##__VA_ARGS__); \ + return false; \ +} while(0) + +inline pipes::buffer json_dump(const Json::Value& value) { + Json::StreamWriterBuilder builder; + builder["indentation"] = ""; // If you want whitespace-less output + auto json = Json::writeString(builder, value); + return pipes::buffer((void*) json.c_str(), json.length()); +} + +Json::Value::Value(long value) : Value(to_string(value)) {} +Json::Value::Value(unsigned long value) : Value(to_string(value)) {} + +bool WebStatistics::handle_message(const std::shared_ptr &client, const pipes::WSMessage &raw_message) { + if(this->update_flood(client, 10)) { + static pipes::buffer _response; + if(_response.empty()) { + Json::Value response; + response["type"] = "error"; + response["code"] = "general"; + response["msg"] = "action not available due flood prevention"; + _response = json_dump(response); + } + + client->pipe_websocket->send({pipes::TEXT, _response}); + return true; + } + + logTrace(LOG_LICENSE_WEB, "[{}] Received message {}", client->client_prefix(), raw_message.data.string()); + + Json::Value message; + try { + istringstream ss(raw_message.data.string()); + ss >> message; + } catch (std::exception& ex) { + logError(LOG_LICENSE_WEB, "[{}] Received an invalid message: {}", client->client_prefix(), raw_message.data.string()); + return false; + } + try { + if(!message["type"].isString()) HERR("Missing/invalid type"); + + if(message["type"].asString() == "request") { + if(!message["request_type"].isString()) HERR("Missing/invalid request type"); + + if(message["request_type"].asString() == "general") { + this->update_flood(client, 50); + + Json::Value response; + response["type"] = "response"; + response["code"] = message["code"]; + + auto stats = this->statistics_manager->general_statistics(); + response["statistics"]["instances"] = to_string(stats->instances); + response["statistics"]["servers"] = to_string(stats->servers); + response["statistics"]["clients"] = to_string(stats->clients); + response["statistics"]["music"] = to_string(stats->bots); + + client->pipe_websocket->send({pipes::TEXT, json_dump(response)}); + return true; + } else if(message["request_type"].asString() == "history") { + auto type = message["history_type"].asInt(); + if(type < 0 || type > stats::HistoryStatistics::LAST_HALF_YEAR) + __throw_range_error("invalid range!"); + + if(type == stats::HistoryStatistics::LAST_DAY) + this->update_flood(client, 50); + if(type == stats::HistoryStatistics::DAY_YESTERDAY) + this->update_flood(client, 50); + if(type == stats::HistoryStatistics::LAST_HALF_YEAR) + this->update_flood(client, 100); + if(type == stats::HistoryStatistics::DAY_7DAYS_AGO) + this->update_flood(client, 60); + if(type == stats::HistoryStatistics::LAST_WEEK) + this->update_flood(client, 70); + if(type == stats::HistoryStatistics::LAST_MONTH) + this->update_flood(client, 80); + + + std::thread([&, client, type, message]() { + auto history = this->statistics_manager->history((stats::HistoryStatistics::HistoryType) type); + + Json::Value response; + response["type"] = "response"; + response["code"] = message["code"]; + + response["history"]["timestamp"] = duration_cast(history->evaluated.time_since_epoch()).count(); + response["history"]["begin"] = duration_cast(history->begin.time_since_epoch()).count(); + response["history"]["end"] = duration_cast(history->end.time_since_epoch()).count(); + response["history"]["interval"] = duration_cast(history->period).count(); + + int index = 0; + for(auto& element : history->statistics) { + response["history"]["data"][index]["instances"] = element->instance_online; + response["history"]["data"][index]["servers"] = element->servers_online; + response["history"]["data"][index]["clients"] = element->clients_online; + response["history"]["data"][index]["music"] = element->bots_online; + index++; + } + + lock_guard lock(client->execute_lock); + if(client->pipe_websocket) + client->pipe_websocket->send({pipes::TEXT, json_dump(response)}); + }).detach(); + return true; + } + } + } catch (const std::exception& ex) { + logError(LOG_LICENSE_WEB, "[{}] Message handling throws exception: {}", client->client_prefix(), ex.what()); + + Json::Value response; + response["type"] = "error"; + response["code"] = message["code"]; + response["message"] = "could not assign action"; + client->pipe_websocket->send({pipes::TEXT, json_dump(response)}); + return false; + } + + { + Json::Value response; + response["type"] = "error"; + response["code"] = message["code"]; + response["message"] = "could not assign action"; + client->pipe_websocket->send({pipes::TEXT, json_dump(response)}); + } + return true; +} + +bool WebStatistics::handle_request(const std::shared_ptr &client, const http::HttpRequest &request, http::HttpResponse &response) { + auto type = request.parameters.at("type"); + logMessage(LOG_LICENSE_WEB, "[{}] Received HTTP status request of type {}", client->client_prefix(), type); + + if(type == "request" && request.parameters.at("request_type") == "general") { + Json::Value json; + json["type"] = "response"; + auto stats = this->statistics_manager->general_statistics(); + json["statistics"]["instances"] = to_string(stats->instances); + json["statistics"]["servers"] = to_string(stats->servers); + json["statistics"]["clients"] = to_string(stats->clients); + json["statistics"]["music"] = to_string(stats->bots); + response.setHeader("data", {json_dump(json).string()}); + response.code = http::code::_200; + } + + return false; +} + +bool WebStatistics::update_flood(const std::shared_ptr &client, int flood_points) { + if(client->flood_reset.time_since_epoch().count() == 0) + client->flood_reset = system_clock::now(); + + client->flood_points += flood_points; + + auto diff = duration_cast(system_clock::now() - client->flood_reset); + if(diff.count() > 1000) { + diff -= milliseconds(1000); + auto reduce = diff.count() / 10; //Reduce 100fp per second + if(client->flood_points > reduce) + client->flood_points = 0; + else + client->flood_points -= reduce; + + client->flood_reset = system_clock::now(); + } + + return client->flood_points > 150; +} + +void WebStatistics::broadcast_message(const Json::Value &value) { + auto raw_value = json_dump(value); + for(const auto& client : this->get_clients()) { + std::lock_guard lock(client->execute_lock); + if(client->pipe_websocket && client->pipe_websocket->getState() == pipes::WebSocketState::CONNECTED) + client->pipe_websocket->send({pipes::TEXT, raw_value}); + } +} + +void WebStatistics::broadcast_notify_general_update() { + Json::Value message; + message["type"] = "notify"; + message["target"] = "general_update"; + this->broadcast_message(message); +} + +void WebStatistics::async_broadcast_notify_general_update() { + this->scheduler.execute([&]{ + this->broadcast_notify_general_update(); + }); +} \ No newline at end of file diff --git a/license/server/WebAPI.h b/license/server/WebAPI.h new file mode 100644 index 0000000..0e8696f --- /dev/null +++ b/license/server/WebAPI.h @@ -0,0 +1,106 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace license { + namespace server { + class LicenseManager; + } + namespace stats { + class StatisticManager; + } + + namespace web { + class WebStatistics { + struct Client { + std::unique_ptr peer_address; + int file_descriptor = 0; + event* event_read = nullptr; + event* event_write = nullptr; + + std::recursive_mutex execute_lock; + std::deque buffer_write; + + std::unique_ptr pipe_websocket; + std::unique_ptr pipe_ssl; + + std::chrono::system_clock::time_point flood_reset; + int flood_points; + + inline std::string client_prefix() { return peer_address ? net::to_string(peer_address->sin_addr) : "unconnected"; } + }; + public: + WebStatistics(const std::shared_ptr& /* license manager */, const std::shared_ptr& /* stats manager */); + virtual ~WebStatistics(); + + bool start(std::string& /* error */, uint16_t /* port */, const std::shared_ptr& /* ssl */); + inline bool running() { + std::lock_guard lock(this->running_lock); + return this->_running; + } + void stop(); + + inline std::deque> get_clients() { + std::lock_guard lock(this->clients_lock); + return this->clients; + } + + void close_connection(const std::shared_ptr& /* client */); + std::shared_ptr find_client_by_fd(int /* file descriptor */); + + void broadcast_message(const Json::Value& /* message */); + + void async_broadcast_notify_general_update(); + void broadcast_notify_general_update(); + + private: + bool _running = false; + std::recursive_mutex running_lock; + + std::shared_ptr license_manager; + std::shared_ptr statistics_manager; + + struct { + std::unique_ptr local_address; + int file_descriptor = 0; + event* event_accept = nullptr; + event_base* event_base = nullptr; + std::unique_ptr event_base_dispatch; + } socket; + + std::shared_ptr ssl; + + std::recursive_mutex clients_lock; + std::deque> clients; + + threads::ThreadPool scheduler{1, "WebStatistics #"}; + + protected: + static void handleEventAccept(int, short, void*); + static void handleEventRead(int, short, void*); + static void handleEventWrite(int, short, void*); + + void initialize_client(const std::shared_ptr& /* client */); + virtual bool handle_message(const std::shared_ptr& /* client */, const pipes::WSMessage& message); + virtual bool handle_request(const std::shared_ptr &client, const http::HttpRequest& /* request */, http::HttpResponse& /* response */); + + bool update_flood(const std::shared_ptr &client, int flood_points); + }; + } +} \ No newline at end of file diff --git a/license/shared/License.cpp b/license/shared/License.cpp new file mode 100644 index 0000000..68c0d9d --- /dev/null +++ b/license/shared/License.cpp @@ -0,0 +1,102 @@ +#include +#include + +//#define NO_OPEN_SSL +#include +#include +#include +#include "crypt.h" +#include "License.h" + +using namespace std; +using namespace std::chrono; + +inline void generate(char* buffer, size_t length){ + for(int index = 0; index < length; index++) + buffer[index] = rand(); +} + +namespace license { + std::string LicenseTypeNames[] = LT_NAMES; + + std::shared_ptr readLocalLicence(const std::string& buffer, std::string& error){ + string bbuffer = base64::decode(buffer); + if(bbuffer.length() < sizeof(License)) { + error = "Invalid license size"; + return nullptr; + } + + auto license = static_cast(malloc(sizeof(License))); + memcpy(license, bbuffer.data(), sizeof(License)); + + if(license->header.version != LICENSE_VERSION){ + error = "Invalid license version (" + to_string(license->header.version) + ")"; + return nullptr; + } + xorBuffer(&((char*) license)[sizeof(License::header)], sizeof(License::data), license->header.cryptKey, sizeof(license->header.cryptKey)); + + auto hash = digest::sha1(reinterpret_cast(&license->data), sizeof(license->data)); + + uint64_t checkSum = 0; + for(int i = 0; i < SHA_DIGEST_LENGTH; i++) + checkSum += (uint8_t) hash[i] << (i % 8); + + if((checkSum ^ *(uint64_t*) &license->header.cryptKey) != MAGIC_NUMER) { + error = "invalid check sum"; + return nullptr; + } + + return shared_ptr(license, [](License* l){ + if(l) free(l); + }); + } + + std::string exportLocalLicense(const std::shared_ptr& ref){ + auto copy = static_cast(malloc(sizeof(License))); + memcpy(copy, ref.get(), sizeof(License)); + + auto hash = digest::sha1(reinterpret_cast(©->data), sizeof(copy->data)); + + uint64_t checkSum = 0; + for(int i = 0; i < SHA_DIGEST_LENGTH; i++) + checkSum += (uint8_t) hash[i] << (i % 8); + checkSum ^= MAGIC_NUMER; + + generate(const_cast(copy->header.cryptKey), sizeof(copy->header.cryptKey)); + *(uint64_t*) ©->header.cryptKey = checkSum; + + + xorBuffer(&((char*) copy)[sizeof(License::header)], sizeof(License::data), copy->header.cryptKey, sizeof(copy->header.cryptKey)); + auto result = base64_encode((const char*) copy, sizeof(License)); + free(copy); + return result; + } + + std::string createLocalLicence(LicenseType type, std::chrono::system_clock::time_point until, std::string licenseOwner){ + auto license = shared_ptr(static_cast(malloc(sizeof(License))), [](License* l) { if(l) free(l); }); + assert(licenseOwner.length() < sizeof(license->data.licenceOwner)); + + license->header.version = LICENSE_VERSION; + generate(const_cast(license->data.licenceKey), sizeof(license->data.licenceKey)); + generate(const_cast(license->data.licenceOwner), sizeof(license->data.licenceOwner)); //Crap data :) + license->data.type = type; + license->data.endTimestamp = duration_cast(until.time_since_epoch()).count(); + memcpy((void *) license->data.licenceOwner, licenseOwner.c_str(), strlen(licenseOwner.c_str()) + 1); //Copy the string into it + + return exportLocalLicense(license); + } + + const char *exceptions::LicenseException::what() const throw() { + return this->errorMessage.c_str(); + } + + protocol::packet::packet(PacketType packetId, const ::google::protobuf::Message& message) { + this->header.packetId = packetId; + this->data = message.SerializeAsString(); + } + + protocol::packet::packet(license::protocol::PacketType packetId, nullptr_t) { + this->header.packetId = packetId; + this->data = ""; + } +} \ No newline at end of file diff --git a/license/shared/License.h b/license/shared/License.h new file mode 100644 index 0000000..5d850fc --- /dev/null +++ b/license/shared/License.h @@ -0,0 +1,186 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#define LICENSE_VERSION 1 +#define LICENSE_PROT_VERSION 2 +#define MAGIC_NUMER 0xBADC0DED + +namespace license { + namespace exceptions { + class LicenseException : public std::exception { + public: + LicenseException() = delete; + LicenseException(std::string message) : errorMessage(std::move(message)) {} + LicenseException(const LicenseException& ref) : errorMessage(ref.errorMessage) {} + LicenseException(LicenseException&& ref) : errorMessage(std::move(ref.errorMessage)) {} + + const char* what() const noexcept override; + private: + std::string errorMessage; + }; + } + + struct LicenseHeader { + uint16_t version; + const char cryptKey[64]; //The dummy key for data de/encryption + } __attribute__ ((__packed__)); + + /* + namespace v2 { + namespace data { + struct ChainHead { + uint32_t chain_version; + uint32_t chain_magic; + uint8_t sign[32]; + } __attribute__ ((__packed__)); + + struct ChainEntryHead { + uint8_t entry_type; //sign bit = contains private + int64_t entry_begin; + int32_t entry_length; + uint8_t key[32]; + } __attribute__ ((__packed__)); + } + + struct LicenseIssuer { + std::string name; + std::string email; + }; + + struct LicenseChainEntry { + data::ChainEntryHead head; + struct { + bool contains; + uint8_t key[32]; + } prv_key; + LicenseIssuer issuer; + }; + + struct LicenseChain { + public: + data::ChainHead head; + std::deque entries; + + private: + }; + } + */ + + enum LicenseType : uint8_t { + INVALID, + DEMO, + PREMIUM, + HOSTER, + PRIVATE, + }; + + inline bool isPremiumLicense(LicenseType type){ return type == HOSTER || type == PREMIUM || type == PRIVATE; } + + #define LT_NAMES {"Invalid", "Demo", "Premium", "Hoster", "Private"}; + extern std::string LicenseTypeNames[5]; + + struct License { + LicenseHeader header; + + //Crypted part + struct { + LicenseType type; + int64_t endTimestamp; + + const char licenceKey[64]; //The actual key + const char licenceOwner[64]; + + } __attribute__ ((__packed__)) data; + + inline std::string key() { return std::string(data.licenceKey, 64); } + inline std::chrono::time_point end() { return std::chrono::system_clock::time_point() + std::chrono::milliseconds(this->data.endTimestamp); } + inline std::string owner() { return std::string(this->data.licenceOwner); } + inline bool isValid() { return data.endTimestamp == 0 || std::chrono::system_clock::now() < this->end(); } + inline bool isPremium(){ return isPremiumLicense(data.type); } + } __attribute__ ((__packed__)); + + struct LicenseInfo { + LicenseType type; + std::string username; + std::string first_name; + std::string last_name; + std::string email; + std::chrono::system_clock::time_point start; + std::chrono::system_clock::time_point end; + std::chrono::system_clock::time_point creation; + + bool deleted = false; + + inline bool isValid() { return (end.time_since_epoch().count() == 0 || std::chrono::system_clock::now() < this->end); } + }; + + extern std::shared_ptr readLocalLicence(const std::string &, std::string &); + extern std::string exportLocalLicense(const std::shared_ptr&); + extern std::string createLocalLicence(LicenseType type, std::chrono::time_point until, std::string licenseOwner); + + namespace protocol { + enum RequestState { + UNCONNECTED, + CONNECTING, + + HANDSCAKE, + SERVER_VALIDATION, + LICENSE_INFO, + PROPERTY_ADJUSTMENT, + MANAGER_AUTHORIZATION, + MANAGER_CONNECTED, + + DISCONNECTING + }; + + enum PacketType : uint8_t { + PACKET_CLIENT_HANDSHAKE, + PACKET_SERVER_HANDSHAKE, + PACKET_CLIENT_SERVER_VALIDATION, + PACKET_SERVER_VALIDATION_RESPONSE, + PACKET_CLIENT_PROPERTY_ADJUSTMENT, + PACKET_SERVER_PROPERTY_ADJUSTMENT, + + PACKET_CLIENT_AUTH_REQUEST, + PACKET_SERVER_AUTH_RESPONSE, + PACKET_CLIENT_LICENSE_CREATE_REQUEST, + PACKET_SERVER_LICENSE_CREATE_RESPONSE, + + PACKET_CLIENT_LIST_REQUEST, + PACKET_SERVER_LIST_RESPONSE, + + PACKET_CLIENT_DELETE_REQUEST, + PACKET_CLIENT_DELETE_RESPONSE, + + PACKET_PING = 0xF0, + PACKET_DISCONNECT = 0xFF + }; + + struct packet { + struct { + PacketType packetId; + mutable uint16_t length; + } header; + std::string data; + + inline void prepare() const { + this->header.length = (uint16_t) data.length(); + } + + packet(PacketType packetId, std::string data) : data(std::move(data)), header({packetId, 0}) {} + +#ifdef GOOGLE_PROTOBUF_MESSAGE_H__ + packet(PacketType packetId, const ::google::protobuf::Message&); +#endif + packet(PacketType packetId, nullptr_t); + }; + } +} +//DEFINE_TRANSFORMS(license::LicenseType, uint8_t); \ No newline at end of file diff --git a/license/shared/LicenseRequest.cpp b/license/shared/LicenseRequest.cpp new file mode 100644 index 0000000..7ea1e5a --- /dev/null +++ b/license/shared/LicenseRequest.cpp @@ -0,0 +1,301 @@ +#include +#include +#include +#include "crypt.h" +#define DEFINE_HELPER +#include "LicenseRequest.h" +#include "License.h" +#include + +using namespace std; +using namespace std::chrono; +using namespace ts; +using namespace license; + +#define DEBUG_LICENSE_CLIENT +#define CERR(message) LICENSE_FERR(this, CouldNotConnectException, message) + + +LicenceRequest::LicenceRequest(const std::shared_ptr & license, const sockaddr_in& remoteAddr) : data(license) { +#ifdef DEBUG_LICENSE_CLIENT + memtrack::allocated(this); +#endif + memcpy(&this->remote_address, &remoteAddr, sizeof(remoteAddr)); + + assert(license->info); +} + +LicenceRequest::~LicenceRequest() { +#ifdef DEBUG_LICENSE_CLIENT + memtrack::freed(this); +#endif + this->abortRequest(); + + if(this->closeThread) { + this->closeThread->join(); + delete this->closeThread; + this->closeThread = nullptr; + } + + + delete this->currentFuture; + this->currentFuture = nullptr; +} + +threads::Future> LicenceRequest::requestInfo() { + { + lock_guard lock(this->lock); + if(this->currentFuture) return *this->currentFuture; + this->currentFuture = new threads::Future>(); + } + + this->beginRequest(); + return *this->currentFuture; +} + + +//Basic IO +void LicenceRequest::handleEventWrite(int fd, short event, void* ptrClient) { + auto* client = static_cast(ptrClient); + + buffer::RawBuffer* buffer = nullptr; + { + lock_guard lock(client->lock); + if((event & EV_TIMEOUT) > 0) { //Connect timeout + LICENSE_FERR(client, ConnectionException, "Connect timeout"); + return; + } + + if(client->state == protocol::CONNECTING){ + client->handleConnected(); + } + + if(client->state == protocol::UNCONNECTED || !client->event_write) + return; + + buffer = TAILQ_FIRST(&client->writeQueue); + if(!buffer) return; + + auto writtenBytes = send(fd, &buffer->buffer[buffer->index], buffer->length - buffer->index, MSG_NOSIGNAL | MSG_DONTWAIT); + buffer->index += writtenBytes; + + if(buffer->index >= buffer->length) { + TAILQ_REMOVE(&client->writeQueue, buffer, tail); + delete buffer; + } + if(!TAILQ_EMPTY(&client->writeQueue)) + event_add(client->event_write, nullptr); + } +} + +void LicenceRequest::sendPacket(const protocol::packet& packet) { + if(this->state == protocol::UNCONNECTED || this->state == protocol::DISCONNECTING) { + if(this->verbose) + logError("Tried to send a packet to an unconnected remote!"); + return; + } + packet.prepare(); + + auto buffer = new buffer::RawBuffer(packet.data.length() + sizeof(packet.header)); + memcpy(buffer->buffer, &packet.header, sizeof(packet.header)); + memcpy(&buffer->buffer[sizeof(packet.header)], packet.data.data(), packet.data.length()); + + if(!this->cryptKey.empty()) + xorBuffer(&buffer->buffer[sizeof(packet.header)], packet.data.length(), this->cryptKey.data(), this->cryptKey.length()); + + { + lock_guard lock(this->lock); + TAILQ_INSERT_TAIL(&this->writeQueue, buffer, tail); + if(this->event_write) + event_add(this->event_write, nullptr); + } +} + +void LicenceRequest::handleEventRead(int fd, short, void* ptrClient) { + auto* client = static_cast(ptrClient); + + auto buffer = std::unique_ptr{malloc(1024), free}; + sockaddr_in remoteAddr{}; + socklen_t remoteAddrSize = sizeof(remoteAddr); + + auto read = recvfrom(fd, buffer.get(), 1024, MSG_NOSIGNAL | MSG_DONTWAIT, reinterpret_cast(&remoteAddr), &remoteAddrSize); + + if(read < 0){ + if(errno == EWOULDBLOCK) return; + if(client->event_read) + event_del_noblock(client->event_read); + LICENSE_FERR(client, ConnectionException, "Invalid read: " + string(strerror(errno)) + "/" + to_string(errno)); + return; + } else if(read == 0) { + if(client->event_read) + event_del_noblock(client->event_read); + LICENSE_FERR(client, ConnectionException, "IO error (" + to_string(errno) + "): " + string(strerror(errno))); + return; + } + + client->handleMessage(string((char*) buffer.get(), read)); +} + + +static int enabled = 1; +static int disabled = 0; +void LicenceRequest::beginRequest() { + lock_guard lock(this->lock); + TAILQ_INIT(&this->writeQueue); + + this->file_descriptor = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); + if(this->file_descriptor < 0) CERR("Socket setup failed"); + + signal(SIGPIPE, SIG_IGN); + + auto state = ::connect(this->file_descriptor, reinterpret_cast(&this->remote_address), sizeof(this->remote_address)); + if(state < 0 && errno != EINPROGRESS) CERR("connect() failed (" + string(strerror(errno)) + ")"); + + if(setsockopt(this->file_descriptor, SOL_SOCKET, SO_REUSEADDR, &enabled, sizeof(enabled)) < 0) CERR("could not set reuse addr"); + if(setsockopt(this->file_descriptor, IPPROTO_TCP, TCP_CORK, &disabled, sizeof(disabled)) < 0) CERR("could not set no push"); + + if(fcntl(this->file_descriptor, F_SETFD, fcntl(this->file_descriptor, F_GETFL, 0) | FD_CLOEXEC | O_NONBLOCK) < 0) CERR("Failed to set FD_CLOEXEC and O_NONBLOCK"); + + this->event_base = event_base_new(); + this->event_read = event_new(this->event_base, this->file_descriptor, EV_READ | EV_PERSIST, LicenceRequest::handleEventRead, this); + this->event_write = event_new(this->event_base, this->file_descriptor, EV_WRITE, LicenceRequest::handleEventWrite, this); + + this->state = protocol::CONNECTING; //First set connected, then we could enable the event loop + + event_dispatch = std::thread([&]() { + signal(SIGPIPE, SIG_IGN); + + { /* now we could start listening */ + lock_guard _lock(this->lock); + if(!this->event_read || !this->event_write) return; + + event_add(this->event_read, nullptr); + timeval connect_timeout{5, 0}; + event_add(this->event_write, &connect_timeout); + } + + event_base_dispatch(this->event_base); + }); +} + +void LicenceRequest::handleConnected() { + this->state = protocol::HANDSCAKE; + + uint8_t handshakeBuffer[4]; + handshakeBuffer[0] = 0xC0; + handshakeBuffer[1] = 0xFF; + handshakeBuffer[2] = 0xEE; + handshakeBuffer[3] = LICENSE_PROT_VERSION; + this->sendPacket(protocol::packet{protocol::PACKET_CLIENT_HANDSHAKE, string((const char*) handshakeBuffer, 4)}); //Initialise packet +} + +void LicenceRequest::handleMessage(const std::string& message) { + if(message.length() < sizeof(protocol::packet::header)) LICENSE_FERR(this, ConnectionException, "Invalid packet size"); + protocol::packet packet{protocol::PACKET_DISCONNECT, ""}; + memcpy(&packet.header, message.data(), sizeof(protocol::packet::header)); + packet.data = message.substr(sizeof(protocol::packet::header)); + + if(!this->cryptKey.empty()) { + xorBuffer((char*) packet.data.data(), packet.data.length(), this->cryptKey.data(), this->cryptKey.length()); + } + + if(packet.header.packetId == protocol::PACKET_SERVER_HANDSHAKE) { + this->handlePacketHandshake(packet.data); + } else if(packet.header.packetId == protocol::PACKET_DISCONNECT) { + this->handlePacketDisconnect(packet.data); + } else if(packet.header.packetId == protocol::PACKET_SERVER_VALIDATION_RESPONSE) { + this->handlePacketLicenseInfo(packet.data); + } else if(packet.header.packetId == protocol::PACKET_SERVER_PROPERTY_ADJUSTMENT) { + this->handlePacketInfoAdjustment(packet.data); + } else + LICENSE_FERR(this, ConnectionException, "Invalid packet id (" + to_string(packet.header.packetId) + ")"); +} + +void LicenceRequest::disconnect(const std::string& message) { + if(this->state != protocol::UNCONNECTED && this->state != protocol::DISCONNECTING) + this->sendPacket({protocol::PACKET_DISCONNECT, message}); + this->closeConnection(); + //TODO flush? +} + +void LicenceRequest::closeConnection() { + event *event_read, *event_write; + { + lock_guard lock(this->lock); + if(this->state == protocol::UNCONNECTED) return; + + if(this->event_dispatch.get_id() == this_thread::get_id()) { //We could not close in the same thread as we read/write (we're joining it later) + if(this->state == protocol::DISCONNECTING) return; + + this->state = protocol::DISCONNECTING; + this->closeThread = new threads::Thread(THREAD_SAVE_OPERATIONS, [&]() { this->closeConnection(); }); + +#ifdef DEBUG_LICENSE_CLIENT + if(this->verbose) { + debugMessage("Running close in a new thread"); + this->closeThread->name("License request close"); + } +#endif + return; + } + this->state = protocol::UNCONNECTED; + + event_read = this->event_read; + event_write = this->event_write; + + this->event_write = nullptr; + this->event_read = nullptr; + } + + if(event_read) { + event_del_block(event_read); + event_free(event_read); + } + + if(event_write) { + event_del_block(event_write); + event_free(event_write); + } + + /* close before base shutdown (else epoll hangup) */ + if(this->file_descriptor > 0) { + shutdown(this->file_descriptor, SHUT_RDWR); + close(this->file_descriptor); + } + this->file_descriptor = 0; + + { + lock_guard lock(this->lock); + ts::buffer::RawBuffer* buffer; + while ((buffer = TAILQ_FIRST(&this->writeQueue))) { + TAILQ_REMOVE(&this->writeQueue, buffer, tail); + delete buffer; + } + } + + { + if(this->event_base) { + timeval seconds{1, 0}; + event_base_loopexit(this->event_base, &seconds); + event_base_loopexit(this->event_base, nullptr); + } + + if(this->event_dispatch.joinable()) { + this->event_dispatch.join(); + } + + if(this->event_base) { + event_base_free(this->event_base); + this->event_base = nullptr; + } + } + +#ifdef DEBUG_LICENSE_CLIENT + if(this->verbose) + debugMessage("Executing close done"); +#endif +} + +void LicenceRequest::abortRequest(const std::chrono::system_clock::time_point &timeout) { + this->closeConnection(); +} diff --git a/license/shared/LicenseRequest.h b/license/shared/LicenseRequest.h new file mode 100644 index 0000000..d582df5 --- /dev/null +++ b/license/shared/LicenseRequest.h @@ -0,0 +1,129 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "License.h" + +#ifdef DEFINE_HELPER + #define LICENSE_FERR(this, class, message) \ + do { \ + this->currentException = std::make_shared(message); \ + if(this->currentFuture && this->currentFuture->state() == threads::FutureState::WORKING) this->currentFuture->executionFailed(); \ + this->disconnect("internal error"); \ + return; \ + } while(0) +#endif + +namespace license { + namespace exceptions { + class CouldNotConnectException : public LicenseException { + public: + explicit CouldNotConnectException(const std::string &message) : LicenseException(message) {} + }; + + class ConnectionException : public LicenseException { + public: + explicit ConnectionException(const std::string &message) : LicenseException(message) {} + }; + + class UnexcpectedDisconnectException : public LicenseException { + public: + explicit UnexcpectedDisconnectException(const std::string &message) : LicenseException(message) {} + }; + + class InvalidResponseException : public LicenseException { + public: + explicit InvalidResponseException(const std::string &message) : LicenseException(message) {} + }; + } + + struct ServerInfo { + std::string unique_identifier; + int64_t timestamp; + std::string uname; + std::string version; + }; + + struct LicenseRequestResponse { + std::shared_ptr license; + bool license_valid; + + int64_t speach_varianz_adjustment; + bool speach_reset; + bool properties_valid; + + std::chrono::system_clock::time_point age; + }; + + struct LicenseRequestData { + std::shared_ptr license = nullptr; + std::shared_ptr info = nullptr; + + int64_t speach_total = 0; + int64_t speach_dead = 0; + int64_t speach_online = 0; + int64_t speach_varianz = 0; + + int64_t client_online = 0; + int64_t web_clients_online = 0; + int64_t bots_online = 0; + int64_t queries_online = 0; + int64_t servers_online = 0; + }; + + class LicenceRequest { + public: + typedef threads::Future> ResponseFuture; + LicenceRequest(const std::shared_ptr&, const sockaddr_in&); + ~LicenceRequest(); + + std::shared_ptr exception(){ return this->currentException; } + void clearExceptions(){ this->currentException = nullptr; } + ResponseFuture requestInfo(); + void abortRequest(const std::chrono::system_clock::time_point& timeout = std::chrono::system_clock::time_point()); + + void sendPacket(const protocol::packet&); + + bool verbose = true; + private: + std::shared_ptr data; + threads::Future>* currentFuture = nullptr; + std::shared_ptr response = nullptr; + std::shared_ptr currentException; + + std::recursive_mutex lock; + protocol::RequestState state = protocol::UNCONNECTED; + + sockaddr_in remote_address; + + int file_descriptor = 0; + std::thread event_dispatch; + threads::Thread* closeThread = nullptr; + struct event_base* event_base = nullptr; + event* event_read = nullptr; + event* event_write = nullptr; + + TAILQ_HEAD(, ts::buffer::RawBuffer) writeQueue; + + std::string cryptKey = ""; + + void beginRequest(); + void handleConnected(); + + static void handleEventRead(int, short, void*); + static void handleEventWrite(int, short, void*); + + void handleMessage(const std::string&); + void disconnect(const std::string&); + void closeConnection(); + + void handlePacketHandshake(const std::string&); + void handlePacketDisconnect(const std::string&); + void handlePacketLicenseInfo(const std::string&); + void handlePacketInfoAdjustment(const std::string&); + }; +} \ No newline at end of file diff --git a/license/shared/LicenseRequestHandler.cpp b/license/shared/LicenseRequestHandler.cpp new file mode 100644 index 0000000..a72acd8 --- /dev/null +++ b/license/shared/LicenseRequestHandler.cpp @@ -0,0 +1,105 @@ +#include +#include + +#define DEFINE_HELPER +#include "LicenseRequest.h" + +using namespace license; +using namespace std; +using namespace std::chrono; + +void LicenceRequest::handlePacketDisconnect(const std::string& message) { + if(this->state != protocol::DISCONNECTING) + LICENSE_FERR(this, UnexcpectedDisconnectException, "Remote side closed the connection (" + message + ")"); +} + +void LicenceRequest::handlePacketHandshake(const std::string& data) { + if(this->state != protocol::HANDSCAKE) LICENSE_FERR(this, InvalidResponseException, "Protocol state mismatch"); + if(data.length() < 3) LICENSE_FERR(this, InvalidResponseException, "Invalid packet size"); + + if((uint8_t) data[0] != 0xAF || (uint8_t) data[1] != 0xFE) LICENSE_FERR(this, InvalidResponseException, "Invalid handshake"); + if((uint8_t) data[2] != LICENSE_PROT_VERSION) LICENSE_FERR(this, InvalidResponseException, "Invalid license protocol version. Please update TeaSpeak!"); + + auto key_length = be2le16(data.data(), 3); + if(data.length() < key_length + 3) LICENSE_FERR(this, InvalidResponseException, "Invalid packet size"); + this->cryptKey = data.substr(5, key_length); + + ts::proto::license::ServerValidation request; + if(this->data->license) { + request.set_licensed(true); + request.set_license_info(true); + request.set_license(exportLocalLicense(this->data->license)); + } else { + request.set_licensed(false); + request.set_license_info(false); + } + request.mutable_info()->set_uname(this->data->info->uname); + request.mutable_info()->set_version(this->data->info->version); + request.mutable_info()->set_timestamp(this->data->info->timestamp); + request.mutable_info()->set_unique_id(this->data->info->unique_identifier); + + this->sendPacket(protocol::packet{protocol::PACKET_CLIENT_SERVER_VALIDATION, request}); + this->state = protocol::LICENSE_INFO; +} + +void LicenceRequest::handlePacketLicenseInfo(const std::string& message) { + ts::proto::license::LicenseResponse response; + if(!response.ParseFromString(message)) LICENSE_FERR(this, InvalidResponseException, "Could not parse response"); + + auto result = make_shared(); + auto licenseInfo = make_shared(); + if(!response.has_license_info() && this->data->license) LICENSE_FERR(this, InvalidResponseException, "Missing license info"); + + if(this->data->license) { + licenseInfo->type = (LicenseType) response.license_info().type(); + licenseInfo->email = response.license_info().email(); + licenseInfo->username = response.license_info().username(); + licenseInfo->first_name = response.license_info().first_name(); + licenseInfo->last_name = response.license_info().last_name(); + licenseInfo->creation = system_clock::time_point() + milliseconds(response.license_info().created()); + licenseInfo->start = system_clock::time_point() + milliseconds(response.license_info().begin()); + licenseInfo->end = system_clock::time_point() + milliseconds(response.license_info().end()); + } else { + licenseInfo->type = LicenseType::DEMO; + licenseInfo->email = "license@teaspeak.de"; + licenseInfo->username = "WolverinDEV"; + licenseInfo->first_name = "Max"; + licenseInfo->last_name = "Musterman"; + licenseInfo->creation = system_clock::now(); + licenseInfo->start = system_clock::now(); + licenseInfo->end = system_clock::time_point(); + } + result->license_valid = response.blacklist().state() == ts::proto::license::VALID; //TODO more detailed + result->age = system_clock::now(); + + result->license = licenseInfo; + this->response = result; + + + ts::proto::license::PropertyUpdateRequest infos; + infos.set_speach_total(this->data->speach_total); + infos.set_speach_dead(this->data->speach_dead); + infos.set_speach_online(this->data->speach_online); + infos.set_speach_varianz(this->data->speach_varianz); + + infos.set_clients_online(this->data->client_online); + infos.set_bots_online(this->data->bots_online); + infos.set_queries_online(this->data->queries_online); + infos.set_servers_online(this->data->servers_online); + infos.set_web_clients_online(this->data->web_clients_online); + this->sendPacket({protocol::PACKET_CLIENT_PROPERTY_ADJUSTMENT, infos}); + this->state = protocol::PROPERTY_ADJUSTMENT; +} + +void LicenceRequest::handlePacketInfoAdjustment(const std::string& message) { + ts::proto::license::PropertyUpdateResponse response; + if(!response.ParseFromString(message)) LICENSE_FERR(this, InvalidResponseException, "Could not parse response"); + + this->response->properties_valid = response.accepted(); + this->response->speach_varianz_adjustment = response.speach_varianz_corrector(); + this->response->speach_reset = response.reset_speach(); + this->currentFuture->executionSucceed(this->response); + this->response = nullptr; + + this->disconnect("query succeeded!"); +} \ No newline at end of file diff --git a/license/shared/crypt.h b/license/shared/crypt.h new file mode 100644 index 0000000..1430a0c --- /dev/null +++ b/license/shared/crypt.h @@ -0,0 +1,6 @@ +#pragma once + +inline void xorBuffer(char *buffer, size_t bufferLength, const char *xOr, size_t xorLength){ + for(int index = 0; index < bufferLength; index++) + buffer[index] ^= xOr[index % xorLength]; +} \ No newline at end of file diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt new file mode 100644 index 0000000..7b9e1db --- /dev/null +++ b/server/CMakeLists.txt @@ -0,0 +1,270 @@ +cmake_minimum_required(VERSION 3.6) +project(TeaSpeak-Server) + +set(CMAKE_VERBOSE_MAKEFILE ON) +#--allow-multiple-definition +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fpermissive -Wall -Wno-reorder -Wno-sign-compare -static-libgcc -static-libstdc++ -g -Wl,-no-whole-archive -pthread ${MEMORY_DEBUG_FLAGS}") +set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3") +set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -O3") + +#NDEBUG +#set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELEASE} -O2") +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/environment/) + +#disable for debug +#add_definitions(-DRELEASE_MODE) + +include_directories(../music/include/) +include_directories(../shared/src) +include_directories(../license/src) +include_directories(../MusicBot/src) +include_directories(/usr/local/include/breakpad) +include_directories(${LIBRARY_PATH}/tomcrypt/src/headers) +include_directories(${LIBRARY_PATH}/spdlog/include) + +add_definitions(-DLTM_DESC) +add_definitions(-DMUSIC_BOT) +add_definitions(-DUSE_BORINGSSL) + +#0 = STABLE +#1 = BETA +#2 = ALPHA +#3 = PRIVATE +option(BUILD_TYPE "Sets the build type" OFF) +option(BUILD_TYPE_NAME "Sets the build type name" OFF) +option(COMPILE_WEB_CLIENT "Enable/Disable the web cleint future" OFF) +set(COMPILE_WEB_CLIENT "ON") + +set(CMAKE_VERBOSE_MAKEFILE ON) +set(SERVER_SOURCE_FILES + main.cpp + src/client/ConnectedClient.cpp + src/client/voice/PrecomputedPuzzles.cpp + src/client/voice/VoiceClient.cpp + src/client/voice/VoiceClientHandschake.cpp + src/client/voice/VoiceClientCommandHandler.cpp + src/client/voice/VoiceClientPacketHandler.cpp + src/client/voice/VoiceClientView.cpp + src/TS3ServerClientManager.cpp + src/TSServer.cpp + src/TS3ServerHeartbeat.cpp + src/SignalHandler.cpp + src/server/VoiceServer.cpp + src/server/POWHandler.cpp + src/client/voice/VoiceClientConnection.cpp + src/client/ConnectedClientCommandHandler.cpp + src/client/ConnectedClientNotifyHandler.cpp + src/ServerManager.cpp + src/server/file/FileServer.cpp + src/channel/ServerChannel.cpp + src/channel/ClientChannelView.cpp + src/client/file/FileClient.cpp + src/client/file/FileClientIO.cpp + src/Group.cpp + src/manager/BanManager.cpp + src/client/InternalClient.cpp + #src/weblist/WeblistClient.cpp + #src/weblist/WebList.cpp + + src/client/DataClient.cpp + src/server/QueryServer.cpp + src/client/query/QueryClient.cpp + src/client/query/QueryClientCommands.cpp + src/client/query/QueryClientNotify.cpp + + + src/manager/IpListManager.cpp + + src/ConnectionStatistics.cpp + + src/manager/TokeManager.cpp + + src/terminal/CommandHandler.cpp + + src/manager/ComplainManager.cpp + src/DatabaseHelper.cpp + + src/manager/LetterManager.cpp + + tomcryptTest.cpp + + src/pinteraction/ApplicationInteraction.cpp + src/ServerManagerSnapshot.cpp + src/ServerManagerSnapshotDeploy.cpp + src/client/music/Song.cpp + src/music/PlayablePlaylist.cpp + src/InstanceHandler.cpp + src/InstanceHandlerSetup.cpp + + src/Configuration.cpp + + src/build.cpp + + src/music/MusicPlaylist.cpp + src/client/music/MusicClient.cpp + src/client/music/MusicClientPlayer.cpp + src/client/ConnectedClientTextCommandHandler.cpp + src/music/MusicBotManager.cpp + src/client/music/internal_provider/channel_replay/ChannelProvider.cpp + + src/geo/GeoLocation.cpp + src/geo/IP2Location.cpp + src/geo/VPNBlocker.cpp + + src/client/query/XMacroEventTypes.h + + src/server/VoiceIOManager.cpp + src/server/WebIoManager.cpp + src/client/SpeakingClient.cpp + + src/lincense/LicenseHelper.cpp + ../shared/src/ssl/SSLManager.cpp + + src/manager/SqlDataManager.cpp + + src/ShutdownHelper.cpp + src/client/music/MusicQueue.cpp + src/lincense/TeamSpeakLicense.cpp + + src/weblist/WebListManager.cpp + src/weblist/TeamSpeakWebClient.cpp +) +if(COMPILE_WEB_CLIENT) + add_definitions(-DCOMPILE_WEB_CLIENT) + + set(SERVER_SOURCE_FILES + ${SERVER_SOURCE_FILES} + + src/server/WebServer.cpp + src/client/web/WebClient.cpp +# src/server/web/WebRTCServer.cpp + src/client/web/WSWebClient.cpp + src/client/web/SampleHandler.cpp + src/client/SpeakingClientHandshake.cpp + src/client/web/VoiceBridge.cpp + ) +endif() + +add_executable(PermHelper helpers/permgen.cpp) +target_link_libraries(PermHelper + ${LIBRARY_PATH_ED255} + + TeaSpeak #Static + TeaLicenseHelper #Static + TeaMusic #Static + ${LIBRARY_PATH_THREAD_POOL} #Static + ${LIBRARY_PATH_TERMINAL} #Static + ${LIBRARY_PATH_VARIBALES} + ${LIBRARY_PATH_YAML} + pthread + stdc++fs + ${LIBEVENT_PATH}/libevent.a + ${LIBEVENT_PATH}/libevent_pthreads.a + ${LIBRARY_PATH_OPUS} + ${LIBRARY_PATH_JSON} + ${LIBRARY_PATH_PROTOBUF} + + #${LIBWEBRTC_LIBRARIES} #ATTENTIAN! WebRTC does not work with crypto! (Already contains a crypto version) + ${LIBRARY_TOM_CRYPT} + ${LIBRARY_TOM_MATH} + + #We're forsed to use boringssl caused by the fact that boringssl is already within webrtc! + + #Require a so + sqlite3 + + ${LIBRARY_PATH_BREAKPAD} + ${LIBRARY_PATH_JDBC} + ${LIBRARY_PATH_PROTOBUF} + + ${LIBRARY_PATH_DATA_PIPES} + ${LIBRARY_PATH_BORINGSSL_SSL} + ${LIBRARY_PATH_BORINGSSL_CRYPTO} + dl + jemalloc +) + +SET(CPACK_PACKAGE_VERSION_MAJOR "1") +SET(CPACK_PACKAGE_VERSION_MINOR "3") +SET(CPACK_PACKAGE_VERSION_PATCH "22") +if(BUILD_TYPE_NAME EQUAL OFF) + SET(CPACK_PACKAGE_VERSION_DATA "beta") +elseif(BUILD_TYPE_NAME STREQUAL "") + SET(CPACK_PACKAGE_VERSION_DATA "") +else() + SET(CPACK_PACKAGE_VERSION_DATA "-${BUILD_TYPE_NAME}") +endif() +if(BUILD_TYPE EQUAL OFF) + SET(BUILD_TYPE "1") +endif() +set_source_files_properties(src/build.cpp PROPERTIES + COMPILE_FLAGS "-DBUILD_MAJOR=${CPACK_PACKAGE_VERSION_MAJOR} -DBUILD_MINOR=${CPACK_PACKAGE_VERSION_MINOR} -DBUILD_PATCH=${CPACK_PACKAGE_VERSION_PATCH} -DBUILD_DATA=\"${CPACK_PACKAGE_VERSION_DATA}\" -DBUILD_TYPE=${BUILD_TYPE} -DBUILD_COUNT=0") +file(WRITE repro/env/buildVersion.txt "${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}${CPACK_PACKAGE_VERSION_DATA}") + +add_executable(TeaSpeakServer ${SERVER_SOURCE_FILES}) +target_link_libraries(TeaSpeakServer + ${LIBRARY_PATH_THREAD_POOL} #Static + TeaSpeak #Static + TeaLicenseHelper #Static + TeaMusic #Static + ${LIBRARY_PATH_TERMINAL} #Static + ${LIBRARY_PATH_VARIBALES} + ${LIBRARY_PATH_YAML} + pthread + stdc++fs + ${LIBEVENT_PATH}/libevent.a + ${LIBEVENT_PATH}/libevent_pthreads.a + ${LIBRARY_PATH_OPUS} + ${LIBRARY_PATH_JSON} + ${LIBRARY_PATH_PROTOBUF} + + #We're forsed to use boringssl caused by the fact that boringssl is already within webrtc! + + #Require a so + sqlite3 + + ${LIBRARY_PATH_BREAKPAD} + ${LIBRARY_PATH_JDBC} + ${LIBRARY_PATH_PROTOBUF} + + #${LIBWEBRTC_LIBRARIES} #ATTENTIAN! WebRTC does not work with crypto! (Already contains a crypto version) + ${LIBRARY_TOM_CRYPT} + ${LIBRARY_TOM_MATH} + + ${LIBRARY_PATH_ED255} +) + +if(${COMPILE_WEB_CLIENT}) + find_package(LibNice REQUIRED) + find_package(UsrSCTP REQUIRED) + target_link_libraries(TeaSpeakServer + LibNice::LibNice + ${LIBRARY_PATH_DATA_PIPES} + ) +endif() +include_directories(${LIBRARY_PATH}/boringssl/include/) +target_link_libraries(TeaSpeakServer + ${LIBRARY_PATH_BORINGSSL_SSL} + ${LIBRARY_PATH_BORINGSSL_CRYPTO} + dl +) + +set(DISABLE_JEMALLOC ON) +if(NOT DISABLE_JEMALLOC) + target_link_libraries(TeaSpeakServer + jemalloc + ) + add_definitions(-DHAVE_JEMALLOC) +endif() + +#Fix RPATH +#patchelf --set-rpath ./libs/ TeaSpeakServer +#patchelf --remove-rpath TeaSpeakServer + +#add_custom_command( +# TARGET TeaSpeakServer +## COMMAND bash -c "patchelf --set-rpath ./libs/ ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}TeaSpeakServer" +# COMMAND bash -c "patchelf --remove-rpath ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}TeaSpeakServer" +# WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +# COMMENT "Cleaning RPATH" +#) diff --git a/server/CreateEnviroment.txt b/server/CreateEnviroment.txt new file mode 100644 index 0000000..b816ada --- /dev/null +++ b/server/CreateEnviroment.txt @@ -0,0 +1,5 @@ +Build opus: +./configure --with-pic CFLAGS="-O2" + +Build libevent 2.1.8 (with -fPIC) + diff --git a/server/VersionHelper.sh b/server/VersionHelper.sh new file mode 100644 index 0000000..f354f0c --- /dev/null +++ b/server/VersionHelper.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +if [ -e "build.data" ]; then + echo "File exists" + DATA=$(cat "build.data") +else + echo "Create new file" + echo "0" > "build.data" +fi + +DATA=$(($DATA+1)) + +echo "Data: $DATA" \ No newline at end of file diff --git a/server/environment/geoloc b/server/environment/geoloc new file mode 120000 index 0000000..6be1434 --- /dev/null +++ b/server/environment/geoloc @@ -0,0 +1 @@ +../repro/env/geoloc/ \ No newline at end of file diff --git a/server/environment/providers b/server/environment/providers new file mode 120000 index 0000000..c041e20 --- /dev/null +++ b/server/environment/providers @@ -0,0 +1 @@ +../../music/bin/providers/ \ No newline at end of file diff --git a/server/environment/resources b/server/environment/resources new file mode 120000 index 0000000..9cca8cc --- /dev/null +++ b/server/environment/resources @@ -0,0 +1 @@ +../repro/env/resources/ \ No newline at end of file diff --git a/server/flood.sh b/server/flood.sh new file mode 100755 index 0000000..1215a20 --- /dev/null +++ b/server/flood.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +while [ true ]; do + +# nc -w 10 172.17.0.2 $1& +#echo "login serveradmin LiPdiKB"; echo "servercreate" + (echo "login serveradmin markus"; echo "use 3"; echo "channellist"; echo "clientlist") | nc -w 10 localhost $1& + #nc -w 10 localhost $1& + # echo "quit" | nc localhost $1& +# echo "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" | openssl s_client -connect localhost:10101 + #nc localhost $1& + PID=$! + ps -p ${PID} > /dev/null 2>&1 + if [ "$?" -ne "0" ]; then + echo -e "\nInvalid command" + else + echo "" + #kill -9 ${PID} + fi +done diff --git a/server/gui/mainwindow.ui b/server/gui/mainwindow.ui new file mode 100644 index 0000000..27aef0f --- /dev/null +++ b/server/gui/mainwindow.ui @@ -0,0 +1,80 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + MainWindow + + + + + + + 0 + + + + true + + + General + + + + + 260 + 180 + 181 + 31 + + + + Stop Instance + + + + + + Servers + + + + + Log + + + + + + + + + + 0 + 0 + 800 + 19 + + + + + + + + ts::gui::ServerTabWidget + QWidget +
src/ServerTabWidget.h
+ 1 +
+
+ + +
diff --git a/server/gui/resources/Smoke.png b/server/gui/resources/Smoke.png new file mode 100644 index 0000000000000000000000000000000000000000..6a254c8fd3ddc03da345834cc370bcded7a682f5 GIT binary patch literal 8817 zcmeHtXIN8Pw=PJPj&x-c6cto@q)8JMQL0E6q!U1TFCl<*X#tVmtRR9EQK|t2kzPa~ zlu)Gzgc>09oVAesd>qf-_dENXd;i_=%sI!HYpg|i-;D8&86MqHSE8jlNkv3NM0@+z zjk`od#DGamL`ecHb_M#{z(Q$ep{)w648S0oeXI8v2oSrYs;vmPg#Z8ghkpSlVh3;F z4`=OuS6z#U_!tQ(894>zaVlyWS~_|L#uH4;EUawo94Ak4LAcMHJ$Ih(!o^Dhg2E!A zP%&`{DQOv5Ir*y!HxzFw-BMOjRa3vCsim!>tEX>p&(P?;shNeP70lYk&fdY%$=Su# z&D+P<58)q>MtIEyd`q+2Hx#tJ2eF@tL5u_A_KOxNj{(;I@D(@|OmcAh={=KSM(`nB z#PmeOuVcvM0Q)5D7ij?V7o)&{gBLLo6*1AY>+%K>(J?car2uCsA@Gozkd|wRKnpr%a|WZd+omqn>`d3v%+Jr~ zKf`ds*wca8N>*HNYMSqd!G{5Q1}mueysO)j-hP^?Y2o~KNk`qOX>SRLSM`nvLfs9A z6IKhKQQOX#GV}GQ(8w;u=1xl?)I~Gsmw6B!tCC{$vu;U48*JF}S+_L23H?s)(3;f9 z#tM3E%b`-LZNocl=6g2y{M@bHS*=Q`78ghVDJbeGd0D@a}eJ{WV|InrvPna5}c&;-_NcMtI!U(M-lf`}uS|a#oW$ZuQ-5c#p=!QJ0CG7az-40&G2gqwE2i)1;Uz)i+OW!g| z*ps=ir?9nCo6Y+|o-cra$Gse;c(Xv58l`AiKdw7Oh^nQ3GS5^{Mqmqo@S%D29SFRm z6chM>!jUCN4+K-IcVh`pKS#efn*R*HL$OYQU(ylDQe3A%T2J{02JVkV%3u9GAhBy_ zFL*n62mj{vepc79QOKK|ZTPh)xPK#k96w?2*@HK=yzeXU;fMVByJgG-?tB#_B==9k zyeH{JFwTj%qqvRfyWmL!!1MW|hbn$S9jqUmq zT)kk{Kb{w)=g4K;8!#QB=_%J0=R4Jz9#8vN*^w?`iILv5^uxQ)EdlO6Qd?YZsQmoK z-nCQw*|lm;UtDHr4NoT(`7P`T)6OH&k zy2gJOIzX5!6|psD9x!S)^cGT`BNul&=QNJuuEbd?kJP~xGKw`hug_hmE!4_136KelR;JGCf>h!IlZO%1*2`hJ^qIuexlW&nVYKRAV zig`ZAA$_W=g`9pNY5HpmxyL2`82h+|tb%BJis`q`PFBGse(nf+D{1GAo*twHk0HBk z0HRQXW>FIz-bG+mFMe-QpX5J9s!~l|SI$JUV9yj{!mB^%q~Prm^h!1!iB-=ueUJ>< zn?_7{>JR9m$lH8q^>HVQr^QU$?LMWU17(Y(eE$tn{|QIb-+@|6=(XONoO1V#2jIX@ zOpfR!X8*wln9rZ|vIZ1R3SgopE}7+kYy7wxi_^IAlU8NE#E+qHP4ZQy{X5Nu6>Qet z3wym+0V|nO=EZx3DgdopAX>8^TEli%PQ8SySqHlVkj7!fUZFZ6R2xmm>$?($N3bLR z4Lh=5*lj%>2HuLq9f#OafY_mwNi-Qktzp-DNHI4VANOsNdQ#PdXv|*fe-ndJdtahY z85wa~HOQr?q7th}UKt^*>f%@;5q`Suv^)f&1^2wUCtPjf+nhn(*v33Jl$gNpP|9O= zCMiBSBW*!kLojo)Ct7Mm-Sq)$PC9(O5gh5^Fbgx!MMe{W@t&&{P z#1}O|8#$?ds-omhm$CCupC2i!P%k5<)vqFV+dsOfHJxg_3w7euYR9;|<#yr>&i>pv zX1xsEF&ddS$yB^|y9Kb)l1jJu4;TmlSL&4pbvETXIT68QA?VUH{sBkgtiq5Ng6K3W!-Ut?kEXX&E}j#>4kKR!G5 zjSq+{K<=y6m0E9T>mk2$4y4p_(F>J^_>8>2kIurDamkD;> zekOG_9Dy28%=!2eGJqNMdGirdp4{2cbSnXhPm3==N4v{TH}%-vtm1OBoD<_QLvpOK z`-f$ZTW@mw+hy(KUx#dT?zTBs=XRrBd8(V^wcnNZblk>fQEem*NVPlgnrq?c>%oqZLINkrpg&+(nTqC&F;>IOE0R6W zZw*QFpNTMc8*pd%gVY75m$pj?2%0QhrsG)55rVABX^pfxpRTVjCE4hl#?5AB#jvSZ zyfwf+7%+tywrRJLfAwWm7H=wZN47m)KvD^JH!^=toVhyDM}B8NQ+SZ2VCNE$6p0$ZAP_L_egV@$| z8sQys6El2<4!nXScHklzlp%lbLt}$u@0uwg`oC8i1-E+lGG!T7Sr?-c*b4cFeYt^@ z%4VfgCV2@oUh&D&UmK}Ih=}U>_JM|Gp-QK6lVOEWOA}#a;tptath{xT;5qFyUUOrN z;~8x?>U=4XYc0WC+Xm#?1Tfc%g1HtJY*}=CK` zv@H)lW#1e4q*qthM9xu@(odJ6cg@x+I9_IO$#8%!S_oCk&M{CoIg7J!vvk%NEL^Hy zsym*vp(5Y)Hq+F$9JM?}dR=;GqYlZ$zkoLO_5|uK$dy6}fqyyK3zTR_VH^ZHoL zEYUi({_t_Rvygg;?KK-+3qaWzKdBb(GP(d(${q!Ma5cB|VCVz14vG)2P@w}Lt(e-P z(Gw2tNb(}p_JqN`bH_UQ9O4!MWz!8RoBvuX&%)JgmsY8#q??fZq*et-u%r78J7N$! zA|htBC{W&n9KQYNLF}plP0FD7<191Z!zT4lC7XB~xBaEJ~p|KWc)s&1_ob04}s9M-PI zP*z}{e?NGHrGUkEWv z9B0$YOCDt%K24SG_eT4RIPdUjnslsK!H-3TFtznjRpx90yvr{)(bb4M?>0DX+sAjr;YXAIp8VSK9Knvk@WQRU^C;g|L zar9oh#MivsSS{`89<9a;{^@kG>s}Y#e&!`VAxnQJ8lJT+ow&8u(@HqQnVYpegk$?$ zJjAX%(^&;j>(5T;1YjSn)Q9b>6XX0Ez`xl;(lLRlLP42Cfk83ZnC zZHc-`G(HMu8X79d$y6e3M-J1sxdWw7f5^cKqe8<>!y1yu#cj_6yV?$d^O{hR`dHS4 z7bNoV3qS}gGPmO7<_a#C1@;GqDuGawe#-1o`h7IlK=u2&ssWe1=3HHUn^qq4koAe& z@ttAHdDt4W%&JZ3eT=;v*M{HLvRUXrO5+o7H2lTL6hU5i(b;AF6S~H+O@j!NyPo~* zpSk;}PNNcyRs*Xq7A}zUUhfnLx&zDl7^T;*Q>l5lqIM|P>*!?Zef+G4)c5o$31MvcQJ`E0@CD@Ulo0#ZZeKxjMZ zCXcmJ_gZMJzCS7#o8ndGX>lr~dqEqtL=__0_(xgxAZpo={s9gvq7O6o*$_v*PWtr^rCU#c;=<2s#MRXM4goNE1GW>iD-z?~4q zsSX?Yr0%ll-G^o6rRNJmJ9|3LB|n>NgL=3%?-5O|#wshsEzcaG2J3IsIPAeUyA9O= zYAhdS4K|P(CCc@8n8OHeci%XC7L4pF?BjLTT0ZBeZ1P#>KIVQ;!?Qcgj6;L0c7<^v z@TbJk%W2SEFPqhMx~*KKMqtNlHW*T6vXPeh3hx=p*ym-t+|mssj{5bN11@nn*GJ|u6Lp7Ae5K{pN30p9jCMv+OYleJ29!b0mum^}d%8Re zDz3?pHj8QPU?P6$F&FAEps;a?M5v&DIpT^L{J7H=t^wc*#*%BRW~1)5k$5)eVhBE8 zPlz9&05}T%Vq{3-QbhjUf!KfL#E}y+?C4H`Y=49K?i7$WkWUe3gW}{05Z?TYnlw5@ z-<~lc8)PFLzjk{}lvF64vyPxg4$#rI?mCQv(dL}*Wi^HIgW*r ziLGtW54a_zON;Hmngnk2Mcdrg#^G9mKlnWs#qIKO0Mc73}OF` zA%|UhX`fjRz>rUe4B-I9jEFL+Cc}+tI4ikX!wtshJ`(R^WJ2Q?P(gXV#McvUvnhsH z=@_Afe)#7ElT(n_)CSF&TF-lRv9JhTV6kO`ul6C7xtQN}XXQoML&SX^K37x!fNnn{ zWct3@4_}SwgrfzWsrm3IsXu=95jL8J!^A_f`MU z=e=xtIge`!(cFXN*2yuEKe3#O)ulYMN*Y(+OKYuDkS+vtw=V9GJ|9lSAn>(AhRnk> z9eu4o#8D$MFKozKMbK197KOC@jYg;RV=KjrIo%vgrKSfzxwd(b02i9AC!wnxThV?u z<#@8=JDdKQOUOq-h!X5dAViNXN+5wpCSWh{@xcUKIJkO83O#}F(v<+_1Q`!Efe|n3 zM7Q=Oii4T_h)p4vpJBIB)6(>0(e5jUm+r@+Eqs4djkAecRp~0CeYWF$k5^S`_g>p< zMj-=@j+^~h|^201j^vU`0 zdMi?!*W^x$^Dm{IZg=fzicB=HMLXr&XgIs_ps9{EK+@xST2{=h{?>O1EkuS_%*XZ1G6~^`6R}pX z6%vw072oDgUcjlyApV3Wqt>$;_~~MO4wWTeS{(1g-`^y|inZjT_7p{uNvAdjWwu#- zR-Y~ziv-p==z8k5Dk4WtD?6{V$NHbSr2$K&ANFF_{3Xt>=nq)vOJBa8c3(qMMt4J! z4nu@PoIobZ#bHlM-(MiibZnCg*R=I6O+|y0=ab zFk}9Z8GjrV66NHKnhK0$Y)>mlNF3{Qs>s6q!{~k-%h68Lp^S^BR2hCxMpJGC}L((ryRE{4TdPNj8athQD=9aV1t^hhBdnQaQnsI4| zSfaq2awQzDJrhQ)s-zOsyukYgkRa~)doj*lTXUfe`;xi}DH$I@9T7`K{8DDEuFM|l z-xAAIW_4yGn4(N*{P+r2MzKJB0}WBmjB&-S){&($Uo-FRpP>z@jnUxf{fj|u%>{-1 zw?9PrtD3O~&lXrJGWFgAlD{p|v=SogD!vLT370!QmYt6pySW;CEeD)IkwDh_=%7*v zMM$`VzDNXDDlMorE(rS?8&VeO2_2}BY1zj^79ZqVu^+Z~Sa5*k6zNGyMX&l6l<`-i zu~Oyy`RKW&a;xch3|Ia-ATxVMg={2ofXoEg(FV=LE#>}lT~mE>>>N?5`*Q=H2WEAf z%y%gXqlaNoKz`EV$yVZ$T!N`C&>kTN_*wGN2ITOpf<>MdRGAZppHP1d@`q*6j0&8pFWut~h%UIXT z_P3;o;vlUWPt6*gKa1axI_XJMTtVFOt@xBb7cG3BpB}=eR3Gc&llGL8mbYyb&6zC( zee%NWI%<4H*T2HY$}9@ffr+c_$UTeo7`QF^NjFwsGRYyC;EW8hNHy^L{Anf5q-BQb zc%!IMC$Xt#LtUO=x*V2nb#pZ}GzQ2DH>XQXd2#y12|)5_{#Wwg1rkX9Kh6?IGYrsm z^wEp2{`6O#D5+{J_MZ!rsrLS{tUvri(#ZSlLV|a}qKmyizbB9k@I7)+(W2BD7oU^y z;5LQ@i2;q<-lrT2rPFee~l4_p;NaI^LIxkDhi%XK`puyU$~m^s9kJ^TSC z6W{@-yQ03kmJ`g~%goh^NCEh75^e=IH!BBodn;FNQ9&t@{k@Ce$9P1y71eJPUNa5) E52I2;uK)l5 literal 0 HcmV?d00001 diff --git a/server/helpers/channel_groups b/server/helpers/channel_groups new file mode 100644 index 0000000..c708380 --- /dev/null +++ b/server/helpers/channel_groups @@ -0,0 +1,9 @@ +Channel Admin +0 +permsid=i_channel_create_modify_with_codec_latency_factor_min permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_make_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_name permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_topic permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_description permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_password permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_codec permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_codec_quality permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_codec_latency_factor permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_maxclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_maxfamilyclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_needed_talk_power permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_make_codec_encrypted permvalue=1 permnegated=0 permskip=0|permsid=b_channel_delete_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_delete_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_channel_delete_flag_force permvalue=1 permnegated=0 permskip=0|permsid=i_channel_delete_power permvalue=50 permnegated=0 permskip=0|permsid=i_channel_description_view_power permvalue=50 permnegated=0 permskip=0|permsid=i_icon_id permvalue=100 permnegated=0 permskip=0|permsid=b_group_is_permanent permvalue=1 permnegated=0 permskip=0|permsid=i_group_auto_update_type permvalue=40 permnegated=0 permskip=0|permsid=i_group_needed_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_group_member_add_power permvalue=50 permnegated=0 permskip=0|permsid=i_group_needed_member_add_power permvalue=50 permnegated=0 permskip=0|permsid=i_group_member_remove_power permvalue=50 permnegated=0 permskip=0|permsid=i_group_needed_member_remove_power permvalue=50 permnegated=0 permskip=0|permsid=b_client_use_channel_commander permvalue=1 permnegated=0 permskip=0|permsid=i_client_kick_from_channel_power permvalue=50 permnegated=0 permskip=0|permsid=i_client_needed_kick_from_channel_power permvalue=50 permnegated=0 permskip=0|permsid=i_client_move_power permvalue=50 permnegated=0 permskip=0|permsid=i_client_needed_complain_power permvalue=25 permnegated=0 permskip=0|permsid=i_client_talk_power permvalue=60 permnegated=0 permskip=0|permsid=b_client_set_flag_talker permvalue=1 permnegated=0 permskip=0 +Operator +0 +permsid=i_icon_id permvalue=200 permnegated=0 permskip=0|permsid=b_group_is_permanent permvalue=1 permnegated=0 permskip=0|permsid=i_group_auto_update_type permvalue=35 permnegated=0 permskip=0|permsid=i_group_needed_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_group_member_add_power permvalue=30 permnegated=0 permskip=0|permsid=i_group_needed_member_add_power permvalue=30 permnegated=0 permskip=0|permsid=i_group_member_remove_power permvalue=30 permnegated=0 permskip=0|permsid=i_group_needed_member_remove_power permvalue=30 permnegated=0 permskip=0|permsid=i_client_needed_complain_power permvalue=25 permnegated=0 permskip=0|permsid=i_client_talk_power permvalue=50 permnegated=0 permskip=0|permsid=b_client_set_flag_talker permvalue=1 permnegated=0 permskip=0 +Guest +0 +permsid=i_group_auto_update_type permvalue=10 permnegated=0 permskip=0|permsid=i_group_needed_modify_power permvalue=75 permnegated=0 permskip=0 diff --git a/server/helpers/permgen.cpp b/server/helpers/permgen.cpp new file mode 100644 index 0000000..2032d11 --- /dev/null +++ b/server/helpers/permgen.cpp @@ -0,0 +1,267 @@ +#include +#include +#include +#include + +#include /* required from permission manager */ +#include "log/LogUtils.h" +#include "Definitions.h" +#include "PermissionManager.h" + +using namespace std; +using namespace ts; + +enum GroupType { + GENERAL, + SERVER, + CHANNEL +}; +enum GroupUpdateType { + NONE = 0, + + CHANNEL_GUEST = 10, + CHANNEL_VOICE = 25, + CHANNEL_OPERATOR = 35, + CHANNEL_ADMIN = 40, + + SERVER_GUEST = 15, + SERVER_NORMAL = 30, + SERVER_ADMIN = 45, + + QUERY_GUEST = 20, + QUERY_ADMIN = 50 +}; + +/* +Value 10: The group will be handled like 'Channel Guest' +Value 15: The group will be handled like 'Server Guest' +Value 20: The group will be handled like 'Query Guest' +Value 25: The group will be handled like 'Channel Voice' +Value 30: The group will be handled like 'Server Normal' +Value 35: The group will be handled like 'Channel Operator' +Value 40: The group will be handled like 'Channel Admin' +Value 45: The group will be handled like 'Server Admin' +Value 50: The group will be handled like 'Query Admin' + */ + + +enum Target { + TARGET_QUERY = 0, + TARGET_SERVER = 1, + TARGET_CHANNEL = 2 +}; + +struct Group { + Target target; + string name; + deque permissions; +}; + +map> property_mapping = { + {TARGET_QUERY, { + {"Guest Server Query", "serverinstance_guest_serverquery_group"}, + {"Admin Server Query", "serverinstance_admin_serverquery_group"} + }}, + {TARGET_SERVER, { + {"Server Admin", "serverinstance_template_serveradmin_group"}, + {"Guest", "serverinstance_template_serverdefault_group"} + }}, + {TARGET_CHANNEL, { + {"Channel Admin", "serverinstance_template_channeladmin_group"}, + {"Guest", "serverinstance_template_channeldefault_group"} + }}, +}; + +inline bool read_line(ifstream& in, string& line) { + if(!getline(in, line)) return false; + while(!line.empty()) { + if(line.back() == '\r') line = line.substr(0, line.length() - 1); + else if(line.front() == ' ' || line.front() == '\r' || (unsigned char) line.front() > 0x80) line = line.substr(1); + else break; + } + return true; +} + +#define PRINT_UNMAP(type) \ +do { \ + cout << type << " => {"; \ + auto e = permission::teamspeak::unmap_key(type, permission::teamspeak::GroupType::SERVER); \ + for(auto it = e.begin(); it != e.end(); it++) { \ + cout << *it; \ + if(it + 1 != e.end()) \ + cout << ", "; \ + } \ + cout << "}" << endl; \ +} while(0) + +#define PRINT_MAP(type) \ +do { \ + cout << type << " => {"; \ + auto e = permission::teamspeak::map_key(type, permission::teamspeak::GroupType::SERVER); \ + for(auto it = e.begin(); it != e.end(); it++) { \ + cout << *it; \ + if(it + 1 != e.end()) \ + cout << ", "; \ + } \ + cout << "}" << endl; \ +} while(0) + +static constexpr bool USE_MAPPING = false; +int main(int argc, char** argv) { + PRINT_UNMAP("i_client_music_needed_rename_power"); + PRINT_UNMAP("b_client_music_channel_list"); + PRINT_UNMAP("i_client_music_info"); + + PRINT_UNMAP("i_client_max_clones_ip"); //=> i_client_max_clones_uid + PRINT_UNMAP("i_client_max_clones_hwid"); //=> i_client_max_clones_uid + PRINT_UNMAP("i_client_max_clones_uid"); //=> i_client_max_clones_uid + + PRINT_UNMAP("i_server_group_needed_modify_power"); //=> i_client_max_clones_uid + PRINT_UNMAP("i_displayed_group_needed_modify_power"); //=> i_client_max_clones_uid + + cout << "--------- map ----------" << endl; + PRINT_MAP("i_client_max_clones_uid"); + PRINT_MAP("i_group_needed_modify_power"); + + deque groups; + { + ifstream file("../helpers/server_groups_new"); /* the new file is already mapped! */ + string line; + while (read_line(file, line)) + { + Group group{}; + group.name = line; + read_line(file, line); + group.target = line == "2" ? TARGET_QUERY : TARGET_SERVER; + read_line(file, line); + auto data = "perms " + line; + ts::Command group_parms = ts::Command::parse(pipes::buffer_view(data.data(), data.length())); + + map grantMapping; + for (int index = 0; index < group_parms.bulkCount(); index++) { + auto permission_name = group_parms[index]["permsid"].string(); + auto permissions = USE_MAPPING ? permission::teamspeak::map_key(permission_name, permission::teamspeak::SERVER) : std::deque({permission_name}); + + for(const auto& permission : permissions) { + auto type = permission::resolvePermissionData(permission); + if(type->type == permission::unknown) { + cerr << "Failed to parse type " << permission << " (" << permission_name << ")!" << endl; + continue; + } + if(type->grantName() == permission) { + permission::update::UpdatePermission entry; + for(auto& perm : group.permissions) + if(perm.name == type->name) { + perm.granted = group_parms[index]["permvalue"]; + goto jmp_out_a; + } + entry.name = type->name; + entry.granted = group_parms[index]["permvalue"]; + group.permissions.push_back(entry); + jmp_out_a:; + } else { + permission::update::UpdatePermission entry; + entry.name = permission; + entry.value = group_parms[index]["permvalue"]; + entry.negated = group_parms[index]["permnegated"]; + entry.skipped = group_parms[index]["permskip"]; + group.permissions.push_back(entry); + } + } + } + groups.push_back(group); + } + file.close(); + } + { + ifstream file("../helpers/channel_groups"); + string line; + while (read_line(file, line)) + { + Group group{}; + group.name = line; + read_line(file, line); + group.target = TARGET_CHANNEL; + read_line(file, line); + auto data = "perms " + line; + ts::Command group_parms = ts::Command::parse(pipes::buffer_view(data.data(), data.length())); + + map grantMapping; + for (int index = 0; index < group_parms.bulkCount(); index++) { + auto permission_name = group_parms[index]["permsid"].string(); + auto permissions = permission::teamspeak::map_key(permission_name, permission::teamspeak::CHANNEL); + + for(const auto& permission : permissions) { + auto type = permission::resolvePermissionData(permission); + if(type->type == permission::unknown) { + cerr << "Failed to parse type " << permission << " (" << permission_name << ")!" << endl; + continue; + } + + if(type->grantName() == permission) { + permission::update::UpdatePermission entry; + for(auto& perm : group.permissions) + if(perm.name == type->name) { + perm.granted = group_parms[index]["permvalue"]; + goto jmp_out_b; + } + entry.name = type->name; + entry.granted = group_parms[index]["permvalue"]; + group.permissions.push_back(entry); + jmp_out_b:; + } else { + permission::update::UpdatePermission entry; + entry.name = permission; + entry.value = group_parms[index]["permvalue"]; + entry.negated = group_parms[index]["permnegated"]; + entry.skipped = group_parms[index]["permskip"]; + group.permissions.push_back(entry); + } + } + } + groups.push_back(group); + } + file.close(); + } + + cout << "Got " << groups.size() << " groups" << endl; + ofstream of("permissions.template"); + + of << "# This is a auto generated template file!" << endl; + of << "# DO NOT EDIT IF YOU'RE NOT SURE WHAT YOU'RE DOING!" << endl; + of << "# Syntax:" << endl; + of << "# Every entry starts with a '--start' and ends with a '--end'" << endl; + of << "# Every entry has the following properties:" << endl; + of << "# name [string] -> The name of the entry" << endl; + of << "# target [numeric] -> The type of the entry {QUERY | SERVER | CHANNEL}" << endl; + of << "# property [string] -> The applied property of the entry" << endl; + of << "# permission [[string],[numeric],[numeric],[flag],[flag]] -> A permission applied on the entry ([name],[value],[granted],[skipped],[negated])" << endl; + of << "#" << endl; + + for(const auto& group : groups) { + of << "--start" << endl; + of << "name:" << group.name << endl; + of << "target:" << group.target << endl; + of << "property:" << property_mapping[group.target][group.name] << endl; + for(const auto& perm : group.permissions) { + of << "permission:" << perm.name << "=" << perm.value << "," << perm.granted << "," << perm.skipped << "," << perm.negated << endl; + } + + if(USE_MAPPING) { + for(const auto& perm : group.permissions) { + if(perm.name == "i_group_auto_update_type") { + for(const auto& insert : permission::update::migrate) { + if(insert.type == perm.value) { + of << "permission:" << insert.permission.name << "=" << insert.permission.value << "," << insert.permission.granted << "," << insert.permission.skipped << "," << insert.permission.negated << endl; + cout << "Auto insert permission " << insert.permission.name << " (" << insert.type << ")" << endl; + } + } + break; + } + } + } + of << "--end" << endl; + } + + of.close(); +} \ No newline at end of file diff --git a/server/helpers/server_groups b/server/helpers/server_groups new file mode 100644 index 0000000..162c9f4 --- /dev/null +++ b/server/helpers/server_groups @@ -0,0 +1,15 @@ +Guest Server Query +2 +permsid=b_serverinstance_help_view permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_version_view permvalue=1 permnegated=0 permskip=0|permsid=b_serverquery_login permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_select permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_permanent permvalue=0 permnegated=1 permskip=1|permsid=b_channel_create_semi_permanent permvalue=0 permnegated=1 permskip=1|permsid=b_channel_create_temporary permvalue=0 permnegated=1 permskip=1|permsid=b_channel_join_permanent permvalue=0 permnegated=1 permskip=1|permsid=b_channel_join_semi_permanent permvalue=0 permnegated=1 permskip=1|permsid=b_channel_join_temporary permvalue=0 permnegated=1 permskip=1|permsid=i_group_auto_update_type permvalue=20 permnegated=0 permskip=0|permsid=i_group_needed_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_needed_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_private_textmessage_power permvalue=-1 permnegated=1 permskip=1|permsid=b_client_server_textmessage_send permvalue=0 permnegated=1 permskip=1|permsid=b_client_channel_textmessage_send permvalue=0 permnegated=1 permskip=1|permsid=b_client_offline_textmessage_send permvalue=0 permnegated=1 permskip=1|permsid=i_client_poke_power permvalue=-1 permnegated=1 permskip=1 +Admin Server Query +2 +permsid=b_serverinstance_help_view permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_version_view permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_virtualserver_list permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_binding_list permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_permission_list permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_permission_find permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_create permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_delete permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_start_any permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_stop_any permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_change_machine_id permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_change_template permvalue=1 permnegated=0 permskip=0|permsid=b_serverquery_login permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_textmessage_send permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_log_view permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_log_add permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_stop permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_modify_settings permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_modify_querygroup permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_modify_templates permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_select permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_connectioninfo_view permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channel_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channel_search permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_search permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_dblist permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_dbsearch permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_dbinfo permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_permission_find permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_custom_search permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_start permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_stop permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_token_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_token_add permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_token_use permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_token_delete permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_log_view permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_log_add permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_join_ignore_password permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_notify_register permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_notify_unregister permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_snapshot_create permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_snapshot_deploy permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_permission_reset permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_name permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_welcomemessage permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_maxclients permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_reserved_slots permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_password permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_default_servergroup permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_default_channelgroup permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_default_channeladmingroup permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_channel_forced_silence permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_complain permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_antiflood permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_ft_settings permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_ft_quotas permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_hostmessage permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_hostbanner permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_hostbutton permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_port permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_autostart permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_needed_identity_security_level permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_priority_speaker_dimm_modificator permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_log_settings permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_min_client_version permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_icon_id permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_weblist permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_codec_encryption_mode permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_temporary_passwords permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_channel_temp_delete_delay_default permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_nickname permvalue=1 permnegated=0 permskip=0|permsid=i_channel_max_depth permvalue=-1 permnegated=0 permskip=0|permsid=i_channel_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=b_channel_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_child permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_private permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_topic permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_description permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_password permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_speex8 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_speex16 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_speex32 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_celtmono48 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_opusvoice permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_opusmusic permvalue=1 permnegated=0 permskip=0|permsid=i_channel_create_modify_with_codec_maxquality permvalue=10 permnegated=0 permskip=0|permsid=i_channel_create_modify_with_codec_latency_factor_min permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_maxclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_maxfamilyclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_sortorder permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_default permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_needed_talk_power permvalue=1 permnegated=0 permskip=0|permsid=i_channel_create_modify_with_temp_delete_delay permvalue=86400 permnegated=0 permskip=0|permsid=b_channel_modify_parent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_make_default permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_make_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_make_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_make_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_name permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_topic permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_description permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_password permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_codec permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_codec_quality permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_codec_latency_factor permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_maxclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_maxfamilyclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_sortorder permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_needed_talk_power permvalue=1 permnegated=0 permskip=0|permsid=i_channel_modify_power permvalue=100 permnegated=0 permskip=0|permsid=b_channel_modify_temp_delete_delay permvalue=1 permnegated=0 permskip=0|permsid=b_channel_delete_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_delete_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_delete_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_channel_delete_flag_force permvalue=1 permnegated=0 permskip=0|permsid=i_channel_delete_power permvalue=100 permnegated=0 permskip=0|permsid=b_channel_join_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_ignore_password permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_ignore_maxclients permvalue=1 permnegated=0 permskip=0|permsid=i_channel_join_power permvalue=100 permnegated=0 permskip=0|permsid=i_channel_subscribe_power permvalue=100 permnegated=0 permskip=0|permsid=i_channel_description_view_power permvalue=100 permnegated=0 permskip=0|permsid=i_icon_id permvalue=500 permnegated=0 permskip=0|permsid=i_max_icon_filesize permvalue=8192 permnegated=0 permskip=0|permsid=b_icon_manage permvalue=1 permnegated=0 permskip=0|permsid=b_group_is_permanent permvalue=1 permnegated=0 permskip=0|permsid=i_group_auto_update_type permvalue=50 permnegated=0 permskip=0|permsid=i_group_auto_update_max_value permvalue=100 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_permission_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_client_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_permission_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_client_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_permission_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channel_permission_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelclient_permission_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_create permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_create permvalue=1 permnegated=0 permskip=0|permsid=i_group_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_group_needed_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_group_member_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_group_needed_member_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_group_member_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_group_needed_member_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=b_permission_modify_power_ignore permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_delete permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_delete permvalue=1 permnegated=0 permskip=0|permsid=i_client_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_needed_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_max_clones_uid permvalue=100 permnegated=0 permskip=0|permsid=i_client_max_channel_subscriptions permvalue=-1 permnegated=0 permskip=0|permsid=b_client_skip_channelgroup_permissions permvalue=1 permnegated=0 permskip=0|permsid=b_client_ignore_bans permvalue=1 permnegated=0 permskip=0|permsid=b_client_ignore_antiflood permvalue=1 permnegated=0 permskip=0|permsid=b_client_issue_client_query_command permvalue=1 permnegated=0 permskip=0|permsid=b_client_use_reserved_slot permvalue=1 permnegated=0 permskip=0|permsid=b_client_use_channel_commander permvalue=1 permnegated=0 permskip=0|permsid=b_client_request_talker permvalue=1 permnegated=0 permskip=0|permsid=b_client_avatar_delete_other permvalue=1 permnegated=0 permskip=0|permsid=b_client_ignore_sticky permvalue=1 permnegated=0 permskip=0|permsid=b_client_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_client_permissionoverview_view permvalue=1 permnegated=0 permskip=0|permsid=b_client_permissionoverview_own permvalue=1 permnegated=0 permskip=0|permsid=b_client_remoteaddress_view permvalue=1 permnegated=0 permskip=0|permsid=i_client_serverquery_view_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_needed_serverquery_view_power permvalue=100 permnegated=0 permskip=0|permsid=b_client_custom_info_view permvalue=1 permnegated=0 permskip=0|permsid=i_client_kick_from_server_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_needed_kick_from_server_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_kick_from_channel_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_needed_kick_from_channel_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_ban_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_needed_ban_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_move_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_needed_move_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_complain_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_needed_complain_power permvalue=100 permnegated=0 permskip=0|permsid=b_client_complain_list permvalue=1 permnegated=0 permskip=0|permsid=b_client_complain_delete_own permvalue=1 permnegated=0 permskip=0|permsid=b_client_complain_delete permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_list permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_create permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_delete_own permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_delete permvalue=1 permnegated=0 permskip=0|permsid=i_client_ban_max_bantime permvalue=-1 permnegated=0 permskip=0|permsid=i_client_private_textmessage_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_needed_private_textmessage_power permvalue=100 permnegated=0 permskip=0|permsid=b_client_server_textmessage_send permvalue=1 permnegated=0 permskip=0|permsid=b_client_channel_textmessage_send permvalue=1 permnegated=0 permskip=0|permsid=b_client_offline_textmessage_send permvalue=1 permnegated=0 permskip=0|permsid=i_client_poke_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_needed_poke_power permvalue=100 permnegated=0 permskip=0|permsid=b_client_set_flag_talker permvalue=1 permnegated=0 permskip=0|permsid=b_client_modify_description permvalue=1 permnegated=0 permskip=0|permsid=b_client_modify_own_description permvalue=1 permnegated=0 permskip=0|permsid=b_client_modify_dbproperties permvalue=1 permnegated=0 permskip=0|permsid=b_client_delete_dbproperties permvalue=1 permnegated=0 permskip=0|permsid=b_client_create_modify_serverquery_login permvalue=1 permnegated=0 permskip=0|permsid=b_ft_ignore_password permvalue=1 permnegated=0 permskip=0|permsid=b_ft_transfer_list permvalue=1 permnegated=0 permskip=0|permsid=i_ft_file_upload_power permvalue=100 permnegated=0 permskip=0|permsid=i_ft_file_download_power permvalue=100 permnegated=0 permskip=0|permsid=i_ft_file_delete_power permvalue=100 permnegated=0 permskip=0|permsid=i_ft_file_rename_power permvalue=100 permnegated=0 permskip=0|permsid=i_ft_file_browse_power permvalue=100 permnegated=0 permskip=0|permsid=i_ft_directory_create_power permvalue=100 permnegated=0 permskip=0|permsid=i_ft_quota_mb_download_per_client permvalue=-1 permnegated=0 permskip=0|permsid=i_ft_quota_mb_upload_per_client permvalue=-1 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_help_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_version_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_info_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_virtualserver_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_binding_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_permission_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_permission_find permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_create permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_delete permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_start_any permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_stop_any permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_change_machine_id permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_change_template permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverquery_login permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_textmessage_send permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_log_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_log_add permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_stop permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_modify_settings permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_modify_querygroup permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_modify_templates permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_select permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_info_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_connectioninfo_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channel_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channel_search permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_search permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_dblist permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_dbsearch permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_dbinfo permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_permission_find permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_custom_search permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_start permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_stop permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_token_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_token_add permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_token_use permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_token_delete permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_log_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_log_add permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_join_ignore_password permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_notify_register permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_notify_unregister permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_snapshot_create permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_snapshot_deploy permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_permission_reset permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_name permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_welcomemessage permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_maxclients permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_reserved_slots permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_password permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_default_servergroup permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_default_channelgroup permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_default_channeladmingroup permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_channel_forced_silence permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_complain permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_antiflood permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_ft_settings permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_ft_quotas permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_hostmessage permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_hostbanner permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_hostbutton permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_port permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_autostart permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_needed_identity_security_level permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_priority_speaker_dimm_modificator permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_log_settings permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_min_client_version permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_icon_id permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_weblist permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_codec_encryption_mode permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_temporary_passwords permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_temporary_passwords_own permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_channel_temp_delete_delay_default permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_nickname permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_min_depth permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_max_depth permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_group_inheritance_end permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_info_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_child permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_permanent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_semi_permanent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_temporary permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_private permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_topic permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_description permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_password permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_speex8 permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_speex16 permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_speex32 permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_celtmono48 permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_opusvoice permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_opusmusic permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_maxquality permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_latency_factor_min permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_maxclients permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_maxfamilyclients permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_sortorder permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_default permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_needed_talk_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_force_password permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_temp_delete_delay permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_parent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_make_default permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_make_permanent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_make_semi_permanent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_make_temporary permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_name permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_topic permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_description permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_password permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_codec permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_codec_quality permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_codec_latency_factor permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_maxclients permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_maxfamilyclients permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_sortorder permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_needed_talk_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_make_codec_encrypted permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_temp_delete_delay permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_delete_permanent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_delete_semi_permanent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_delete_temporary permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_delete_flag_force permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_delete_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_delete_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_permanent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_semi_permanent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_temporary permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_ignore_password permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_ignore_maxclients permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_join_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_subscribe_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_subscribe_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_description_view_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_description_view_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_icon_id permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_max_icon_filesize permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_icon_manage permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_is_permanent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_auto_update_type permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_auto_update_max_value permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_sort_id permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_show_name_in_tree permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_servergroup_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_servergroup_permission_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_servergroup_client_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelgroup_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelgroup_permission_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelgroup_client_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_permission_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channel_permission_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelclient_permission_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_servergroup_create permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelgroup_create permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_needed_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_member_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_needed_member_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_member_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_needed_member_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_permission_modify_power_ignore permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_servergroup_delete permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelgroup_delete permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_clones_uid permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_idletime permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_avatar_filesize permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_channel_subscriptions permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_is_priority_speaker permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_skip_channelgroup_permissions permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_force_push_to_talk permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ignore_bans permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ignore_antiflood permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_issue_client_query_command permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_use_reserved_slot permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_use_channel_commander permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_request_talker permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_avatar_delete_other permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_is_sticky permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ignore_sticky permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_info_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_permissionoverview_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_permissionoverview_own permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_remoteaddress_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_serverquery_view_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_serverquery_view_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_custom_info_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_kick_from_server_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_kick_from_server_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_kick_from_channel_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_kick_from_channel_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_ban_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_move_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_move_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_complain_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_complain_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_complain_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_complain_delete_own permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_complain_delete permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_create permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_delete_own permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_delete permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_max_bantime permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_private_textmessage_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_private_textmessage_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_server_textmessage_send permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_channel_textmessage_send permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_offline_textmessage_send permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_talk_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_talk_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_poke_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_poke_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_set_flag_talker permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_whisper_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_whisper_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_modify_description permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_modify_own_description permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_modify_dbproperties permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_delete_dbproperties permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_create_modify_serverquery_login permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_ignore_password permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_transfer_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_file_upload_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_file_upload_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_file_download_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_file_download_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_file_delete_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_file_delete_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_file_rename_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_file_rename_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_file_browse_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_file_browse_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_directory_create_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_directory_create_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_quota_mb_download_per_client permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_quota_mb_upload_per_client permvalue=100 permnegated=0 permskip=0 +Server Admin +0 +permsid=b_virtualserver_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_connectioninfo_view permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channel_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channel_search permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_search permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_dblist permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_dbsearch permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_dbinfo permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_permission_find permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_custom_search permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_start permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_stop permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_token_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_token_add permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_token_use permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_token_delete permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_log_view permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_log_add permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_join_ignore_password permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_notify_register permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_notify_unregister permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_name permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_welcomemessage permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_maxclients permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_reserved_slots permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_password permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_default_servergroup permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_default_channelgroup permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_default_channeladmingroup permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_channel_forced_silence permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_complain permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_antiflood permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_ft_settings permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_ft_quotas permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_hostmessage permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_hostbanner permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_hostbutton permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_port permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_autostart permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_needed_identity_security_level permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_priority_speaker_dimm_modificator permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_log_settings permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_min_client_version permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_icon_id permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_weblist permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_codec_encryption_mode permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_temporary_passwords permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_channel_temp_delete_delay_default permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_nickname permvalue=1 permnegated=0 permskip=0|permsid=i_channel_max_depth permvalue=10 permnegated=0 permskip=0|permsid=i_channel_permission_modify_power permvalue=75 permnegated=0 permskip=0|permsid=b_channel_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_child permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_private permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_topic permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_description permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_password permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_speex8 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_speex16 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_speex32 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_celtmono48 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_opusvoice permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_opusmusic permvalue=1 permnegated=0 permskip=0|permsid=i_channel_create_modify_with_codec_maxquality permvalue=10 permnegated=0 permskip=0|permsid=i_channel_create_modify_with_codec_latency_factor_min permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_maxclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_maxfamilyclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_sortorder permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_default permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_needed_talk_power permvalue=1 permnegated=0 permskip=0|permsid=i_channel_create_modify_with_temp_delete_delay permvalue=86400 permnegated=0 permskip=0|permsid=b_channel_modify_parent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_make_default permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_make_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_make_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_make_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_name permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_topic permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_description permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_password permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_codec permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_codec_quality permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_codec_latency_factor permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_maxclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_maxfamilyclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_sortorder permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_needed_talk_power permvalue=1 permnegated=0 permskip=0|permsid=i_channel_modify_power permvalue=75 permnegated=0 permskip=0|permsid=b_channel_modify_make_codec_encrypted permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_temp_delete_delay permvalue=1 permnegated=0 permskip=0|permsid=b_channel_delete_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_delete_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_delete_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_channel_delete_flag_force permvalue=1 permnegated=0 permskip=0|permsid=i_channel_delete_power permvalue=75 permnegated=0 permskip=0|permsid=b_channel_join_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_ignore_password permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_ignore_maxclients permvalue=1 permnegated=0 permskip=0|permsid=i_channel_join_power permvalue=75 permnegated=0 permskip=0|permsid=i_channel_subscribe_power permvalue=75 permnegated=0 permskip=0|permsid=i_channel_description_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_icon_id permvalue=300 permnegated=0 permskip=0|permsid=i_max_icon_filesize permvalue=8192 permnegated=0 permskip=0|permsid=b_icon_manage permvalue=1 permnegated=0 permskip=0|permsid=b_group_is_permanent permvalue=1 permnegated=0 permskip=0|permsid=i_group_auto_update_type permvalue=45 permnegated=0 permskip=0|permsid=i_group_auto_update_max_value permvalue=45 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_permission_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_client_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_permission_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_client_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_permission_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channel_permission_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelclient_permission_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_create permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_create permvalue=1 permnegated=0 permskip=0|permsid=i_group_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_group_needed_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_group_member_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_group_needed_member_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_group_member_remove_power permvalue=75 permnegated=0 permskip=0|permsid=i_group_needed_member_remove_power permvalue=75 permnegated=0 permskip=0|permsid=i_permission_modify_power permvalue=75 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_delete permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_delete permvalue=1 permnegated=0 permskip=0|permsid=i_client_permission_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_needed_permission_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_max_clones_uid permvalue=10 permnegated=0 permskip=0|permsid=i_client_max_avatar_filesize permvalue=200000 permnegated=0 permskip=0|permsid=i_client_max_channel_subscriptions permvalue=-1 permnegated=0 permskip=0|permsid=b_client_skip_channelgroup_permissions permvalue=1 permnegated=0 permskip=0|permsid=b_client_ignore_bans permvalue=1 permnegated=0 permskip=0|permsid=b_client_ignore_antiflood permvalue=1 permnegated=0 permskip=0|permsid=b_client_issue_client_query_command permvalue=1 permnegated=0 permskip=0|permsid=b_client_use_reserved_slot permvalue=1 permnegated=0 permskip=0|permsid=b_client_use_channel_commander permvalue=1 permnegated=0 permskip=0|permsid=b_client_request_talker permvalue=1 permnegated=0 permskip=0|permsid=b_client_avatar_delete_other permvalue=1 permnegated=0 permskip=0|permsid=b_client_ignore_sticky permvalue=1 permnegated=0 permskip=0|permsid=b_client_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_client_permissionoverview_view permvalue=1 permnegated=0 permskip=0|permsid=b_client_permissionoverview_own permvalue=1 permnegated=0 permskip=0|permsid=b_client_remoteaddress_view permvalue=1 permnegated=0 permskip=0|permsid=i_client_serverquery_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_needed_serverquery_view_power permvalue=75 permnegated=0 permskip=0|permsid=b_client_custom_info_view permvalue=1 permnegated=0 permskip=0|permsid=i_client_kick_from_server_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_needed_kick_from_server_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_kick_from_channel_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_needed_kick_from_channel_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_ban_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_needed_ban_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_move_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_needed_move_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_complain_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_needed_complain_power permvalue=75 permnegated=0 permskip=0|permsid=b_client_complain_list permvalue=1 permnegated=0 permskip=0|permsid=b_client_complain_delete_own permvalue=1 permnegated=0 permskip=0|permsid=b_client_complain_delete permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_list permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_create permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_delete_own permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_delete permvalue=1 permnegated=0 permskip=0|permsid=i_client_ban_max_bantime permvalue=-1 permnegated=0 permskip=0|permsid=i_client_private_textmessage_power permvalue=75 permnegated=0 permskip=0|permsid=b_client_server_textmessage_send permvalue=1 permnegated=0 permskip=0|permsid=b_client_channel_textmessage_send permvalue=1 permnegated=0 permskip=0|permsid=b_client_offline_textmessage_send permvalue=1 permnegated=0 permskip=0|permsid=i_client_talk_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_poke_power permvalue=75 permnegated=0 permskip=0|permsid=b_client_set_flag_talker permvalue=1 permnegated=0 permskip=0|permsid=b_client_modify_description permvalue=1 permnegated=0 permskip=0|permsid=b_client_modify_own_description permvalue=1 permnegated=0 permskip=0|permsid=b_client_modify_dbproperties permvalue=1 permnegated=0 permskip=0|permsid=b_client_delete_dbproperties permvalue=1 permnegated=0 permskip=0|permsid=b_client_create_modify_serverquery_login permvalue=1 permnegated=0 permskip=0|permsid=b_ft_ignore_password permvalue=1 permnegated=0 permskip=0|permsid=b_ft_transfer_list permvalue=1 permnegated=0 permskip=0|permsid=i_ft_file_upload_power permvalue=75 permnegated=0 permskip=0|permsid=i_ft_file_download_power permvalue=75 permnegated=0 permskip=0|permsid=i_ft_file_delete_power permvalue=75 permnegated=0 permskip=0|permsid=i_ft_file_rename_power permvalue=75 permnegated=0 permskip=0|permsid=i_ft_file_browse_power permvalue=75 permnegated=0 permskip=0|permsid=i_ft_directory_create_power permvalue=75 permnegated=0 permskip=0|permsid=i_ft_quota_mb_download_per_client permvalue=-1 permnegated=0 permskip=0|permsid=i_ft_quota_mb_upload_per_client permvalue=-1 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_info_view permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_connectioninfo_view permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channel_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channel_search permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_search permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_dblist permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_dbsearch permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_dbinfo permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_permission_find permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_custom_search permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_start permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_stop permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_token_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_token_add permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_token_use permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_token_delete permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_log_view permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_log_add permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_join_ignore_password permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_notify_register permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_notify_unregister permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_name permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_welcomemessage permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_maxclients permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_reserved_slots permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_password permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_default_servergroup permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_default_channelgroup permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_default_channeladmingroup permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_channel_forced_silence permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_complain permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_antiflood permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_ft_settings permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_ft_quotas permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_hostmessage permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_hostbanner permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_hostbutton permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_port permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_autostart permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_needed_identity_security_level permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_priority_speaker_dimm_modificator permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_log_settings permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_min_client_version permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_icon_id permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_weblist permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_codec_encryption_mode permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_temporary_passwords permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_temporary_passwords_own permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_channel_temp_delete_delay_default permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_nickname permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_min_depth permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_max_depth permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_group_inheritance_end permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_permission_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_permission_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_info_view permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_child permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_permanent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_semi_permanent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_temporary permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_private permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_topic permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_description permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_password permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_speex8 permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_speex16 permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_speex32 permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_celtmono48 permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_opusvoice permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_opusmusic permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_maxquality permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_latency_factor_min permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_maxclients permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_maxfamilyclients permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_sortorder permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_default permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_needed_talk_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_force_password permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_temp_delete_delay permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_parent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_make_default permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_make_permanent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_make_semi_permanent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_make_temporary permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_name permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_topic permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_description permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_password permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_codec permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_codec_quality permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_codec_latency_factor permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_maxclients permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_maxfamilyclients permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_sortorder permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_needed_talk_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_make_codec_encrypted permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_temp_delete_delay permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_delete_permanent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_delete_semi_permanent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_delete_temporary permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_delete_flag_force permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_delete_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_delete_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_permanent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_semi_permanent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_temporary permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_ignore_password permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_ignore_maxclients permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_join_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_subscribe_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_subscribe_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_description_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_description_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_icon_id permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_max_icon_filesize permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_icon_manage permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_is_permanent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_auto_update_type permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_sort_id permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_show_name_in_tree permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_servergroup_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_servergroup_permission_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_servergroup_client_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelgroup_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelgroup_permission_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelgroup_client_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_permission_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channel_permission_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelclient_permission_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_servergroup_create permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelgroup_create permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_needed_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_member_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_needed_member_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_member_remove_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_needed_member_remove_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_permission_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_servergroup_delete permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelgroup_delete permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_permission_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_permission_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_clones_uid permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_idletime permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_avatar_filesize permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_channel_subscriptions permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_is_priority_speaker permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_skip_channelgroup_permissions permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_force_push_to_talk permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ignore_bans permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ignore_antiflood permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_issue_client_query_command permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_use_reserved_slot permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_use_channel_commander permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_request_talker permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_avatar_delete_other permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_is_sticky permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ignore_sticky permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_info_view permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_permissionoverview_view permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_permissionoverview_own permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_remoteaddress_view permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_serverquery_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_serverquery_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_custom_info_view permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_kick_from_server_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_kick_from_server_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_kick_from_channel_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_kick_from_channel_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_ban_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_move_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_move_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_complain_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_complain_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_complain_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_complain_delete_own permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_complain_delete permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_create permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_delete_own permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_delete permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_max_bantime permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_private_textmessage_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_private_textmessage_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_server_textmessage_send permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_channel_textmessage_send permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_offline_textmessage_send permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_talk_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_talk_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_poke_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_poke_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_set_flag_talker permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_whisper_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_whisper_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_modify_description permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_modify_own_description permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_modify_dbproperties permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_delete_dbproperties permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_create_modify_serverquery_login permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_ignore_password permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_transfer_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_file_upload_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_file_upload_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_file_download_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_file_download_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_file_delete_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_file_delete_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_file_rename_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_file_rename_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_file_browse_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_file_browse_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_directory_create_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_directory_create_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_quota_mb_download_per_client permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_quota_mb_upload_per_client permvalue=75 permnegated=0 permskip=0 +Normal +0 +permsid=b_virtualserver_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_connectioninfo_view permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channel_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_token_use permvalue=1 permnegated=0 permskip=0|permsid=i_channel_max_depth permvalue=5 permnegated=0 permskip=0|permsid=b_channel_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_child permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_private permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_topic permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_description permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_password permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_speex8 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_speex16 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_speex32 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_opusvoice permvalue=1 permnegated=0 permskip=0|permsid=i_channel_create_modify_with_codec_maxquality permvalue=7 permnegated=0 permskip=0|permsid=i_channel_create_modify_with_codec_latency_factor_min permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_maxclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_maxfamilyclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_needed_talk_power permvalue=1 permnegated=0 permskip=0|permsid=i_channel_modify_power permvalue=50 permnegated=0 permskip=0|permsid=b_channel_join_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_temporary permvalue=1 permnegated=0 permskip=0|permsid=i_channel_join_power permvalue=50 permnegated=0 permskip=0|permsid=i_channel_subscribe_power permvalue=50 permnegated=0 permskip=0|permsid=i_channel_description_view_power permvalue=50 permnegated=0 permskip=0|permsid=b_group_is_permanent permvalue=1 permnegated=0 permskip=0|permsid=i_group_auto_update_type permvalue=30 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_client_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_client_list permvalue=1 permnegated=0 permskip=0|permsid=i_group_needed_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_group_needed_member_add_power permvalue=60 permnegated=0 permskip=0|permsid=i_group_needed_member_remove_power permvalue=60 permnegated=0 permskip=0|permsid=i_client_max_clones_uid permvalue=2 permnegated=0 permskip=0|permsid=i_client_max_avatar_filesize permvalue=200000 permnegated=0 permskip=0|permsid=i_client_max_channel_subscriptions permvalue=-1 permnegated=0 permskip=0|permsid=b_client_request_talker permvalue=1 permnegated=0 permskip=0|permsid=b_client_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_client_permissionoverview_view permvalue=1 permnegated=0 permskip=0|permsid=b_client_permissionoverview_own permvalue=1 permnegated=0 permskip=0|permsid=i_client_needed_serverquery_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_needed_kick_from_server_power permvalue=50 permnegated=0 permskip=0|permsid=i_client_needed_kick_from_channel_power permvalue=50 permnegated=0 permskip=0|permsid=i_client_needed_ban_power permvalue=50 permnegated=0 permskip=0|permsid=i_client_needed_move_power permvalue=50 permnegated=0 permskip=0|permsid=i_client_complain_power permvalue=50 permnegated=0 permskip=0|permsid=i_client_needed_complain_power permvalue=50 permnegated=0 permskip=0|permsid=i_client_private_textmessage_power permvalue=50 permnegated=0 permskip=0|permsid=b_client_channel_textmessage_send permvalue=1 permnegated=0 permskip=0|permsid=b_client_offline_textmessage_send permvalue=1 permnegated=0 permskip=0|permsid=i_client_poke_power permvalue=50 permnegated=0 permskip=0|permsid=i_ft_file_upload_power permvalue=50 permnegated=0 permskip=0|permsid=i_ft_file_download_power permvalue=50 permnegated=0 permskip=0|permsid=i_ft_file_delete_power permvalue=50 permnegated=0 permskip=0|permsid=i_ft_file_rename_power permvalue=50 permnegated=0 permskip=0|permsid=i_ft_file_browse_power permvalue=50 permnegated=0 permskip=0|permsid=i_ft_directory_create_power permvalue=50 permnegated=0 permskip=0|permsid=i_ft_quota_mb_download_per_client permvalue=-1 permnegated=0 permskip=0|permsid=i_ft_quota_mb_upload_per_client permvalue=-1 permnegated=0 permskip=0 +Guest +0 +permsid=b_virtualserver_token_use permvalue=1 permnegated=0 permskip=0|permsid=i_channel_max_depth permvalue=0 permnegated=0 permskip=0|permsid=b_channel_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_topic permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_password permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_speex8 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_speex16 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_opusvoice permvalue=1 permnegated=0 permskip=0|permsid=i_channel_create_modify_with_codec_maxquality permvalue=7 permnegated=0 permskip=0|permsid=i_channel_create_modify_with_codec_latency_factor_min permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_maxclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_needed_talk_power permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_temporary permvalue=1 permnegated=0 permskip=0|permsid=i_group_auto_update_type permvalue=15 permnegated=0 permskip=0|permsid=i_group_needed_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_max_clones_uid permvalue=0 permnegated=0 permskip=0|permsid=i_client_max_avatar_filesize permvalue=200000 permnegated=0 permskip=0|permsid=i_client_max_channel_subscriptions permvalue=-1 permnegated=0 permskip=0|permsid=b_client_request_talker permvalue=1 permnegated=0 permskip=0|permsid=b_client_info_view permvalue=1 permnegated=0 permskip=0|permsid=i_client_needed_serverquery_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_needed_kick_from_server_power permvalue=25 permnegated=0 permskip=0|permsid=i_client_needed_kick_from_channel_power permvalue=25 permnegated=0 permskip=0|permsid=i_client_needed_ban_power permvalue=25 permnegated=0 permskip=0|permsid=i_client_needed_move_power permvalue=25 permnegated=0 permskip=0|permsid=b_client_channel_textmessage_send permvalue=1 permnegated=0 permskip=0|permsid=i_ft_file_download_power permvalue=25 permnegated=0 permskip=0|permsid=i_ft_file_browse_power permvalue=25 permnegated=0 permskip=0|permsid=i_ft_quota_mb_download_per_client permvalue=-1 permnegated=0 permskip=0|permsid=i_ft_quota_mb_upload_per_client permvalue=-1 permnegated=0 permskip=0 diff --git a/server/helpers/server_groups_new b/server/helpers/server_groups_new new file mode 100644 index 0000000..96a2673 --- /dev/null +++ b/server/helpers/server_groups_new @@ -0,0 +1,15 @@ +Guest Server Query +2 +sgid=1 permsid=b_serverinstance_help_view permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_version_view permvalue=1 permnegated=0 permskip=0|permsid=b_serverquery_login permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_select permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_permanent permvalue=0 permnegated=0 permskip=0|permsid=b_channel_create_semi_permanent permvalue=0 permnegated=0 permskip=0|permsid=b_channel_create_temporary permvalue=0 permnegated=0 permskip=0|permsid=b_channel_join_permanent permvalue=0 permnegated=0 permskip=0|permsid=b_channel_join_semi_permanent permvalue=0 permnegated=0 permskip=0|permsid=b_channel_join_temporary permvalue=0 permnegated=0 permskip=0|permsid=i_group_auto_update_type permvalue=20 permnegated=0 permskip=0|permsid=i_server_group_needed_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_channel_group_needed_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_group_needed_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_needed_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_private_textmessage_power permvalue=-1 permnegated=0 permskip=0|permsid=b_client_server_textmessage_send permvalue=0 permnegated=0 permskip=0|permsid=b_client_channel_textmessage_send permvalue=0 permnegated=0 permskip=0|permsid=b_client_offline_textmessage_send permvalue=0 permnegated=0 permskip=0|permsid=i_client_poke_power permvalue=-1 permnegated=0 permskip=0 +Admin Server Query +2 +sgid=2 permsid=b_serverinstance_help_view permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_version_view permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_virtualserver_list permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_binding_list permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_permission_list permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_permission_find permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_create permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_delete permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_start_any permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_stop_any permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_change_machine_id permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_change_template permvalue=1 permnegated=0 permskip=0|permsid=b_serverquery_login permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_textmessage_send permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_log_view permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_log_add permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_stop permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_modify_settings permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_modify_querygroup permvalue=1 permnegated=0 permskip=0|permsid=b_serverinstance_modify_templates permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_select permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_select_godmode permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_connectioninfo_view permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channel_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channel_search permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_search permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_dblist permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_dbsearch permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_dbinfo permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_permission_find permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_custom_search permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_start permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_stop permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_token_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_token_add permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_token_use permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_token_delete permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_log_view permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_log_add permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_join_ignore_password permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_notify_register permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_notify_unregister permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_snapshot_create permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_snapshot_deploy permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_permission_reset permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_name permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_welcomemessage permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_maxclients permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_reserved_slots permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_password permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_default_servergroup permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_default_musicgroup permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_default_channelgroup permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_default_channeladmingroup permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_channel_forced_silence permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_complain permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_antiflood permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_ft_settings permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_ft_quotas permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_hostmessage permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_hostbanner permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_hostbutton permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_port permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_host permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_default_messages permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_autostart permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_needed_identity_security_level permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_priority_speaker_dimm_modificator permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_log_settings permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_min_client_version permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_icon_id permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_weblist permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_codec_encryption_mode permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_temporary_passwords permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_temporary_passwords_own permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_channel_temp_delete_delay_default permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_music_bot_limit permvalue=1 permnegated=0 permskip=0|permsid=i_channel_max_depth permvalue=-1 permnegated=0 permskip=0|permsid=i_channel_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=b_channel_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_child permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_private permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_topic permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_description permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_password permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_speex8 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_speex16 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_speex32 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_celtmono48 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_opusvoice permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_opusmusic permvalue=1 permnegated=0 permskip=0|permsid=i_channel_create_modify_with_codec_maxquality permvalue=10 permnegated=0 permskip=0|permsid=i_channel_create_modify_with_codec_latency_factor_min permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_maxclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_maxfamilyclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_sortorder permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_default permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_needed_talk_power permvalue=1 permnegated=0 permskip=0|permsid=i_channel_create_modify_with_temp_delete_delay permvalue=86400 permnegated=0 permskip=0|permsid=b_channel_modify_parent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_make_default permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_make_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_make_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_make_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_name permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_topic permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_description permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_password permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_codec permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_codec_quality permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_codec_latency_factor permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_maxclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_maxfamilyclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_sortorder permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_needed_talk_power permvalue=1 permnegated=0 permskip=0|permsid=i_channel_modify_power permvalue=100 permnegated=0 permskip=0|permsid=b_channel_modify_make_codec_encrypted permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_temp_delete_delay permvalue=1 permnegated=0 permskip=0|permsid=b_channel_delete_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_delete_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_delete_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_channel_delete_flag_force permvalue=1 permnegated=0 permskip=0|permsid=i_channel_delete_power permvalue=100 permnegated=0 permskip=0|permsid=b_channel_join_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_ignore_password permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_ignore_maxclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_ignore_view_power permvalue=1 permnegated=0 permskip=0|permsid=i_channel_join_power permvalue=100 permnegated=0 permskip=0|permsid=b_channel_ignore_join_power permvalue=1 permnegated=0 permskip=0|permsid=i_channel_view_power permvalue=100 permnegated=0 permskip=0|permsid=i_channel_subscribe_power permvalue=100 permnegated=0 permskip=0|permsid=i_channel_description_view_power permvalue=100 permnegated=0 permskip=0|permsid=i_icon_id permvalue=500 permnegated=0 permskip=0|permsid=i_max_icon_filesize permvalue=8192 permnegated=0 permskip=0|permsid=b_icon_manage permvalue=1 permnegated=0 permskip=0|permsid=b_group_is_permanent permvalue=1 permnegated=0 permskip=0|permsid=i_group_auto_update_type permvalue=50 permnegated=0 permskip=0|permsid=i_group_auto_update_max_value permvalue=100 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_create permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_permission_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_client_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_create permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_permission_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_client_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_permission_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channel_permission_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelclient_permission_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_playlist_permission_list permvalue=1 permnegated=0 permskip=0|permsid=i_server_group_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_server_group_needed_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_server_group_member_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_server_group_self_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_server_group_needed_member_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_server_group_member_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_server_group_self_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_server_group_needed_member_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_channel_group_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_channel_group_needed_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_channel_group_member_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_channel_group_self_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_channel_group_needed_member_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_channel_group_member_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_channel_group_self_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_channel_group_needed_member_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_group_member_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_group_needed_member_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_group_member_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_group_needed_member_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_group_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_group_needed_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=b_permission_modify_power_ignore permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_delete permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_delete permvalue=1 permnegated=0 permskip=0|permsid=i_client_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_needed_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_max_clones_uid permvalue=100 permnegated=0 permskip=0|permsid=i_client_max_clones_ip permvalue=100 permnegated=0 permskip=0|permsid=i_client_max_clones_hwid permvalue=100 permnegated=0 permskip=0|permsid=i_client_max_channel_subscriptions permvalue=-1 permnegated=0 permskip=0|permsid=b_client_skip_channelgroup_permissions permvalue=1 permnegated=0 permskip=0|permsid=b_client_ignore_bans permvalue=1 permnegated=0 permskip=0|permsid=b_client_ignore_vpn permvalue=1 permnegated=0 permskip=0|permsid=b_client_ignore_antiflood permvalue=1 permnegated=0 permskip=0|permsid=b_client_allow_invalid_packet permvalue=1 permnegated=0 permskip=0|permsid=b_client_allow_invalid_badges permvalue=1 permnegated=0 permskip=0|permsid=b_client_issue_client_query_command permvalue=1 permnegated=0 permskip=0|permsid=b_client_use_reserved_slot permvalue=1 permnegated=0 permskip=0|permsid=b_client_use_channel_commander permvalue=1 permnegated=0 permskip=0|permsid=b_client_request_talker permvalue=1 permnegated=0 permskip=0|permsid=b_client_avatar_delete_other permvalue=1 permnegated=0 permskip=0|permsid=b_client_ignore_sticky permvalue=1 permnegated=0 permskip=0|permsid=b_client_music_create_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_client_music_create_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_client_music_create_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_client_music_modify_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_client_music_modify_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_client_music_modify_temporary permvalue=1 permnegated=0 permskip=0|permsid=i_client_music_limit permvalue=100 permnegated=0 permskip=0|permsid=i_client_music_delete_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_music_play_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_music_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_music_rename_power permvalue=100 permnegated=0 permskip=0|permsid=b_playlist_create permvalue=1 permnegated=0 permskip=0|permsid=i_playlist_view_power permvalue=100 permnegated=0 permskip=0|permsid=i_playlist_needed_view_power permvalue=100 permnegated=0 permskip=0|permsid=i_playlist_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_playlist_needed_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_playlist_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_playlist_needed_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_playlist_delete_power permvalue=100 permnegated=0 permskip=0|permsid=i_playlist_needed_delete_power permvalue=100 permnegated=0 permskip=0|permsid=i_playlist_song_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_playlist_song_needed_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_playlist_song_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_playlist_song_needed_remove_power permvalue=100 permnegated=0 permskip=0|permsid=b_client_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_client_permissionoverview_view permvalue=1 permnegated=0 permskip=0|permsid=b_client_permissionoverview_own permvalue=1 permnegated=0 permskip=0|permsid=b_client_remoteaddress_view permvalue=1 permnegated=0 permskip=0|permsid=i_client_serverquery_view_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_needed_serverquery_view_power permvalue=100 permnegated=0 permskip=0|permsid=b_client_custom_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_client_music_channel_list permvalue=1 permnegated=0 permskip=0|permsid=b_client_music_server_list permvalue=1 permnegated=0 permskip=0|permsid=i_client_music_info permvalue=100 permnegated=0 permskip=0|permsid=i_client_kick_from_server_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_needed_kick_from_server_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_kick_from_channel_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_needed_kick_from_channel_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_ban_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_needed_ban_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_move_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_needed_move_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_complain_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_needed_complain_power permvalue=100 permnegated=0 permskip=0|permsid=b_client_complain_list permvalue=1 permnegated=0 permskip=0|permsid=b_client_complain_delete_own permvalue=1 permnegated=0 permskip=0|permsid=b_client_complain_delete permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_list permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_list_global permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_trigger_list permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_create permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_create_global permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_name permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_ip permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_hwid permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_edit permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_edit_global permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_delete_own permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_delete permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_delete_own_global permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_delete_global permvalue=1 permnegated=0 permskip=0|permsid=i_client_ban_max_bantime permvalue=-1 permnegated=0 permskip=0|permsid=i_client_private_textmessage_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_needed_private_textmessage_power permvalue=100 permnegated=0 permskip=0|permsid=b_client_even_textmessage_send permvalue=1 permnegated=0 permskip=0|permsid=b_client_server_textmessage_send permvalue=1 permnegated=0 permskip=0|permsid=b_client_channel_textmessage_send permvalue=1 permnegated=0 permskip=0|permsid=b_client_offline_textmessage_send permvalue=1 permnegated=0 permskip=0|permsid=i_client_talk_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_poke_power permvalue=100 permnegated=0 permskip=0|permsid=i_client_needed_poke_power permvalue=100 permnegated=0 permskip=0|permsid=b_client_set_flag_talker permvalue=1 permnegated=0 permskip=0|permsid=i_client_whisper_power permvalue=100 permnegated=0 permskip=0|permsid=b_client_modify_description permvalue=1 permnegated=0 permskip=0|permsid=b_client_modify_own_description permvalue=1 permnegated=0 permskip=0|permsid=b_client_use_bbcode_any permvalue=1 permnegated=0 permskip=0|permsid=b_client_use_bbcode_url permvalue=1 permnegated=0 permskip=0|permsid=b_client_use_bbcode_image permvalue=1 permnegated=0 permskip=0|permsid=b_client_modify_dbproperties permvalue=1 permnegated=0 permskip=0|permsid=b_client_delete_dbproperties permvalue=1 permnegated=0 permskip=0|permsid=b_client_create_modify_serverquery_login permvalue=1 permnegated=0 permskip=0|permsid=b_client_query_create permvalue=1 permnegated=0 permskip=0|permsid=b_client_query_list permvalue=1 permnegated=0 permskip=0|permsid=b_client_query_list_own permvalue=1 permnegated=0 permskip=0|permsid=b_client_query_rename permvalue=1 permnegated=0 permskip=0|permsid=b_client_query_rename_own permvalue=1 permnegated=0 permskip=0|permsid=b_client_query_change_password permvalue=1 permnegated=0 permskip=0|permsid=b_client_query_change_own_password permvalue=1 permnegated=0 permskip=0|permsid=b_client_query_change_password_global permvalue=1 permnegated=0 permskip=0|permsid=b_client_query_delete permvalue=1 permnegated=0 permskip=0|permsid=b_client_query_delete_own permvalue=1 permnegated=0 permskip=0|permsid=b_ft_ignore_password permvalue=1 permnegated=0 permskip=0|permsid=b_ft_transfer_list permvalue=1 permnegated=0 permskip=0|permsid=i_ft_file_upload_power permvalue=100 permnegated=0 permskip=0|permsid=i_ft_file_download_power permvalue=100 permnegated=0 permskip=0|permsid=i_ft_file_delete_power permvalue=100 permnegated=0 permskip=0|permsid=i_ft_file_rename_power permvalue=100 permnegated=0 permskip=0|permsid=i_ft_file_browse_power permvalue=100 permnegated=0 permskip=0|permsid=i_ft_directory_create_power permvalue=100 permnegated=0 permskip=0|permsid=i_ft_quota_mb_download_per_client permvalue=-1 permnegated=0 permskip=0|permsid=i_ft_quota_mb_upload_per_client permvalue=-1 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_help_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_version_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_info_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_virtualserver_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_binding_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_permission_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_permission_find permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_create permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_delete permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_start_any permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_stop_any permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_change_machine_id permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_change_template permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverquery_login permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_textmessage_send permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_log_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_log_add permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_stop permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_modify_settings permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_modify_querygroup permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_serverinstance_modify_templates permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_select permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_select_godmode permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_info_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_connectioninfo_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channel_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channel_search permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_search permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_dblist permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_dbsearch permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_dbinfo permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_permission_find permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_custom_search permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_start permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_stop permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_token_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_token_add permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_token_use permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_token_delete permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_log_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_log_add permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_join_ignore_password permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_notify_register permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_notify_unregister permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_snapshot_create permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_snapshot_deploy permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_permission_reset permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_name permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_welcomemessage permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_maxclients permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_reserved_slots permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_password permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_default_servergroup permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_default_musicgroup permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_default_channelgroup permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_default_channeladmingroup permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_channel_forced_silence permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_complain permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_antiflood permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_ft_settings permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_ft_quotas permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_hostmessage permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_hostbanner permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_hostbutton permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_port permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_host permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_default_messages permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_autostart permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_needed_identity_security_level permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_priority_speaker_dimm_modificator permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_log_settings permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_min_client_version permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_icon_id permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_weblist permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_codec_encryption_mode permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_temporary_passwords permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_temporary_passwords_own permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_channel_temp_delete_delay_default permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_music_bot_limit permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_min_depth permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_max_depth permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_group_inheritance_end permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_info_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_child permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_permanent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_semi_permanent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_temporary permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_private permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_topic permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_description permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_password permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_speex8 permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_speex16 permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_speex32 permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_celtmono48 permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_opusvoice permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_opusmusic permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_maxquality permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_latency_factor_min permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_maxclients permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_maxfamilyclients permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_sortorder permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_default permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_needed_talk_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_force_password permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_temp_delete_delay permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_parent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_make_default permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_make_permanent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_make_semi_permanent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_make_temporary permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_name permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_topic permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_description permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_password permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_codec permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_codec_quality permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_codec_latency_factor permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_maxclients permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_maxfamilyclients permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_sortorder permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_needed_talk_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_make_codec_encrypted permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_temp_delete_delay permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_delete_permanent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_delete_semi_permanent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_delete_temporary permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_delete_flag_force permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_delete_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_delete_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_permanent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_semi_permanent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_temporary permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_ignore_password permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_ignore_maxclients permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_ignore_view_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_join_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_ignore_join_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_view_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_view_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_subscribe_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_subscribe_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_description_view_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_description_view_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_icon_id permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_max_icon_filesize permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_icon_manage permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_is_permanent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_auto_update_type permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_auto_update_max_value permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_sort_id permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_show_name_in_tree permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_servergroup_create permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_servergroup_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_servergroup_permission_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_servergroup_client_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelgroup_create permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelgroup_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelgroup_permission_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelgroup_client_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_permission_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channel_permission_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelclient_permission_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_playlist_permission_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_server_group_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_server_group_needed_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_server_group_member_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_server_group_self_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_server_group_needed_member_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_server_group_member_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_server_group_self_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_server_group_needed_member_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_group_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_group_needed_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_group_member_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_group_self_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_group_needed_member_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_group_member_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_group_self_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_group_needed_member_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_member_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_needed_member_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_member_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_needed_member_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_needed_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_permission_modify_power_ignore permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_servergroup_delete permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelgroup_delete permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_clones_uid permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_clones_ip permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_clones_hwid permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_idletime permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_avatar_filesize permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_channel_subscriptions permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_channels permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_temporary_channels permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_semi_channels permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_permanent_channels permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_use_priority_speaker permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_skip_channelgroup_permissions permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_force_push_to_talk permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ignore_bans permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ignore_vpn permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ignore_antiflood permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_enforce_valid_hwid permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_allow_invalid_packet permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_allow_invalid_badges permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_issue_client_query_command permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_use_reserved_slot permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_use_channel_commander permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_request_talker permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_avatar_delete_other permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_is_sticky permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ignore_sticky permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_create_permanent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_create_semi_permanent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_create_temporary permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_modify_permanent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_modify_semi_permanent permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_modify_temporary permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_create_modify_max_volume permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_limit permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_needed_delete_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_delete_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_play_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_needed_play_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_needed_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_rename_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_needed_rename_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_create permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_view_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_needed_view_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_needed_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_needed_permission_modify_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_delete_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_needed_delete_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_song_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_song_needed_add_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_song_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_song_needed_remove_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_info_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_permissionoverview_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_permissionoverview_own permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_remoteaddress_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_serverquery_view_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_serverquery_view_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_custom_info_view permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_channel_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_server_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_info permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_needed_info permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_kick_from_server_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_kick_from_server_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_kick_from_channel_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_kick_from_channel_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_ban_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_move_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_move_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_complain_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_complain_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_complain_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_complain_delete_own permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_complain_delete permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_list_global permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_trigger_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_create permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_create_global permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_name permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_ip permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_hwid permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_edit permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_edit_global permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_delete_own permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_delete permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_delete_own_global permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_delete_global permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_max_bantime permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_private_textmessage_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_private_textmessage_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_even_textmessage_send permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_server_textmessage_send permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_channel_textmessage_send permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_offline_textmessage_send permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_talk_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_talk_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_poke_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_poke_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_set_flag_talker permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_whisper_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_whisper_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_modify_description permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_modify_own_description permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_use_bbcode_any permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_use_bbcode_url permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_use_bbcode_image permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_modify_dbproperties permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_delete_dbproperties permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_create_modify_serverquery_login permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_query_create permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_query_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_query_list_own permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_query_rename permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_query_rename_own permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_query_change_password permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_query_change_own_password permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_query_change_password_global permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_query_delete permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_query_delete_own permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_ignore_password permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_transfer_list permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_file_upload_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_file_upload_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_file_download_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_file_download_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_file_delete_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_file_delete_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_file_rename_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_file_rename_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_file_browse_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_file_browse_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_directory_create_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_directory_create_power permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_quota_mb_download_per_client permvalue=100 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_quota_mb_upload_per_client permvalue=100 permnegated=0 permskip=0 +Server Admin +0 +sgid=3 permsid=b_virtualserver_select_godmode permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_connectioninfo_view permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channel_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channel_search permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_search permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_dblist permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_dbsearch permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_dbinfo permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_permission_find permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_custom_search permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_start permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_stop permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_token_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_token_add permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_token_use permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_token_delete permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_log_view permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_log_add permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_join_ignore_password permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_notify_register permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_notify_unregister permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_name permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_welcomemessage permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_maxclients permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_reserved_slots permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_password permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_default_servergroup permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_default_musicgroup permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_default_channelgroup permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_default_channeladmingroup permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_channel_forced_silence permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_complain permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_antiflood permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_ft_settings permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_ft_quotas permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_hostmessage permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_hostbanner permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_hostbutton permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_port permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_host permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_default_messages permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_autostart permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_needed_identity_security_level permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_priority_speaker_dimm_modificator permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_log_settings permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_min_client_version permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_icon_id permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_weblist permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_codec_encryption_mode permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_temporary_passwords permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_temporary_passwords_own permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_channel_temp_delete_delay_default permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_modify_music_bot_limit permvalue=1 permnegated=0 permskip=0|permsid=i_channel_max_depth permvalue=10 permnegated=0 permskip=0|permsid=i_channel_permission_modify_power permvalue=75 permnegated=0 permskip=0|permsid=b_channel_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_child permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_private permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_topic permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_description permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_password permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_speex8 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_speex16 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_speex32 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_celtmono48 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_opusvoice permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_opusmusic permvalue=1 permnegated=0 permskip=0|permsid=i_channel_create_modify_with_codec_maxquality permvalue=10 permnegated=0 permskip=0|permsid=i_channel_create_modify_with_codec_latency_factor_min permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_maxclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_maxfamilyclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_sortorder permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_default permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_needed_talk_power permvalue=1 permnegated=0 permskip=0|permsid=i_channel_create_modify_with_temp_delete_delay permvalue=86400 permnegated=0 permskip=0|permsid=b_channel_modify_parent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_make_default permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_make_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_make_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_make_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_name permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_topic permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_description permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_password permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_codec permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_codec_quality permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_codec_latency_factor permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_maxclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_maxfamilyclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_sortorder permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_needed_talk_power permvalue=1 permnegated=0 permskip=0|permsid=i_channel_modify_power permvalue=75 permnegated=0 permskip=0|permsid=b_channel_modify_make_codec_encrypted permvalue=1 permnegated=0 permskip=0|permsid=b_channel_modify_temp_delete_delay permvalue=1 permnegated=0 permskip=0|permsid=b_channel_delete_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_delete_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_delete_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_channel_delete_flag_force permvalue=1 permnegated=0 permskip=0|permsid=i_channel_delete_power permvalue=75 permnegated=0 permskip=0|permsid=b_channel_join_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_ignore_password permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_ignore_maxclients permvalue=1 permnegated=0 permskip=0|permsid=i_channel_join_power permvalue=75 permnegated=0 permskip=0|permsid=b_channel_ignore_join_power permvalue=1 permnegated=0 permskip=0|permsid=i_channel_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_channel_subscribe_power permvalue=75 permnegated=0 permskip=0|permsid=i_channel_description_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_icon_id permvalue=300 permnegated=0 permskip=0|permsid=i_max_icon_filesize permvalue=8192 permnegated=0 permskip=0|permsid=i_max_playlist_size permvalue=50 permnegated=0 permskip=0|permsid=i_max_playlists permvalue=10 permnegated=0 permskip=0|permsid=b_icon_manage permvalue=1 permnegated=0 permskip=0|permsid=b_group_is_permanent permvalue=1 permnegated=0 permskip=0|permsid=i_group_auto_update_type permvalue=45 permnegated=0 permskip=0|permsid=i_group_auto_update_max_value permvalue=45 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_create permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_permission_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_client_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_create permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_permission_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_client_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_permission_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channel_permission_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelclient_permission_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_playlist_permission_list permvalue=1 permnegated=0 permskip=0|permsid=i_server_group_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_server_group_needed_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_server_group_member_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_server_group_self_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_server_group_needed_member_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_server_group_member_remove_power permvalue=75 permnegated=0 permskip=0|permsid=i_server_group_self_remove_power permvalue=75 permnegated=0 permskip=0|permsid=i_server_group_needed_member_remove_power permvalue=75 permnegated=0 permskip=0|permsid=i_channel_group_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_channel_group_needed_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_channel_group_member_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_channel_group_self_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_channel_group_needed_member_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_channel_group_member_remove_power permvalue=75 permnegated=0 permskip=0|permsid=i_channel_group_self_remove_power permvalue=75 permnegated=0 permskip=0|permsid=i_channel_group_needed_member_remove_power permvalue=75 permnegated=0 permskip=0|permsid=i_group_member_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_group_needed_member_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_group_member_remove_power permvalue=75 permnegated=0 permskip=0|permsid=i_group_needed_member_remove_power permvalue=75 permnegated=0 permskip=0|permsid=i_group_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_group_needed_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_permission_modify_power permvalue=75 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_delete permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_delete permvalue=1 permnegated=0 permskip=0|permsid=i_client_permission_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_needed_permission_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_max_clones_uid permvalue=10 permnegated=0 permskip=0|permsid=i_client_max_clones_ip permvalue=10 permnegated=0 permskip=0|permsid=i_client_max_clones_hwid permvalue=10 permnegated=0 permskip=0|permsid=i_client_max_avatar_filesize permvalue=200000 permnegated=0 permskip=0|permsid=i_client_max_channel_subscriptions permvalue=-1 permnegated=0 permskip=0|permsid=b_client_use_priority_speaker permvalue=1 permnegated=0 permskip=0|permsid=b_client_skip_channelgroup_permissions permvalue=1 permnegated=0 permskip=0|permsid=b_client_ignore_bans permvalue=1 permnegated=0 permskip=0|permsid=b_client_ignore_vpn permvalue=1 permnegated=0 permskip=0|permsid=b_client_ignore_antiflood permvalue=1 permnegated=0 permskip=0|permsid=b_client_allow_invalid_packet permvalue=1 permnegated=0 permskip=0|permsid=b_client_allow_invalid_badges permvalue=1 permnegated=0 permskip=0|permsid=b_client_issue_client_query_command permvalue=1 permnegated=0 permskip=0|permsid=b_client_use_reserved_slot permvalue=1 permnegated=0 permskip=0|permsid=b_client_use_channel_commander permvalue=1 permnegated=0 permskip=0|permsid=b_client_request_talker permvalue=1 permnegated=0 permskip=0|permsid=b_client_avatar_delete_other permvalue=1 permnegated=0 permskip=0|permsid=b_client_ignore_sticky permvalue=1 permnegated=0 permskip=0|permsid=b_client_music_create_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_client_music_create_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_client_music_create_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_client_music_modify_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_client_music_modify_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_client_music_modify_temporary permvalue=1 permnegated=0 permskip=0|permsid=i_client_music_limit permvalue=75 permnegated=0 permskip=0|permsid=i_client_music_delete_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_music_play_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_music_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_music_rename_power permvalue=75 permnegated=0 permskip=0|permsid=b_playlist_create permvalue=1 permnegated=0 permskip=0|permsid=i_playlist_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_playlist_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_playlist_permission_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_playlist_delete_power permvalue=75 permnegated=0 permskip=0|permsid=i_playlist_song_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_playlist_song_remove_power permvalue=75 permnegated=0 permskip=0|permsid=b_client_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_client_permissionoverview_view permvalue=1 permnegated=0 permskip=0|permsid=b_client_permissionoverview_own permvalue=1 permnegated=0 permskip=0|permsid=b_client_remoteaddress_view permvalue=1 permnegated=0 permskip=0|permsid=i_client_serverquery_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_needed_serverquery_view_power permvalue=75 permnegated=0 permskip=0|permsid=b_client_custom_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_client_music_channel_list permvalue=1 permnegated=0 permskip=0|permsid=b_client_music_server_list permvalue=1 permnegated=0 permskip=0|permsid=i_client_music_info permvalue=75 permnegated=0 permskip=0|permsid=i_client_kick_from_server_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_needed_kick_from_server_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_kick_from_channel_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_needed_kick_from_channel_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_ban_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_needed_ban_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_move_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_needed_move_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_complain_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_needed_complain_power permvalue=75 permnegated=0 permskip=0|permsid=b_client_complain_list permvalue=1 permnegated=0 permskip=0|permsid=b_client_complain_delete_own permvalue=1 permnegated=0 permskip=0|permsid=b_client_complain_delete permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_list permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_list_global permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_trigger_list permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_create permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_name permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_ip permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_hwid permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_edit permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_delete_own permvalue=1 permnegated=0 permskip=0|permsid=b_client_ban_delete permvalue=1 permnegated=0 permskip=0|permsid=i_client_ban_max_bantime permvalue=-1 permnegated=0 permskip=0|permsid=i_client_private_textmessage_power permvalue=75 permnegated=0 permskip=0|permsid=b_client_even_textmessage_send permvalue=1 permnegated=0 permskip=0|permsid=b_client_server_textmessage_send permvalue=1 permnegated=0 permskip=0|permsid=b_client_channel_textmessage_send permvalue=1 permnegated=0 permskip=0|permsid=b_client_offline_textmessage_send permvalue=1 permnegated=0 permskip=0|permsid=i_client_talk_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_poke_power permvalue=75 permnegated=0 permskip=0|permsid=b_client_set_flag_talker permvalue=1 permnegated=0 permskip=0|permsid=b_client_modify_description permvalue=1 permnegated=0 permskip=0|permsid=b_client_modify_own_description permvalue=1 permnegated=0 permskip=0|permsid=b_client_use_bbcode_any permvalue=1 permnegated=0 permskip=0|permsid=b_client_use_bbcode_url permvalue=1 permnegated=0 permskip=0|permsid=b_client_use_bbcode_image permvalue=1 permnegated=0 permskip=0|permsid=b_client_modify_dbproperties permvalue=1 permnegated=0 permskip=0|permsid=b_client_delete_dbproperties permvalue=1 permnegated=0 permskip=0|permsid=b_client_create_modify_serverquery_login permvalue=1 permnegated=0 permskip=0|permsid=b_client_query_create permvalue=1 permnegated=0 permskip=0|permsid=b_client_query_list permvalue=1 permnegated=0 permskip=0|permsid=b_client_query_list_own permvalue=1 permnegated=0 permskip=0|permsid=b_client_query_rename permvalue=1 permnegated=0 permskip=0|permsid=b_client_query_rename_own permvalue=1 permnegated=0 permskip=0|permsid=b_client_query_change_password permvalue=1 permnegated=0 permskip=0|permsid=b_client_query_change_own_password permvalue=1 permnegated=0 permskip=0|permsid=b_client_query_delete permvalue=1 permnegated=0 permskip=0|permsid=b_client_query_delete_own permvalue=1 permnegated=0 permskip=0|permsid=b_ft_ignore_password permvalue=1 permnegated=0 permskip=0|permsid=b_ft_transfer_list permvalue=1 permnegated=0 permskip=0|permsid=i_ft_file_upload_power permvalue=75 permnegated=0 permskip=0|permsid=i_ft_file_download_power permvalue=75 permnegated=0 permskip=0|permsid=i_ft_file_delete_power permvalue=75 permnegated=0 permskip=0|permsid=i_ft_file_rename_power permvalue=75 permnegated=0 permskip=0|permsid=i_ft_file_browse_power permvalue=75 permnegated=0 permskip=0|permsid=i_ft_directory_create_power permvalue=75 permnegated=0 permskip=0|permsid=i_ft_quota_mb_download_per_client permvalue=-1 permnegated=0 permskip=0|permsid=i_ft_quota_mb_upload_per_client permvalue=-1 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_select_godmode permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_info_view permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_connectioninfo_view permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channel_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channel_search permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_search permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_dblist permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_dbsearch permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_dbinfo permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_permission_find permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_custom_search permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_start permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_stop permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_token_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_token_add permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_token_use permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_token_delete permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_log_view permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_log_add permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_join_ignore_password permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_notify_register permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_notify_unregister permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_name permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_welcomemessage permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_maxclients permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_reserved_slots permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_password permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_default_servergroup permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_default_musicgroup permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_default_channelgroup permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_default_channeladmingroup permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_channel_forced_silence permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_complain permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_antiflood permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_ft_settings permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_ft_quotas permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_hostmessage permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_hostbanner permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_hostbutton permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_port permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_host permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_default_messages permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_autostart permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_needed_identity_security_level permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_priority_speaker_dimm_modificator permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_log_settings permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_min_client_version permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_icon_id permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_weblist permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_codec_encryption_mode permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_temporary_passwords permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_temporary_passwords_own permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_channel_temp_delete_delay_default permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_modify_music_bot_limit permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_min_depth permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_max_depth permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_group_inheritance_end permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_permission_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_permission_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_info_view permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_child permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_permanent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_semi_permanent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_temporary permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_private permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_topic permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_description permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_password permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_speex8 permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_speex16 permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_speex32 permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_celtmono48 permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_opusvoice permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_opusmusic permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_maxquality permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_codec_latency_factor_min permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_maxclients permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_maxfamilyclients permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_sortorder permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_default permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_with_needed_talk_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_force_password permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_create_modify_with_temp_delete_delay permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_parent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_make_default permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_make_permanent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_make_semi_permanent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_make_temporary permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_name permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_topic permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_description permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_password permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_codec permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_codec_quality permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_codec_latency_factor permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_maxclients permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_maxfamilyclients permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_sortorder permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_needed_talk_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_make_codec_encrypted permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_modify_temp_delete_delay permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_delete_permanent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_delete_semi_permanent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_delete_temporary permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_delete_flag_force permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_delete_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_delete_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_permanent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_semi_permanent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_temporary permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_ignore_password permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_ignore_maxclients permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_ignore_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_join_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_join_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_ignore_join_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_subscribe_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_subscribe_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_description_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_needed_description_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_icon_id permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_max_icon_filesize permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_max_playlist_size permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_max_playlists permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_icon_manage permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_is_permanent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_auto_update_type permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_sort_id permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_show_name_in_tree permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_servergroup_create permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_servergroup_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_servergroup_permission_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_servergroup_client_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelgroup_create permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelgroup_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelgroup_permission_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelgroup_client_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_client_permission_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channel_permission_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelclient_permission_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_playlist_permission_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_server_group_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_server_group_needed_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_server_group_member_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_server_group_self_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_server_group_needed_member_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_server_group_member_remove_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_server_group_self_remove_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_server_group_needed_member_remove_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_group_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_group_needed_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_group_member_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_group_self_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_group_needed_member_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_group_member_remove_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_group_self_remove_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_channel_group_needed_member_remove_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_member_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_needed_member_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_member_remove_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_needed_member_remove_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_group_needed_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_permission_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_servergroup_delete permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_virtualserver_channelgroup_delete permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_permission_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_permission_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_clones_uid permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_clones_ip permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_clones_hwid permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_idletime permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_avatar_filesize permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_channel_subscriptions permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_channels permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_temporary_channels permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_semi_channels permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_max_permanent_channels permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_use_priority_speaker permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_skip_channelgroup_permissions permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_force_push_to_talk permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ignore_bans permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ignore_vpn permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ignore_antiflood permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_enforce_valid_hwid permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_allow_invalid_packet permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_allow_invalid_badges permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_issue_client_query_command permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_use_reserved_slot permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_use_channel_commander permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_request_talker permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_avatar_delete_other permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_is_sticky permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ignore_sticky permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_create_permanent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_create_semi_permanent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_create_temporary permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_modify_permanent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_modify_semi_permanent permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_modify_temporary permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_create_modify_max_volume permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_limit permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_needed_delete_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_delete_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_play_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_needed_play_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_needed_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_rename_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_needed_rename_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_create permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_needed_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_needed_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_permission_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_needed_permission_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_delete_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_needed_delete_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_song_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_song_needed_add_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_song_remove_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_playlist_song_needed_remove_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_info_view permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_permissionoverview_view permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_permissionoverview_own permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_remoteaddress_view permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_serverquery_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_serverquery_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_custom_info_view permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_channel_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_server_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_info permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_music_needed_info permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_kick_from_server_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_kick_from_server_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_kick_from_channel_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_kick_from_channel_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_ban_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_move_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_move_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_complain_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_complain_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_complain_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_complain_delete_own permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_complain_delete permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_list_global permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_trigger_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_create permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_name permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_ip permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_hwid permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_edit permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_delete_own permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_delete permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_ban_max_bantime permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_private_textmessage_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_private_textmessage_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_even_textmessage_send permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_server_textmessage_send permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_channel_textmessage_send permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_offline_textmessage_send permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_talk_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_talk_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_poke_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_poke_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_set_flag_talker permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_whisper_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_needed_whisper_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_modify_description permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_modify_own_description permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_use_bbcode_any permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_use_bbcode_url permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_use_bbcode_image permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_modify_dbproperties permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_delete_dbproperties permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_create_modify_serverquery_login permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_query_create permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_query_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_query_list_own permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_query_rename permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_query_rename_own permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_query_change_password permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_query_change_own_password permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_query_delete permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_client_query_delete_own permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_ignore_password permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_transfer_list permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_file_upload_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_file_upload_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_file_download_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_file_download_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_file_delete_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_file_delete_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_file_rename_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_file_rename_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_file_browse_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_file_browse_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_directory_create_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_needed_directory_create_power permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_quota_mb_download_per_client permvalue=75 permnegated=0 permskip=0|permsid=i_needed_modify_power_ft_quota_mb_upload_per_client permvalue=75 permnegated=0 permskip=0 +Normal +0 +sgid=4 permsid=b_virtualserver_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_connectioninfo_view permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channel_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_client_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_token_use permvalue=1 permnegated=0 permskip=0|permsid=i_channel_max_depth permvalue=5 permnegated=0 permskip=0|permsid=b_channel_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_child permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_private permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_topic permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_description permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_password permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_speex8 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_speex16 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_speex32 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_opusvoice permvalue=1 permnegated=0 permskip=0|permsid=i_channel_create_modify_with_codec_maxquality permvalue=7 permnegated=0 permskip=0|permsid=i_channel_create_modify_with_codec_latency_factor_min permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_maxclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_maxfamilyclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_needed_talk_power permvalue=1 permnegated=0 permskip=0|permsid=i_channel_modify_power permvalue=50 permnegated=0 permskip=0|permsid=b_channel_join_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_temporary permvalue=1 permnegated=0 permskip=0|permsid=i_channel_join_power permvalue=50 permnegated=0 permskip=0|permsid=i_channel_subscribe_power permvalue=50 permnegated=0 permskip=0|permsid=i_channel_description_view_power permvalue=50 permnegated=0 permskip=0|permsid=b_group_is_permanent permvalue=1 permnegated=0 permskip=0|permsid=i_group_auto_update_type permvalue=30 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_servergroup_client_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_list permvalue=1 permnegated=0 permskip=0|permsid=b_virtualserver_channelgroup_client_list permvalue=1 permnegated=0 permskip=0|permsid=i_server_group_needed_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_server_group_needed_member_add_power permvalue=60 permnegated=0 permskip=0|permsid=i_server_group_needed_member_remove_power permvalue=60 permnegated=0 permskip=0|permsid=i_channel_group_needed_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_channel_group_needed_member_add_power permvalue=60 permnegated=0 permskip=0|permsid=i_channel_group_needed_member_remove_power permvalue=60 permnegated=0 permskip=0|permsid=i_group_needed_member_add_power permvalue=60 permnegated=0 permskip=0|permsid=i_group_needed_member_remove_power permvalue=60 permnegated=0 permskip=0|permsid=i_group_needed_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_max_clones_uid permvalue=2 permnegated=0 permskip=0|permsid=i_client_max_clones_ip permvalue=2 permnegated=0 permskip=0|permsid=i_client_max_clones_hwid permvalue=2 permnegated=0 permskip=0|permsid=i_client_max_avatar_filesize permvalue=200000 permnegated=0 permskip=0|permsid=i_client_max_channel_subscriptions permvalue=-1 permnegated=0 permskip=0|permsid=i_client_max_channels permvalue=2 permnegated=0 permskip=0|permsid=b_client_request_talker permvalue=1 permnegated=0 permskip=0|permsid=i_playlist_view_power permvalue=50 permnegated=0 permskip=0|permsid=b_client_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_client_permissionoverview_view permvalue=1 permnegated=0 permskip=0|permsid=b_client_permissionoverview_own permvalue=1 permnegated=0 permskip=0|permsid=i_client_needed_serverquery_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_needed_kick_from_server_power permvalue=50 permnegated=0 permskip=0|permsid=i_client_needed_kick_from_channel_power permvalue=50 permnegated=0 permskip=0|permsid=i_client_needed_ban_power permvalue=50 permnegated=0 permskip=0|permsid=i_client_needed_move_power permvalue=50 permnegated=0 permskip=0|permsid=i_client_complain_power permvalue=50 permnegated=0 permskip=0|permsid=i_client_needed_complain_power permvalue=50 permnegated=0 permskip=0|permsid=i_client_private_textmessage_power permvalue=50 permnegated=0 permskip=0|permsid=b_client_channel_textmessage_send permvalue=1 permnegated=0 permskip=0|permsid=b_client_offline_textmessage_send permvalue=1 permnegated=0 permskip=0|permsid=i_client_poke_power permvalue=50 permnegated=0 permskip=0|permsid=b_client_use_bbcode_url permvalue=1 permnegated=0 permskip=0|permsid=i_ft_file_upload_power permvalue=50 permnegated=0 permskip=0|permsid=i_ft_file_download_power permvalue=50 permnegated=0 permskip=0|permsid=i_ft_file_delete_power permvalue=50 permnegated=0 permskip=0|permsid=i_ft_file_rename_power permvalue=50 permnegated=0 permskip=0|permsid=i_ft_file_browse_power permvalue=50 permnegated=0 permskip=0|permsid=i_ft_directory_create_power permvalue=50 permnegated=0 permskip=0|permsid=i_ft_quota_mb_download_per_client permvalue=-1 permnegated=0 permskip=0|permsid=i_ft_quota_mb_upload_per_client permvalue=-1 permnegated=0 permskip=0 +Guest +0 +sgid=5 permsid=b_virtualserver_token_use permvalue=1 permnegated=0 permskip=0|permsid=i_channel_max_depth permvalue=0 permnegated=0 permskip=0|permsid=b_channel_info_view permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_temporary permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_topic permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_password permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_speex8 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_speex16 permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_modify_with_codec_opusvoice permvalue=1 permnegated=0 permskip=0|permsid=i_channel_create_modify_with_codec_maxquality permvalue=7 permnegated=0 permskip=0|permsid=i_channel_create_modify_with_codec_latency_factor_min permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_maxclients permvalue=1 permnegated=0 permskip=0|permsid=b_channel_create_with_needed_talk_power permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_semi_permanent permvalue=1 permnegated=0 permskip=0|permsid=b_channel_join_temporary permvalue=1 permnegated=0 permskip=0|permsid=i_group_auto_update_type permvalue=15 permnegated=0 permskip=0|permsid=i_server_group_needed_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_channel_group_needed_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_group_needed_modify_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_max_clones_uid permvalue=0 permnegated=0 permskip=0|permsid=i_client_max_clones_ip permvalue=0 permnegated=0 permskip=0|permsid=i_client_max_clones_hwid permvalue=0 permnegated=0 permskip=0|permsid=i_client_max_avatar_filesize permvalue=200000 permnegated=0 permskip=0|permsid=i_client_max_channel_subscriptions permvalue=-1 permnegated=0 permskip=0|permsid=i_client_max_channels permvalue=1 permnegated=0 permskip=0|permsid=b_client_request_talker permvalue=1 permnegated=0 permskip=0|permsid=b_client_info_view permvalue=1 permnegated=0 permskip=0|permsid=i_client_needed_serverquery_view_power permvalue=75 permnegated=0 permskip=0|permsid=i_client_needed_kick_from_server_power permvalue=25 permnegated=0 permskip=0|permsid=i_client_needed_kick_from_channel_power permvalue=25 permnegated=0 permskip=0|permsid=i_client_needed_ban_power permvalue=25 permnegated=0 permskip=0|permsid=i_client_needed_move_power permvalue=25 permnegated=0 permskip=0|permsid=b_client_channel_textmessage_send permvalue=1 permnegated=0 permskip=0|permsid=i_ft_file_download_power permvalue=25 permnegated=0 permskip=0|permsid=i_ft_file_browse_power permvalue=25 permnegated=0 permskip=0|permsid=i_ft_quota_mb_download_per_client permvalue=-1 permnegated=0 permskip=0|permsid=i_ft_quota_mb_upload_per_client permvalue=-1 permnegated=0 permskip=0 diff --git a/server/lock_concept b/server/lock_concept new file mode 100644 index 0000000..f258142 --- /dev/null +++ b/server/lock_concept @@ -0,0 +1,58 @@ +CommandResult handleCommandClientUpdate(Command&); +CommandResult handleCommandClientEdit(Command&); +CommandResult handleCommandClientEdit(Command&, const std::shared_ptr& /* target */); +CommandResult handleCommandClientMove(Command&); +CommandResult handleCommandClientGetVariables(Command&); +CommandResult handleCommandClientKick(Command&); +CommandResult handleCommandClientPoke(Command&); + +CommandResult handleCommandChannelSubscribe(Command&); read lock: server channel tree => client lock write +CommandResult handleCommandChannelSubscribeAll(Command&); read lock: server channel tree => client lock write +CommandResult handleCommandChannelUnsubscribe(Command&); read lock: server channel tree => client lock write +CommandResult handleCommandChannelUnsubscribeAll(Command&); read lock: server channel tree => client lock write +CommandResult handleCommandChannelCreate(Command&); write lock server channel tree => iterate clients lock (write) +CommandResult handleCommandChannelDelete(Command&); write lock server channel tree => iterate clients lock (write) +CommandResult handleCommandChannelEdit(Command&); write lock server channel tree +CommandResult handleCommandChannelGetDescription(Command&); read lock: server channel tree +CommandResult handleCommandChannelMove(Command&); write lock server channel tree + +CommandResult handleCommandChannelAddPerm(Command&); read lock: server channel tree => all clients write lock +CommandResult handleCommandChannelDelPerm(Command&); read lock: server channel tree => all clients write lock + +CommandResult handleCommandChannelGroupDel(Command&); read lock: server channel tree => all clients in the group should not be allowed to switch a channel +CommandResult handleCommandSetClientChannelGroup(Command&); read lock: server channel tree => client should not be allowed to switch channel + +CommandResult handleCommandPluginCmd(Command&); + +CommandResult handleCommandClientMute(Command&); +CommandResult handleCommandClientUnmute(Command&); + +//Original from query but still reachable for all +CommandResult handleCommandClientList(Command&); + +CommandResult handleCommandClientFind(Command&); +CommandResult handleCommandClientInfo(Command&); + +CommandResult handleCommandVerifyChannelPassword(Command&); read lock: server channel tree + +handleCommandChannelFind read lock: server channel tree +handleCommandChannelInfo read lock: server channel tree + +General command handling: client_command_lock + Ensure that only one command at time will be handeled + + +Read access server channel tree: read lock channel_tree_lock +Write access server channel tree: lock channel_tree_lock +Write access client channel tree: read lock channel_tree_lock => lock client channel tree + if we write to the server channel tree no client should have their channel tree updated +Read access client channel tree: no lock required + Note: the server channel tree should not be accessed! + +Move client acts like access server channel tree: write lock channel_tree_lock => for each client write lock their tree + + +TODO: Some kind of perm channel lock +TODO: Fix handleCommandChannelEdit + +Test: Channel hide & show with clients! Multible clients as well! \ No newline at end of file diff --git a/server/main.cpp b/server/main.cpp new file mode 100644 index 0000000..041c08f --- /dev/null +++ b/server/main.cpp @@ -0,0 +1,434 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "src/Configuration.h" +#include "src/TSServer.h" +#include "src/InstanceHandler.h" +#include "src/server/QueryServer.h" +#include "src/server/file/FileServer.h" +#include "src/terminal/CommandHandler.h" +#include "src/client/InternalClient.h" +#include "src/SignalHandler.h" +#include "src/build.h" + +using namespace std; +using namespace std::chrono; + +#define BUILD_CREATE_TABLE(tblName, types) "CREATE TABLE IF NOT EXISTS `" tblName "` (" types ")" + +#define CREATE_TABLE(table, types) \ +result = sql::command(sqlData, BUILD_CREATE_TABLE(table, types)).execute();\ +if(!result){\ + logger::logger(0)->critical("Could not setup sql tables. Command '{}' returns {}", BUILD_CREATE_TABLE(table, types), result.fmtStr());\ + goto stopApp;\ +} + +bool mainThreadActive = true; +bool mainThreadDone = false; + + +ts::server::InstanceHandler* serverInstance = nullptr; + +extern void testTomMath(); + +#ifndef FUCK_CLION + #define DB_NAME_BEG "TeaData" + #define DB_NAME_END ".sqlite" + #define DB_NAME DB_NAME_BEG DB_NAME_END +#else + #define DB_NAME "TeaData.sqlite" +#endif + +#include +#include +#include "src/client/music/internal_provider/channel_replay/ChannelProvider.h" + +class CLIParser{ + public: + CLIParser (int &argc, char **argv){ + for (int i = 1; i < argc; i++) + this->tokens.emplace_back(argv[i]); + } + + std::deque getCmdOptions(const std::string &option) const { + std::deque result; + + auto itr = this->tokens.begin(); + while(true) { + itr = std::find(itr, this->tokens.end(), option); + if (itr != this->tokens.end() && ++itr != this->tokens.end()){ + result.push_back(*itr); + itr++; + } else break; + } + return result; + } + + std::deque getCmdOptionsBegins(const std::string &option) const { + std::deque result; + + for(const auto& token : this->tokens) + if(token.find(option) == 0) + result.push_back(token); + + return result; + } + + const std::string& get_option(const std::string &option) const { + auto itr = std::find(this->tokens.begin(), this->tokens.end(), option); + if (itr != this->tokens.end() && ++itr != this->tokens.end()){ + return *itr; + } + static const std::string empty_string; + return empty_string; + } + + bool cmdOptionExists(const std::string &option) const{ + return std::find(this->tokens.begin(), this->tokens.end(), option) != this->tokens.end(); + } + private: + std::vector tokens; +}; + +/* addr is where the exception identifier is stored + id is the exception identifier. */ +void __raise_exception (void **addr, void *id); + +#define T(address) \ +std::cout << "Testing: " << address << " => "; \ +{\ + sockaddr_storage storage;\ + net::resolve_address(address, storage);\ + std::cout << manager.contains(storage) << std::endl;\ +} + +#define CONFIG_NAME "config.yml" +const char *malloc_conf = ""; //retain:false"; //,dirty_decay_ms:0"; +int main(int argc, char** argv) { +#ifdef HAVE_JEMALLOC + (void*) malloc_conf; +#endif + + CLIParser arguments(argc, argv); + SSL_load_error_strings(); + OpenSSL_add_ssl_algorithms(); + ts::permission::setup_permission_resolve(); + + { + u_char buffer[SHA512_DIGEST_LENGTH]; + SHA512_CTX md{}; + SHA512_Init(&md); + SHA512_Update(&md, "Hello World", 11); + SHA512_Final(buffer,&md); + } + { + auto evthread_use_pthreads_result = evthread_use_pthreads(); + assert(evthread_use_pthreads_result == 0); + } + terminal::install(); + if(!terminal::active()){ cerr << "could not setup terminal!" << endl; return -1; } + assert(ts::property::impl::validateUnique()); + + if(arguments.cmdOptionExists("--help") || arguments.cmdOptionExists("-h")) { + #define HELP_FMT " {} {} | {}" + logMessageFmt(true, LOG_GENERAL, "Available command line parameters:"); + logMessageFmt(true, LOG_GENERAL, HELP_FMT, "-h", "--help", "Shows this page"); + logMessageFmt(true, LOG_GENERAL, HELP_FMT, "-q", "--set_query_password", "Changed the server admin query password"); + logMessageFmt(true, LOG_GENERAL, HELP_FMT, "-P=", "--property:=", "Override a config value manual"); + logMessageFmt(true, LOG_GENERAL, HELP_FMT, "-l", "--property-list", "List all available properties"); + terminal::uninstall(); + return 0; + } + if(arguments.cmdOptionExists("--property-list") || arguments.cmdOptionExists("-l")) { + logMessageFmt(true, LOG_GENERAL, "Available properties:"); + auto properties = ts::config::create_bindings(); + for(const auto& property : properties) { + logMessageFmt(true, LOG_GENERAL, " " + property->key); + for(const auto& entry : property->description) { + if(entry.first.empty()) { + for(const auto& line : entry.second) + logMessageFmt(true, LOG_GENERAL, " " + line); + } else { + logMessageFmt(true, LOG_GENERAL, " " + entry.first + ":"); + for(const auto& line : entry.second) + logMessageFmt(true, LOG_GENERAL, " " + line); + } + } + logMessageFmt(true, LOG_GENERAL, " " + property->value_description()); + } + return 0; + } + + if(!arguments.cmdOptionExists("--valgrind")) { + ts::syssignal::setup(); + } + ts::syssignal::setup_threads(); + + map override_settings; + { + auto short_override = arguments.getCmdOptionsBegins("-P"); + for(const auto& entry : short_override) { + if(entry.length() < 2) continue; + auto ei = entry.find('='); + if(ei == string::npos || ei == 2) { + logErrorFmt(true, LOG_GENERAL, "Invalid command line parameter. (\"" + entry + "\")"); + return 1; + } + + auto key = entry.substr(2, ei - 2); + auto value = entry.substr(ei + 1); + override_settings[key] = value; + } + } + { + auto short_override = arguments.getCmdOptionsBegins("--property:"); + for(const auto& entry : short_override) { + if(entry.length() < 11) continue; + auto ei = entry.find('='); + if(ei == string::npos || ei == 11) { + logErrorFmt(true, LOG_GENERAL, "Invalid command line parameter. (\"" + entry + "\")"); + return 1; + } + + auto key = entry.substr(11, ei - 11); + auto value = entry.substr(ei + 1); + override_settings[key] = value; + } + } + + { + auto bindings = ts::config::create_bindings(); + for(const auto& setting : bindings) { + for(auto it = override_settings.begin(); it != override_settings.end(); it++) { + if(it->first == setting->key) { + try { + setting->read_argument(it->second); + } catch(const std::exception& ex) { + logErrorFmt(true, LOG_GENERAL, "Failed to apply value for given property '" + it->first + "': " + ex.what()); + } + override_settings.erase(it); + break; + } + } + } + for(const auto& entry : override_settings) { + logMessageFmt(true, LOG_GENERAL, "Missing property " + entry.first + ". Value unused!"); + } + } + /* + std::string error; + if(!interaction::waitForAttach(error)){ + cerr << "Rsult: " << error << endl; + } + + while(interaction::memoryInfo()){ + usleep(1 * 1000 * 1000); + logMessage("Current instances: " + to_string(interaction::memoryInfo()->instanceCount) + "/" + to_string(interaction::memoryInfo()->maxInstances)); + } + + interaction::removeMemoryHook(); + if(true) return 0; + */ + + //debugMessage(LOG_GENERAL, "Sizeof ViewEntry {} Sizeof LinkedTreeEntry {} Sizeof shared_ptr {} Sizeof ClientChannelView {}", sizeof(ts::ViewEntry), sizeof(ts::TreeView::LinkedTreeEntry), sizeof(shared_ptr), sizeof(ts::ClientChannelView)); + { + //http://git.mcgalaxy.de/WolverinDEV/tomcrypt/blob/develop/src/misc/crypt/crypt_inits.c#L40-86 + std::string descriptors = "LTGE"; + bool crypt_init = false; + for(const auto& c : descriptors) + if(crypt_init |= crypt_mp_init(&c)) + break; + if(!crypt_init) { + logCritical(LOG_GENERAL, "Could not initialise libtomcrypt mp descriptors!"); + return 1; + } + if(register_prng(&sprng_desc) == -1) { + cerr << "could not setup prng" << endl; + return EXIT_FAILURE; + } + if (register_cipher(&rijndael_desc) == -1) { + cerr << "could not setup rijndael" << endl; + return EXIT_FAILURE; + } + testTomMath(); + } + + ts::server::SqlDataManager* sql = nullptr; + std::string errorMessage; + shared_ptr logConfig = nullptr; + std::string line; + + logMessage("Loading configuration"); + terminal::instance()->writeMessage("Loading configuration"); + auto cfgErrors = ts::config::parseConfig(CONFIG_NAME); + if(!cfgErrors.empty()){ + logError("Could not load configuration. Errors: (" + to_string(cfgErrors.size()) + ")"); + for(const auto& entry : cfgErrors) + logError(" - " + entry); + logError("Stopping server..."); + goto stopApp; + } + + logMessage("Setting up log"); + logConfig = make_shared(); + logConfig->logfileLevel = (spdlog::level::level_enum) ts::config::log::logfileLevel; + logConfig->terminalLevel = (spdlog::level::level_enum) ts::config::log::terminalLevel; + logConfig->file_colored = ts::config::log::logfileColored; + logConfig->logPath = ts::config::log::path; + logConfig->vs_group_size = ts::config::log::vs_size; + logger::setup(logConfig); + threads::timer::function_log = [](const std::string& message, bool debug) { + if(debug) + debugMessage(LOG_GENERAL, message); + else + logWarning(LOG_GENERAL, message); + }; + + logger::updateLogLevels(); + if(ts::config::license_original && ts::config::license_original->data.type != license::LicenseType::DEMO){ + logMessageFmt(true, LOG_GENERAL, "[]---------------------------------------------------------[]"); + logMessageFmt(true, LOG_GENERAL, " §aThank you for buying the TeaSpeak-§lPremium-§aSoftware! "); + logMessageFmt(true, LOG_GENERAL, " §aLicense information:"); + logMessageFmt(true, LOG_GENERAL, " §aLicense owner : §e" + ts::config::license_original->owner()); + logMessageFmt(true, LOG_GENERAL, " §aLicense type : §e" + license::LicenseTypeNames[ts::config::license_original->data.type]); + + if(ts::config::license_original->end().time_since_epoch().count() == 0){ + logMessageFmt(true, LOG_GENERAL, " §aLicense expires: §enever"); + } else { + char timeBuffer[32]; + time_t t = duration_cast(ts::config::license_original->end().time_since_epoch()).count(); + tm* stime = localtime(&t); + strftime(timeBuffer, 32, "%c", stime); + logMessageFmt(true, LOG_GENERAL, " §aLicense expires: §e" + string(timeBuffer)); + } + logMessage(string() + " §aLicense valid : " + (ts::config::license_original->isValid() ? "§ayes" : "§cno")); + logMessageFmt(true, LOG_GENERAL, "[]---------------------------------------------------------[]"); + } + + logMessage(LOG_GENERAL, "Starting TeaSpeak-Server v{}", build::version()->string(true)); + logMessage(LOG_GENERAL, "Starting music providers"); + + terminal::instance()->setPrompt("§aStarting server. §7[§aloading music§7]"); + if(ts::config::music::enabled && !arguments.cmdOptionExists("--valgrind")) { + ::music::manager::loadProviders("providers"); + ::music::manager::register_provider(::music::provider::ChannelProvider::create_provider()); + } + + terminal::instance()->setPrompt("§aStarting server. §7[§aloading geoloc§7]"); + + if(!ts::config::geo::staticFlag) { + if(ts::config::geo::type == geoloc::PROVIDER_SOFTWARE77) + geoloc::provider = new geoloc::Software77Provider(ts::config::geo::mappingFile); + else if(ts::config::geo::type == geoloc::PROVIDER_IP2LOCATION) + geoloc::provider = new geoloc::IP2LocationProvider(ts::config::geo::mappingFile); + else { + logCritical("Invalid geo resolver type!"); + } + if(geoloc::provider && !geoloc::provider->load(errorMessage)) { + logCritical("Could not setup geoloc! Fallback to default flag!"); + logCritical("Message: " + errorMessage); + geoloc::provider = nullptr; + errorMessage = ""; + } + } + if(ts::config::geo::vpn_block) { + geoloc::provider_vpn = new geoloc::IPCatBlocker(ts::config::geo::vpn_file); + + if(geoloc::provider_vpn && !geoloc::provider_vpn->load(errorMessage)) { + logCritical("Could not setup vpn detector!"); + logCritical("Message: " + errorMessage); + geoloc::provider_vpn = nullptr; + errorMessage = ""; + } + } + terminal::instance()->setPrompt("§aStarting server. §7[§aloading sql§7]"); + + sql = new ts::server::SqlDataManager(); + if(!sql->initialize(errorMessage)) { + logCritical("Could not initialize SQL!"); + if(errorMessage.find("database is locked") != string::npos) { + logCriticalFmt(true, LOG_GENERAL, "----------------------------[ ATTENTION ]----------------------------"); + logCriticalFmt(true, LOG_GENERAL, "{:^69}", "You're database is already in use!"); + logCriticalFmt(true, LOG_GENERAL, "{:^69}", "Stop the other instance first!"); + logCriticalFmt(true, LOG_GENERAL, "----------------------------[ ATTENTION ]----------------------------"); + } else { + logCriticalFmt(true, LOG_GENERAL, errorMessage); + } + goto stopApp; + } + + terminal::instance()->setPrompt("§aStarting server. §7[§astarting instance§7]"); + + serverInstance = new ts::server::InstanceHandler(sql); //if error than mainThreadActive = false + if(!mainThreadActive || !serverInstance->startInstance()) + goto stopApp; + + if(arguments.cmdOptionExists("-q") || arguments.cmdOptionExists("--set_query_password")) { + auto password = arguments.cmdOptionExists("-q") ? arguments.get_option("-q") : arguments.get_option("--set_query_password"); + if(!password.empty()) { + logMessageFmt(true, LOG_GENERAL, "Updating server admin query password to \"{}\"", password); + auto accounts = serverInstance->getQueryServer()->find_query_accounts_by_unique_id(serverInstance->getInitalServerAdmin()->getUid()); + bool found = false; + for(const auto& account : accounts) { + if(account->bound_server != 0) continue; + if(!serverInstance->getQueryServer()->change_query_password(account, password)) { + logErrorFmt(true, LOG_GENERAL, "Failed to update server admin query password! (Internal error)"); + } + found = true; + break; + } + if(!found) { + logErrorFmt(true, LOG_GENERAL, "Failed to update server admin query password! Login does not exists!"); + } + } + } + + terminal::instance()->setPrompt("§7> §f"); + while(mainThreadActive) { + usleep(5 * 1000); + + if(terminal::instance()->linesAvailable() > 0){ + while(!(line = terminal::instance()->readLine("§7> §f")).empty()) + threads::Thread(THREAD_DETACHED, [line](){ terminal::chandler::handleCommand(line); }); + } + } + + + stopApp: + logMessage("Stopping application"); + if(serverInstance) + serverInstance->stopInstance(); + delete serverInstance; + serverInstance = nullptr; + + + if(sql) + sql->finalize(); + delete sql; + logMessage("Application suspend successful!"); + + logger::uninstall(); + terminal::uninstall(); + mainThreadDone = true; + return 0; +} + +/* + * 2][OUT] (188.225.34.225:9988) ftinitdownload clientftfid=4096 name=\/icon_166694597 cid=0 cpw seekpos=0 proto=1 return_code= +[02][OUT] (188.225.34.225:9988) ftinitdownload clientftfid=4095 name=\/icon_4113966246 cid=0 cpw seekpos=0 proto=1 return_code= +[02][OUT] (188.225.34.225:9988) ftinitdownload clientftfid=4094 name=\/icon_3002705295 cid=0 cpw seekpos=0 proto=1 return_code= +[02][OUT] (188.225.34.225:9988) ftinitdownload clientftfid=4093 name=\/icon_494035633 cid=0 cpw seekpos=0 proto=1 return_code= +[02][OUT] (188.225.34.225:9988) ftinitdownload clientftfid=4092 name=\/icon_847789427 cid=0 cpw seekpos=0 proto=1 return_code= +[02][ IN] (188.225.34.225:9988) notifyclientupdated clid=5 client_version=3.2.0\s[Build:\s1533739581] client_platform=Linux client_login_name=WolverinDEV client_created=1536521950 client_lastconnected=1536522252 client_totalconnections=2 client_month_bytes_uploaded=0 client_month_bytes_downloaded=0 client_total_bytes_uploaded=0 client_total_bytes_downloaded=0 client_icon_id=0 client_country=DE +[02][ IN] (188.225.34.225:9988) notifystartdownload clientftfid=4096 proto=1 serverftfid=1 ftkey=R0Vcnx4fNdrXuMFg port=30303 size=1086 +[02][ IN] (188.225.34.225:9988) notifystartdownload clientftfid=4095 proto=1 serverftfid=1 ftkey=3eYwsuviQvTWme42 port=30303 size=822 +[02][ IN] (188.225.34.225:9988) notifystartdownload clientftfid=4094 proto=1 serverftfid=1 ftkey=dM5oaVuLYLwia2me port=30303 size=852 +[02][ IN] (188.225.34.225:9988) notifystartdownload clientftfid=4093 proto=1 serverftfid=1 ftkey=60BltUu8fbUqgLhj port=30303 size=3441 +[02][ IN] (188.225.34.225:9988) notifystartdownload clientftfid=4092 proto=1 serverftfid=1 ftkey=a0wmURVHqhNE71H2 port=30303 size=1452 + + */ \ No newline at end of file diff --git a/server/repro/build.sh b/server/repro/build.sh new file mode 100755 index 0000000..eaac7c0 --- /dev/null +++ b/server/repro/build.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +BUILD_PATH=$1 +if [ -z ${BUILD_PATH} ]; then + echo "Missing versions path!" + exit 1 +fi + +./generate_version.sh ${BUILD_PATH} +if [ $? -ne 0 ]; then + echo "Failed to generate version!" + exit 1 +fi + +./package_server.sh ${BUILD_PATH} +if [ $? -ne 0 ]; then + echo "Failed to package server!" + exit 1 +fi + +./deploy_build.sh ${BUILD_PATH} +if [ $? -ne 0 ]; then + echo "Failed to deploy package!" + exit 1 +fi \ No newline at end of file diff --git a/server/repro/build_private_key b/server/repro/build_private_key new file mode 100644 index 0000000..9049969 --- /dev/null +++ b/server/repro/build_private_key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA8Vu+RBxR2fIKke2b3yb731nB+BwkkVUO0cHAw2p/WJGyZOZv +o4y5gQzfTxWbOGLM33BrneaWmmQIz59bStaBhW7MABP+wTpRHUFktITHMZmvz4Xm +GHqQWFoU8mlyndzv9l4tdtApbGE3Yn6jiToKZJX+5EyUTZe6YwBAcuzxyO7YVEmB +87cPCsciir+ZQUS+MqyPY7GSy1jxCBZ80AMIOSIuEZaOBZaDEhvDJbBAugYIaw1P +jp06DmImudXoXi71Wv0xmM7f0KFryeO0Hxq01ds1pes8S5bslCaNRX395R3Q1sdl +R27eskVuXMLDPJSQTwd3WfQ9d1r5/BYTofzmcQIDAQABAoIBAQDqpBdQHfwRFvbh +sY8kncCl/ZvOOoXuaDO1BlkBYeqVz2cQItqLtIaviDUcrFOvuJWV77Qf2Qm25OOP +/UuCcRGQCAv5U3cKoUg0Wduuh5sjhFbgODtetuDXlBPjK1KLWDxNVnd6l5p9y/FN +JvKTuUJbUVtw1WYkHQrNrnP5hpL0lCN098F+aaXPgdXtyJhGGj5dlyFwezC1nTxB +LIjjINg9JlWfmhnsszkLYLKIzj2x+i42k/bQwlXvxiPOUmDELc4Z6NZRpsMy2og3 +qNqMGp895R3tSjaPEgISpXPZJovIJutJO/ci3MeIMRV1vtqv54qeJpNgoQZYyHkE +RsZecRT1AoGBAP6q9bZdN1mAD0+xsYLsFBEXUIWbaLsb3rOtBgaYCQYrWxweD6wo +rbg2J9lJ9k0mZtSy86a3sPB83H3no5oE8HOoofnepE9FVhXQE39jwAg4NwzDh2WZ +feEcVF/8SSvlPwkFZ8SlkJmkVZ21TJC7/w5OiTrVJtiEL64XIq26zZxLAoGBAPKe +9cF2OLfIhk7OJeTNyvt3iO5w+XhGPebQYhyYFrL/1Cpu7esQowAen/MHiB40+luo +wjvhSfPfKG9htAwpXPhZlRzFutQCGJqjlAwhRDt1UamXhY3wt2PrBwxKpa/MoTDK +wDcSVTnBgfCR6GVJd8rfurrEoYWNh7IT26MNaxqzAoGBANt4O98YgF3KRee5TDB3 +AWglPoiWAPDXONqBbyL5nTVK4e7eXUVRnTyWt0rEOdYNFSuSuPXhckQeZuq+WLig +LxSNrGGpJNMH/wM3WcY8Eb9DqvV+AE5ntBrvDivWQLzqiKNenCnVQUOXYw1RtBU7 +XrhWqF8iWKLZcDnIyGMODKn3AoGAfQhqgg7Y3+2ZOZeHc8iSaNGLYcYa6l7Ym9Cc +HzxJxmmM/2k3d0KVngQqTeZ9wYNv2ji4EH/jyqgggHYLgZD3do1ECRXlWEjUQS03 +qKCkNzgYo5uQmjuJZxbCBRWbGWQNVcXHFRp/jUoqGr4206vu7kAqTQH0c40idyVA +tWC+530CgYBjacl9NtsJD7ulcS9gtQ5J+d3SrQuYVjCNyCJP0YuDBGZ8IE04qGJE +1JTmrZFTpFe/Em27N3PETB/Tm3PHgxmATcHXJOS+K63lwkgqY20+5fBCEqHb6hgj +i5n/Z6GgtZQYUusrLSBsFrUH9cYtcmmOPYIgnmr/CaEJQx1AByeTTg== +-----END RSA PRIVATE KEY----- diff --git a/server/repro/build_public_key b/server/repro/build_public_key new file mode 100644 index 0000000..389c4c2 --- /dev/null +++ b/server/repro/build_public_key @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDxW75EHFHZ8gqR7ZvfJvvfWcH4HCSRVQ7RwcDDan9YkbJk5m+jjLmBDN9PFZs4YszfcGud5paaZAjPn1tK1oGFbswAE/7BOlEdQWS0hMcxma/PheYYepBYWhTyaXKd3O/2Xi120ClsYTdifqOJOgpklf7kTJRNl7pjAEBy7PHI7thUSYHztw8KxyKKv5lBRL4yrI9jsZLLWPEIFnzQAwg5Ii4Rlo4FloMSG8MlsEC6BghrDU+OnToOYia51eheLvVa/TGYzt/QoWvJ47QfGrTV2zWl6zxLluyUJo1Fff3lHdDWx2VHbt6yRW5cwsM8lJBPB3dZ9D13Wvn8FhOh/OZx build@teaspeak.de diff --git a/server/repro/deploy_build.sh b/server/repro/deploy_build.sh new file mode 100755 index 0000000..6465050 --- /dev/null +++ b/server/repro/deploy_build.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +BUILD_PATH=$1 +if [[ -z ${BUILD_PATH} ]]; then + echo "Missing versions path!" + #exit 1 +fi + +BUILD_INFO=($(cat build_version.txt)) +BUILD_FULL_NAME=${BUILD_INFO[0]} +BUILD_NAME=${BUILD_INFO[1]} +BUILD_VERSION=${BUILD_INFO[2]} +BUILD_FILENAME=${BUILD_INFO[3]} + +echo "Publishing build ${BUILD_FILENAME}" +if [[ ! -f ${BUILD_FILENAME} ]]; then + echo "Failed to find file!" + exit 1 +fi + +if [[ -d symbols ]]; then + echo "Uploading symbols" + scp -i build_private_key -rpC symbols/ TeaSpeak-Jenkins@mcgalaxy.de:symbols/ + if [[ $? -ne 0 ]]; then + echo "Failed to upload symbols!" + exit 1 + fi + rm -r symbols/ +else + echo "Failed to find symbols! Skipping step!" +fi + +echo "Creating versions mark" +ssh -i build_private_key TeaSpeak-Jenkins@mcgalaxy.de " +if [ ! -d versions/${BUILD_PATH} ]; then + mkdir -p versions/${BUILD_PATH} +fi +if [ ! -d files/${BUILD_PATH} ]; then #Creating for files as well + mkdir -p files/${BUILD_PATH} +fi +echo '' > versions/${BUILD_PATH}/${BUILD_FULL_NAME} +echo '${BUILD_FULL_NAME}' > versions/${BUILD_PATH}/latest" +if [[ $? -ne 0 ]]; then + echo "Failed to create versions mark!" + exit 1 +fi + +echo "Uploading build (${BUILD_FILENAME})" +scp -i build_private_key -pC "${BUILD_FILENAME}" "TeaSpeak-Jenkins@mcgalaxy.de:files/${BUILD_PATH}/" +if [[ $? -ne 0 ]]; then + echo "Failed to upload version!" + exit 1 +fi diff --git a/server/repro/env/TeaSpeakServer b/server/repro/env/TeaSpeakServer new file mode 120000 index 0000000..ce88778 --- /dev/null +++ b/server/repro/env/TeaSpeakServer @@ -0,0 +1 @@ +../../environment/TeaSpeakServer \ No newline at end of file diff --git a/server/repro/env/certs b/server/repro/env/certs new file mode 120000 index 0000000..7a4f740 --- /dev/null +++ b/server/repro/env/certs @@ -0,0 +1 @@ +../../../git-teaspeak/default_files/certs/ \ No newline at end of file diff --git a/server/repro/env/commanddocs b/server/repro/env/commanddocs new file mode 120000 index 0000000..4891fe4 --- /dev/null +++ b/server/repro/env/commanddocs @@ -0,0 +1 @@ +../../../git-teaspeak/default_files/commanddocs/ \ No newline at end of file diff --git a/server/repro/env/geoloc b/server/repro/env/geoloc new file mode 120000 index 0000000..f3e1b9d --- /dev/null +++ b/server/repro/env/geoloc @@ -0,0 +1 @@ +../../../git-teaspeak/default_files/geoloc/ \ No newline at end of file diff --git a/server/repro/env/install_libnice.sh b/server/repro/env/install_libnice.sh new file mode 120000 index 0000000..65420f7 --- /dev/null +++ b/server/repro/env/install_libnice.sh @@ -0,0 +1 @@ +../../../git-teaspeak/default_files/install_libnice.sh \ No newline at end of file diff --git a/server/repro/env/install_music.sh b/server/repro/env/install_music.sh new file mode 120000 index 0000000..f99f7ee --- /dev/null +++ b/server/repro/env/install_music.sh @@ -0,0 +1 @@ +../../../git-teaspeak/default_files/install_music.sh \ No newline at end of file diff --git a/server/repro/env/libs/libDataPipes.so b/server/repro/env/libs/libDataPipes.so new file mode 120000 index 0000000..306d809 --- /dev/null +++ b/server/repro/env/libs/libDataPipes.so @@ -0,0 +1 @@ +../../../../../libraries/DataPipes/build/libDataPipes.so \ No newline at end of file diff --git a/server/repro/env/libs/libTeaMusic.so b/server/repro/env/libs/libTeaMusic.so new file mode 120000 index 0000000..1e2f627 --- /dev/null +++ b/server/repro/env/libs/libTeaMusic.so @@ -0,0 +1 @@ +../../../../MusicBot/libs/libTeaMusic.so \ No newline at end of file diff --git a/server/repro/env/libs/libcrypto.so b/server/repro/env/libs/libcrypto.so new file mode 120000 index 0000000..fae1f22 --- /dev/null +++ b/server/repro/env/libs/libcrypto.so @@ -0,0 +1 @@ +../../../../../libraries/boringssl/build/crypto/libcrypto.so \ No newline at end of file diff --git a/server/repro/env/libs/libjemalloc.so.2 b/server/repro/env/libs/libjemalloc.so.2 new file mode 120000 index 0000000..7443d95 --- /dev/null +++ b/server/repro/env/libs/libjemalloc.so.2 @@ -0,0 +1 @@ +../../../../../libraries/jemalloc/build/lib/libjemalloc.so.2 \ No newline at end of file diff --git a/server/repro/env/libs/libmysqlcppconn.so.7 b/server/repro/env/libs/libmysqlcppconn.so.7 new file mode 120000 index 0000000..9fccf81 --- /dev/null +++ b/server/repro/env/libs/libmysqlcppconn.so.7 @@ -0,0 +1 @@ +../../../../../libraries/mysqlconnector/build/jdbc/driver/libmysqlcppconn.so.7 \ No newline at end of file diff --git a/server/repro/env/libs/libopus.so.0 b/server/repro/env/libs/libopus.so.0 new file mode 120000 index 0000000..7728f6c --- /dev/null +++ b/server/repro/env/libs/libopus.so.0 @@ -0,0 +1 @@ +/usr/local/lib/libopus.so.0 \ No newline at end of file diff --git a/server/repro/env/libs/libsqlite3.so.0 b/server/repro/env/libs/libsqlite3.so.0 new file mode 120000 index 0000000..2f47b65 --- /dev/null +++ b/server/repro/env/libs/libsqlite3.so.0 @@ -0,0 +1 @@ +/usr/lib/x86_64-linux-gnu/libsqlite3.so.0 \ No newline at end of file diff --git a/server/repro/env/libs/libssl.so b/server/repro/env/libs/libssl.so new file mode 120000 index 0000000..1855f24 --- /dev/null +++ b/server/repro/env/libs/libssl.so @@ -0,0 +1 @@ +../../../../../libraries/boringssl/build/ssl/libssl.so \ No newline at end of file diff --git a/server/repro/env/providers b/server/repro/env/providers new file mode 120000 index 0000000..4dedb55 --- /dev/null +++ b/server/repro/env/providers @@ -0,0 +1 @@ +../../../music/bin/providers/ \ No newline at end of file diff --git a/server/repro/env/resources b/server/repro/env/resources new file mode 120000 index 0000000..bb21fde --- /dev/null +++ b/server/repro/env/resources @@ -0,0 +1 @@ +../../../git-teaspeak/default_files/resources/ \ No newline at end of file diff --git a/server/repro/env/tealoop.sh b/server/repro/env/tealoop.sh new file mode 120000 index 0000000..bea769c --- /dev/null +++ b/server/repro/env/tealoop.sh @@ -0,0 +1 @@ +../../../git-teaspeak/default_files/tealoop.sh \ No newline at end of file diff --git a/server/repro/env/teastart.sh b/server/repro/env/teastart.sh new file mode 120000 index 0000000..0d98912 --- /dev/null +++ b/server/repro/env/teastart.sh @@ -0,0 +1 @@ +../../../git-teaspeak/default_files/teastart.sh \ No newline at end of file diff --git a/server/repro/env/teastart_autorestart.sh b/server/repro/env/teastart_autorestart.sh new file mode 120000 index 0000000..8f17a38 --- /dev/null +++ b/server/repro/env/teastart_autorestart.sh @@ -0,0 +1 @@ +../../../git-teaspeak/default_files/teastart_autorestart.sh \ No newline at end of file diff --git a/server/repro/env/teastart_minimal.sh b/server/repro/env/teastart_minimal.sh new file mode 120000 index 0000000..7536805 --- /dev/null +++ b/server/repro/env/teastart_minimal.sh @@ -0,0 +1 @@ +../../../git-teaspeak/default_files/teastart_minimal.sh \ No newline at end of file diff --git a/server/repro/generate_version.sh b/server/repro/generate_version.sh new file mode 100755 index 0000000..3523448 --- /dev/null +++ b/server/repro/generate_version.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +BUILD_PATH=$1 + +function debug() { + #eval "" + echo "${@}" +} + +VERSION_FILE="build_version.txt" +if [[ -f ${VERSION_FILE} ]]; then + rm ${VERSION_FILE} +fi + +if [[ -z ${BUILD_PATH} ]]; then + echo "Missing versions path!" + #exit 1 +fi + +CURRENT_VERSION=`cat env/buildVersion.txt` +CURRENT_VERSION_ESCAPED=$(echo "${CURRENT_VERSION}" | sed -e 's/[\/&\.\-]/\\&/g') +AVAILABLE_VERSIONS=`ssh -i build_private_key TeaSpeak-Jenkins@mcgalaxy.de " +if [ -d versions/${BUILD_PATH} ]; then + ls versions/${BUILD_PATH} | grep -E '^${CURRENT_VERSION_ESCAPED}(\-[0-9]+)?$' +fi +"` +debug "${AVAILABLE_VERSIONS}" + +TARGET_VERSION="" +TARGET_VERSION_INDEX=0 + +while [[ true ]]; do + if [[ ! ${TARGET_VERSION_INDEX} -eq 0 ]]; then + TARGET_VERSION="${CURRENT_VERSION}-${TARGET_VERSION_INDEX}" + else + TARGET_VERSION="${CURRENT_VERSION}" + fi + debug "Testing => ${TARGET_VERSION}" + debug "${AVAILABLE_VERSIONS}" | grep "${TARGET_VERSION}" &>/dev/null + if [[ $? -ne 0 ]]; then + debug "Found version ${TARGET_VERSION}" + break + fi + + TARGET_VERSION_INDEX=$(($TARGET_VERSION_INDEX+1)) +done + +echo "${TARGET_VERSION} ${CURRENT_VERSION} ${TARGET_VERSION_INDEX} TeaSpeak-${TARGET_VERSION}.tar.gz" > ${VERSION_FILE} \ No newline at end of file diff --git a/server/repro/make_symbol.sh b/server/repro/make_symbol.sh new file mode 100755 index 0000000..d656db8 --- /dev/null +++ b/server/repro/make_symbol.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +SYMBOL_ROOT="symbols" +BINARY_PATH="env" +TMP_FILE="temp" + +function mkdir_not_exists() { + if [[ ! -d $1 ]]; then + mkdir $1 + fi +} + +function create_dump() { + local BINARY_PATH=${1} + local BINARY_NAME=${2} + + echo "Creating dump file for ${BINARY_NAME} (${BINARY_PATH}/${BINARY_NAME})" + dump_syms ${BINARY_PATH}/${BINARY_NAME} > ${TMP_FILE} + SYM_INFO=$(head -n1 < ${TMP_FILE}) + SYM_INFO=($SYM_INFO) + DUMP_ID=${SYM_INFO[3]} + + echo "Dump ID: $DUMP_ID" + mkdir_not_exists ${SYMBOL_ROOT} + mkdir_not_exists ${SYMBOL_ROOT}/${BINARY_NAME} + mkdir_not_exists ${SYMBOL_ROOT}/${BINARY_NAME}/${DUMP_ID} + + DUMP_PATH=${SYMBOL_ROOT}/${BINARY_NAME}/${DUMP_ID}/${BINARY_NAME}.sym + mv ${TMP_FILE} ${DUMP_PATH} +} + +create_dump "env" "TeaSpeakServer" +create_dump "env/providers" "000ProviderFFMpeg.so" +create_dump "env/providers" "001ProviderYT.so" +echo "Created dump symbols!" diff --git a/server/repro/package_server.sh b/server/repro/package_server.sh new file mode 100755 index 0000000..3be6a56 --- /dev/null +++ b/server/repro/package_server.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +BUILD_INFO=($(cat build_version.txt)) +BUILD_FULL_NAME=${BUILD_INFO[0]} +BUILD_NAME=${BUILD_INFO[1]} +BUILD_VERSION=${BUILD_INFO[2]} +BUILD_FILENAME=${BUILD_INFO[3]} + +echo "Creating TeaSpeak ${BUILD_NAME} Build index ${BUILD_VERSION}" + +cp -r env finalenv +cd finalenv +echo -e "# Version: ${BUILD_FULL_NAME} +# TeaSpeak version: ${BUILD_NAME} +# Build version: ${BUILD_VERSION} + +{build_name: \"${BUILD_FULL_NAME}\", build_version: \"${BUILD_NAME}\", build_index: ${BUILD_VERSION}}" > buildVersion.txt + +#Create a copy and save unstripped +cp TeaSpeakServer TeaSpeakServerTmp +rm TeaSpeakServer +mv TeaSpeakServerTmp TeaSpeakServer + +strip -s -p -v TeaSpeakServer +tar --dereference -czvf "../${BUILD_FILENAME}" * +cd .. +rm -r finalenv +./make_symbol.sh + +echo "Package created (${BUILD_FILENAME})" diff --git a/server/repro/test_docker.sh b/server/repro/test_docker.sh new file mode 100755 index 0000000..96b1a23 --- /dev/null +++ b/server/repro/test_docker.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +BVERSION="$(cat env/buildVersion.txt)" +FBUILD=0 + +function buildName() { + if [ ! -z "$1" ]; then + L_FBUILD="$1" + else + L_FBUILD="${FBUILD}" + fi + if [ "$L_FBUILD" -eq "0" ]; then + FULLNAME="TeaSpeak-$BVERSION" + else + FULLNAME="TeaSpeak-$BVERSION-$L_FBUILD" + fi +} +FULLNAME="" +buildName + +while [ -f "${FULLNAME}.tar.gz" ] +do + FBUILD=$(($FBUILD+1)) + buildName +done +#Last known +FBUILD=$(($FBUILD-1)) +buildName + +echo "Got last release (${FULLNAME})" +echo "Copy to docker!" +docker cp ${FULLNAME}.tar.gz TeaSpeak-Test:/test/TeaSpeak.tar.gz +docker start -i TeaSpeak-Test \ No newline at end of file diff --git a/server/repro/test_release.sh b/server/repro/test_release.sh new file mode 100755 index 0000000..b99806b --- /dev/null +++ b/server/repro/test_release.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +BVERSION="$(cat env/buildVersion.txt)" +FNAME="TeaSpeak-$BVERSION.tar.gz" + +if [ -d test ]; then + echo "Deleting old test directory" + rm -r test +fi + +echo "Unpackaging release ($FNAME)" +mkdir test +tar xf $FNAME -C test/ + +cd test +#chmod +x teastart_minimal.sh +#./teastart_minimal.sh +LD_LIBRARY_PATH="./libs/" +./TeaSpeakServer +cd .. +#rm -r test diff --git a/server/src/Configuration.cpp b/server/src/Configuration.cpp new file mode 100644 index 0000000..086fdf5 --- /dev/null +++ b/server/src/Configuration.cpp @@ -0,0 +1,1635 @@ +#include + +#include "Configuration.h" +#include "build.h" +#include "../../license/shared/License.h" +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace std::chrono; +using namespace ts; +using namespace ts::config; + +inline std::string decode_string(const std::string& input) +{ + const size_t passwordLength = 16; + static const char password[passwordLength] = "invalid pointer"; + // out = in XOR NOT(password) + std::string result = input; + for (size_t i = 0; i < input.length(); i++) + result[i] ^= ~password[i % passwordLength]; + return result; +} + +//"Password" is "invalid pointe" +#define CRYPTED_VERSION_PLATFORM decode_string("\xda\xf8\xe7\xeb\xeb") //Its "Linux" +#define CRYPTED_VERSION_PREFIX decode_string("\xc2\xf4\xe8\xcd\xe3\xf3\xfa\xb4\xaf") //Its "TeaSpeak " + +//Variable define +std::string config::database::url; +std::string config::database::sqlite::locking_mode; +std::string config::database::sqlite::journal_mode; +std::string config::database::sqlite::sync_mode; + +std::shared_ptr config::license; +std::shared_ptr config::license_original; +bool config::experimental_31 = false; + +bool ts::config::binding::enforce_default_voice_host = false; +std::string ts::config::binding::DefaultVoiceHost; +std::string ts::config::binding::DefaultWebHost; +std::string ts::config::binding::DefaultQueryHost; +std::string ts::config::binding::DefaultFileHost; +uint16_t ts::config::binding::DefaultQueryPort; +uint16_t ts::config::binding::DefaultFilePort; + +std::string config::server::DefaultServerVersion; +std::string config::server::DefaultServerPlatform; +LicenseType config::server::DefaultServerLicense; +bool config::server::enable_teamspeak_weblist; +bool config::server::strict_ut8_mode; +bool config::server::show_invisible_clients_as_online; +bool config::server::disable_ip_saving; +ssize_t config::server::max_virtual_server; +bool config::server::badges::allow_badges; +bool config::server::badges::allow_overwolf; +bool config::server::authentication::name; + +uint16_t config::voice::default_voice_port; +size_t config::voice::DefaultPuzzlePrecomputeSize; +bool config::server::delete_missing_icon_permissions; +bool config::server::delete_old_bans; +int config::voice::RsaPuzzleLevel; +bool config::voice::warn_on_permission_editor; +bool config::voice::enforce_coocie_handshake; +int config::voice::connectLimit; +int config::voice::clientConnectLimit; +bool config::voice::notifyMuted; +bool config::voice::suppress_myts_warnings; +bool config::voice::allow_session_reinitialize; + +std::string config::query::motd; +std::string config::query::newlineCharacter; +int config::query::sslMode; +std::string config::query::ssl::certFile; +std::string config::query::ssl::keyFile; + +std::string config::messages::applicationCrashed; +std::string config::messages::applicationStopped; +std::string config::messages::serverStopped; + +std::string config::messages::idle_time_exceeded; +std::string config::messages::mute_notify_message; +std::string config::messages::unmute_notify_message; + +std::string config::messages::kick_invalid_badges; +std::string config::messages::kick_invalid_command; +std::string config::messages::kick_invalid_hardware_id; +std::string config::messages::kick_vpn; + +std::string config::messages::shutdown::scheduled; +std::string config::messages::shutdown::interval; +std::string config::messages::shutdown::now; +std::string config::messages::shutdown::canceled; +std::vector> config::messages::shutdown::intervals; + +std::string config::messages::music::song_announcement; + +std::string config::messages::timeout::packet_resend_failed; +std::string config::messages::timeout::connection_reinitialized; + +size_t config::threads::ticking; +size_t config::threads::voice::execute_limit; +size_t config::threads::voice::execute_per_server; +size_t config::threads::voice::events_per_server; +size_t config::threads::voice::io_min; +size_t config::threads::voice::io_per_server; +size_t config::threads::voice::io_limit; +bool config::threads::voice::bind_io_thread_to_kernel_thread; +size_t config::threads::music::execute_limit; +size_t config::threads::music::execute_per_bot; +size_t config::threads::web::io_loops; +std::string config::messages::teamspeak_permission_editor; + +bool config::web::activated; +std::deque> config::web::ssl::certificates; +uint16_t config::web::webrtc_port_max; +uint16_t config::web::webrtc_port_min; +deque config::web::ice_servers; +bool config::web::enable_upnp; + +size_t config::log::vs_size; +std::string config::log::path; +spdlog::level::level_enum config::log::terminalLevel; +spdlog::level::level_enum config::log::logfileLevel; +bool config::log::logfileColored; + +std::string config::geo::countryFlag; +std::string config::geo::mappingFile; +bool config::geo::staticFlag; +geoloc::ProviderType config::geo::type; + +bool config::geo::vpn_block; +std::string config::geo::vpn_file; + +std::string config::crash_path = "."; + +bool config::music::enabled; +std::string config::music::command_prefix; + +//Parse stuff +#define CREATE_IF_NOT_EXISTS 0b00000001 +#define PREMIUM_ONLY 0b00000010 +#define FLAG_REQUIRE 0b00000100 + +#define COMMENT(path, comment) commentMapping[path].emplace_back(comment) +#define WARN_SENSITIVE(path) COMMENT(path, "Do NOT TOUCH unless you're 100% sure!") + +class ConfigParseError : public exception { + public: + ConfigParseError(shared_ptr entry, std::string message) : binding(move(entry)), message(std::move(message)) { } + + const char *what() const noexcept { + return message.c_str(); + } + + shared_ptr entry() const { return this->binding; } + private: + shared_ptr binding; + std::string message; +}; + +class PathNodeError : public exception { + public: + PathNodeError(std::string path, std::string message) : _message(std::move(message)), _path(std::move(path)) { } + + const char *what() const noexcept { + return _message.c_str(); + } + + const std::string path() const { return this->_path; } + const std::string message() const { return this->_message; } + private: + std::string _path; + std::string _message; +}; + +std::string escapeJsonString(const std::string& input) { + std::ostringstream ss; + for (auto iter = input.cbegin(); iter != input.cend(); iter++) { + //C++98/03: + //for (std::string::const_iterator iter = input.begin(); iter != input.end(); iter++) { + switch (*iter) { + case '\\': ss << "\\\\"; break; + case '"': ss << "\\\""; break; + case '/': ss << "\\/"; break; + case '\b': ss << "\\b"; break; + case '\f': ss << "\\f"; break; + case '\n': ss << "\\n"; break; + case '\r': ss << "\\r"; break; + case '\t': ss << "\\t"; break; + default: ss << *iter; break; + } + } + return ss.str(); +} + +std::string escapeHeaderString(const std::string& input) { + std::ostringstream ss; + for (auto iter = input.cbegin(); iter != input.cend(); iter++) { + switch (*iter) { + case '\b': ss << "\\b"; break; + case '\f': ss << "\\f"; break; + case '\n': ss << "\\n"; break; + case '\r': ss << "\\r"; break; + case '\t': ss << "\\t"; break; + case '\"': ss << "\\\""; break; + default: ss << *iter; break; + } + } + return ss.str(); +} + +std::string deescapeJsonString(const std::string& input) { + std::ostringstream ss; + for (auto iter = input.cbegin(); iter != input.cend(); iter++) { + if(*iter == '\\'){ + if(iter++ != input.cend()) return ss.str(); + + switch (*++iter) { + case 't': ss << "\t"; break; + case 'n': ss << "\n"; break; + case 'r': ss << "\r"; break; + case 'b': ss << "\b"; break; + case 'f': ss << "\f"; break; + case '/': ss << "/"; break; + case '\\': ss << "\\\\"; break; + default: ss << *iter; break; + } + } else ss << *iter; + + } + return ss.str(); +} + +static bool saveConfig = false; + +std::vector split(const std::string &text, char sep) { + std::vector tokens; + std::size_t start = 0, end = 0; + while ((end = text.find(sep, start)) != std::string::npos) { + tokens.push_back(text.substr(start, end - start)); + start = end + 1; + } + if(!text.substr(start).empty()) + tokens.push_back(text.substr(start)); + return tokens; +} + +//We need to keep the root nodes in memory +vector resolveNode(const YAML::Node &root,const string& path, bool override_old = false){ + vector result; + result.push_back(root); + + auto entries = split(path, '.'); + for(auto it = entries.begin(); it != entries.end(); it++) { + auto& back = result.back(); + if(back.IsMap() || it == entries.end() || back.IsNull()) + result.push_back(back[*it]); + else if(!back.IsDefined() || override_old) + result.push_back((back = YAML::Node(YAML::NodeType::Map))[*it]); + else + throw PathNodeError(path, "Node '" + *it + "' isnt a sequence"); + } + return result; +} + +void remapValue(YAML::Node& node, const string &oldPath, const string &newPath){ + auto old = resolveNode(node, oldPath); + if(!old.back()) return; + + auto _new = resolveNode(node, newPath, true); + _new.back() = YAML::Clone(old.back()); + + if(old.size() > 1) { + auto oldNode = old[old.size() - 1]; + oldNode = YAML::Null; + if(old[old.size() - 2].remove(oldNode)) logError("Could not remove old config entry"); + } +} + +void build_comments(map>& map, const std::deque>& bindings) { + for(const auto& entry : bindings) { + for(const auto& message : entry->description) { + if(message.first.empty()) { + for(const auto& m : message.second) + map[entry->key].push_back(m); + } else { + map[entry->key].push_back(message.first + ":"); + for(const auto& m : message.second) + map[entry->key].push_back(" " + m); + } + } + + if(entry->value_description) + map[entry->key].push_back(entry->value_description()); + } +} + +void read_bindings(YAML::Node& root, const std::deque>& bindings) { + for(const auto& entry : bindings) { + if(entry->bounded_by != 0) continue; + auto nodes = resolveNode(root, entry->key); + assert(!nodes.empty()); + assert(entry->read_config); + + if((entry->flags & PREMIUM_ONLY) > 0 && !config::license->isPremium()) { + const auto default_value = entry->default_value(); + if(nodes.back().IsNull() || !nodes.back().IsDefined()) + entry->set_default(nodes.back()); + + for(const auto& e : entry->default_value()) + entry->read_argument(e); + continue; + } + + entry->read_config(nodes.back()); + } +} + +inline string apply_comments(stringstream &in, map>& comments); +std::deque> create_local_bindings(int& version, std::string& license); + +#define CURRENT_CONFIG_VERSION 13 +vector config::parseConfig(const std::string& path) { + //FIXME test for premium! + vector errors; + saveConfig = false; + + ifstream cfgStream(path); + YAML::Node config; + try { + config = YAML::Load(cfgStream); + } catch (const YAML::ParserException& ex){ + errors.push_back("Could not load config file: " + ex.msg + " @" + to_string(ex.mark.line) + ":" + to_string(ex.mark.column)); + return errors; + } + cfgStream.close(); + + map> comments; + try { + int config_version; + string teaspeak_license; + { + auto bindings = create_local_bindings(config_version, teaspeak_license); + read_bindings(config, bindings); + build_comments(comments, bindings); + } + if(config_version > CURRENT_CONFIG_VERSION) { + errors.push_back("Given config version is higher that currently supported config version!"); + errors.push_back("Decrease the version by hand to " + to_string(CURRENT_CONFIG_VERSION)); + errors.push_back("Attention: Decreasing the version could may lead to data loss!"); + return errors; + } + { + if(config_version != CURRENT_CONFIG_VERSION) { + logMessage("You're using an outdated config."); + logMessage("Updating config"); + + switch (config_version){ + case 1: + remapValue(config, "voice.rsa_puzzle_precompute_size", "voice.rsa.puzzle_pool_size"); + case 2: + remapValue(config, "messages.voice.app_stopped", "messages.application.stop"); + remapValue(config, "messages.voice.app_crashed", "messages.application.crash"); + remapValue(config, "messages.voice.server_stopped", "messages.voice.server_stop"); + case 3: + remapValue(config, "voice.kick_invalid_packet", "voice.protocol.kick_invalid_packet"); + case 4: + remapValue(config, "messages.voice.default_country", "voice.fallback_country"); + case 6: + config["geolocation"] = YAML::Node(YAML::NodeType::Map); + remapValue(config, "voice.fallback_country", "geolocation.fallback_country"); + remapValue(config, "voice.force_fallback_country.", "geolocation.force_fallback_country"); + case 7: + remapValue(config, "web.ssh.certificate", "web.ssl.certificate"); + remapValue(config, "web.ssh.privatekey", "web.ssl.privatekey"); + case 8: + remapValue(config, "voice.rsa.puzzle_level", "voice.handshake.puzzle_level"); + case 9: + if(config["general"]["dbFile"].IsDefined()) + config["general"]["dbFile"] = "sqlite://" + config["general"]["dbFile"].as(); + remapValue(config, "general.dbFile", "general.database_url"); + case 10: + remapValue(config, "messages.mute.kick_invalid.hardware_id", "messages.kick_invalid.hardware_id"); + remapValue(config, "messages.mute.kick_invalid.command", "messages.kick_invalid.command"); + remapValue(config, "messages.mute.kick_invalid.badges", "messages.kick_invalid.badges"); + remapValue(config, "messages.level", "log.terminal_level"); + case 11: + remapValue(config, "general.database_url", "general.database.url"); + case 12: + { + auto nodes_certificate = YAML::Clone(resolveNode(config, "web.ssl.certificate").back()); //We'll clone here because we're overriding it later + auto nodes_key = resolveNode(config, "web.ssl.privatekey").back(); + + if(nodes_certificate.IsDefined() && nodes_key.IsDefined()) { + auto node_certificates_default = resolveNode(config, "web.ssl.certificate.default", true); + node_certificates_default.back() = YAML::Node(YAML::NodeType::Map); + node_certificates_default.back()["certificate"] = nodes_certificate; + node_certificates_default.back()["private_key"] = nodes_key; + } + + nodes_key = YAML::Node(YAML::NodeType::Undefined); + } + default: + break; + } + config["version"] = CURRENT_CONFIG_VERSION; + config_version = CURRENT_CONFIG_VERSION; + } + } + + //License parsing + license_parsing: + { + string err; + if(teaspeak_license.empty() || teaspeak_license == "none") { + //Due to an implementation mistake every default license looks like this: + //AQA7CN26AAAAAMoLy9BIR2S9HyMeqBx7ZMUUc1rFXkt5YztwZCQRngncqtSs8hsQrzszzeNQSEcVXLtvIhm6m331OgjdugAAAADKbA25Oxab9/MK0xK3iZ8mUg+YkaZQkYS2Bj4Kcq2WFTCynv+sIfeYaei+VV8f/AJvxJDUfADJoSoGX85BIT3cTV+usRs3Adx0Ix/Wi5G4roL8Ypl0p8lYwELLGEVyEQf21rYMWOtVkQk2yoGuQikgLxr6p9sShKmAoES1lbHr8Xk= + //teaspeak_license = license::createLocalLicence(license::LicenseType::DEMO, system_clock::time_point(), "TeaSpeak"); + teaspeak_license = "AQA7CN26AAAAAMoLy9BIR2S9HyMeqBx7ZMUUc1rFXkt5YztwZCQRngncqtSs8hsQrzszzeNQSEcVXLtvIhm6m331OgjdugAAAADKbA25Oxab9/MK0xK3iZ8mUg+YkaZQkYS2Bj4Kcq2WFTCynv+sIfeYaei+VV8f/AJvxJDUfADJoSoGX85BIT3cTV+usRs3Adx0Ix/Wi5G4roL8Ypl0p8lYwELLGEVyEQf21rYMWOtVkQk2yoGuQikgLxr6p9sShKmAoES1lbHr8Xk="; + config::license = license::readLocalLicence(teaspeak_license, err); + } else { + config::license_original = license::readLocalLicence(teaspeak_license, err); + config::license = config::license_original; + } + + if(!config::license){ +#if true + logErrorFmt(true, LOG_GENERAL, "The given license isnt valid!"); + logErrorFmt(true, LOG_GENERAL, "Falling back to the default license."); + teaspeak_license = "none"; + goto license_parsing; +#else + errors.push_back("Invalid license code! (" + err + ")"); + return errors; +#endif + } + if(!config::license){ + errors.emplace_back("Invalid license code!"); + return errors; + } + if(!config::license->isValid()) { + if(config::license->data.type == license::LicenseType::INVALID) { + errors.emplace_back("Give license isn't valid!"); + return errors; + } + + logErrorFmt(true, LOG_GENERAL, "The given license isnt valid!"); + logErrorFmt(true, LOG_GENERAL, "Falling back to the default license."); + teaspeak_license = "none"; + goto license_parsing; + } + } + + { + auto bindings = create_bindings(); + read_bindings(config, bindings); + build_comments(comments, bindings); + + for(const auto& entry : config::web::ice_servers) { + auto dp = entry.find(':'); + if(dp == string::npos) { + errors.push_back("Invalid ice server entry! Missing port"); + continue; + } + auto host = entry.substr(0, dp); + auto port = entry.substr(dp + 1); + if(port.find_last_not_of("0123456789") != string::npos) { + errors.push_back("Invalid ice server entry! Invalid port (" + port + ")"); + continue; + } + + try { + stoi(port); + } catch(std::exception& ex) { + errors.push_back("Invalid ice server entry! Invalid port (" + port + ")"); + } + } + } + + auto currentVersion = CRYPTED_VERSION_PREFIX + build::version()->string(true); + if(currentVersion != config::server::DefaultServerVersion) { + auto ref = config::server::DefaultServerVersion; + try { + auto pattern = "TeaSpeak " + build::pattern(); + static std::regex const matcher(pattern); + if (std::regex_match(ref, matcher)) { + logMessage("Updating displayed version in config to " + currentVersion); + config["server"]["version"] = currentVersion; + config::server::DefaultServerVersion = currentVersion; + } else { + debugMessage("Pattern not match (" + pattern + ")"); + } + } catch(std::exception& e){ + logError("Could not update displayed version (" + string(e.what()) + ")"); + } + } + + stringstream off; + off << config; + + ofstream foff(path); + foff << apply_comments(off, comments) << endl; + foff.close(); + } catch(const YAML::Exception& ex) { + errors.push_back("Could not read config: " + ex.msg + " @" + to_string(ex.mark.line) + ":" + to_string(ex.mark.column)); + return errors; + } catch(const ConfigParseError& ex) { + errors.push_back("Failed to parse config entry \"" + ex.entry()->key + "\": " + ex.what()); + return errors; + } catch(const PathNodeError& ex) { + errors.push_back("Expected sequence for path " + ex.path() + ": " + ex.message()); + return errors; + } + return errors; +} + +void bind_string_description(const shared_ptr& _entry, std::string& target, const std::string& default_value) { + _entry->default_value = [default_value]() -> std::deque { return { default_value }; }; + _entry->value_description = [] { return "The value must be a string"; }; +} + +void bind_string_parse(const shared_ptr& _entry, std::string& target, const std::string& default_value) { + weak_ptr weak_entry = _entry; + + _entry->set_default = [weak_entry, default_value](YAML::Node& node) { + auto entry = weak_entry.lock(); + if(!entry) throw ConfigParseError(nullptr, "Entry handle got deleted!"); + + node = default_value; + }; + + _entry->read_config = [weak_entry, default_value, &target](YAML::Node& node) { + auto entry = weak_entry.lock(); + if(!entry) throw ConfigParseError(nullptr, "Entry handle got deleted!"); + + if(!node.IsDefined() || node.IsNull()) { + if((entry->flags & FLAG_REQUIRE) > 0) + throw ConfigParseError(entry, "missing required setting"); + else + entry->set_default(node); + } + try { + target = node.as(); + entry->bounded_by = 1; + } catch (const YAML::BadConversion& e) { + throw ConfigParseError(entry, "Invalid node content. Requested was a string!"); + } + }; + + _entry->read_argument = [weak_entry, &target](const std::string& value) { + auto entry = weak_entry.lock(); + if(!entry) throw ConfigParseError(nullptr, "Entry handle got deleted!"); + entry->bounded_by = 2; + + target = value; + }; +} + +template +inline std::string type_name() { + int status; + std::string tname = typeid(T).name(); + char *demangled_name = abi::__cxa_demangle(tname.c_str(), NULL, NULL, &status); + if(status == 0) { + tname = demangled_name; + std::free(demangled_name); + } + return tname; +} + +template +static typename std::enable_if::value, type_t>::type integral_parse(const std::string& value) { + try { + auto result = std::stoull(value); + if(result < numeric_limits::min()) throw std::out_of_range(""); + if(result > numeric_limits::max()) throw std::out_of_range(""); + return (type_t) result; + } catch(std::out_of_range& ex) { + throw YAML::BadConversion(YAML::Mark::null_mark()); + } catch(std::invalid_argument& ex) { + throw YAML::BadConversion(YAML::Mark::null_mark()); + } +} + +template +static typename std::enable_if::value, type_t>::type integral_parse(const std::string& value) { + try { + auto result = std::stoll(value); + if(result < numeric_limits::min()) throw std::out_of_range(""); + if(result > numeric_limits::max()) throw std::out_of_range(""); + return (type_t) result; + } catch(std::out_of_range& ex) { + throw YAML::BadConversion(YAML::Mark::null_mark()); + } catch(std::invalid_argument& ex) { + throw YAML::BadConversion(YAML::Mark::null_mark()); + } +} + +template +static typename std::enable_if::type enum_number_cast(type_t value) { return (uint16_t) value; } +template +static typename std::enable_if::type enum_number_cast(type_t value) { return (uint16_t) value; } +template +static typename std::enable_if::type enum_number_cast(type_t value) { return (uint32_t) value; } +template +static typename std::enable_if::type enum_number_cast(type_t value) { return (uint64_t) value; } + +static map integral_mapping = { + {"bool", "boolean"}, + {"unsigned char", "positive numeric value"}, + {"unsigned short", "positive numeric value"}, + {"unsigned int", "positive numeric value"}, + {"unsigned long short", "positive numeric value"}, + {"char", "numeric value"}, + {"short", "numeric value"}, + {"int", "numeric value"}, + {"long short", "numeric value"}, +}; + +template ::value || std::is_enum::value, int>::type = 0> +void bind_integral_description(const shared_ptr& _entry, type_t& target, type_t default_value, type_t min_value, type_t max_value) { + _entry->default_value = [default_value]() -> std::deque { return { to_string(default_value) }; }; + _entry->value_description = [min_value, max_value] { + auto type_name = ::type_name((type_t) 0))>(); + return "The value must be a " + (integral_mapping.count(type_name) > 0 ? integral_mapping[type_name] : type_name) + " between " + to_string(min_value) + " and " + to_string(max_value); + }; +} + +template ::value || std::is_enum::value, int>::type = 0> +void bind_integral_parse(const shared_ptr& _entry, type_t& target, type_t default_value, type_t min_value, type_t max_value) { + weak_ptr weak_entry = _entry; + + _entry->set_default = [weak_entry, default_value](YAML::Node& node) { + auto entry = weak_entry.lock(); + if(!entry) throw ConfigParseError(nullptr, "Entry handle got deleted!"); + + node = enum_number_cast(default_value); + }; + + _entry->read_config = [weak_entry, default_value, &target, min_value, max_value](YAML::Node& node) { + auto entry = weak_entry.lock(); + if(!entry) throw ConfigParseError(nullptr, "Entry handle got deleted!"); + + if(!node.IsDefined() || node.IsNull()) { + if((entry->flags & FLAG_REQUIRE) > 0) + throw ConfigParseError(entry, "missing required setting"); + else + entry->set_default(node); + } + try { + auto str = node.as(); + auto value = (type_t) node.as((type_t) 0))>(); + if(value < min_value) throw ConfigParseError(entry, "Invalid value (" + to_string(value) + "). Received value substeps boundary (" + to_string(min_value) + ")"); + if(value > max_value) throw ConfigParseError(entry, "Invalid value (" + to_string(value) + "). Received value exceeds boundary (" + to_string(max_value) + ")"); + + target = value; + entry->bounded_by = 1; + } catch (const YAML::BadConversion& e) { + throw ConfigParseError(entry, "Invalid node content. Requested was a integral of type " + type_name() + "!"); + } + }; + + _entry->read_argument = [weak_entry, &target, min_value, max_value](const std::string& string) { + auto entry = weak_entry.lock(); + if(!entry) throw ConfigParseError(nullptr, "Entry handle got deleted!"); + entry->bounded_by = 2; + + try { + auto value = (type_t) integral_parse((type_t) 0))>(string); + if(value < min_value) throw ConfigParseError(entry, "Invalid value (" + to_string(value) + "). Received value substeps boundary (" + to_string(min_value) + ")"); + if(value > max_value) throw ConfigParseError(entry, "Invalid value (" + to_string(value) + "). Received value exceeds boundary (" + to_string(max_value) + ")"); + + target = value; + entry->bounded_by = 2; + } catch (const YAML::BadConversion& e) { + throw ConfigParseError(entry, "Invalid node content. Requested was a integral of type " + type_name() + "!"); + } + }; +} + +void bind_vector_description(const shared_ptr& _entry, deque&, const deque& default_value) { + _entry->default_value = [default_value]() -> std::deque { return default_value; }; + _entry->value_description = [] { return "The value must be a sequence"; }; +} + +void bind_vector_parse(const shared_ptr& _entry, deque& target, const deque& default_value) { + weak_ptr weak_entry = _entry; + + + _entry->set_default = [weak_entry, default_value](YAML::Node& node) { + auto entry = weak_entry.lock(); + if(!entry) throw ConfigParseError(nullptr, "Entry handle got deleted!"); + + node = YAML::Node(YAML::NodeType::Sequence); + for(const auto& entry : default_value) + node.push_back(entry); + }; + + _entry->read_config = [weak_entry, default_value, &target](YAML::Node& node) { + auto entry = weak_entry.lock(); + if(!entry) throw ConfigParseError(nullptr, "Entry handle got deleted!"); + + if(!node.IsDefined() || node.IsNull()) { + if((entry->flags & FLAG_REQUIRE) > 0) + throw ConfigParseError(entry, "missing required setting"); + else { + entry->set_default(node); + } + } + if(!node.IsSequence()) + throw ConfigParseError(entry, "node requires to be a sequence"); + + try { + target.clear(); + for(const auto& element : node) { + target.push_back(element.as()); + } + } catch (const YAML::BadConversion& e) { + throw ConfigParseError(entry, "Invalid node sequence content"); + } + }; + + _entry->read_argument = [weak_entry, &target](const std::string& string) { + auto entry = weak_entry.lock(); + if(!entry) throw ConfigParseError(nullptr, "Entry handle got deleted!"); + if(entry->bounded_by != 2) + target.clear(); + entry->bounded_by = 2; + + target.push_back(string); + }; +} + +struct GroupStackEntry { + GroupStackEntry(deque& list, string path) : list(list), path(move(path)) { + list.push_back(this->path); + } + ~GroupStackEntry() { + assert(list.back() == this->path); + list.pop_back(); + } + + string path; + deque& list; +}; + +inline std::string join_path(const deque& stack, const std::string& entry) { + stringstream ss; + for(const auto& e : stack) + ss << e << "."; + ss << entry; + return ss.str(); +} + +#define STR(x) #x +#define BIND_GROUP(name) GroupStackEntry group_ ##name(group_stack, STR(name)); + +#define CREATE_BINDING(name, _flags) \ + auto binding = make_shared(); \ + binding->key = join_path(group_stack, name); \ + binding->flags = _flags; \ + result.push_back(binding) + +#define BIND_STRING(target, default) \ + bind_string_parse(binding, target, default); \ + bind_string_description(binding, target, default) + +#define BIND_VECTOR(target, default) \ + bind_vector_parse(binding, target, default); \ + bind_vector_description(binding, target, default) + +#define BIND_INTEGRAL(target, default, min, max) \ + bind_integral_parse::type>( \ + binding, \ + target, \ + (typename std::remove_reference::type) default, \ + (typename std::remove_reference::type) min, \ + (typename std::remove_reference::type) max \ + ); \ + bind_integral_description::type>( \ + binding, \ + target, \ + (typename std::remove_reference::type) default, \ + (typename std::remove_reference::type) min, \ + (typename std::remove_reference::type) max \ + ) + +#define BIND_BOOL(target, default) BIND_INTEGRAL(target, default, false, true) + +#define ADD_DESCRIPTION(desc, ...) \ + for(const auto& entry : {desc, ##__VA_ARGS__}) \ + binding->description["Description"].emplace_back(entry) + +#define ADD_NOTE(desc, ...) \ + for(const auto& entry : {desc, ##__VA_ARGS__}) \ + binding->description["Notes"].emplace_back(entry) + +#define ADD_WARN(desc, ...) \ + for(const auto& entry : {desc, ##__VA_ARGS__}) \ + binding->description["Warning"].emplace_back(entry) + +#define ADD_SENSITIVE() ADD_WARN("Do NOT TOUCH unless you're 100% sure!") + +std::deque> create_local_bindings(int& version, std::string& license) { + deque> result; + deque group_stack; + + { + CREATE_BINDING("version", 0); + BIND_INTEGRAL(version, CURRENT_CONFIG_VERSION, 0, 10000000000); + ADD_DESCRIPTION("The current config version"); + ADD_WARN("This is an auto-generated id!", "Modification could cause data loss!"); + } + { + CREATE_BINDING("general.license", 0); + BIND_STRING(license, "none"); + ADD_DESCRIPTION("Insert here your TeaSpeak license code (if you have one)"); + } + + return result; +} + +static std::deque> _create_bindings; +std::deque> config::create_bindings() { + if(!_create_bindings.empty()) return _create_bindings; + + deque> result; + deque group_stack; + + { + BIND_GROUP(general); + + //CREATE_BINDING("database_url", 0); old + { + BIND_GROUP(database); + { + CREATE_BINDING("url", 0); + BIND_STRING(config::database::url, "sqlite://TeaData.sqlite"); + ADD_DESCRIPTION("Available urls:"); + ADD_DESCRIPTION(" sqlite://[file]"); + ADD_DESCRIPTION(" mysql://[host][:port]/[database][?propertyName1=propertyValue1[&propertyName2=propertyValue2]...]"); + ADD_DESCRIPTION(""); + ADD_DESCRIPTION("More info about about the mysql url could be found here: https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-configuration-properties.html"); + ADD_DESCRIPTION("There's also a new property called 'connections', which describes how many connections and queries could be executed synchronously"); + ADD_DESCRIPTION("MySQL example: mysql://localhost:3306/teaspeak?userName=root&password=mysecretpassword&connections=4"); + ADD_DESCRIPTION("Attention: If you're using MySQL you need at least 3 connections!"); + } + + { + BIND_GROUP(sqlite); + { + CREATE_BINDING("locking_mode", 0); + BIND_STRING(config::database::sqlite::locking_mode, "EXCLUSIVE"); + ADD_DESCRIPTION("Sqlite database locking mode."); + ADD_DESCRIPTION("Set it to nothing (\"\") to use the default driver setting"); + ADD_DESCRIPTION("More information could be found here: https://www.sqlite.org/lockingv3.html"); + } + { + CREATE_BINDING("sync_mode", 0); + BIND_STRING(config::database::sqlite::sync_mode, "NORMAL"); + ADD_DESCRIPTION("Sqlite database synchronous mode."); + ADD_DESCRIPTION("Set it to nothing (\"\") to use the default driver setting"); + ADD_DESCRIPTION("More information could be found here: https://www.sqlite.org/pragma.html#pragma_synchronous"); + } + { + CREATE_BINDING("journal_mode", 0); + BIND_STRING(config::database::sqlite::journal_mode, "WAL"); + ADD_DESCRIPTION("Sqlite database journal mode."); + ADD_DESCRIPTION("Set it to nothing (\"\") to use the default driver setting"); + ADD_DESCRIPTION("More information could be found here: https://www.sqlite.org/pragma.html#pragma_journal_mode"); + } + } + } + { + CREATE_BINDING("crash_path", 0); + BIND_STRING(config::crash_path, "crash_dumps/"); + ADD_DESCRIPTION("Define the folder where the crash dump files will be moved, when the server crashes"); + } + { + CREATE_BINDING("command_prefix", 0); + BIND_STRING(config::music::command_prefix, "."); + ADD_DESCRIPTION("The default channel chat command prefix"); + } + } + { + BIND_GROUP(log) + { + CREATE_BINDING("level", 0); + BIND_INTEGRAL(config::log::logfileLevel, spdlog::level::debug, spdlog::level::trace, spdlog::level::off); + ADD_DESCRIPTION("The log level within the log files"); + ADD_DESCRIPTION("Available types:"); + ADD_DESCRIPTION(" 0: Trace"); + ADD_DESCRIPTION(" 1: Debug"); + ADD_DESCRIPTION(" 2: Info"); + ADD_DESCRIPTION(" 3: Warn"); + ADD_DESCRIPTION(" 4: Error"); + ADD_DESCRIPTION(" 5: Critical"); + ADD_DESCRIPTION(" 6: Off"); + } + { + CREATE_BINDING("terminal_level", 0); + BIND_INTEGRAL(config::log::terminalLevel, spdlog::level::info, spdlog::level::trace, spdlog::level::off); + ADD_DESCRIPTION("The log level within the TeaSpeak server terminal"); + ADD_DESCRIPTION("Available types:"); + ADD_DESCRIPTION(" 0: Trace"); + ADD_DESCRIPTION(" 1: Debug"); + ADD_DESCRIPTION(" 2: Info"); + ADD_DESCRIPTION(" 3: Warn"); + ADD_DESCRIPTION(" 4: Error"); + ADD_DESCRIPTION(" 5: Critical"); + ADD_DESCRIPTION(" 6: Off"); + } + { + CREATE_BINDING("colored", 0); + BIND_BOOL(config::log::logfileColored, false); + ADD_DESCRIPTION("Disable/enable ascii codes within the log file"); + } + { + CREATE_BINDING("vs_size", 0); + BIND_INTEGRAL(config::log::vs_size, 0, 0, numeric_limits::max()); + ADD_DESCRIPTION("Virtual server log chunk size"); + } + { + CREATE_BINDING("path", 0); + BIND_STRING(config::log::path, "logs/log_${time}(%Y-%m-%d_%H:%M:%S)_${group}.log"); + ADD_DESCRIPTION("The log file path"); + } + } + { + BIND_GROUP(binding); + { + BIND_GROUP(voice); + { + CREATE_BINDING("default_host", 0); + BIND_STRING(config::binding::DefaultVoiceHost, "0.0.0.0,::"); + ADD_NOTE("Multibinding supported here! Host delimiter is \",\""); + } + { + CREATE_BINDING("enforce", 0); + BIND_BOOL(config::binding::enforce_default_voice_host, false); + ADD_NOTE("Enforce the default host for every virtual server. Ignoring the server specific host"); + } + } + { + BIND_GROUP(web); + { + CREATE_BINDING("default_host", 0); + BIND_STRING(config::binding::DefaultWebHost, "0.0.0.0"); + ADD_NOTE("Multibinding like the voice server isnt supported yet!"); + } + } + { + BIND_GROUP(query); + { + CREATE_BINDING("port", 0); + BIND_INTEGRAL(config::binding::DefaultQueryPort, 10101, 1, 65535); + } + { + CREATE_BINDING("host", 0); + BIND_STRING(config::binding::DefaultQueryHost, "0.0.0.0"); + ADD_NOTE("Multibinding like the voice server isnt supported yet!"); + } + } + { + BIND_GROUP(file); + { + CREATE_BINDING("port", 0); + BIND_INTEGRAL(config::binding::DefaultFilePort, 30303, 1, 65535); + } + { + CREATE_BINDING("host", 0); + BIND_STRING(config::binding::DefaultFileHost, "0.0.0.0"); + ADD_NOTE("Multibinding like the voice server isnt supported yet!"); + } + } + } + { + BIND_GROUP(query); + { + CREATE_BINDING("nl_char", 0); + BIND_STRING(config::query::newlineCharacter, "\n\r"); + ADD_DESCRIPTION("Change the query newline character"); + } + { + CREATE_BINDING("motd", 0); + BIND_STRING(config::query::motd, "TeaSpeak\n\rWelcome on the TeaSpeak ServerQuery interface.\n\r"); + ADD_DESCRIPTION("The query welcome message"); + + ADD_NOTE("If not like TeamSpeak then some applications may not recognize the Query"); + ADD_NOTE("Default TeamSpeak 3 MOTD:"); + ADD_NOTE("TS3\n\rWelcome to the TeamSpeak 3 ServerQuery interface, type \"help\" for a list of commands and \"help \" for information on a specific command.\n\r"); + ADD_NOTE("NOTE: Sometimes you have to append one \n\r more!"); + } + { + CREATE_BINDING("enableSSL", 0); + BIND_INTEGRAL(config::query::sslMode, 2, 0, 2); + ADD_DESCRIPTION("Enable/disable SSL for query"); + ADD_DESCRIPTION("Available modes:"); + ADD_DESCRIPTION(" 0: Disabled"); + ADD_DESCRIPTION(" 1: Enabled (Enforced encryption)"); + ADD_DESCRIPTION(" 2: Hybrid (Prefer encryption but fallback when it isnt available)"); + } + { + BIND_GROUP(ssl); + { + CREATE_BINDING("certificate", 0); + BIND_STRING(config::query::ssl::certFile, "certs/query_certificate.pem"); + ADD_DESCRIPTION("The SSL certificate for the query client"); + } + { + CREATE_BINDING("privatekey", 0); + BIND_STRING(config::query::ssl::keyFile, "certs/query_privatekey.pem"); + ADD_DESCRIPTION("The SSL private key for the query client (You have to export the key without a password!)"); + } + } + } + { + BIND_GROUP(voice) + { + CREATE_BINDING("default_port", 0); + BIND_INTEGRAL(config::voice::default_voice_port, 9987, 1, 65535); + ADD_DESCRIPTION("Change the default voice server port", "This also defines the start where the instance search for free server ports on a new server creation"); + ADD_NOTE("This setting only apply once, when you create a new instance.", + "Once applied the default server port would not be changed!", + "The start point for the server creation still apply."); + } + { + CREATE_BINDING("notifymute", 0); + BIND_BOOL(config::voice::notifyMuted, false); + ADD_DESCRIPTION("Enable/disable the mute notify"); + } + { + CREATE_BINDING("suppress_myts_warnings", 0); + BIND_BOOL(config::voice::suppress_myts_warnings, true); + ADD_DESCRIPTION("Suppress the MyTS integration warnings"); + } + { + CREATE_BINDING("allow_session_reinitialize", 0); + BIND_BOOL(config::voice::allow_session_reinitialize, true); + ADD_DESCRIPTION("Enable/disable fast session reinitialisation."); + ADD_SENSITIVE(); + } + { + CREATE_BINDING("rsa.puzzle_pool_size", 0); + BIND_INTEGRAL(config::voice::DefaultPuzzlePrecomputeSize, 128, 1, 65536); + ADD_DESCRIPTION("The amount of precomputed puzzles"); + ADD_SENSITIVE(); + } + { + BIND_GROUP(handshake); + { + CREATE_BINDING("puzzle_level", 0); + BIND_INTEGRAL(config::voice::RsaPuzzleLevel, 1000, 512, 1048576); + ADD_DESCRIPTION("The puzzle level. (A higher number will result a longer calculation time for the manager RSA puzzle)"); + ADD_SENSITIVE(); + } + { + CREATE_BINDING("enforce_cookie", 0); + BIND_BOOL(config::voice::enforce_coocie_handshake, true); + ADD_DESCRIPTION("Enforces the cookie exchange (Low level protection against distributed denial of service attacks (DDOS attacks))"); + ADD_NOTE("This option is highly recommended!"); + ADD_SENSITIVE(); + } + { + CREATE_BINDING("warn_on_permission_editor", 0); + BIND_BOOL(config::voice::warn_on_permission_editor, true); + ADD_DESCRIPTION("Enables/disabled the warning popup for the TeamSpeak 3 permission editor."); + ADD_NOTE("This option is highly recommended!"); + } + } + { + CREATE_BINDING("connect_limit", 0); + BIND_INTEGRAL(config::voice::connectLimit, 10, 0, 1024); + ADD_DESCRIPTION("Maximum amount of join attempts per second."); + ADD_NOTE("A value of zero means unlimited"); + } + { + CREATE_BINDING("client_connect_limit", 0); + BIND_INTEGRAL(config::voice::clientConnectLimit, 3, 0, 1024); + ADD_DESCRIPTION("Maximum amount of join attempts per second per ip."); + ADD_NOTE("A value of zero means unlimited"); + } + { + CREATE_BINDING("protocol.experimental_31", 0); + BIND_BOOL(config::experimental_31, false); + ADD_DESCRIPTION("Enables the newer and safer protocol based on TeamSpeak's documented standard"); + ADD_NOTE("An invalid protocol chain could lead clients to calculate a wrong shared secret result"); + ADD_NOTE("This may cause a connection setup fail and the client will be unable to connect!"); + } + } + { + BIND_GROUP(server) + { + CREATE_BINDING("platform", PREMIUM_ONLY); + BIND_STRING(config::server::DefaultServerPlatform, CRYPTED_VERSION_PLATFORM); + ADD_DESCRIPTION("The displayed platform to the client"); + ADD_NOTE("This option is only for the premium version."); + } + { + CREATE_BINDING("version", PREMIUM_ONLY); + BIND_STRING(config::server::DefaultServerVersion, CRYPTED_VERSION_PREFIX + build::version()->string(true)); + ADD_DESCRIPTION("The displayed version to the client"); + ADD_NOTE("This option is only for the premium version."); + } + { + CREATE_BINDING("licence", PREMIUM_ONLY); + BIND_INTEGRAL(config::server::DefaultServerLicense, LicenseType::LICENSE_AUTOMATIC_SERVER, LicenseType::_LicenseType_MIN, LicenseType::_LicenseType_MAX); + ADD_DESCRIPTION("The displayed licence type to every TeaSpeak 3 Client"); + ADD_DESCRIPTION("Available types:"); + ADD_DESCRIPTION(" 0: No Licence"); + ADD_DESCRIPTION(" 1: Authorised TeaSpeak Host Provider License (ATHP)"); + ADD_DESCRIPTION(" 2: Offline/Lan Licence"); + ADD_DESCRIPTION(" 3: Non-Profit License (NPL)"); + ADD_DESCRIPTION(" 4: Unknown Licence"); + ADD_DESCRIPTION(" 5: ~placeholder~"); + ADD_DESCRIPTION(" 6: Auto-License (Server based)"); + ADD_DESCRIPTION(" 7: Auto-License (Instance based)"); + ADD_NOTE("This option just work for non 3.2 clients!"); + ADD_NOTE("This option is only for the premium version."); + } + { + CREATE_BINDING("delete_old_bans", 0); + BIND_BOOL(config::server::delete_old_bans, true); + ADD_DESCRIPTION("Enable/disable the deletion of old bans within the database"); + } + { + CREATE_BINDING("delete_missing_icon_permissions", 0); + BIND_BOOL(config::server::delete_missing_icon_permissions, true); + ADD_DESCRIPTION("Enable/disable the deletion of invalid icon id permissions"); + } + { + CREATE_BINDING("allow_weblist", 0); + BIND_BOOL(config::server::enable_teamspeak_weblist, true); + ADD_DESCRIPTION("Enable/disable weblist reports globally! (Server setting wount be disabled, they will be just not send)"); + } + { + CREATE_BINDING("strict_ut8_mode", 0); + BIND_BOOL(config::server::strict_ut8_mode, false); + ADD_DESCRIPTION("If enabled an error will be throws on invalid UTF-8 characters within the protocol (Query & Client)."); + ADD_DESCRIPTION("Else the property pair will be dropped silently!"); + } + { + CREATE_BINDING("show_invisible_clients", 0); + BIND_BOOL(config::server::show_invisible_clients_as_online, true); + ADD_DESCRIPTION("Allow anybody to send text messages to clients which are in invisible channels"); + } + { + CREATE_BINDING("disable_ip_saving", 0); + BIND_BOOL(config::server::disable_ip_saving, false); + ADD_DESCRIPTION("Disable the saving of IP addresses within the database"); + } + { + CREATE_BINDING("max_virtual_servers", 0); + BIND_INTEGRAL(config::server::max_virtual_server, 16, -1, 999999); + ADD_DESCRIPTION("Set the limit for maximal virtual servers. -1 means unlimited."); + } + { + /* + BIND_GROUP(badges); + { + CREATE_BINDING("badges", 0); + BIND_BOOL(config::server::badges::allow_badges, true); + ADD_DESCRIPTION("Allow or disallow TeamSpeak badges"); + } + { + CREATE_BINDING("overwolf", 0); + BIND_BOOL(config::server::badges::allow_overwolf, true); + ADD_DESCRIPTION("Allow or disallow the Overwolf badge"); + } + */ + } + { + BIND_GROUP(authentication); + { + CREATE_BINDING("name", 0); + BIND_BOOL(config::server::authentication::name, false); + ADD_DESCRIPTION("Allow or disallow client authentication just by their name"); + } + } + } + { + BIND_GROUP(web); + { + CREATE_BINDING("enabled", 0); + BIND_BOOL(config::web::activated, true); + ADD_DESCRIPTION("Disable/enable the possibility to connect via the TeaSpeak web client"); + ADD_NOTE("If you've disabled this feature the TeaClient wound be able to join too."); + } + { + CREATE_BINDING("upnp", 0); + BIND_BOOL(config::web::enable_upnp, false); + ADD_DESCRIPTION("Disable/enable UPNP support"); + ADD_SENSITIVE(); + } + { + BIND_GROUP(ssl) + { + CREATE_BINDING("certificate", 0); + binding->type = 4; + + /* no terminal handling */ + binding->read_argument = [](const std::string&) { + logError(LOG_GENERAL, "Failed to parse ssl certificate. Its only possible to configure them via config!"); + }; + binding->default_value = []() -> deque { return {}; }; + + binding->set_default = [](YAML::Node& node) { + auto default_node = node["default"]; + default_node["certificate"] = "default_certificate.pem"; + default_node["private_key"] = "default_privatekey.pem"; + }; + + weak_ptr _binding = binding; + binding->read_config = [_binding](YAML::Node& node) { + auto b = _binding.lock(); + if(!b) + return; + + if(!node.IsDefined() || node.IsNull()) + b->set_default(node); + + for(auto it = node.begin(); it != node.end(); it++) { + auto node_cert = it->second["certificate"]; + auto node_key = it->second["private_key"]; + if(!node_cert.IsDefined()) { + logError(LOG_GENERAL, "Failed to parse web certificate. Missing \"certificate\" key."); + continue; + } + if(!node_key.IsDefined()) { + logError(LOG_GENERAL, "Failed to parse web certificate. Missing \"private_key\" key."); + continue; + } + + config::web::ssl::certificates.push_back({it->first.as(), node_key.as(), node_cert.as()}); + } + }; + } + /* + { + CREATE_BINDING("certificate", 0); + BIND_STRING(config::web::ssl::certFile, "certs/default_certificate.pem"); + ADD_DESCRIPTION("The SSL certificate for the web client"); + } + { + CREATE_BINDING("privatekey", 0); + BIND_STRING(config::web::ssl::keyFile, "certs/default_privatekey.pem"); + ADD_DESCRIPTION("The SSL private key for the web client (You have to export the key without a password!)"); + } + */ + } + + { + CREATE_BINDING("webrtc.port_min", 0); + BIND_INTEGRAL(config::web::webrtc_port_min, 50000, 0, 65535); + ADD_DESCRIPTION("Define the port range within the web client and TeaClient operates in"); + ADD_DESCRIPTION("A port of zero stands for no limit"); + ADD_NOTE("These ports must opened to use the voice bridge (Protocol: UDP)"); + } + { + CREATE_BINDING("webrtc.port_max", 0); + BIND_INTEGRAL(config::web::webrtc_port_max, 56000, 0, 65535); + ADD_DESCRIPTION("Define the port range within the web client and TeaClient operates in"); + ADD_DESCRIPTION("A port of zero stands for no limit"); + ADD_NOTE("These ports must opened to use the voice bridge (Protocol: UDP)"); + } + { + CREATE_BINDING("webrtc.ice", 0); + BIND_VECTOR(config::web::ice_servers, {"stun.l.google.com:19302"}); + ADD_DESCRIPTION("A list of possible offered ice servers"); + } + } + { + BIND_GROUP(geolocation); + { + CREATE_BINDING("fallback_country", 0); + BIND_STRING(config::geo::countryFlag, "DE"); + ADD_DESCRIPTION("The fallback country if lookup fails"); + } + { + CREATE_BINDING("force_fallback_country", 0); + BIND_BOOL(config::geo::staticFlag, false); + ADD_DESCRIPTION("Enforce the default country and disable resolve"); + } + + { + CREATE_BINDING("mapping.file", 0); + BIND_STRING(config::geo::mappingFile, "geoloc/IP2Location.CSV"); + ADD_DESCRIPTION("The mapping file for the given provider"); + ADD_DESCRIPTION("Default for IP2Location: geoloc/IP2Location.CSV"); + ADD_DESCRIPTION("Default for Software77: geoloc/IpToCountry.csv"); + } + { + CREATE_BINDING("mapping.type", 0); + BIND_INTEGRAL(config::geo::type, geoloc::PROVIDER_IP2LOCATION, geoloc::PROVIDER_MIN, geoloc::PROVIDER_MAX); + ADD_DESCRIPTION("The IP 2 location resolver"); + ADD_DESCRIPTION("0 = IP2Location"); + ADD_DESCRIPTION("1 = Software77"); + } + { + BIND_GROUP(vpn); + { + CREATE_BINDING("file", 0); + BIND_STRING(config::geo::vpn_file, "geoloc/ipcat.csv"); + ADD_DESCRIPTION("The mapping file for vpn checker (https://github.com/client9/ipcat/blob/master/datacenters.csv)"); + } + { + CREATE_BINDING("enabled", 0); + BIND_BOOL(config::geo::vpn_block,false); + ADD_DESCRIPTION("Disable/enable the vpn detection"); + } + } + } + { + BIND_GROUP(music) + { + CREATE_BINDING("enabled", 0); + BIND_BOOL(config::music::enabled, true); + ADD_DESCRIPTION("Enable/disable the music bots"); + } + } + { + BIND_GROUP(messages); + { + CREATE_BINDING("voice.server_stop", 0); + BIND_STRING(config::messages::serverStopped, "Server stopped"); + } + { + CREATE_BINDING("application.stop", 0); + BIND_STRING(config::messages::applicationStopped, "Application stopped"); + } + { + CREATE_BINDING("application.crash", 0); + BIND_STRING(config::messages::applicationCrashed, "Application crashed"); + } + { + CREATE_BINDING("idle_time", 0); + BIND_STRING(config::messages::idle_time_exceeded, "Idle time exceeded"); + } + { + CREATE_BINDING("teamspeak_permission_editor", 0); + BIND_STRING(config::messages::teamspeak_permission_editor, "\n[b][COLOR=#aa0000]ATTENTION[/COLOR][/b]:\nIt seems like you're trying to edit the TeaSpeak permissions with the TeamSpeak 3 client!\nThis is [b]really[/b] buggy due a bug within the client you're using.\n\nWe recommand to [b]use the [url=https://web.teaspeak.de/]TeaSpeak-Web[/url][/b] client or the [b][url=https://teaspeak.de/]TeaSpeak client[/url][/b].\nYatQA is a good option as well.\n\nTo disable/edit this message please edit the config.yml\nPlease note: Permission bugs, which will be reported wound be accepted."); + } + { + BIND_GROUP(mute); + { + CREATE_BINDING("mute_message", 0); + BIND_STRING(config::messages::mute_notify_message, "Hey!\nI muted you!"); + } + { + CREATE_BINDING("unmute_message", 0); + BIND_STRING(config::messages::mute_notify_message, "Hey!\nI unmuted you!"); + } + } + { + BIND_GROUP(kick_invalid); + { + CREATE_BINDING("hardware_id", 0); + BIND_STRING(config::messages::kick_invalid_hardware_id, "Invalid hardware id. Protocol hacked?"); + } + { + CREATE_BINDING("command", 0); + BIND_STRING(config::messages::kick_invalid_hardware_id, "Invalid command. Protocol hacked?"); + } + { + CREATE_BINDING("badges", 0); + BIND_STRING(config::messages::kick_invalid_hardware_id, "Invalid badges. Protocol hacked?"); + } + } + { + CREATE_BINDING("vpn.kick", 0); + BIND_STRING(config::messages::kick_vpn, "Please disable your VPN! (Provider: ${provider.name})"); + ADD_DESCRIPTION("This is the kick/ban message when a client tries to connect with a vpn"); + + ADD_DESCRIPTION("Variables are enabled. Available:"); + ADD_DESCRIPTION(" - provider.name => Contains the provider of the ip which has been flaged as vps"); + ADD_DESCRIPTION(" - provider.website => Contains the website provider of the ip which has been flaged as vps"); + } + { + BIND_GROUP(shutdown); + { + CREATE_BINDING("scheduled", 0); + BIND_STRING(config::messages::shutdown::scheduled, "[b][color=#DA9100]Scheduled shutdown at ${time}(%Y-%m-%d %H:%M:%S)[/color][/b]"); + } + { + CREATE_BINDING("interval", 0); + BIND_STRING(config::messages::shutdown::interval, "[b][color=red]Server instance shutting down in ${interval}[/color][/b]"); + ADD_DESCRIPTION("${interval} is defined via map in 'intervals'"); + } + { + CREATE_BINDING("intervals", 0); + binding->default_value = []() { return deque{}; }; + binding->type = 4; + + weak_ptr weak_binding = binding; + binding->read_config = [weak_binding](YAML::Node& node) { + auto bind = weak_binding.lock(); + if(!bind) return; + + if(node.IsNull() || !node.IsDefined() || !node.IsMap()) + bind->set_default(node); + + try { + auto intervals = node.as>(); + for(const auto& pair : intervals) + config::messages::shutdown::intervals.push_back({seconds(pair.first), pair.second}); + } catch(YAML::Exception& ex) { + logError(LOG_GENERAL, "Failed to parse shutdown intervals! Exception: {}", ex.what()); + } + }; + + binding->set_default = [weak_binding](YAML::Node& node) { + auto bind = weak_binding.lock(); + if(!bind) return; + + node = YAML::Node(); + + const static vector> intervals = { + {1, "1 second"}, + {2, "2 seconds"}, + {3, "3 seconds"}, + {4, "4 seconds"}, + {5, "5 seconds"}, + {10, "10 seconds"}, + {20, "20 seconds"}, + {30, "30 seconds"}, + {60, "1 minute"}, + {2 * 60, "2 minutes"}, + {3 * 60, "3 minutes"}, + {5 * 60, "5 minutes"}, + {10 * 60, "10 minutes"}, + {20 * 60, "20 minutes"}, + {30 * 60, "30 minutes"}, + {60 * 60, "1 hour"}, + {2 * 60 * 60, "2 hours"}, + }; + + for(const auto& pair : intervals) + node[to_string(pair.first)] = pair.second; + }; + ADD_DESCRIPTION("Add or delete intervals as you want"); + } + { + CREATE_BINDING("now", 0); + BIND_STRING(config::messages::shutdown::now, "[b][color=red]Server instance shutting down in now[/color][/b]"); + } + { + CREATE_BINDING("canceled", 0); + BIND_STRING(config::messages::shutdown::canceled, "[b][color=green]Scheduled instance shutdown canceled![/color][/b]"); + } + } + { + BIND_GROUP(music); + { + CREATE_BINDING("song_announcement", 0); + BIND_STRING(config::messages::music::song_announcement, "Now replaying ${title} (${url}) added by ${invoker}"); + ADD_DESCRIPTION("${title} title of the song"); + ADD_DESCRIPTION("${description} description of the song"); + ADD_DESCRIPTION("${url} url of the song"); + ADD_DESCRIPTION("${invoker} link to the song adder"); + } + } + { + BIND_GROUP(timeout); + { + CREATE_BINDING("connection_reinitialized", 0); + BIND_STRING(config::messages::timeout::connection_reinitialized, "Connection lost"); + } + { + CREATE_BINDING("packet_resend_failed", 0); + BIND_STRING(config::messages::timeout::packet_resend_failed, "Packet resend failed"); + } + } + } + { + BIND_GROUP(threads); + { + CREATE_BINDING("ticking", 0); + BIND_INTEGRAL(config::threads::ticking, 2, 1, 128); + ADD_DESCRIPTION("Thread pool size for the ticking task of a VirtualServer"); + ADD_SENSITIVE(); + } + { + BIND_GROUP(music); + { + CREATE_BINDING("execute_limit", 0); + BIND_INTEGRAL(config::threads::music::execute_limit, 15, 1, 1024); + ADD_DESCRIPTION("Max number of threads for command handling on the instance"); + ADD_SENSITIVE(); + } + { + CREATE_BINDING("execute_per_bot", 0); + BIND_INTEGRAL(config::threads::music::execute_per_bot, 1, 1, 128); + ADD_DESCRIPTION("Threads per server for command executing"); + ADD_SENSITIVE(); + } + } + { + CREATE_BINDING("web.io_loops", 0); + BIND_INTEGRAL(config::threads::web::io_loops, 4, 1, 128); + ADD_DESCRIPTION("Thread pool size for the ticking task of a VirtualServer"); + ADD_SENSITIVE(); + } + { + BIND_GROUP(voice) + { + CREATE_BINDING("events_per_server", 0); + BIND_INTEGRAL(config::threads::voice::events_per_server, 2, 1, 16); + ADD_DESCRIPTION("Kernel events per server"); + ADD_SENSITIVE(); + } + { + CREATE_BINDING("execute_per_server", 0); + BIND_INTEGRAL(config::threads::voice::execute_per_server, 2, 1, 128); + ADD_DESCRIPTION("Threads per server for command executing"); + ADD_SENSITIVE(); + } + { + CREATE_BINDING("execute_limit", 0); + BIND_INTEGRAL(config::threads::voice::execute_limit, 10, 1, 1024); + ADD_DESCRIPTION("Max number of threads for command handling threads within the instance"); + ADD_SENSITIVE(); + } + { + CREATE_BINDING("io_min", 0); + BIND_INTEGRAL(config::threads::voice::io_min, 2, 1, 1024); + ADD_DESCRIPTION("Minimum IO threads"); + ADD_SENSITIVE(); + } + { + CREATE_BINDING("io_per_server", 0); + BIND_INTEGRAL(config::threads::voice::io_per_server, 2, 1, 64); + ADD_DESCRIPTION("IO Thread increase per server"); + ADD_SENSITIVE(); + } + { + CREATE_BINDING("io_limit", 0); + BIND_INTEGRAL(config::threads::voice::io_limit, 10, 1, 1024); + ADD_DESCRIPTION("Max IO threads"); + ADD_SENSITIVE(); + } + { + CREATE_BINDING("bind_io_thread_to_kernel_thread", 0); + BIND_BOOL(config::threads::voice::bind_io_thread_to_kernel_thread, false); + ADD_DESCRIPTION("Bind each IO thread to one kernel thread to improve socket IO performance"); + ADD_SENSITIVE(); + } + } + } + return _create_bindings = result; +} + + +inline string apply_comments(stringstream &in, map>& comments) { + stringstream out; + stringstream lineBuffer; + + vector tree; + vector deepness; + + char read; + + //The header + for(const auto& comment : comments["header"]) + out << "#" << escapeHeaderString(comment) << endl; + + while(in){ + int deep = 0; + + do { + read = static_cast(in.get()); + if(read != ' ' && read != '\t') break; + lineBuffer << read; + deep++; + } while(in); + assert(read != ' ' && read != '\t'); + + stringstream keyStream; + stringstream pathStream; + std::string key; + std::string path; + + if(read == '-') { //We have a list entry + lineBuffer << read; + goto writeUntilNewLine; + } + + do { + lineBuffer << read; + if(read == ':') break; + keyStream << read; + read = static_cast(in.get()); + } while(in); + assert(read == ':'); + + key = keyStream.str(); + while (!deepness.empty() && deep <= deepness.back()) { + deepness.pop_back(); + tree.pop_back(); + } + deepness.push_back(deep); + tree.push_back(key); + + for(const auto& entry : tree) + pathStream << "." << entry; + path = pathStream.str().substr(1); + //cout << "having key " << key << " at deep " << deep << " - " << deepness.size() << " - " << path << endl; + for(const auto& comment : comments[path]){ + for(int index = 0; index < deepness.back(); index++) + out << " "; + out << "#" << escapeHeaderString(comment) << endl; + } + writeUntilNewLine: + out << lineBuffer.str(); + lineBuffer = stringstream(); + + do { + read = static_cast(in.get()); + if(!in) break; //End of lstream reached :D + out << read; + } while(in && read != '\n'); + } + assert(lineBuffer.str().empty()); + + //The footer + for(const auto& comment : comments["footer"]) + out << "#" << escapeJsonString(comment) << endl; + + return out.str(); +} \ No newline at end of file diff --git a/server/src/Configuration.h b/server/src/Configuration.h new file mode 100644 index 0000000..69b89db --- /dev/null +++ b/server/src/Configuration.h @@ -0,0 +1,218 @@ +#pragma once + +#include +#include +#include +#ifdef byte + #undef byte +#endif +#include +#include "geo/GeoLocation.h" +#include "../../license/shared/License.h" + +namespace YAML { + class Node; +} +namespace ts { + namespace config { + struct EntryBinding { + std::string key; + std::map> description; + uint8_t flags = 0; + + int type = 0; /* 0 = unbound | 1 = string | 2 = number | 3 = boolean | 4 = user defined */ + int bounded_by = 0; /* 0 = unbound | 1 = config | 2 = command line */ + + std::function()> default_value; + std::function value_description; + std::function set_default; + std::function read_config; + std::function read_argument; + }; + + extern std::vector parseConfig(const std::string& /* path */); + extern std::deque> create_bindings(); + + namespace database { + extern std::string url; + namespace sqlite { + extern std::string journal_mode; + extern std::string locking_mode; + extern std::string sync_mode; + } + } + + extern std::shared_ptr license; + extern std::shared_ptr license_original; + + extern bool experimental_31; + + namespace binding { + extern bool enforce_default_voice_host; + extern std::string DefaultVoiceHost; + extern std::string DefaultWebHost; + extern std::string DefaultQueryHost; + extern std::string DefaultFileHost; + + extern uint16_t DefaultQueryPort; + extern uint16_t DefaultFilePort; + } + + namespace server { + extern std::string DefaultServerVersion; + extern std::string DefaultServerPlatform; + + extern bool delete_old_bans; + extern bool delete_missing_icon_permissions; + + extern LicenseType DefaultServerLicense; + + extern bool strict_ut8_mode; + + extern bool enable_teamspeak_weblist; + extern bool show_invisible_clients_as_online; + extern bool disable_ip_saving; + + namespace badges { + extern bool allow_overwolf; + extern bool allow_badges; + } + + namespace authentication { + extern bool name; + } + + extern ssize_t max_virtual_server; + } + + namespace voice { + extern size_t DefaultPuzzlePrecomputeSize; + extern int RsaPuzzleLevel; + extern bool enforce_coocie_handshake; + + + extern bool notifyMuted; + extern int connectLimit; + extern int clientConnectLimit; + + extern bool suppress_myts_warnings; + extern uint16_t default_voice_port; + + extern bool warn_on_permission_editor; + extern bool allow_session_reinitialize; + } + + namespace geo { + extern std::string countryFlag; + extern bool staticFlag; + + extern std::string mappingFile; + extern geoloc::ProviderType type; + + extern bool vpn_block; + extern std::string vpn_file; + } + + namespace query { + extern std::string motd; + extern std::string newlineCharacter; + + extern int sslMode; + namespace ssl { + extern std::string keyFile; + extern std::string certFile; + } + } + + namespace music { + extern bool enabled; + extern std::string command_prefix; + } + + namespace messages { + extern std::string serverStopped; + extern std::string applicationStopped; + extern std::string applicationCrashed; + + extern std::string idle_time_exceeded; + + extern std::string mute_notify_message; + extern std::string unmute_notify_message; + + extern std::string kick_invalid_hardware_id; + extern std::string kick_invalid_badges; + extern std::string kick_invalid_command; + + extern std::string kick_vpn; + + extern std::string teamspeak_permission_editor; + + namespace shutdown { + extern std::string scheduled; + extern std::string interval; + extern std::string now; + extern std::string canceled; + + extern std::vector> intervals; + } + + namespace music { + extern std::string song_announcement; + } + + namespace timeout { + extern std::string packet_resend_failed; + extern std::string connection_reinitialized; + } + } + + namespace web { + extern bool activated; + + namespace ssl { + /* servername; private key file; public key file*/ + extern std::deque> certificates; + } + + extern uint16_t webrtc_port_min; + extern uint16_t webrtc_port_max; + extern std::deque ice_servers; + extern bool enable_upnp; + } + + namespace threads { + extern size_t ticking; + + namespace voice { + extern size_t execute_per_server; + extern size_t execute_limit; + + extern size_t events_per_server; + extern size_t io_min; + extern size_t io_per_server; + extern size_t io_limit; + + extern bool bind_io_thread_to_kernel_thread; + } + + namespace music { + extern size_t execute_per_bot; + extern size_t execute_limit; + } + + namespace web { + extern size_t io_loops; + } + } + + namespace log { + extern std::string path; + extern size_t vs_size; + extern spdlog::level::level_enum logfileLevel; + extern bool logfileColored; + extern spdlog::level::level_enum terminalLevel; + } + + extern std::string crash_path; + } +} \ No newline at end of file diff --git a/server/src/ConnectionStatistics.cpp b/server/src/ConnectionStatistics.cpp new file mode 100644 index 0000000..636b98e --- /dev/null +++ b/server/src/ConnectionStatistics.cpp @@ -0,0 +1,326 @@ +// +// Created by wolverindev on 11.11.17. +// + +#include +#include "ConnectionStatistics.h" +#include "TSServer.h" + +using namespace std; +using namespace std::chrono; +using namespace ts; +using namespace ts::server; +using namespace ts::stats; +using namespace ts::protocol; + +ConnectionStatistics::ConnectionStatistics(const shared_ptr& handle, bool properties) : handle(handle) { + memtrack::allocated(this); + + if(properties) { + this->properties = make_shared(); //TODO load etc? + this->properties->register_property_type(); + } + + /* + this->properties->registerProperty("connection_packets_sent_speech", 0, PROP_STATISTIC); + this->properties->registerProperty("connection_bytes_sent_speech", 0, PROP_STATISTIC); + this->properties->registerProperty("connection_packets_received_speech", 0, PROP_STATISTIC); + this->properties->registerProperty("connection_bytes_received_speech", 0, PROP_STATISTIC); + + this->properties->registerProperty("connection_packets_sent_keepalive", 0, PROP_STATISTIC); + this->properties->registerProperty("connection_bytes_sent_keepalive", 0, PROP_STATISTIC); + this->properties->registerProperty("connection_packets_received_keepalive", 0, PROP_STATISTIC); + this->properties->registerProperty("connection_bytes_received_keepalive", 0, PROP_STATISTIC); + + this->properties->registerProperty("connection_packets_sent_control", 0, PROP_STATISTIC); + this->properties->registerProperty("connection_bytes_sent_control", 0, PROP_STATISTIC); + this->properties->registerProperty("connection_packets_received_control", 0, PROP_STATISTIC); + this->properties->registerProperty("connection_bytes_received_control", 0, PROP_STATISTIC); + + this->properties->registerProperty("connection_packets_sent_total", 0, PROP_STATISTIC); + this->properties->registerProperty("connection_bytes_sent_total", 0, PROP_STATISTIC); + this->properties->registerProperty("connection_packets_received_total", 0, PROP_STATISTIC); + this->properties->registerProperty("connection_bytes_received_total", 0, PROP_STATISTIC); + + this->properties->registerProperty("connection_bandwidth_sent_last_second_total", 0, PROP_STATISTIC); + this->properties->registerProperty("connection_bandwidth_sent_last_minute_total", 0, PROP_STATISTIC); + this->properties->registerProperty("connection_bandwidth_received_last_second_total", 0, PROP_STATISTIC); + this->properties->registerProperty("connection_bandwidth_received_last_minute_total", 0, PROP_STATISTIC); + + this->properties->registerProperty("connection_filetransfer_bandwidth_sent", 0, PROP_STATISTIC); + this->properties->registerProperty("connection_filetransfer_bandwidth_received", 0, PROP_STATISTIC); + this->properties->registerProperty("connection_filetransfer_bytes_sent_total", 0, PROP_STATISTIC); + this->properties->registerProperty("connection_filetransfer_bytes_received_total", 0, PROP_STATISTIC); + */ +} + +ConnectionStatistics::~ConnectionStatistics() { + memtrack::freed(this); + + { + lock_guard lock(this->history_lock_incoming); + + for(auto entry : this->history_incoming) + if(entry->use_count.fetch_sub(1) == 1) + delete entry; + for(auto entry : this->history_file_incoming) + if(entry->use_count.fetch_sub(1) == 1) + delete entry; + + this->history_incoming.clear(); + this->history_file_incoming.clear(); + } + { + lock_guard lock(this->history_lock_outgoing); + + for(auto entry : this->history_outgoing) + if(entry->use_count.fetch_sub(1) == 1) + delete entry; + for(auto entry : this->history_file_outgoing) + if(entry->use_count.fetch_sub(1) == 1) + delete entry; + + this->history_outgoing.clear(); + this->history_file_outgoing.clear(); + } +} + +std::shared_ptr ConnectionStatistics::statistics() { + return this->properties; +} + +/* general statistics */ +inline int8_t typeIndex(const PacketTypeInfo& type){ + if(type == PacketTypeInfo::Command || type == PacketTypeInfo::CommandLow) + return 1; + else if(type == PacketTypeInfo::Ack || type == PacketTypeInfo::AckLow) + return 2; + else if(type == PacketTypeInfo::Voice || type == PacketTypeInfo::VoiceWhisper) + return 3; + return -1; +} + +void ConnectionStatistics::logIncomingPacket(const ClientPacket& pkt) { + auto info_entry = new StatisticEntry{}; + info_entry->timestamp = system_clock::now(); + info_entry->size = uint16_t(pkt.data().length() + pkt.header().length() + pkt.mac().length()); + + auto index = typeIndex(pkt.type()); + this->_log_incoming_packet(info_entry, index); +} + +void ConnectionStatistics::_log_incoming_packet(ts::stats::StatisticEntry *info_entry, int8_t index) { + if(index >= 0 && index <= 3) { + this->connection_packets_received[index] ++; + this->connection_bytes_received[index] += info_entry->size; + } + this->connection_packets_received[0] ++; + this->connection_bytes_received[0] += info_entry->size; + + if(this->_measure_bandwidths) { + auto lock_count = info_entry->use_count++; + assert(lock_count >= 0); + + lock_guard lock(this->history_lock_incoming); + this->history_incoming.push_back(info_entry); + } + if(this->handle) + this->handle->_log_incoming_packet(info_entry, index); +} + +void ConnectionStatistics::logOutgoingPacket(const ServerPacket& pkt) { + auto info_entry = new StatisticEntry{}; + info_entry->timestamp = system_clock::now(); + info_entry->size = uint16_t(pkt.data().length() + pkt.header().length() + pkt.mac().length()); + + auto index = typeIndex(pkt.type()); + this->_log_outgoing_packet(info_entry, index); +} + +void ConnectionStatistics::_log_outgoing_packet(ts::stats::StatisticEntry *info_entry, int8_t index) { + if(index >= 0 && index <= 3) { + this->connection_packets_sent[index] ++; + this->connection_bytes_sent[index] += info_entry->size; + } + this->connection_packets_sent[0] ++; + this->connection_bytes_sent[0] += info_entry->size; + + if(this->_measure_bandwidths) { + auto lock_count = info_entry->use_count++; + assert(lock_count >= 0); + + lock_guard lock(this->history_lock_outgoing); + this->history_outgoing.push_back(info_entry); + } + if(this->handle) + this->handle->_log_outgoing_packet(info_entry, index); +} + +/* file transfer */ +void ConnectionStatistics::logFileTransfareIn(uint64_t bytes) { + auto info_entry = new StatisticEntry{}; + info_entry->timestamp = system_clock::now(); + info_entry->size = bytes; + + this->_log_incoming_file_packet(info_entry); +} + +void ConnectionStatistics::_log_incoming_file_packet(ts::stats::StatisticEntry *info_entry) { + this->file_bytes_received += info_entry->size; + + if(this->_measure_bandwidths) { + auto lock_count = info_entry->use_count++; + assert(lock_count >= 0); + + lock_guard lock(this->history_lock_incoming); + this->history_file_incoming.push_back(info_entry); + } + + if(this->handle) + this->handle->_log_incoming_file_packet(info_entry); +} + +void ConnectionStatistics::logFileTransfareOut(uint64_t bytes) { + auto info_entry = new StatisticEntry{}; + info_entry->timestamp = system_clock::now(); + info_entry->size = bytes; + + this->_log_outgoing_file_packet(info_entry); +} + +void ConnectionStatistics::_log_outgoing_file_packet(ts::stats::StatisticEntry *info_entry) { + this->file_bytes_sent += info_entry->size; + + if(this->_measure_bandwidths) { + auto lock_count = info_entry->use_count++; + assert(lock_count >= 0); + + lock_guard lock(this->history_lock_outgoing); + this->history_file_outgoing.push_back(info_entry); + } + + if(this->handle) + this->handle->_log_outgoing_file_packet(info_entry); +} + +void ConnectionStatistics::tick() { + StatisticEntry* entry; + { + auto timeout_min = system_clock::now() - minutes(1); + + lock_guard lock(this->history_lock_incoming); + + while(!this->history_incoming.empty() && (entry = this->history_incoming[0])->timestamp < timeout_min) { + if(entry->use_count.fetch_sub(1) == 1) + delete entry; + + this->history_incoming.pop_front(); + } + + while(!this->history_file_incoming.empty() && (entry = this->history_file_incoming[0])->timestamp < timeout_min) { + if(entry->use_count.fetch_sub(1) == 1) + delete entry; + + this->history_file_incoming.pop_front(); + } + } + { + auto timeout_min = system_clock::now() - minutes(1); + lock_guard lock(this->history_lock_outgoing); + + while(!this->history_outgoing.empty() && (entry = this->history_outgoing[0])->timestamp < timeout_min) { + if(entry->use_count.fetch_sub(1) == 1) + delete entry; + + this->history_outgoing.pop_front(); + } + + while(!this->history_file_outgoing.empty() && (entry = this->history_file_outgoing[0])->timestamp < timeout_min) { + if(entry->use_count.fetch_sub(1) == 1) + delete entry; + + this->history_file_outgoing.pop_front(); + } + } + + if(this->properties) { + auto& _properties = *this->properties; +#define M(type, index) \ + _properties[property::CONNECTION_BYTES_SENT_ ##type] = (uint64_t) this->connection_bytes_sent[index]; \ + _properties[property::CONNECTION_PACKETS_SENT_ ##type] = (uint64_t) this->connection_packets_sent[index]; \ + _properties[property::CONNECTION_BYTES_RECEIVED_ ##type] = (uint64_t) this->connection_bytes_received[index]; \ + _properties[property::CONNECTION_PACKETS_RECEIVED_ ##type] = (uint64_t) this->connection_packets_received[index]; \ + + M(TOTAL, 0); + M(CONTROL, 1); + M(KEEPALIVE, 2); + M(SPEECH, 3); + + _properties[property::CONNECTION_FILETRANSFER_BYTES_RECEIVED_TOTAL] = (uint64_t) this->file_bytes_received; + _properties[property::CONNECTION_FILETRANSFER_BYTES_SENT_TOTAL] = (uint64_t) this->file_bytes_sent; + } +} + +DataSummery ConnectionStatistics::dataReport() { + DataSummery report{}; + auto minTimeout = system_clock::now() - seconds(1); + + + { + lock_guard lock(this->history_lock_incoming); + + for(const auto& elm : this->history_incoming){ + if(elm->timestamp >= minTimeout) { + report.recv_second += elm->size; + } + + report.recv_minute += elm->size; + } + + for(const auto& elm : this->history_file_incoming) { + report.file_recv += elm->size; + } + } + + { + lock_guard lock(this->history_lock_outgoing); + + for(const auto& elm : this->history_outgoing){ + if(elm->timestamp >= minTimeout) { + report.send_second += elm->size; + } + + report.send_minute += elm->size; + } + + for(const auto& elm : this->history_file_outgoing) { + report.file_send += elm->size; + } + } + + report.recv_minute /= 60; + report.send_minute /= 60; + return report; +} + +std::pair ConnectionStatistics::mark_file_bytes() { + std::pair result; + + { + lock_guard lock(this->history_lock_incoming); + if(this->mark_file_bytes_received < this->file_bytes_received) + result.second = this->file_bytes_received - this->mark_file_bytes_received; + this->mark_file_bytes_received = (uint64_t) this->file_bytes_received; + } + + { + + lock_guard lock(this->history_lock_outgoing); + + if(this->mark_file_bytes_sent < this->file_bytes_sent) + result.first = this->file_bytes_sent - this->mark_file_bytes_sent; + this->mark_file_bytes_sent = (uint64_t) this->file_bytes_sent; + } + + return result; +} \ No newline at end of file diff --git a/server/src/ConnectionStatistics.h b/server/src/ConnectionStatistics.h new file mode 100644 index 0000000..acc90cd --- /dev/null +++ b/server/src/ConnectionStatistics.h @@ -0,0 +1,106 @@ +#pragma once + +#include +#include +#include +#include "Definitions.h" + +namespace ts { + namespace server { + class TSServer; + } + + namespace stats { + struct StatisticEntry { + std::atomic use_count{0}; + std::chrono::time_point timestamp; + uint16_t size = 0; + }; + + struct DataSummery { + uint32_t send_minute; + uint32_t send_second; + + uint32_t recv_minute; + uint32_t recv_second; + + uint32_t file_recv; + uint32_t file_send; + }; + + class ConnectionStatistics { + public: + explicit ConnectionStatistics(const std::shared_ptr& /* root */, bool /* spawn properties */); + ~ConnectionStatistics(); + + std::shared_ptr statistics(); + + void logIncomingPacket(const protocol::ClientPacket&); + void logOutgoingPacket(const protocol::ServerPacket&); + void logFileTransfareIn(uint64_t); + void logFileTransfareOut(uint64_t); + + void tick(); + + DataSummery dataReport(); + + std::pair mark_file_bytes(); + + inline bool measure_bandwidths() { return this->_measure_bandwidths; } + void measure_bandwidths(bool flag) { this->_measure_bandwidths = flag; } + + inline bool has_properties() { return !!this->properties; } + private: + class spin_lock { + std::atomic_flag locked = ATOMIC_FLAG_INIT; + public: + void lock() { + uint8_t round = 0; + while (locked.test_and_set(std::memory_order_acquire)) { + //Yield when we're using this lock for a longer time, which we usually not doing + if(round++ % 8 == 0) + std::this_thread::yield(); + } + } + + inline bool try_lock() { + return !locked.test_and_set(std::memory_order_acquire); + } + + void unlock() { + locked.clear(std::memory_order_release); + } + }; + typedef spin_lock fast_lock_t; + + bool _measure_bandwidths = true; + std::shared_ptr handle; + std::shared_ptr properties; + + + std::atomic connection_packets_sent[4]{0, 0, 0, 0}; + std::atomic connection_bytes_sent[4]{0, 0, 0, 0}; + std::atomic connection_packets_received[4]{0, 0, 0, 0}; + std::atomic connection_bytes_received[4]{0, 0, 0, 0}; + + std::atomic file_bytes_sent = 0; + std::atomic file_bytes_received = 0; + + std::atomic mark_file_bytes_sent = 0; + std::atomic mark_file_bytes_received = 0; + + fast_lock_t history_lock_outgoing; + fast_lock_t history_lock_incoming; + std::deque history_file_incoming{}; + std::deque history_file_outgoing{}; + std::deque history_incoming{}; + std::deque history_outgoing{}; + + void _log_incoming_packet(StatisticEntry */* statistics */, int8_t /* type index */); + void _log_outgoing_packet(StatisticEntry* /* statistics */, int8_t /* type index */); + + void _log_incoming_file_packet(StatisticEntry */* statistics */); + void _log_outgoing_file_packet(StatisticEntry* /* statistics */); + }; + } +} \ No newline at end of file diff --git a/server/src/DatabaseHelper.cpp b/server/src/DatabaseHelper.cpp new file mode 100644 index 0000000..77026eb --- /dev/null +++ b/server/src/DatabaseHelper.cpp @@ -0,0 +1,1361 @@ +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace std::chrono; +using namespace ts; +using namespace ts::server; +using namespace ts::permission; + +//#define DISABLE_CACHING + +DatabaseHelper::DatabaseHelper(sql::SqlManager* srv) : sql(srv) {} +DatabaseHelper::~DatabaseHelper() { + for(const auto& elm : cachedPermissionManagers) + delete elm; + cachedPermissionManagers.clear(); +} + +void DatabaseHelper::tick() { + { + threads::MutexLock l(this->permManagerLock); + auto cpy = this->cachedPermissionManagers; + for(const auto& mgr : cpy){ + //if(mgr->ownLock && system_clock::now() - mgr->lastAccess > minutes(5)) //TODO remove instand delete! + mgr->ownLock.reset(); + if(mgr->manager.expired()){ + this->cachedPermissionManagers.erase(std::find(this->cachedPermissionManagers.begin(), this->cachedPermissionManagers.end(), mgr)); + delete mgr; + } + } + } + + { + threads::MutexLock l(this->propsLock); + auto pcpy = this->cachedProperties; + for(const auto& mgr : pcpy){ + if(mgr->ownLock && system_clock::now() - mgr->lastAccess > minutes(5)) + mgr->ownLock.reset(); + if(mgr->properties.expired()) { + this->cachedProperties.erase(std::find(this->cachedProperties.begin(), this->cachedProperties.end(), mgr)); + delete mgr; + } + } + } +} + +int collectData(deque>* list, int length, char** values, char** columns){ + shared_ptr entry = std::make_shared(); + + for(int index = 0; index < length; index++) + if(strcmp(columns[index], "cldbid") == 0) + entry->cldbid = static_cast(stol(values[index])); + else if(strcmp(columns[index], "clientUid") == 0) + entry->uniqueId = values[index]; + else if(strcmp(columns[index], "firstConnect") == 0) + entry->created = time_point() + seconds(stoll(values[index])); + else if(strcmp(columns[index], "lastConnect") == 0) + entry->lastjoin = time_point() + seconds(stoll(values[index])); + else if(strcmp(columns[index], "connections") == 0) + entry->connections = static_cast(stoi(values[index])); + else if(strcmp(columns[index], "lastName") == 0) + entry->lastName = values[index]; + else if(strcmp(columns[index], "serverId") == 0); + else logError("Invalid db key for manager data. Key: " + string(columns[index])); + + list->push_back(entry); + return 0; +} + +#define MAX_QUERY 32 +std::deque> DatabaseHelper::queryDatabaseInfo(const std::shared_ptr& server, const std::deque& list) { + if(list.empty()) return {}; + + deque> result; + + if(list.size() <= MAX_QUERY){ + std::string query = "SELECT * FROM `clients` WHERE `serverId` = :serverId AND ("; + for(auto elm : list) + query += " `cldbid` = " + to_string(elm) + " OR"; + query = query.substr(0, query.length() - 3) + ")"; + logTrace("[SQL] queryDatabseInfo(...) -> " + query); + auto state = sql::command(this->sql, query, variable{":serverId", server ? server->getServerId() : 0}).query(std::function(collectData), &result); + auto pf = LOG_SQL_CMD; + pf(state); + if(!state) return {}; + } else { + std::deque sub; + do { + sub.insert(sub.begin(), list.begin(), list.begin() + min(list.size(), (size_t) MAX_QUERY)); + list.erase(list.begin(), list.begin() + min(list.size(), (size_t) MAX_QUERY)); + + auto res = this->queryDatabaseInfo(server, sub); + result.insert(result.end(), res.begin(), res.end()); + sub.clear(); + } while(!list.empty()); + } + + return result; +} + +std::deque> DatabaseHelper::queryDatabaseInfoByUid(const std::shared_ptr &server, std::deque list) { + if(list.empty()) return {}; + + deque> result; + + if(list.size() <= MAX_QUERY){ + std::string query = "SELECT * FROM `clients` WHERE `serverId` = :serverId AND ("; + for(const auto &elm : list) + query += " `clientUid` = '" + elm + "' OR"; + query = query.substr(0, query.length() - 3) + ")"; + logTrace("[SQL] queryDatabseInfoByUid(...) -> " + query); + auto state = sql::command(this->sql, query, variable{":serverId", server ? server->getServerId() : 0}).query(function(collectData), &result); + auto pf = LOG_SQL_CMD; + pf(state); + if(!state) return {}; + } else { + std::deque sub; + do { + sub.insert(sub.begin(), list.begin(), list.begin() + min(list.size(), (size_t) MAX_QUERY)); + list.erase(list.begin(), list.begin() + min(list.size(), (size_t) MAX_QUERY)); + + auto res = this->queryDatabaseInfoByUid(server, sub); + result.insert(result.end(), res.begin(), res.end()); + sub.clear(); + } while(!list.empty()); + } + + return result; +} + +bool DatabaseHelper::validClientDatabaseId(const std::shared_ptr& server, ClientDbId cldbid) { return cldbid > 0; } //TODO here check + +void DatabaseHelper::deleteClient(const std::shared_ptr& server, ClientDbId cldbid) { + ServerId sid = static_cast(server ? server->getServerId() : 0); + { + lock_guard lock(permManagerLock); + for(auto permMgr : this->cachedPermissionManagers) + if(permMgr->cldbid == cldbid && permMgr->sid == sid) { + this->cachedPermissionManagers.erase(std::find(this->cachedPermissionManagers.begin(), this->cachedPermissionManagers.end(), permMgr)); + delete permMgr; + break; + } + } + //TODO remove from props cache? + + + auto state = sql::command(this->sql, "DELETE FROM `properties` WHERE `serverId` = :sid AND (`type` = :type1 OR `type` = :type2) AND `id` = :id", variable{":sid", sid}, variable{":type1", property::PROP_TYPE_CONNECTION}, variable{":type2", property::PROP_TYPE_CLIENT}, variable{":id", cldbid}).execute(); + state = sql::command(this->sql, "DELETE FROM `permissions` WHERE `serverId` = :sid AND `type` = :type AND `id` = :id", variable{":sid", sid}, variable{":type", permission::SQL_PERM_USER}, variable{":id", cldbid}).execute(); + state = sql::command(this->sql, "DELETE FROM `clients` WHERE `serverId` = :sid AND `cldbid` = :id", variable{":sid", sid}, variable{":id", cldbid}).execute(); + state = sql::command(this->sql, "DELETE FROM `bannedClients` WHERE `serverId` = :sid AND `invokerDbid` = :id", variable{":sid", sid}, variable{":id", cldbid}).execute(); + state = sql::command(this->sql, "DELETE FROM `assignedGroups` WHERE `serverId` = :sid AND `cldbid` = :id", variable{":sid", sid}, variable{":id", cldbid}).execute(); + + //TODO delete letters + //TODO delete query + //TODO delete complains +} + +struct PermissionArguments { + sql::command& command; + PermissionManager* manager; + TSServer* server; + + inline int serverId() { return this->server ? this->server->getServerId() : 0; } +}; +inline sql::result load_permissions(const std::shared_ptr& server, PermissionManager* manager, sql::command& command) { + auto start = system_clock::now(); + + auto data = PermissionArguments{command, manager, server.get()}; + return command.query([](PermissionArguments* data, int length, char** values, char** names){ + permission::PermissionType key = permission::PermissionType::undefined; + permission::PermissionValue value = 0, granted = 0; + bool negated = false, skipped = false; + std::shared_ptr channel; + + int index; + try { + for(index = 0; index < length; index++) { + if(strcmp(names[index], "permId") == 0) { + key = permission::resolvePermissionData(values[index])->type; + if(key == permission::unknown){ + debugMessage(data->serverId(), "[SQL] Permission entry contains invalid permission type! Type: {} Command: {}", values[index], data->command.sqlCommand()); + return 0; + } + if(key == permission::undefined){ + debugMessage(data->serverId(), "[SQL] Permission entry contains undefined permission type! Type: {} Command: {}", values[index], data->command.sqlCommand()); + return 0; + } + } else if(strcmp(names[index], "channelId") == 0) { + auto channelId = stoull(values[index]); + if(channelId > 0) { + if(!data->server) + logError(data->serverId(), "[SQL] Cant find channel for channel bound permission (No server given)! Command: {} ChannelID: {}", data->command.sqlCommand(), values[index]); + else { + channel = data->server->getChannelTree()->findChannel(channelId); + if(!channel) + logError(data->serverId(), "[SQL] Cant find channel for channel bound permission! Command: {} ChannelID: {}", data->command.sqlCommand(), values[index]); + } + } + } else if(strcmp(names[index], "value") == 0) { + value = stoi(values[index]); + } else if(strcmp(names[index], "grant") == 0) { + granted = stoi(values[index]); + } else if(strcmp(names[index], "flag_skip") == 0) + skipped = strcmp(values[index], "1") == 0; + else if(strcmp(names[index], "flag_negate") == 0) + negated = strcmp(values[index], "1") == 0; + } + } catch(std::exception& ex) { + logError(data->serverId(), "[SQL] Cant load permissions! Failed to parse value! Command: {} Message: {} Key: {} Value: {}", data->command.sqlCommand(), ex.what(),names[index], values[index]); + return 0; + } + + if(key == permission::undefined) { + debugMessage(data->serverId(), "[SQL] Permission entry misses permission type! Command: {}", data->command.sqlCommand()); + return 0; + } + + auto permission = data->manager->registerPermission(key, value, channel); + permission->granted = granted; + permission->value = value; + permission->dbReference = true; + permission->flag_negate = negated; + permission->flag_skip = skipped; + return 0; + }, &data); + + auto end = system_clock::now(); + auto time = end - start; + logTrace(server ? server->getServerId() : 0, "[SQL] load_permissions(\"{}\") took {}ms", command.sqlCommand(), duration_cast(time).count()); +} +inline sql::result load_permissions_v2(const std::shared_ptr& server, v2::PermissionManager* manager, sql::command& command, bool resolve_channel /* only used for client permissions (client channel permissions) */) { + auto start = system_clock::now(); + + auto server_id = server ? server->getServerId() : 0; + return command.query([&](int length, char** values, char** names){ + permission::PermissionType key = permission::PermissionType::undefined; + permission::PermissionValue value = 0, granted = 0; + bool negated = false, skipped = false; + std::shared_ptr channel; + + int index; + try { + for(index = 0; index < length; index++) { + if(strcmp(names[index], "permId") == 0) { + key = permission::resolvePermissionData(values[index])->type; + if(key == permission::unknown){ + debugMessage(server_id, "[SQL] Permission entry contains invalid permission type! Type: {} Command: {}", values[index], command.sqlCommand()); + return 0; + } + if(key == permission::undefined){ + debugMessage(server_id, "[SQL] Permission entry contains undefined permission type! Type: {} Command: {}", values[index], command.sqlCommand()); + return 0; + } + } else if(strcmp(names[index], "channelId") == 0) { + if(resolve_channel) { + auto channelId = stoull(values[index]); + if(channelId > 0) { + if(!server) + logError(server_id, "[SQL] Cant find channel for channel bound permission (No server given)! Command: {} ChannelID: {}", command.sqlCommand(), values[index]); + else { + channel = server->getChannelTree()->findChannel(channelId); + if(!channel) + logError(server_id, "[SQL] Cant find channel for channel bound permission! Command: {} ChannelID: {}", command.sqlCommand(), values[index]); + } + } + } + } else if(strcmp(names[index], "value") == 0) { + value = stoi(values[index]); + } else if(strcmp(names[index], "grant") == 0) { + granted = stoi(values[index]); + } else if(strcmp(names[index], "flag_skip") == 0) + skipped = strcmp(values[index], "1") == 0; + else if(strcmp(names[index], "flag_negate") == 0) + negated = strcmp(values[index], "1") == 0; + } + } catch(std::exception& ex) { + logError(server_id, "[SQL] Cant load permissions! Failed to parse value! Command: {} Message: {} Key: {} Value: {}", command.sqlCommand(), ex.what(),names[index], values[index]); + return 0; + } + + if(key == permission::undefined) { + debugMessage(server_id, "[SQL] Permission entry misses permission type! Command: {}", command.sqlCommand()); + return 0; + } + + if(!resolve_channel || !channel) + manager->load_permission(key, {value, granted}, negated, skipped, value != permNotGranted, granted != permNotGranted); + else + manager->load_permission(key, {value, granted}, channel->channelId(), negated, skipped, value != permNotGranted, granted != permNotGranted); + return 0; + }); + + auto end = system_clock::now(); + auto time = end - start; + logTrace(server_id, "[SQL] load_permissions(\"{}\") took {}ms", command.sqlCommand(), duration_cast(time).count()); +} + +#define UPDATE_COMMAND "UPDATE `permissions` SET `value` = :value, `grant` = :grant, `flag_skip` = :flag_skip, `flag_negate` = :flag_negate WHERE `serverId` = :serverId AND `type` = :type AND `id` = :id AND `permId` = :permId AND `channelId` = :chId" +#define INSERT_COMMAND "INSERT INTO `permissions` (`serverId`, `type`, `id`, `channelId`, `permId`, `value`, `grant`, `flag_skip`, `flag_negate`) VALUES (:serverId, :type, :id, :chId, :permId, :value, :grant, :flag_skip, :flag_negate)" +#define DELETE_COMMAND "DELETE FROM `permissions` WHERE `serverId` = :serverId AND `type` = :type AND `id` = :id AND `permId` = :permId AND `channelId` = :chId" + +std::shared_ptr DatabaseHelper::loadClientPermissionManager(const std::shared_ptr& server, ClientDbId cldbid) { + auto server_id = server ? server->getServerId() : 0; +#ifndef DISABLE_CACHING + { + lock_guard lock(permManagerLock); + for(auto permMgr : this->cachedPermissionManagers) + if(permMgr->cldbid == cldbid && permMgr->sid == (server ? server->getServerId() : 0)){ + auto ptr = permMgr->manager.lock(); + if(!ptr){ + this->cachedPermissionManagers.erase(std::find(this->cachedPermissionManagers.begin(), this->cachedPermissionManagers.end(), permMgr)); + delete permMgr; + break; + } + permMgr->lastAccess = system_clock::now(); + return ptr; + } + } +#endif + + logTrace(server_id, "[Permission] Loading client permission manager for client {}", cldbid); + auto pMgr = std::make_shared(); + bool loaded = false; + if(this->use_startup_cache && server) { + shared_ptr entry; + { + threads::MutexLock lock(this->startup_lock); + for(const auto& entries : this->startup_entries) { + if(entries->sid == server->getServerId()) { + entry = entries; + break; + } + } + } + if(entry) { + for(const auto& perm : entry->permissions) { + if(perm->type == permission::SQL_PERM_USER && perm->id == cldbid) { + auto channel = perm->channelId > 0 ? server->getChannelTree()->findChannel(perm->channelId) : nullptr; + + if(channel) + pMgr->load_permission(perm->permission->type, {perm->value, perm->grant}, channel->channelId(), perm->flag_negate, perm->flag_skip, perm->value != permNotGranted, perm->grant != permNotGranted); + else + pMgr->load_permission(perm->permission->type, {perm->value, perm->grant}, perm->flag_negate, perm->flag_skip, perm->value != permNotGranted, perm->grant != permNotGranted); + } + } + loaded = true; + } + } + if(!loaded) { + auto command = sql::command(this->sql, "SELECT `permId`, `value`, `channelId`, `grant`, `flag_skip`, `flag_negate` FROM `permissions` WHERE `serverId` = :serverId AND `type` = :type AND `id` = :id", + variable{":serverId", server ? server->getServerId() : 0}, + variable{":type", permission::SQL_PERM_USER}, + variable{":id", cldbid}); + LOG_SQL_CMD(load_permissions_v2(server, pMgr.get(), command, true)); + } + + +#ifndef DISABLE_CACHING + this->permManagerLock.lock(); + auto entry = new CachedPermissionManager(); + entry->sid = server_id; + entry->manager = pMgr; + entry->ownLock = pMgr; + entry->cldbid = cldbid; + entry->lastAccess = system_clock::now(); + this->cachedPermissionManagers.push_back(entry); + this->permManagerLock.unlock(); +#endif + return pMgr; +} + + +void DatabaseHelper::saveClientPermissions(const std::shared_ptr &server, ts::ClientDbId client_dbid, const std::shared_ptr &permissions) { + const auto updates = permissions->flush_db_updates(); + if(updates.empty()) + return; + + auto server_id = server ? server->getServerId() : 0; + for(auto& update : updates) { + std::string query = update.flag_delete ? DELETE_COMMAND : (update.flag_db ? UPDATE_COMMAND : INSERT_COMMAND); + + auto permission_data = permission::resolvePermissionData(update.permission); + logTrace(server_id, "[CHANNEL] Updating client permission for client {}: {}. New value: {}. New grant: {}. Query: {}", + client_dbid, + permission_data->name, + update.values.value, + update.values.grant, + query + ); + sql::command(this->sql, query, + variable{":serverId", server ? server->getServerId() : 0}, + variable{":id", client_dbid}, + variable{":chId", 0}, + variable{":type", permission::SQL_PERM_USER}, + + variable{":permId", permission_data->name}, + variable{":value", update.values.value}, + variable{":grant", update.values.grant}, + variable{":flag_skip", update.flag_skip}, + variable{":flag_negate", update.flag_negate}) + .executeLater().waitAndGetLater(LOG_SQL_CMD, {-1, "future error"}); + } +} + + +std::shared_ptr DatabaseHelper::loadGroupPermissions(const std::shared_ptr& server, ts::GroupId group_id) { + auto result = std::make_shared(); + if(this->use_startup_cache && server) { + shared_ptr entry; + { + threads::MutexLock lock(this->startup_lock); + for(const auto& entries : this->startup_entries) { + if(entries->sid == server->getServerId()) { + entry = entries; + break; + } + } + } + if(entry) { + for(const auto& perm : entry->permissions) { + if(perm->type == permission::SQL_PERM_GROUP && perm->id == group_id) { + result->load_permission(perm->permission->type, {perm->value, perm->grant}, perm->flag_negate, perm->flag_skip, perm->value != permNotGranted, perm->grant != permNotGranted); + } + } + return result; + } + } + + //7931 + auto command = sql::command(this->sql, "SELECT `channelId`, `permId`, `value`, `grant`, `flag_skip`, `flag_negate` FROM `permissions` WHERE `serverId` = :serverId AND `type` = :type AND `id` = :id", + variable{":serverId", server ? server->getServerId() : 0}, + variable{":type", permission::SQL_PERM_GROUP}, + variable{":id", group_id}); + LOG_SQL_CMD(load_permissions_v2(server, result.get(), command, false)); + return result; +} + +void DatabaseHelper::saveGroupPermissions(const std::shared_ptr &server, ts::GroupId group_id, const std::shared_ptr &permissions) { + const auto updates = permissions->flush_db_updates(); + if(updates.empty()) + return; + + auto server_id = server ? server->getServerId() : 0; + for(auto& update : updates) { + std::string query = update.flag_delete ? DELETE_COMMAND : (update.flag_db ? UPDATE_COMMAND : INSERT_COMMAND); + + auto permission_data = permission::resolvePermissionData(update.permission); + logTrace(server_id, "[CHANNEL] Updating group permission for group {}: {}. New value: {}. New grant: {}. Query: {}", + group_id, + permission_data->name, + update.values.value, + update.values.grant, + query + ); + sql::command(this->sql, query, + variable{":serverId", server ? server->getServerId() : 0}, + variable{":id", group_id}, + variable{":chId", 0}, + variable{":type", permission::SQL_PERM_GROUP}, + + variable{":permId", permission_data->name}, + variable{":value", update.values.value}, + variable{":grant", update.values.grant}, + variable{":flag_skip", update.flag_skip}, + variable{":flag_negate", update.flag_negate}) + .executeLater().waitAndGetLater(LOG_SQL_CMD, {-1, "future error"}); + } +} + +std::shared_ptr DatabaseHelper::loadPlaylistPermissions(const std::shared_ptr &server, ts::PlaylistId playlist_id) { + shared_ptr result; + if(this->use_startup_cache && server) { + shared_ptr entry; + { + threads::MutexLock lock(this->startup_lock); + for(const auto& entries : this->startup_entries) { + if(entries->sid == server->getServerId()) { + entry = entries; + break; + } + } + } + if(entry) { + result = std::make_shared(); + + for(const auto& perm : entry->permissions) { + if(perm->type == permission::SQL_PERM_PLAYLIST && perm->id == playlist_id) { + auto permission = result->registerPermission(perm->permission, perm->value, server->getChannelTree()->findChannel(perm->channelId)); + permission->granted = perm->grant; + permission->value = perm->value; + permission->dbReference = true; + permission->flag_negate = perm->flag_negate; + permission->flag_skip = perm->flag_skip; + } + } + } + } + + if(!result) { + result = std::make_shared(); + auto command = sql::command(this->sql, "SELECT `channelId`, `permId`, `value`, `grant`, `flag_skip`, `flag_negate` FROM `permissions` WHERE `serverId` = :serverId AND `type` = :type AND `id` = :id", + variable{":serverId", server ? server->getServerId() : 0}, + variable{":type", permission::SQL_PERM_PLAYLIST}, + variable{":id", playlist_id}); + LOG_SQL_CMD(load_permissions(server, result.get(), command)); + } + + assert(result); + weak_ptr weak_server = server; + auto server_id = server ? server->getServerId() : 0; + + result->registerUpdateHandler([&, weak_server, server_id, playlist_id](std::shared_ptr permission) { + auto server = weak_server.lock(); + if(!server && server_id != 0) { + logError(server_id, "Tried to update a playlist permission of a expired server!"); + return; + } + + std::string query; + + /* + if(permission->deleted){ + query = "DELETE FROM `permissions` WHERE `serverId` = :serverId AND `id` = :id AND `type`= :type AND `permId`= :permId AND `channelId` = :chId"; + } else + */ + if(permission->dbReference){ + query = UPDATE_COMMAND; + } else { + permission->dbReference = true; + query = INSERT_COMMAND; + } + + logTrace(server ? server->getServerId() : 0, "[SQL] Executing group permission command: {}", query); + sql::command(this->sql, query, + variable{":serverId", server ? server->getServerId() : 0}, + variable{":id", playlist_id}, + variable{":type", permission::SQL_PERM_PLAYLIST}, + variable{":chId", permission->channelId()}, + + variable{":permId", permission->type->name}, + variable{":value", permission->value}, + variable{":grant", permission->granted}, + variable{":flag_skip", permission->flag_skip}, + variable{":flag_negate", permission->flag_negate}) + .executeLater().waitAndGetLater(LOG_SQL_CMD, {-1, "failed to save playlist permission " + to_string(playlist_id)}); + }); + + return result; +} + +std::shared_ptr DatabaseHelper::loadChannelPermissions(const std::shared_ptr& server, ts::ChannelId channel) { + auto result = std::make_shared(); + if(this->use_startup_cache && server) { + shared_ptr entry; + { + threads::MutexLock lock(this->startup_lock); + for(const auto& entries : this->startup_entries) { + if(entries->sid == server->getServerId()) { + entry = entries; + break; + } + } + } + if(entry) { + for(const auto& perm : entry->permissions) { + if(perm->type == permission::SQL_PERM_CHANNEL && perm->channelId == channel) { + result->load_permission(perm->permission->type, {perm->value, perm->grant}, perm->flag_negate, perm->flag_skip, perm->value != permNotGranted, perm->grant != permNotGranted); + } + } + return result; + } + } + + auto command = sql::command(sql, "SELECT `permId`, `value`, `grant`, `flag_skip`, `flag_negate` FROM `permissions` WHERE `serverId` = :serverId AND `channelId` = :chid AND `type` = :type AND `id` = :id", + variable{":serverId", server ? server->getServerId() : 0}, + variable{":chid", channel}, + variable{":id", 0}, + variable{":type", permission::SQL_PERM_CHANNEL}); + LOG_SQL_CMD(load_permissions_v2(server, result.get(), command, false)); + return result; +} + +void DatabaseHelper::saveChannelPermissions(const std::shared_ptr &server, ts::ChannelId channel_id, const std::shared_ptr &permissions) { + const auto updates = permissions->flush_db_updates(); + if(updates.empty()) + return; + + auto server_id = server ? server->getServerId() : 0; + for(auto& update : updates) { + std::string query = update.flag_delete ? DELETE_COMMAND : (update.flag_db ? UPDATE_COMMAND : INSERT_COMMAND); + + auto permission_data = permission::resolvePermissionData(update.permission); + logTrace(server_id, "[CHANNEL] Updating channel permission for channel {}: {}. New value: {}. New grant: {}. Query: {}", + channel_id, + permission_data->name, + update.values.value, + update.values.grant, + query + ); + sql::command(this->sql, query, + variable{":serverId", server ? server->getServerId() : 0}, + variable{":id", 0}, + variable{":chId", channel_id}, + variable{":type", permission::SQL_PERM_CHANNEL}, + + variable{":permId", permission_data->name}, + variable{":value", update.values.value}, + variable{":grant", update.values.grant}, + variable{":flag_skip", update.flag_skip}, + variable{":flag_negate", update.flag_negate}) + .executeLater().waitAndGetLater(LOG_SQL_CMD, {-1, "future error"}); + } +} + +#define DBSAVE_MUSIC_BOT (type == CLIENT_MUSIC ? PROP_DB_SAVE : 0) +void DatabaseHelper::assign_default_properties_client(Properties *properties, ClientType type){ + Properties& _properties = *properties; + + _properties.register_property_type(); + _properties.register_property_type(); + + if(type == ClientType::CLIENT_MUSIC){ + //_properties.registerProperty("channel_last", 0, PROP_DB_SAVE); + //_properties.registerProperty("bot_owner", 0, PROP_TEMP); //saved in bot table + + //_properties.registerProperty("music_volume", 1.f, PROP_DB_SAVE); + //_properties.registerProperty("music_track_id", 0, PROP_CLIENT_VARIABLE | PROP_CLIENT_VIEW_INFO | PROP_NEW); + //_properties.registerProperty("music_player_state", 0, PROP_CLIENT_VARIABLE | PROP_CLIENT_VIEW_INFO | PROP_NEW); + + _properties[property::CLIENT_INPUT_HARDWARE] = true; + _properties[property::CLIENT_OUTPUT_HARDWARE] = true; + } else if(type == ClientType::CLIENT_QUERY) { + _properties[property::CLIENT_INPUT_HARDWARE] = true; + _properties[property::CLIENT_OUTPUT_HARDWARE] = true; + } + /* + _properties.registerProperty("client_type", type, PROP_CLIENT_VIEW_INFO); + _properties.registerProperty("client_type_exact", type, PROP_CLIENT_VIEW_INFO); + + _properties.registerProperty("clid", 0, PROP_SERVER_BOUND | PROP_CLIENT_VIEW_INFO | PROP_CLIENT_VARIABLE); + _properties.registerProperty("client_database_id", 0, PROP_CLIENT_VIEW_INFO); + _properties.registerProperty("hwid", "", PROP_PRIVATE | PROP_DB_SAVE); + _properties.registerProperty("connection_client_ip", "undefined", PROP_PRIVATE | PROP_DB_SAVE | PROP_SNAPSHOT); + + _properties.registerProperty("client_version", "unknown", PROP_CLIENT_VARIABLE | PROP_CLIENT_VIEW_INFO | PROP_DB_SAVE | PROP_SNAPSHOT); + _properties.registerProperty("client_platform", "unknown", PROP_CLIENT_VARIABLE | PROP_CLIENT_VIEW_INFO | PROP_DB_SAVE | PROP_SNAPSHOT); + _properties.registerProperty("client_login_name", "", PROP_CLIENT_VARIABLE); + _properties.registerProperty("client_created", 0, PROP_SERVER_BOUND | PROP_CLIENT_VARIABLE | DBSAVE_MUSIC_BOT | PROP_SNAPSHOT); + + _properties.registerProperty("client_lastconnected", 0, PROP_SERVER_BOUND | DBSAVE_MUSIC_BOT | PROP_CLIENT_VARIABLE | PROP_SNAPSHOT); + _properties.registerProperty("client_totalconnections", 0, PROP_SERVER_BOUND | PROP_CLIENT_VARIABLE | PROP_CLIENT_VIEW_INFO | PROP_SNAPSHOT); + _properties.registerProperty("client_month_bytes_uploaded", 0, PROP_SERVER_BOUND | PROP_CLIENT_VARIABLE); + _properties.registerProperty("client_month_bytes_downloaded", 0, PROP_SERVER_BOUND | PROP_CLIENT_VARIABLE); + _properties.registerProperty("client_total_bytes_uploaded", 0, PROP_SERVER_BOUND | PROP_CLIENT_VARIABLE); + _properties.registerProperty("client_total_bytes_downloaded", 0, PROP_SERVER_BOUND | PROP_CLIENT_VARIABLE); + + _properties.registerProperty("connection_server2client_packetloss_keepalive", 0, PROP_SERVER_BOUND); + _properties.registerProperty("connection_server2client_packetloss_control", 0, PROP_SERVER_BOUND); + _properties.registerProperty("connection_server2client_packetloss_speech", 0, PROP_SERVER_BOUND); + _properties.registerProperty("connection_server2client_packetloss_total", 0, PROP_SERVER_BOUND); + + _properties.registerProperty("client_unique_identifier", "", PROP_CLIENT_VIEW_INFO | PROP_SNAPSHOT); + _properties.registerProperty("client_nickname", "", PROP_CLIENT_VIEW_INFO | DBSAVE_MUSIC_BOT | PROP_SNAPSHOT); + _properties.registerProperty("client_nickname_phonetic", "", PROP_CLIENT_VIEW_INFO); + + _properties.registerProperty("client_input_muted", false, PROP_CLIENT_VIEW_INFO); + _properties.registerProperty("client_output_muted", false, PROP_CLIENT_VIEW_INFO); + _properties.registerProperty("client_outputonly_muted", false, PROP_CLIENT_VIEW_INFO); + + _properties.registerProperty("client_input_hardware", false, PROP_CLIENT_VIEW_INFO); + _properties.registerProperty("client_output_hardware", false, PROP_CLIENT_VIEW_INFO); + _properties.registerProperty("client_is_recording", false, PROP_CLIENT_VIEW_INFO); + + _properties.registerProperty("client_meta_data", "", PROP_SERVER_BOUND | PROP_CLIENT_VIEW_INFO); //unused + + _properties.registerProperty("client_channel_group_id", "0", PROP_SERVER_BOUND | PROP_CLIENT_VIEW_INFO); + _properties.registerProperty("client_servergroups", "0", PROP_SERVER_BOUND | PROP_CLIENT_VIEW_INFO); + + _properties.registerProperty("client_away", false, PROP_CLIENT_VIEW_INFO); + _properties.registerProperty("client_away_message", "", PROP_CLIENT_VIEW_INFO);CLIENT_DESCRIPTION + + _properties.registerProperty("client_flag_avatar", "", PROP_SERVER_BOUND | PROP_CLIENT_VIEW_INFO | PROP_DB_SAVE); //Some random uuid. File id or stuff like that? + _properties.registerProperty("client_description", ts::config::messages::defaultClientDescription, PROP_SERVER_BOUND | PROP_CLIENT_VIEW_INFO | PROP_DB_SAVE | PROP_SNAPSHOT); + + _properties.registerProperty("client_talk_power", 0, PROP_SERVER_BOUND | PROP_CLIENT_VIEW_INFO); + _properties.registerProperty("client_talk_request", false, PROP_SERVER_BOUND | PROP_CLIENT_VIEW_INFO); + _properties.registerProperty("client_talk_request_msg", "", PROP_SERVER_BOUND | PROP_CLIENT_VIEW_INFO); + + _properties.registerProperty("client_is_talker", false, PROP_SERVER_BOUND | PROP_CLIENT_VIEW_INFO); + _properties.registerProperty("client_is_priority_speaker", false, PROP_SERVER_BOUND | PROP_CLIENT_VIEW_INFO); + _properties.registerProperty("client_is_channel_commander", false, PROP_SERVER_BOUND | PROP_CLIENT_VIEW_INFO); + + _properties.registerProperty("client_unread_messages", 0, PROP_SERVER_BOUND | PROP_CLIENT_VIEW_INFO); + _properties.registerProperty("client_needed_serverquery_view_power", 0, PROP_SERVER_BOUND | PROP_CLIENT_VIEW_INFO); + _properties.registerProperty("client_icon_id", 0, PROP_SERVER_BOUND | PROP_CLIENT_VIEW_INFO | PROP_DB_SAVE); + + _properties.registerProperty("client_country", ts::config::geo::countryFlag, PROP_CLIENT_VIEW_INFO); //Country shortcuts + _properties.registerProperty("client_channel_group_inherited_channel_id", 0, PROP_SERVER_BOUND | PROP_CLIENT_VIEW_INFO); + _properties.registerProperty("client_badges", "", PROP_CLIENT_VIEW_INFO | PROP_DB_SAVE); + + _properties.registerProperty("client_teaforum_id", 0, PROP_CLIENT_VIEW_INFO | PROP_NEW); + _properties.registerProperty("client_teaforum_name", "", PROP_CLIENT_VIEW_INFO | PROP_NEW); + + //Client default channel settings + _properties.registerProperty("client_default_channel", "", PROP_PRIVATE); + _properties.registerProperty("client_default_channel_password", "", PROP_PRIVATE); + */ +} + +bool DatabaseHelper::assignDatabaseId(sql::SqlManager *sql, ServerId id, std::shared_ptr cl) { + cl->loadDataForCurrentServer(); + if(cl->getClientDatabaseId() == 0){ //Client does not exist + ClientDbId cldbid = 0; + auto res = sql::command(sql, "SELECT `cldbid` FROM `clients` WHERE `serverId` = 0 AND `clientUid` = :cluid", variable{":cluid", cl->getUid()}).query([](ClientDbId* ptr, int length, char** values, char** names){ + *ptr = static_cast(stoll(values[0])); + return 0; + }, &cldbid); + auto pf = LOG_SQL_CMD; + pf(res); + if(!res) return false; + + auto insertTemplate = sql::model(sql, "INSERT INTO `clients` (`serverId`, `cldbId`, `clientUid`, `lastName`,`firstConnect`,`lastConnect`, `connections`) VALUES (:serverId, :cldbid, :cluid, :name, :fconnect, :lconnect, :connections)", + variable{":cluid", cl->getUid()}, variable{":name", cl->getDisplayName()}, variable{":fconnect", duration_cast(system_clock::now().time_since_epoch()).count()}, variable{":lconnect", 0}, variable{":connections", 0}); + if(cldbid == 0){ //Completly new user + res = sql::command(sql, "SELECT `cldbid` FROM `clients` WHERE `serverId` = 0 ORDER BY `cldbid` DESC LIMIT 1").query([](ClientDbId* ptr, int length, char** values, char** names){ + *ptr = static_cast(stoll(values[0])); + return 0; + }, &cldbid); + pf(res); + if(!res) return false; + + cldbid += 1; + res = insertTemplate.command().values(variable{":serverId", 0}, variable{":cldbid", cldbid}).execute(); //Insert global + pf(res); + if(!res) return false; + debugMessage(id, "Having new instance user on server {}. (Database ID: {} Name: {})", id, cldbid, cl->getDisplayName()); + } else { + debugMessage(id, "Having new server user on server {}. (Database ID: {} Name: {})", id, cldbid, cl->getDisplayName()); + } + + if(id != 0){ //Else already inserted + res = insertTemplate.command().values(variable{":serverId", id}, variable{":cldbid", cldbid}).execute(); + pf(res); + if(!res) return false; + } + + return assignDatabaseId(sql, id, cl); + } + + logTrace(id, "Loaded client from database. Database id: {} Unique id: {}", cl->getClientDatabaseId(), cl->getUid()); + return true; +} + +inline sql::result load_properties(ServerId sid, deque>& properties, sql::command& command) { + auto start = system_clock::now(); + + auto result = command.query([&](int length, string* values, string* names) { + string key, value; + property::PropertyType type = property::PROP_TYPE_UNKNOWN; + + for(int index = 0; index < length; index++) { + try { + if(names[index] == "key") key = values[index]; + else if(names[index] == "value") value = values[index]; + else if(names[index] == "type") type = (property::PropertyType) stoll(values[index]); + } catch(const std::exception& ex) { + logError(sid, "Failed to load parse property \"{}\". key: {}, value: {}, message: {}", key,names[index],values[index],ex.what()); + return 0; + } + } + + const auto &info = property::impl::info_key(type, key); + if(info->name == "undefined") { + logError(sid, "Found unknown property in database! ({})", key); + return 0; + } + + /* + auto prop = properties->operator[](info); + prop = value; + prop.setModified(true); + prop.setDbReference(true); + */ + + auto data = make_unique(); + data->value = value; + data->type = info; + properties.push_back(move(data)); + return 0; + }); + + auto end = system_clock::now(); + auto time = end - start; + logTrace(sid, "[SQL] load_properties(\"{}\") needs {}ms", command.sqlCommand(), duration_cast(time).count()); + return result; +} + +std::shared_ptr DatabaseHelper::loadServerProperties(const std::shared_ptr& server) { + auto props = std::make_shared(); + + props->register_property_type(); + (*props)[property::VIRTUALSERVER_HOST] = config::binding::DefaultVoiceHost; + (*props)[property::VIRTUALSERVER_WEB_HOST] = config::binding::DefaultWebHost; + + bool loaded = false; + if(use_startup_cache && server) { + shared_ptr entry; + { + threads::MutexLock lock(this->startup_lock); + for(const auto& entries : this->startup_entries) { + if(entries->sid == server->getServerId()) { + entry = entries; + break; + } + } + } + if(entry) { + for(const auto& prop : entry->properties) { + if(prop->type == property::PROP_TYPE_SERVER && prop->id == 0) { + auto p = (*props)[prop->info]; + p = prop->value; + p.setModified(true); + p.setDbReference(true); + } + } + loaded = true; + } + } + if(!loaded) { + auto command = sql::command(this->sql, "SELECT `key`, `value`, `type` FROM properties WHERE `serverId` = :serverId AND `type` = :type", variable{":serverId", server ? server->getServerId() : 0}, variable{":type", property::PropertyType::PROP_TYPE_SERVER}); + + deque> property_list; + LOG_SQL_CMD(load_properties(server ? server->getServerId() : 0, property_list, command)); + for(const auto& entry : property_list) { + auto prop = props->operator[](entry->type); + prop = entry->value; + prop.setModified(true); + prop.setDbReference(true); + } + } + + weak_ptr weak = server; + ServerId serverId = server ? server->getServerId() : 0; + props->registerNotifyHandler([&, serverId, weak](Property& prop){ + if((prop.type().flags & property::FLAG_SAVE) == 0) { + prop.setModified(false); + return; + } + auto weak_server = weak.lock(); + if(!weak_server && serverId != 0) return; + + string sql; + if(prop.hasDbReference()) + sql = "UPDATE `properties` SET `value` = :value WHERE `serverId` = :sid AND `type` = :type AND `id` = :id AND `key` = :key"; + else { + prop.setDbReference(true); + sql = "INSERT INTO `properties` (`serverId`, `type`, `id`, `key`, `value`) VALUES (:sid, :type, :id, :key, :value)"; + } + + logTrace(serverId, "Updating server property: " + prop.type().name + ". New value: " + prop.value() + ". Query: " + sql); + sql::command(this->sql, sql, + variable{":sid", serverId}, + variable{":type", property::PropertyType::PROP_TYPE_SERVER}, + variable{":id", 0}, + variable{":key", prop.type().name}, + variable{":value", prop.value()} + ).executeLater().waitAndGetLater(LOG_SQL_CMD, sql::result{1, "future failed"}); + }); + return props; +} + +std::shared_ptr DatabaseHelper::loadPlaylistProperties(const std::shared_ptr& server, PlaylistId id) { + auto props = std::make_shared(); + + props->register_property_type(); + (*props)[property::PLAYLIST_ID] = id; + + bool loaded = false; + if(use_startup_cache && server) { + shared_ptr entry; + { + threads::MutexLock lock(this->startup_lock); + for(const auto& entries : this->startup_entries) { + if(entries->sid == server->getServerId()) { + entry = entries; + break; + } + } + } + if(entry) { + for(const auto& prop : entry->properties) { + if(prop->type == property::PROP_TYPE_PLAYLIST && prop->id == id) { + auto p = (*props)[prop->info]; + p = prop->value; + p.setModified(true); + p.setDbReference(true); + } + } + loaded = true; + } + } + if(!loaded) { + auto command = sql::command(this->sql, "SELECT `key`, `value`, `type` FROM properties WHERE `serverId` = :serverId AND `type` = :type AND `id` = :id", variable{":serverId", server ? server->getServerId() : 0}, variable{":type", property::PropertyType::PROP_TYPE_PLAYLIST}, variable{":id", id}); + + deque> property_list; + LOG_SQL_CMD(load_properties(server ? server->getServerId() : 0, property_list, command)); + for(const auto& entry : property_list) { + auto prop = props->operator[](entry->type); + prop = entry->value; + prop.setModified(true); + prop.setDbReference(true); + } + } + + weak_ptr weak = server; + ServerId serverId = server ? server->getServerId() : 0; + props->registerNotifyHandler([&, serverId, weak, id](Property& prop){ + if((prop.type().flags & property::FLAG_SAVE) == 0) { + prop.setModified(false); + return; + } + auto weak_server = weak.lock(); + if(!weak_server && serverId != 0) return; + + string sql; + if(prop.hasDbReference()) + sql = "UPDATE `properties` SET `value` = :value WHERE `serverId` = :sid AND `type` = :type AND `id` = :id AND `key` = :key"; + else { + prop.setDbReference(true); + sql = "INSERT INTO `properties` (`serverId`, `type`, `id`, `key`, `value`) VALUES (:sid, :type, :id, :key, :value)"; + } + + logTrace(serverId, "Updating playlist property for {}. Key: {} Value: {}", id, prop.type().name, prop.value()); + sql::command(this->sql, sql, + variable{":sid", serverId}, + variable{":type", property::PropertyType::PROP_TYPE_PLAYLIST}, + variable{":id", id}, + variable{":key", prop.type().name}, + variable{":value", prop.value()} + ).executeLater().waitAndGetLater(LOG_SQL_CMD, sql::result{1, "future failed"}); + }); + return props; +} + +std::shared_ptr DatabaseHelper::loadChannelProperties(const shared_ptr& server, ChannelId channel) { + ServerId serverId = server ? server->getServerId() : 0U; + auto props = std::make_shared(); + + props->register_property_type(); + if(server) { + props->operator[](property::CHANNEL_TOPIC) = server->properties()[property::VIRTUALSERVER_DEFAULT_CHANNEL_TOPIC].value(); + props->operator[](property::CHANNEL_DESCRIPTION) = server->properties()[property::VIRTUALSERVER_DEFAULT_CHANNEL_DESCRIPTION].value(); + } + + bool loaded = false; + if(use_startup_cache && server) { + shared_ptr entry; + { + threads::MutexLock lock(this->startup_lock); + for(const auto& entries : this->startup_entries) { + if(entries->sid == server->getServerId()) { + entry = entries; + break; + } + } + } + if(entry) { + for(const auto& prop : entry->properties) { + if(prop->type == property::PROP_TYPE_CHANNEL && prop->id == channel) { + auto p = (*props)[prop->info]; + p = prop->value; + p.setModified(true); + p.setDbReference(true); + } + } + loaded = true; + } + } + if(!loaded) { + auto command = sql::command(this->sql, "SELECT `key`, `value`, `type` FROM properties WHERE `serverId` = :serverId AND `type` = :type AND `id` = :id", variable{":serverId", serverId}, variable{":type", property::PropertyType::PROP_TYPE_CHANNEL}, variable{":id", channel}); + + deque> property_list; + LOG_SQL_CMD(load_properties(serverId, property_list, command)); + for(const auto& entry : property_list) { + auto prop = props->operator[](entry->type); + prop = entry->value; + prop.setModified(true); + prop.setDbReference(true); + } + } + + weak_ptr weak = server; + props->registerNotifyHandler([&, weak, serverId, channel](Property& prop){ + auto weak_server = weak.lock(); + if(!weak_server && serverId != 0) + return; + if((prop.type().flags & property::FLAG_SAVE) == 0) + return; + if(!prop.isModified()) + return; + + std::string query; + if(prop.type() == property::CHANNEL_PID){ + query = "UPDATE `channels` SET `parentId` = :value WHERE `serverId` = :serverId AND `channelId` = :id"; + } else if(!prop.hasDbReference()){ + query = "INSERT INTO `properties` (`serverId`, `type`, `id`, `key`, `value`) VALUES (:serverId, :type, :id, :key, :value)"; + } else { + query = "UPDATE `properties` SET `value` = :value WHERE `serverId` = :serverId AND `id` = :id AND `key` = :key AND `type` = :type"; + } + logTrace(serverId, "[CHANNEL] Updating channel property for channel {}: {}. New value: '{}'. Query: {}", channel, prop.type().name, prop.value(), query); + + sql::command(this->sql, query, + variable{":serverId", serverId}, + variable{":id", channel}, + variable{":type", property::PropertyType::PROP_TYPE_CHANNEL}, + variable{":key", prop.type().name}, + variable{":value", prop.value()} + ).executeLater().waitAndGetLater(LOG_SQL_CMD, {-1, "future error"}); + prop.setModified(false); + prop.setDbReference(true); + }); + + return props; +} + +std::shared_ptr DatabaseHelper::loadClientProperties(const std::shared_ptr& server, ClientDbId cldbid, ClientType type) { + auto props = std::make_shared(); + assign_default_properties_client(props.get(), type); + if(server) { + props->operator[](property::CLIENT_DESCRIPTION) = server->properties()[property::VIRTUALSERVER_DEFAULT_CLIENT_DESCRIPTION].value(); + } + bool loaded = false; + if(use_startup_cache && server) { + shared_ptr entry; + { + threads::MutexLock lock(this->startup_lock); + for(const auto& entries : this->startup_entries) { + if(entries->sid == server->getServerId()) { + entry = entries; + break; + } + } + } + if(entry) { + for(const auto& prop : entry->properties) { + if(prop->id == cldbid && (prop->type == property::PROP_TYPE_CLIENT || prop->type == property::PROP_TYPE_CONNECTION)) { + auto p = (*props)[prop->info]; + p = prop->value; + p.setModified(true); + p.setDbReference(true); + } + } + loaded = true; + } + } + if(!loaded) { + auto command = sql::command(this->sql, "SELECT `key`, `value`, `type` FROM properties WHERE `serverId` = :serverId AND (`type` = :type1 OR `type` = :type2) AND `id` = :id", variable{":serverId", server ? server->getServerId() : 0}, variable{":type1", property::PropertyType::PROP_TYPE_CONNECTION}, variable{":type2", property::PropertyType::PROP_TYPE_CLIENT}, variable{":id", cldbid}); + + deque> property_list; + LOG_SQL_CMD(load_properties(server ? server->getServerId() : 0, property_list, command)); + for(const auto& entry : property_list) { + auto prop = props->operator[](entry->type); + prop = entry->value; + prop.setModified(true); + prop.setDbReference(true); + } + } + + + weak_ptr weak_server = server; + auto server_id = server ? server->getServerId() : 0; + props->registerNotifyHandler([&, weak_server, server_id, cldbid](Property& prop){ //General save + auto server = weak_server.lock(); + if(!server && server_id != 0) { + logError(server_id, "Tried to update client permissions of a expired server!"); + return; + } + + if(!prop.isModified()) return; + if((prop.type().flags & property::FLAG_SAVE) == 0 && (prop.type().flags & property::FLAG_SAVE_MUSIC) == 0) { + logTrace(server ? server->getServerId() : 0, "[Property] Not saving property '" + prop.type().name + "', changed for " + to_string(cldbid) + " (New value: " + prop.value() + ")"); + return; + } + if(!prop.get_handle()) return; + if(!prop.get_handle()->isSaveEnabled()) return; + if(!prop.hasDbReference() && (prop.default_value() == prop.value())) return; //No changes to default value + prop.setModified(false); + + std::string sql; + if(prop.hasDbReference()) + sql = "UPDATE `properties` SET `value` = :value WHERE `serverId` = :serverId AND `type` = :type AND `id` = :id AND `key` = :key"; + else { + prop.setDbReference(true); + sql = "INSERT INTO `properties` (`serverId`, `type`, `id`, `key`, `value`) VALUES (:serverId, :type, :id, :key, :value)"; + } + logTrace(server ? server->getServerId() : 0, "[Property] Changed property in db key: " + prop.type().name + " value: " + prop.value()); + sql::command(this->sql, sql, + variable{":serverId", server ? server->getServerId() : 0}, + variable{":type", prop.type().type_property}, + variable{":id", cldbid}, + variable{":key", prop.type().name}, + variable{":value", prop.value()} + ).executeLater().waitAndGetLater(LOG_SQL_CMD, sql::result{1, "future failed"}); + }); + + props->registerNotifyHandler([&, weak_server, server_id, cldbid](Property& prop){ + auto server = weak_server.lock(); + if(!server && server_id != 0) { + logError(server_id, "Tried to update client permissions of a expired server!"); + return; + } + + std::string query; + if(prop.type() == property::CLIENT_TOTALCONNECTIONS) + query = "UPDATE `clients` SET `connections` = :value WHERE `serverId` = :serverId AND `cldbid` = :cldbid"; + else if(prop.type() == property::CLIENT_NICKNAME) + query = "UPDATE `clients` SET `lastName` = :value WHERE `serverId` = :serverId AND `cldbid` = :cldbid"; + else if(prop.type() == property::CLIENT_LASTCONNECTED) + query = "UPDATE `clients` SET `lastConnect` = :value WHERE `serverId` = :serverId AND `cldbid` = :cldbid"; + if(query.empty()) return; + debugMessage(server ? server->getServerId() : 0, "[Property] Changing client property '" + prop.type().name + "' for " + to_string(cldbid) + " (New value: " + prop.value() + ", SQL: " + query + ")"); + sql::command(this->sql, query, variable{":serverId", server ? server->getServerId() : 0}, variable{":cldbid", cldbid}, variable{":value", prop.value()}).executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); + }); + + return props; +} + +void DatabaseHelper::loadStartupCache() { + this->loadStartupPermissionCache(); + this->loadStartupPropertyCache(); + + this->use_startup_cache = true; +} + +size_t DatabaseHelper::cacheBinarySize() { + size_t result = 0; + result += sizeof(this->startup_entries); + for(const auto& entry : this->startup_entries) { + result += sizeof(entry); + result += sizeof(*entry.get()); + for(const auto& e : entry->permissions) { + result += sizeof(e); + result += sizeof(e.get()); + } + for(const auto& e : entry->properties) { + result += sizeof(e); + result += sizeof(e.get()); + result += e->value.length(); + } + } + return result; +} + +void DatabaseHelper::clearStartupCache(ts::ServerId sid) { + if(sid == 0) { + threads::MutexLock lock(this->startup_lock); + this->startup_entries.clear(); + this->use_startup_cache = false; + } else { + threads::MutexLock lock(this->startup_lock); + /* + this->startup_entries.erase(std::remove_if(this->startup_entries.begin(), this->startup_entries.end(), [&](const shared_ptr& entry) { + return entry->sid == sid; + }), this->startup_entries.end()); + */ + } +} + +//SELECT `serverId`, `type`, `id`, `key`, `value` FROM properties ORDER BY `serverId` +//SELECT `serverId`, `type`, `id`, `channelId`, `permId`, `value`, `grant`, `flag_skip`, `flag_negate` FROM permissions ORDER BY `serverId` +struct StartupPermissionArgument { + std::shared_ptr current_server; +}; + +void DatabaseHelper::loadStartupPermissionCache() { + StartupPermissionArgument arg; + sql::command(this->sql, "SELECT `serverId`, `type`, `id`, `channelId`, `permId`, `value`, `grant`, `flag_skip`, `flag_negate` FROM permissions ORDER BY `serverId`").query([&](StartupPermissionArgument* arg, int length, char** values, char** names) { + auto key = permission::PermissionTypeEntry::unknown; + permission::PermissionValue value = 0, granted = 0; + permission::PermissionSqlType type = SQL_PERM_GROUP; + bool negated = false, skipped = false; + ChannelId channel = 0; + uint64_t id = 0; + ServerId serverId = 0; + + int index; + try { + for(index = 0; index < length; index++) { + if(strcmp(names[index], "permId") == 0) { + key = permission::resolvePermissionData(values[index]); + if(key->type == permission::unknown || key->type == permission::undefined) { + debugMessage(0, "[SQL] Permission entry contains invalid permission type! Type: {}", values[index]); + return 0; + } + } else if(strcmp(names[index], "channelId") == 0) { + channel = stoull(values[index]); + } else if(strcmp(names[index], "id") == 0) { + id = stoull(values[index]); + } else if(strcmp(names[index], "value") == 0) { + value = stoi(values[index]); + } else if(strcmp(names[index], "grant") == 0) { + granted = stoi(values[index]); + } else if(strcmp(names[index], "flag_skip") == 0) + skipped = strcmp(values[index], "1") == 0; + else if(strcmp(names[index], "flag_negate") == 0) + negated = strcmp(values[index], "1") == 0; + else if(strcmp(names[index], "serverId") == 0) + serverId = stoll(values[index]); + else if(strcmp(names[index], "type") == 0) + type = static_cast(stoll(values[index])); + } + } catch(std::exception& ex) { + logError(0, "[SQL] Cant load permissions! Failed to parse value! Message: {}. Key: {}, Value: \"{}\"", ex.what(),names[index],values[index]); + return 0; + } + + if(key == permission::PermissionTypeEntry::unknown) { + debugMessage(0, "[SQL] Permission entry misses permission type!"); + return 0; + } + if(serverId == 0) return 0; + + if(!arg->current_server || arg->current_server->sid != serverId) { + arg->current_server = nullptr; + { + threads::MutexLock lock(this->startup_lock); + for(const auto& entry : this->startup_entries) { + if(entry->sid == serverId) { + arg->current_server = entry; + break; + } + } + if(!arg->current_server) { + arg->current_server = make_shared(); + arg->current_server->sid = serverId; + this->startup_entries.push_back(arg->current_server); + } + } + } + + auto entry = make_unique(); + entry->permission = key; + entry->type = type; + entry->value = value; + entry->grant = granted; + entry->flag_negate = negated; + entry->flag_skip = skipped; + entry->id = id; + entry->channelId = channel; + arg->current_server->permissions.push_back(std::move(entry)); + return 0; + }, &arg); +} + +void DatabaseHelper::loadStartupPropertyCache() { + StartupPermissionArgument arg; + sql::command(this->sql, "SELECT `serverId`, `type`, `id`, `key`, `value` FROM properties ORDER BY `serverId`").query([&](StartupPermissionArgument* arg, int length, char** values, char** names) { + std::string key, value; + property::PropertyType type = property::PROP_TYPE_UNKNOWN; + ServerId serverId = 0; + uint64_t id = 0; + for(int index = 0; index < length; index++) { + try { + if(strcmp(names[index], "key") == 0) key = values[index]; + else if(strcmp(names[index], "value") == 0) value = values[index] == nullptr ? "" : values[index]; + else if(strcmp(names[index], "type") == 0) type = (property::PropertyType) stoll(values[index]); + else if(strcmp(names[index], "serverId") == 0) serverId = stoll(values[index]); + else if(strcmp(names[index], "id") == 0) id = stoll(values[index]); + } catch(const std::exception& ex) { + logError(0, "[SQL] Cant load property! Failed to parse value! Message: {}. Key: {}, Value: \"{}\"", ex.what(),names[index],values[index]); + return 0; + } + } + + auto info = property::impl::info_key(type, key); + if(info == property::PropertyDescription::unknown) { + logError(serverId, "Invalid property ({} | {})", key, type); + return 0; + } + if(serverId == 0) return 0; + + if(!arg->current_server || arg->current_server->sid != serverId) { + arg->current_server = nullptr; + { + threads::MutexLock lock(this->startup_lock); + for(const auto& entry : this->startup_entries) { + if(entry->sid == serverId) { + arg->current_server = entry; + break; + } + } + if(!arg->current_server) { + arg->current_server = make_shared(); + arg->current_server->sid = serverId; + this->startup_entries.push_back(arg->current_server); + } + } + } + + auto entry = make_unique(); + entry->info = info; + entry->value = value; + entry->id = id; + entry->type = type; + arg->current_server->properties.push_back(std::move(entry)); + return 0; + }, &arg); +} + +bool DatabaseHelper::deleteGroupPermissions(const std::shared_ptr &server, ts::GroupId group_id) { + auto command = sql::command(this->sql, "DELETE FROM `permissions` WHERE `serverId` = :serverId AND `type` = :type AND `id` = :id", + variable{":serverId", server ? server->getServerId() : 0}, + variable{":type", permission::SQL_PERM_GROUP}, + variable{":id", group_id}).execute(); + LOG_SQL_CMD(command); + return !!command; +} + +bool DatabaseHelper::deleteChannelPermissions(const std::shared_ptr &server, ts::ChannelId channel_id) { + auto command = sql::command(sql, "DELETE FROM `permissions` WHERE `serverId` = :serverId AND `channelId` = :chid", + variable{":serverId", server ? server->getServerId() : 0}, + variable{":chid", channel_id}).execute(); + LOG_SQL_CMD(command); + return !!command; +} + +std::deque> DatabaseHelper::query_properties(ts::ServerId server_id, ts::property::PropertyType type, uint64_t id) { + deque> result; + + auto command = sql::command(this->sql, "SELECT `key`, `value`, `type` FROM properties WHERE `serverId` = :serverId AND `type` = :type AND `id` = :id", variable{":serverId", server_id}, variable{":type", type}, variable{":id", id}); + LOG_SQL_CMD(load_properties(server_id, result, command)); + + return result; +} + +bool DatabaseHelper::deletePlaylist(const std::shared_ptr &server, ts::PlaylistId playlist_id) { + auto server_id = server ? server->getServerId() : (ServerId) 0; + + sql::command(this->sql, "DELETE FROM `playlists` WHERE `serverId` = :server_id AND `playlist_id` = :playlist_id", + variable{":server_id", server_id}, + variable{":playlist_id", playlist_id} + ).executeLater().waitAndGetLater(LOG_SQL_CMD, {-1, "failed to delete playlist " + to_string(playlist_id) + " from table `playlists`"}); + + sql::command(this->sql, "DELETE FROM `permissions` WHERE `serverId` = :serverId AND `type` = :type AND `id` = :id", + variable{":serverId", server ? server->getServerId() : 0}, + variable{":type", permission::SQL_PERM_PLAYLIST}, + variable{":id", playlist_id} + ).executeLater().waitAndGetLater(LOG_SQL_CMD, {-1, "failed to delete playlist permissions for playlist " + to_string(playlist_id)}); + + sql::command(this->sql, "DELETE FROM `properties` WHERE `serverId` = :serverId AND `type` = :type AND `id` = :id", + variable{":serverId", server ? server->getServerId() : 0}, + variable{":type", property::PROP_TYPE_PLAYLIST}, + variable{":id", playlist_id} + ).executeLater().waitAndGetLater(LOG_SQL_CMD, {-1, "failed to delete playlist properties for playlist " + to_string(playlist_id)}); + + return true; +} \ No newline at end of file diff --git a/server/src/DatabaseHelper.h b/server/src/DatabaseHelper.h new file mode 100644 index 0000000..ebe3b25 --- /dev/null +++ b/server/src/DatabaseHelper.h @@ -0,0 +1,131 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace ts { + namespace server { + class TSServer; + class DataClient; + + struct ClientDatabaseInfo { + ServerId sid; + ClientDbId cldbid; + std::chrono::time_point created; + std::chrono::time_point lastjoin; + std::string uniqueId; + std::string lastName; + uint32_t connections; + }; + + struct CachedPermissionManager { + ServerId sid; + ClientDbId cldbid; + std::weak_ptr manager; + std::shared_ptr ownLock; + std::chrono::time_point lastAccess; + }; + + struct CachedProperties { + ServerId sid; + ClientDbId cldbid; + std::weak_ptr properties; + std::shared_ptr ownLock; + std::chrono::time_point lastAccess; + }; + + /* + CREATE_TABLE("properties", "`serverId` INTEGER DEFAULT -1, `type` INTEGER, `id` INTEGER, `key` VARCHAR(" UNKNOWN_KEY_LENGTH "), `value` TEXT", command_append_utf8); + CREATE_TABLE("permissions", "`serverId` INT NOT NULL, `type` INT, `id` INT, `channelId` INT, `permId` VARCHAR(" UNKNOWN_KEY_LENGTH "), `value` INT, `grant` INT", command_append_utf8); + */ + struct StartupPermissionEntry { + permission::PermissionSqlType type = permission::SQL_PERM_CHANNEL; + uint64_t id = 0; + ChannelId channelId = 0; + std::shared_ptr permission = permission::PermissionTypeEntry::unknown; + permission::PermissionValue value = 0; + permission::PermissionValue grant = 0; + + bool flag_skip = false; + bool flag_negate = false; + }; + + struct StartupPropertyEntry { + property::PropertyType type = property::PropertyType::PROP_TYPE_UNKNOWN; + uint64_t id = 0; + std::shared_ptr info = property::PropertyDescription::unknown; + std::string value; + }; + + struct StartupCacheEntry { + ServerId sid; + + std::deque> permissions; + std::deque> properties; + }; + + struct FastPropertyEntry { + std::shared_ptr type; + std::string value; + }; + + class DatabaseHelper { + public: + static void assign_default_properties_client(Properties *, ClientType type); + static bool assignDatabaseId(sql::SqlManager *, ServerId id, std::shared_ptr); + + explicit DatabaseHelper(sql::SqlManager*); + ~DatabaseHelper(); + + void loadStartupCache(); + size_t cacheBinarySize(); + void clearStartupCache(ServerId sid = 0); + + void deleteClient(const std::shared_ptr&,ClientDbId); + bool validClientDatabaseId(const std::shared_ptr&, ClientDbId); + std::deque> queryDatabaseInfo(const std::shared_ptr&, const std::deque&); + std::deque> queryDatabaseInfoByUid(const std::shared_ptr &, std::deque); + + std::shared_ptr loadClientPermissionManager(const std::shared_ptr&, ClientDbId); //Just and write + void saveClientPermissions(const std::shared_ptr&, ClientDbId , const std::shared_ptr& /* permission manager */); + + std::shared_ptr loadChannelPermissions(const std::shared_ptr&, ChannelId); //Just read + void saveChannelPermissions(const std::shared_ptr&, ChannelId, const std::shared_ptr& /* permission manager */); + + std::shared_ptr loadGroupPermissions(const std::shared_ptr&, GroupId); //Just read + void saveGroupPermissions(const std::shared_ptr&, GroupId, const std::shared_ptr& /* permission manager */); + + std::shared_ptr loadPlaylistPermissions(const std::shared_ptr&, PlaylistId /* playlist id */); //Read and write + + std::shared_ptr loadServerProperties(const std::shared_ptr&); //Read and write + std::shared_ptr loadPlaylistProperties(const std::shared_ptr&, PlaylistId); //Read and write + std::shared_ptr loadChannelProperties(const std::shared_ptr&, ChannelId); //Read and write + std::shared_ptr loadClientProperties(const std::shared_ptr&, ClientDbId, ClientType); + + bool deleteGroupPermissions(const std::shared_ptr&, GroupId); + bool deleteChannelPermissions(const std::shared_ptr&, ChannelId); + bool deletePlaylist(const std::shared_ptr&, PlaylistId /* playlist id */); + std::deque> query_properties(ServerId /* server */, property::PropertyType /* type */, uint64_t /* id */); /* required for server snapshots */ + + void tick(); + private: + void loadStartupPermissionCache(); + void loadStartupPropertyCache(); + + bool use_startup_cache = false; + threads::Mutex startup_lock; + std::deque> startup_entries; + + sql::SqlManager* sql = nullptr; + threads::Mutex permManagerLock; + std::deque cachedPermissionManagers; + threads::Mutex propsLock; + std::deque cachedProperties; + }; + } +} \ No newline at end of file diff --git a/server/src/Group.cpp b/server/src/Group.cpp new file mode 100644 index 0000000..0ad8965 --- /dev/null +++ b/server/src/Group.cpp @@ -0,0 +1,942 @@ +#include +#include +#include +#include +#include "Group.h" +#include "TSServer.h" +#include "src/client/ConnectedClient.h" +#include "InstanceHandler.h" +#include "src/server/file/FileServer.h" + +using namespace std; +using namespace std::chrono; +using namespace ts; +using namespace ts::server; +using namespace ts::permission; + +extern InstanceHandler* serverInstance; +Group::Group(GroupManager* handle, GroupTarget target, GroupType type, GroupId groupId) : _target(target), _type(type) { + memtrack::allocated(this); + this->handle = handle; + + this->_properties = new Properties(); + this->_properties->register_property_type(); + this->setPermissionManager(make_shared()); + + this->properties()[property::GROUP_ID] = groupId; + this->properties()[property::GROUP_TYPE] = type; + /* + this->_properties->registerProperty("sgid", groupId, PROP_GROUP_INIT_SERVER); + this->_properties->registerProperty("cgid", groupId, PROP_GROUP_INIT_CHANNEL); + this->_properties->registerProperty("gid", groupId, PROP_PRIVATE_TEMP); + + + this->_properties->registerProperty("type", (uint8_t) type, PROP_GROUP_INIT); + this->_properties->registerProperty("name", "Undefined group #" + to_string(groupId), PROP_GROUP_INIT | PROP_SNAPSHOT); + this->_properties->registerProperty("sortid", 0, PROP_GROUP_INIT); + this->_properties->registerProperty("savedb", 0, PROP_GROUP_INIT); + this->_properties->registerProperty("namemode", 0, PROP_GROUP_INIT); + this->_properties->registerProperty("iconid", 0, PROP_GROUP_INIT); + + this->_properties->registerProperty("default_group", false, PROP_PRIVATE_TEMP); + */ + + this->_properties->registerNotifyHandler([&](Property& prop){ + if((prop.type().flags & property::FLAG_SAVE) == 0) return; + if(!this->handle) return; + + std::string sql; + if(prop.hasDbReference()){ + sql = "UPDATE `properties` SET `value` = :value WHERE `serverId` = :sid AND `type` = :type AND `id` = :gid AND `key` = :key"; + } else { + prop.setDbReference(true); + sql = "INSERT INTO `properties` (`serverId`, `type`, `id`, `key`, `value`) VALUES (:sid, :type, :gid, :key, :value)"; + } + + sql::command(this->handle->sql, sql, + variable{":sid", this->handle->getServerId()}, variable{":type", property::PropertyType::PROP_TYPE_GROUP}, variable{":gid", this->groupId()}, variable{":key", prop.type().name}, variable{":value", prop.value()}) + .executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); + }); +} + +void Group::setPermissionManager(const std::shared_ptr &manager) { + this->_permissions = manager; + this->apply_properties_from_permissions(); +} + +void Group::apply_properties_from_permissions() { + { + auto permission = this->_permissions->permission_value_flagged(permission::i_icon_id); + this->properties()[property::GROUP_ICONID] = permission.has_value ? permission.value : 0; + } + { + auto permission = this->_permissions->permission_value_flagged(permission::i_group_show_name_in_tree); + this->properties()[property::GROUP_NAMEMODE] = permission.has_value ? permission.value : 0; + } + { + auto permission = this->_permissions->permission_value_flagged(permission::i_group_sort_id); + this->properties()[property::GROUP_SORTID] = permission.has_value ? permission.value : 0; + } + { + auto permission = this->_permissions->permission_value_flagged(permission::b_group_is_permanent); + this->properties()[property::GROUP_SAVEDB] = permission.has_value ? permission.value : 0; + } +} + +Group::~Group() { + delete this->_properties; + memtrack::freed(this); +} + +GroupManager::GroupManager(const shared_ptr &server, sql::SqlManager *sql, std::shared_ptr root) : server(server), sql(sql), root(std::move(root)) { } + +GroupManager::~GroupManager() {} + +bool GroupManager::loadGroupFormDatabase(GroupId id) { + if(id == 0){ + this->groups.clear(); + + auto res = sql::command(this->sql, "SELECT * FROM `groups` WHERE `serverId` = :sid", variable{":sid", this->getServerId()}).query(&GroupManager::insertGroupFromDb, this); + LOG_SQL_CMD(res); + + return true; + } else { + for(const auto &e : this->groups) + if(e->groupId() == id){ + this->groups.erase(find(this->groups.begin(), this->groups.end(), e)); + break; + } + auto res = sql::command(this->sql, "SELECT * FROM `groups` WHERE `serverId` = :sid AND `groupId` = :gid", + variable{":sid", this->getServerId()}, variable{":gid", id}).query(&GroupManager::insertGroupFromDb, this); + auto fn = LOG_SQL_CMD; + fn(res); + return res; + } +} + +std::vector> GroupManager::availableGroups(bool root) { + std::vector> response; + for(const auto& group : this->groups) + response.push_back(group); + if(root && this->root){ + auto elm = this->root->availableGroups(); + for(const auto& e : elm) + response.push_back(e); + } + return response; +} + +std::vector> GroupManager::availableServerGroups(bool root){ + std::vector> response; + for(const auto& group : this->groups) + if(group->target() == GroupTarget::GROUPTARGET_SERVER) + response.push_back(group); + if(root && this->root){ + auto elm = this->root->availableServerGroups(); + for(const auto& e : elm) + response.push_back(e); + } + return response; +} + +std::vector> GroupManager::availableChannelGroups(bool root) { + std::vector> response; + for(const auto& group : this->groups) + if(group->target() == GroupTarget::GROUPTARGET_CHANNEL) + response.push_back(group); + if(root && this->root){ + auto elm = this->root->availableChannelGroups(true); + for(const auto& e : elm) + response.push_back(e); + } + return response; +} + +int GroupManager::insertGroupFromDb(int count, char **values, char **column) { + GroupId groupId = 0; + auto target = (GroupTarget) 0xff; + auto type = (GroupType) 0xff; + std::string targetName; + + for(int index = 0; index < count; index++){ + if(strcmp(column[index], "target") == 0) + target = (GroupTarget) stoll(values[index]); + else if(strcmp(column[index], "type") == 0) + type = (GroupType) stoll(values[index]); + else if(strcmp(column[index], "groupId") == 0) + groupId = (GroupType) stoll(values[index]); + else if(strcmp(column[index], "displayName") == 0) + targetName = values[index]; + else if(strcmp(column[index], "serverId") == 0); + else cerr << "Invalid group table row " << column[index] << endl; + } + + if(groupId == 0 || target == 0xff || type == 0xff || targetName.empty()) { + logCritical(this->getServerId(), "Found invalid group ad database! (GroupId " + to_string(groupId) + ", Target " + to_string(target) + ", Type " + to_string(type) + ", Name '" + targetName + "')"); + return 0; + } + /* + assert(groupId != 0); + assert(target != 0xff); + assert(type != 0xff); + assert(!targetName.empty()); + */ + + shared_ptr group = std::make_shared(this, target, type, groupId); + + /* + auto res = sql::command(this->sql, "SELECT `key`, `value` FROM `properties` WHERE `serverId` = :sid AND `type` = :type AND `id` = :gid", + variable{":sid", this->getServerId()}, variable{":type", property::PROP_TYPE_GROUP}, variable{":gid", group->groupId()}).query([this](Group* g, int length, char** values, char** columns){ + string key, value; + for(int index = 0; index < length; index++) + if(strcmp(columns[index], "key") == 0) + key = values[index]; + else if(strcmp(columns[index], "value") == 0) + value = values[index]; + + auto info = property::info(key); + if(info == property::GROUP_UNDEFINED) { + logError(this->getServerId(), "Invalid property for group: " + key); + return 0; + } + + auto prop = g->properties()[info.property]; + prop.setDbReference(true); + prop.value(value, false); + return 0; + }, group.get()); + auto print = LOG_SQL_CMD; + print(res); + */ + //FIXME load group properties view database helper (or drop it full because no saved properties) + + group->properties()[property::GROUP_NAME] = targetName; + + group->setPermissionManager(serverInstance->databaseHelper()->loadGroupPermissions(this->server.lock(), group->groupId())); + + debugMessage(this->getServerId(), "Push back group -> " + to_string(group->groupId()) + " - " + group->name()); + this->groups.push_back(group); + + auto iconId = (IconId) group->icon_id(); + if(iconId != 0 && serverInstance && serverInstance->getFileServer() && !serverInstance->getFileServer()->iconExists(this->server.lock(), iconId)) { + logMessage(this->getServerId(), "[FILE] Missing group icon (" + to_string(iconId) + ")."); + if(config::server::delete_missing_icon_permissions) group->permissions()->set_permission(permission::i_icon_id, {0, 0}, permission::v2::delete_value, permission::v2::do_nothing); + } + return 0; +} + +void GroupManager::handleChannelDeleted(std::shared_ptr channel) { + cacheLock.lock(); + for(const auto &entry : this->cachedClients) + entry->channelGroups.erase(channel->channelId()); + cacheLock.unlock(); +} + +bool GroupManager::isLocalGroup(std::shared_ptr gr) { + return std::find(this->groups.begin(), this->groups.end(), gr) != this->groups.end(); +} + +std::shared_ptr GroupManager::defaultGroup(GroupTarget type, bool enforce_property) { + threads::MutexLock lock(this->cacheLock); + if(this->groups.empty()) return nullptr; + + auto server = this->server.lock(); + auto id = + server ? + server->properties()[type == GroupTarget::GROUPTARGET_SERVER ? property::VIRTUALSERVER_DEFAULT_SERVER_GROUP : property::VIRTUALSERVER_DEFAULT_CHANNEL_GROUP].as_save() : + serverInstance->properties()[property::SERVERINSTANCE_GUEST_SERVERQUERY_GROUP].as_save(); + auto group = this->findGroupLocal(id); + if(group || enforce_property) return group; + + for(auto elm : this->groups) + if(elm->target() == type) + return elm; + + return nullptr; //Worst case! +} + +std::shared_ptr GroupManager::findGroup(GroupId groupId) { + auto result = this->findGroupLocal(groupId); + if(!result && this->root) result = this->root->findGroup(groupId); + return result; +} + +std::shared_ptr GroupManager::findGroupLocal(GroupId groupId) { + for(const auto& elm : this->groups) + if(elm->groupId() == groupId) return elm; + return nullptr; +} + +std::vector> GroupManager::findGroup(GroupTarget target, std::string name) { + vector> res; + for(const auto &elm : this->groups) + if(elm->name() == name && elm->target() == target) res.push_back(elm); + if(this->root) { + auto r = root->findGroup(target, name); + for(const auto &e : r) res.push_back(e); + } + return res; +} + +ServerId GroupManager::getServerId() { + auto l = this->server.lock(); + return l ? l->getServerId() : 0; +} + +std::shared_ptr GroupManager::createGroup(GroupTarget target, GroupType type, std::string name) { + if(type != GROUP_TYPE_NORMAL && this->root) return root->createGroup(target, type, name); + + auto rawId = generateGroupId(this->sql); + if(rawId <= 0) { + logError(this->getServerId(), "Could not create a new group! ({})", "Could not generate group id"); + return nullptr; + } + auto groupId = (GroupId) rawId; + auto res = sql::command(this->sql, "INSERT INTO `groups` (`serverId`, `groupId`, `target`, `type`, `displayName`) VALUES (:sid, :gid, :target, :type, :name)", + variable{":sid", this->getServerId()}, variable{":gid", groupId}, variable{":target", target}, variable{":type", type}, variable{":name", name}).execute(); + auto print = LOG_SQL_CMD; + print(res); + if(!res) return nullptr; + + std::shared_ptr group = std::make_shared(this, target, type, groupId); + group->properties()[property::GROUP_NAME] = name; + group->setPermissionManager(serverInstance->databaseHelper()->loadGroupPermissions(this->server.lock(), group->groupId())); + this->groups.push_back(group); + return group; +} + +bool GroupManager::copyGroup(std::shared_ptr group, GroupType type, std::string name, ServerId targetServerId) { + auto group_server = group->handle->getServerId(); + auto groupId = generateGroupId(this->sql); + + auto print = LOG_SQL_CMD; + auto res = sql::command(this->sql, "INSERT INTO `permissions` (`serverId`, `type`, `id`, `channelId`, `permId`, `value`, `grant`) SELECT :tsid AS `serverId`, `type`, :target AS `id`, 0 AS `channelId`, `permId`, `value`,`grant` FROM `permissions` WHERE `serverId` = :ssid AND `type` = :type AND `id` = :source", + variable{":target", groupId}, variable{":ssid", group_server}, variable{":tsid", targetServerId}, variable{":source", group->groupId()}, variable{":type", SQL_PERM_GROUP}).execute(); + print(res); + + //Properties not used currently + res = sql::command(this->sql, "INSERT INTO `properties` (`serverId`, `type`, `id`, `key`, `value`) SELECT :tsid AS `serverId`, `type`, :target AS `id`, `key`, `value` FROM `properties` WHERE `serverId` = :ssid AND `type` = :type AND `id` = :source", + variable{":target", groupId}, variable{":ssid", group_server}, variable{":tsid", targetServerId}, variable{":source", group->groupId()}, variable{":type", property::PropertyType::PROP_TYPE_GROUP}).execute(); + print(res); + + res = sql::command(this->sql, "INSERT INTO `groups` (`serverId`, `groupId`, `target`, `type`, `displayName`) VALUES (:sid, :gid, :target, :type, :name)", + variable{":sid", targetServerId}, variable{":gid", groupId}, variable{":target", group->target()}, variable{":type", type}, variable{":name", name}).execute(); + print(res); + + if(targetServerId == (this->getServerId())) + this->loadGroupFormDatabase(groupId); + else if(this->root) + this->root->loadGroupFormDatabase(groupId); + return true; +} + +bool GroupManager::copyGroupPermissions(const shared_ptr &source, const shared_ptr &target) { + auto targetServer = target->handle->getServerId(); + auto sourceServer = source->handle->getServerId(); + + auto res = sql::command(this->sql, "DELETE FROM `permissions` WHERE `serverId` = :sid AND `type` = :type AND `id` = :id", variable{":sid", this->getServerId()}, variable{":type", SQL_PERM_GROUP}, variable{":id", target->groupId()}).execute(); + LOG_SQL_CMD(res); + + res = sql::command(this->sql, "INSERT INTO `permissions` (`serverId`, `type`, `id`, `channelId`, `permId`, `value`, `grant`) SELECT :tsid AS `serverId`, `type`, :target AS `id`, 0 AS `channelId`, `permId`, `value`,`grant` FROM `permissions` WHERE `serverId` = :ssid AND `type` = :type AND `id` = :source", + variable{":ssid", sourceServer}, variable{":tsid", targetServer}, variable{":type", SQL_PERM_GROUP}, variable{":source", source->groupId()}, variable{":target", target->groupId()}).execute(); + + target->setPermissionManager(serverInstance->databaseHelper()->loadGroupPermissions(target->handle->server.lock(), target->groupId())); + LOG_SQL_CMD(res); + return true; +} + +bool GroupManager::reloadGroupPermissions(std::shared_ptr group) { + if(!isLocalGroup(group)){ + if(this->root) return this->root->reloadGroupPermissions(group); + return false; + } + + group->setPermissionManager(serverInstance->databaseHelper()->loadGroupPermissions(this->server.lock(), group->groupId())); + return true; +} + +bool GroupManager::renameGroup(std::shared_ptr group, std::string name) { + if(!isLocalGroup(group)){ + if(this->root) return this->root->renameGroup(group, name); + return false; + } + + // CREATE_TABLE("groups", "`serverId` INT NOT NULL, `groupId` INTEGER, `target` INT, `type` INT, `displayName` TEXT"); + group->properties()[property::GROUP_NAME] = name; + sql::command(this->sql, "UPDATE `groups` SET `displayName` = :name WHERE `serverId` = :sid AND `groupId` = :gid AND `target` = :target", + variable{":name", name}, variable{":gid", group->groupId()}, variable{":target", group->target()}, variable{":sid", this->getServerId()}).executeLater().waitAndGetLater(LOG_SQL_CMD, {-1, "failed"}); + return true; +} + +bool GroupManager::deleteAllGroups() { + LOG_SQL_CMD(sql::command(this->sql, "DELETE FROM `groups` WHERE `serverId` = :sid", variable{":sid", this->getServerId()}).execute()); + LOG_SQL_CMD(sql::command(this->sql, "DELETE FROM `assignedGroups` WHERE `serverId` = :sid", variable{":sid", this->getServerId()}).execute()); + LOG_SQL_CMD(sql::command(this->sql, "DELETE FROM `permissions` WHERE `serverId` = :sid AND `type` = :type", variable{":sid", this->getServerId()}, variable{":type", SQL_PERM_GROUP}).execute()); + { + threads::MutexLock lock(this->cacheLock); + for(const auto& entry : this->cachedClients) { + threads::MutexLock lock_entry(entry->lock); + bool iterate = true; + while(iterate) { + iterate = false; + for(const auto& assignment : entry->channelGroups) { + if(std::find(this->groups.begin(), this->groups.end(), assignment.second->group) != this->groups.end()) { + entry->channelGroups.erase(assignment.first); + iterate = true; + break; + } + } + } + + entry->serverGroups.erase(std::remove_if(entry->serverGroups.begin(), entry->serverGroups.end(), [&](const shared_ptr& assignment){ + return std::find(this->groups.begin(), this->groups.end(), assignment->group) != this->groups.end(); + }), entry->serverGroups.end()); + } + } + this->groups.clear(); + return true; +} + +bool GroupManager::deleteGroup(std::shared_ptr group) { + if(!isLocalGroup(group)){ + if(this->root) return this->root->deleteGroup(group); + return false; + } + + this->groups.erase(std::find(this->groups.begin(), this->groups.end(), group)); + + bool flag_sql = false; + auto res = sql::command(this->sql, "DELETE FROM `groups` WHERE `serverId` = :sid AND `groupId` = :gid", variable{":sid", this->getServerId()}, variable{":gid", group->groupId()}).execute(); + LOG_SQL_CMD(res); + flag_sql |= !res; + + res = sql::command(this->sql, "DELETE FROM `assignedGroups` WHERE `serverId` = :sid AND `groupId` = :gid", variable{":sid", this->getServerId()}, variable{":gid", group->groupId()}).execute(); + LOG_SQL_CMD(res); + flag_sql |= !res; + + flag_sql &= serverInstance->databaseHelper()->deleteGroupPermissions(this->server.lock(), group->groupId()); + if(flag_sql) + logError(this->getServerId(), "Could not delete group {} ({}) from database. May leader to invalid data", group->name(), group->groupId()); + + this->cacheLock.lock(); + for(const auto &entry : this->cachedClients){ + bool iterate = true; + while(iterate) { + iterate = false; + for(const auto &assignment : entry->serverGroups){ + if(assignment->group == group){ + entry->serverGroups.erase(std::find(entry->serverGroups.begin(), entry->serverGroups.end(), assignment)); + iterate = true; + break; + } + } + + } + iterate = true; + while(iterate) { + iterate = false; + for(const auto& pair : entry->channelGroups){ + if(pair.second->group == group){ + entry->channelGroups.erase(pair.first); + iterate = true; + break; + } + } + } + } + this->cacheLock.unlock(); + return true; +} + +int64_t GroupManager::generateGroupId(sql::SqlManager* sql) { + int64_t hightestGroupId = 0; + sql::command(sql, "SELECT `groupId` FROM `groups` ORDER BY `groupId` DESC LIMIT 1").query([](int64_t* ptr, int, char** values, char**){ + *ptr = stoul(values[0]); + return 0; + }, &hightestGroupId); + return hightestGroupId + 1; +} + +std::deque GroupManager::update_server_group_property(const shared_ptr &client, bool channel_lock) { + std::deque changed; + + //Server groups + { + auto groups = this->getServerGroups(client->getClientDatabaseId(), client->getType()); + string group_string; + for(const auto& group : groups) + if(group_string.empty()) group_string += to_string(group->group->groupId()); + else group_string += "," + to_string(group->group->groupId()); + if(client->properties()[property::CLIENT_SERVERGROUPS] != group_string) { + client->properties()[property::CLIENT_SERVERGROUPS] = group_string; + changed.push_back(property::CLIENT_SERVERGROUPS); + + unique_lock chan_lock(client->channel_lock, defer_lock); + if(channel_lock) + chan_lock.lock(); + + client->cached_server_groups.clear(); + client->cached_server_groups.reserve(groups.size()); + for(const auto& group : groups) + client->cached_server_groups.push_back(group->group->groupId()); + } + + } + + //Channel groups + if(client->getChannel()){ + shared_ptr group = this->getChannelGroup(client->getClientDatabaseId(), client->getChannel(), true); + if(client->properties()[property::CLIENT_CHANNEL_GROUP_INHERITED_CHANNEL_ID] != group->channelId) { + client->properties()[property::CLIENT_CHANNEL_GROUP_INHERITED_CHANNEL_ID] = group->channelId; + changed.push_back(property::CLIENT_CHANNEL_GROUP_INHERITED_CHANNEL_ID); + } + if(client->properties()[property::CLIENT_CHANNEL_GROUP_ID] != group->group->groupId()) { + client->properties()[property::CLIENT_CHANNEL_GROUP_ID] = group->group->groupId(); + changed.push_back(property::CLIENT_CHANNEL_GROUP_ID); + + unique_lock chan_lock(client->channel_lock, defer_lock); + if(channel_lock) + chan_lock.lock(); + + client->cached_channel_group = group->group->groupId(); + } + } + + return changed; +} + +void GroupManager::cleanupAssignments(ClientDbId client) { + if(this->root) + this->root->cleanupAssignments(client); + + for(const auto& assignment : this->getAssignedServerGroups(client)) { + if(!assignment->group->is_permanent()) + this->removeServerGroup(client, assignment->group); + } +} + +template +struct DBLoadCacheParmStruct { + GroupManager* manager; + DataType* data; +}; + + +void GroupManager::enableCache(std::shared_ptr client) { + if(this->root) + this->root->enableCache(client); + + shared_ptr entry = std::make_shared(); + entry->client = client; + + DBLoadCacheParmStruct parm = {this, entry.get()}; + + auto res = sql::command(this->sql, "SELECT `groupId`, `channelId`, `until` FROM `assignedGroups` WHERE `serverId` = :sid AND `cldbid` = :cldbid", variable{":sid", this->getServerId()}, variable{":cldbid", client->getClientDatabaseId()}).query([&](DBLoadCacheParmStruct* parms, int length, char** value, char** column) { + shared_ptr group = nullptr; + time_point until; + ChannelId channelId = 0; + + for(int index = 0; index < length; index++){ + if(value[index] == nullptr) { + logError(this->getServerId(), string() + "Invalid value at " + column[index]); + continue; + } + if(strcmp(column[index], "groupId") == 0){ + group = parms->manager->findGroup(stoll(value[index])); + } else if(strcmp(column[index], "until") == 0){ + until = time_point() + milliseconds(stoll(value[index])); + } else if(strcmp(column[index], "channelId") == 0){ + channelId = stoll(value[index]); + } else cerr << "Invalid column " << column[index] << endl; + } + if(!group) + return 0; + + shared_ptr assignment = std::make_shared(); + assignment->group = group; + assignment->until = until; + assignment->parent = parms->data; + assignment->channelId = channelId; + assignment->server = this->getServerId(); + if(channelId == 0) + parms->data->serverGroups.push_back(assignment); + else parms->data->channelGroups[channelId] = assignment; + return 0; + }, &parm); + + this->cacheLock.lock(); + this->cachedClients.push_back(entry); + this->cacheLock.unlock(); +} + +void GroupManager::disableCache(const shared_ptr &client) { + if(this->root) this->root->disableCache(client); + + threads::MutexLock lock(this->cacheLock); + bool found = false; + for(const auto& entry : this->cachedClients){ + if(entry->client == client){ + this->cachedClients.erase(std::find(this->cachedClients.begin(), this->cachedClients.end(), entry)); + found = true; + break; + } + } + if(!found) + ;//debugMessage("Tried to attempt to delete a not existing cached manager. (" + manager->getDisplayName() + ")"); +} + +void GroupManager::clearCache() { + if(this->root) this->root->clearCache(); + + threads::MutexLock lock(cacheLock); + this->cachedClients.clear(); +} + +bool GroupManager::isClientCached(const shared_ptr &client) { + { + threads::MutexLock lock(cacheLock); + for(const auto &entry : this->cachedClients){ + if(entry->client == client){ + return true; + } + } + } + + if(this->root) return this->root->isClientCached(client); + return false; +} + +typedef std::vector> ResList; +std::vector> GroupManager::listGroupMembers(std::shared_ptr group, bool names) { //TODO juse inner join only on names = true + if(!isLocalGroup(group)){ + if(this->root) return this->root->listGroupMembers(group, names); + return {}; + } + ResList result; + + sql::command(this->sql, + "SELECT assignedGroups.cldbid, assignedGroups.channelId, assignedGroups.until, clients.clientUid, clients.lastName FROM assignedGroups INNER JOIN clients ON (clients.cldbid = assignedGroups.cldbid AND clients.serverId = assignedGroups.serverId) WHERE assignedGroups.`serverId` = :sid AND `groupId` = :gid;", + variable{":sid", this->getServerId()}, variable{":gid", group->groupId()}) + .query([&](ResList* list, int columnCount, char** values, char** columnName){ + std::shared_ptr member = std::make_shared(); + member->displayName = "undefined"; + member->uid = "undefined"; + for(int index = 0; index < columnCount; index++){ + if(values[index] == nullptr) { + logError(this->getServerId(), string() + "Invalid value at " + columnName[index]); + continue; + } + if(strcmp(columnName[index], "cldbid") == 0) + member->cldbId = stoll(values[index]); + else if(strcmp(columnName[index], "until") == 0) + member->until = time_point() + milliseconds(stoll(values[index])); + else if(strcmp(columnName[index], "clientUid") == 0) + member->uid = values[index]; + else if(strcmp(columnName[index], "lastName") == 0) + member->displayName = values[index]; + else if(strcmp(columnName[index], "channelId") == 0) + member->channelId = stoll(values[index]); + else cerr << "Invalid column name " << columnName[index] << endl; + } + list->push_back(member); + return 0; + }, &result); + return result; +} + +typedef DBLoadCacheParmStruct>> GAGroupCache; +vector> GroupManager::listGroupAssignments(ClientDbId cldbId) { + vector> result; + GAGroupCache parm = {this, &result}; + sql::result res; + + auto cached = resolveCached(cldbId); + if(cached) { + { + threads::MutexLock l(cached->lock); + for(const auto &serverGroup : cached->serverGroups) + result.push_back(serverGroup); + for(auto& channelGroup : cached->channelGroups) + result.push_back(channelGroup.second); + } + + if(this->root){ + auto append = this->root->listGroupAssignments(cldbId); + for(const auto &elm : append) + result.push_back(elm); + } + + return result; + } + + + res = sql::command(this->sql, "SELECT `groupId`, `until`, `channelId` FROM `assignedGroups` WHERE `serverId` = :sid AND `cldbid` = :cldbid", variable{":sid", this->getServerId()}, variable{":cldbid", cldbId}).query([&](GAGroupCache* parms, int length, char** value, char** column){ + shared_ptr group = nullptr; + time_point until; + uint64_t channelId = 0; + + for(int index = 0; index < length; index++){ + if(value[index] == nullptr) { + logError(this->getServerId(), string() + "Invalid value at " + column[index]); + continue; + } + if(strcmp(column[index], "groupId") == 0){ + group = parms->manager->findGroup(stoll(value[index])); + } else if(strcmp(column[index], "until") == 0){ + until = time_point() + milliseconds(stoll(value[index])); + } else if(strcmp(column[index], "channelId") == 0){ + channelId = stoll(value[index]); + } else cerr << "Invalid column " << column[index] << endl; + } + if(!group) + return 0; + + shared_ptr assignment = std::make_shared(); + assignment->parent = nullptr; + assignment->group = group; + assignment->until = until; + assignment->channelId = channelId; + assignment->server = this->getServerId(); + parms->data->push_back(assignment); + return 0; + }, &parm); + (LOG_SQL_CMD)(res); + + if(this->root){ + auto append = this->root->listGroupAssignments(cldbId); + for(const auto &elm : append) + result.push_back(elm); + } + + return result; +} + +std::shared_ptr GroupManager::resolveCached(ClientDbId cldbId) { + threads::MutexLock lock(this->cacheLock); + for(const auto& cl : this->cachedClients) + if(cl->client->getClientDatabaseId() == cldbId){ + return cl; + } + return nullptr; +} + +typedef DBLoadCacheParmStruct>> SGroupCache; +std::vector> GroupManager::getAssignedServerGroups(ClientDbId cldbid) { + auto cached = this->resolveCached(cldbid); + sql::result res; + std::vector> result; + if(this->root) { + auto root = this->root->getAssignedServerGroups(cldbid); + result.insert(result.begin(), root.begin(), root.end()); + } + + SGroupCache parm = {this, &result}; + + if(cached) { + threads::MutexLock l(cached->lock); + for(const auto &elm : cached->serverGroups) result.push_back(elm); + return result; + } + + debugMessage("DB query groups! for -> " + to_string(cldbid) + " - server " + to_string(this->getServerId())); + + res = sql::command(this->sql, "SELECT `groupId`, `until` FROM `assignedGroups` WHERE `serverId` = :sid AND `cldbid` = :cldbid AND `channelId` = 0", variable{":sid", this->getServerId()}, variable{":cldbid", cldbid}).query([&](SGroupCache* parms, int length, char** value, char** column){ + shared_ptr group = nullptr; + time_point until; + + for(int index = 0; index < length; index++){ + if(value[index] == nullptr) { + logError(this->getServerId(), string() + "Invalid value at " + column[index]); + continue; + } + if(strcmp(column[index], "groupId") == 0 && value[index] != nullptr){ + group = parms->manager->findGroup(stoll(value[index])); + } else if(strcmp(column[index], "until") == 0){ + until = time_point() + milliseconds(stoll(value[index] == nullptr ? "0" : value[index])); + } else cerr << "Invalid column " << column[index] << endl; + } + if(!group) + return 0; + + shared_ptr assignment = std::make_shared(); + assignment->parent = nullptr; + assignment->group = group; + assignment->until = until; + assignment->server = this->getServerId(); + parms->data->push_back(assignment); + return 0; + }, &parm); + LOG_SQL_CMD(res); + return result; +} + +std::vector> GroupManager::getServerGroups(ClientDbId cldbid, server::ClientType type) { + auto result = this->getAssignedServerGroups(cldbid); + if(result.empty()) return this->defaultServerGroupGroupAssignments(cldbid, type); + return result; +} + +std::vector> GroupManager::defaultServerGroupGroupAssignments(ClientDbId client, ClientType type) { + std::vector> result; + if(type == ClientType::CLIENT_QUERY && this->root) { + auto root = this->root->defaultServerGroupGroupAssignments(client, type); + result.insert(result.begin(), root.begin(), root.end()); + } else if(type == ClientType::CLIENT_MUSIC) { + threads::MutexLock lock(this->cacheLock); + + auto server = this->server.lock(); + auto id = + server ? + server->properties()[property::VIRTUALSERVER_DEFAULT_MUSIC_GROUP].as_save() : + serverInstance->properties()[property::SERVERINSTANCE_TEMPLATE_MUSICDEFAULT_GROUP].as_save(); + auto group = this->findGroupLocal(id); + if(group) { + result.push_back(std::make_shared(nullptr, this->getServerId(), 0, group, time_point())); + return result; + } + } + result.push_back(std::make_shared(nullptr, this->getServerId(), 0, this->defaultGroup(GroupTarget::GROUPTARGET_SERVER), time_point())); + return result; +} + +typedef DBLoadCacheParmStruct> CGroupCache; +std::shared_ptr GroupManager::getChannelGroupExact(ClientDbId cldbId, const std::shared_ptr& channel, bool assign_default) { + auto cached = resolveCached(cldbId); + if(cached) { + threads::MutexLock l(cached->lock); + if(cached->channelGroups.count(channel->channelId()) > 0) { + return cached->channelGroups[channel->channelId()]; + } else + return assign_default ? this->defaultChannelGroupAssignment(cldbId, channel) : nullptr; + } + + std::shared_ptr result; + CGroupCache parm = {this, &result}; + auto res = sql::command(this->sql, "SELECT `groupId`, `until` FROM `assignedGroups` WHERE `serverId` = :sid AND `cldbid` = :cldbid AND `channelId` = :chid", variable{":sid", this->getServerId()}, variable{":cldbid", cldbId}, variable{":chid", channel->channelId()}).query([&](CGroupCache* parms, int length, char** value, char** column){ + shared_ptr group = nullptr; + time_point until; + + for(int index = 0; index < length; index++){ + if(value[index] == nullptr) { + logError(this->getServerId(), string() + "Invalid value at " + column[index]); + continue; + } + if(strcmp(column[index], "groupId") == 0){ + group = parms->manager->findGroup(stoll(value[index])); + } else if(strcmp(column[index], "until") == 0){ + until = time_point() + milliseconds(stoll(value[index])); + } else cerr << "Invalid column " << column[index] << endl; + } + if(!group) + return 0; + + shared_ptr assignment = std::make_shared(); + assignment->parent = nullptr; + assignment->group = group; + assignment->until = until; + assignment->server = this->getServerId(); + assignment->channelId = channel->channelId(); + *parms->data = assignment; + return 0; + }, &parm); + (LOG_SQL_CMD)(res); + + return !result && assign_default ? this->defaultChannelGroupAssignment(cldbId, channel) : result; +} + +std::shared_ptr GroupManager::getChannelGroup(ClientDbId cldbId, const shared_ptr &channel, bool assign_default) { + shared_ptr group; + std::shared_ptr inheritance_channel = channel; + while(inheritance_channel && !group) { + group = this->getChannelGroupExact(cldbId, inheritance_channel, false); + if(!group) { + auto inheritance = inheritance_channel->permissions()->permission_value_flagged(permission::b_channel_group_inheritance_end); + if(inheritance.has_value && inheritance.value == 1) + break; + + inheritance_channel = inheritance_channel->parent(); + } + } + return !group && assign_default ? this->defaultChannelGroupAssignment(cldbId, channel) : group; +} + +std::shared_ptr GroupManager::defaultChannelGroupAssignment(ClientDbId cldbId, const std::shared_ptr &channel) { + return std::make_shared(nullptr, this->getServerId(), channel->channelId(), this->defaultGroup(GroupTarget::GROUPTARGET_CHANNEL), time_point()); +} + +void GroupManager::addServerGroup(ClientDbId cldbId, std::shared_ptr group, time_point until) { + /* + if(!this->isLocalGroup(group)) { + if(this->root) this->root->addServerGroup(cldbId, group, until); + return; + } + if(hasServerGroup(cldbId, group)) return; + */ + + auto cached = resolveCached(cldbId); + if(cached) { + threads::MutexLock l(cached->lock); + cached->serverGroups.push_back(std::make_shared(cached.get(), this->getServerId(), 0, group, until)); + } + + sql::command(this->sql, "INSERT INTO `assignedGroups` (`serverId`, `cldbid`, `groupId`, `channelId`, `until`) VALUES (:sid, :cldbid, :gid, :chid, :until)", + variable{":sid", this->getServerId()}, + variable{":cldbid", cldbId}, + variable{":gid", group->groupId()}, + variable{":chid", 0}, + variable{":until", time_point_cast(until).time_since_epoch().count()}) + .executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); +} + +void GroupManager::removeServerGroup(ClientDbId cldbId, std::shared_ptr group) { + if(!this->hasServerGroupAssigned(cldbId, group)) return; + auto cached = resolveCached(cldbId); + if(cached) { + threads::MutexLock l(cached->lock); + for(const auto &entry : cached->serverGroups) + if(entry->group == group){ + cached->serverGroups.erase(std::find(cached->serverGroups.begin(), cached->serverGroups.end(), entry)); + break; + } + } + + sql::command(this->sql, "DELETE FROM `assignedGroups` WHERE `serverId` = :sid AND `cldbid` = :cldbid AND `groupId` = :gid AND `channelId` = :chid", + variable{":sid", this->getServerId()}, + variable{":cldbid", cldbId}, + variable{":gid", group->groupId()}, + variable{":chid", 0}) + .executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); +} + +void GroupManager::setChannelGroup(ClientDbId cldbId, std::shared_ptr group, std::shared_ptr channel, time_point until) { + auto old_group = getChannelGroupExact(cldbId, channel, false); + if(old_group) { + if(old_group->group == group) return; + } else if(!group) return; + + auto default_group = !group || group == this->defaultGroup(GroupTarget::GROUPTARGET_CHANNEL); + auto cached = resolveCached(cldbId); + if(cached) { + threads::MutexLock l(cached->lock); + if(default_group) + cached->channelGroups.erase(channel->channelId()); + else + cached->channelGroups[channel->channelId()] = std::make_shared(cached.get(), this->getServerId(), channel->channelId(), group, until); + } + + sql::command(this->sql, "DELETE FROM `assignedGroups` WHERE `serverId` = :sid AND `cldbid` = :cldbid AND `channelId` = :chid", + variable{":sid", this->getServerId()}, + variable{":cldbid", cldbId}, + variable{":chid", channel->channelId()}) + .executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); + + if(!default_group) { + sql::command(this->sql, "INSERT INTO `assignedGroups` (`serverId`, `cldbid`, `groupId`, `channelId`, `until`) VALUES (:sid, :cldbid, :gid, :chid, :until)", + variable{":sid", this->getServerId()}, + variable{":cldbid", cldbId}, + variable{":gid", group->groupId()}, + variable{":chid", channel->channelId()}, + variable{":until", time_point_cast(until).time_since_epoch().count()}) + .executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); + } +} \ No newline at end of file diff --git a/server/src/Group.h b/server/src/Group.h new file mode 100644 index 0000000..1bd5809 --- /dev/null +++ b/server/src/Group.h @@ -0,0 +1,226 @@ +#pragma once + +#include +#include +#include +#include +#include "PermissionManager.h" +#include "Properties.h" +#include "channel/ServerChannel.h" +#include "Definitions.h" +#include "Properties.h" +#include + +namespace ts { + namespace server { + class TSServer; + class ConnectedClient; + } + + class CachedClient; + class GroupManager; + class Group; + + enum GroupType { + GROUP_TYPE_TEMPLATE, + GROUP_TYPE_NORMAL, + GROUP_TYPE_QUERY //Only aplies for servergroups + }; + + enum GroupTarget { + GROUPTARGET_SERVER, + GROUPTARGET_CHANNEL + }; + + enum GroupNameMode { + GROUP_NAMEMODE_HIDDEN, + GROUP_NAMEMODE_BEFORE, + GROUP_NAMEMODE_BEHIND + }; +} + +DEFINE_TRANSFORMS(ts::GroupType, uint8_t); +DEFINE_TRANSFORMS(ts::GroupTarget, uint8_t); +DEFINE_TRANSFORMS(ts::GroupNameMode, uint8_t); + +namespace ts { + struct GroupMember { + std::string uid; + uint64_t cldbId; + uint64_t channelId; + std::string displayName; + std::chrono::time_point until; + }; + struct GroupAssignment { + GroupAssignment() {} + GroupAssignment(CachedClient *parent, ServerId server, uint64_t channelId, const std::shared_ptr &group, const std::chrono::time_point &until) : parent(parent), server(server), channelId(channelId), group(group), until(until) {} + + CachedClient* parent = nullptr; //Could be null! + + ServerId server = 0; + uint64_t channelId = 0; + std::shared_ptr group; + std::chrono::time_point until; + + inline bool isPermanent(){ return std::chrono::time_point_cast(until).time_since_epoch().count() == 0; } + }; + + struct CachedClient { + std::shared_ptr client; + std::vector> serverGroups; + std::map> channelGroups; + threads::Mutex lock; + }; + + class Group { + friend class GroupManager; + public: + Group(GroupManager* handle, GroupTarget target, GroupType type, GroupId groupId); + ~Group(); + + std::shared_ptr permissions(){ return this->_permissions; } + Properties& properties(){ return *this->_properties; } + + GroupNameMode nameMode(){ return (GroupNameMode) (uint8_t) properties()[property::GROUP_NAMEMODE]; } + + std::string name(){ return properties()[property::GROUP_NAME]; } + + GroupId groupId(){ return properties()[property::GROUP_ID]; } + + GroupTarget target() { return _target; } + GroupType type() { return _type; } + + void apply_properties_from_permissions(); + + inline permission::PermissionValue updateType() { + auto permission_manager = this->permissions(); /* copy the manager */ + assert(permission_manager); + const auto data = permission_manager->permission_value_flagged(permission::i_group_auto_update_type); + return data.has_value ? data.value : 0; + } + + inline bool permission_granted(const permission::PermissionType& permission, const permission::v2::PermissionFlaggedValue& granted_value, bool require_granted_value) { + auto permission_manager = this->permissions(); /* copy the manager */ + assert(permission_manager); + const auto data = permission_manager->permission_value_flagged(permission); + if(!data.has_value) { + return !require_granted_value || granted_value.has_value; + } + if(!granted_value.has_value) { + return false; + } + if(data.value == -1) { + return granted_value.value == -1; + } + return granted_value.value >= data.value; + } + + inline bool is_permanent() { + auto permission_manager = this->permissions(); /* copy the manager */ + assert(permission_manager); + const auto data = permission_manager->permission_value_flagged(permission::b_group_is_permanent); + return data.has_value ? data.value == 1 : false; + } + + inline IconId icon_id() { + auto permission_manager = this->permissions(); /* copy the manager */ + assert(permission_manager); + const auto data = permission_manager->permission_value_flagged(permission::i_icon_id); + return data.has_value ? data.value : 0; + } + private: + void setPermissionManager(const std::shared_ptr& manager); + + GroupManager* handle; + std::shared_ptr _permissions; + Properties* _properties; + GroupTarget _target; + GroupType _type; + }; + + class GroupManager { + friend class ServerChannelTree; + friend class Group; + friend class server::TSServer; + public: + static int64_t generateGroupId(sql::SqlManager* sql); + + GroupManager(const std::shared_ptr &, sql::SqlManager *, std::shared_ptr root = nullptr); + ~GroupManager(); + + bool loadGroupFormDatabase(GroupId id = 0); + + std::vector> availableGroups(bool root = true); + std::vector> availableServerGroups(bool root = true); + std::vector> availableChannelGroups(bool root = true); + + std::vector> getAssignedServerGroups(ClientDbId cldbid); + inline bool hasServerGroupAssigned(uint64_t cldbId, const std::shared_ptr& group){ + for(const auto& assign : this->getAssignedServerGroups(cldbId)) if(assign->group == group) return true; + return false; + } + + std::vector> getServerGroups(ClientDbId cldbid, server::ClientType type); + inline bool hasServerGroup(uint64_t cldbId, server::ClientType type, const std::shared_ptr& group){ + for(const auto& assign : this->getServerGroups(cldbId, type)) if(assign->group == group) return true; + return false; + } + + + //Gets the channel group (may inherited) + std::shared_ptr getChannelGroup(ClientDbId cldbId, const std::shared_ptr &, bool assign_default); + //Gets the channel group within the channel + std::shared_ptr getChannelGroupExact(ClientDbId cldbId, const std::shared_ptr&, bool assign_default); + + std::vector> defaultServerGroupGroupAssignments(ClientDbId, server::ClientType); + std::shared_ptr defaultChannelGroupAssignment(ClientDbId cldbId, const std::shared_ptr &); + + void addServerGroup(ClientDbId cldbId, std::shared_ptr, std::chrono::time_point until = std::chrono::time_point()); + void removeServerGroup(ClientDbId cldbId, std::shared_ptr); + void setChannelGroup(ClientDbId cldbId, std::shared_ptr, std::shared_ptr , std::chrono::time_point until = std::chrono::time_point()); + + std::shared_ptr createGroup(GroupTarget target, GroupType type, std::string name); + bool copyGroup(std::shared_ptr group, GroupType type, std::string name, ServerId targetServerId); + bool copyGroupPermissions(const std::shared_ptr& source, const std::shared_ptr& target); + + bool reloadGroupPermissions(std::shared_ptr); + + bool renameGroup(std::shared_ptr, std::string); + bool deleteGroup(std::shared_ptr); + bool deleteAllGroups(); + std::vector> listGroupMembers(std::shared_ptr, bool names = false); + std::vector> listGroupAssignments(ClientDbId client); + + void cleanupAssignments(ClientDbId); + + std::shared_ptr findGroup(GroupId); + std::shared_ptr findGroupLocal(GroupId); + std::vector> findGroup(GroupTarget target, std::string); + + std::shared_ptr defaultGroup(GroupTarget type, bool enforce_property = false); + + std::deque update_server_group_property(const std::shared_ptr &client, bool channel_lock); + void enableCache(std::shared_ptr client); + void disableCache(const std::shared_ptr &client); + bool isClientCached(const std::shared_ptr &client); + void clearCache(); + + + bool isLocalGroup(std::shared_ptr); + protected: + void handleChannelDeleted(std::shared_ptr ); + private: + std::shared_ptr root = nullptr; + std::weak_ptr server; + ServerId getServerId(); + + sql::SqlManager* sql; + std::vector> groups; + threads::Mutex cacheLock; + std::vector> cachedClients; + + int insertGroupFromDb(int count, char** values, char** column); + + inline std::shared_ptr resolveCached(ClientDbId cldbId); + }; +} \ No newline at end of file diff --git a/server/src/InstanceHandler.cpp b/server/src/InstanceHandler.cpp new file mode 100644 index 0000000..795154d --- /dev/null +++ b/server/src/InstanceHandler.cpp @@ -0,0 +1,662 @@ +#define XMALLOC undefined_malloc /* fix jemalloc and tomcrypt */ +#define XCALLOC undefined_calloc +#define XFREE undefined_free +#define XREALLOC undefined_realloc + +#include +#include "../../license/shared/LicenseRequest.h" +#include "src/weblist/WebListManager.h" +#include +#include "InstanceHandler.h" +#include "src/client/InternalClient.h" +#include "src/server/QueryServer.h" +#include "src/server/file/FileServer.h" +#include "SignalHandler.h" +#include +#include "ShutdownHelper.h" +#include +#include "build.h" +#include +#include +#include +#include + +#ifndef _POSIX_SOURCE + #define _POSIX_SOURCE +#endif +#include +#undef _POSIX_SOURCE +#include + +using namespace std; +using namespace std::chrono; +using namespace ts; +using namespace ts::server; + +#define INSTANCE_TICK_NAME "instance" + +#define _STRINGIFY(x) #x +#define STRINGIFY(x) _STRINGIFY(x) + +extern bool mainThreadActive; +extern InstanceHandler* serverInstance; +InstanceHandler::InstanceHandler(SqlDataManager *sql) : sql(sql) { + serverInstance = this; + this->tick_manager = make_shared(config::threads::ticking, "tick task "); + this->statistics = make_shared(nullptr, true); + this->statistics->measure_bandwidths(true); + + this->licenseHelper = make_shared(); + this->dbHelper = new DatabaseHelper(this->getSql()); + + this->_properties = new Properties(); + this->_properties->register_property_type(); + this->properties()[property::SERVERINSTANCE_FILETRANSFER_PORT] = ts::config::binding::DefaultFilePort; + this->properties()[property::SERVERINSTANCE_FILETRANSFER_HOST] = ts::config::binding::DefaultFileHost; + this->properties()[property::SERVERINSTANCE_QUERY_PORT] = ts::config::binding::DefaultQueryPort; + this->properties()[property::SERVERINSTANCE_QUERY_HOST] = ts::config::binding::DefaultQueryHost; + + + auto result = sql::command(this->getSql(), "SELECT * FROM `properties` WHERE `id` = :id AND `type` = :type AND `serverId` = :serverId", variable{":id", 0}, variable{":serverId", 0}, variable{":type", property::PropertyType::PROP_TYPE_INSTANCE}) + .query([](InstanceHandler *instance, int length, char **values, char **columns) { + string key, value; + for (int index = 0; index < length; index++) { + if (strcmp(columns[index], "key") == 0) { + key = values[index]; + } else if (strcmp(columns[index], "value") == 0) { + value = values[index] == nullptr ? "" : values[index]; + } + } + + const auto &info = property::impl::info(key); + if(*info == property::SERVERINSTANCE_UNDEFINED) { + logError(0, "Got an unknown instance property " + key); + return 0; + } + auto prop = instance->properties()[info]; + prop = value; + prop.setDbReference(true); + prop.setModified(false); + return 0; + }, this); + if (!result) cerr << result << endl; + this->_properties->registerNotifyHandler([&](Property &prop) { + if ((prop.type().flags & property::FLAG_SAVE) == 0) { + prop.setModified(false); + return; + } + + string sqlQuery; + if (prop.hasDbReference()) + sqlQuery = "UPDATE `properties` SET `value` = :value WHERE `serverId` = :sid AND `type` = :type AND `id` = :id AND `key` = :key"; + else { + prop.setDbReference(true); + sqlQuery = "INSERT INTO `properties` (`serverId`, `type`, `id`, `key`, `value`) VALUES (:sid, :type, :id, :key, :value)"; + } + + sql::command(this->getSql(), sqlQuery, variable{":sid", 0}, variable{":type", property::PropertyType::PROP_TYPE_INSTANCE}, variable{":id", 0}, variable{":key", prop.type().name}, variable{":value", prop.value()}) + .executeLater().waitAndGetLater(LOG_SQL_CMD, sql::result{1, "future failed"}); + }); + this->properties()[property::SERVERINSTANCE_DATABASE_VERSION] = this->sql->getVersion(); + + + globalServerAdmin = std::make_shared(this->getSql(), nullptr, "serveradmin", true); + static_pointer_cast(globalServerAdmin)->setSharedLock(globalServerAdmin); + ts::server::DatabaseHelper::assignDatabaseId(this->getSql(), 0, globalServerAdmin); + + this->_musicRoot = std::make_shared(this->getSql(), nullptr, "Music Manager", false); + static_pointer_cast(this->_musicRoot)->setSharedLock(this->_musicRoot); + + { + this->groupManager = std::make_shared(nullptr, this->getSql()); + this->groupManager->loadGroupFormDatabase(); + + if (this->groupManager->availableServerGroups(false).empty()){ + if(!this->setupDefaultGroups()){ + logCritical(LOG_INSTANCE, "Could not setup server instance! Stopping..."); + mainThreadActive = false; + return; + } + } + + debugMessage(LOG_INSTANCE, "Instance admin group id " + to_string(this->properties()[property::SERVERINSTANCE_ADMIN_SERVERQUERY_GROUP].as())); + auto instance_server_admin = this->groupManager->findGroup(this->properties()[property::SERVERINSTANCE_ADMIN_SERVERQUERY_GROUP].as()); + if (!instance_server_admin) { + instance_server_admin = this->groupManager->availableServerGroups(false).front(); + logCritical(LOG_INSTANCE, "Missing instance server admin group! Using first available (" + (instance_server_admin ? instance_server_admin->name() : "nil") + ")"); + } + if(this->groupManager->listGroupAssignments(this->globalServerAdmin->getClientDatabaseId()).empty()) + this->groupManager->addServerGroup(this->globalServerAdmin->getClientDatabaseId(), instance_server_admin); + + debugMessage(LOG_INSTANCE, "Server guest group id " + to_string(this->properties()[property::SERVERINSTANCE_GUEST_SERVERQUERY_GROUP].as())); + auto instance_server_guest = this->groupManager->findGroup(this->properties()[property::SERVERINSTANCE_GUEST_SERVERQUERY_GROUP].as_save()); + if (!instance_server_guest) { + instance_server_guest = this->groupManager->availableServerGroups(false).front(); + logCritical(LOG_INSTANCE, "Missing instance server guest group! Using first available (" + (instance_server_guest ? instance_server_guest->name() : "nil") + ")"); + } + this->properties()[property::SERVERINSTANCE_GUEST_SERVERQUERY_GROUP] = instance_server_guest->groupId(); + + debugMessage(LOG_INSTANCE, "Server music group id " + to_string(this->properties()[property::SERVERINSTANCE_TEMPLATE_MUSICDEFAULT_GROUP].as())); + auto instance_server_music = this->groupManager->findGroup(this->properties()[property::SERVERINSTANCE_TEMPLATE_MUSICDEFAULT_GROUP].as_save()); + if (!instance_server_music) { + instance_server_music = instance_server_guest; + logCritical(LOG_INSTANCE, "Missing instance server music group! Using serverguest (" + (instance_server_music ? instance_server_music->name() : "nil") + ")"); + } + this->properties()[property::SERVERINSTANCE_TEMPLATE_MUSICDEFAULT_GROUP] = instance_server_music->groupId(); + } + + { + this->default_tree = make_shared(nullptr, this->getSql()); + this->default_tree->loadChannelsFromDatabase(); + + this->default_tree->deleteSemiPermanentChannels(); + if(this->default_tree->channel_count() == 0){ + logMessage(LOG_GENERAL, "Generating default tree"); + + std::shared_ptr ch; + ch = this->default_tree->createChannel(0, 0, "[cspacer01]┏╋━━━━━━◥◣◆◢◤━━━━━━╋┓"); + ch = this->default_tree->createChannel(0, ch->channelId(), "[cspacer02] TeaSpeak Server"); + ch = this->default_tree->createChannel(0, ch->channelId(), "[cspacer03]┗╋━━━━━━◥◣◆◢◤━━━━━━╋┛"); + ch = this->default_tree->createChannel(0, ch->channelId(), "[cspacer04]Default Channel"); + this->default_tree->setDefaultChannel(ch); + + this->properties()[property::SERVERINSTANCE_UNIQUE_ID] = ""; /* we def got a new instance */ + } + if(!this->default_tree->getDefaultChannel()) this->default_tree->setDefaultChannel(this->default_tree->findChannel("[cspacer04]Default Channel", nullptr)); + if(!this->default_tree->getDefaultChannel()) this->default_tree->setDefaultChannel(*this->default_tree->channels().begin()); + assert(this->default_tree->getDefaultChannel()); + } + + { + this->default_server_properties = serverInstance->databaseHelper()->loadServerProperties(nullptr); + } + + + if(this->properties()[property::SERVERINSTANCE_MONTHLY_TIMESTAMP].as() == 0) { + debugMessage(LOG_INSTANCE, "Setting up monthly reset timestamp!"); + this->properties()[property::SERVERINSTANCE_MONTHLY_TIMESTAMP] = duration_cast(system_clock::now().time_since_epoch()).count(); + } + + this->banMgr = new BanManager(this->getSql()); + this->banMgr->loadBans(); + + this->web_list = make_shared(); +} + +void InstanceHandler::executeTick(TSServer* server) { + auto str = "server_" + to_string(server->getServerId()); + if(!this->tick_manager->schedule(str, std::bind(&TSServer::executeServerTick, server), milliseconds(500))) { + logCritical(LOG_INSTANCE, "Could not schedule server ticking task!"); + } +} + +void InstanceHandler::cancelExecute(TSServer* server) { + auto str = "server_" + to_string(server->getServerId()); + if(!this->tick_manager->cancelTask(str)){ + logError(LOG_INSTANCE, "Could not stop server tick task!"); + } +} + + +InstanceHandler::~InstanceHandler() { + delete this->_properties; + delete this->banMgr; + delete this->dbHelper; + + groupManager = nullptr; + globalServerAdmin = nullptr; + _musicRoot = nullptr; + + licenseHelper = nullptr; + statistics = nullptr; + tick_manager = nullptr; +} + +inline sockaddr_in* resolveAddress(const string& host, uint16_t port) { + hostent* record = gethostbyname(host.c_str()); + if (!record) { + cerr << "Cant resolve bind host! (" << host << ")" << endl; + return nullptr; + } + auto addr = new sockaddr_in{}; + addr->sin_addr.s_addr = ((in_addr *) record->h_addr)->s_addr; + addr->sin_family = AF_INET; + addr->sin_port = htons(port); + return addr; +} + +bool InstanceHandler::startInstance() { + if (this->active) return false; + active = true; + this->web_list->enabled = ts::config::server::enable_teamspeak_weblist; + + this->sslMgr = new ssl::SSLManager(); + if(!this->sslMgr->initialize()) { + logCritical("Failed to initialize ssl manager."); + return false; + } + + //Startup file server + sockaddr_in *fAddr = resolveAddress(this->properties()[property::SERVERINSTANCE_FILETRANSFER_HOST].as(), this->properties()[property::SERVERINSTANCE_FILETRANSFER_PORT].as()); + if (!fAddr) { + logCritical(LOG_FT, "Could not resolve file server host"); + return false; + } + logMessage(LOG_FT, "Starting server on {}:{}", inet_ntoa(fAddr->sin_addr), ntohs(fAddr->sin_port)); + + fileServer = new ts::server::FileServer(); + if (!fileServer->start(*fAddr)) { + logCritical(LOG_FT, "Failed to start file server."); + delete fAddr; + return false; + } + delete fAddr; + + if(config::query::sslMode > 0) { + string error; + auto result = this->sslMgr->initializeContext("query", config::query::ssl::keyFile, config::query::ssl::certFile, error, false, make_shared(ssl::SSLGenerator{ + .subjects = {}, + .issues = {{"O", "TeaSpeak"}, {"OU", "Query server"}, {"creator", "WolverinDEV"}} + })); + if(!result) { + logCritical(LOG_QUERY, "Failed to initialize query certificate! (" + error + ")"); + return false; + } + } + + string errorMessage; + queryServer = new ts::server::QueryServer(this->getSql()); + { + auto server_query = queryServer->find_query_account_by_name("serveradmin"); + if(!server_query) { + string queryPassword = rnd_string(12); + if((server_query = queryServer->create_query_account("serveradmin", 0, "serveradmin", queryPassword))) { + logMessageFmt(true, LOG_GENERAL, "------------------ [Server Query] ------------------"); + logMessageFmt(true, LOG_GENERAL, " New Admin Server Query login credentials generated"); + logMessageFmt(true, LOG_GENERAL, " Username: serveradmin"); + logMessageFmt(true, LOG_GENERAL, " Password: " + queryPassword); + logMessageFmt(true, LOG_GENERAL, "------------------ [Server Query] ------------------"); + } else { + logCriticalFmt(true,LOG_GENERAL,"Failed to create a new server admin query account!"); + } + } + } + sockaddr_in *qAddr = resolveAddress(this->properties()[property::SERVERINSTANCE_QUERY_HOST].as(), this->properties()[property::SERVERINSTANCE_QUERY_PORT].as()); + if (!qAddr) { + logCritical(LOG_QUERY, "Could not resolve query server host"); + return false; + } + logMessage(LOG_QUERY, "Starting server on {}:{}", inet_ntoa(qAddr->sin_addr), ntohs(qAddr->sin_port)); + if (!queryServer->start(*qAddr, errorMessage)) { + logCritical(LOG_QUERY, "Could not start Query server.\nMessage: " + errorMessage); + delete qAddr; + return false; + } + delete qAddr; + +#ifdef COMPILE_WEB_CLIENT + if(config::web::activated) { + string error; + for(auto& certificate : config::web::ssl::certificates) { + auto result = this->sslMgr->initializeContext("web_" + get<0>(certificate), get<1>(certificate), get<2>(certificate), error, false, make_shared(ts::ssl::SSLGenerator{ + .subjects = {}, + .issues = {{"O", "TeaSpeak"}, {"OU", "Web server"}, {"creator", "WolverinDEV"}} + })); + if(!result) { + logError(LOG_INSTANCE, "Failed to initialize web certificate for servername {}! (Private key: {}, Certificate: {})", get<0>(certificate), get<1>(certificate), get<2>(certificate)); + continue; + } + } + + auto rsa = this->sslMgr->initializeSSLKey("teaforo_sign", R"( +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsfsTByPTE0aIqi6pJl4f +Xr4UqsIZkU5wYtktKIFpoDGHCHspCTMXF0fOXJkSGaTBtvTUEraRZz0+zshU+aiy +92qZ9DlC6Px3A94WW6mS48q2wEqZuj2q6Is4vf+DdjiqTzcZsqVJQj6WcqPg24pZ +cC9Yg9mys1IoBEoHmUXYVMFC5ibzRwjxfcAan0qSa+h983pL+4hva/+nHK1kaR2w +feTyUopv10ndkg9jxvAt5+roV3ID2fuHZBsEknWwFTTTjzPsf2Y+B6YYh4CW7haw +vf11A3V+xDFIrSbS9pix1jWgztrQbUcHDczQozArcyflE5+rUMuPPRp3IyRuSq/6 +FwIDAQAB +-----END PUBLIC KEY----- +)", error, true); + if(!rsa) { //TODO just disable the forum verification + logCritical("Failed to initialize WebClient TeaForum key! (" + error + ")"); + return false; + } + this->web_event_loop = make_shared(); + } +#endif + + if(config::experimental_31) { + this->teamspeak_license.reset(new TeamSpeakLicense("protocol_key.txt")); + if(!this->teamspeak_license->load(errorMessage)) { + logCritical(LOG_INSTANCE, "§cFailed to load the protocol key chain! ({})", errorMessage); + return false; + } + } + + this->voiceServerManager = new ServerManager(this); + if (!this->voiceServerManager->initialize(true)) { + logCritical(LOG_INSTANCE, "Could not load servers!"); + delete this->voiceServerManager; + this->voiceServerManager = nullptr; + return false; + } + + if (voiceServerManager->serverInstances().empty()) { + logMessage(LOG_INSTANCE, "§aCreating new TeaSpeak server..."); + auto server = voiceServerManager->createServer(config::binding::DefaultVoiceHost, config::voice::default_voice_port); + if (!server) + logCritical(LOG_INSTANCE, "§cCould not create a new server!"); + else { + string error; + if (!server->start(error)) { + logCritical(LOG_INSTANCE, "Could not start new server. Message: \n" + error); + } + } + } + + startTimestamp = system_clock::now(); + this->voiceServerManager->executeAutostart(); + + this->scheduler()->schedule(INSTANCE_TICK_NAME, bind(&InstanceHandler::tickInstance, this), milliseconds(100)); + return true; +} + +void InstanceHandler::stopInstance() { + { + lock_guard lock(this->activeLock); + if(!this->active) return; + this->active = false; + this->activeCon.notify_all(); + } + this->web_list->enabled = false; + + threads::MutexLock lock_tick(this->lock_tick); + this->scheduler()->cancelTask(INSTANCE_TICK_NAME); + + this->save_channel_permissions(); + this->save_group_permissions(); + + debugMessage(LOG_INSTANCE, "Stopping all virtual servers"); + if (this->voiceServerManager) + this->voiceServerManager->shutdownAll(ts::config::messages::applicationStopped); + delete this->voiceServerManager; + this->voiceServerManager = nullptr; + debugMessage(LOG_INSTANCE, "All virtual server stopped"); + + debugMessage(LOG_QUERY, "Stopping query server"); + if (this->queryServer) this->queryServer->stop(); + delete this->queryServer; + this->queryServer = nullptr; + debugMessage(LOG_QUERY, "Query server stopped"); + + debugMessage(LOG_FT, "Stopping file server"); + if (this->fileServer) this->fileServer->stop(); + delete this->fileServer; + this->fileServer = nullptr; + debugMessage(LOG_FT, "File server stopped"); + + delete this->sslMgr; + this->sslMgr = nullptr; + + this->web_event_loop = nullptr; +} + +void InstanceHandler::tickInstance() { + threads::MutexLock lock(this->lock_tick); + if(!this->active) return; + + auto now = system_clock::now(); + + if(generalUpdateTimestamp + seconds(5) < now) { + generalUpdateTimestamp = now; + + { + ALARM_TIMER(t, "InstanceHandler::tickInstance -> db helper tick", milliseconds(5)); + this->dbHelper->tick(); + } + { + ALARM_TIMER(t, "InstanceHandler::tickInstance -> license tick", milliseconds(5)); + this->licenseHelper->tick(); + } + } + { + ALARM_TIMER(t, "InstanceHandler::tickInstance -> flush", milliseconds(5)); + //logger::flush(); + } + if(statisticsUpdateTimestamp + seconds(5) < now) { + statisticsUpdateTimestamp = now; + { + ALARM_TIMER(t, "InstanceHandler::tickInstance -> statistics tick", milliseconds(5)); + this->statistics->tick(); + } + + { + ALARM_TIMER(t, "InstanceHandler::tickInstance -> statistics tick [monthly]", milliseconds(2)); + auto month_timestamp = system_clock::time_point() + seconds(this->properties()[property::SERVERINSTANCE_MONTHLY_TIMESTAMP].as()); + auto time_t_old = system_clock::to_time_t(month_timestamp); + auto time_t_new = system_clock::to_time_t(system_clock::now()); + + tm tm_old{}, tm_new{}; + gmtime_r(&time_t_old, &tm_old); + gmtime_r(&time_t_new, &tm_new); + + if(tm_old.tm_mon != tm_new.tm_mon) { + logMessage(LOG_INSTANCE, "We entered a new month! Resetting monthly stats!"); + if(!this->resetMonthlyStats()) logError(LOG_INSTANCE, "Monthly stats reset failed!"); + else { + logMessage(LOG_INSTANCE, "Monthly stats reset done!"); + this->properties()[property::SERVERINSTANCE_MONTHLY_TIMESTAMP] = duration_cast(system_clock::now().time_since_epoch()).count(); + } + } + } + } + if(memcleanTimestamp + minutes(10) < now) { + memcleanTimestamp = now; + { + { + ALARM_TIMER(t, "InstanceHandler::tickInstance -> mem cleanup -> buffer cleanup", milliseconds(5)); + auto info = buffer::cleanup_buffers(buffer::cleanmode::CHUNKS_BLOCKS); + if(info.bytes_freed_buffer != 0 || info.bytes_freed_internal != 0) + logMessage(LOG_INSTANCE, "Cleanupped buffers. (Buffer: {}, Internal: {})", info.bytes_freed_buffer, info.bytes_freed_internal); + } + } + } + { + ALARM_TIMER(t, "InstanceHandler::tickInstance -> fileserver tick", milliseconds(5)); + if(this->fileServer) this->fileServer->instanceTick(); + } + { + ALARM_TIMER(t, "InstanceHandler::tickInstance -> sql_test tick", milliseconds(5)); + if(this->sql && this->active) { + if(sqlTestTimestamp + seconds(10) < now) { + sqlTestTimestamp = now; + threads::Thread(THREAD_SAVE_OPERATIONS | THREAD_DETACHED, [&](){ + auto command = this->sql->sql()->getType() == sql::TYPE_SQLITE ? "SELECT * FROM `sqlite_master`" : "SHOW TABLES"; + auto result = sql::command(this->getSql(), command).query([command](int, string*, string*){ return 0; }); + if(!result) { + logCritical(LOG_INSTANCE, "Dummy sql connection test faild! (Failed to execute command \"{}\". Error message: {})", command, result.fmtStr()); + logCritical(LOG_INSTANCE, "Stopping instance!"); + ts::server::shutdownInstance("invalid sql connection!"); + } + //debugMessage(0, "SQL connection still alive!"); + }); + } + } + } + if(groupSaveTimestamp + minutes(1) < now) { + speachUpdateTimestamp = now; + this->save_group_permissions(); + } + if(channelSaveTimestamp + minutes(1) < now) { + speachUpdateTimestamp = now; + this->save_channel_permissions(); + } + if(speachUpdateTimestamp + seconds(5) < now) { + speachUpdateTimestamp = now; + + this->properties()[property::SERVERINSTANCE_SPOKEN_TIME_ALIVE] = this->calculateSpokenTime().count(); + this->properties()[property::SERVERINSTANCE_SPOKEN_TIME_TOTAL] = + this->properties()[property::SERVERINSTANCE_SPOKEN_TIME_ALIVE].as() + + this->properties()[property::SERVERINSTANCE_SPOKEN_TIME_DELETED].as() + + this->properties()[property::SERVERINSTANCE_SPOKEN_TIME_VARIANZ].as(); + } + + this->web_list->tick(); +} + +void InstanceHandler::save_group_permissions() { + auto groups = this->getGroupManager()->availableGroups(false); + for(auto& group : groups) { + auto permissions = group->permissions(); + if(permissions->require_db_updates()) { + auto begin = system_clock::now(); + serverInstance->databaseHelper()->saveGroupPermissions(nullptr, group->groupId(), permissions); + auto end = system_clock::now(); + debugMessage(0, "Saved instance group permissions for group {} ({}) in {}ms", group->groupId(), group->name(), duration_cast(end - begin).count()); + } + } +} + +void InstanceHandler::save_channel_permissions() { + shared_lock tree_lock(this->getChannelTreeLock()); + auto channels = this->getChannelTree()->channels(); + tree_lock.unlock(); + + for(auto& channel : channels) { + auto permissions = channel->permissions(); + if(permissions->require_db_updates()) { + auto begin = system_clock::now(); + serverInstance->databaseHelper()->saveChannelPermissions(nullptr, channel->channelId(), permissions); + auto end = system_clock::now(); + debugMessage(0, "Saved instance channel permissions for channel {} ({}) in {}ms", channel->channelId(), channel->name(), duration_cast(end - begin).count()); + } + } +} + +std::chrono::milliseconds InstanceHandler::calculateSpokenTime() { + std::chrono::milliseconds result{}; + for(const auto& server : this->voiceServerManager->serverInstances()) + result += server->spoken_time; + return result; +} + +void InstanceHandler::resetSpeechTime() { + this->properties()[property::SERVERINSTANCE_SPOKEN_TIME_DELETED] = 0; + this->properties()[property::SERVERINSTANCE_SPOKEN_TIME_VARIANZ] = 0; + for(const auto& server : this->voiceServerManager->serverInstances()) + server->properties()[property::VIRTUALSERVER_SPOKEN_TIME] = 0; +} + +#include +#include +#include +#include +#include + +string get_mac_address() { + struct ifreq ifr; + struct ifconf ifc; + char buf[1024]; + int success = 0; + + int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + if (sock == -1) { return "undefined"; }; + + ifc.ifc_len = sizeof(buf); + ifc.ifc_buf = buf; + if (ioctl(sock, SIOCGIFCONF, &ifc) == -1) { /* handle error */ } + + struct ifreq* it = ifc.ifc_req; + const struct ifreq* const end = it + (ifc.ifc_len / sizeof(struct ifreq)); + + for (; it != end; ++it) { + strcpy(ifr.ifr_name, it->ifr_name); + if (ioctl(sock, SIOCGIFFLAGS, &ifr) == 0) { + if (! (ifr.ifr_flags & IFF_LOOPBACK)) { // don't count loopback + if (ioctl(sock, SIOCGIFHWADDR, &ifr) == 0) { + success = 1; + break; + } + } + } + else { return "undefined"; } + } + + return success ? base64::encode(ifr.ifr_hwaddr.sa_data, 6) : "undefined"; +} + +#define SN_BUFFER 1024 +std::shared_ptr InstanceHandler::generateLicenseData() { + auto response = make_shared(); + response->license = config::license; + response->servers_online = this->voiceServerManager->runningServers(); + auto report = this->voiceServerManager->clientReport(); + response->client_online = report.clients_ts; + response->web_clients_online = report.clients_web; + response->bots_online = report.bots; + response->queries_online = report.queries; + response->speach_total = this->properties()[property::SERVERINSTANCE_SPOKEN_TIME_TOTAL].as(); + response->speach_varianz = this->properties()[property::SERVERINSTANCE_SPOKEN_TIME_VARIANZ].as(); + response->speach_online = this->properties()[property::SERVERINSTANCE_SPOKEN_TIME_ALIVE].as(); + response->speach_dead = this->properties()[property::SERVERINSTANCE_SPOKEN_TIME_DELETED].as(); + + { + auto info = make_shared(); + info->timestamp = duration_cast(system_clock::now().time_since_epoch()).count(); + info->version = build::version()->string(true); + + { /* uname */ + utsname retval{}; + if(uname(&retval) < 0) { // <---- + info->uname = "unknown (" + string(strerror(errno)) + ")"; + } else { + char buffer[SN_BUFFER]; + snprintf(buffer, SN_BUFFER, "sys:%s version:%s release:%s", retval.sysname, retval.version, retval.release); + info->uname = string(buffer); + } + + } + + { /* unique id */ + auto property_unique_id = this->properties()[property::SERVERINSTANCE_UNIQUE_ID]; + if(property_unique_id.as().empty()) + property_unique_id = rnd_string(64); + + auto hash = digest::sha256(info->uname); + hash = digest::sha256(hash + property_unique_id.as() + get_mac_address()); + info->unique_identifier = base64::encode(hash); + } + + response->info = info; + } + return response; +} + +bool InstanceHandler::resetMonthlyStats() { + //serverId` INTEGER DEFAULT -1, `type` INTEGER, `id` INTEGER, `key` VARCHAR(" UNKNOWN_KEY_LENGTH "), `value` TEXT + auto result = sql::command(this->getSql(), "UPDATE `properties` SET `value` = 0 WHERE " + "`key` = 'serverinstance_monthly_timestamp' OR " + "`key` = 'virtualserver_month_bytes_downloaded' OR " + "`key` = 'virtualserver_month_bytes_uploaded' OR " + "`key` = 'client_month_bytes_downloaded' OR " + "`key` = 'client_month_bytes_uploaded' OR " + "`key` = 'client_month_online_time'").execute(); + if(!result) { + logError(LOG_INSTANCE, "Failed to reset monthly stats ({})", result.fmtStr()); + return false; + } + + for(const auto& server : this->getVoiceServerManager()->serverInstances()) { + server->properties()[property::VIRTUALSERVER_MONTH_BYTES_UPLOADED] = 0; + server->properties()[property::VIRTUALSERVER_MONTH_BYTES_DOWNLOADED] = 0; + + for(const auto& client : server->getClients()) { + client->properties()[property::CLIENT_MONTH_ONLINE_TIME] = 0; + client->properties()[property::CLIENT_MONTH_BYTES_UPLOADED] = 0; + client->properties()[property::CLIENT_MONTH_BYTES_DOWNLOADED] = 0; + } + } + return true; +} \ No newline at end of file diff --git a/server/src/InstanceHandler.h b/server/src/InstanceHandler.h new file mode 100644 index 0000000..0f63226 --- /dev/null +++ b/server/src/InstanceHandler.h @@ -0,0 +1,116 @@ +#pragma once + +#include +#include +#include "ServerManager.h" +#include "../../license/shared/LicenseRequest.h" +#include "lincense/LicenseHelper.h" +#include +#include "manager/SqlDataManager.h" +#include "lincense/TeamSpeakLicense.h" +#include "server/WebIoManager.h" + +namespace ts { + namespace weblist { + class WebListManager; + } + + namespace server { + class InstanceHandler { + public: + explicit InstanceHandler(SqlDataManager*); + ~InstanceHandler(); + + bool startInstance(); + void stopInstance(); + + ts::Properties& properties(){ + return *_properties; + } + + std::shared_ptr getInitalServerAdmin(){ return globalServerAdmin; } + std::shared_ptr getGroupManager(){ return groupManager; } + + std::shared_ptr getChannelTree() { return this->default_tree; } + std::shared_mutex& getChannelTreeLock() { return this->default_tree_lock; } + + ServerManager* getVoiceServerManager(){ return this->voiceServerManager; } + FileServer* getFileServer(){ return fileServer; } + QueryServer* getQueryServer(){ return queryServer; } + DatabaseHelper* databaseHelper(){ return this->dbHelper; } + BanManager* banManager(){ return this->banMgr; } + ssl::SSLManager* sslManager(){ return this->sslMgr; } + sql::SqlManager* getSql(){ return sql->sql(); } + + std::chrono::time_point getStartTimestamp(){ return startTimestamp; } + + void executeTick(TSServer*); + void cancelExecute(TSServer*); + + const std::shared_ptr& musicRoot() { return this->_musicRoot; } + + std::chrono::milliseconds calculateSpokenTime(); + void resetSpeechTime(); + + bool resetMonthlyStats(); + + std::shared_ptr getStatistics(){ return statistics; } + std::shared_ptr scheduler(){ return this->tick_manager; } + std::shared_ptr generateLicenseData(); + + std::shared_ptr getTeamSpeakLicense() { return this->teamspeak_license; } + std::shared_ptr getDefaultServerProperties() { return this->default_server_properties; } + std::shared_ptr getWebIoLoop() { return this->web_event_loop; } + std::shared_ptr getWebList() { return this->web_list; } + private: + std::mutex activeLock; + std::condition_variable activeCon; + bool active = false; + + std::chrono::system_clock::time_point startTimestamp; + std::chrono::system_clock::time_point sqlTestTimestamp; + std::chrono::system_clock::time_point speachUpdateTimestamp; + std::chrono::system_clock::time_point groupSaveTimestamp; + std::chrono::system_clock::time_point channelSaveTimestamp; + std::chrono::system_clock::time_point generalUpdateTimestamp; + std::chrono::system_clock::time_point statisticsUpdateTimestamp; + std::chrono::system_clock::time_point memcleanTimestamp; + SqlDataManager* sql; + + FileServer* fileServer = nullptr; + QueryServer* queryServer = nullptr; + ServerManager* voiceServerManager = nullptr; + DatabaseHelper* dbHelper = nullptr; + BanManager* banMgr = nullptr; + ssl::SSLManager* sslMgr = nullptr; + + ts::Properties* _properties = nullptr; + + std::shared_ptr web_event_loop = nullptr; + std::shared_ptr web_list = nullptr; + + std::shared_ptr default_server_properties = nullptr; + std::shared_ptr default_tree = nullptr; + std::shared_mutex default_tree_lock; + std::shared_ptr groupManager = nullptr; + + std::shared_ptr globalServerAdmin = nullptr; + std::shared_ptr _musicRoot = nullptr; + + std::shared_ptr licenseHelper = nullptr; + std::shared_ptr statistics = nullptr; + std::shared_ptr tick_manager = nullptr; + + std::shared_ptr teamspeak_license = nullptr; + + threads::Mutex lock_tick; + private: + bool setupDefaultGroups(); + void tickInstance(); + + void save_group_permissions(); + void save_channel_permissions(); + }; + } +} +extern ts::server::InstanceHandler* serverInstance; \ No newline at end of file diff --git a/server/src/InstanceHandlerSetup.cpp b/server/src/InstanceHandlerSetup.cpp new file mode 100644 index 0000000..31cdcc4 --- /dev/null +++ b/server/src/InstanceHandlerSetup.cpp @@ -0,0 +1,146 @@ +#include +#include +#include +#include +#include +#include "InstanceHandler.h" +#include "src/client/InternalClient.h" +#include "src/server/QueryServer.h" +#include "src/server/file/FileServer.h" + +using namespace std; +using namespace std::chrono; +using namespace ts; +using namespace ts::server; + +#define TEST_COMMENT if(line.find('#') == 0 || line.empty()) continue +#define PERMISSION_TEMPLATE_FILE "resources/permissions.template" + +struct GroupInfo { + /** + * 0 = Query + * 1 = Server + * 2 = Channel + */ + int target; + string propertyName; + string name; + /* permission type, value, granted, skip, negate */ + deque> permissions; +}; + +/* TODO may use a transaction here? */ +bool InstanceHandler::setupDefaultGroups() { + debugMessage(LOG_INSTANCE, "Creating new instance groups"); + deque> groups; + + ifstream in(PERMISSION_TEMPLATE_FILE); + if(!in) { + logCritical(LOG_INSTANCE, "Could not open default permissions file {}", PERMISSION_TEMPLATE_FILE); + return false; + } + string line; + while(getline(in, line)){ + TEST_COMMENT; + + if(line != "--start") { + logCritical(LOG_INSTANCE, R"(Permission template file contains invalid start line ("{}", expected "{}")!)", line, "--start"); + return false; + } + auto group = make_shared(); + while(true){ + getline(in, line); + TEST_COMMENT; + + if(line == "--end") break; + if(line.find("name:") == 0) { + group->name = line.substr(5); + continue; + } + if(line.find("target:") == 0) { + group->target = stoi(line.substr(7)); + continue; + } + if(line.find("property:") == 0) { + group->propertyName = line.substr(9); + continue; + } + + if(line.find("permission:") == 0) { + line = line.substr(11); + auto assign_index = line.find('='); + + string permission_name = line.substr(0, assign_index); + string string_value = line.substr(assign_index + 1); + string string_granted, string_skip, string_negate; + + if(string_value.find(',') != -1) { + string_granted = string_value.substr(string_value.find(',') + 1); + string_value = string_value.substr(0, string_value.find(',')); + } + if(string_granted.find(',') != -1) { + string_skip = string_granted.substr(string_granted.find(',') + 1); + string_granted = string_granted.substr(0, string_granted.find(',')); + } + if(string_skip.find(',') != -1) { + string_negate = string_skip.substr(string_skip.find(',') + 1); + string_skip = string_skip.substr(0, string_skip.find(',')); + } + + auto permInfo = permission::resolvePermissionData(permission_name); + if(permInfo->type == permission::unknown){ + logError(LOG_INSTANCE, "Default permission file contains unknown permission. Key: {}", permission_name); + continue; + } + + permission::PermissionValue permission_value; + try { + permission_value = stoi(string_value); + } catch(std::exception& ex) { + logError(LOG_INSTANCE, "Failed to parse value for key {}. Value: {}", permission_name, string_value); + continue; + } + + permission::PermissionValue permission_granted = permNotGranted; + if(!string_granted.empty()) { + try { + permission_granted = stoi(string_granted); + } catch(std::exception& ex) { + logError(LOG_INSTANCE, "Failed to parse granted value for key {}. Value: {}", permission_name, string_granted); + continue; + } + } + + bool flag_skip = string_skip == "true" || string_skip == "1"; + bool flag_negate = string_negate == "true" || string_negate == "1"; + + group->permissions.emplace_back(make_tuple(permInfo->type, permission_value, permission_granted, flag_skip, flag_negate)); + } + } + groups.push_back(group); + } + + debugMessage(LOG_INSTANCE, "Read " + to_string(groups.size()) + " default groups"); + for(const auto& info : groups) { + debugMessage(LOG_INSTANCE, "Creating default group {} with type {}", info->name, to_string(info->target)); + //Query groups + auto group = this->groupManager->createGroup( + info->target != 2 ? GroupTarget::GROUPTARGET_SERVER : GroupTarget::GROUPTARGET_CHANNEL, + info->target == 0 ? GroupType::GROUP_TYPE_QUERY : GroupType::GROUP_TYPE_TEMPLATE, + info->name + ); + for(auto perm : info->permissions) { + group->permissions()->set_permission(get<0>(perm), {get<1>(perm), get<2>(perm)}, permission::v2::set_value, permission::v2::set_value, get<3>(perm), get<4>(perm)); + } + if(!info->propertyName.empty()) { + const auto& prop = property::impl::info(info->propertyName); + if(*prop == property::SERVERINSTANCE_UNDEFINED) { + logCritical(LOG_INSTANCE, "Invalid template property name: " + info->propertyName); + } else { + this->properties()[prop] = group->groupId(); + } + } + } + this->getSql()->pool->threads()->wait_for(); //Wait for all permissions to flush + return true; +} diff --git a/server/src/ServerManager.cpp b/server/src/ServerManager.cpp new file mode 100644 index 0000000..b670ff6 --- /dev/null +++ b/server/src/ServerManager.cpp @@ -0,0 +1,402 @@ +#include +#include +#include "ServerManager.h" +#include "src/server/VoiceServer.h" +#include "InstanceHandler.h" +#include "src/server/file/FileServer.h" +#include "src/client/ConnectedClient.h" + +using namespace std; +using namespace std::chrono; +using namespace ts::server; + +ServerManager::ServerManager(InstanceHandler* handle) : handle(handle) { + this->puzzles = new protocol::PuzzleManager(); + this->handshakeTickers = new threads::Scheduler(1, "handshake ticker"); + this->execute_loop = new event::EventExecutor("executor #"); + this->_ioManager = new io::VoiceIOManager(); + + this->handshakeTickers->schedule("ticker", [&](){ this->tickHandshakeClients(); }, seconds(1)); +} + +ServerManager::~ServerManager() { + this->state = State::STOPPED; + { + threads::MutexLock lock(this->instanceLock); + this->instances.clear(); + } + + { + this->acknowledge.condition.notify_all(); + if(this->acknowledge.thread) + this->acknowledge.thread->join(); + delete this->acknowledge.thread; + } + + delete this->puzzles; + this->puzzles = nullptr; + + if(this->execute_loop) + this->execute_loop->shutdown(); + delete this->execute_loop; + this->execute_loop = nullptr; + + if(this->handshakeTickers) { + this->handshakeTickers->shutdown(); + } + delete this->handshakeTickers; + this->handshakeTickers = nullptr; + + if(this->_ioManager) this->_ioManager->shutdownGlobally(); + delete this->_ioManager; + this->_ioManager = nullptr; +} + +bool ServerManager::initialize(bool autostart) { + this->execute_loop->initialize(1); + + this->state = State::STARTING; + logMessage("Generating server puzzles..."); + auto start = system_clock::now(); + this->puzzles->precomputePuzzles(config::voice::DefaultPuzzlePrecomputeSize); + logMessage("Puzzles generated! Time required: " + to_string(duration_cast(system_clock::now() - start).count()) + "ms"); + + size_t serverCount = 0; + sql::command(this->handle->getSql(), "SELECT COUNT(`serverId`) FROM `servers`").query([](size_t& ptr, int, char** v, char**) { ptr = stoll(v[0]); return 0; }, serverCount); + + { + logMessage(LOG_INSTANCE, "Loading startup cache (This may take a while)"); + auto beg = system_clock::now(); + this->handle->databaseHelper()->loadStartupCache(); + auto end = system_clock::now(); + logMessage(LOG_INSTANCE, "Required {}ms to preload the startup cache. Cache needs {}mb", + duration_cast(end - beg).count(), + this->handle->databaseHelper()->cacheBinarySize() / 1024 / 1024 + ); + } + + auto beg = system_clock::now(); + size_t server_count = 0; + sql::command(this->handle->getSql(), "SELECT `serverId`, `host`, `port` FROM `servers`").query([&](ServerManager* mgr, int length, char** values, char** columns){ + ServerId id = 0; + std::string host; + uint16_t port = 0; + + for(int index = 0; index < length; index++) + if(strcmp(columns[index], "serverId") == 0) + id = static_cast(stoll(values[index])); + else if(strcmp(columns[index], "host") == 0) + host = values[index]; + else if(strcmp(columns[index], "port") == 0) + port = static_cast(stoul(values[index])); + + assert(id != 0); + assert(port != 0); + assert(!host.empty()); + + auto server = make_shared(id, this->handle->getSql()); + server->self = server; + if(!server->initialize(true)) { + //FIXME error handling + } + server->properties()[property::VIRTUALSERVER_HOST] = host; + server->properties()[property::VIRTUALSERVER_PORT] = port; + + { + threads::MutexLock l(this->instanceLock); + this->instances.push_back(server); + } + + if(autostart && server->properties()[property::VIRTUALSERVER_AUTOSTART].as()) { + logMessage(server->getServerId(), "Starting server"); + string msg; + try { + if(!server->start(msg)) + logError(server->getServerId(), "Failed to start server.\n Message: " + msg); + } catch (const std::exception& ex) { + logError(string() + "Could not start server! Got an active exception. Message " + ex.what()); + } + } + if(id > 0) + this->handle->databaseHelper()->clearStartupCache(id); + + server_count++; + return 0; + }, this); + auto time = duration_cast(system_clock::now() - beg).count(); + logMessage(LOG_INSTANCE, "Loaded {} servers within {}ms. Server/sec: {:2f}", + server_count, + time, + (float) server_count / (time / 1024 == 0 ? 1 : time / 1024) + ); + this->handle->databaseHelper()->clearStartupCache(0); + this->adjust_executor_threads(); + + { + this->acknowledge.thread = new threads::Thread(THREAD_SAVE_OPERATIONS, [&] { + system_clock::time_point next_execute = system_clock::now() + milliseconds(500); + while(this->state == State::STARTED || this->state == State::STARTING) { + unique_lock lock(this->acknowledge.lock); + this->acknowledge.condition.wait_until(lock, next_execute, [&](){ return this->state != State::STARTED && this->state != State::STARTING; }); + + auto now = system_clock::now(); + next_execute = now + milliseconds(500); + for(const auto& server : this->serverInstances()) { + auto vserver = server->getVoiceServer(); //Read this only once + if(vserver) + vserver->execute_resend(now, next_execute); + } + } + return 0; + }); + } + + this->state = State::STARTED; + return true; +} + +shared_ptr ServerManager::findServerById(ServerId sid) { + for(auto server : this->serverInstances()) + if(server->getServerId() == sid) + return server; + return nullptr; +} + +shared_ptr ServerManager::findServerByPort(uint16_t port) { + for(const auto& server : this->serverInstances()){ + if(server->properties()[property::VIRTUALSERVER_PORT] == port) return server; + if(server->running() && server->getVoiceServer()) + for(const auto& binding : server->getVoiceServer()->activeBindings()) + if(binding->address_port() == port) return server; + } + return nullptr; +} + +uint16_t ServerManager::next_available_port() { + auto instances = this->serverInstances(); + deque unallowed_ports; + for(const auto& instance : instances) { + unallowed_ports.push_back(instance->properties()[property::VIRTUALSERVER_PORT].as()); + + auto vserver = instance->getVoiceServer(); + if(instance->running() && vserver) { + for(const auto& binding : vserver->activeBindings()) { + unallowed_ports.push_back(binding->address_port()); + } + } + } + + uint16_t port = config::voice::default_voice_port; + while(true) { + if(port < 1024) goto c; + + for(auto& p : unallowed_ports) { + if(p == port) + goto c; + } + break; + + c: + port++; + } + return port; +} + +ts::ServerId ServerManager::next_available_server_id(bool& success) { + auto server_id_base = this->handle->properties()[property::SERVERINSTANCE_VIRTUAL_SERVER_ID_INDEX].as(); + if(server_id_base > 65530) { + success = false; + return 0; + } + ServerId serverId = server_id_base != 0 ? server_id_base : (ServerId) 1; + + auto instances = this->serverInstances(); + vector used_ids; + used_ids.reserve(instances.size()); + + for(const auto& server : instances) + used_ids.push_back(server->getServerId()); + + std::stable_sort(used_ids.begin(), used_ids.end(), [](ServerId a, ServerId b) { return b > a; }); + while(true) { + auto it = used_ids.begin(); + while(it != used_ids.end() && *it < serverId) + it++; + + if(it == used_ids.end() || *it != serverId) { + break; + } else { + used_ids.erase(used_ids.begin(), it + 1); + serverId++; + } + } + + /* increase counter */ + if(server_id_base != 0) + this->handle->properties()[property::SERVERINSTANCE_VIRTUAL_SERVER_ID_INDEX] = serverId; + + success = true; + return serverId; +} + +ServerReport ServerManager::report() { + ServerReport result{}; + for(const auto& sr : this->serverInstances()) { + result.avariable++; + if(sr->running()) { + result.online++; + result.slots += sr->properties()[property::VIRTUALSERVER_MAXCLIENTS].as(); + result.onlineClients += sr->onlineClients(); + result.onlineChannels += sr->onlineChannels(); + } + + } + return result; +} + +OnlineClientReport ServerManager::clientReport() { + OnlineClientReport result{}; + for(const auto& server : this->serverInstances()) { + if(!server->running()) continue; + auto sr = server->onlineStats(); + result.bots += sr.bots; + result.queries += sr.queries; + result.clients_web += sr.clients_web; + result.clients_ts += sr.clients_ts; + } + return result; +} + +size_t ServerManager::runningServers() { + size_t res = 0; + for(const auto& sr : this->serverInstances()) + if(sr->running()) res++; + return res; +} + +size_t ServerManager::usedSlots() { + size_t res = 0; + for(const auto& sr : this->serverInstances()) + res += sr->properties()[property::VIRTUALSERVER_MAXCLIENTS].as(); + return res; +} + +shared_ptr ServerManager::createServer(std::string hosts, uint16_t port) { + bool sid_success = false; + + ServerId serverId = this->next_available_server_id(sid_success); + if(!sid_success) + return nullptr; + + sql::command(this->handle->getSql(), "INSERT INTO `servers` (`serverId`, `host`, `port`) VALUES (:sid, :host, :port)", variable{":sid", serverId}, variable{":host", hosts}, variable{":port", port}).executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); + //`serverId` INTEGER DEFAULT -1, `type` INTEGER, `id` INTEGER, `key` VARCHAR(" UNKNOWN_KEY_LENGTH "), `value` TEXT + auto prop_copy = sql::command(this->handle->getSql(), "INSERT INTO `properties` (`serverId`, `type`, `id`, `key`, `value`) SELECT :target_sid AS `serverId`, `type`, `id`, `key`, `value` FROM `properties` WHERE `type` = :type AND `id` = 0 AND `serverId` = 0;", + variable{":target_sid", serverId}, + variable{":type", property::PROP_TYPE_SERVER}).execute(); + if(!prop_copy.success) + logCritical(LOG_GENERAL, "Failed to copy default server properties: {}", prop_copy.fmtStr()); + + auto server = make_shared(serverId, this->handle->getSql()); + server->self = server; + if(!server->initialize(true)) { + //FIXME error handling + } + server->properties()[property::VIRTUALSERVER_HOST] = hosts; + server->properties()[property::VIRTUALSERVER_PORT] = port; + + { + threads::MutexLock l(this->instanceLock); + this->instances.push_back(server); + } + this->adjust_executor_threads(); + return server; +} + +bool ServerManager::deleteServer(shared_ptr server) { + { + threads::MutexLock l(this->instanceLock); + bool found = false; + for(const auto& s : this->instances) + if(s == server) { + found = true; + break; + } + if(!found) return false; + this->instances.erase(std::remove_if(this->instances.begin(), this->instances.end(), [&](const shared_ptr& s) { + return s == server; + }), this->instances.end()); + } + this->adjust_executor_threads(); + + if(server->getState() != ServerState::OFFLINE) + server->stop("server deleted"); + { + for(const shared_ptr& client : server->getClients()) { + if(client && client->getType() == ClientType::CLIENT_QUERY) { + lock_guard lock(client->command_lock); + client->server = nullptr; + + client->loadDataForCurrentServer(); + } + } + } + { + threads::MutexLock locK(server->stateLock); + server->state = ServerState::DELETING; + } + + this->handle->properties()[property::SERVERINSTANCE_SPOKEN_TIME_DELETED] += server->properties()[property::VIRTUALSERVER_SPOKEN_TIME].as(); + sql::command(this->handle->getSql(), "DELETE FROM `tokens` WHERE `serverId` = :sid", variable{":sid", server->getServerId()}).executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); + sql::command(this->handle->getSql(), "DELETE FROM `properties` WHERE `serverId` = :sid", variable{":sid", server->getServerId()}).executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); + sql::command(this->handle->getSql(), "DELETE FROM `permissions` WHERE `serverId` = :sid", variable{":sid", server->getServerId()}).executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); + sql::command(this->handle->getSql(), "DELETE FROM `groups` WHERE `serverId` = :sid", variable{":sid", server->getServerId()}).executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); + sql::command(this->handle->getSql(), "DELETE FROM `clients` WHERE `serverId` = :sid", variable{":sid", server->getServerId()}).executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); + sql::command(this->handle->getSql(), "DELETE FROM `channels` WHERE `serverId` = :sid", variable{":sid", server->getServerId()}).executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); + sql::command(this->handle->getSql(), "DELETE FROM `bannedClients` WHERE `serverId` = :sid", variable{":sid", server->getServerId()}).executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); + sql::command(this->handle->getSql(), "DELETE FROM `assignedGroups` WHERE `serverId` = :sid", variable{":sid", server->getServerId()}).executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); + sql::command(this->handle->getSql(), "DELETE FROM `servers` WHERE `serverId` = :sid", variable{":sid", server->getServerId()}).executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); + sql::command(this->handle->getSql(), "DELETE FROM `musicbots` WHERE `serverId` = :sid", variable{":sid", server->getServerId()}).executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); + + sql::command(this->handle->getSql(), "DELETE FROM `bannedClients` WHERE `serverId` = :sid", variable{":sid", server->getServerId()}).executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); + sql::command(this->handle->getSql(), "DELETE FROM `ban_trigger` WHERE `server_id` = :sid", variable{":sid", server->getServerId()}).executeLater().waitAndGetLater(LOG_SQL_CMD, {1, "future failed"}); + this->handle->getFileServer()->deleteServer(server); + + return true; +} + +void ServerManager::executeAutostart() { + threads::MutexLock l(this->instanceLock); + auto lastStart = system_clock::time_point(); + for(const auto& server : this->instances){ + if(!server->running() && server->properties()[property::VIRTUALSERVER_AUTOSTART].as()){ + threads::self::sleep_until(lastStart + milliseconds(10)); //Don't start all server at the same point (otherwise all servers would tick at the same moment) + lastStart = system_clock::now(); + logMessage(server->getServerId(), "Starting server"); + string msg; + try { + if(!server->start(msg)) + logError(server->getServerId(), "Failed to start server.\n Message: " + msg); + } catch (const std::exception& ex) { + logError(string() + "Could not start server! Got an active exception. Message " + ex.what()); + } + } + } +} + +void ServerManager::shutdownAll(const std::string& msg) { + for(const auto &server : this->serverInstances()) + server->preStop(msg); + for(const auto &server : this->serverInstances()){ + if(server->running()) server->stop(msg); + } + + this->execute_loop->shutdown(); +} + +void ServerManager::tickHandshakeClients() { + for(const auto& server : this->serverInstances()) { + auto vserver = server->getVoiceServer(); + if(vserver) + vserver->tickHandshakingClients(); + } +} \ No newline at end of file diff --git a/server/src/ServerManager.h b/server/src/ServerManager.h new file mode 100644 index 0000000..4082f93 --- /dev/null +++ b/server/src/ServerManager.h @@ -0,0 +1,99 @@ +#pragma once + +#include +#include +#include "client/voice/PrecomputedPuzzles.h" +#include "server/VoiceIOManager.h" +#include "TSServer.h" + +namespace ts { + namespace server { + class InstanceHandler; + + struct ServerReport { + size_t avariable; + size_t online; + + size_t slots; + size_t onlineClients; + size_t onlineChannels; + }; + class ServerManager { + public: + enum State { + STOPPED, + STARTING, + STARTED, + STOPPING + }; + + explicit ServerManager(InstanceHandler*); + ~ServerManager(); + + bool initialize(bool execute_autostart = true); + + std::shared_ptr createServer(std::string, uint16_t); + bool deleteServer(std::shared_ptr); + + std::shared_ptr findServerById(ServerId); + std::shared_ptr findServerByPort(uint16_t); + uint16_t next_available_port(); + ServerId next_available_server_id(bool& /* success */); + + std::deque> serverInstances(){ + threads::MutexLock l(this->instanceLock); + return instances; + } + + ServerReport report(); + OnlineClientReport clientReport(); + size_t runningServers(); + size_t usedSlots(); + + void executeAutostart(); + void shutdownAll(const std::string&); + + //Dotn use shared_ptr references to keep sure that they be hold in memory + bool createServerSnapshot(Command &cmd, std::shared_ptr server, int version, std::string &error); + std::shared_ptr createServerFromSnapshot(std::shared_ptr old, std::string, uint16_t, const ts::Command &, std::string &); + + size_t maxSlotLimit(){ return 254; } + + protocol::PuzzleManager* rsaPuzzles() { return this->puzzles; } + + event::EventExecutor* get_executor_loop() { return this->execute_loop; } + + inline void adjust_executor_threads() { + std::unique_lock instance_lock(this->instanceLock); + auto instance_count = this->instances.size(); + instance_lock.unlock(); + + auto threads = std::min(config::threads::voice::execute_per_server * instance_count, config::threads::voice::execute_limit); + this->execute_loop->threads(threads); + } + io::VoiceIOManager* ioManager(){ return this->_ioManager; } + + threads::Mutex server_create_lock; + + State getState() { return this->state; } + private: + State state = State::STOPPED; + InstanceHandler* handle; + threads::Mutex instanceLock; + std::deque> instances; + protocol::PuzzleManager* puzzles = nullptr; + + event::EventExecutor* execute_loop = nullptr; + threads::Scheduler* handshakeTickers = nullptr; + io::VoiceIOManager* _ioManager = nullptr; + + struct { + threads::Thread* thread = nullptr; + std::condition_variable condition; + std::mutex lock; + } acknowledge; + + void tickHandshakeClients(); + }; + } +} \ No newline at end of file diff --git a/server/src/ServerManagerSnapshot.cpp b/server/src/ServerManagerSnapshot.cpp new file mode 100644 index 0000000..66de0b7 --- /dev/null +++ b/server/src/ServerManagerSnapshot.cpp @@ -0,0 +1,12 @@ +#include +#include +#include +#include "ServerManager.h" +#include "src/server/VoiceServer.h" +#include "InstanceHandler.h" + +using namespace std; +using namespace ts; +using namespace ts::server; + +#define PREFIX string("[SNAPSHOT] ") diff --git a/server/src/ServerManagerSnapshotDeploy.cpp b/server/src/ServerManagerSnapshotDeploy.cpp new file mode 100644 index 0000000..49187a0 --- /dev/null +++ b/server/src/ServerManagerSnapshotDeploy.cpp @@ -0,0 +1,1223 @@ +#include +#include +#include +#include +#include +#include "ServerManager.h" +#include "src/server/VoiceServer.h" +#include "InstanceHandler.h" +#include "InstanceHandler.h" + +using namespace std; +using namespace ts; +using namespace ts::server; + +#define PREFIX string("[SNAPSHOT] ") + +inline std::string avArguments(const ts::Command& cmd, int index){ + stringstream res; + for(const auto &key : cmd[index].keys()) + res << key << "=" << cmd[index][key].as() << " "; + return res.str(); +} + +//TeamSpeak: permid=i_channel_needed_permission_modify_power permvalue=75 permskip=0 permnegated=0 +//TeaSpeak: perm=i_channel_needed_permission_modify_power value=75 granted=75 flag_skip=0 flag_negated=0 +struct SnapshotPermissionEntry { + std::shared_ptr type = permission::PermissionTypeEntry::unknown; + permission::PermissionValue value = permNotGranted; + permission::PermissionValue grant = permNotGranted; + bool negated = false; + bool skipped = false; + + SnapshotPermissionEntry(const std::shared_ptr& type, permission::PermissionValue value, permission::PermissionValue grant) : type(std::move(type)), value(value), grant(grant) {} + SnapshotPermissionEntry(const std::shared_ptr& type, permission::PermissionValue value, permission::PermissionValue grant, bool negated, bool skipped) : type(type), value(value), grant(grant), negated(negated), skipped(skipped) {} + SnapshotPermissionEntry() = default; + + static deque> parse(const Command& cmd, int& index, permission::teamspeak::GroupType type, int version, const std::vector& ends, bool ignore_first = false) { + deque> result{}; + + while(true) { + bool end_reached = false; + for(const auto& e : ends) + if(cmd[index].has(e)) { end_reached = true; break; } + if(end_reached || cmd.bulkCount() < index) { + if(ignore_first) ignore_first = false; + else break; + } + + if(version == 0) { + auto permission_name = cmd[index]["permid"].string(); + for(const auto& mapped : permission::teamspeak::map_key(permission_name, type)) { + auto permission = permission::resolvePermissionData(mapped); + if(permission->type == permission::unknown) { + logError(0, "Failed to parse permission {}. Original: {}", mapped, permission_name); + index++; + continue; + } + + bool found = false; + for(auto& e : result) + if(e->type->type == permission->type) { + found = true; + if(permission->grantName() == mapped) + e->grant = cmd[index]["permvalue"]; + else { + e->value = cmd[index]["permvalue"]; + e->negated = cmd[index]["permnegated"]; + e->skipped = cmd[index]["permskip"]; + } + } + if(!found) { + auto entry = make_unique(); + entry->type = permission; + if(permission->grantName() == mapped) + entry->grant = cmd[index]["permvalue"]; + else { + entry->value = cmd[index]["permvalue"]; + entry->negated = cmd[index]["permnegated"]; + entry->skipped = cmd[index]["permskip"]; + } + result.push_back(std::move(entry)); + } + } + } else if(version >= 1) { + auto permission_name = cmd[index]["perm"].string(); + + auto permission = permission::resolvePermissionData(permission_name); + if(permission->type == permission::unknown) { + logError(0, "Failed to parse permission {}", permission_name); + index++; + continue; + } + + auto entry = make_unique(); + entry->type = permission; + entry->value = cmd[index]["value"]; + entry->grant = cmd[index]["grant"]; + entry->skipped = cmd[index]["flag_skip"]; + entry->negated = cmd[index]["flag_negated"]; + result.push_back(std::move(entry)); + } else { + logError(0, "Failed to parse snapshot permission entry! Invalid version!"); + index++; + continue; + } + + index++; + } + + + deque> addings{}; + for(const auto& perm : result) { + if(perm->type->type == permission::i_group_auto_update_type) { //Migrate this type + for(const auto& e : permission::update::migrate) + if(e.type == perm->value) { + auto _type = permission::resolvePermissionData(e.permission.name); + if(_type->type == permission::unknown) { + logError(0, "Invalid update permission for update group {}. Permission name: {}", e.type, e.permission.name); + } else { + addings.push_back(make_unique(_type, e.permission.value, e.permission.granted, e.permission.negated, e.permission.skipped)); + } + } + } + } + for(auto& a : addings) + result.push_back(move(a)); + + return result; + } + + void write(Command& cmd, int& index, permission::teamspeak::GroupType type, int version) { + if(version == 0) { + if(this->value != permNotGranted) { + for(const auto& name : permission::teamspeak::unmap_key(this->type->name, type)) { + cmd[index]["permid"] = name; + cmd[index]["permvalue"] = this->value; + cmd[index]["permskip"] = this->skipped; + cmd[index]["permnegated"] = this->negated; + index++; + } + } + if(this->grant != permNotGranted) { + for(const auto& name : permission::teamspeak::unmap_key(this->type->grantName(), type)) { + cmd[index]["permid"] = name; + cmd[index]["permvalue"] = this->grant; + cmd[index]["permskip"] = false; + cmd[index]["permnegated"] = false; + index++; + } + } + } else if(version >= 1) { + cmd[index]["perm"] = this->type->name; + cmd[index]["value"] = this->value; + cmd[index]["grant"] = this->grant; + cmd[index]["flag_skip"] = this->skipped; + cmd[index]["flag_negated"] = this->negated; + index++; + } else { + logError(0, "Could not write snapshot permission! Invalid version. ({})", version); + } + } +}; + +std::shared_ptr ServerManager::createServerFromSnapshot(shared_ptr old, std::string host, + uint16_t port, const ts::Command &arguments, + std::string &error) { + ServerId serverId = 0; + map> channelGroupRelations; //cid is the new cgid + map channelGroupMapping; + map> serverGroupRelations; //gid is the new gid + map serverGroupMapping; + map db_mapping_client_id; + + deque>> futures; + + bool sid_success = false; + serverId = this->next_available_server_id(sid_success); + if(!sid_success) { + error = "failed to generate new sid"; + return nullptr; + } + ServerId log_server_id = old ? old->getServerId() : serverId; + + futures.push_back({ + "server id register", + sql::command(this->handle->getSql(), "INSERT INTO `servers` (`serverId`, `host`, `port`) VALUES (:sid, :host, :port)", variable{":sid", serverId}, variable{":host", host}, variable{":port", port}).executeLater() + }); + sql::model sql_insert_channel(this->handle->getSql(), "INSERT INTO `channels` (`serverId`, `channelId`, `type`, `parentId`) VALUES (:sid, :id, :type, :pid)", variable{":sid", serverId}); + sql::model sql_insert_group_assignment(this->handle->getSql(), "INSERT INTO `assignedGroups` (`serverId`, `cldbid`, `groupId`, `channelId`, `until`) VALUES (:sid, :cldbid, :gid, :chid, :until)", variable{":sid", serverId}, variable{":until", 0}); + sql::model sql_insert_permission(this->handle->getSql(), "INSERT INTO `permissions` (`serverId`, `type`, `id`, `channelId`, `permId`, `value`, `grant`, `flag_skip`, `flag_negate`) VALUES (:serverId, :type, :id, :chId, :permId, :value, :grant, :flag_skip, :flag_negate)", + variable{":serverId", serverId}); + sql::model sql_update_permission_grant(this->handle->getSql(), "UPDATE `permissions` SET `grant` = :grant WHERE `serverId` = :sid AND `type` = :type AND `id` = :id AND `channelId` = :chid AND `permId` = :key", variable{":sid", serverId}); + sql::model sql_insert_property(this->handle->getSql(), "INSERT INTO `properties` (`serverId`, `type`, `id`, `key`, `value`) VALUES (:sid, :type, :id, :key, :value)", variable{":sid", serverId}); + sql::model sql_insert_bot(this->handle->getSql(), "INSERT INTO `musicbots` (`serverId`, `botId`, `uniqueId`, `owner`) VALUES (:server_id, :bot_id, :unique_id, :owner)", variable{":server_id", serverId}); + sql::model sql_insert_playlist(this->handle->getSql(), "INSERT INTO `playlists` (`serverId`, `playlist_id`) VALUES (:server_id, :playlist_id)", variable{":server_id", serverId}); + sql::model sql_insert_playlist_song(this->handle->getSql(), "INSERT INTO `playlist_songs` (`serverId`, `playlist_id`, `song_id`, `order_id`, `invoker_dbid`, `url`, `url_loader`, `loaded`, `metadata`) VALUES (:server_id, :playlist_id, :song_id, :order_id, :invoker_dbid, :url, :url_loader, :loaded, :metadata)", variable{":server_id", serverId}); + + int index = 0; + auto snapshot_version = arguments[index].has("snapshot_version") ? arguments[index++]["snapshot_version"] : 0; + debugMessage(0, "Got server snapshot with version {}", snapshot_version); + while(true){ + for(auto &key : arguments[index].keys()){ + if(key == "end_virtualserver") continue; + if(key == "begin_virtualserver") continue; + if(snapshot_version == 0) { + if(key == "virtualserver_download_quota" || key == "virtualserver_upload_quota" || key == "virtualserver_max_download_total_bandwidth" || key == "virtualserver_max_upload_total_bandwidth") { + auto value = arguments[index][key].string(); + try { + arguments[index][key] = to_string((int64_t) std::stoull(value)); + } catch(const std::exception& ex) { + continue; + } + } + } + debugMessage(LOG_GENERAL, PREFIX + " Having server key {} = {}", key, arguments[index][key].as()); + futures.push_back({ + "server property import proprty=" + key, + sql_insert_property.command().values( + variable{":type", property::PropertyType::PROP_TYPE_SERVER}, + variable{":id", 0}, + variable{":key", key}, + variable{":value", arguments[index][key].string()} + ).executeLater() + }); + } + + if(arguments[index].has("end_virtualserver")) break; + index++; + } + + + //`serverId` INT NOT NULL, `playlist_id` INT, `song_id` INT, `order_id` INT, `invoker_dbid` INT, `url` TEXT, `url_loader` TEXT + /* + * sql::command(this->handle->getSql(), "INSERT INTO `playlists` (`serverId`, `playlist_id`) VALUES (:server_id, :playlist_id)", + variable{":server_id", this->ref_server()->getServerId()}, + variable{":playlist_id", playlist_id} + ) + */ + index++; + for(;index < arguments.bulkCount(); index++) { + if (arguments[index].has("begin_channels")) { + while (true) { + if (arguments[index].has("end_channels")) break; + + auto channel_id = arguments[index]["channel_id"].string(); + futures.push_back({ + "channel register cid=" + channel_id, + sql_insert_channel.command().values(variable{":id", channel_id}, variable{":type", -1}, variable{":pid", arguments[index]["channel_pid"].string()}).executeLater() + }); //TODO findout type! + for (const auto &key : arguments[index].keys()) { + if(key == "channel_id") continue; + if(key == "channel_pid") continue; + if(key == "channel_security_salt") continue; + if(key == "channel_filepath") continue; //TODO implement channel_filepath? + if(key == "begin_channels") continue; + + debugMessage(log_server_id, PREFIX + "Having channel key " + key + "=" + arguments[index][key].as() + " for channel " + arguments[index]["channel_id"].as()); + + futures.push_back({ + "channel prop register cid=" + channel_id + " property=" + key, + sql_insert_property.command().values(variable{":sid", serverId}, + variable{":type", property::PropertyType::PROP_TYPE_CHANNEL}, + variable{":id", channel_id}, + variable{":key", key}, + variable{":value", arguments[index][key].as()} + ).executeLater() + }); + } + index++; + } + } else if (arguments[index].has("begin_clients")) { + while (true) { + if (arguments[index].has("end_clients")) break; + debugMessage(log_server_id, PREFIX + "Create client {}/{} -> end: {}",arguments[index]["client_id"].as(), arguments[index]["client_nickname"].as(), to_string(arguments[index].has("end_clients"))); + + ClientDbId oldId = 0; + auto res = sql::command(this->handle->getSql(), "SELECT `cldbid` FROM `clients` WHERE `clientUid` = :uid AND `serverId` = 0", variable{":uid", arguments[index]["client_unique_id"].as()}).query([](ClientDbId* ptr, int, char** v, char**){ *ptr = stoll(v[0]); return 0;}, &oldId); + LOG_SQL_CMD(res); + if(oldId == 0){ + res = sql::command(this->handle->getSql(), "SELECT `cldbid` FROM `clients` WHERE `serverId` = 0 ORDER BY `cldbid` DESC LIMIT 1").query([](ClientDbId* ptr, int length, char** values, char** names){ + *ptr = static_cast(stoll(values[0])); + return 0; + }, &oldId); + LOG_SQL_CMD(res); + + oldId += 1; + //Create a new client + sql::command(this->handle->getSql(), "INSERT INTO `clients` (`serverId`, `cldbid`, `clientUid`, `firstConnect`, `lastConnect`, `connections`, `lastName`) VALUES (:sid, :cldbid, :cluid, :firstConnect, :lastConnect, :connections, :name)", + variable{":sid", 0}, variable{":cldbid", oldId}, variable{":cluid", arguments[index]["client_unique_id"].as()}, + variable{":firstConnect", arguments[index]["client_created"].as()}, + variable{":lastConnect", "0"}, variable{":connections", "0"}, variable{":name", arguments[index]["client_nickname"].as()}).execute(); //TODO log error + } + db_mapping_client_id[arguments[index]["client_id"].as()] = oldId; + sql::command(this->handle->getSql(), "INSERT INTO `clients` (`serverId`, `cldbid`, `clientUid`, `firstConnect`, `lastConnect`, `connections`, `lastName`) VALUES (:sid, :cldbid, :cluid, :firstConnect, :lastConnect, :connections, :name)", + variable{":sid", serverId}, variable{":cldbid", oldId}, variable{":cluid", arguments[index]["client_unique_id"].as()}, + variable{":firstConnect", arguments[index]["client_created"].as()}, + variable{":lastConnect", "0"}, variable{":connections", "0"}, variable{":name", arguments[index]["client_nickname"].as()}).execute(); //TODO log error + //TODO here + index++; + } + } else if (arguments[index].has("begin_permissions")) { + while (true) { + index++; + if (arguments[index].has("end_permissions")) break; + + else if (arguments[index].has("server_groups")) { + GroupId currentGroupId = 1; + while (true) { + if (arguments[index].has("end_groups")) break; + + if(!arguments[index].has("id")){ //new group + logError(0, "Invalid server group permission entry! Missing group id!"); + index++; + continue; + } + + currentGroupId = static_cast(GroupManager::generateGroupId(this->handle->getSql())); + debugMessage(log_server_id, PREFIX + "Insert group: " + to_string(currentGroupId) + " - " + arguments[index]["name"].as()); + serverGroupMapping[arguments[index]["id"].as()] = currentGroupId; + + LOG_SQL_CMD(sql::command(this->handle->getSql(), "INSERT INTO `groups` (`serverId`, `groupId`, `target`, `type`, `displayName`) VALUES (:sid, :gid, :target, :type, :name)", + variable{":sid", serverId}, + variable{":gid", currentGroupId}, + variable{":target", GROUPTARGET_SERVER}, + variable{":type", GroupType::GROUP_TYPE_NORMAL}, + variable{":name", arguments[index]["name"].as()}) + .execute()); + + auto permissions = SnapshotPermissionEntry::parse(arguments, index, permission::teamspeak::SERVER, snapshot_version, {"end_group"}); + for(const auto& entry : permissions) { + futures.push_back({ + "server group permission sgid=" + to_string(currentGroupId) + " permId=" + entry->type->name, + sql_insert_permission.command().values( + variable{":id", currentGroupId}, + variable{":type", permission::SQL_PERM_GROUP}, + variable{":chId", 0}, + + variable{":permId", entry->type->name}, + variable{":value", entry->value}, + variable{":grant", entry->grant}, + variable{":flag_skip", entry->skipped}, + variable{":flag_negate", entry->negated} + ).executeLater() + }); + } + index++; + } + + //Relations for sgroups after list + index++; + while (true) { + if (arguments[index].has("end_relations")) break; + serverGroupRelations[db_mapping_client_id[arguments[index]["cldbid"].as()]].push_back(serverGroupMapping[arguments[index]["gid"].as()]); + futures.push_back({ + "server group relation", + sql_insert_group_assignment.command().values(variable{":cldbid", db_mapping_client_id[arguments[index]["cldbid"].as()]}, variable{":gid", serverGroupMapping[arguments[index]["gid"].as()]}, variable{":chid", "0"}).executeLater() + }); + index++; + } + } else if(arguments[index].has("channel_groups")){ + GroupId currentGroupId = 1; + while (true) { + if (arguments[index].has("end_groups")) break; + + if(!arguments[index].has("id")){ //new group + logError(0, "Invalid server group permission entry! Missing group id!"); + index++; + continue; + } + + currentGroupId = static_cast(GroupManager::generateGroupId(this->handle->getSql())); + debugMessage(log_server_id, PREFIX + "Insert channel group: " + to_string(currentGroupId) + " - " + arguments[index]["name"].as()); + channelGroupMapping[arguments[index]["id"].as()] = currentGroupId; + + LOG_SQL_CMD(sql::command(this->handle->getSql(), "INSERT INTO `groups` (`serverId`, `groupId`, `target`, `type`, `displayName`) VALUES (:sid, :gid, :target, :type, :name)", + variable{":sid", serverId}, + variable{":gid", currentGroupId}, + variable{":target", GROUPTARGET_CHANNEL}, + variable{":type", GroupType::GROUP_TYPE_NORMAL}, + variable{":name", arguments[index]["name"].as()}) + .execute()); + + auto permissions = SnapshotPermissionEntry::parse(arguments, index, permission::teamspeak::SERVER, snapshot_version, {"end_group"}); + for(const auto& entry : permissions) { + futures.push_back({ + "channel group property insert cgid=" + to_string(currentGroupId) + " property=" + entry->type->name, + sql_insert_permission.command().values( + variable{":id", currentGroupId}, + variable{":type", permission::SQL_PERM_GROUP}, + variable{":chId", 0}, + + variable{":permId", entry->type->name}, + variable{":value", entry->value}, + variable{":grant", entry->grant}, + variable{":flag_skip", entry->skipped}, + variable{":flag_negate", entry->negated} + ).executeLater() + }); + } + index++; + } + + //Relations for sgroups after list + ChannelId chId = 0; + index++; + while (true) { + if (arguments[index].has("end_relations")) break; + if(arguments[index].has("iid")) chId = arguments[index]["iid"]; + channelGroupRelations[db_mapping_client_id[arguments[index]["cldbid"].as()]][chId] = channelGroupMapping[arguments[index]["gid"].as()]; + futures.push_back({ + "channel group relation", + sql_insert_group_assignment.command().values(variable{":cldbid", db_mapping_client_id[arguments[index]["cldbid"].as()]}, variable{":gid", channelGroupMapping[arguments[index]["gid"].as()]}, variable{":chid", chId}).executeLater() + }); + index++; + } + } else if (arguments[index].has("client_flat")) { + ClientDbId currentId = 0; + while (true) { + if (arguments[index].has("end_flat")) break; + if(arguments[index].has("id1")) + currentId = arguments[index]["id1"]; + + auto permissions = SnapshotPermissionEntry::parse(arguments, index, permission::teamspeak::CLIENT, snapshot_version, {"id1", "end_flat"}, true); + for(const auto& entry : permissions) { + futures.push_back({ + "client permission insert clientId=" + to_string(currentId) + " permId=" + entry->type->name, + sql_insert_permission.command().values( + variable{":id", currentId}, + variable{":type", permission::SQL_PERM_USER}, + variable{":chId", 0}, + + variable{":permId", entry->type->name}, + variable{":value", entry->value}, + variable{":grant", entry->grant}, + variable{":flag_skip", entry->skipped}, + variable{":flag_negate", entry->negated} + ).executeLater() + }); + } + } + } else if (arguments[index].has("channel_flat")) { + ChannelId currentChannelId = 0; + while (true) { + if (arguments[index].has("end_flat")) break; + + if (arguments[index].has("id1")) + currentChannelId = arguments[index]["id1"]; //We also have id2 unknown for what this is. Maybe parent? + + auto permissions = SnapshotPermissionEntry::parse(arguments, index, permission::teamspeak::CHANNEL, snapshot_version, {"id1", "end_flat"}, true); + for(const auto& entry : permissions) { + futures.push_back({ + "channel permission insert channelId=" + to_string(currentChannelId) + " permId=" + entry->type->name, + sql_insert_permission.command().values( + variable{":id", 0}, + variable{":type", permission::SQL_PERM_CHANNEL}, + variable{":chId", currentChannelId}, + + variable{":permId", entry->type->name}, + variable{":value", entry->value}, + variable{":grant", entry->grant}, + variable{":flag_skip", entry->skipped}, + variable{":flag_negate", entry->negated} + ).executeLater() + }); + } + } + } else if (arguments[index].has("channel_client_flat")) { + ClientDbId currentClientId = 0; + ChannelId currentChannelId = 0; + while (true) { + if (arguments[index].has("end_flat")) break; + //id1 = channel id + //id2 = client id + if(arguments[index].has("id1")) + currentChannelId = arguments[index]["id1"]; + if(arguments[index].has("id2")) + currentClientId = db_mapping_client_id[arguments[index]["id2"]]; + if(currentChannelId > 0 && currentClientId > 0){ + auto permissions = SnapshotPermissionEntry::parse(arguments, index, permission::teamspeak::CLIENT, snapshot_version, {"id1", "id2", "end_flat"}, true); + for(const auto& entry : permissions) { + futures.push_back({ + "client channel permission insert", + sql_insert_permission.command().values( + variable{":id", currentClientId}, + variable{":type", permission::SQL_PERM_CHANNEL}, + variable{":chId", currentChannelId}, + + variable{":permId", entry->type->name}, + variable{":value", entry->value}, + variable{":grant", entry->grant}, + variable{":flag_skip", entry->skipped}, + variable{":flag_negate", entry->negated} + ).executeLater() + }); + } + } else index++; + } + } else { + error = "Invalid tag in permissions. Available: " + avArguments(arguments, index); + return nullptr; + } + } + } else if(arguments[index].has("begin_music")) { + PlaylistId playlist_index = 0; + PlaylistId playlist_error = 0xFFFFFFFF; + map db_mapping_playlist; + + /* + //gather static info (not needed because its a new server) + { + auto sql_result = sql::command(this->handle->getSql(), "SELECT `playlist_id` FROM `playlists` WHERE `serverId` = :server_id ORDER BY `playlist_id` DESC", variable{":server_id", serverId}).query([&](int length, string* values, string* names){ + if(length != 1) return; + + try { + playlist_index = (PlaylistId) stoll(values[0]); + } catch(const std::exception& ex) { + error = "Failed to parse playlist id from database. ID: " + values[0]; + return; + } + }); + LOG_SQL_CMD(sql_result); + + if(playlist_index == 0) { + error = "failed to gather info"; + return nullptr; + } + } + */ + + while(!arguments[index].has("end_music")) { + if(arguments[index].has("begin_bots")) { + while(!arguments[index].has("end_bots")) { + ClientDbId bot_id = arguments[index]["bot_id"]; + if(db_mapping_client_id.find(bot_id) == db_mapping_client_id.end()) { + logError(log_server_id, PREFIX + "Failed to insert music bot within id {}. (Mapping not found)", bot_id); + index++; + continue; + } + ClientDbId new_bot_id = db_mapping_client_id[bot_id]; + + //begin_bots + auto sql_result = sql_insert_bot.command().values( + variable{":bot_id", new_bot_id}, + variable{":unique_id", arguments[index]["bot_unique_id"].string()}, + variable{":owner", arguments[index]["bot_owner_id"].string()} + ).execute(); + if(!sql_result) { + logError(log_server_id, PREFIX + "Failed to insert music bot with id {} (old: {}). ({})", new_bot_id, bot_id, sql_result.fmtStr()); + index++; + continue; + } + + for(const auto& key : arguments[index].keys()) { + if(key == "begin_music") continue; + if(key == "begin_bots") continue; + if(key == "bot_unique_id") continue; + if(key == "bot_owner_id") continue; + if(key == "bot_id") continue; + + const auto& property = property::info(key); + if(property->property_index == property::CLIENT_UNDEFINED) { + debugMessage(log_server_id, PREFIX + "Failed to parse give music bot property {} for bot {} (old: {}). Value: {}", key, new_bot_id, bot_id, arguments[index][key].string()); + continue; + } + + futures.push_back({ + "music bot insert", + sql_insert_property.command().values( + variable{":type", property::PropertyType::PROP_TYPE_CLIENT}, + variable{":id", new_bot_id}, + variable{":key", key}, + variable{":value", arguments[index][key].string()} + ).executeLater() + }); + } + + index++; + } + } else if(arguments[index].has("begin_playlist")) { + while(!arguments[index].has("end_playlist")) { + PlaylistId playlist_id = arguments[index]["playlist_id"]; + db_mapping_playlist[++playlist_index] = playlist_id; + + auto sql_result = sql_insert_playlist.command().values(variable{":playlist_id", playlist_index}).execute(); + if(!sql_result) { + db_mapping_playlist[playlist_index] = playlist_error; + logError(log_server_id, PREFIX + "Failed to insert playlist with id {} (old: {}). ({})", playlist_index, playlist_id, sql_result.fmtStr()); + index++; + continue; + } + + + for(const auto& key : arguments[index].keys()) { + if(key == "begin_music") continue; + if(key == "begin_playlist") continue; + if(key == "playlist_id") continue; + + const auto& property = property::info(key); + if(property->property_index == property::CLIENT_UNDEFINED) { + debugMessage(log_server_id, PREFIX + "Failed to parse given playlist property {} for playlist {} (old: {}). Value: {}", key, playlist_index, playlist_id, arguments[index][key].string()); + continue; + } + + futures.push_back({ + "playlist insert", + sql_insert_property.command().values( + variable{":type", property::PropertyType::PROP_TYPE_PLAYLIST}, + variable{":id", playlist_index}, + variable{":key", key}, + variable{":value", arguments[index][key].string()} + ).executeLater() + }); + } + + index++; + } + } else if(arguments[index].has("begin_playlist_songs")) { + PlaylistId current_playlist = 0; + PlaylistId current_playlist_mapped = 0; + while(!arguments[index].has("end_playlist_songs")) { + if(arguments[index].has("song_playlist_id")) { + current_playlist = arguments[index]["song_playlist_id"]; + current_playlist_mapped = (PlaylistId) db_mapping_client_id[current_playlist]; + if(current_playlist_mapped == 0) { + logError(log_server_id, PREFIX + "Failed to insert playlist song entry {}. Playlist id {} hasn't been mapped", arguments[index]["song_id"].value(), current_playlist); + current_playlist_mapped = playlist_error; + } + } + if(current_playlist_mapped == playlist_error) { /* current list is an error */ + index++; + continue; + } + + auto sql_future = sql_insert_playlist_song.command().values( + variable{":playlist_id", current_playlist_mapped}, + variable{":song_id", arguments[index]["song_id"].value()}, + variable{":order_id", arguments[index]["song_order"].value()}, + variable{":invoker_dbid", arguments[index]["song_invoker"].value()}, + variable{":url", arguments[index]["song_url"].value()}, + variable{":url_loader", arguments[index]["song_url_loader"].value()}, + variable{":loaded", arguments[index]["song_loaded"].value()}, + variable{":metadata", arguments[index]["song_metadata"].value()} + ).executeLater(); + futures.push_back({ + "song insert", + sql_future + }); + + index++; + } + } + index++; + } + } else { + error = "Invalid root tag. Available: " + avArguments(arguments, index); + return nullptr; + } + } + + debugMessage("Wait for success!"); + + for(const auto& future : futures) { + auto result = future.second.waitAndGet({-1, "timeout"}, chrono::system_clock::now() + chrono::seconds(5)); + if(!result) { + logError(serverId, "Failed to execute snapshotdeploy command {}: {}", future.first, result.fmtStr()); + } + } + + if(old) + this->deleteServer(old); + //Now we load the server + auto server = make_shared(serverId, this->handle->getSql()); + server->self = server; + if(!server->initialize(false)) { + //FIXME Error handling! + } + server->properties()[property::VIRTUALSERVER_HOST] = host; + server->properties()[property::VIRTUALSERVER_PORT] = port; + server->properties()[property::VIRTUALSERVER_ASK_FOR_PRIVILEGEKEY] = false; + + { + threads::MutexLock l(this->instanceLock); + this->instances.push_back(server); + } + this->adjust_executor_threads(); + + server->properties()[property::VIRTUALSERVER_DEFAULT_SERVER_GROUP] = serverGroupMapping[server->properties()[property::VIRTUALSERVER_DEFAULT_SERVER_GROUP].as()]; + server->properties()[property::VIRTUALSERVER_DEFAULT_CHANNEL_GROUP] = channelGroupMapping[server->properties()[property::VIRTUALSERVER_DEFAULT_CHANNEL_GROUP].as()]; + server->properties()[property::VIRTUALSERVER_DEFAULT_CHANNEL_ADMIN_GROUP] = channelGroupMapping[server->properties()[property::VIRTUALSERVER_DEFAULT_CHANNEL_ADMIN_GROUP].as()]; + server->ensureValidDefaultGroups(); + return server; +} + +struct CommandTuple { + Command& cmd; + int& index; + int version; +}; + +struct PermissionCommandTuple { + Command& cmd; + int& index; + int version; + ClientDbId client; + ChannelId channel; +}; + +inline bool writePermissions(const shared_ptr& manager, Command& cmd, int& index, int version, permission::teamspeak::GroupType type, std::string& error) { + for(const auto& permission_container : manager->permissions()) { + auto permission = get<1>(permission_container); + SnapshotPermissionEntry{ + permission::resolvePermissionData(get<0>(permission_container)), + permission.flags.value_set ? permission.values.value : permNotGranted, + permission.flags.grant_set ? permission.values.grant : permNotGranted, + permission.flags.negate, + permission.flags.skip + }.write(cmd, index, type, version); + } + return true; +} + +inline void writeRelations(const shared_ptr& server, GroupTarget type, Command& cmd, int& index, int version) { + PermissionCommandTuple parm{cmd, index, version, 0, 0}; + auto res = sql::command(server->getSql(), "SELECT `cldbid`, `groups`.`groupId`, `channelId`, `until` FROM `assignedGroups` INNER JOIN `groups` ON `groups`.`serverId` = `assignedGroups`.`serverId` AND `groups`.`groupId` = `assignedGroups`.`groupId` WHERE `groups`.`serverId` = :sid AND `groups`.target = :type", + variable{":sid", server->getServerId()}, + variable{":type", type} + ).query([](PermissionCommandTuple* commandIndex, int length, char** value, char** name) { + ClientDbId cldbid = 0; + ChannelId channelId = 0; + GroupId gid = 0; + int64_t until = 0; + + for(int idx = 0; idx < length; idx++) { + try { + if(strcmp(name[idx], "cldbid") == 0) + cldbid = stoul(value[idx]); + else if(strcmp(name[idx], "groupId") == 0) + gid = stoul(value[idx]); + else if(strcmp(name[idx], "channelId") == 0) + channelId = stoul(value[idx]); + else if(strcmp(name[idx], "until") == 0) + until = stoll(value[idx]); + } catch (std::exception& ex) { + logError(0, "Failed to write snapshot group relation (Skipping it)! Message: {} @ {} => {}", ex.what(), name[idx], value[idx]); + return 0; + } + } + + if(commandIndex->channel != channelId) { + commandIndex->channel = channelId; + commandIndex->cmd[commandIndex->index]["iid"] = channelId; + } + commandIndex->cmd[commandIndex->index]["gid"] = gid; + commandIndex->cmd[commandIndex->index]["until"] = until; + commandIndex->cmd[commandIndex->index++]["cldbid"] = cldbid; + + return 0; + }, &parm); + LOG_SQL_CMD(res); +} + +struct DatabaseMusicbot { + ClientDbId bot_id; + ClientDbId bot_owner_id; + std::string bot_unique_id; +}; + +bool ServerManager::createServerSnapshot(Command &cmd, shared_ptr server, int version, std::string &error) { + + int index = 0; + + if(version == -1) version = 2; //Auto versioned + if(version < 0 || version > 2) { + error = "Invalid snapshot version!"; + return false; + } + if(version > 0) cmd[index++]["snapshot_version"] = version; + //Server + { + cmd[index]["begin_virtualserver"] = ""; + for(const auto& serverProperty : server->properties().list_properties(property::FLAG_SNAPSHOT)) { + if(version == 0) { + switch (serverProperty.type().property_index) { + case property::VIRTUALSERVER_DOWNLOAD_QUOTA: + case property::VIRTUALSERVER_UPLOAD_QUOTA: + case property::VIRTUALSERVER_MAX_DOWNLOAD_TOTAL_BANDWIDTH: + case property::VIRTUALSERVER_MAX_UPLOAD_TOTAL_BANDWIDTH: + cmd[index][serverProperty.type().name] = (uint64_t) serverProperty.as_save(); + default: + break; + } + } + cmd[index][serverProperty.type().name] = serverProperty.value(); + } + cmd[index++]["end_virtualserver"] = ""; + } + + //Channels + { + cmd[index]["begin_channels"] = ""; + for(const auto& channel : server->getChannelTree()->channels()) { + for(const auto& channelProperty : channel->properties().list_properties(property::FLAG_SNAPSHOT)) { + if(channelProperty.type() == property::CHANNEL_ID) + cmd[index]["channel_id"] = channelProperty.as(); + else if(channelProperty.type() == property::CHANNEL_PID) + cmd[index]["channel_pid"] = channelProperty.as(); + else + cmd[index][channelProperty.type().name] = channelProperty.as(); + } + index++; + } + cmd[index++]["end_channels"] = ""; + } + + //Clients + { + cmd[index]["begin_clients"] = ""; + CommandTuple parm{cmd, index, version}; + auto res = sql::command(server->getSql(), "SELECT `cldbid`, `clientUid`, `firstConnect`, `lastName` FROM `clients` WHERE `serverId` = :sid", + variable{":sid", server->getServerId()} + ).query([&](CommandTuple* commandIndex, int length, char** value, char** name) { + ClientDbId cldbid = 0; + int64_t clientCreated = 0; + string clientUid; + string lastName; + string description; //TODO description + + for(int idx = 0; idx < length; idx++) { + try { + if(strcmp(name[idx], "cldbid") == 0) + cldbid = value[idx] && strlen(value[idx]) > 0 ? stoul(value[idx]) : 0; + else if(strcmp(name[idx], "clientUid") == 0) + clientUid = value[idx]; + else if(strcmp(name[idx], "firstConnect") == 0) + clientCreated = value[idx] && strlen(value[idx]) > 0 ? stoll(value[idx]) : 0L; + else if(strcmp(name[idx], "lastName") == 0) + lastName = value[idx]; + } catch (std::exception& ex) { + logError(0, "Failed to write snapshot client (Skipping it)! Message: {} @ {} => {}", ex.what(), name[idx], value[idx]); + return 0; + } + } + + + commandIndex->cmd[commandIndex->index]["client_id"] = cldbid; + commandIndex->cmd[commandIndex->index]["client_unique_id"] = clientUid; + commandIndex->cmd[commandIndex->index]["client_nickname"] = lastName; + commandIndex->cmd[commandIndex->index]["client_created"] = clientCreated; + commandIndex->cmd[commandIndex->index]["client_description"] = description; + if(commandIndex->version == 0) + commandIndex->cmd[commandIndex->index]["client_unread_messages"] = 0; + commandIndex->index++; + return 0; + }, &parm); + LOG_SQL_CMD(res); + cmd[index++]["end_clients"] = ""; + } + + //music and music bots + if(version >= 2) { + cmd[index]["begin_music"] = ""; + + /* music bots */ + { + cmd[index]["begin_bots"] = ""; + + deque music_bots; + + auto sql_result = sql::command(server->getSql(), "SELECT `botId`, `uniqueId`, `owner` FROM `musicbots` WHERE `serverId` = :sid", variable{":sid", server->getServerId()}).query([&](int length, string* values, string* names) { + DatabaseMusicbot data; + for(int column = 0; column < length; column++) { + try { + if(names[column] == "botId") + data.bot_id = stoll(values[column]); + else if(names[column] == "uniqueId") + data.bot_unique_id = values[column]; + else if(names[column] == "owner") + data.bot_owner_id = stoll(values[column]); + } catch(std::exception& ex) { + return; + } + } + + music_bots.emplace_back(data); + }); + if(!sql_result) + logError(server->getServerId(), PREFIX + "Failed to write music bots to snapshot. {}", sql_result.fmtStr()); + + for(const auto& music_bot : music_bots) { + auto properties = serverInstance->databaseHelper()->query_properties(server->getServerId(), property::PROP_TYPE_CLIENT, music_bot.bot_id); + + cmd[index]["bot_unique_id"] = music_bot.bot_unique_id; + cmd[index]["bot_owner_id"] = music_bot.bot_owner_id; + cmd[index]["bot_id"] = music_bot.bot_id; + + for(const auto& property : properties) { + if((property->type->flags & (property::FLAG_SAVE_MUSIC | property::FLAG_SAVE)) == 0) continue; + if(property->value == property->type->default_value) continue; + + cmd[index][property->type->name] = property->value; + } + + index++; + } + cmd[index++]["end_bots"] = ""; + } + + /* playlists */ + { + cmd[index]["begin_playlist"] = ""; + deque playlist_ids; + + auto sql_result = sql::command(server->getSql(), "SELECT `playlist_id` FROM `playlists` WHERE `serverId` = :sid", variable{":sid", server->getServerId()}).query([&](int length, string* values, string* names) { + try { + playlist_ids.push_back(stoll(values[0])); + } catch(std::exception& ex) { + return; + } + }); + if(!sql_result) + logError(server->getServerId(), PREFIX + "Failed to write playlists to snapshot. {}", sql_result.fmtStr()); + + for(const auto& playlist : playlist_ids) { + auto properties = serverInstance->databaseHelper()->query_properties(server->getServerId(), property::PROP_TYPE_PLAYLIST, playlist); + + cmd[index]["playlist_id"] = playlist; + + for(const auto& property : properties) { + if((property->type->flags & (property::FLAG_SAVE_MUSIC | property::FLAG_SAVE)) == 0) continue; + if(property->value == property->type->default_value) continue; + + cmd[index][property->type->name] = property->value; + } + + index++; + } + + cmd[index++]["end_playlist"] = ""; + } + + /* playlist info */ + { + cmd[index]["begin_playlist_songs"] = ""; + //playlist_songs => `serverId` INT NOT NULL, `playlist_id` INT, `song_id` INT, `order_id` INT, `invoker_dbid` INT, `url` TEXT, `url_loader` TEXT, `loaded` BOOL, `metadata` TEXT + PlaylistId current_playlist = 0; + + auto sql_result = sql::command(server->getSql(), + "SELECT `playlist_id`, `song_id`, `order_id`, `invoker_dbid`, `url`, `url_loader`, `loaded`, `metadata` FROM `playlist_songs` WHERE `serverId` = :sid ORDER BY `playlist_id`", variable{":sid", server->getServerId()} + ).query([&](int length, string* values, string* names) { + for(int column = 0; column < length; column++) { + if(names[column] == "song_id") + cmd[index]["song_id"] = values[column]; + else if(names[column] == "order_id") + cmd[index]["song_order"] = values[column]; + else if(names[column] == "invoker_dbid") + cmd[index]["song_invoker"] = values[column]; + else if(names[column] == "url") + cmd[index]["song_url"] = values[column]; + else if(names[column] == "url_loader") + cmd[index]["song_url_loader"] = values[column]; + else if(names[column] == "loaded") + cmd[index]["song_loaded"] = values[column]; + else if(names[column] == "metadata") + cmd[index]["song_metadata"] = values[column]; + else if(names[column] == "playlist_id") { + try { + auto playlist_id = stoll(values[column]); + if(current_playlist != playlist_id) { + cmd[index]["song_playlist_id"] = values[column]; /* song_playlist_id will be only set if the playlist id had changed */ + current_playlist = playlist_id; + } + } catch(std::exception& ex) { + logError(server->getServerId(), PREFIX + "Failed to parse playlist id. value: {}, message: {}", values[column], ex.what()); + } + } + } + index++; + }); + if(!sql_result) + logError(server->getServerId(), PREFIX + "Failed to write playlist songs to snapshot. {}", sql_result.fmtStr()); + cmd[index++]["end_playlist_songs"] = ""; + } + + cmd[index++]["end_music"] = ""; + } + + //Permissions + { + cmd[index++]["begin_permissions"] = ""; + //Server groups + { + //List groups + { + cmd[index]["server_groups"] = ""; + for(const auto& group : server->getGroupManager()->availableServerGroups(false)) { + cmd[index]["id"] = group->groupId(); + cmd[index]["name"] = group->name(); + if(!writePermissions(group->permissions(), cmd, index, version, permission::teamspeak::SERVER, error)) break; + cmd[index++]["end_group"] = ""; + } + cmd[index++]["end_groups"] = ""; + } + + //List relations + { + cmd[index]["begin_relations"] = ""; + writeRelations(server, GROUPTARGET_SERVER, cmd, index, version); + cmd[index++]["end_relations"] = ""; + } + } + + //Channel groups + { + //List groups + { + cmd[index]["channel_groups"] = ""; + for(const auto& group : server->getGroupManager()->availableChannelGroups(false)) { + cmd[index]["id"] = group->groupId(); + cmd[index]["name"] = group->name(); + if(!writePermissions(group->permissions(), cmd, index, version, permission::teamspeak::SERVER, error)) break; + cmd[index++]["end_group"] = ""; + } + cmd[index++]["end_groups"] = ""; + } + + //List relations + { + cmd[index]["begin_relations"] = ""; + writeRelations(server, GROUPTARGET_CHANNEL, cmd, index, version); + cmd[index++]["end_relations"] = ""; + } + } + + //Client rights + { + cmd[index]["client_flat"] = ""; + PermissionCommandTuple parm{cmd, index, version, 0, 0}; + auto res = sql::command(server->getSql(), "SELECT `id`, `permId`, `value`, `grant`, `flag_skip`, `flag_negate` FROM `permissions` WHERE `serverId` = :sid AND `type` = :type AND `channelId` = 0 ORDER BY `id`", + variable{":sid", server->getServerId()}, + variable{":type", permission::SQL_PERM_USER} + ).query([&](PermissionCommandTuple* commandIndex, int length, char** values, char**names){ + auto type = permission::resolvePermissionData(permission::unknown); + permission::PermissionValue value = 0, grant = 0; + bool skipped = false, negated = false; + ClientDbId cldbid = 0; + + for(int idx = 0; idx < length; idx++) { + try { + if(strcmp(names[idx], "id") == 0) + cldbid = stoul(values[idx] && strlen(values[idx]) > 0 ? values[idx] : "0"); + else if(strcmp(names[idx], "value") == 0) + value = values[idx] && strlen(values[idx]) > 0 ? stoi(values[idx]) : permNotGranted; + else if(strcmp(names[idx], "grant") == 0) + grant = values[idx] && strlen(values[idx]) > 0 ? stoi(values[idx]) : permNotGranted; + else if(strcmp(names[idx], "permId") == 0) { + type = permission::resolvePermissionData(values[idx]); + if(type->type == permission::unknown) { + logError(0, "Could not parse client permission for snapshot (Invalid type {})! Skipping it!", values[idx]); + return 0; + } + } + else if(strcmp(names[idx], "flag_skip") == 0) + skipped = values[idx] ? strcmp(values[idx], "1") == 0 : false; + else if(strcmp(names[idx], "flag_negate") == 0) + negated = values[idx] ? strcmp(values[idx], "1") == 0 : false; + } catch (std::exception& ex) { + logError(0, "Failed to write snapshot client permission (Skipping it)! Message: {} @ {} => {}", ex.what(), names[idx], values[idx]); + return 0; + } + } + + if(type->type == permission::unknown) { + logError(0, "Could not parse client permission for snapshot (Missing type)! Skipping it!"); + return 0; + } + if(cldbid == 0) { + logError(0, "Could not parse client permission for snapshot (Missing cldbid)! Skipping it!"); + return 0; + } + if(value == permNotGranted && grant == permNotGranted && !skipped && !negated) return 0; + + if(commandIndex->client != cldbid) { + commandIndex->client = cldbid; + commandIndex->cmd[commandIndex->index]["id1"] = cldbid; + commandIndex->cmd[commandIndex->index]["id2"] = 0; + } + SnapshotPermissionEntry{type, value, grant, negated, skipped}.write(commandIndex->cmd, commandIndex->index, permission::teamspeak::CLIENT, commandIndex->version); + return 0; + }, &parm); + LOG_SQL_CMD(res); + cmd[index++]["end_flat"] = ""; + } + + //Channel rights + { + cmd[index]["channel_flat"] = ""; + PermissionCommandTuple parm{cmd, index, version, 0, 0}; + auto res = sql::command(server->getSql(), "SELECT `channelId`, `permId`, `value`, `grant`, `flag_skip`, `flag_negate` FROM `permissions` WHERE `serverId` = :sid AND `type` = :type", + variable{":sid", server->getServerId()}, + variable{":type", permission::SQL_PERM_CHANNEL}).query( + [&](PermissionCommandTuple* commandIndex, int length, char** values, char**names){ + auto type = permission::resolvePermissionData(permission::unknown); + permission::PermissionValue value = 0, grant = 0; + bool skipped = false, negated = false; + ChannelId chid = 0; + + for(int idx = 0; idx < length; idx++) { + try { + if(strcmp(names[idx], "channelId") == 0) + chid = stoul(values[idx] && strlen(values[idx]) > 0 ? values[idx] : "0"); + else if(strcmp(names[idx], "value") == 0) + value = values[idx] && strlen(values[idx]) > 0 ? stoi(values[idx]) : permNotGranted; + else if(strcmp(names[idx], "grant") == 0) + grant = values[idx] && strlen(values[idx]) > 0 ? stoi(values[idx]) : permNotGranted; + else if(strcmp(names[idx], "permId") == 0) { + type = permission::resolvePermissionData(values[idx]); + if(type->type == permission::unknown) { + logError(0, "Could not parse channel permission for snapshot (Invalid type {})! Skipping it!", values[idx]); + return 0; + } + } + else if(strcmp(names[idx], "flag_skip") == 0) + skipped = values[idx] ? strcmp(values[idx], "1") == 0 : false; + else if(strcmp(names[idx], "flag_negate") == 0) + negated = values[idx] ? strcmp(values[idx], "1") == 0 : false; + } catch (std::exception& ex) { + logError(0, "Failed to write snapshot channel permission (Skipping it)! Message: {} @ {} => {}", ex.what(), names[idx], values[idx]); + return 0; + } + } + + if(type->type == permission::unknown) { + logError(0, "Could not parse channel permission for snapshot (Missing type)! Skipping it!"); + return 0; + } + if(chid == 0) { + logError(0, "Could not parse channel permission for snapshot (Missing channel id)! Skipping it!"); + return 0; + } + if(value == permNotGranted && grant == permNotGranted && !skipped && !negated) return 0; + + if(commandIndex->channel != chid) { + commandIndex->channel = chid; + commandIndex->cmd[commandIndex->index]["id1"] = chid; + commandIndex->cmd[commandIndex->index]["id2"] = 0; + } + SnapshotPermissionEntry{type, value, grant, negated, skipped}.write(commandIndex->cmd, commandIndex->index, permission::teamspeak::CHANNEL, commandIndex->version); + return 0; + }, &parm); + LOG_SQL_CMD(res); + cmd[index++]["end_flat"] = ""; + } + + //Client channel rights + { + cmd[index]["channel_client_flat"] = ""; + PermissionCommandTuple parm{cmd, index, version, 0, 0}; + auto res = sql::command(server->getSql(), "SELECT `id`, `channelId`, `permId`, `value`, `grant`, `flag_skip`, `flag_negate` FROM `permissions` WHERE `serverId` = :sid AND `type` = :type AND `channelId` != 0", + variable{":sid", server->getServerId()}, + variable{":type", permission::SQL_PERM_USER}).query( + [&](PermissionCommandTuple* commandIndex, int length, char** values, char**names){ + auto type = permission::resolvePermissionData(permission::unknown); + permission::PermissionValue value = 0, grant = 0; + bool skipped = false, negated = false; + ChannelId chid = 0; + ClientDbId cldbid = 0; + for(int idx = 0; idx < length; idx++) { + try { + if(strcmp(names[idx], "channelId") == 0) + chid = stoul(values[idx] && strlen(values[idx]) > 0 ? values[idx] : "0"); + if(strcmp(names[idx], "id") == 0) + cldbid = stoul(values[idx] && strlen(values[idx]) > 0 ? values[idx] : "0"); + else if(strcmp(names[idx], "value") == 0) + value = values[idx] && strlen(values[idx]) > 0 ? stoi(values[idx]) : permNotGranted; + else if(strcmp(names[idx], "grant") == 0) + grant = values[idx] && strlen(values[idx]) > 0 ? stoi(values[idx]) : permNotGranted; + else if(strcmp(names[idx], "permId") == 0) { + type = permission::resolvePermissionData(values[idx]); + if(type->type == permission::unknown) { + logError(0, "Could not parse channel client permission for snapshot (Invalid type {})! Skipping it!", values[idx]); + return 0; + } + } + else if(strcmp(names[idx], "flag_skip") == 0) + skipped = values[idx] ? strcmp(values[idx], "1") == 0 : false; + else if(strcmp(names[idx], "flag_negate") == 0) + negated = values[idx] ? strcmp(values[idx], "1") == 0 : false; + } catch (std::exception& ex) { + logError(0, "Failed to write snapshot channel client permission (Skipping it)! Message: {} @ {} => {}", ex.what(), names[idx], values[idx]); + return 0; + } + } + + if(type->type == permission::unknown) { + logError(0, "Could not parse channel client permission for snapshot (Missing type)! Skipping it!"); + return 0; + } + if(chid == 0) { + logError(0, "Could not parse channel client permission for snapshot (Missing channel id)! Skipping it!"); + return 0; + } + if(cldbid == 0) { + logError(0, "Could not parse channel client permission for snapshot (Missing cldbid)! Skipping it!"); + return 0; + } + if(value == permNotGranted && grant == permNotGranted && !skipped && !negated) return 0; + + if(commandIndex->channel != chid || commandIndex->client != cldbid) { + commandIndex->channel = chid; + commandIndex->client = cldbid; + commandIndex->cmd[commandIndex->index]["id1"] = chid; + commandIndex->cmd[commandIndex->index]["id2"] = cldbid; + } + SnapshotPermissionEntry{type, value, grant, negated, skipped}.write(commandIndex->cmd, commandIndex->index, permission::teamspeak::CLIENT, commandIndex->version); + return 0; + }, &parm); + LOG_SQL_CMD(res); + cmd[index++]["end_flat"] = ""; + } + cmd[index++]["end_permissions"] = ""; + } + return true; +} \ No newline at end of file diff --git a/server/src/ShutdownHelper.cpp b/server/src/ShutdownHelper.cpp new file mode 100644 index 0000000..920cb24 --- /dev/null +++ b/server/src/ShutdownHelper.cpp @@ -0,0 +1,130 @@ +// +// Created by wolverindev on 01.04.18. +// + +#include +#include +#include "ShutdownHelper.h" +#include "InstanceHandler.h" + +using namespace std; +using namespace std::chrono; +using namespace ts; +using namespace ts::server; + +extern bool mainThreadActive; +extern ts::server::InstanceHandler* serverInstance; + +bool shuttingDown = false; +void ts::server::shutdownInstance(const std::string& message) { + if(shuttingDown) return; + shuttingDown = true; + threads::Thread(THREAD_EXECUTE_LATER, [](){ + threads::self::sleep_for(chrono::seconds(30)); + logCritical("Could not shutdown server within 30 seconds! (Hangup!)"); + logCritical("Killing server!"); + + threads::Thread(THREAD_EXECUTE_LATER, [](){ + threads::self::sleep_for(chrono::seconds(5)); + logCritical("Failed to exit normally!"); + logCritical("executing raise(SIGKILL);"); + raise(SIGKILL); + }).name("Stop exit controller").execute().detach(); + exit(2); + }).name("Stop controller").execute().detach(); + + + logMessage("Stopping all server instances!"); + if(serverInstance && serverInstance->getVoiceServerManager()) + serverInstance->getVoiceServerManager()->shutdownAll(message); + + mainThreadActive = false; +} + +std::shared_ptr currentShutdown = nullptr; +std::shared_ptr server::scheduledShutdown() { return currentShutdown; } + +inline void broadcastMessage(const std::string& message) { + if(!serverInstance || !serverInstance->getVoiceServerManager()); + for(const auto &server : serverInstance->getVoiceServerManager()->serverInstances()) { + if(server->running()) { + server->broadcastMessage(server->getServerRoot(), message); + } + } +} + +void executeScheduledShutdown(const std::shared_ptr& data); +bool server::scheduleShutdown(const std::chrono::system_clock::time_point& time, const std::string& reason) { + server::cancelShutdown(false); //Cancel old shutdown + + auto data = std::make_shared(); + data->active = true; + data->time_point = time; + data->reason = reason; + + data->shutdownThread = new threads::Thread(THREAD_EXECUTE_LATER | THREAD_SAVE_OPERATIONS, [data](){ executeScheduledShutdown(data); }); + data->shutdownThread->name("Shutdown Executor").execute(); + currentShutdown = data; + return true; +} + +void server::cancelShutdown(bool notify) { + if(!currentShutdown) return; + if(notify && !config::messages::shutdown::canceled.empty()) { + broadcastMessage(config::messages::shutdown::canceled); + } + + auto current = server::scheduledShutdown(); + current->active = false; + current->shutdownNotify.notify_all(); + if(current->shutdownThread->join(seconds(3)) != 0) { + logCritical("Could not terminal shutdown thread!"); + } + delete current->shutdownThread; + current->shutdownThread = nullptr; + + currentShutdown = nullptr; +} + +void executeScheduledShutdown(const shared_ptr& data) { + std::time_t time_point = system_clock::to_time_t(data->time_point); + { + auto message = strvar::transform(config::messages::shutdown::scheduled, strvar::FunctionValue("time", (strvar::FunctionValue::FValueFNEasy) [&](std::deque value) { + auto pattern = !value.empty() ? value[0] : "%Y-%m-%d_%H:%M:%S"; + + tm* tm_info = localtime(&time_point); + + char timeBuffer[1024]; + if(strftime(timeBuffer, 1024, pattern.c_str(), tm_info) == 0) { + return string("string is longer than the buffer"); + } + + return string(timeBuffer); + })); + broadcastMessage(message); + } + + while(data->time_point > system_clock::now()) { + auto per = data->time_point - system_clock::now(); + pair best_period = {seconds(0), ""}; + + for(const auto& period : config::messages::shutdown::intervals) { + if(period.first > per) continue; + if(period.first > best_period.first) best_period = period; + } + + { + std::unique_lock lock(data->shutdownMutex); + data->shutdownNotify.wait_until(lock, data->time_point - best_period.first, [data](){ return !data->active; }); + if(!data->active) return; + } + + if(best_period.first.count() == 0) + broadcastMessage(config::messages::shutdown::now); + else + broadcastMessage(strvar::transform(config::messages::shutdown::interval, strvar::StringValue{"interval", best_period.second})); + } + + ts::server::shutdownInstance(data->reason); + //No need to delete own task +} \ No newline at end of file diff --git a/server/src/ShutdownHelper.h b/server/src/ShutdownHelper.h new file mode 100644 index 0000000..b707983 --- /dev/null +++ b/server/src/ShutdownHelper.h @@ -0,0 +1,24 @@ +#pragma once + +#include "Configuration.h" + +namespace ts { + namespace server { + extern void shutdownInstance(const std::string& reason = ts::config::messages::applicationStopped); + + + struct ShutdownData { + std::string reason; + std::chrono::system_clock::time_point time_point; + bool active; + + threads::Thread* shutdownThread = nullptr; + std::mutex shutdownMutex; + std::condition_variable shutdownNotify; + }; + + extern bool scheduleShutdown(const std::chrono::system_clock::time_point&, const std::string& = ts::config::messages::applicationStopped); + extern std::shared_ptr scheduledShutdown(); + extern void cancelShutdown(bool notify = true); + } +} \ No newline at end of file diff --git a/server/src/SignalHandler.cpp b/server/src/SignalHandler.cpp new file mode 100644 index 0000000..8177eea --- /dev/null +++ b/server/src/SignalHandler.cpp @@ -0,0 +1,101 @@ + +#include +#include "TSServer.h" +#include "SignalHandler.h" +#include "ServerManager.h" +#include "InstanceHandler.h" +#include "ShutdownHelper.h" +#include +#include +#include + +using namespace std; +namespace fs = std::experimental::filesystem; + +google_breakpad::ExceptionHandler* globalExceptionHandler = nullptr; +#define SIG(s, c) \ + if(signal(s, c) != nullptr) logError(lstream << "Cant setup " #s); + + +extern bool mainThreadDone; +static bool dumpCallback(const google_breakpad::MinidumpDescriptor& descriptor, void* context, bool succeeded) { + logCritical("The server crashed!"); + try { + if(!fs::exists(fs::u8path(ts::config::crash_path))) + fs::create_directories(fs::u8path(ts::config::crash_path)); + + auto path = fs::u8path(descriptor.path()); + path = fs::u8path(ts::config::crash_path + "crash_dump_" + path.filename().string()); + fs::rename(fs::u8path(descriptor.path()), path); + logCritical("Wrote crash dump to " + path.relative_path().string()); + } catch (...) { + logCritical("Failed to write/move crash dump!"); + } + if(std::current_exception()) { + logCritical(LOG_GENERAL, "Exception reached stack root and cause the server to crash!"); + logCritical(LOG_GENERAL, " Type: {}", std::current_exception().__cxa_exception_type()->name()); + try { + std::rethrow_exception(std::current_exception()); + } catch(std::exception& ex) { + logCritical(LOG_GENERAL, " Message: {}", ex.what()); + } catch(...) {} + } + logCritical("Please report this crash to the TeaSpeak maintainer WolverinDEV"); + logCritical("Official issue and bug tracker url: https://github.com/TeaSpeak/TeaSpeak/issues"); + logCritical("Any reports of crashes are useless if you not provide the above generated crashlog!"); + logCritical("Stopping server"); + ts::server::shutdownInstance(ts::config::messages::applicationCrashed); + while(!mainThreadDone) threads::self::sleep_for(chrono::seconds(1)); + return succeeded; +} + +std::atomic spawn_failed_count = 0; +bool ts::syssignal::setup() { + logMessage("Setting up exception handler"); + globalExceptionHandler = new google_breakpad::ExceptionHandler(google_breakpad::MinidumpDescriptor("."), nullptr, dumpCallback, nullptr, true, -1); + + SIG(SIGTERM, &ts::syssignal::handleStopSignal); + if(isatty(fileno(stdin))) //We cant listen for this siganl if stdin ist a atty + SIG(SIGINT, &ts::syssignal::handleStopSignal); + + return true; +} + +bool ts::syssignal::setup_threads() { + threads::set_global_error_handler([](auto error) { + if(error == threads::ThreadError::HANDLE_DELETE_UNDETACHED) { + logCritical(LOG_GENERAL, "Missed out thread detachment! This could lead to memory leaks!"); + return threads::ThreadErrorAction::IGNORE; + } else if(error == threads::ThreadError::SPAWN_FAILED) { + logCritical(LOG_GENERAL, "Spawning a new thread failed!"); + if(spawn_failed_count++ == 0) { + logCritical(LOG_GENERAL, "Stopping process!"); + try { + std::thread([]{ + ts::server::shutdownInstance("Failed to spawn new threads! Safety shutdown"); + }).detach(); + } catch(...) { + logCritical(LOG_GENERAL, "Failed to spawn shutdown thread (Of cause...). Stopping application directly!"); + logCritical(LOG_GENERAL, "If this happens frequently dont forget to checkout std stderr channel for more information"); + raise(SIGKILL); + } + } + return threads::ThreadErrorAction::RAISE; + } + + return threads::ThreadErrorAction::RAISE; + }); + return true; +} + +atomic_int signal_count = 0; +void ts::syssignal::handleStopSignal(int signal) { + logMessageFmt(true, LOG_INSTANCE, "Got stop signal ({}). Stopping instance.", signal == SIGTERM ? "SIGTERM" : + signal == SIGINT ? "SIGINT" : + "UNKNOWN (" + to_string(signal) + ")"); + if(signal_count++ >= 3) { + logMessageFmt(true, LOG_INSTANCE, "Got stop signal more that tree times. Force exiting instance."); + raise(SIGKILL); + } + ts::server::shutdownInstance(); +} \ No newline at end of file diff --git a/server/src/SignalHandler.h b/server/src/SignalHandler.h new file mode 100644 index 0000000..6ff5215 --- /dev/null +++ b/server/src/SignalHandler.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include "Configuration.h" + +namespace ts { + namespace syssignal { + extern bool setup(); + extern bool setup_threads(); + extern void handleStopSignal(int); + } +} \ No newline at end of file diff --git a/server/src/TS3ServerClientManager.cpp b/server/src/TS3ServerClientManager.cpp new file mode 100644 index 0000000..0b8b724 --- /dev/null +++ b/server/src/TS3ServerClientManager.cpp @@ -0,0 +1,540 @@ +#include +#include +#include "client/voice/VoiceClient.h" +#include "client/InternalClient.h" +#include "TSServer.h" +#include +#include +#include +#include "InstanceHandler.h" + +using namespace std; +using namespace ts::server; +using namespace ts::protocol; +using namespace ts::buffer; +using namespace ts::permission; +using namespace std::chrono; + +bool TSServer::registerClient(shared_ptr client) { + sassert(client); + + { + lock_guard lock(this->clients.lock); + if(client->getClientId() > 0) { + logCritical(this->getServerId(), "Client {} ({}|{}) has been already registered!", client->getDisplayName(), client->getClientId(), client->getUid()); + return false; + } + + ClientId client_id = 0; + ClientId max_client_id = this->clients.clients.size(); + + while(client_id < max_client_id && this->clients.clients[client_id]) + client_id++; + if(client_id == max_client_id) + this->clients.clients.push_back(client); + else + this->clients.clients[client_id] = client; + this->clients.count++; + client->setClientId(client_id); + } + + { + lock_guard lock(this->client_nickname_lock); + + auto login_name = client->getDisplayName(); + while(login_name.length() < 3) + login_name += "."; + + if(client->getExternalType() == ClientType::CLIENT_TEAMSPEAK) + client->properties()[property::CLIENT_LOGIN_NAME] = login_name; + + std::shared_ptr found_client = nullptr; + + auto client_name = login_name; + size_t counter = 0; + + { + lock_guard clients_lock(this->clients.lock); + while(true) { + for(auto& _client : this->clients.clients) { + if(!_client) continue; + + if(_client->getDisplayName() == client_name && _client != client) + goto increase_name; + } + goto nickname_valid; + + increase_name: + client_name = login_name + to_string(++counter); + } + } + nickname_valid: + client->setDisplayName(client_name); + } + + + + if(client->getType() == ClientType::CLIENT_TEAMSPEAK || client->getType() == ClientType::CLIENT_WEB) { + this->properties()[property::VIRTUALSERVER_CLIENT_CONNECTIONS] ++; //increase manager connections + this->properties()[property::VIRTUALSERVER_LAST_CLIENT_CONNECT] = duration_cast(system_clock::now().time_since_epoch()).count(); + } + else if(client->getType() == ClientType::CLIENT_QUERY) { + this->properties()[property::VIRTUALSERVER_LAST_QUERY_CONNECT] = duration_cast(system_clock::now().time_since_epoch()).count(); + this->properties()[property::VIRTUALSERVER_QUERY_CLIENT_CONNECTIONS] ++; //increase manager connections + } + + return true; +} + +bool TSServer::unregisterClient(shared_ptr cl, std::string reason, std::unique_lock& chan_tree_lock) { + if(cl->getType() == ClientType::CLIENT_TEAMSPEAK && cl->getType() == ClientType::CLIENT_WEB) { + sassert(cl->state == ConnectionState::DISCONNECTED); + } + + auto client_id = cl->getClientId(); + if(client_id == 0) return false; /* not registered */ + { + + lock_guard lock(this->clients.lock); + if(client_id >= this->clients.clients.size()) { + logCritical(this->getServerId(), "Client {} ({}|{}) has been registered, but client id exceed client id! Failed to unregister client.", cl->getDisplayName(), client_id, cl->getUid()); + } else { + auto& client_container = this->clients.clients[client_id]; + if(client_container != cl) { + logCritical(this->getServerId(), "Client {} ({}|{}) has been registered, but container hasn't client set! Failed to unregister client.", cl->getDisplayName(), client_id, cl->getUid()); + } else { + client_container.reset(); + this->clients.count--; + } + } + } + + if(cl->getType() == ClientType::CLIENT_TEAMSPEAK || cl->getType() == ClientType::CLIENT_WEB) + this->properties()[property::VIRTUALSERVER_LAST_CLIENT_DISCONNECT] = duration_cast(system_clock::now().time_since_epoch()).count(); + else if(cl->getType() == ClientType::CLIENT_QUERY) + this->properties()[property::VIRTUALSERVER_LAST_QUERY_DISCONNECT] = duration_cast(system_clock::now().time_since_epoch()).count(); + + { + if(!chan_tree_lock.owns_lock()) + chan_tree_lock.lock(); + + if(cl->currentChannel) //We dont have to make him invisible if he hasnt even a channel + this->client_move(cl, nullptr, nullptr, "client destroyed", ViewReasonId::VREASON_SERVER_LEFT, false, chan_tree_lock); + } + + serverInstance->databaseHelper()->saveClientPermissions(this->ref(), cl->getClientDatabaseId(), cl->clientPermissions); + cl->setClientId(0); + return true; +} + +void TSServer::registerInternalClient(std::shared_ptr client) { + client->state = ConnectionState::CONNECTED; + { + lock_guard lock(this->clients.lock); + if(client->getClientId() > 0) { + logCritical(this->getServerId(), "Internal client {} ({}|{}) has been already registered!", client->getDisplayName(), client->getClientId(), client->getUid()); + return; + } + + ClientId client_id = 0; + ClientId max_client_id = this->clients.clients.size(); + while(client_id < max_client_id && this->clients.clients[client_id]) + client_id++; + if(client_id == max_client_id) + this->clients.clients.push_back(client); + else + this->clients.clients[client_id] = client; + + this->clients.clients[client_id] = client; + this->clients.count++; + client->setClientId(client_id); + } +} + +void TSServer::unregisterInternalClient(std::shared_ptr client) { + client->state = ConnectionState::DISCONNECTED; + + { + auto client_id = client->getClientId(); + + lock_guard lock(this->clients.lock); + if(client_id >= this->clients.clients.size()) { + logCritical(this->getServerId(), "Client {} ({}|{}) has been registered, but client id exceed client id! Failed to unregister internal client.", client->getDisplayName(), client_id, client->getUid()); + } else { + auto& client_container = this->clients.clients[client_id]; + if(client_container != client) { + logCritical(this->getServerId(), "Client {} ({}|{}) has been registered, but container hasn't client set! Failed to unregister internal client.", client->getDisplayName(), client_id, client->getUid()); + } else { + this->clients.count--; + client_container.reset(); + } + } + } +} + +bool TSServer::assignDefaultChannel(const shared_ptr& client, bool join) { + shared_lock server_channel_lock(this->channel_tree_lock); + std::shared_ptr channel = nullptr; + if(client->properties()->hasProperty(property::CLIENT_DEFAULT_CHANNEL) && !client->properties()[property::CLIENT_DEFAULT_CHANNEL].as().empty()) { + auto str = client->properties()[property::CLIENT_DEFAULT_CHANNEL].as(); + if(str[0] == '/' && str.find_first_not_of("0123456789", 1) == string::npos) + channel = this->channelTree->findChannel(static_cast(stoll(str.substr(1)))); + else + channel = this->channelTree->findChannelByPath(str); + if (channel) { + if(!channel->permission_granted(permission::i_channel_needed_join_power, client->calculate_permission_value(permission::i_channel_join_power, channel->channelId()), false)) { + channel = nullptr; + logMessage(this->serverId, "[{}] Client tried to connect to a channel which he hasn't permission for. Channel: {} ({})", CLIENT_STR_LOG_PREFIX_(client), channel->channelId(), channel->name()); + } else if (!channel->passwordMatch(client->properties()[property::CLIENT_DEFAULT_CHANNEL_PASSWORD], true) && client->permissionValue(permission::PERMTEST_ORDERED, permission::b_channel_join_ignore_password, channel) < 1) { + logMessage(this->serverId, "[{}] Client tried to connect to a channel which is password protected and he hasn't the right password. Channel: {} ({})", CLIENT_STR_LOG_PREFIX_(client), channel->channelId(), channel->name()); + channel = nullptr; + } + } else + logMessage("Client " + client->getDisplayName() + "/" + client->getUid() + " tried to join on a not existing channel. Name: " + client->properties()[property::CLIENT_DEFAULT_CHANNEL].as()); + } + if(!channel) channel = this->channelTree->getDefaultChannel(); + if(!channel) return false; + + if(join) { + server_channel_lock.unlock(); + unique_lock server_channel_w_lock(this->channel_tree_lock); + this->client_move(client, channel, nullptr, "", ViewReasonId::VREASON_USER_ACTION, false, server_channel_w_lock); + } else { + client->currentChannel = channel; + } + return true; +} + +void TSServer::testBanStateChange(const std::shared_ptr& invoker) { + this->forEachClient([&](shared_ptr client) { + auto ban = client->resolveActiveBan(client->getPeerIp()); + if(ban) { + debugMessage(this->getServerId(), "Found online client with a active ban. ({})", CLIENT_STR_LOG_PREFIX_(client)); + auto entryTime = ban->until.time_since_epoch().count() > 0 ? (uint64_t) duration_cast(ban->until - system_clock::now()).count() : 0UL; + this->notify_client_ban(client, invoker, ban->reason, entryTime); + client->closeConnection(system_clock::now() + seconds(1)); + } + }); +} + +bool TSServer::could_default_create_channel() { + { + + auto default_group = this->getGroupManager()->defaultGroup(GroupTarget::GROUPTARGET_SERVER); + if(default_group) { + auto flag = default_group->permissions()->permission_value_flagged(permission::b_channel_create_temporary).value == 1; + flag = flag ? flag : default_group->permissions()->permission_value_flagged(permission::b_channel_create_semi_permanent).value == 1; + flag = flag ? flag : default_group->permissions()->permission_value_flagged(permission::b_channel_create_permanent).value == 1; + if(flag) + return true; + } + } + { + + auto default_group = this->getGroupManager()->defaultGroup(GroupTarget::GROUPTARGET_CHANNEL); + if(default_group) { + auto flag = default_group->permissions()->permission_value_flagged(permission::b_channel_create_temporary).value == 1; + flag = flag ? flag : default_group->permissions()->permission_value_flagged(permission::b_channel_create_semi_permanent).value == 1; + flag = flag ? flag : default_group->permissions()->permission_value_flagged(permission::b_channel_create_permanent).value == 1; + if(flag) + return true; + } + } + + return false; +} + +/* + * + for (auto &cl : this->server->getClients()) + if (cl->isClientVisible(client) || client == cl) + cl->notifyClientLeftViewKicked(client, client->currentChannel, nullptr, cmd["reasonmsg"].as(), this); + */ + +void TSServer::notify_client_ban(const shared_ptr &target, const std::shared_ptr &invoker, const std::string &reason, size_t time) { + /* the target is not allowed to execute anything; Must before channel tree lock because the target may waits for us to finish the channel stuff */ + lock_guard command_lock(target->command_lock); + unique_lock server_channel_lock(this->channel_tree_lock); /* we're "moving" a client! */ + + if(target->currentChannel) { + for(const auto& client : this->getClients()) { + if(!client || client == target) + continue; + + unique_lock client_channel_lock(client->channel_lock); + if(client->isClientVisible(target, false)) + client->notifyClientLeftViewBanned(target, reason, invoker, time, false); + } + + auto s_channel = dynamic_pointer_cast(target->currentChannel); + s_channel->unregister_client(target); + } + + /* now disconnect the target itself */ + unique_lock client_channel_lock(target->channel_lock); + target->notifyClientLeftViewBanned(target, reason, invoker, time, false); + target->currentChannel = nullptr; +} + +void TSServer::notify_client_kick( + const std::shared_ptr &target, + const std::shared_ptr &invoker, + const std::string &reason, + const std::shared_ptr &target_channel) { + + if(target_channel) { + /* use the move! */ + unique_lock server_channel_lock(this->channel_tree_lock, defer_lock); + this->client_move(target, target_channel, invoker, reason, ViewReasonId::VREASON_CHANNEL_KICK, true, server_channel_lock); + } else { + /* the target is not allowed to execute anything; Must before channel tree lock because the target may waits for us to finish the channel stuff */ + lock_guard command_lock(target->command_lock); + unique_lock server_channel_lock(this->channel_tree_lock); /* we're "moving" a client! */ + + if(target->currentChannel) { + for(const auto& client : this->getClients()) { + if(!client || client == target) + continue; + + unique_lock client_channel_lock(client->channel_lock); + if(client->isClientVisible(target, false)) + client->notifyClientLeftViewKicked(target, nullptr, reason, invoker, false); + } + + auto s_channel = dynamic_pointer_cast(target->currentChannel); + s_channel->unregister_client(target); + } + + /* now disconnect the target itself */ + unique_lock client_channel_lock(target->channel_lock); + target->notifyClientLeftViewKicked(target, nullptr, reason, invoker, false); + target->currentChannel = nullptr; + } +} + +/* + * 1. flag channel as deleted (lock channel tree so no moves) + * 2. Gather all clients within the channel (lock their execute lock) + * 3. Unlock channel tree and lock client locks + * 4. lock channel tree again and move the clients (No new clients should be joined because channel is flagged as deleted!) + * + * Note: channel cant be a ref because the channel itself gets deleted! + */ +void TSServer::delete_channel(shared_ptr channel, const shared_ptr &invoker, const std::string& kick_message, unique_lock &tree_lock) { + if(!tree_lock.owns_lock()) + tree_lock.lock(); + if(channel->deleted) + return; + + deque> clients; + { + for(const auto& sub_channel : this->channelTree->channels(channel)) { + auto s_channel = dynamic_pointer_cast(sub_channel); + assert(s_channel); + + auto chan_clients = this->getClientsByChannel(sub_channel); + clients.insert(clients.end(), chan_clients.begin(), chan_clients.end()); + s_channel->deleted = true; + } + auto chan_clients = this->getClientsByChannel(channel); + clients.insert(clients.end(), chan_clients.begin(), chan_clients.end()); + channel->deleted = true; + } + auto default_channel = this->channelTree->getDefaultChannel(); + tree_lock.unlock(); + + deque> command_locks; + for(const auto& client : clients) { + command_locks.push_back(move(unique_lock(client->command_lock))); + } + + for(const auto& client : clients) + this->client_move(client, default_channel, invoker, kick_message, ViewReasonId::VREASON_CHANNEL_KICK, true, tree_lock); + + if(!tree_lock.owns_lock()) + tree_lock.lock(); /* no clients left within that tree */ + command_locks.clear(); + + auto channel_ids = this->channelTree->delete_channel_root(channel); + this->forEachClient([&](const shared_ptr& client) { + unique_lock client_channel_lock(client->channel_lock); + client->notifyChannelDeleted(client->channels->delete_channel_root(channel), invoker); + }); +} + +void TSServer::client_move( + const shared_ptr &target, + shared_ptr target_channel, + const std::shared_ptr &invoker, + const std::string &reason_message, + ts::ViewReasonId reason_id, + bool notify_client, + std::unique_lock &server_channel_write_lock) { + + TIMING_START(timings); + if(server_channel_write_lock.owns_lock()) + server_channel_write_lock.unlock(); + + lock_guard client_command_lock(target->command_lock); + server_channel_write_lock.lock(); + TIMING_STEP(timings, "chan tree l"); + if(target->currentChannel == target_channel) + return; + + /* first step: resolve the target channel / or fix missing */ + auto s_target_channel = dynamic_pointer_cast(target_channel); + auto s_source_channel = dynamic_pointer_cast(target->currentChannel); + assert(!target->currentChannel || s_source_channel != nullptr); + + if(target_channel) { + assert(s_target_channel); + if(s_target_channel->deleted) { + target_channel = this->channelTree->getDefaultChannel(); + s_target_channel = dynamic_pointer_cast(target_channel); + assert(s_target_channel); + } + } + auto l_target_channel = s_target_channel ? this->channelTree->findLinkedChannel(s_target_channel->channelId()) : nullptr; + auto l_source_channel = s_source_channel ? this->channelTree->findLinkedChannel(s_source_channel->channelId()) : nullptr; + TIMING_STEP(timings, "channel res"); + + /* second step: show the target channel to the client if its not shown and let him subscibe to the channel */ + if(target_channel && notify_client) { + unique_lock client_channel_lock(target->channel_lock); + + bool success = false; + /* TODO: Use a bunk here and not a notify for every single */ + for(const auto& channel : target->channels->show_channel(l_target_channel, success)) + target->notifyChannelShow(channel->channel(), channel->previous_channel); + sassert(success); + if(!success) + return; + + target->subscribeChannel({target_channel}, false, true); + } + TIMING_STEP(timings, "target show"); + + if(target_channel) { + this->forEachClient([&](const shared_ptr& client) { + if (!notify_client && client == target) return; + + unique_lock client_channel_lock(client->channel_lock); + auto chan_target = client->channels->find_channel(target_channel); + + if(chan_target) { + auto chan_source = client->channels->find_channel(s_source_channel); + if(chan_source) { + if (chan_target->subscribed || client == target) { + if (client->isClientVisible(target, false) || client == target) { + client->notifyClientMoved(target, s_target_channel, reason_id, reason_message, invoker, false); + } else { + client->notifyClientEnterView(target, invoker, reason_message, s_target_channel, reason_id, s_source_channel, false); + } + } else if(client->isClientVisible(target, false)){ + //Client got out of view + client->notifyClientLeftView(target, s_target_channel, reason_id, reason_message.empty() ? string("view left") : reason_message, invoker, false); + } + } else { + if(client == target && client->getType() != ClientType::CLIENT_INTERNAL && client->getType() != ClientType::CLIENT_MUSIC) + logCritical(this->getServerId(), "{} Client enters visibility twice!", CLIENT_STR_LOG_PREFIX_(client)); + + //Client entered view + if(chan_target->subscribed) + client->notifyClientEnterView(target, invoker, reason_message, s_target_channel, ViewReasonId::VREASON_USER_ACTION, nullptr, false); + } + } else { + /* target channel isn't visible => so client gone out of view */ + if(client == target && client->getType() != ClientType::CLIENT_INTERNAL && client->getType() != ClientType::CLIENT_MUSIC) + logCritical(this->getServerId(), "{} Moving own client into a not visible channel! This shall not happen!", CLIENT_STR_LOG_PREFIX_(client)); + //Test for in view? (Notify already does but nvm) + + if(client->isClientVisible(target, false)){ + //Client got out of view + if(reason_id == ViewReasonId::VREASON_USER_ACTION) + client->notifyClientLeftView(target, nullptr, ViewReasonId::VREASON_SERVER_LEFT, reason_message.empty() ? "joined a hidden channel" : reason_message, invoker, false); + else + client->notifyClientLeftView(target, nullptr, ViewReasonId::VREASON_SERVER_LEFT, reason_message.empty() ? "moved to a hidden channel" : reason_message, invoker, false); + } + } + }); + + if(s_source_channel) + s_source_channel->unregister_client(target); + s_target_channel->register_client(target); + } else { + /* client left the server */ + if(target->currentChannel) { + for(const auto& client : this->getClients()) { + if(!client || client == target) + continue; + + unique_lock client_channel_lock(client->channel_lock); + if(client->isClientVisible(target, false)) + client->notifyClientLeftView(target, nullptr, reason_id, reason_message, invoker, false); + } + + s_source_channel->unregister_client(target); + } + } + TIMING_STEP(timings, "notify view"); + + target->currentChannel = target_channel; + server_channel_write_lock.unlock(); + + /* third step: update stuff for the client (remember: the client cant execute anything at the moment!) */ + shared_lock server_channel_read_lock(this->channel_tree_lock); + unique_lock client_channel_lock(target->channel_lock); + TIMING_STEP(timings, "lock own tr"); + + deque client_updates; + if (s_source_channel) { + s_source_channel->properties()[property::CHANNEL_LAST_LEFT] = chrono::duration_cast(chrono::system_clock::now().time_since_epoch()).count(); + auto source_channel_group = this->groups->getChannelGroupExact(target->getClientDatabaseId(), s_source_channel, false); + if(source_channel_group) { + auto default_data = source_channel_group->group->permissions()->permission_value_flagged(permission::b_group_is_permanent); + if(!default_data.has_value || default_data.value != 1) + this->groups->setChannelGroup(target->getClientDatabaseId(), nullptr, s_source_channel); + } + + auto update = target->properties()[property::CLIENT_IS_TALKER].as() || target->properties()[property::CLIENT_TALK_REQUEST].as() > 0; + if(update) { + target->properties()[property::CLIENT_IS_TALKER] = 0; + target->properties()[property::CLIENT_TALK_REQUEST] = 0; + target->properties()[property::CLIENT_TALK_REQUEST_MSG] = ""; + client_updates.push_back(property::CLIENT_IS_TALKER); + client_updates.push_back(property::CLIENT_TALK_REQUEST); + client_updates.push_back(property::CLIENT_TALK_REQUEST_MSG); + } + TIMING_STEP(timings, "src chan up"); + } + + if (s_target_channel) { + auto changed_groups = this->groups->update_server_group_property(target, false); + if(target->update_cached_permissions()) /* update cached calculated permissions */ + target->sendNeededPermissions(false); + client_updates.insert(client_updates.end(), changed_groups.begin(), changed_groups.end()); + TIMING_STEP(timings, "perm gr upd"); + + if(s_source_channel) { + deque deleted; + for(const auto& channel : target->channels->test_channel(l_source_channel, l_target_channel)) + deleted.push_back(channel->channelId()); + + if(!deleted.empty()) + target->notifyChannelHide(deleted, false); + + if(!s_source_channel->permission_granted(permission::i_channel_needed_subscribe_power, target->calculate_permission_value(permission::i_channel_subscribe_power, s_source_channel->channelId()), false)) + target->unsubscribeChannel({s_source_channel}, false); //Unsubscribe last channel (hasn't permissions) + TIMING_STEP(timings, "src hide ts"); + } + } + client_channel_lock.unlock(); + /* both methods lock if they require stuff */ + this->notifyClientPropertyUpdates(target, client_updates, s_source_channel ? true : false); + TIMING_STEP(timings, "notify cpro"); + if(s_target_channel) { + target->updateChannelClientProperties(false, s_source_channel ? true : false); + TIMING_STEP(timings, "notify_t_pr"); + } + debugMessage(this->getServerId(), "{} Client move timings: {}", CLIENT_STR_LOG_PREFIX_(target), TIMING_FINISH(timings)); +} \ No newline at end of file diff --git a/server/src/TS3ServerHeartbeat.cpp b/server/src/TS3ServerHeartbeat.cpp new file mode 100644 index 0000000..790d534 --- /dev/null +++ b/server/src/TS3ServerHeartbeat.cpp @@ -0,0 +1,271 @@ +#include +#include +#include "client/voice/VoiceClient.h" +#include +#include "InstanceHandler.h" +#include "TSServer.h" + +using namespace std; +using namespace std::chrono; +using namespace ts::server; +using namespace ts::protocol; +using namespace ts::buffer; + +extern InstanceHandler* serverInstance; +inline void banClientFlood(TSServer* server, const shared_ptr& cl, time_point until){ + auto time = until.time_since_epoch().count() == 0 ? 0L : chrono::ceil(until - system_clock::now()).count(); + + std::string reason = "You're flooding too much"; + serverInstance->banManager()->registerBan(server->getServerId(), cl->getClientDatabaseId(), reason, cl->getUid(), cl->getLoggingPeerIp(), "", "", until); + + for(const auto &client : server->findClientsByUid(cl->getUid())) { + server->notify_client_ban(client, server->getServerRoot(), reason, time); + client->closeConnection(system_clock::now() + seconds(1)); + } +} + +#define BEGIN_TIMINGS() timing_begin = system_clock::now() +#define END_TIMINGS(variable) \ +timing_end = system_clock::now(); \ +variable = duration_cast(timing_end - timing_begin); + +void TSServer::executeServerTick() { + threads::MutexTryLock l(this->stateLock); //Should not attempt to shutdown or start the server while ticking + if(!l) { + if(this->running()) + logError(this->getServerId(), "Failed to lock tick mutex!"); + return; + } + if(!this->running()) return; + + try { + if(lastTick.time_since_epoch().count() != 0) { + auto delay = system_clock::now() - lastTick; + auto delay_ms = duration_cast(delay).count(); + if(delay_ms > 510) { + if(delay_ms < 750) + logWarning(this->getServerId(), "Found varianzes within the server tick! (Supposed: 500ms Hold: {}ms)", delay_ms); + else + logError(this->getServerId(), "Found varianzes within the server tick! This long delay could be an issue. (Supposed: 500ms Hold: {}ms)", delay_ms); + } + } + lastTick = system_clock::now(); + + system_clock::time_point timing_begin, timing_end; + milliseconds timing_update_states, timing_client_tick, timing_channel, timing_statistic, timing_groups; + + auto client_list = this->getClients(); + + { + BEGIN_TIMINGS(); + + size_t clientOnline = 0; + size_t queryOnline = 0; + for(const auto& conn : this->getClients()){ + switch (conn->getType()){ + case ClientType::CLIENT_TEAMSPEAK: + case ClientType::CLIENT_TEASPEAK: + case ClientType::CLIENT_WEB: + clientOnline++; + break; + case ClientType::CLIENT_QUERY: + case ClientType::CLIENT_MUSIC: + queryOnline++; + break; + default: + break; + } + } + + properties()[property::VIRTUALSERVER_UPTIME] = std::chrono::duration_cast(std::chrono::system_clock::now() - this->startTimestamp).count(); + properties()[property::VIRTUALSERVER_CLIENTS_ONLINE] = clientOnline + queryOnline; + properties()[property::VIRTUALSERVER_QUERYCLIENTS_ONLINE] = queryOnline; + if(clientOnline + queryOnline == 0) //We don't need to tick, when server is empty! + return; + properties()[property::VIRTUALSERVER_CHANNELS_ONLINE] = this->channelTree->channel_count(); + properties()[property::VIRTUALSERVER_TOTAL_PING] = this->averagePing(); + + END_TIMINGS(timing_update_states); + } + + { + BEGIN_TIMINGS(); + + auto flood_decrease = this->properties()[property::VIRTUALSERVER_ANTIFLOOD_POINTS_TICK_REDUCE].as(); + auto flood_block = this->properties()[property::VIRTUALSERVER_ANTIFLOOD_POINTS_NEEDED_IP_BLOCK].as(); + + bool flag_update_spoken = this->spoken_time_timestamp + seconds(30) < system_clock::now(); + + system_clock::time_point tick_client_begin, tick_client_end = system_clock::now(); + for(const auto& cl : this->getClients()) { + tick_client_begin = tick_client_end; + if(cl->server != this) { + logError(this->getServerId(), "Got registered client, but client does not think hes bound to this server!"); + + { + lock_guard lock(this->clients.lock); + for(auto& client : this->clients.clients) { + if(client != cl) continue; + + client.reset(); + this->clients.count--; + break; + } + } + continue; //Fully ha? + } + if(cl->floodPoints > flood_block){ + if(!cl->ignoresFlood()) { + banClientFlood(this, cl, system_clock::now() + minutes(10)); + continue; + } + } + if(cl->floodPoints > flood_decrease) + cl->floodPoints -= flood_decrease; + else cl->floodPoints = 0; + cl->tick(tick_client_end); + auto voice = dynamic_pointer_cast(cl); + if(flag_update_spoken && voice) + this->spoken_time += voice->takeSpokenTime(); + tick_client_end = system_clock::now(); + + auto passed_time = tick_client_end - tick_client_begin; + if(passed_time > microseconds(2500)) { + if(passed_time > milliseconds(10)) { + logError(this->serverId, "Ticking of client {:1} ({:2}) needs more that 2500 microseconds! ({:3} microseconds)", + cl->getLoggingPeerIp() + ":" + to_string(cl->getPeerPort()), + cl->getDisplayName(), + duration_cast(tick_client_end - tick_client_begin).count() + ); + } else { + logWarning(this->serverId, "Ticking of client {:1} ({:2}) needs more that 2500 microseconds! ({:3} microseconds)", + cl->getLoggingPeerIp() + ":" + to_string(cl->getPeerPort()), + cl->getDisplayName(), + duration_cast(tick_client_end - tick_client_begin).count() + ); + } + } + + if(cl->clientPermissions->require_db_updates()) { + auto begin = system_clock::now(); + serverInstance->databaseHelper()->saveClientPermissions(this->ref(), cl->getClientDatabaseId(), cl->clientPermissions); + auto end = system_clock::now(); + debugMessage(this->serverId, "Saved client permissions for client {} ({}) in {}ms", cl->getClientDatabaseId(), cl->getDisplayName(), duration_cast(end - begin).count()); + } + } + if(flag_update_spoken) + this->spoken_time_timestamp = system_clock::now(); + + END_TIMINGS(timing_client_tick); + } + + + { + BEGIN_TIMINGS(); + + unique_lock channel_lock(this->channel_tree_lock); + auto channels = this->channelTree->channels(); + channel_lock.unlock(); + + for(const auto& channel : this->channelTree->channels()){ + if(channel->channelType() == ChannelType::temporary) { + auto server_channel = dynamic_pointer_cast(channel); + assert(server_channel); + if(server_channel->client_count() > 0 || !this->getClientsByChannelRoot(channel, true).empty()) + continue; + + seconds deleteTimeout(0); + if(channel->properties().hasProperty(property::CHANNEL_DELETE_DELAY)) + deleteTimeout = seconds(channel->properties()[property::CHANNEL_DELETE_DELAY].as()); + + auto last_left = time_point() + milliseconds(channel->properties()[property::CHANNEL_LAST_LEFT].as()); + auto channel_created = channel->createdTimestamp(); + + if(system_clock::now() - last_left < deleteTimeout) continue; //One second stay + if(system_clock::now() - channel_created < deleteTimeout + seconds(1)) continue; //One second stay + + this->delete_channel(server_channel, this->serverRoot, "temporary autodelete", channel_lock); + if(channel_lock.owns_lock()) + channel_lock.unlock(); + } + { + auto permission_manager = channel->permissions(); + if(permission_manager->require_db_updates()) { + auto begin = system_clock::now(); + serverInstance->databaseHelper()->saveChannelPermissions(this->ref(), channel->channelId(), permission_manager); + auto end = system_clock::now(); + debugMessage(this->serverId, "Saved channel permissions for channel {} ({}) in {}ms", channel->channelId(), channel->name(), duration_cast(end - begin).count()); + } + } + } + + END_TIMINGS(timing_channel); + } + + { + BEGIN_TIMINGS(); + + this->serverStatistics->tick(); + + if(fileStatisticsTimestamp + seconds(5) < system_clock::now()) { + fileStatisticsTimestamp = system_clock::now(); + auto update = this->serverStatistics->mark_file_bytes(); + if(update.first > 0) { + this->properties()[property::VIRTUALSERVER_MONTH_BYTES_DOWNLOADED] += update.first; + this->properties()[property::VIRTUALSERVER_TOTAL_BYTES_DOWNLOADED] += update.first; + } + if(update.second > 0) { + this->properties()[property::VIRTUALSERVER_MONTH_BYTES_UPLOADED] += update.second; + this->properties()[property::VIRTUALSERVER_TOTAL_BYTES_UPLOADED] += update.second; + } + } + + { + lock_guard lock(this->join_attempts_lock); + if(system_clock::now() > this->join_last_decrease + seconds(5)) { + for(auto& elm : this->join_attempts) + if(elm.second > 0) elm.second--; + + auto copy = this->join_attempts; + for(const auto& elm : copy) + if(elm.second == 0){ + auto found = find(this->join_attempts.begin(), this->join_attempts.end(), elm); + if(found != this->join_attempts.end()) this->join_attempts.erase(found); + } + this->join_last_decrease = system_clock::now(); + } + } + END_TIMINGS(timing_statistic); + } + + { + BEGIN_TIMINGS(); + + auto groups = this->getGroupManager()->availableGroups(false); + for(auto& group : groups) { + auto permissions = group->permissions(); + if(permissions->require_db_updates()) { + auto begin = system_clock::now(); + serverInstance->databaseHelper()->saveGroupPermissions(this->ref(), group->groupId(), permissions); + auto end = system_clock::now(); + debugMessage(this->serverId, "Saved group permissions for group {} ({}) in {}ms", group->groupId(), group->name(), duration_cast(end - begin).count()); + } + } + + END_TIMINGS(timing_groups); + } + + if(system_clock::now() - lastTick > milliseconds(100)) { + //milliseconds timing_update_states, timing_client_tick, timing_channel, timing_statistic; + logError(this->serverId, "Server tick tooks to long ({}ms => Status updates: {}ms Client tick: {}ms, Channel tick: {}ms, Statistic tick: {}ms)", + duration_cast(system_clock::now() - lastTick).count(), + timing_update_states.count(), + timing_client_tick.count(), + timing_channel.count(), + timing_statistic.count() + ); + } + } catch (std::exception& ex) { + logCritical(this->serverId, "Failed to tick server! Got exception message: {}", ex.what()); + } +} \ No newline at end of file diff --git a/server/src/TSServer.cpp b/server/src/TSServer.cpp new file mode 100644 index 0000000..5330e3a --- /dev/null +++ b/server/src/TSServer.cpp @@ -0,0 +1,1172 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "weblist/WebListManager.h" +#include "client/voice/VoiceClient.h" +#include "client/InternalClient.h" +#include "client/music/MusicClient.h" +#include "music/MusicBotManager.h" +#include "server/VoiceServer.h" +#include "server/file/FileServer.h" +#include "server/QueryServer.h" +#include "InstanceHandler.h" +#include "Configuration.h" +#include "TSServer.h" +#include + +using namespace std; +using namespace std::chrono; +using namespace ts; +using namespace ts::server; +using namespace ts::protocol; +using namespace ts::buffer; + +#define ECC_TYPE_INDEX 5 + +#ifndef BUILD_VERSION +#define BUILD_VERSION "Unknown build" +#endif + +extern ts::server::InstanceHandler* serverInstance; +TSServer::TSServer(uint16_t serverId, sql::SqlManager* database) : serverId(serverId), sql(database) { + memtrack::allocated(this); +} + +bool TSServer::initialize(bool test_properties) { + assert(self.lock()); + + this->_properties = serverInstance->databaseHelper()->loadServerProperties(self.lock()); + this->_properties->registerNotifyHandler([&](Property& prop){ + if(prop.type() == property::VIRTUALSERVER_DISABLE_IP_SAVING) { + this->_disable_ip_saving = prop.as(); + return; + } + if(prop.type() == property::VIRTUALSERVER_CODEC_ENCRYPTION_MODE) { + this->_voice_encryption_mode = prop.as(); + return; + } + std::string sql; + if(prop.type() == property::VIRTUALSERVER_HOST) + sql = "UPDATE `servers` SET `host` = :value WHERE `serverId` = :sid"; + else if(prop.type() == property::VIRTUALSERVER_PORT) + sql = "UPDATE `servers` SET `port` = :value WHERE `serverId` = :sid"; + if(sql.empty()) return; + sql::command(this->sql, sql, variable{":sid", this->getServerId()}, variable{":value", prop.value()}) + .executeLater().waitAndGetLater(LOG_SQL_CMD, sql::result{1, "future failed"}); + }); + + this->properties()[property::VIRTUALSERVER_PLATFORM] = config::server::DefaultServerPlatform; + this->properties()[property::VIRTUALSERVER_VERSION] = config::server::DefaultServerVersion; + this->properties()[property::VIRTUALSERVER_ID] = serverId; + this->_disable_ip_saving = this->properties()[property::VIRTUALSERVER_DISABLE_IP_SAVING]; + + if(!properties()[property::VIRTUALSERVER_KEYPAIR].as().empty()){ + debugMessage(this->serverId, "Importing server keypair"); + this->_serverKey = new ecc_key; + auto bytes = base64::decode(properties()[property::VIRTUALSERVER_KEYPAIR].as()); + int err; + if((err = ecc_import(reinterpret_cast(bytes.data()), bytes.length(), this->_serverKey)) != CRYPT_OK){ + logError(this->getServerId(), "Cant import key. ({} => {})", err, error_to_string(err)); + logError(this->serverId, "Could not import server keypair! {} ({}). Generating new one!", err, error_to_string(err)); + delete this->_serverKey; + this->_serverKey = nullptr; + properties()[property::VIRTUALSERVER_KEYPAIR] = ""; + } + } + int err; + if(!_serverKey){ + debugMessage(this->serverId, "Generating new server keypair"); + this->_serverKey = new ecc_key; + prng_state state{}; + if((err = ecc_make_key_ex(&state, find_prng("sprng"), this->_serverKey, <c_ecc_sets[ECC_TYPE_INDEX])) != CRYPT_OK){ + logError(this->serverId, "Could not generate a server keypair! {} ({})", err, error_to_string(err)); + delete this->_serverKey; + this->_serverKey = nullptr; + return false; + } + + size_t bytesBufferLength = 1024; + char bytesBuffer[bytesBufferLength]; + if((err = ecc_export(reinterpret_cast(bytesBuffer), &bytesBufferLength, PK_PRIVATE, this->_serverKey)) != CRYPT_OK){ + logError(this->serverId, "Could not export the server keypair (private)! {} ({})", err, error_to_string(err)); + delete this->_serverKey; + this->_serverKey = nullptr; + return false; + } + properties()[property::VIRTUALSERVER_KEYPAIR] = base64_encode(bytesBuffer, bytesBufferLength); + this->properties()[property::VIRTUALSERVER_CREATED] = duration_cast(system_clock::now().time_since_epoch()).count(); + } + if(_serverKey){ + size_t bufferLength = 265; + char buffer[bufferLength]; + if((err = ecc_export(reinterpret_cast(buffer), &bufferLength, PK_PUBLIC, this->_serverKey)) != CRYPT_OK) + logError(this->serverId, "Could not generate server uid! (Could not export the server keypair (public)! {} ({}))", err, error_to_string(err)); + properties()[property::VIRTUALSERVER_UNIQUE_IDENTIFIER] = base64::encode(digest::sha1(base64::encode(buffer, bufferLength))); + } + + channelTree = new ServerChannelTree(self.lock(), this->sql); + channelTree->loadChannelsFromDatabase(); + + this->groups = new GroupManager(self.lock(), this->sql, serverInstance->getGroupManager()); + if(!this->groups->loadGroupFormDatabase()){ //TODO exception etc + logCritical(this->serverId, "Cant setup group manager!"); + return false; + } + + if(channelTree->channel_count() == 0){ + logMessage(this->serverId, "Creating new channel tree (Copy from server 0)"); + LOG_SQL_CMD(sql::command(this->getSql(), "INSERT INTO `channels` (`serverId`, `channelId`, `type`, `parentId`) SELECT :serverId AS `serverId`, `channelId`, `type`, `parentId` FROM `channels` WHERE `serverId` = 0", variable{":serverId", this->serverId}).execute()); + LOG_SQL_CMD(sql::command(this->getSql(), "INSERT INTO `properties` (`serverId`, `type`, `id`, `key`, `value`) SELECT :serverId AS `serverId`, `type`, `id`, `key`, `value` FROM `properties` WHERE `serverId` = 0 AND `type` = :type", + variable{":serverId", this->serverId}, variable{":type", property::PROP_TYPE_CHANNEL}).execute()); + LOG_SQL_CMD(sql::command(this->getSql(), "INSERT INTO `permissions` (`serverId`, `type`, `id`, `channelId`, `permId`, `value`, `grant`, `flag_skip`, `flag_negate`) " + "SELECT :serverId AS `serverId`, `type`, `id`, `channelId`, `permId`, `value`, `grant`, `flag_skip`, `flag_negate` FROM `permissions` WHERE `serverId` = 0 AND `type` = :type", + variable{":serverId", this->serverId}, variable{":type", permission::SQL_PERM_CHANNEL}).execute()); + + channelTree->loadChannelsFromDatabase(); + if(channelTree->channel_count() == 0){ + logCritical(this->serverId, "Failed to setup channel tree!"); + return 0; + } + if(!channelTree->getDefaultChannel()) { + logError(this->serverId, "Missing default channel! Using first one!"); + channelTree->setDefaultChannel(channelTree->channels().front()); + } + } + if(!channelTree->getDefaultChannel()) channelTree->setDefaultChannel(*channelTree->channels().begin()); + auto default_channel = channelTree->getDefaultChannel(); + assert(default_channel); + if(default_channel->properties()[property::CHANNEL_FLAG_PASSWORD].as()) + default_channel->properties()[property::CHANNEL_FLAG_PASSWORD] = false; + + this->tokenManager = new token::TokenManager(this); + this->tokenManager->loadTokens(); + + this->complains = new ComplainManager(this); + if(!this->complains->loadComplains()) logError(this->serverId, "Could not load complains"); + + //Setup new server if needed + if(this->groups->availableServerGroups(false).empty() || this->groups->availableChannelGroups(false).empty()){ + if(!this->properties()[property::VIRTUALSERVER_AUTOGENERATED_PRIVILEGEKEY].as().empty()) { + logCritical(this->getServerId(), "Missing default groups. Applying permission reset!"); + } + string token; + if(!this->resetPermissions(token)) + logCritical(this->serverId, "Failed to reset server permissions! This could be fatal!"); + logMessageFmt(true, this->serverId, "---------------------- Token ----------------------"); + logMessageFmt(true, this->serverId, "{:^51}", "The server's serveradmin token:"); + logMessageFmt(true, this->serverId, "{:^51}", token); + logMessageFmt(true, this->serverId, ""); + logMessageFmt(true, this->serverId, "{:^51}", "Note: This token could be used just once!"); + logMessageFmt(true, this->serverId, "---------------------- Token ----------------------"); + } + if(test_properties) + this->ensureValidDefaultGroups(); + + letters = new letter::LetterManager(this); + + serverStatistics = make_shared(serverInstance->getStatistics(), true); + + this->serverRoot = std::make_shared(this->sql, self.lock(), this->properties()[property::VIRTUALSERVER_NAME].as(), false); + static_pointer_cast(this->serverRoot)->setSharedLock(this->serverRoot); + this->properties().registerNotifyHandler([&](Property& property) { + if(property.type() == property::VIRTUALSERVER_NAME) static_pointer_cast(this->serverRoot)->properties()[property::CLIENT_NICKNAME] = property.as(); + }); + this->serverRoot->server = nullptr; + + this->serverAdmin = std::make_shared(this->sql, self.lock(), "serveradmin", true); + static_pointer_cast(this->serverAdmin)->setSharedLock(this->serverAdmin); + DatabaseHelper::assignDatabaseId(this->sql, this->serverId, this->serverAdmin); + this->serverAdmin->server = nullptr; + this->registerInternalClient(this->serverAdmin); /* lets assign server id 0 */ + + if(serverInstance->getFileServer()) + serverInstance->getFileServer()->setupServer(self.lock()); + + this->channelTree->printChannelTree([&](std::string msg){ debugMessage(this->serverId, msg); }); + this->musicManager = make_shared(self.lock()); + this->musicManager->_self = this->musicManager; + this->musicManager->load_playlists(); + this->musicManager->load_bots(); + + if(this->properties()[property::VIRTUALSERVER_ICON_ID] != (IconId) 0) + if(!serverInstance->getFileServer()->iconExists(self.lock(), this->properties()[property::VIRTUALSERVER_ICON_ID])) { + debugMessage(this->getServerId(), "Removing invalid icon id of server"); + this->properties()[property::VIRTUALSERVER_ICON_ID] = 0; + } + + for(const auto& type : vector{ + property::VIRTUALSERVER_DOWNLOAD_QUOTA, + property::VIRTUALSERVER_UPLOAD_QUOTA, + property::VIRTUALSERVER_MAX_UPLOAD_TOTAL_BANDWIDTH, + property::VIRTUALSERVER_MAX_DOWNLOAD_TOTAL_BANDWIDTH, + }) { + auto info = property::impl::info(type); + auto prop = this->properties()[type]; + if(prop.default_value() == prop.value()) continue; + if(!info->validate_input(this->properties()[type].value())) { + this->properties()[type] = info->default_value; + logMessage(this->getServerId(), "Server property " + info->name + " contains an invalid value! Resetting it."); + } + } + + if(this->properties()[property::VIRTUALSERVER_FILEBASE].value().empty()) { + this->properties()[property::VIRTUALSERVER_FILEBASE] = serverInstance->getFileServer()->server_file_base(self.lock()); + } + return true; +} + +TSServer::~TSServer() { + memtrack::freed(this); + delete this->tokenManager; + delete this->groups; + delete this->channelTree; + delete this->letters; + delete this->complains; + + if(this->_serverKey) ecc_free(this->_serverKey); + delete this->_serverKey; +} + +inline bool evaluateAddress4(const string &input, in_addr &address) { + if(input == "0.0.0.0") { + address.s_addr = INADDR_ANY; + return true; + }; + auto record = gethostbyname(input.c_str()); + if(!record) return false; + address.s_addr = ((in_addr*) record->h_addr)->s_addr; + return true; +} + +inline bool evaluateAddress6(const string &input, in6_addr &address) { + if(input == "::") { + address = IN6ADDR_ANY_INIT; + return true; + }; + auto record = gethostbyname2(input.c_str(), AF_INET6); + if(!record) return false; + address = *(in6_addr*) record->h_addr; + return true; +} + +inline string strip(std::string message) { + while(!message.empty()) { + if(message[0] == ' ') + message = message.substr(1); + else if(message[message.length() - 1] == ' ') + message = message.substr(0, message.length() - 1); + else break; + } + return message; +} + +inline vector split_hosts(const std::string& message, char delimiter) { + vector result; + size_t found, index = 0; + do { + found = message.find(delimiter, index); + result.push_back(strip(message.substr(index, found - index))); + index = found + 1; + } while(index != 0); + return result; +} + +bool TSServer::start(std::string& error) { + { + threads::Mutex lock(this->stateLock); + if(this->state != ServerState::OFFLINE){ + error = "Server isn't offline"; + return false; + } + this->state = ServerState::BOOTING; + } + this->serverRoot->server = self.lock(); + this->serverAdmin->server = self.lock(); + + { //Client delete after server stop/start + lock_guard lock(this->clients.lock); + + for(auto& client : this->clients.clients) { + if(!client) continue; + if(client->getType() == ClientType::CLIENT_WEB || client->getType() == ClientType::CLIENT_TEAMSPEAK) { + client.reset(); + } + } + } + + auto host = this->properties()[property::VIRTUALSERVER_HOST].as(); + if(config::binding::enforce_default_voice_host) + host = config::binding::DefaultVoiceHost; + + if(host.empty()){ + error = "invalid host (\"" + host + "\")"; + this->stop("failed to start"); + return false; + } + if(this->properties()[property::VIRTUALSERVER_PORT].as() <= 0){ + error = "invalid port"; + this->stop("failed to start"); + return false; + } + + deque> bindings; + for(const auto& address : split_hosts(host, ',')) { + auto entry = make_shared(); + if(net::is_ipv4(address)) { + sockaddr_in addr{}; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(this->properties()[property::VIRTUALSERVER_PORT].as()); + if(!evaluateAddress4(address, addr.sin_addr)) { + logError(this->serverId, "Fail to resolve v4 address info for \"{}\"", address); + continue; + } + + memcpy(&entry->address, &addr, sizeof(addr)); + } else if(net::is_ipv6(address)) { + sockaddr_in6 addr{}; + memset(&addr, 0, sizeof(addr)); + addr.sin6_family = AF_INET6; + addr.sin6_port = htons(this->properties()[property::VIRTUALSERVER_PORT].as()); + if(!evaluateAddress6(address, addr.sin6_addr)) { + logError(this->serverId, "Fail to resolve v6 address info for \"{}\"", address); + continue; + } + + memcpy(&entry->address, &addr, sizeof(addr)); + } else { + logError(this->serverId, "Failed to determinate address type for \"{}\"", address); + continue; + } + bindings.push_back(entry); + } + if(bindings.empty()) { + error = "failed to resole any host!"; + this->stop("failed to start"); + return false; + } + + //Setup voice server + udpVoiceServer = make_shared(self.lock()); + if(!udpVoiceServer->start(bindings, error)) { + error = "could not start voice server. Message: " + error; + this->stop("failed to start"); + return false; + } + + if(ts::config::web::activated && serverInstance->sslManager()->web_ssl_options()) { + string webHostname = this->properties()[property::VIRTUALSERVER_WEB_HOST]; + if(webHostname.empty()) webHostname = this->properties()[property::VIRTUALSERVER_HOST].as(); + + sockaddr_in webAddress{}; + memset(&webAddress, 0, sizeof(webAddress)); + webAddress.sin_family = AF_INET; + if(!evaluateAddress4(webHostname, webAddress.sin_addr)) { + error = "could not resolve host (WebServer)"; + this->stop("Could not start web server"); + return false; + } + auto port = this->properties()[property::VIRTUALSERVER_WEB_PORT].as(); + if(port == 0) port = this->properties()[property::VIRTUALSERVER_PORT].as(); + webAddress.sin_port = htons(port); + logMessage(this->serverId, "Starting web server on " + webHostname + ":" + to_string(port)); + startTimestamp = std::chrono::system_clock::now(); + +#ifdef COMPILE_WEB_CLIENT + webControlServer = new WebControlServer(self.lock()); + if(!webControlServer->start(&webAddress, error)) { + error = "could not start web server. Message: " + error; + this->stop("failed to start"); + return false; + } +#endif + } + + //Startup ticking + serverInstance->executeTick(this); + + if(this->properties()[property::VIRTUALSERVER_WEBLIST_ENABLED].as()) + serverInstance->getWebList()->enable_report(this->self.lock()); + + properties()[property::VIRTUALSERVER_CLIENTS_ONLINE] = 0; + properties()[property::VIRTUALSERVER_QUERYCLIENTS_ONLINE] = 0; + properties()[property::VIRTUALSERVER_CHANNELS_ONLINE] = 0; + properties()[property::VIRTUALSERVER_UPTIME] = 0; + this->startTimestamp = system_clock::now(); + + this->musicManager->cleanup_semi_bots(); + this->musicManager->connectBots(); + + { + threads::MutexLock lock(this->stateLock); + this->state = ServerState::ONLINE; + } + return true; +} + +std::string TSServer::publicServerKey() { + size_t keyBufferLength = 265; + char keyBuffer[keyBufferLength]; + if(ecc_export((unsigned char *) keyBuffer, &keyBufferLength, PK_PUBLIC, this->_serverKey) != CRYPT_OK) return ""; + return base64::encode(string(keyBuffer, keyBufferLength)); +} + +bool TSServer::running() { + return this->state == ServerState::BOOTING || this->state == ServerState::ONLINE; +} + +void TSServer::preStop(const std::string& reason) { + { + threads::MutexLock lock(this->stateLock); + if(!this->running() && this->state != ServerState::SUSPENDING) return; + this->state = ServerState::SUSPENDING; + } + + for(const auto& cl : this->getClients()) { + unique_lock channel_lock(cl->channel_lock); + if (cl->currentChannel) { + if(cl->getType() == CLIENT_TEAMSPEAK) + ((VoiceClient*) cl.get())->disconnect(VREASON_SERVER_SHUTDOWN, reason, nullptr, false); + else + cl->notifyClientLeftView(cl, nullptr, ViewReasonId::VREASON_SERVER_SHUTDOWN, reason, nullptr, false); + } + cl->visibleClients.clear(); + cl->mutedClients.clear(); + } +} + +void TSServer::stop(const std::string& reason) { + auto self_lock = this->self.lock(); + assert(self_lock); + { + threads::MutexLock lock(this->stateLock); + if(!this->running() && this->state != ServerState::SUSPENDING) return; + this->state = ServerState::SUSPENDING; + } + + this->preStop(reason); + + for(const auto& cl : this->getClients()) { //start disconnecting + if(cl->getType() == CLIENT_TEAMSPEAK || cl->getType() == CLIENT_TEASPEAK || cl->getType() == CLIENT_WEB) { + cl->closeConnection(chrono::system_clock::now() + chrono::seconds(1)); + } else if(cl->getType() == CLIENT_QUERY){ + threads::MutexLock lock(cl->command_lock); + cl->currentChannel = nullptr; + + continue; //We dont need to disconnect the query + cl->server = nullptr; + cl->loadDataForCurrentServer(); + } else if(cl->getType() == CLIENT_MUSIC) { + cl->properties()[property::CLIENT_LAST_CHANNEL] = cl->currentChannel ? cl->currentChannel->channelId() : 0; + cl->currentChannel = nullptr; + } else if(cl->getType() == CLIENT_INTERNAL) { + + } else { + logError("Got manager with unknown type: " + to_string(cl->getType())); + } + } + this->musicManager->disconnectBots(); + + serverInstance->cancelExecute(this); + + if(this->udpVoiceServer) this->udpVoiceServer->stop(); + this->udpVoiceServer = nullptr; + +#ifdef COMPILE_WEB_CLIENT + if(this->webControlServer) this->webControlServer->stop(); + delete this->webControlServer; + this->webControlServer = nullptr; +#endif + + { + auto list = serverInstance->getWebList(); + if(list) + list->disable_report(self_lock); + } + + if(this->groups) { + this->groups->clearCache(); + } + + properties()[property::VIRTUALSERVER_CLIENTS_ONLINE] = 0; + properties()[property::VIRTUALSERVER_QUERYCLIENTS_ONLINE] = 0; + properties()[property::VIRTUALSERVER_CHANNELS_ONLINE] = 0; + properties()[property::VIRTUALSERVER_UPTIME] = 0; + + { + threads::MutexLock lock(this->stateLock); + this->state = ServerState::OFFLINE; + } + this->serverRoot->server = nullptr; + this->serverAdmin->server = nullptr; +} + +size_t TSServer::onlineClients() { + size_t result = 0; + + lock_guard lock(this->clients.lock); + for(const auto &cl : this->clients.clients) { + if(!cl) + continue; + if(cl->getType() == CLIENT_TEAMSPEAK || cl->getType() == CLIENT_QUERY) + result++; + } + return result; +} + +OnlineClientReport TSServer::onlineStats() { + OnlineClientReport response{}; + + { + lock_guard lock(this->clients.lock); + for(const auto &cl : this->clients.clients) { + if(!cl) continue; + + switch (cl->getType()) { + case CLIENT_TEAMSPEAK: + response.clients_ts++; + break; + case CLIENT_WEB: + response.clients_web++; + break; + case CLIENT_QUERY: + response.queries++; + break; + case CLIENT_MUSIC: + response.bots++; + break; + default: + break; + } + } + } + + return response; +} + +std::shared_ptr TSServer::findClient(sockaddr_in *addr) { + lock_guard lock(this->clients.lock); + for(const auto& elm : this->clients.clients) { + if(elm && elm->isAddressV4()) + if(elm->getAddressV4()->sin_addr.s_addr == addr->sin_addr.s_addr) + if(elm->getAddressV4()->sin_port == addr->sin_port) + return elm; + } + return nullptr; +} + +std::shared_ptr TSServer::findClient(uint16_t client_id) { + lock_guard lock(this->clients.lock); + if(this->clients.clients.size() > client_id) + return this->clients.clients[client_id]; + else + return nullptr; +} + +deque> TSServer::findClientsByCldbId(uint64_t cldbId) { + deque> result; + + lock_guard lock(this->clients.lock); + for(const auto &client : this->clients.clients) { + if(!client) continue; + + if(client->getClientDatabaseId() == cldbId) + result.push_back(client); + } + return result; +} + +deque> TSServer::findClientsByUid(std::string uid) { + lock_guard lock(this->clients.lock); + + deque> result; + for(const auto &client : this->clients.clients) { + if(!client) continue; + + if(client->getUid() == uid) { + result.push_back(client); + } + } + return result; +} + +std::shared_ptr TSServer::findClient(std::string name, bool ignoreCase) { + if(ignoreCase) { + std::transform(name.begin(), name.end(), name.begin(), ::tolower); + } + + { + + lock_guard lock(this->clients.lock); + + for(const auto& client : this->clients.clients) { + if(!client) continue; + + string clName = client->getDisplayName(); + if(ignoreCase) { + std::transform(clName.begin(), clName.end(), clName.begin(), ::tolower); + } + + if(clName == name) + return client; + } + } + return nullptr; +} + +bool TSServer::forEachClient(std::function)> function) { + for(const auto& elm : this->getClients()) { + shared_lock close_lock(elm->finalDisconnectLock, try_to_lock_t{}); + if(close_lock.owns_lock()) //If not locked than client is on the way to disconnect + if(elm->state == ConnectionState::CONNECTED && elm->getType() != ClientType::CLIENT_INTERNAL) { + function(elm); + } + } + return true; +} + +std::vector> TSServer::getClients() { + vector> clients; + + { + lock_guard lock(this->clients.lock); + clients.reserve(this->clients.count); + + for(auto& client : this->clients.clients) { + if(!client) continue; + clients.push_back(client); + } + } + + return clients; +} + +deque> TSServer::getClientsByChannel(std::shared_ptr channel) { + assert(this); + + auto s_channel = dynamic_pointer_cast(channel); + if(!s_channel) return {}; /* what had we done wrong here... :D */ + + shared_lock client_lock(s_channel->client_lock); + auto weak_clients = s_channel->clients; + client_lock.unlock(); + + std::deque> result; + for(const auto& weak_client : weak_clients) { + auto client = weak_client.lock(); + if(!client) continue; + if(client->connectionState() != ConnectionState::CONNECTED) continue; + if(client->getChannel() != channel) continue; /* to be sure */ + + result.push_back(move(client)); + } + return result; +} + +deque> TSServer::getClientsByChannelRoot(const std::shared_ptr &root, bool lock) { + assert(this); + + shared_lock channel_lock(this->channel_tree_lock, defer_lock); + if(lock) + channel_lock.lock(); + + std::deque> result; + for(const auto& channel : this->channelTree->channels(root)) { + auto clients = this->getClientsByChannel(channel); + result.insert(result.end(), clients.begin(), clients.end()); + } + + return result; +} + +bool TSServer::notifyServerEdited(std::shared_ptr invoker, deque keys) { + if(!invoker) return false; + + Command cmd("notifyserveredited"); + + cmd["invokerid"] = invoker->getClientId(); + cmd["invokername"] = invoker->getDisplayName(); + cmd["invokeruid"] = invoker->getUid(); + cmd["reasonid"] = ViewReasonId::VREASON_EDITED; + for(const auto& key : keys) { + auto info = property::impl::info(key); + if(*info == property::VIRTUALSERVER_UNDEFINED) { + logError(this->getServerId(), "Tried to broadcast a server update with an unknown info: " + key); + continue; + } + cmd[key] = properties()[info].as(); + } + this->forEachClient([&cmd](shared_ptr client){ + client->sendCommand(cmd); + }); + return true; +} + +bool TSServer::notifyClientPropertyUpdates(std::shared_ptr client, const deque>& keys, bool selfNotify) { + if(keys.empty()) return false; + this->forEachClient([&](const shared_ptr& cl) { + shared_lock client_channel_lock(client->channel_lock); + if(cl->isClientVisible(client, false) || (cl == client && selfNotify)) + cl->notifyClientUpdated(client, keys, false); + }); + return true; +} + +void TSServer::broadcastMessage(std::shared_ptr invoker, std::string message) { + if(!invoker) { + logCritical(this->serverId, "Tried to broadcast with an invalid invoker!"); + return; + } + this->forEachClient([&](shared_ptr cl){ + cl->notifyTextMessage(ChatMessageMode::TEXTMODE_SERVER, invoker, 0, message); + }); +} + +std::vector> CalculateCache::getGroupAssignments(TSServer* server, ClientDbId cldbid, ClientType type) { + if(assignment_server_groups_set) return assignment_server_groups; + + assignment_server_groups = server->getGroupManager()->getServerGroups(cldbid, type); + assignment_server_groups_set = true; + return assignment_server_groups; +} + +std::shared_ptr CalculateCache::getChannelAssignment(TSServer* server, ClientDbId client_dbid, ChannelId channel_id) { + if(this->assignment_channel_group_set && this->assignment_channel_group_channel == channel_id) return this->assignment_channel_group; + + auto channel = this->getServerChannel(server, channel_id); + assignment_channel_group = channel ? server->getGroupManager()->getChannelGroup(client_dbid, channel, true) : nullptr; + assignment_channel_group_set = true; + assignment_channel_group_channel = channel_id; + return assignment_channel_group; +} + +std::shared_ptr CalculateCache::getServerChannel(ts::server::TSServer *server, ts::ChannelId channel_id) { + if(this->last_server_channel == channel_id) + return this->server_channel; + this->last_server_channel = channel_id; + this->server_channel = server && channel_id > 0 ? server->getChannelTree()->findChannel(channel_id) : nullptr; + return this->server_channel; +} + +ts_always_inline bool channel_ignore_permission(ts::permission::PermissionType type) { + return permission::i_icon_id == type; +} + +vector> TSServer::calculatePermissions2( + ClientDbId client_dbid, + const std::deque& permissions, + ClientType client_type, + ChannelId channel_id, + bool calculate_granted, + std::shared_ptr cache) { + if(permissions.empty()) return {}; + + vector> result; + result.reserve(permissions.size()); + + + if(!cache) { + cache = make_shared(); + } + + if(!cache->client_permissions) { + cache->client_permissions = serverInstance->databaseHelper()->loadClientPermissionManager(self.lock(), client_dbid); + } + + bool have_skip_permission = false; + int skip_permission_type = -1; /* -1 := unset | 0 := skip, not explicit | 1 := skip, explicit */ + + bool have_skip; + + /* + * server_group_data[0] := Server group id + * server_group_data[1] := Skip flag + * server_group_data[2] := Negate flag + * server_group_data[3] := Permission value + */ + typedef std::tuple GroupData; + bool server_group_data_initialized = false; + vector server_group_data; + GroupData* active_server_group; + + /* function to calculate skip permission */ + auto calculate_skip = [&]{ + skip_permission_type = 0; + /* test for skip permission within the client permission manager */ + { + auto skip_value = cache->client_permissions->permission_value_flagged(permission::b_client_skip_channelgroup_permissions); + if(skip_value.has_value) { + have_skip_permission = skip_value.value == 1; + skip_permission_type = 1; + logTrace(this->serverId, "[Permission] Found skip permission in client permissions. Value: {}", have_skip_permission); + } + } + /* test for skip permission within all server groups */ + if(skip_permission_type != 1) { + for(const auto& assignment : cache->getGroupAssignments(this, client_dbid, client_type)) { + auto group_permissions = assignment->group->permissions(); + auto flagged_value = group_permissions->permission_value_flagged(permission::b_client_skip_channelgroup_permissions); + if(flagged_value.has_value) { + have_skip_permission |= flagged_value.value == 1; + if(have_skip_permission) { + logTrace(this->serverId, "[Permission] Found skip permission in client server group. Group: {} ({}), Value: {}", assignment->group->groupId(), assignment->group->name(), have_skip_permission); + break; + } + } + } + } + }; + + auto initialize_group_data = [&](const permission::PermissionType& permission_type) { + server_group_data_initialized = true; + active_server_group = nullptr; + + auto groups = cache->getGroupAssignments(this, client_dbid, client_type); + server_group_data.resize(groups.size()); + auto it = server_group_data.begin(); + for(auto& group : groups) { + auto group_permissions = group->group->permissions(); + auto permission_flags = group_permissions->permission_flags(permission_type); + + auto flag_set = calculate_granted ? permission_flags.grant_set : permission_flags.value_set; + if(!flag_set) + continue; + + //TODO: Test if there is may a group channel permissions + auto value = group_permissions->permission_values(permission_type); + *it = std::make_tuple(group->group->groupId(), (bool) permission_flags.skip, (bool) permission_flags.negate, calculate_granted ? value.grant : value.value); + it++; + } + if(it == server_group_data.begin()) + return; /* no server group has that permission */ + + server_group_data.erase(it, server_group_data.end()); /* remove unneeded */ + + auto found_negate = false; + for(auto& group : server_group_data) { + if(std::get<2>(group)) { + found_negate = true; + break; + } + } + + if(found_negate) { + server_group_data.erase(find_if(server_group_data.begin(), server_group_data.end(), [](auto data) { return std::get<2>(data); }), server_group_data.end()); + logTrace(this->serverId, "[Permission] Found negate flag within server groups. Groups left: {}", server_group_data.size()); + sassert(!server_group_data.empty()); /* this should never happen! */ + permission::PermissionValue current_lowest = 0; + for(auto& group : server_group_data) { + if(!active_server_group || (std::get<3>(group) < current_lowest && std::get<3>(group) != -1)) { + current_lowest = std::get<3>(group); + active_server_group = &group; + } + } + } else { + permission::PermissionValue current_highest = 0; + for(auto& group : server_group_data) { + if(!active_server_group || (std::get<3>(group) > current_highest || std::get<3>(group) == -1)) { + current_highest = std::get<3>(group); + active_server_group = &group; + } + } + } + }; + + for(const auto& permission : permissions) { + if(permission == permission::b_client_skip_channelgroup_permissions) { + if(skip_permission_type == -1) /* initialize skip flag */ + calculate_skip(); + result.push_back({permission, {have_skip_permission, skip_permission_type == 1}}); + continue; + } + + server_group_data_initialized = false; /* reset all group data */ + auto client_permission_flags = cache->client_permissions->permission_flags(permission); + /* lets try to resolve the channel specific permission */ + if(channel_id > 0 && client_permission_flags.channel_specific) { + auto data = cache->client_permissions->channel_permission(permission, channel_id); + if(calculate_granted ? data.flags.grant_set : data.flags.value_set) { + result.push_back({permission, {calculate_granted ? data.values.grant : data.values.value, true}}); + logTrace(this->serverId, "[Permission] Calculation for client {} of permission {} returned {} (Client channel permission)", client_dbid, permission::resolvePermissionData(permission)->name, data.values.value); + continue; + } + } + + + have_skip = channel_id == 0; + if(!have_skip) { + /* look if somewhere is the skip permission flag set */ + if(skip_permission_type == -1) /* initialize skip flag */ + calculate_skip(); + have_skip = have_skip_permission; + } + if(!have_skip) { + /* okey we've no global skip. Then now lookup the groups and the client permissions */ + if(calculate_granted ? client_permission_flags.grant_set : client_permission_flags.value_set) { + /* okey the client has the permission, this counts */ + have_skip = client_permission_flags.skip; + } else { + if(!server_group_data_initialized) + initialize_group_data(permission); + if(active_server_group) + have_skip = std::get<1>(*active_server_group); + } + } + + if(!have_skip) { + /* lookup the channel group */ + { + auto channel_assignment = cache->getChannelAssignment(this, client_dbid, channel_id); + if(channel_assignment) { + auto group_permissions = channel_assignment->group->permissions(); + auto permission_flags = group_permissions->permission_flags(permission); + + auto flag_set = calculate_granted ? permission_flags.grant_set : permission_flags.value_set; + if(flag_set) { + auto value = group_permissions->permission_values(permission); + result.push_back({permission, {calculate_granted ? value.grant : value.value, true}}); + logTrace(this->serverId, "[Permission] Calculation for client {} of permission {} returned {} (Channel group permission)", client_dbid, permission::resolvePermissionData(permission)->name, calculate_granted ? value.grant : value.value); + continue; + } + } + } + + /* lookup the channel permissions. Whyever? */ + { + auto channel = cache->getServerChannel(this, channel_id); + if(channel) { + auto channel_permissions = channel->permissions(); + auto data = calculate_granted ? channel_permissions->permission_granted_flagged(permission) : channel_permissions->permission_value_flagged(permission); + if(data.has_value) { + result.push_back({permission, {data.value, true}}); + logTrace(this->serverId, "[Permission] Calculation for client {} of permission {} returned {} (Channel permission)", client_dbid, permission::resolvePermissionData(permission)->name, data.value); + continue; + } + } + } + } + + if(calculate_granted ? client_permission_flags.grant_set : client_permission_flags.value_set) { + auto client_value = cache->client_permissions->permission_values(permission); + result.push_back({permission, {calculate_granted ? client_value.grant : client_value.value, true}}); + logTrace(this->serverId, "[Permission] Calculation for client {} of permission {} returned {} (Client permission)", client_dbid, permission::resolvePermissionData(permission)->name, client_value.value); + continue; + } + + if(!server_group_data_initialized) + initialize_group_data(permission); + if(active_server_group) { + result.push_back({permission, {get<3>(*active_server_group), true}}); + logTrace(this->serverId, "[Permission] Calculation for client {} of permission {} returned {} (Server group permission of group {})", client_dbid, permission::resolvePermissionData(permission)->name, get<3>(*active_server_group), get<0>(*active_server_group)); + continue; + } + + logTrace(this->serverId, "[Permission] Calculation for client {} of permission {} returned in no permission.", client_dbid, permission::resolvePermissionData(permission)->name); + result.push_back({permission, {permNotGranted, false}}); + } + + return result; +} + +deque> TSServer::calculatePermissions( + permission::PermissionTestType test_type, + ClientDbId client_dbid, + const std::deque& permissions, + ClientType client_type, + const std::shared_ptr &channel, + std::shared_ptr cache) { + if(permissions.empty()) return {}; + + auto data = calculatePermissions2(client_dbid, permissions, client_type, channel ? channel->channelId() : 0, false, cache); + deque> result; + for(auto& entry : data) + result.emplace_back(entry.first, entry.second.has_value ? entry.second.value : permNotGranted); + return result; +} + +ts::permission::PermissionValue TSServer::calculatePermission(permission::PermissionTestType test, ClientDbId cldbid, permission::PermissionType permission, ClientType client_type, const std::shared_ptr& channel, std::shared_ptr cache) { + auto result = this->calculatePermissions(test, cldbid, {permission}, client_type, channel, cache); + if(result.empty()) return permNotGranted; + return result.front().second; +} + +ts::permission::PermissionValue TSServer::calculatePermissionGrant(permission::PermissionTestType test, ClientDbId cldbid, permission::PermissionType permission, ClientType client_type, const std::shared_ptr& channel) { + auto result = this->calculatePermissions2(cldbid, {permission}, client_type, channel ? channel->channelId() : 0, true, nullptr); + assert(!result.empty()); + return result[0].second.has_value ? result[0].second.value : permNotGranted; +} + +bool TSServer::verifyServerPassword(std::string password, bool hashed) { + if(!this->properties()[property::VIRTUALSERVER_FLAG_PASSWORD].as()) return true; + if(password.empty()) return false; + + if(!hashed){ + char buffer[SHA_DIGEST_LENGTH]; + SHA1(reinterpret_cast(password.data()), password.length(), reinterpret_cast(buffer)); + password = base64_encode(string(buffer, SHA_DIGEST_LENGTH)); + } + + return password == this->properties()[property::VIRTUALSERVER_PASSWORD].as(); +} + +float TSServer::averagePacketLoss() { + //TODO Average packet loss + return 0.f; +} + +float TSServer::averagePing() { + float count = 0; + float sum = 0; + + this->forEachClient([&count, &sum](shared_ptr client) { + if(client->getType() != ClientType::CLIENT_TEAMSPEAK) return; + count++; + sum += duration_cast(dynamic_pointer_cast(client)->calculatePing()).count(); + }); + + if(count == 0) return 0; + return sum / count; +} + +bool TSServer::resetPermissions(std::string& token) { + LOG_SQL_CMD(sql::command(this->sql, "DELETE FROM `permissions` WHERE `serverId` = :serverId", variable{":serverId", this->serverId}).execute()); + LOG_SQL_CMD(sql::command(this->sql, "DELETE FROM `assignedGroups` WHERE `serverId` = :serverId", variable{":serverId", this->serverId}).execute()); + LOG_SQL_CMD(sql::command(this->sql, "DELETE FROM `groups` WHERE `serverId` = :serverId", variable{":serverId", this->serverId}).execute()); + + + { + threads::MutexLock lock(this->getGroupManager()->cacheLock); + this->getGroupManager()->deleteAllGroups(); + deque> saved_groups; + for(const auto& group : serverInstance->getGroupManager()->availableGroups(false)){ + if(group->type() != GroupType::GROUP_TYPE_TEMPLATE) continue; + + debugMessage(this->serverId, "Copy default group {{Id: {}, Type: {}, Target: {}, Name: {}}} to server", group->groupId(), group->type(), group->target(), group->name()); + this->getGroupManager()->copyGroup(group, GroupType::GROUP_TYPE_NORMAL, group->name(), this->serverId); + } + } + + //Server admin + auto default_server_admin = serverInstance->getGroupManager()->findGroup(serverInstance->properties()[property::SERVERINSTANCE_TEMPLATE_SERVERADMIN_GROUP].as()); + auto default_server_music = serverInstance->getGroupManager()->findGroup(serverInstance->properties()[property::SERVERINSTANCE_TEMPLATE_MUSICDEFAULT_GROUP].as()); + auto default_server_guest = serverInstance->getGroupManager()->findGroup(serverInstance->properties()[property::SERVERINSTANCE_TEMPLATE_SERVERDEFAULT_GROUP].as()); + + auto default_channel_admin = serverInstance->getGroupManager()->findGroup(serverInstance->properties()[property::SERVERINSTANCE_TEMPLATE_CHANNELADMIN_GROUP].as()); + auto default_channel_guest = serverInstance->getGroupManager()->findGroup(serverInstance->properties()[property::SERVERINSTANCE_TEMPLATE_CHANNELDEFAULT_GROUP].as()); + + if(!default_server_guest) { + logCritical(0, "Missing default server guest template group!"); + assert(!serverInstance->getGroupManager()->availableChannelGroups().empty()); + + default_server_guest = serverInstance->getGroupManager()->availableServerGroups().front(); + logCritical(0, "Using group {} as default server guest group for server {}.", default_server_guest->name(), this->serverId); + } + if(!default_channel_admin) { + logCritical(0, "Missing default channel guest template group!"); + assert(!serverInstance->getGroupManager()->availableChannelGroups().empty()); + + default_channel_admin = serverInstance->getGroupManager()->availableChannelGroups().front(); + logCritical(0, "Using group {} as channel server guest group for server {}.", default_channel_admin->name(), this->serverId); + } + if(!default_server_music) { + logCritical(0, "Missing default channel guest template group!"); + assert(!serverInstance->getGroupManager()->availableChannelGroups().empty()); + + default_server_music = serverInstance->getGroupManager()->availableChannelGroups().front(); + logCritical(0, "Using group {} as channel server guest group for server {}.", default_server_music->name(), this->serverId); + } + + if(!default_server_admin) { + logCritical(0, "Missing default server admin template group! Using default guest group ({})", default_server_guest->name()); + default_server_admin = default_server_guest; + } + + if(!default_channel_admin) { + logCritical(0, "Missing default channel admin template group! Using default guest group ({})", default_channel_guest->name()); + default_channel_admin = default_channel_guest; + } + + this->properties()[property::VIRTUALSERVER_DEFAULT_SERVER_GROUP] = this->getGroupManager()->findGroup(GroupTarget::GROUPTARGET_SERVER, default_server_guest->name()).front()->groupId(); + this->properties()[property::VIRTUALSERVER_DEFAULT_MUSIC_GROUP] = this->getGroupManager()->findGroup(GroupTarget::GROUPTARGET_SERVER, default_server_music->name()).front()->groupId(); + this->properties()[property::VIRTUALSERVER_DEFAULT_CHANNEL_ADMIN_GROUP] = this->getGroupManager()->findGroup(GroupTarget::GROUPTARGET_CHANNEL, default_channel_admin->name()).front()->groupId(); + this->properties()[property::VIRTUALSERVER_DEFAULT_CHANNEL_GROUP] = this->getGroupManager()->findGroup(GroupTarget::GROUPTARGET_CHANNEL, default_channel_guest->name()).front()->groupId(); + + auto token_admin = this->getGroupManager()->findGroup(GroupTarget::GROUPTARGET_SERVER, default_server_admin->name()).front()->groupId(); + auto created = this->tokenManager->createToken(token::TOKEN_SERVER, token_admin, "Default server token for the server admin."); + if(!created) { + logCritical(this->serverId, "Failed to generate default serveradmin token!"); + } else { + token = created->token; + this->properties()[property::VIRTUALSERVER_AUTOGENERATED_PRIVILEGEKEY] = token; + this->properties()[property::VIRTUALSERVER_ASK_FOR_PRIVILEGEKEY] = true; + } + if(this->properties()[property::VIRTUALSERVER_ASK_FOR_PRIVILEGEKEY].as()) { + auto requested_token = this->tokenManager->findToken(this->properties()[property::VIRTUALSERVER_AUTOGENERATED_PRIVILEGEKEY]); + if(!requested_token) { + logError(this->serverId, "Failed to resolve default token! Don't ask for privilege key anymore."); + this->properties()[property::VIRTUALSERVER_ASK_FOR_PRIVILEGEKEY] = false; + this->properties()[property::VIRTUALSERVER_AUTOGENERATED_PRIVILEGEKEY] = ""; + } + } + this->ensureValidDefaultGroups(); + + for(const auto& client : this->getClients()) { + if(client->getType() != ClientType::CLIENT_QUERY) { + client->notifyServerGroupList(); + client->notifyChannelGroupList(); + } + if(this->notifyClientPropertyUpdates(client, this->getGroupManager()->update_server_group_property(client, true))) { + if(client->update_cached_permissions()) /* update cached calculated permissions */ + client->sendNeededPermissions(false); /* cached permissions had changed, notify the client */ + } + client->updateChannelClientProperties(true, true); + } + return true; +} + +void TSServer::ensureValidDefaultGroups() { + auto default_server_group = this->getGroupManager()->defaultGroup(GROUPTARGET_SERVER, true); + if(!default_server_group) { + logError(this->serverId, "Missing server's default server group! (Id: {})", this->properties()[property::VIRTUALSERVER_DEFAULT_SERVER_GROUP].value()); + + default_server_group = this->getGroupManager()->availableServerGroups(false).front(); + logError(this->serverId, "Using {} ({}) instead!", default_server_group->groupId(), default_server_group->name()); + this->properties()[property::VIRTUALSERVER_DEFAULT_SERVER_GROUP] = default_server_group->groupId(); + } + + auto default_music_group = this->getGroupManager()->defaultGroup(GROUPTARGET_SERVER, true); + if(!default_music_group) { + logError(this->serverId, "Missing server's default music group! (Id: {})", this->properties()[property::VIRTUALSERVER_DEFAULT_MUSIC_GROUP].value()); + + default_music_group = default_server_group; + logError(this->serverId, "Using {} ({}) instead!", default_music_group->groupId(), default_music_group->name()); + this->properties()[property::VIRTUALSERVER_DEFAULT_MUSIC_GROUP] = default_music_group->groupId(); + } + + auto default_channel_group = this->getGroupManager()->defaultGroup(GROUPTARGET_CHANNEL, true); + if(!default_channel_group) { + logError(this->serverId, "Missing server's default channel group! (Id: {})", this->properties()[property::VIRTUALSERVER_DEFAULT_CHANNEL_GROUP].value()); + + default_channel_group = this->getGroupManager()->availableChannelGroups(false).front(); + logError(this->serverId, "Using {} ({}) instead!", default_channel_group->groupId(), default_channel_group->name()); + this->properties()[property::VIRTUALSERVER_DEFAULT_CHANNEL_GROUP] = default_channel_group->groupId(); + } + + auto admin_channel_group = this->getGroupManager()->findGroupLocal(this->properties()[property::VIRTUALSERVER_DEFAULT_CHANNEL_ADMIN_GROUP].as_save()); + if(!admin_channel_group) { + logError(this->serverId, "Missing server's default channel admin group! (Id: {})", this->properties()[property::VIRTUALSERVER_DEFAULT_CHANNEL_ADMIN_GROUP].value()); + + admin_channel_group = this->getGroupManager()->availableChannelGroups(false).front(); + logError(this->serverId, "Using {} ({}) instead!", admin_channel_group->groupId(), admin_channel_group->name()); + this->properties()[property::VIRTUALSERVER_DEFAULT_CHANNEL_ADMIN_GROUP] = admin_channel_group->groupId(); + } +} \ No newline at end of file diff --git a/server/src/TSServer.h b/server/src/TSServer.h new file mode 100644 index 0000000..8fc43ba --- /dev/null +++ b/server/src/TSServer.h @@ -0,0 +1,333 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "Group.h" +#include "Properties.h" +#include "query/Command.h" +#include "channel/ServerChannel.h" +#include "manager/BanManager.h" +#include "Definitions.h" +#include "ConnectionStatistics.h" +#include "manager/TokeManager.h" +#include "manager/ComplainManager.h" +#include "DatabaseHelper.h" +#include "manager/LetterManager.h" +#include "Configuration.h" +#include "protocol/ringbuffer.h" + +#include +#undef byte + +#ifdef COMPILE_WEB_CLIENT + #include "server/WebServer.h" +#endif + +template +inline bool operator==(T* elm, const std::shared_ptr<_Tp>& __a) noexcept { return elm == __a.get(); } + +template +inline bool operator==(const std::shared_ptr<_Tp>& __a, T* elm) noexcept { return elm == __a.get(); } + +template +inline bool operator!=(T* elm, const std::shared_ptr<_Tp>& __a) noexcept { return elm != __a.get(); } + +template +inline bool operator!=(const std::shared_ptr<_Tp>& __a, T* elm) noexcept { return elm != __a.get(); } + +namespace ts { + class ServerChannelTree; + + namespace music { + class MusicBotManager; + } + + namespace server { + class ConnectedClient; + class VoiceClient; + class QueryClient; + class WebClient; + class InternalClient; + + class InstanceHandler; + class VoiceServer; + class QueryServer; + class FileServer; + class SpeakingClient; + + class WebControlServer; + + + struct ServerState { + enum value { + OFFLINE, + BOOTING, + ONLINE, + SUSPENDING, + DELETING + }; + + inline static std::string string(value state) { + switch (state) { + case ServerState::OFFLINE: + return "offline"; + case ServerState::BOOTING: + return "booting"; + case ServerState::ONLINE: + return "online"; + case ServerState::SUSPENDING: + return "suspending"; + case ServerState::DELETING: + return "deleting"; + default: + return "unknown"; + } + } + }; + + struct OnlineClientReport { + uint16_t clients_ts = 0; + uint16_t clients_web = 0; + uint16_t queries = 0; + uint16_t bots = 0; + }; + + struct CalculateCache { + bool global_skip = false; + bool global_skip_set = false; + + std::shared_ptr client_permissions; + std::vector> assignment_server_groups; + bool assignment_server_groups_set = false; + + ChannelId assignment_channel_group_channel; + std::shared_ptr assignment_channel_group; + bool assignment_channel_group_set = false; + + std::shared_ptr server_channel; + ChannelId last_server_channel = 0; + + inline std::vector> getGroupAssignments(TSServer* server, ClientDbId cldbid, ClientType type); + inline std::shared_ptr getChannelAssignment(TSServer* server, ClientDbId client_dbid, ChannelId channel); + inline std::shared_ptr getServerChannel(TSServer*, ChannelId); + }; + + class TSServer { + friend class WebClient; + friend class DataClient; + friend class VoiceClient; + friend class MusicClient; + friend class ConnectedClient; + friend class InternalClient; + friend class QueryServer; + friend class QueryClient; + friend class SpeakingClient; + friend class music::MusicBotManager; + friend class InstanceHandler; + friend class ServerManager; + public: + TSServer(ServerId serverId, sql::SqlManager*); + ~TSServer(); + + bool initialize(bool test_properties); + + bool start(std::string& error); + bool running(); + void preStop(const std::string&); + void stop(const std::string& reason = ts::config::messages::serverStopped); + + size_t onlineClients(); + OnlineClientReport onlineStats(); + size_t onlineChannels(){ return this->channelTree->channel_count(); } + std::shared_ptr findClient(sockaddr_in* addr); + std::shared_ptr findClient(ClientId clientId); + std::deque> findClientsByCldbId(ClientDbId cldbId); + std::deque> findClientsByUid(ClientUid uid); + std::shared_ptr findClient(std::string name, bool ignoreCase = true); + bool forEachClient(std::function)>); + //bool forEachClient(std::function>, bool executeLaterIfLocked = true); + + std::vector> getClients(); + std::deque> getClientsByChannel(std::shared_ptr); + std::deque> getClientsByChannelRoot(const std::shared_ptr &, bool lock_channel_tree); + + template + std::vector> getClientsByChannel(const std::shared_ptr& ch) { + std::vector> result; + for(const auto& cl : this->getClientsByChannel(ch)) + if(std::dynamic_pointer_cast(cl)) + result.push_back(std::dynamic_pointer_cast(cl)); + return result; + } + + ecc_key* serverKey(){ return _serverKey; } + std::string publicServerKey(); + + Properties& properties(){ return *this->_properties; } + + inline sql::SqlManager * getSql(){ return this->sql; } + sql::AsyncSqlPool* getSqlPool(){ return this->sql->pool; } + + inline ServerId getServerId(){ return this->serverId; } + inline ServerChannelTree* getChannelTree(){ return this->channelTree; } + inline GroupManager* getGroupManager() { return this->groups; } + + bool notifyServerEdited(std::shared_ptr, std::deque keys); + bool notifyClientPropertyUpdates(std::shared_ptr, const std::deque>& keys, bool selfNotify = true); /* execute only with at least channel tree read lock! */ + inline bool notifyClientPropertyUpdates(const std::shared_ptr& client, const std::deque& keys, bool selfNotify = true) { + if(keys.empty()) return false; + std::deque> _keys; + for(const auto& key : keys) _keys.push_back(property::impl::info(key)); + return this->notifyClientPropertyUpdates(client, _keys, selfNotify); + }; + + void broadcastMessage(std::shared_ptr, std::string message); + +#ifndef __deprecated + #define __deprecated __attribute__((deprecated)) +#endif + __deprecated void registerInternalClient(std::shared_ptr); + __deprecated void unregisterInternalClient(std::shared_ptr); + + std::shared_ptr getServerRoot(){ return this->serverRoot; } + + std::string getDisplayName(){ return properties()[property::VIRTUALSERVER_NAME]; } + + std::shared_ptr getServerStatistics(){ return serverStatistics; } + + std::shared_ptr getVoiceServer(){ return this->udpVoiceServer; } + WebControlServer* getWebServer(){ return this->webControlServer; } + + std::deque> calculatePermissions( + permission::PermissionTestType, + ClientDbId, + const std::deque&, + ClientType type, + const std::shared_ptr& channel, + std::shared_ptr cache = nullptr); + + std::vector> calculatePermissions2( + ClientDbId /* client db id */, + const std::deque& /* permissions to calculate */, + ClientType type /* client type for default permissions */, + ChannelId /* target channel id */, + bool /* calculate granted */, + std::shared_ptr cache = nullptr /* calculate cache */); + + permission::PermissionValue calculatePermission(permission::PermissionTestType, ClientDbId, permission::PermissionType, ClientType type, const std::shared_ptr& channel, std::shared_ptr cache = nullptr); + permission::PermissionValue calculatePermissionGrant(permission::PermissionTestType, ClientDbId, permission::PermissionType, ClientType type, const std::shared_ptr& channel); + + bool verifyServerPassword(std::string, bool hashed = false); + + void testBanStateChange(const std::shared_ptr& invoker); + + float averagePing(); + float averagePacketLoss(); + + bool resetPermissions(std::string&); + void ensureValidDefaultGroups(); + + ServerState::value getState() { return this->state; } + + bool could_default_create_channel(); + + inline std::shared_ptr ref() { return this->self.lock(); } + inline bool disable_ip_saving() { return this->_disable_ip_saving; } + inline std::chrono::system_clock::time_point start_timestamp() { return this->startTimestamp; }; + + /* Note: Use only this method to disconnect the client and notify everybody else that he has been banned! */ + void notify_client_ban(const std::shared_ptr& /* client */, const std::shared_ptr& /* invoker */, const std::string& /* reason */, size_t /* length */); + void notify_client_kick( + const std::shared_ptr& /* client */, + const std::shared_ptr& /* invoker */, + const std::string& /* reason */, + const std::shared_ptr& /* target channel */ + ); + + void client_move( + const std::shared_ptr& /* client */, + std::shared_ptr /* target channel */, + const std::shared_ptr& /* invoker */, + const std::string& /* reason */, + ViewReasonId /* reason id */, + bool /* notify the client */, + std::unique_lock& /* tree lock */ + ); + + void delete_channel( + std::shared_ptr /* target channel */, + const std::shared_ptr& /* invoker */, + const std::string& /* kick message */, + std::unique_lock& /* tree lock */ + ); + + inline int voice_encryption_mode() { return this->_voice_encryption_mode; } + protected: + bool registerClient(std::shared_ptr); + bool unregisterClient(std::shared_ptr, std::string, std::unique_lock& channel_tree_lock); + bool assignDefaultChannel(const std::shared_ptr&, bool join); + + private: + std::weak_ptr self; + + //Locks by tick, start and stop + threads::Mutex stateLock; + ServerState::value state = ServerState::OFFLINE; + std::chrono::system_clock::time_point lastTick; + void executeServerTick(); + + std::shared_ptr udpVoiceServer = nullptr; + WebControlServer* webControlServer = nullptr; + token::TokenManager* tokenManager = nullptr; + ComplainManager* complains = nullptr; + letter::LetterManager* letters = nullptr; + std::shared_ptr musicManager; + std::shared_ptr serverStatistics; + + sql::SqlManager* sql; + + uint16_t serverId = 1; + + std::chrono::system_clock::time_point startTimestamp; + std::chrono::system_clock::time_point fileStatisticsTimestamp; + + //The client list + struct { + size_t count = 0; + std::mutex lock; + std::vector> clients; + } clients; + + std::recursive_mutex client_nickname_lock; + + //General server properties + ecc_key* _serverKey = nullptr; + std::shared_ptr _properties; + int _voice_encryption_mode = 2; /* */ + + ServerChannelTree* channelTree = nullptr; + std::shared_mutex channel_tree_lock; /* lock if access channel tree! */ + + GroupManager* groups = nullptr; + + std::shared_ptr serverRoot = nullptr; + std::shared_ptr serverAdmin = nullptr; + + threads::Mutex join_attempts_lock; + std::map join_attempts; + threads::Mutex join_lock; + std::chrono::system_clock::time_point join_last_decrease; + + std::chrono::milliseconds spoken_time{0}; + std::chrono::system_clock::time_point spoken_time_timestamp; + + bool _disable_ip_saving = false; + }; + } +} \ No newline at end of file diff --git a/server/src/build.cpp b/server/src/build.cpp new file mode 100644 index 0000000..8e63faa --- /dev/null +++ b/server/src/build.cpp @@ -0,0 +1,72 @@ +#include +#include +#include "build.h" + +#ifndef BUILD_MAJOR + #define BUILD_MAJOR 0 +#endif + +#ifndef BUILD_MINOR + #define BUILD_MINOR 0 +#endif + +#ifndef BUILD_PATCH + #define BUILD_PATCH 0 +#endif + +#ifndef BUILD_TYPE + #define BUILD_TYPE build::BuildType::PRIVATE +#endif + +#ifndef BUILD_DATA + #define BUILD_DATA "Unknown build" +#endif + +#ifndef BUILD_COUNT + #define BUILD_COUNT 0 +#endif + +#define STR1(x) #x +#define STR2(x) STR1(x) + +using namespace build; +using namespace std; +using namespace std::chrono; + +unique_ptr local_version{[]() -> Version * { + const char *build_timestamp = __TIME__; //23:59:01 + const char *build_date = __DATE__; //Feb 12 1996 + + cout << "Time " << build_timestamp << " date " << build_date << endl; + tm timestamp{}, date{}; + if (!strptime(build_timestamp, "%H:%M:%S", ×tamp)) cerr << "Could not parse build timestamp!" << endl; + if (!strptime(build_date, "%b %d %Y", &date)) cerr << "Could not parse build date!" << endl; + + system_clock::time_point time = + system_clock::time_point() + seconds(mktime(&date)) + hours(timestamp.tm_hour) + minutes(timestamp.tm_min) + + seconds(timestamp.tm_sec); + return new Version{BUILD_MAJOR, BUILD_MINOR, BUILD_PATCH, STR2(BUILD_DATA), time}; +}()}; + +namespace build { + const std::unique_ptr& version() { + return local_version; + } + + std::string Version::string(bool timestamp) { + stringstream ss; + ss << this->major << "." << this->minor << "." << this->patch << this->additional; + if(timestamp) ss << " [Build: " << duration_cast(this->timestamp.time_since_epoch()).count() << "]"; + return ss.str(); + } + + BuildType type() { return static_cast(BUILD_TYPE); } + + std::string additionalData(){ return STR2(BUILD_DATA); } + + int buildCount(){ return BUILD_COUNT; } + + std::string pattern(){ + return R"([0-9]{1,5}\.[0-9]{1,5}\.[0-9]{1,5}(\-.*)?)"; + } +} \ No newline at end of file diff --git a/server/src/build.h b/server/src/build.h new file mode 100644 index 0000000..f63e832 --- /dev/null +++ b/server/src/build.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include + +namespace build { + enum BuildType { + STABLE, + BETA, + ALPHA, + PRIVATE + }; + + struct Version { + int major; + int minor; + int patch; + std::string additional; + + std::chrono::system_clock::time_point timestamp; + + inline bool valid() const { return timestamp.time_since_epoch().count() > 0; } + + inline bool operator>(const Version &other) const { + if (other.major < this->major) return true; + else if (other.major > this->major) return false; + + if (other.minor < this->minor) return true; + else if (other.minor > this->minor) return false; + + if (other.patch < this->patch) return true; + else if (other.patch > this->patch) return false; + return false; + } + + inline bool operator==(const Version &other) const { + return this->major == other.major && this->minor == other.minor && this->patch == other.patch; + } + + inline bool operator<(const Version &other) const { return other.operator>(*this); } + + inline bool operator>=(const Version &other) const { return this->operator>(other) || this->operator==(other); } + + inline bool operator<=(const Version &other) const { return this->operator<(other) || this->operator==(other); } + + Version(int major, int minor, int patch, std::string additional, + const std::chrono::system_clock::time_point ×tamp) : patch(patch), + additional(std::move(additional)), + timestamp(timestamp) { + this->major = major; + this->minor = minor; + } + + std::string string(bool timestamp = true); + }; + + extern const std::unique_ptr& version(); + extern BuildType type(); + extern int buildCount(); + + extern std::string pattern(); +} \ No newline at end of file diff --git a/server/src/channel/ClientChannelView.cpp b/server/src/channel/ClientChannelView.cpp new file mode 100644 index 0000000..fbb8ca1 --- /dev/null +++ b/server/src/channel/ClientChannelView.cpp @@ -0,0 +1,439 @@ +#include +#include +#include +#include "src/client/ConnectedClient.h" +#include "ClientChannelView.h" + +using namespace std; +using namespace ts; +using namespace ts::server; + +ViewEntry::ViewEntry(const std::shared_ptr &handle, bool editable) : handle(handle), editable(editable) { + memtrack::allocated(this); + assert(handle); + + this->view_timestamp = chrono::system_clock::now(); + this->previous_channel = handle->channelOrder(); + this->cached_channel_id = handle->channelId(); + this->cached_parent_id = handle->hasParent() ? handle->parent()->channelId() : 0; +} + +ViewEntry::~ViewEntry() { + memtrack::freed(this); +} + +ChannelId ts::ViewEntry::channelId() const { + if(this->cached_channel_id != 0) return this->cached_channel_id; //We've already got a channel id + + //TODO cached_channel_id should be > 0 every time? + auto channel = this->handle.lock(); + if(!channel) { + logCritical(LOG_GENERAL, "ViewEntry::channelId() called without a valid handle and cached_channel_id == 0!"); + return 0; + } + return channel->channelId(); +} + +ChannelId ViewEntry::parentId() const { + if(this->cached_parent_id != 0) return this->cached_parent_id; + + auto channel = this->handle.lock(); + return channel && channel->parent() ? channel->parent()->channelId() : 0; +} + +ChannelId ts::ViewEntry::previousChannelId() const { + return previous_channel; +} + +void ts::ViewEntry::setPreviousChannelId(ts::ChannelId id) { + assert(this->editable); + this->previous_channel = id; +} + +void ts::ViewEntry::setParentChannelId(ts::ChannelId id) { + assert(this->editable); + this->cached_parent_id = id; +} + +ClientChannelView::ClientChannelView(server::ConnectedClient* handle) : owner(handle) { + memtrack::allocated(this); +} +ClientChannelView::~ClientChannelView() { + memtrack::freed(this); +} + +ServerId ClientChannelView::getServerId() { + return owner ? owner->getServerId() : 0; +} + +std::deque> ClientChannelView::channels(const std::shared_ptr &head, + int deep) { + std::deque> result; + for(const auto& entry : this->entries(head ? make_shared(head) : nullptr, deep)) { + auto channel = dynamic_pointer_cast(entry)->handle.lock(); + if(channel) + result.push_back(channel); + } + + return result; +} + +bool ClientChannelView::channel_visible(const std::shared_ptr &channel, + const std::shared_ptr &head, + int deep) { + if(!channel) return true; //I thing the void is kind of visible :D + return this->has_entry(make_shared(channel), head ? make_shared(head) : nullptr, deep); +} + +std::shared_ptr ClientChannelView::find_channel(ts::ChannelId id) { + return dynamic_pointer_cast(this->find_entry(id)); +} + +std::shared_ptr ClientChannelView::find_channel(const std::shared_ptr &channel) { + if(!channel) return nullptr; + + deque> heads{channel}; + shared_ptr head = nullptr; + bool deep_search = false; + + while(heads.front()) { + auto parent = heads.front()->parent(); + if(!parent && heads.front()->properties()[property::CHANNEL_PID].as() != 0) { + head = this->find_linked_entry(channel->channelId(), nullptr);//We're searching for a deleted head! So lets iterate over everything + deep_search = true; + break; + } + heads.push_front(parent); + } + + if(!deep_search) { + heads.pop_front(); + + while(heads.size() > 1) { + auto front = move(heads.front()); + heads.pop_front(); + + head = this->find_linked_entry(front->channelId(), head, 1); + if(!head) return nullptr; //Channel tree not visible! + } + + head = this->find_linked_entry(channel->channelId(), head, 1); + } + + return head ? static_pointer_cast(head->entry) : nullptr; +} + +std::deque> ClientChannelView::insert_channels(shared_ptr head, bool test_permissions, bool first_only, std::shared_ptr cache) { + std::deque> result; + + if(!cache && test_permissions) cache = make_shared(); + bool has_perm = !test_permissions || owner->permissionGranted(permission::PERMTEST_ORDERED, permission::b_channel_ignore_view_power, 1, nullptr, true, cache); + bool first = true; + while(head) { + if(!first && first_only) break; + first = false; + + auto channel = dynamic_pointer_cast(head->entry); + if(this->channel_visible(channel)) { + if(head->child_head) { + for(const auto& sub : this->insert_channels(head->child_head, test_permissions, false, cache)) + result.push_back(sub); + } + + head = head->next; + continue; + } + + if(!has_perm) { + if(!channel->permission_granted(permission::i_channel_needed_view_power, this->owner->calculate_permission_value(permission::i_channel_view_power, channel->channelId()), false)) { + head = head->next; + debugMessage(this->getServerId(), "{}[CHANNEL] Dropping channel {} ({}) (No permissions)", CLIENT_STR_LOG_PREFIX_(this->owner), channel->channelId(), channel->name()); + continue; + } + } + auto entry = make_shared(dynamic_pointer_cast(head->entry), true); + std::shared_ptr parent, previous; + + if(head->parent.lock()) { + auto remote_parent = head->parent.lock(); + parent = dynamic_pointer_cast(this->find_entry(remote_parent->entry->channelId())); + sassert(parent); + } + + auto remote_previous = head->previous; //Get our first thing + while(remote_previous && !(previous = dynamic_pointer_cast(this->find_entry(remote_previous->entry->channelId())))) { + remote_previous = remote_previous->previous; + } + + auto previous_channel = previous ? previous->channel() : nullptr; + if(!this->insert_entry(entry, parent, previous)) { + logError(this->getServerId(), "Failed to insert channel into client view!"); + head = head->next; + continue; + }; + debugMessage(this->getServerId(), "{}[CHANNELS] Insert channel {} ({} => order {}) after {} ({})", + CLIENT_STR_LOG_PREFIX_(this->owner), + channel->channelId(), channel->name(), entry->previousChannelId(), + previous ? previous->channelId() : 0, previous_channel ? previous_channel->name() : "" + ); + + result.push_back(entry); + + if(head->child_head) { + for(const auto& sub : this->insert_channels(head->child_head, test_permissions, false, cache)) + result.push_back(sub); + } + head = head->next; + } + + return result; +} + +std::deque> ClientChannelView::show_channel(std::shared_ptr l_channel, bool& success) { + success = true; + if(this->channel_visible(dynamic_pointer_cast(l_channel->entry))) return {}; + + std::deque> result; + deque> parents = {l_channel}; + while(parents.front()) { + auto parent = parents.front()->parent.lock(); + if(parent && !this->channel_visible(dynamic_pointer_cast(parent->entry))) { + parents.push_front(parent); + } else { + parents.push_front(nullptr); + break; + } + } + parents.pop_front(); + + for(const auto& root_channel : parents) { + auto channel = dynamic_pointer_cast(root_channel->entry); + auto entry = make_shared(channel, true); + std::shared_ptr parent, previous; + + if(root_channel->parent.lock()) { + auto remote_parent = root_channel->parent.lock(); + parent = dynamic_pointer_cast(this->find_entry(remote_parent->entry->channelId())); + sassert(parent); + } + + auto remote_previous = root_channel->previous; //Get our first thing + while(remote_previous && !(previous = dynamic_pointer_cast(this->find_entry(remote_previous->entry->channelId())))) { + remote_previous = remote_previous->previous; + } + auto previous_channel = previous ? previous->channel() : nullptr; //weak could be may nullptr + debugMessage(this->getServerId(), "{}[CHANNELS] Insert channel {} ({}) after {} ({})", + CLIENT_STR_LOG_PREFIX_(this->owner), + channel->channelId(), channel->name(), + previous ? previous->channelId() : 0, previous_channel ? previous_channel->name() : "" + ); + + if(!this->insert_entry(entry, parent, previous)) { + logError(this->getServerId(), "Failed to insert channel into client view! (Aborting root)"); + success = false; + break; + }; + + result.push_back(entry); + } + + return result; +} + +std::deque> ClientChannelView::test_channel(std::shared_ptr l_old, + std::shared_ptr channel_new, shared_ptr cache) { + if(!cache) cache = make_shared(); + + std::deque> result; + bool has_perm = owner->permissionGranted(permission::PERMTEST_ORDERED, permission::b_channel_ignore_view_power, 1, nullptr, true, cache); + if(has_perm) return {}; + + deque> parents = {l_old}; + while(parents.front()) { + auto parent = parents.front(); + auto current = channel_new; + while(current && current != parent) current = current->parent.lock(); + if(current == parent) { + //parents.push_front(nullptr); + break; + } + parents.push_front(parent->parent.lock()); + } + parents.pop_front(); + + for(const auto& l_entry : parents) { + auto entry = this->find_entry(l_entry->entry->channelId()); + if(!entry) break; //Already cut out! + auto channel = dynamic_pointer_cast(l_entry->entry); + sassert(entry->channelId() == channel->channelId()); + + if(!channel->permission_granted(permission::i_channel_needed_view_power, this->owner->calculate_permission_value(permission::i_channel_view_power, channel->channelId()), false)) { + + for(const auto& te : this->delete_entry(entry)) + result.push_back(dynamic_pointer_cast(te)); + + debugMessage(this->getServerId(), "{}[CHANNEL] Moving channel tree out of view. Root: {} ({}) (No permissions)", CLIENT_STR_LOG_PREFIX_(this->owner), channel->channelId(), channel->name()); + break; + } + } + + return result; +} + +std::deque>> ClientChannelView::update_channel( + std::shared_ptr l_channel, std::shared_ptr l_own, shared_ptr cache) { + return update_channel_path(std::move(l_channel), std::move(l_own), std::move(cache), 1); +} + +std::deque>> ClientChannelView::update_channel_path(std::shared_ptr l_channel, std::shared_ptr l_own, shared_ptr cache, ssize_t length) { + if(!cache) cache = make_shared(); + std::deque>> result; + bool has_perm = owner->permissionGranted(permission::PERMTEST_ORDERED, permission::b_channel_ignore_view_power, 1, nullptr, true, cache); + + while(l_channel && length-- != 0) { + auto b_channel = dynamic_pointer_cast(l_channel->entry); + sassert(b_channel); + auto visible = this->channel_visible(b_channel); + if(!visible) { + if(l_channel->parent.lock() && !this->channel_visible(dynamic_pointer_cast(l_channel->parent.lock()->entry))) { + l_channel = l_channel->next; + continue; /* all subchannels had been checked, because parent isnt visible */ + } + //Test if channel comes visible again! + visible = true; + + if(!has_perm) { + if(!b_channel->permission_granted(permission::i_channel_needed_view_power, this->owner->calculate_permission_value(permission::i_channel_view_power, b_channel->channelId()), false)) { + visible = false; + } + } + if(visible) { + for(const auto& entry : this->show_channel(l_channel, visible)) + result.emplace_back(true, entry); + for(const auto& entry : this->insert_channels(l_channel->child_head, true, false, cache)) + result.emplace_back(true, entry); + } + + l_channel = l_channel->next; + continue; /* all subchannels had been checked */ + } else if(visible && !has_perm) { + //Test if tree comes invisible + for(const auto& entry : this->test_channel(l_channel, l_own, cache)) + result.emplace_back(false, entry); + } + + //Root node is okey, test children + if(l_channel->child_head) { + auto entries = this->update_channel_path(l_channel->child_head, l_own, cache, -1); + result.insert(result.end(), entries.begin(), entries.end()); + } + + + l_channel = l_channel->next; + } + + return result; +} + +std::deque>> ClientChannelView::change_order(const shared_ptr &channel, const std::shared_ptr &parent, const shared_ptr &head) { + std::deque>> result; + auto l_entry = this->find_linked_entry(channel->entry->channelId()); + auto l_parent = parent ? this->find_linked_entry(parent->entry->channelId()) : nullptr; + if(!l_entry) { //Channel not visible yet + if(!l_parent && parent) return {}; //The invisible channel was moved into an invisible tree + + bool has_perm = owner->permissionGranted(permission::PERMTEST_ORDERED, permission::b_channel_ignore_view_power, 1, nullptr); + if(!has_perm) { + has_perm = dynamic_pointer_cast(channel->entry)->permission_granted(permission::i_channel_needed_view_power, this->owner->calculate_permission_value(permission::i_channel_view_power, dynamic_pointer_cast(channel->entry)->channelId()), false); + } + if(!has_perm) return {}; //Channel wasn't visible and he still has no permission for that :) + + std::shared_ptr previous; + auto remote_previous = head; //Get our first thing + while(remote_previous && !(previous = dynamic_pointer_cast(this->find_entry(remote_previous->entry->channelId())))) { + remote_previous = remote_previous->previous; + } + + for(const auto& shown : this->insert_channels(channel, true, true)) + result.push_back({ClientChannelView::ENTER_VIEW, shown}); + return result; //An invisible channel became visible + } + //Channel visible! + + if(!l_parent && parent) { //Channel was visible and moved to invisible tree + auto remove = this->delete_entry(l_entry->entry); + for(const auto& entry : remove) + result.push_back({ClientChannelView::DELETE_VIEW, dynamic_pointer_cast(entry)}); + return result; + } + + //We have just to readjust the order or the parent + std::shared_ptr previous; + auto remote_previous = head; //Get our first thing + while(remote_previous && !(previous = dynamic_pointer_cast(this->find_entry(remote_previous->entry->channelId())))) { + remote_previous = remote_previous->previous; + } + + auto parent_switch = l_parent != l_entry->parent.lock(); + if(!this->move_entry(l_entry->entry, l_parent ? l_parent->entry : nullptr, previous)) return {}; + + return {{parent_switch ? ClientChannelView::MOVE : ClientChannelView::REORDER, dynamic_pointer_cast(l_entry->entry)}}; +} + +std::shared_ptr ClientChannelView::add_channel(const std::shared_ptr& l_channel) { + auto l_parent_channel = l_channel->parent.lock(); + auto parent_channel = l_parent_channel ? this->find_linked_entry(l_parent_channel->entry->channelId()) : nullptr; + if(!parent_channel && l_parent_channel) return nullptr; //Tree not visible! + + shared_ptr previous; + auto remote_previous = l_channel->previous; //Get our first thing + while(remote_previous && !(previous = dynamic_pointer_cast(this->find_entry(remote_previous->entry->channelId())))) { + remote_previous = remote_previous->previous; + } + + auto entry = make_shared(dynamic_pointer_cast(l_channel->entry), true); + if(!this->insert_entry(entry, parent_channel ? parent_channel->entry : nullptr, previous)) return nullptr; + return entry; +} + +bool ClientChannelView::remove_channel(ts::ChannelId channelId) { + auto entry = this->find_entry(channelId); + if(!entry) + return false; + + auto result = this->delete_entry(entry); + if(result.size() != 1) { + logError(this->owner->getServerId(), "ClientChannelView::remove_channel(...) returned more than one channel! ({})", result.size()); + for(const auto& entry : result) + debugMessage(this->owner->getServerId(), " - {}", entry->channelId()); + } + return true; +} + +std::deque ClientChannelView::delete_channel_root(const std::shared_ptr &channel){ + auto linked = this->find_channel(channel); + if(!linked) return {}; + + std::deque result; + for(const auto& channel : this->delete_entry(linked)) + result.push_back(channel->channelId()); + return result; +} + +void ClientChannelView::reset() { + while(this->head) + this->delete_entry(this->head->entry); +} + +void ClientChannelView::print() { + debugMessage(this->owner->getServerId(), "{}'s channel tree: ", this->owner->getDisplayName()); + this->print_tree([&](const std::shared_ptr& entry, int deep) { + string prefix; + while(deep > 0) { + prefix += " "; + deep--; + } + + debugMessage(this->owner->getServerId(), "{} - {} ({})", prefix, entry->channelId(), entry->previousChannelId()); + }); +} diff --git a/server/src/channel/ClientChannelView.h b/server/src/channel/ClientChannelView.h new file mode 100644 index 0000000..fdc0f84 --- /dev/null +++ b/server/src/channel/ClientChannelView.h @@ -0,0 +1,111 @@ +#pragma once + +#include +#include + +namespace ts { + namespace server { + class ConnectedClient; + struct CalculateCache; + } + + struct ViewEntry : public TreeEntry { + public: + ViewEntry(const std::shared_ptr&, bool /* editable */ = false); + ~ViewEntry(); + + inline std::shared_ptr channel() { return this->handle.lock(); } + ChannelId channelId() const override; + ChannelId parentId() const; + ChannelId previousChannelId() const override; + void setPreviousChannelId(ChannelId id) override; + void setParentChannelId(ChannelId id) override; + + bool subscribed = false; + bool editable = false; + bool _deleted = false; + ChannelId previous_channel = 0; + std::weak_ptr handle; + std::chrono::system_clock::time_point view_timestamp; + private: + ChannelId cached_channel_id = 0; + ChannelId cached_parent_id = 0; + }; + + typedef std::shared_lock TreeLock; + class ClientChannelView : private TreeView { + public: + enum ChannelAction { + NOTHING, + ENTER_VIEW, + DELETE_VIEW, + REORDER, + MOVE + }; + + explicit ClientChannelView(server::ConnectedClient*); + ~ClientChannelView() override; + + inline size_t count_channels() const { return this->entry_count(); } + std::deque> channels(const std::shared_ptr& /* head */ = nullptr, int deep = -1); + bool channel_visible(const std::shared_ptr& /* entry */, const std::shared_ptr& /* head */ = nullptr, int deep = -1); + std::shared_ptr find_channel(ChannelId /* channel id */); + std::shared_ptr find_channel(const std::shared_ptr& /* channel */); + + /* add channel tree with siblings */ + std::deque> insert_channels( + std::shared_ptr /* head */, + bool test_permissions, + bool first_only, + std::shared_ptr cache = nullptr + ); + + /* shows the specific channel and their parents */ + std::deque> show_channel( + std::shared_ptr /* channel */, + bool& /* success */ + ); + + /* remove invalid channel */ + std::deque> test_channel( + std::shared_ptr /* old channel */, + std::shared_ptr /* new channel */, + std::shared_ptr cache = nullptr + ); + + /* [{ add := true | delete := false, channel}] */ + std::deque>> update_channel( + std::shared_ptr /* channel */, + std::shared_ptr /* own channel */, + std::shared_ptr cache = nullptr + ); + + /* [{ add := true | delete := false, channel}] */ + std::deque>> update_channel_path( + std::shared_ptr /* channel */, + std::shared_ptr /* own channel */, + std::shared_ptr cache = nullptr, + ssize_t length = -1 + ); + + /* triggered on channel create */ + std::shared_ptr add_channel(const std::shared_ptr& /* channel */); + /* triggered on channel delete */ + bool remove_channel(ChannelId channelId); + + + std::deque>> change_order( + const std::shared_ptr &/* channel */, + const std::shared_ptr /* parent */&, + const std::shared_ptr /* previous */& + ); + + std::deque delete_channel_root(const std::shared_ptr& /* channel */); + + void print(); + void reset(); + private: + ServerId getServerId(); + server::ConnectedClient* owner; + }; +} \ No newline at end of file diff --git a/server/src/channel/ServerChannel.cpp b/server/src/channel/ServerChannel.cpp new file mode 100644 index 0000000..5ee17a4 --- /dev/null +++ b/server/src/channel/ServerChannel.cpp @@ -0,0 +1,552 @@ +#include +#include +#include +#include +#include "misc/rnd.h" +#include "src/TSServer.h" +#include "src/client/ConnectedClient.h" +#include "src/server/file/FileServer.h" +#include "src/InstanceHandler.h" + +using namespace std; +using namespace ts; +using namespace ts::server; + +extern InstanceHandler* serverInstance; + +ServerChannel::ServerChannel(ChannelId parentId, ChannelId channelId) : BasicChannel(parentId, channelId) { + memtrack::allocated(this); +} + +ServerChannel::~ServerChannel() { + memtrack::freed(this); +} + +void ServerChannel::register_client(const std::shared_ptr &client) { + unique_lock lock(this->client_lock); + for(const auto& _client : this->clients) { + if(_client.lock() == client) { + return; + } + } + + this->clients.push_back(client); +} + +void ServerChannel::unregister_client(const std::shared_ptr &client) { + unique_lock lock(this->client_lock); + this->clients.erase(remove_if(this->clients.begin(), this->clients.end(), [client](const auto& weak) { + auto locked = weak.lock(); + if(!locked || client == locked) return true; + return false; + }), this->clients.end()); +} + +size_t ServerChannel::client_count() { + shared_lock lock(this->client_lock); + size_t result = 0; + for(const auto& weak_entry : this->clients) { + if(auto entry = weak_entry.lock()) { + auto current_channel = entry->getChannel(); + if(current_channel && current_channel.get() == this) + result++; + } + } + return result; +} + +void ServerChannel::setProperties(const std::shared_ptr &ptr) { + BasicChannel::setProperties(ptr); +} + +ServerChannelTree::ServerChannelTree(const std::shared_ptr& server, sql::SqlManager* sql) : sql(sql), server(server) { } + +ServerChannelTree::~ServerChannelTree() { } + +void ServerChannelTree::deleteSemiPermanentChannels() { + loop: + + for(const auto& ch : this->channels()) + if(ch->channelType() == ChannelType::semipermanent || ch->channelType() == ChannelType::temporary){ //We also delete private channels + this->delete_channel_root(ch); + goto loop; + } +} + +ChannelId ServerChannelTree::generateChannelId() { + ChannelId channelId = 0; + + auto res = sql::command(this->sql, "SELECT `channelId` FROM `channels` WHERE `serverId` = :sid ORDER BY `channelId` DESC LIMIT 1", variable{":sid", this->getServerId()}).query([](ChannelId* num, int, char** value, char**){ + *num = (ChannelId) stoll(value[0]); + return 0; + }, &channelId); + auto pf = LOG_SQL_CMD; + pf(res); + if(!res) return 0; + return channelId + 1; +} + +std::shared_ptr ServerChannelTree::createChannel(ChannelId parentId, ChannelId orderId, const string &name) { + std::shared_ptr channel = BasicChannelTree::createChannel(parentId, orderId, name); + if(!channel) return channel; + + auto properties = serverInstance->databaseHelper()->loadChannelProperties(this->server.lock(), channel->channelId()); + for(const auto& prop : channel->properties().list_properties()) + if(prop.isModified()) //Copy the already set properties + (*properties)[prop.type()] = prop.value(); + static_pointer_cast(channel)->setProperties(properties); + static_pointer_cast(channel)->setPermissionManager(serverInstance->databaseHelper()->loadChannelPermissions(this->server.lock(), channel->channelId())); + + channel->properties()[property::CHANNEL_CREATED_AT] = chrono::duration_cast(chrono::system_clock::now().time_since_epoch()).count(); + channel->properties()[property::CHANNEL_LAST_LEFT] = chrono::duration_cast(chrono::system_clock::now().time_since_epoch()).count(); + + + auto result = sql::command(this->sql, "INSERT INTO `channels` (`serverId`, `channelId`, `type`, `parentId`) VALUES(:sid, :chid, :type, :parent);", variable{":sid", this->getServerId()}, variable{":chid", channel->channelId()}, variable{":type", channel->channelType()}, variable{":parent", channel->parent() ? channel->parent()->channelId() : 0}).execute(); + auto pf = LOG_SQL_CMD; + pf(result); + + return channel; +} + +inline std::shared_ptr findChannelByPool(const std::deque>& pool, size_t chId){ + for(const auto& elm : pool) + if(elm->entry->channelId() == chId) return dynamic_pointer_cast(elm->entry); + return nullptr; +} + +inline std::shared_ptr findLinkedChannelByPool(const std::deque>& pool, size_t chId){ + for(const auto& elm : pool) + if(elm->entry->channelId() == chId) return elm; + return nullptr; +} + +ServerId ServerChannelTree::getServerId() { + auto s = this->server.lock(); + return s ? s->getServerId() : 0UL; +} + +bool ServerChannelTree::initializeTempParents() { + auto channelList = this->tmpChannelList; + for(const auto& linked_channel : channelList) { + auto channel = dynamic_pointer_cast(linked_channel->entry); + assert(channel); + + if(channel->properties().hasProperty(property::CHANNEL_PID) && channel->properties()[property::CHANNEL_PID].as() != 0){ + if(!channel->parent()) + linked_channel->parent = findLinkedChannelByPool(this->tmpChannelList, channel->properties()[property::CHANNEL_PID]); + if(!channel->parent()){ + logError(this->getServerId(), "Invalid channel parent (Channel does not exists). Channel id: {} ({}) Missing parent id: {}", channel->channelId(), channel->name(), channel->properties()[property::CHANNEL_PID].as()); + logError(this->getServerId(), "Resetting parent"); + channel->properties()[property::CHANNEL_PID] = 0; + } + } + } + return true; +} + +typedef std::deque> ChannelPool; +inline ChannelPool remoteTopChannels(const std::shared_ptr& parent, ChannelPool& pool){ + ChannelPool result; + for(const auto& elm : pool) + if(elm->parent.lock() == parent) + result.push_back(elm); + for(const auto& elm : result) + pool.erase(std::find(pool.begin(), pool.end(), elm)); + return result; +} + +inline ChannelPool resolveChannelHeads(ServerId serverId, ChannelPool pool) { + ChannelPool result; + + while(!pool.empty()) { + auto element = pool.front(); + if(!element) continue; + + ChannelPool tmp_pool = pool; + auto f = find(tmp_pool.begin(), tmp_pool.end(), element); + if(f != tmp_pool.end()) + tmp_pool.erase(f); + + while(element->previous) { + auto found = find(tmp_pool.begin(), tmp_pool.end(), element->previous); + if(found == tmp_pool.end()) { + logError(serverId, "Found recursive channel heads! Cutting"); + if(element->previous && element->previous->next == element) + element->previous->next = nullptr; + element->previous = nullptr; + break; + } + tmp_pool.erase(found); + element = element->previous; + }; //Find the head + result.push_back(element); + + auto last = element; + while(element) { + auto it = find(pool.begin(), pool.end(), element); + if(it == pool.end()) { + logError(serverId, "Circular tail. Cutting previus."); + if(last != element) { + if(element->previous == last) + element->previous = nullptr; + + if(last->next == element) + last->next = nullptr; + } + break; + } else + pool.erase(it); + + last = element; + element = element->next; + } + } + + return result; +} + +inline std::shared_ptr buildChannelTree(ServerId serverId, const std::shared_ptr& parent, ChannelPool& channel_pool){ + auto top_channels = remoteTopChannels(parent, channel_pool); + if(top_channels.empty()) return nullptr; + + bool brokenTree = false; + for(const auto& linked_channel : top_channels){ + auto channel = dynamic_pointer_cast(linked_channel->entry); + assert(channel); + + if(channel->channelOrder() != 0) { + if(channel->channelOrder() == channel->channelId()) { + brokenTree = true; + logError("Channel order refers to itself! (Resetting)"); + channel->properties()[property::CHANNEL_ORDER] = 0; + continue; + } + + auto previous_linked = findLinkedChannelByPool(top_channels, channel->channelOrder()); + if(!previous_linked) { + brokenTree = true; + logError(serverId, "Failed to resolve previous channel for channel {} ({}). Previous channel id: {} (Resetting order)", channel->channelId(), channel->name(), channel->channelOrder()); + channel->properties()[property::CHANNEL_ORDER] = 0; + continue; + } + + if(previous_linked->next) { + if(previous_linked->next != linked_channel) { + brokenTree = true; + logError(serverId, "Previous channel {} ({}) of channel {} ({}) is already linked with another channel {} ({}).", + previous_linked->entry->channelId(), dynamic_pointer_cast(previous_linked->entry)->name(), + channel->channelId(), channel->name(), + previous_linked->next->entry->channelId(), dynamic_pointer_cast(previous_linked->next->entry)->name() + ); + logError(serverId, "Inserting channel anyway!"); + previous_linked->next->previous = linked_channel; + linked_channel->next = previous_linked->next; + } + } + previous_linked->next = linked_channel; + linked_channel->previous = previous_linked; + } + } + + for(const auto& elm : top_channels){ + if(elm->next) { + if(elm->next->previous != elm) { + brokenTree = true; + logError(serverId, "Test 'elm->next->previous != elm' failed! Assigning elm->next->previous to elm!"); + elm->next->previous = elm; + } + } + if(elm->previous) { + if(elm->previous->next != elm) { + brokenTree = true; + logError(serverId, "Test 'elm->previous->next != elm' failed! Assigning elm->previous->next to elm!"); + elm->previous->next = elm; + } + } + } + + + + /* Get the heads and merge them */ + auto heads = resolveChannelHeads(serverId, top_channels); + if(heads.size() > 1){ + brokenTree = true; + logError(serverId, "Got multiple channel heads! (Count {})", heads.size()); + logError(serverId, "Try to appending them", heads.size()); + + debugMessage(serverId, "Got head:"); + //FIXME print head + + for(int index = 1; index < heads.size(); index++) { + auto tail = heads[0]; + while(tail->next) tail = tail->next; + + tail->next = heads[index]; + tail->next->previous = tail; + } + } + + /* Testing tree */ + + if(brokenTree) { + auto new_heads = resolveChannelHeads(serverId, top_channels); + if(new_heads.size() != 1) { + logCritical(serverId, "Failed to merge channel heads (Got {} heads)! Unknown reason!", new_heads.size()); + logCritical(serverId, "Dropping a part!"); + } + } + + brokenTree = true; + if(brokenTree){ + auto entry = heads[0]; + while(entry) { + auto channel = dynamic_pointer_cast(entry->entry); + assert(channel); + + auto evaluated_order_id = entry->previous ? entry->previous->entry->channelId() : 0; + if(evaluated_order_id != channel->previousChannelId()) { + channel->setPreviousChannelId(evaluated_order_id); + debugMessage(serverId, "Fixed order id for channel {} ({}). New previous channel {}", entry->entry->channelId(), channel->name(), channel->channelOrder()); + } + + auto evaluated_parent_id = channel->parent() ? channel->parent()->channelId() : 0; + if(evaluated_parent_id != channel->properties()[property::CHANNEL_PID].as()) { + debugMessage(serverId, "Fixed parent id for channel {} ({}). New parent channel {}", entry->entry->channelId(), channel->name(), evaluated_parent_id); + channel->properties()[property::CHANNEL_PID] = evaluated_parent_id; + } + entry = entry->next; + } + } + + { /* building sub trees */ + auto entry = heads[0]; + while(entry) { + entry->child_head = buildChannelTree(serverId, entry, channel_pool); + entry = entry->next; + } + } + + return heads[0]; +} + +bool ServerChannelTree::buildChannelTreeFromTemp() { + if(this->tmpChannelList.empty()) return true; + + this->head = buildChannelTree(this->getServerId(), nullptr, this->tmpChannelList); + assert(tmpChannelList.empty()); + return true; +} + + +inline ssize_t count_characters(const std::string& in) { + size_t index = 0; + size_t count = 0; + while(index < in.length()){ + count++; + + auto current = (uint8_t) in[index]; + if(current >= 128) { //UTF8 check + if(current >= 192 && (current <= 193 || current >= 245)) { + return -1; + } else if(current >= 194 && current <= 223) { + if(in.length() - index <= 1) return -1; + else if((uint8_t) in[index + 1] >= 128 && (uint8_t) in[index + 1] <= 191) index += 1; //Valid + else return -1; + } else if(current >= 224 && current <= 239) { + if(in.length() - index <= 2) return -1; + else if((uint8_t) in[index + 1] >= 128 && (uint8_t) in[index + 1] <= 191 && + (uint8_t) in[index + 2] >= 128 && (uint8_t) in[index + 2] <= 191) index += 2; //Valid + else return -1; + } else if(current >= 240 && current <= 244) { + if(in.length() - index <= 3) return -1; + else if((uint8_t) in[index + 1] >= 128 && (uint8_t) in[index + 1] <= 191 && + (uint8_t) in[index + 2] >= 128 && (uint8_t) in[index + 2] <= 191 && + (uint8_t) in[index + 3] >= 128 && (uint8_t) in[index + 3] <= 191) index += 3; //Valid + else return -1; + } else { + return -1; + } + } + index++; + } + return count; +} + +bool ServerChannelTree::validateChannelNames() { + /* + + if (count_characters(cmd["channel_name"]) < 1) return {findError("channel_name_inuse"), "Invalid channel name (too short)"}; + if (count_characters(cmd["channel_name"]) > 40) return {findError("channel_name_inuse"), "Invalid channel name (too long)"}; + + */ + /* + for(const auto &channel : this->channels()){ + mainSearch: + for(const auto& ref : this->channels(channel->parent(), 1)) + if(ref->name() == channel->name() && ref != channel) { + logError(lstream << "Duplicated channel name '" << channel->name() << "'. Fixing it by appending '1'" << endl); + channel->properties()["channel_name"] = channel->name() + "1"; + goto mainSearch; + } + if(channel->name().length() > 40) { + channel->properties()["channel_name"] = channel->name().substr(0, 35) + rnd_string(5); + goto mainSearch; + } + } + */ + + for(const auto &channel : this->channels()){ + auto name_length = count_characters(channel->name()); + if(name_length > 40) { + logError(this->getServerId(), "Channel {} loaded an invalid name from the database (name to long). Cutting channel name"); + channel->properties()[property::CHANNEL_NAME] = channel->name().substr(0, 40); //FIXME count UTF8 + } else if(name_length < 1) { + logError(this->getServerId(), "Channel {} loaded an invalid name from the database (empty name). Resetting channel name"); + channel->properties()[property::CHANNEL_NAME] = "undefined"; + } + } + + function &)> test_level; + + test_level = [&](const std::shared_ptr &head) { + map> used_names; + auto it = head; + while(it) { + auto channel = dynamic_pointer_cast(it->entry); + auto name = channel->name(); + + if(used_names.count(name) > 0) { + auto taken_channel = used_names[name]; + assert(taken_channel); + + size_t index = 1; + while(true) { + auto _name = name + to_string(index); + if(_name.length() > 40) //FIXME count UTF8 + _name = _name.substr(_name.length() - 40); + if(used_names[_name]) + index++; + else { + name = _name; + break; + } + } + channel->properties()[property::CHANNEL_NAME] = name; + + logError(this->getServerId(), "Channel {} has the same name as channel {}. Name: {}. Changing name to {} by appending an index.", + channel->channelId(), + taken_channel->channelId(), + taken_channel->name(), + name + ); + } + used_names[name] = channel; + + if(it->child_head) + test_level(it->child_head); + it = it->next; + } + }; + + test_level(this->tree_head()); + return true; +} + +bool ServerChannelTree::validateChannelIcons() { + for(const auto &channel : this->channels()) { + auto iconId = (IconId) channel->properties()[property::CHANNEL_ICON_ID]; + if(iconId != 0 && !serverInstance->getFileServer()->iconExists(this->server.lock(), iconId)) { + logMessage(this->getServerId(), "[FILE] Missing channel icon (" + to_string(iconId) + ")."); + if(config::server::delete_missing_icon_permissions) { + channel->properties()[property::CHANNEL_ICON_ID] = 0; + channel->permissions()->set_permission(permission::i_icon_id, {0, 0}, permission::v2::PermissionUpdateType::set_value, permission::v2::PermissionUpdateType::do_nothing); + } + } + } + return true; +} + +void ServerChannelTree::loadChannelsFromDatabase() { + auto res = sql::command(this->sql, "SELECT * FROM `channels` WHERE `serverId` = :sid", variable{":sid", this->getServerId()}).query(&ServerChannelTree::loadChannelFromData, this); + (LOG_SQL_CMD)(res); + if(!res){ + logError(this->getServerId(), "Could not load channel tree from database"); + return; + } + + logMessage(this->getServerId(), lstream << "Got " << this->tmpChannelList.size() << " saved channels" << endl); + this->initializeTempParents(); + this->buildChannelTreeFromTemp(); + this->validateChannelNames(); + this->validateChannelIcons(); + //this->printChannelTree(); +} + +int ServerChannelTree::loadChannelFromData(int argc, char **data, char **column) { + ChannelId channelId = 0; + ChannelId parentId = 0; + auto type = static_cast(0xFF); + + + int index = 0; + try { + for(index = 0; index < argc; index++){ + if(strcmp(column[index], "channelId") == 0) channelId = static_cast(stoll(data[index])); + else if(strcmp(column[index], "parentId") == 0) parentId = static_cast(stoll(data[index])); + else if(strcmp(column[index], "type") == 0) type = static_cast(stoll(data[index])); + else if(strcmp(column[index], "serverId") == 0) {} + else logError(this->getServerId(), "ServerChannelTree::loadChannelFromData called with invalid column from sql \"{}\"", column[index]); + } + } catch (std::exception& ex) { + logError(this->getServerId(), "Failed to load channel. Got exception {}. Exception was thrown at parsing row {} with data \"{}\"", ex.what(), column[index], data[index]); + return 0; + } + + //assert(type != 0xFF); + assert(channelId != 0); + + auto channel = std::make_shared(parentId, channelId); + auto server = this->server.lock(); + static_pointer_cast(channel)->setProperties(serverInstance->databaseHelper()->loadChannelProperties(server, channelId)); + static_pointer_cast(channel)->setPermissionManager(serverInstance->databaseHelper()->loadChannelPermissions(server, channel->channelId())); + + auto entry = make_shared(channel); + channel->setLinkedHandle(entry); + this->tmpChannelList.push_back(entry); + return 0; +} + +deque ServerChannelTree::deleteChannelRoot(const std::shared_ptr &channel) { + auto server = this->server.lock(); + + auto channels = this->delete_channel_root(channel); + deque channel_ids; + for(const auto& channel : channels) + channel_ids.push_back(channel->channelId()); + return channel_ids; +} + +void ServerChannelTree::on_channel_entry_deleted(const shared_ptr &channel) { + BasicChannelTree::on_channel_entry_deleted(channel); + + auto server = this->server.lock(); + if(server) + server->getGroupManager()->handleChannelDeleted(channel); + else + serverInstance->getGroupManager()->handleChannelDeleted(channel); + + + auto sql_result = sql::command(this->sql, "DELETE FROM `channels` WHERE `serverId` = '" + to_string(this->getServerId()) + "' AND `channelId` = '" + to_string(channel->channelId()) + "'").execute(); + LOG_SQL_CMD(sql_result); + + sql_result = sql::command(this->sql, "DELETE FROM `properties` WHERE `serverId` = '" + to_string(this->getServerId()) + "' AND `id` = '" + to_string(channel->channelId()) + "' AND `type` = " + to_string(property::PropertyType::PROP_TYPE_CHANNEL)).execute(); + LOG_SQL_CMD(sql_result); + + serverInstance->databaseHelper()->deleteChannelPermissions(this->server.lock(), channel->channelId()); + sql_result = sql::command(this->sql, "DELETE FROM `assignedGroups` WHERE `serverId` = '" + to_string(this->getServerId()) + "' AND `channelId` = '" + to_string(channel->channelId()) + "'").execute(); + LOG_SQL_CMD(sql_result); +} + +std::shared_ptr ServerChannelTree::allocateChannel(const shared_ptr &parent, ChannelId channelId) { + return std::make_shared(parent ? parent->channelId() : 0, channelId); +} diff --git a/server/src/channel/ServerChannel.h b/server/src/channel/ServerChannel.h new file mode 100644 index 0000000..b9c66ec --- /dev/null +++ b/server/src/channel/ServerChannel.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include "Properties.h" +#include "PermissionManager.h" +#include "BasicChannel.h" +#include "../Group.h" +#include +#include + +namespace ts { + namespace server { + class TSServer; + class ConnectedClient; + } + + class ServerChannelTree; + class ServerChannel : public BasicChannel { + friend class ServerChannelTree; + public: + ServerChannel(ChannelId parentId, ChannelId channelId); + + ~ServerChannel(); + void setProperties(const std::shared_ptr &ptr) override; + + std::shared_mutex client_lock; + std::deque> clients; + + void unregister_client(const std::shared_ptr& /* client */); + void register_client(const std::shared_ptr& /* client */); + + bool deleted = false; + size_t client_count(); + }; + + class ServerChannelTree : public BasicChannelTree { + public: + ServerChannelTree(const std::shared_ptr&, sql::SqlManager*); + virtual ~ServerChannelTree(); + void loadChannelsFromDatabase(); + + virtual std::shared_ptr createChannel(ChannelId parentId, ChannelId orderId, const std::string &name) override; + virtual std::deque deleteChannelRoot(const std::shared_ptr &channel); + + void deleteSemiPermanentChannels(); + + std::shared_ptr tree_head() { return this->head; } + protected: + virtual ChannelId generateChannelId() override; + + virtual void on_channel_entry_deleted(const std::shared_ptr &channel) override; + + std::shared_ptr allocateChannel(const std::shared_ptr &parent, ChannelId channelId) override; + + private: + std::weak_ptr server; + ServerId getServerId(); + sql::SqlManager* sql; + + std::deque> tmpChannelList; + + bool initializeTempParents(); + bool buildChannelTreeFromTemp(); + bool validateChannelNames(); + bool validateChannelIcons(); + int loadChannelFromData(int argc, char** data, char** column); + }; +} \ No newline at end of file diff --git a/server/src/client/ConnectedClient.cpp b/server/src/client/ConnectedClient.cpp new file mode 100644 index 0000000..0c77967 --- /dev/null +++ b/server/src/client/ConnectedClient.cpp @@ -0,0 +1,941 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../TSServer.h" +#include "voice/VoiceClient.h" +#include "../server/VoiceServer.h" +#include "../server/file/FileServer.h" +#include "../InstanceHandler.h" +#include "ConnectedClient.h" + +using namespace std; +using namespace std::chrono; +using namespace ts; +using namespace ts::server; +using namespace ts::token; + +extern ts::server::InstanceHandler* serverInstance; + +ConnectedClient::ConnectedClient(sql::SqlManager* db, const std::shared_ptr&server) : DataClient(db, server) { + memtrack::allocated(this); + memset(&this->remote_address, 0, sizeof(this->remote_address)); + + connectionStatistics = make_shared(server ? server->getServerStatistics() : nullptr, false); + this->connectionStatistics->measure_bandwidths(false); /* done by the client and we trust this */ + + channels = make_shared(this); +} + +ConnectedClient::~ConnectedClient() { + memtrack::freed(this); +} + +std::shared_ptr ConnectedClient::requestConnectionInfo(const std::shared_ptr& receiver) { + auto& info = this->connection_info; + + lock_guard info_lock(info.lock); + if(info.data){ + if(chrono::system_clock::now() - info.data_age < chrono::seconds(1)) + return info.data; + + if(chrono::system_clock::now() - info.data_age > chrono::seconds(5)) //Data timeout + info.data = nullptr; + } + + info.receiver.erase(std::remove_if(info.receiver.begin(), info.receiver.end(), [&](const weak_ptr& weak) { + auto locked = weak.lock(); + return !locked || locked == receiver; + }), info.receiver.end()); + info.receiver.push_back(receiver); + + if(chrono::system_clock::now() - info.last_requested >= chrono::seconds(1)) { + info.last_requested = chrono::system_clock::now(); + + Command cmd("notifyconnectioninforequest"); + + string invoker; + for(const auto& weak_request : info.receiver) { + auto request = weak_request.lock(); + if(!request) continue; + invoker += (invoker.empty() ? "" : ",") + to_string(request->getClientId()); + } + + cmd["invokerids"] = invoker; + this->sendCommand(cmd); + } + + return info.data; +} + +//Attention the client should be only read only locked! +void ConnectedClient::updateChannelClientProperties(bool lock_channel_tree, bool notify_self) { + /* this->server may be null! */ + shared_ptr server_ref = this->server; + + auto permissions = this->permissionValues(permission::PERMTEST_ORDERED, { + permission::i_client_talk_power, + permission::b_client_is_priority_speaker, + permission::b_client_ignore_antiflood, + permission::i_channel_view_power, + permission::b_channel_ignore_view_power + }, this->currentChannel); + + permission::PermissionValue + permission_talk_power = permNotGranted, + permission_priority_speaker = permNotGranted, + permission_ignore_antiflood = permNotGranted, + permission_channel_view_power = permNotGranted, + permission_channel_ignore_view_power = permNotGranted; + for(const auto& perm : permissions) { + if(perm.first == permission::i_client_talk_power) + permission_talk_power = perm.second; + else if(perm.first == permission::b_client_is_priority_speaker) + permission_priority_speaker = perm.second; + else if(perm.first == permission::b_client_ignore_antiflood) + permission_ignore_antiflood = perm.second; + else if(perm.first == permission::i_channel_view_power) + permission_channel_view_power = perm.second; + else if(perm.first == permission::b_channel_ignore_view_power) + permission_channel_ignore_view_power = perm.second; + else sassert(false); + } + + if(permission_talk_power < -2) permission_talk_power = -2; + else if(permission_talk_power == -2) permission_talk_power = 0; + + deque notifyList; + debugMessage(this->getServerId(), "{} Got a channel talk power of {} Talk power set is {}", CLIENT_STR_LOG_PREFIX, permission_talk_power, this->properties()[property::CLIENT_TALK_POWER].as()); + if(permission_talk_power != this->properties()[property::CLIENT_TALK_POWER].as() && this->currentChannel) { //We do not have to update tp if there's no channel + this->properties()[property::CLIENT_TALK_POWER] = permission_talk_power; + notifyList.emplace_back(property::CLIENT_TALK_POWER); + + auto update = this->properties()[property::CLIENT_IS_TALKER].as() || this->properties()[property::CLIENT_TALK_REQUEST].as() > 0; + if(update) { + if(this->currentChannel->talk_power_granted({permission_talk_power, permission_talk_power != permNotGranted})) { + this->properties()[property::CLIENT_IS_TALKER] = 0; + this->properties()[property::CLIENT_TALK_REQUEST] = 0; + this->properties()[property::CLIENT_TALK_REQUEST_MSG] = ""; + notifyList.emplace_back(property::CLIENT_IS_TALKER); + notifyList.emplace_back(property::CLIENT_TALK_REQUEST); + notifyList.emplace_back(property::CLIENT_TALK_REQUEST_MSG); + } + } + } + + auto icon_permission = this->calculate_permission_value(permission::i_icon_id, -1); + IconId iconId = icon_permission.has_value ? icon_permission.value : 0; + logTrace(this->getServerId(), "[CLIENT] Updating client icon from " + to_string(this->properties()[property::CLIENT_ICON_ID].as()) + " to " + to_string(iconId)); + if(this->properties()[property::CLIENT_ICON_ID].as() != iconId){ + if(server_ref && iconId != 0) { + auto dir = serverInstance->getFileServer()->iconDirectory(server_ref); + if(!serverInstance->getFileServer()->iconExists(server_ref, iconId)) { + logMessage(this->getServerId(), "[FILE] Missing client icon (" + to_string(iconId) + ")."); + iconId = 0; + } + } + if(this->properties()[property::CLIENT_ICON_ID].as() != iconId) { + this->properties()[property::CLIENT_ICON_ID] = (IconId) iconId; + notifyList.emplace_back(property::CLIENT_ICON_ID); + } + } + + auto pSpeaker = permission_priority_speaker > 0; + if(properties()[property::CLIENT_IS_PRIORITY_SPEAKER].as() != pSpeaker){ + properties()[property::CLIENT_IS_PRIORITY_SPEAKER] = pSpeaker; + notifyList.emplace_back(property::CLIENT_IS_PRIORITY_SPEAKER); + } + + block_flood = permission_ignore_antiflood <= 0 || permission_ignore_antiflood == permNotGranted; + if(server_ref) + server_ref->notifyClientPropertyUpdates(_this.lock(), notifyList, notify_self); + this->updateTalkRights(permission_talk_power); + + if((this->channels_view_power != permission_channel_view_power || this->channels_ignore_view != permission_channel_ignore_view_power) && notify_self && this->currentChannel && server_ref) { + this->channels_view_power = permission_channel_view_power; + this->channels_ignore_view = permission_channel_ignore_view_power; + + shared_lock server_channel_lock(server_ref->channel_tree_lock, defer_lock); + unique_lock client_channel_lock(this->channel_lock, defer_lock); + + if(lock_channel_tree) { + /* first read lock server channel tree */ + server_channel_lock.lock(); + client_channel_lock.lock(); + } + + deque deleted; + for(const auto& update_entry : this->channels->update_channel_path(server_ref->channelTree->tree_head(), server_ref->channelTree->find_linked_entry(this->currentChannel->channelId()))) { + if(update_entry.first) + this->notifyChannelShow(update_entry.second->channel(), update_entry.second->previous_channel); + else deleted.push_back(update_entry.second->channelId()); + } + if(!deleted.empty()) + this->notifyChannelHide(deleted, false); /* we've locked the tree before */ + } +} + +void ConnectedClient::updateTalkRights(permission::PermissionValue talk_power) { + bool flag = false; + flag |= this->properties()[property::CLIENT_IS_TALKER].as(); + if(!flag && this->currentChannel) { + flag = this->currentChannel->talk_power_granted({talk_power, talk_power != permNotGranted}); + } + this->allowedToTalk = flag; +} + +void ConnectedClient::resetIdleTime() { + this->idleTimestamp = chrono::system_clock::now(); +} + +void ConnectedClient::increaseFloodPoints(uint16_t num) { + this->floodPoints += num; +} + +bool ConnectedClient::shouldFloodBlock() { + if(!this->server) return false; + if(!this->block_flood) return false; + return this->floodPoints > this->server->properties()[property::VIRTUALSERVER_ANTIFLOOD_POINTS_NEEDED_COMMAND_BLOCK].as(); +} + +std::deque> ConnectedClient::subscribeChannel(const std::deque>& targets, bool lock_channel, bool enforce) { + deque> subscribed_channels; + + auto ref_server = this->server; + if(!ref_server) + return {}; + + auto general_granted = enforce || this->permission_granted(this->permissionValue(permission::b_channel_ignore_subscribe_power, nullptr), 1, true); + { + shared_lock server_channel_lock(ref_server->channel_tree_lock, defer_lock); + unique_lock client_channel_lock(this->channel_lock, defer_lock); + if(lock_channel) { + server_channel_lock.lock(); + client_channel_lock.lock(); + } + + auto cache = make_shared(); + for (const auto& channel : targets) { + auto local_channel = this->channels->find_channel(channel); + if(!local_channel) continue; //Not visible + if(local_channel->subscribed) continue; //Already subscribed + + if(!general_granted && channel != this->currentChannel) { + auto granted_permission = this->calculate_permission_value(permission::i_channel_subscribe_power, channel->channelId()); + if(!channel->permission_granted(permission::i_channel_needed_subscribe_power, granted_permission, false) && + !this->permission_granted(this->permissionValue(permission::b_channel_ignore_subscribe_power, channel), 1, true)) { + continue; + } + } + + local_channel->subscribed = true; + subscribed_channels.push_back(channel); + } + + deque> visible_clients; + for(const auto& target_channel : subscribed_channels) { + /* ref_server->getClientsByChannel only acquires channel client lock */ + for(const auto& client : ref_server->getClientsByChannel(target_channel)) { + visible_clients.push_back(client); + } + } + + this->notifyClientEnterView(visible_clients, ViewReasonSystem); + + if (!subscribed_channels.empty()) + this->notifyChannelSubscribed(subscribed_channels); + } + + return subscribed_channels; +} + +std::deque> ConnectedClient::unsubscribeChannel(const std::deque>& targets, bool lock_channel) { + auto ref_server = this->server; + if(!ref_server) + return {}; + + deque > unsubscribed_channels; + + { + shared_lock server_channel_lock(ref_server->channel_tree_lock, defer_lock); + unique_lock client_channel_lock(this->channel_lock, defer_lock); + if(lock_channel) { + server_channel_lock.lock(); + client_channel_lock.lock(); + } + + for (const auto& channel : targets) { + if(this->currentChannel == channel) continue; + + auto chan = this->channels->find_channel(channel); + if(!chan || !chan->subscribed) continue; + chan->subscribed = false; + + /* ref_server->getClientsByChannel only acquires channel client lock */ + auto clients = this->server->getClientsByChannel(channel); + this->visibleClients.erase(std::remove_if(this->visibleClients.begin(), this->visibleClients.end(), [&, clients](const weak_ptr& weak) { + auto c = weak.lock(); + if(!c) { + logError(this->getServerId(), "{} Got \"dead\" client in visible client list! This can cause a remote client disconnect within the future!", CLIENT_STR_LOG_PREFIX); + return true; + } + + return std::find(clients.begin(), clients.end(), c) != clients.end(); + }), this->visibleClients.end()); + + unsubscribed_channels.push_back(channel); + } + + if (!unsubscribed_channels.empty()) + this->notifyChannelUnsubscribed(unsubscribed_channels); + } + + return unsubscribed_channels; +} + +bool ConnectedClient::isClientVisible(const std::shared_ptr& client, bool lock) { + for(const auto& entry : this->getVisibleClients(lock)) + if(entry.lock() == client) + return true; + return false; +} + +bool ConnectedClient::notifyClientLeftView(const std::deque> &clients, const std::string &reason, bool lock, const ts::ViewReasonServerLeftT &_vrsl) { + if(clients.empty()) + return true; + + if(lock) { + /* TODO: add locking of server channel tree in read mode and client tree in write mode! */ + assert(false); + } + + Command cmd("notifyclientleftview"); + cmd["reasonmsg"] = reason; + cmd["reasonid"] = ViewReasonId::VREASON_SERVER_LEFT; + cmd["ctid"] = 0; + + ChannelId current_channel_id = 0; + + size_t index = 0; + auto it = clients.begin(); + while(it != clients.end()) { + auto client = *it; + assert(client->getClientId() > 0); + assert(client->currentChannel || &*client == this); + if(!client->currentChannel) + continue; + + if(current_channel_id != (client->currentChannel ? client->currentChannel->channelId() : 0)) { + if(current_channel_id != 0) + break; + else + cmd[index]["cfid"] = (current_channel_id = client->currentChannel->channelId()); + } + cmd[index]["clid"] = client->getClientId(); + it++; + index++; + } + + this->visibleClients.erase(std::remove_if(this->visibleClients.begin(), this->visibleClients.end(), [&](const weak_ptr& weak) { + auto c = weak.lock(); + if(!c) { + logError(this->getServerId(), "{} Got \"dead\" client in visible client list! This can cause a remote client disconnect within the future!", CLIENT_STR_LOG_PREFIX); + return true; + } + return std::find(clients.begin(), it, c) != it; + }), this->visibleClients.end()); + + this->sendCommand(cmd); + if(it != clients.end()) + return this->notifyClientLeftView({it, clients.end()}, reason, false, _vrsl); + return true; +} + +bool ConnectedClient::notifyClientLeftView( + const shared_ptr &client, + const std::shared_ptr &target_channel, + ViewReasonId reason_id, + const std::string& reason_message, + std::shared_ptr invoker, + bool lock_channel_tree) { + assert(!lock_channel_tree); /* not supported yet! */ + assert(client && client->getClientId() != 0); + assert(client->currentChannel || &*client == this); + + if(client != this) { + if(reason_id == VREASON_SERVER_STOPPED || reason_id == VREASON_SERVER_SHUTDOWN) { + debugMessage(this->getServerId(), "Replacing notify left reason " + to_string(reason_id) + " with " + to_string(VREASON_SERVER_LEFT)); + reason_id = VREASON_SERVER_LEFT; + } + } + + /* + switch (reasonId) { + case ViewReasonId::VREASON_MOVED: + case ViewReasonId::VREASON_BAN: + case ViewReasonId::VREASON_CHANNEL_KICK: + case ViewReasonId::VREASON_SERVER_KICK: + case ViewReasonId::VREASON_SERVER_SHUTDOWN: + case ViewReasonId::VREASON_SERVER_STOPPED: + if(reasonMessage.empty()) { + logCritical(this->getServerId(), "{} ConnectedClient::notifyClientLeftView() => missing reason message for reason id {}", CLIENT_STR_LOG_PREFIX, reasonId); + reasonMessage = ""; + } + break; + default: + break; + } + */ + + switch (reason_id) { + case ViewReasonId::VREASON_MOVED: + case ViewReasonId::VREASON_BAN: + case ViewReasonId::VREASON_CHANNEL_KICK: + case ViewReasonId::VREASON_SERVER_KICK: + if(!invoker) { + logCritical(this->getServerId(), "{} ConnectedClient::notifyClientLeftView() => missing invoker for reason id {}", CLIENT_STR_LOG_PREFIX, reason_id); + if(this->server) + invoker = this->server->serverRoot; + } + break; + default: + break; + } + + Command cmd("notifyclientleftview"); + cmd["reasonmsg"] = reason_message; + cmd["reasonid"] = reason_id; + cmd["clid"] = client->getClientId(); + cmd["cfid"] = client->currentChannel ? client->currentChannel->channelId() : 0; + cmd["ctid"] = target_channel ? target_channel->channelId() : 0; + + if (invoker) { + cmd["invokerid"] = invoker->getClientId(); + cmd["invokername"] = invoker->getDisplayName(); + cmd["invokeruid"] = invoker->getUid(); + } + + + /* TODO: Critical warn if the client hasn't been found? */ + this->visibleClients.erase(std::remove_if(this->visibleClients.begin(), this->visibleClients.end(), [&, client](const weak_ptr& weak) { + auto c = weak.lock(); + if(!c) { + logError(this->getServerId(), "{} Got \"dead\" client in visible client list! This can cause a remote client disconnect within the future!", CLIENT_STR_LOG_PREFIX); + return true; + } + return c == client; + }), this->visibleClients.end()); + this->sendCommand(cmd); + return true; +} + +bool ConnectedClient::notifyClientLeftViewKicked(const std::shared_ptr &client, + const std::shared_ptr &target_channel, + const std::string& message, + std::shared_ptr invoker, + bool lock_channel_tree) { + assert(!lock_channel_tree); /* not supported yet! */ + assert(client && client->getClientId() != 0); + assert(client->currentChannel || &*client == this); + + /* TODO: Critical warn if the client hasn't been found? */ + this->visibleClients.erase(std::remove_if(this->visibleClients.begin(), this->visibleClients.end(), [&, client](const weak_ptr& weak) { + auto c = weak.lock(); + if(!c) { + logError(this->getServerId(), "{} Got \"dead\" client in visible client list! This can cause a remote client disconnect within the future!", CLIENT_STR_LOG_PREFIX); + return true; + } + return c == client; + }), this->visibleClients.end()); + + if(!invoker) { + logCritical(this->getServerId(), "{} ConnectedClient::notifyClientLeftViewKicked() => missing invoker for reason id {}", CLIENT_STR_LOG_PREFIX, target_channel ? ViewReasonId::VREASON_CHANNEL_KICK : ViewReasonId::VREASON_SERVER_KICK); + if(this->server) + invoker = this->server->serverRoot; + } + + Command cmd("notifyclientleftview"); + + cmd["clid"] = client->getClientId(); + cmd["cfid"] = client->currentChannel ? client->currentChannel->channelId() : 0; + cmd["ctid"] = target_channel ? target_channel->channelId() : 0; + cmd["reasonid"] = (uint8_t) (target_channel ? ViewReasonId::VREASON_CHANNEL_KICK : ViewReasonId::VREASON_SERVER_KICK); + cmd["reasonmsg"] = message; + + if (invoker) { + cmd["invokerid"] = invoker->getClientId(); + cmd["invokername"] = invoker->getDisplayName(); + cmd["invokeruid"] = invoker->getUid(); + } + + this->sendCommand(cmd); + return true; +} + +bool ConnectedClient::notifyClientLeftViewBanned( + const shared_ptr &client, + const std::string& message, + std::shared_ptr invoker, + size_t length, + bool lock_channel_tree) { + + assert(!lock_channel_tree); /* not supported yet! */ + assert(client && client->getClientId() != 0); + assert(client->currentChannel || &*client == this); + + Command cmd("notifyclientleftview"); + + cmd["clid"] = client->getClientId(); + cmd["cfid"] = client->currentChannel ? client->currentChannel->channelId() : 0; + cmd["ctid"] = 0; + cmd["reasonid"] = ViewReasonId::VREASON_BAN; + cmd["reasonmsg"] = message; + + if (invoker) { + cmd["invokerid"] = invoker->getClientId(); + cmd["invokername"] = invoker->getDisplayName(); + cmd["invokeruid"] = invoker->getUid(); + } + + if (length > 0) { + cmd["bantime"] = length; + } + + /* TODO: Critical warn if the client hasn't been found? */ + this->visibleClients.erase(std::remove_if(this->visibleClients.begin(), this->visibleClients.end(), [&, client](const weak_ptr& weak) { + auto c = weak.lock(); + if(!c) { + logError(this->getServerId(), "{} Got \"dead\" client in visible client list! This can cause a remote client disconnect within the future!", CLIENT_STR_LOG_PREFIX); + return true; + } + return c == client; + }), this->visibleClients.end()); + + this->sendCommand(cmd); + return true; +} + +bool ConnectedClient::sendNeededPermissions(bool enforce) { + if(!enforce && this->state != ConnectionState::CONNECTED) return false; + + if(!enforce && chrono::system_clock::now() - this->lastNeededNotify < chrono::seconds(5) && this->lastNeededPermissionNotifyChannel == this->currentChannel) { //Dont spam these (hang up ui) + this->requireNeededPermissionResend = true; + return true; + } + this->lastNeededNotify = chrono::system_clock::now(); + this->lastNeededPermissionNotifyChannel = this->currentChannel; + this->requireNeededPermissionResend = false; + + return this->notifyClientNeededPermissions(); +} + +bool ConnectedClient::notifyClientNeededPermissions() { + Command cmd("notifyclientneededpermissions"); + int index = 0; + + unique_lock cache_lock(this->cached_permissions_lock); + auto permissions = this->cached_permissions; + cache_lock.unlock(); + + for(const auto& value : permissions) { + if(value.second != permNotGranted || value.first == permission::b_client_force_push_to_talk) { + cmd[index]["permid"] = value.first; + cmd[index++]["permvalue"] = value.second == permNotGranted ? 0 : value.second; + } + } + if(permissions.empty()) { + cmd[0]["permid"] = permission::b_client_force_push_to_talk; + cmd[0]["permvalue"] = false; + } + + this->sendCommand(cmd); + return true; +} + +bool ConnectedClient::notifyError(const CommandResult& result, const std::string& retCode) { + Command cmd("error"); + + cmd["id"] = result.error.errorId; + cmd["msg"] = result.error.message; + + if(retCode.length() > 0) + cmd["return_code"] = retCode; + + for(const auto& extra : result.extraProperties) + cmd[extra.first] = extra.second; + + this->sendCommand(cmd); + return true; +} + +inline std::shared_ptr pop_view_entry(std::deque>& pool, ChannelId id) { + for(auto it = pool.begin(); it != pool.end(); it++) { + if((*it)->channelId() == id) { + auto handle = *it; + pool.erase(it); + return handle; + } + } + return nullptr; +} + +using ChannelIT = std::deque>::iterator; +inline void send_channels(ConnectedClient* client, ChannelIT begin, const ChannelIT& end, bool override_orderid){ + if(begin == end) + return; + + Command channellist("channellist"); + size_t index = 0; + + while(begin != end) { + auto channel = (*begin)->channel(); + if(!channel) continue; + + for (const auto &elm : channel->properties().list_properties(property::FLAG_CHANNEL_VIEW, client->getType() == CLIENT_TEAMSPEAK ? property::FLAG_NEW : (uint16_t) 0)) { + if(elm.type() == property::CHANNEL_ORDER) + channellist[index][elm.type().name] = override_orderid ? 0 : (*begin)->previous_channel; + else + channellist[index][elm.type().name] = elm.as(); + } + + begin++; + if(++index > 6) + break; + } + client->sendCommand(channellist); + if(begin != end) + send_channels(client, begin, end, override_orderid); +} + +void ConnectedClient::sendChannelList(bool lock_channel_tree) { + shared_lock server_channel_lock(this->server->channel_tree_lock, defer_lock); + unique_lock client_channel_lock(this->channel_lock, defer_lock); + if(lock_channel_tree) { + server_channel_lock.lock(); + client_channel_lock.lock(); + } + + auto channels = this->channels->insert_channels(this->server->channelTree->tree_head(), true, false); + + if(this->currentChannel) { + bool send_success; + for(const auto& channel : this->channels->show_channel(this->server->channelTree->find_linked_entry(this->currentChannel->channelId()), send_success)) + channels.push_back(channel); + if(!send_success) + logCritical(this->getServerId(), "ConnectedClient::sendChannelList => failed to insert default channel!"); + } + + /* + this->channels->print(); + auto channels_left = channels; + for(const auto& channel : channels) { + if(channel->previous_channel == 0) continue; + + bool erased = false; + bool own = true; + for(const auto& entry : channels_left) { + if(entry->channelId() == channel->channelId()) + own = true; + if(entry->channelId() == channel->previous_channel) { + channels_left.erase(find(channels_left.begin(), channels_left.end(), entry)); + erased = true; + break; + } + } + if(!erased || !own) { + logCritical(this->getServerId(), "Client {} would get an invalid channel order disconnect! Channel {} is not send before his channel! (Flags: erased := {} | own := {})", CLIENT_STR_LOG_PREFIX_(this), channel->previous_channel, erased, own); + } + } + */ + + + std::deque> entry_channels{pop_view_entry(channels, this->currentChannel->channelId())}; + while(entry_channels.front()) entry_channels.push_front(pop_view_entry(channels, entry_channels.front()->parentId())); + entry_channels.pop_front(); + if(entry_channels.empty()) + logCritical(this->getServerId(), "ConnectedClient::sendChannelList => invalid (empty) own channel path!"); + + send_channels(this, entry_channels.begin(), entry_channels.end(), true); + this->notifyClientEnterView(_this.lock(), nullptr, "", this->currentChannel, ViewReasonId::VREASON_SYSTEM, nullptr, false); //Notify self after path is send + + send_channels(this, channels.begin(), channels.end(), false); + + this->sendCommand(Command("channellistfinished")); +} + +void ConnectedClient::sendChannelDescription(const std::shared_ptr& channel, bool lock) { + shared_lock tree_lock(this->channel_lock, defer_lock); + if(lock) + tree_lock.lock(); + + if(!this->channels->channel_visible(channel)) return; + + Command cmd("notifychanneledited"); + cmd["cid"] = channel->channelId(); + cmd["reasonid"] = 9; + cmd["channel_description"] = channel->properties()[property::CHANNEL_DESCRIPTION].as(); + this->sendCommand(cmd, true); +} + +void ConnectedClient::tick(const std::chrono::system_clock::time_point &time) { + ALARM_TIMER(A1, "ConnectedClient::tick", milliseconds(2)); + if(this->state == ConnectionState::CONNECTED) { + if(this->requireNeededPermissionResend) + this->sendNeededPermissions(false); + if(this->lastOnlineTimestamp.time_since_epoch().count() == 0) { + this->lastOnlineTimestamp = time; + } else if(time - this->lastOnlineTimestamp > seconds(120)) { + this->properties()[property::CLIENT_MONTH_ONLINE_TIME] += duration_cast(time - lastOnlineTimestamp).count(); + this->properties()[property::CLIENT_TOTAL_ONLINE_TIME] += duration_cast(time - lastOnlineTimestamp).count(); + lastOnlineTimestamp = time; + } + if(time - this->lastTransfareTimestamp > seconds(5)) { + lastTransfareTimestamp = time; + auto update = this->connectionStatistics->mark_file_bytes(); + if(update.first > 0) { + this->properties()[property::CLIENT_MONTH_BYTES_DOWNLOADED] += update.first; + this->properties()[property::CLIENT_TOTAL_BYTES_DOWNLOADED] += update.first; + } + if(update.second > 0) { + this->properties()[property::CLIENT_MONTH_BYTES_UPLOADED] += update.second; + this->properties()[property::CLIENT_TOTAL_BYTES_UPLOADED] += update.second; + } + } + } + + + if(this->last_statistics_tick + seconds(5) < time) { + this->last_statistics_tick = time; + this->connectionStatistics->tick(); + } +} + +void ConnectedClient::sendServerInit() { + Command command("initserver"); + + for(const auto& prop : this->server->properties().list_properties(property::FLAG_SERVER_VIEW, this->getType() == CLIENT_TEAMSPEAK ? property::FLAG_NEW : (uint16_t) 0)) { + command[prop.type().name] = prop.value(); + } + command["virtualserver_maxclients"] = 32; + + //Server stuff + command["client_talk_power"] = this->properties()[property::CLIENT_TALK_POWER].as(); + command["client_needed_serverquery_view_power"] = this->properties()[property::CLIENT_NEEDED_SERVERQUERY_VIEW_POWER].as(); + command["client_myteamspeak_id"] = this->properties()[property::CLIENT_MYTEAMSPEAK_ID].as(); + command["client_integrations"] = this->properties()[property::CLIENT_INTEGRATIONS].as(); + + if(ts::config::server::DefaultServerLicense == LicenseType::LICENSE_AUTOMATIC_INSTANCE){ + if(serverInstance->getVoiceServerManager()->usedSlots() <= 32) + command["lt"] = LicenseType::LICENSE_NONE; + else if(serverInstance->getVoiceServerManager()->usedSlots() <= 512) + command["lt"] = LicenseType::LICENSE_NPL; + else + command["lt"] = LicenseType::LICENSE_HOSTING; + } else if(ts::config::server::DefaultServerLicense == LicenseType::LICENSE_AUTOMATIC_SERVER){ + if(this->server->properties()[property::VIRTUALSERVER_MAXCLIENTS].as() <= 32) + command["lt"] = LicenseType::LICENSE_NONE; + else if(this->server->properties()[property::VIRTUALSERVER_MAXCLIENTS].as() <= 512) + command["lt"] = LicenseType::LICENSE_NPL; + else + command["lt"] = LicenseType::LICENSE_HOSTING; + } else + command["lt"] = ts::config::server::DefaultServerLicense; + command["pv"] = 6; //Protocol version + command["acn"] = this->getDisplayName(); + command["aclid"] = this->getClientId(); + this->sendCommand(command); +} + +bool ConnectedClient::handleCommandFull(Command& cmd, bool disconnectOnFail) { + system_clock::time_point start, end; + start = system_clock::now(); +#ifdef PKT_LOG_CMD + logTrace(this->getServerId(), "{}[Command][Client -> Server] Processing command: {}", CLIENT_STR_LOG_PREFIX, cmd.build(false)); +#endif + + CommandResult result; + try { + result = this->handleCommand(cmd); + } catch(invalid_argument& ex){ + debugMessage(this->getServerId(), "{}[Command] Execution throws invalid_argument exception ({}).", CLIENT_STR_LOG_PREFIX, ex.what()); + if(disconnectOnFail) { + this->disconnect("Invalid argument (" + string(ex.what()) + ")"); + return false; + } else { + result = {findError("parameter_convert"), "Invalid argument (" + string(ex.what()) + ")"}; + } + } catch (exception& ex) { + if(disconnectOnFail) { + this->disconnect("Error while command handling (" + string(ex.what()) + ")!"); + return false; + } else { + result = {findError("vs_critical"), "error while command handling (" + string(ex.what()) + ")"}; + } + } catch (...) { + this->disconnect("Error while command handling! (unknown)"); + return false; + } + bool generateReturnStatus = false; + if(result.type() == PERM_ERROR){ + generateReturnStatus = true; + } else if(cmd["return_code"].size() > 0) { + generateReturnStatus = !cmd["return_code"].string().empty(); + } + if(this->getType() == ClientType::CLIENT_QUERY) + generateReturnStatus = true; + + if (!result) { + generateReturnStatus = true; + + stringstream ss; + ss << "{"; + for(auto it = result.extraProperties.begin(); it != result.extraProperties.end();){ + ss << it->first << " = " << it->second; + if(++it != result.extraProperties.end()) + ss << ", "; + } + ss << "}" << endl; + logTrace(this->getServerId(), "{}[Command] Command {} with return code {} fails and returns error code {:#06x}. Properties: {}", CLIENT_STR_LOG_PREFIX, cmd.command(), cmd["return_code"].size() ? "\"" + cmd["return_code"].first().as() + "\"" : "", result.error.errorId, ss.str()); + } + + if(generateReturnStatus) + this->notifyError(result, cmd["return_code"].size() > 0 ? cmd["return_code"].first().as() : ""); + + if(!result && this->state == ConnectionState::INIT_HIGH) { + this->closeConnection(system_clock::now()); //Disconnect now + } + + for (const auto& handler : postCommandHandler) + handler(); + postCommandHandler.clear(); + end = system_clock::now(); + if(end - start > milliseconds(10)) { + if(end - start > milliseconds(100)) + logError(this->getServerId(), "Command handling of command {} needs {}ms. This could be an issue!", cmd.command(), duration_cast(end - start).count()); + else + logWarning(this->getServerId(), "Command handling of command {} needs {}ms.", cmd.command(), duration_cast(end - start).count()); + } + return true; +} + +std::shared_ptr ConnectedClient::resolveActiveBan(const std::string& ip_address) { + if(this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_client_ignore_bans, 1)) return nullptr; + + //Check if manager banned + auto banManager = serverInstance->banManager(); + shared_ptr banEntry = nullptr; + deque> entries; + + if (!banEntry) { + banEntry = banManager->findBanByName(this->server->getServerId(), this->getDisplayName()); + if(banEntry) + debugMessage(this->getServerId(), "{} Resolved name ban ({}). Record id {}, server id {}", + CLIENT_STR_LOG_PREFIX, + this->getDisplayName(), + banEntry->banId, + banEntry->serverId); + } + if (!banEntry) { + banEntry = banManager->findBanByUid(this->server->getServerId(), this->getUid()); + if(banEntry) + debugMessage(this->getServerId(), "{} Resolved uuid ban ({}). Record id {}, server id {}", + CLIENT_STR_LOG_PREFIX, + this->getUid(), + banEntry->banId, + banEntry->serverId); + } + if (!banEntry && !ip_address.empty()) { + banEntry = banManager->findBanByIp(this->server->getServerId(), ip_address); + if(banEntry) + debugMessage(this->getServerId(), "{} Resolved ip ban ({}). Record id {}, server id {}", + CLIENT_STR_LOG_PREFIX, + ip_address, + banEntry->banId, + banEntry->serverId); + } + auto vclient = dynamic_cast(this); + if(vclient) + if (!banEntry) { + banEntry = banManager->findBanByHwid(this->server->getServerId(), vclient->getHardwareId()); + if(banEntry) + debugMessage(this->getServerId(), "{} Resolved hwid ban ({}). Record id {}, server id {}", + CLIENT_STR_LOG_PREFIX, + vclient->getHardwareId(), + banEntry->banId, + banEntry->serverId); + } + + return banEntry; +} + +bool ConnectedClient::update_cached_permissions() { + auto values = this->permissionValues(permission::PERMTEST_ORDERED, permission::neededPermissions, shared_ptr(this->currentChannel)); /* copy the channel here so it does not change */ + auto updated = false; + + + { + lock_guard cached_lock(this->cached_permissions_lock); + + vector old_permissions; + old_permissions.reserve(this->cached_permissions.size()); + + for(const auto& value : this->cached_permissions) + old_permissions.push_back(value.first); + + for(const auto& value : values) { + auto value_it = cached_permissions.find(value.first); + if(value_it == cached_permissions.end()) { /* new entry */ + updated = true; + this->cached_permissions[value.first] = value.second; + continue; /* no need to remove that from old_permissions because it isn't there */ + } else if(value_it->second != value.second) { /* entry changed */ + updated = true; + value_it->second = value.second; + } + + { /* we've updated the value or verified it */ + auto old_it = find(old_permissions.begin(), old_permissions.end(), value.first); + if(old_it != old_permissions.end()) + old_permissions.erase(old_it); + } + } + + for(const auto& left : old_permissions) { + auto value_it = cached_permissions.find(left); + if(value_it != cached_permissions.end()) { + cached_permissions.erase(value_it); + updated = true; + } + } + } + + return updated; +} + +void ConnectedClient::sendTSPermEditorWarning() { + if(config::voice::warn_on_permission_editor) { + if(system_clock::now() - this->command_times.servergrouplist > milliseconds(1000)) return; + if(system_clock::now() - this->command_times.channelgrouplist > milliseconds(1000)) return; + + this->command_times.last_notify = system_clock::now(); + this->notifyClientPoke(this->server->serverRoot, config::messages::teamspeak_permission_editor); + } +} + +permission::v2::PermissionFlaggedValue ConnectedClient::calculate_permission_value(const ts::permission::PermissionType &permission, ts::ChannelId channel_id) { + if(channel_id == (this->currentChannel ? this->currentChannel->channelId() : 0) || channel_id == -1) { + std::lock_guard lock(this->cached_permissions_lock); + auto index = this->cached_permissions.find(permission); + if(index != this->cached_permissions.end()) + return {index->second, index->second != permNotGranted}; + } + + auto value = this->permissionValue(permission::PERMTEST_ORDERED, permission, nullptr); + return {value, value != permNotGranted}; +} \ No newline at end of file diff --git a/server/src/client/ConnectedClient.h b/server/src/client/ConnectedClient.h new file mode 100644 index 0000000..010745f --- /dev/null +++ b/server/src/client/ConnectedClient.h @@ -0,0 +1,626 @@ +#pragma once + +#include +#include +#include +#include "music/Song.h" +#include "../channel/ClientChannelView.h" +#include "DataClient.h" + +#define CLIENT_LOG_PREFIX_(this) "[" << this->getLoggingPeerIp() << ":" << this->getPeerPort() << "/" << this->getDisplayName() << "]" +#define CLIENT_LOG_PREFIX CLIENT_LOG_PREFIX_(this) + +#define CLIENT_STR_LOG_PREFIX_(this) (std::string("[") + this->getLoggingPeerIp() + ":" + std::to_string(this->getPeerPort()) + "/" + this->getDisplayName() + " | " + std::to_string(this->getClientId()) + "]") +#define CLIENT_STR_LOG_PREFIX CLIENT_STR_LOG_PREFIX_(this) + +#define CMD_REQ_SERVER \ +if(!this->server) return {findError("server_invalid_id"), "invalid bound server"} + +/* TODO: Play lock the server here with read? So the client dosn't get kicked within that moment */ +#define CMD_REF_SERVER(variable_name) \ +std::shared_ptr variable_name = this->getServer(); \ +if(!variable_name) return {findError("server_invalid_id"), "invalid bound server"} + +#define CMD_REQ_CHANNEL \ +if(!this->currentChannel) return {findError("channel_invalid_id"), "invalid bound channel"} + +#define CMD_RESET_IDLE \ +do { \ + this->resetIdleTime(); \ +} while(false) + +#define CMD_REQ_PARM(parm) \ +if(!cmd[0].has(parm)) return {findError("parameter_not_found"), parm} + +//the message here is show to the manager! +#define CMD_CHK_AND_INC_FLOOD_POINTS(num) \ +this->increaseFloodPoints(num); \ +if(this->shouldFloodBlock()) return {findError("ban_flooding"), "You reached the flood limit"}; + +#define CMD_CHK_PARM_COUNT(count) \ +if(cmd.bulkCount() != count) return {findError("parameter_invalid_count"), ""}; + + + +#define PERM_CHECK_CHANNEL_CR(pr, required, channel, req, cache) \ +do { \ + if(!this->permissionGranted(permission::PERMTEST_ORDERED, pr, required, channel, req, cache)) {\ + return ts::CommandResultPermissionError(pr); \ + }\ +} while(false) + +//If a permission is required it could not be -1 +#define PERM_CHECK_CHANNELR(pr, required, channel, req) PERM_CHECK_CHANNEL_CR(pr, required, channel, req, nullptr) + +//If a permission is required it could not be -1 +#define PERM_CHECK_CHANNEL(pr, required, channel) PERM_CHECK_CHANNELR(pr, required, channel, false) + +#define PERM_CHECKR(pr, required, req) PERM_CHECK_CHANNELR(pr, (required), nullptr, req) +#define PERM_CHECK(pr, required) PERM_CHECKR(pr, required, false) + +/* optional parameter required. By default true */ +#define CACHED_PERM_CHECK(permission_type, required, ...) \ +do { \ + if(!this->permission_granted(this->cached_permission_value(permission_type), required, #__VA_ARGS__)) \ + return CommandResultPermissionError{permission_type}; \ +} while(0) + +//p = permission | target_permission = channel permission | channel = target channel | requires a power + +#define PERM_CHECK_CHANNEL_NEEDED_CR(p, target_permission, channel, req, cache) \ +this->permissionGranted(permission::PERMTEST_ORDERED, p, (channel)->permissions()->getPermissionValue(permission::PERMTEST_ORDERED, target_permission, nullptr), channel, req, cache) + +#define PERM_CHECK_CHANNEL_NEEDEDR(p, target_permission, channel, req) PERM_CHECK_CHANNEL_NEEDED_CR(p, target_permission, channel, req, nullptr) + +#define PERM_CHECK_CHANNEL_NEEDED(p, target_permission, channel) PERM_CHECK_CHANNEL_NEEDEDR(p, target_permission, channel, true) + +namespace ts { + class GroupManager; + namespace connection { + class VoiceClientConnection; + } + + namespace server { + class TSServer; + class MusicClient; + class WebClient; + class MusicClient; + + struct ConnectionInfoData { + std::chrono::time_point timestamp; + std::map properties; + }; + + class ConnectedClient : public DataClient { + friend class TSServer; + friend class VoiceServer; + friend class VoiceClient; + friend class MusicClient; + friend class WebClient; + friend class WebControlServer; + friend class music::MusicBotManager; + friend class QueryServer; + friend class DataClient; + friend class SpeakingClient; + friend class connection::VoiceClientConnection; + friend class ts::GroupManager; + friend class ServerManager; + public: + explicit ConnectedClient(sql::SqlManager*, const std::shared_ptr& server); + ~ConnectedClient() override; + + ConnectionState connectionState(){ return this->state; } + std::string getLoggingPeerIp() { return config::server::disable_ip_saving || (this->server && this->server->disable_ip_saving()) ? "X.X.X.X" : this->getPeerIp(); } + std::string getPeerIp(){ return this->isAddressV4() ? net::to_string(this->getAddressV4()->sin_addr) : this->isAddressV6() ? net::to_string(this->getAddressV6()->sin6_addr) : "localhost"; } + uint16_t getPeerPort(){ return ntohs(this->isAddressV4() ? this->getAddressV4()->sin_port : this->isAddressV6() ? this->getAddressV6()->sin6_port : (uint16_t) 0); } + std::string getHardwareId(){ return properties()[property::CLIENT_HARDWARE_ID]; } + + //General connection stuff + bool isAddressV4() { return this->remote_address.ss_family == AF_INET; } + const sockaddr_in* getAddressV4(){ return (sockaddr_in*) &this->remote_address; } + bool isAddressV6() { return this->remote_address.ss_family == AF_INET6; } + const sockaddr_in6* getAddressV6(){ return (sockaddr_in6*) &this->remote_address; } + + virtual void sendCommand(const ts::Command& command, bool low = false) = 0; + + //General manager stuff + //FIXME cache the client id for speedup + virtual uint16_t getClientId() { return this->properties()[property::CLIENT_ID]; } + virtual void setClientId(uint16_t clId) { properties()[property::CLIENT_ID] = clId; } + + inline std::shared_ptr getChannel(){ return this->currentChannel; } + inline std::shared_ptr getServer(){ return this->server; } + inline ServerId getServerId(){ return this->server ? this->server->getServerId() : (ServerId) 0; } + + //bool channelSubscribed(const std::shared_ptr&); + /* if lock_channel == false then channel_lock must be write locked! */ + std::deque> subscribeChannel(const std::deque>& target, bool lock_channel, bool /* enforce */); + /* if lock_channel == false then channel_lock must be write locked! */ + std::deque> unsubscribeChannel(const std::deque>& target, bool lock_channel); + + bool isClientVisible(const std::shared_ptr&, bool /* lock channel lock */); + inline std::deque> getVisibleClients(bool lock_channel){ + std::shared_lock lock(this->channel_lock, std::defer_lock); + if(lock_channel) + lock.lock(); + return this->visibleClients; + } + + /** Notifies general stuff **/ + virtual bool notifyError(const CommandResult&, const std::string& retCode = ""); + + /** Notifies (after request) */ + bool sendNeededPermissions(bool /* force an update */); /* invoke this because it dosn't spam the client */ + virtual bool notifyClientNeededPermissions(); + virtual bool notifyServerGroupList(); + virtual bool notifyGroupPermList(const std::shared_ptr&, bool); + virtual bool notifyClientPermList(ClientDbId, const std::shared_ptr&, bool); + virtual bool notifyChannelGroupList(); + virtual bool notifyConnectionInfo(std::shared_ptr,std::shared_ptr); + virtual bool notifyChannelSubscribed(const std::deque> &); + virtual bool notifyChannelUnsubscribed(const std::deque> &); + + /** Notifies (without request) */ + //Group server + virtual bool notifyServerUpdated(std::shared_ptr); + //Group manager + virtual bool notifyClientPoke(std::shared_ptr invoker, std::string msg); + virtual bool notifyClientUpdated( + const std::shared_ptr &, + const std::deque> &, + bool lock_channel_tree + ); /* invalid client id causes error: invalid clientID */ + + virtual bool notifyPluginCmd(std::string name, std::string msg, std::shared_ptr); + //Group manager chat + virtual bool notifyClientChatComposing(const std::shared_ptr &); + virtual bool notifyClientChatClosed(const std::shared_ptr &); + virtual bool notifyTextMessage(ChatMessageMode mode, const std::shared_ptr &sender, uint64_t targetId, const std::string &textMessage); + inline void sendChannelMessage(const std::shared_ptr& sender, const std::string& textMessage){ + this->notifyTextMessage(ChatMessageMode::TEXTMODE_CHANNEL, sender, this->currentChannel ? this->currentChannel->channelId() : 0, textMessage); + } + //Group Client Groups + virtual bool notifyServerGroupClientAdd(const std::shared_ptr &invoker, const std::shared_ptr &client, const std::shared_ptr &group); + virtual bool notifyServerGroupClientRemove(std::shared_ptr invoker, std::shared_ptr client, std::shared_ptr group); + /* invalid client id causes error: invalid clientID */ + /* invalid channel id causes error: invalid channelID */ + /* an invalid channel or not a client's channel causes: invalid channelID */ + virtual bool notifyClientChannelGroupChanged( + const std::shared_ptr& invoker, + const std::shared_ptr& client, + const std::shared_ptr& /*channel*/, + const std::shared_ptr& /*inherited channel*/, + const std::shared_ptr& group, + bool lock_channel_tree /* server channel tree AND client tree must be at least read locked! */ + ); + //Group channel + virtual bool notifyChannelMoved(const std::shared_ptr &channel, ChannelId order, const std::shared_ptr &invoker); + virtual bool notifyChannelDescriptionChanged(std::shared_ptr channel); + virtual bool notifyChannelPasswordChanged(std::shared_ptr); + virtual bool notifyChannelEdited(std::shared_ptr, std::vector keys, ConnectedClient* invoker); /* clients channel tree must be at least read locked */ + + virtual bool notifyChannelHide(const std::deque &channels, bool lock_channel_tree); + virtual bool notifyChannelShow(const std::shared_ptr &channel, ChannelId orderId); /* client channel tree must be unique locked and server channel tree shared locked */ + virtual bool notifyChannelCreate(const std::shared_ptr &channel, ChannelId orderId, const std::shared_ptr &invoker); + virtual bool notifyChannelDeleted(const std::deque& /* channel_ids */, const std::shared_ptr& /* invoker */); + //Client view + virtual bool notifyClientEnterView( + const std::shared_ptr &client, + const std::shared_ptr &invoker, + const std::string& /* reason */, + const std::shared_ptr &to, + ViewReasonId reasonId, + const std::shared_ptr &from, + bool lock_channel_tree + ); + virtual bool notifyClientEnterView(const std::deque>& /* clients */, const ViewReasonSystemT& /* mode */); /* channel lock must be write locked */ + virtual bool notifyClientMoved( + const std::shared_ptr &client, + const std::shared_ptr &target_channel, + ViewReasonId reason, + std::string msg, + std::shared_ptr invoker, + bool lock_channel_tree + ); + virtual bool notifyClientLeftView( + const std::shared_ptr &client, + const std::shared_ptr &target_channel, + ViewReasonId reasonId, + const std::string& reasonMessage, + std::shared_ptr invoker, + bool lock_channel_tree + ); + virtual bool notifyClientLeftView( + const std::deque>& /* clients */, + const std::string& /* reason */, + bool /* lock channel view */, + const ViewReasonServerLeftT& /* mode */ + ); + + virtual bool notifyClientLeftViewKicked( + const std::shared_ptr &client, + const std::shared_ptr &target_channel, + const std::string& message, + std::shared_ptr invoker, + bool lock_channel_tree + ); + virtual bool notifyClientLeftViewBanned( + const std::shared_ptr &client, + const std::string& message, + std::shared_ptr invoker, + size_t length, + bool lock_channel_tree + ); + + virtual bool notifyMusicPlayerSongChange(const std::shared_ptr& bot, const std::shared_ptr& newEntry); + virtual bool notifyMusicQueueAdd(const std::shared_ptr& bot, const std::shared_ptr& entry, int index, const std::shared_ptr& invoker); + virtual bool notifyMusicQueueRemove(const std::shared_ptr& bot, const std::deque>& entry, const std::shared_ptr& invoker); + virtual bool notifyMusicQueueOrderChange(const std::shared_ptr& bot, const std::shared_ptr& entry, int order, const std::shared_ptr& invoker); + virtual bool notifyMusicPlayerStatusUpdate(const std::shared_ptr&); + + virtual bool closeConnection(const std::chrono::system_clock::time_point &timeout = std::chrono::system_clock::time_point()) = 0; + virtual bool disconnect(const std::string& reason) = 0; + + void resetIdleTime(); + void increaseFloodPoints(uint16_t); + bool shouldFloodBlock(); + + virtual bool ignoresFlood() { return !this->block_flood; } + std::shared_ptr requestConnectionInfo(const std::shared_ptr& /* receiver */); + + virtual void updateChannelClientProperties(bool /* lock channel tree */, bool /* notify our self */); + void updateTalkRights(permission::PermissionValue talk_power); + + virtual std::shared_ptr resolveActiveBan(const std::string& ip_address); + + inline std::shared_ptr getConnectionStatistics() { + return this->connectionStatistics; + } + + inline std::shared_ptr channel_view() { return this->channels; } + + inline std::shared_ptr ref() { return _this.lock(); } + + /* + * permission stuff + */ + inline permission::PermissionValue cached_permission_value(permission::PermissionType type) const { + std::lock_guard lock(this->cached_permissions_lock); + auto index = this->cached_permissions.find(type); + if(index != this->cached_permissions.end()) + return index->second; + + /* We're only caching permissions which are granted to reduce memory */ + //logError(this->getServerId(), "{} Looked up cached permission, which hasn't been cached!", CLIENT_STR_LOG_PREFIX); + return permNotGranted; + } + bool update_cached_permissions(); + permission::v2::PermissionFlaggedValue calculate_permission_value(const permission::PermissionType& /* permission type */, ChannelId /* target channel */); + protected: + std::weak_ptr _this; + + //General states + std::mutex state_lock; + ConnectionState state = ConnectionState::UNKNWON; + sockaddr_storage remote_address; + + std::shared_ptr currentChannel = nullptr; + bool allowedToTalk = false; + + threads::Mutex disconnectLock; + std::shared_mutex finalDisconnectLock; + + std::vector cached_server_groups{}; /* variable locked with channel_lock */ + GroupId cached_channel_group = 0; /* variable locked with channel_lock */ + + std::deque> visibleClients{}; /* variable locked with channel_lock */ + std::deque> mutedClients{}; /* variable locked with channel_lock */ + std::deque> openChats{}; /* variable locked with channel_lock */ + + std::chrono::system_clock::time_point lastNeededNotify; + std::shared_ptr lastNeededPermissionNotifyChannel = nullptr; + bool requireNeededPermissionResend = false; + + std::chrono::system_clock::time_point connectTimestamp; + std::chrono::system_clock::time_point lastOnlineTimestamp; + std::chrono::system_clock::time_point lastTransfareTimestamp; + std::chrono::system_clock::time_point idleTimestamp; + std::chrono::system_clock::time_point last_statistics_tick; + + struct { + std::mutex lock; + std::shared_ptr data; + std::chrono::system_clock::time_point data_age; + + std::deque> receiver; + std::chrono::system_clock::time_point last_requested; + } connection_info; + + struct { + std::chrono::system_clock::time_point servergrouplist; + std::chrono::system_clock::time_point channelgrouplist; + + std::chrono::system_clock::time_point last_notify; + std::chrono::milliseconds notify_timeout = std::chrono::seconds(60); + } command_times; + + std::shared_ptr connectionStatistics = nullptr; + + bool block_flood = true; + FloodPoints floodPoints = 0; + + std::shared_ptr channels; + std::shared_mutex channel_lock; + + std::mutex cached_permissions_lock; + std::map cached_permissions; /* contains all needed permissions which are set */ + #pragma pack(push, 1) + struct CachedPermission { + bool flag_skip : 1; /* could be enabled by server / channel or client group. If this flag is set we need no lookup for channel permissions */ + bool flag_value : 1; /* says if we have a value or not */ + permission::PermissionValue value; + }; + static_assert(sizeof(CachedPermission) == 5); + #pragma pack(pop) + + + permission::PermissionValue channels_view_power = permNotGranted; + permission::PermissionValue channels_ignore_view = permNotGranted; + + bool subscribeToAll = false; + + std::weak_ptr selectedBot; + std::weak_ptr subscribed_bot; + + virtual void tick(const std::chrono::system_clock::time_point &time); + //Locked by everything who has something todo with command handling + threads::Mutex command_lock; /* Note: This mutex must be recursive! */ + std::vector> postCommandHandler; + virtual bool handleCommandFull(Command&, bool disconnectOnFail = false); + virtual CommandResult handleCommand(Command&); + + CommandResult handleCommandServerGetVariables(Command&); + CommandResult handleCommandServerEdit(Command&); + + CommandResult handleCommandGetConnectionInfo(Command&); + CommandResult handleCommandSetConnectionInfo(Command&); + CommandResult handleCommandServerRequestConnectionInfo(Command&); + CommandResult handleCommandConnectionInfoAutoUpdate(Command&); + CommandResult handleCommandPermissionList(Command&); + CommandResult handleCommandPropertyList(Command&); + + CommandResult handleCommandServerGroupList(Command&); + + CommandResult handleCommandClientGetIds(Command&); + CommandResult handleCommandClientUpdate(Command&); + CommandResult handleCommandClientEdit(Command&); + CommandResult handleCommandClientEdit(Command&, const std::shared_ptr& /* target */); + CommandResult handleCommandClientMove(Command&); //TODO: Use cached permission values + CommandResult handleCommandClientGetVariables(Command&); + CommandResult handleCommandClientKick(Command&); + CommandResult handleCommandClientPoke(Command&); + + CommandResult handleCommandChannelSubscribe(Command&); + CommandResult handleCommandChannelSubscribeAll(Command&); + CommandResult handleCommandChannelUnsubscribe(Command&); + CommandResult handleCommandChannelUnsubscribeAll(Command&); + CommandResult handleCommandChannelCreate(Command&); + CommandResult handleCommandChannelDelete(Command&); + CommandResult handleCommandChannelEdit(Command&); + CommandResult handleCommandChannelGetDescription(Command&); + CommandResult handleCommandChannelMove(Command&); + CommandResult handleCommandChannelPermList(Command&); + CommandResult handleCommandChannelAddPerm(Command&); + CommandResult handleCommandChannelDelPerm(Command&); + + //Server group manager management + CommandResult handleCommandServerGroupCopy(Command&); + CommandResult handleCommandServerGroupAdd(Command&); + CommandResult handleCommandServerGroupRename(Command&); + CommandResult handleCommandServerGroupDel(Command&); + CommandResult handleCommandServerGroupClientList(Command&); + CommandResult handleCommandServerGroupDelClient(Command&); + CommandResult handleCommandServerGroupAddClient(Command&); + CommandResult handleCommandServerGroupPermList(Command&); + CommandResult handleCommandServerGroupAddPerm(Command&); + CommandResult handleCommandServerGroupDelPerm(Command&); + + CommandResult handleCommandServerGroupAutoAddPerm(Command&); + CommandResult handleCommandServerGroupAutoDelPerm(Command&); + + CommandResult handleCommandClientAddPerm(Command&); //TODO: Use cached permission values + CommandResult handleCommandClientDelPerm(Command&); //TODO: Use cached permission values + CommandResult handleCommandClientPermList(Command&); //TODO: Use cached permission values + + CommandResult handleCommandChannelClientAddPerm(Command&); //TODO: Use cached permission values + CommandResult handleCommandChannelClientDelPerm(Command&); //TODO: Use cached permission values + CommandResult handleCommandChannelClientPermList(Command&); //TODO: Use cached permission values + + CommandResult handleCommandChannelGroupAdd(Command&); + CommandResult handleCommandChannelGroupCopy(Command&); + CommandResult handleCommandChannelGroupRename(Command&); + CommandResult handleCommandChannelGroupDel(Command&); + CommandResult handleCommandChannelGroupList(Command&); + CommandResult handleCommandChannelGroupClientList(Command&); + CommandResult handleCommandChannelGroupPermList(Command&); + CommandResult handleCommandChannelGroupAddPerm(Command&); + CommandResult handleCommandChannelGroupDelPerm(Command&); + CommandResult handleCommandSetClientChannelGroup(Command&); + + CommandResult handleCommandSendTextMessage(Command&); + CommandResult handleCommandClientChatComposing(Command&); + CommandResult handleCommandClientChatClosed(Command&); + + //File transfare commands + CommandResult handleCommandFTGetFileList(Command&); //TODO: Use cached permission values + CommandResult handleCommandFTCreateDir(Command&); //TODO: Use cached permission values + CommandResult handleCommandFTDeleteFile(Command&); //TODO: Use cached permission values + CommandResult handleCommandFTInitUpload(Command&); //TODO: Use cached permission values + CommandResult handleCommandFTInitDownload(Command&); //TODO: Use cached permission values + CommandResult handleCommandFTGetFileInfo(Command&); //TODO: Use cached permission values + //CMD_TODO handleCommandFTGetFileInfo -> 5 points + //CMD_TODO handleCommandFTStop -> 5 points + //CMD_TODO handleCommandFTRenameFile -> 5 points + //CMD_TODO handleCommandFTList -> 5 points + + CommandResult handleCommandBanList(Command&); + CommandResult handleCommandBanAdd(Command&); + CommandResult handleCommandBanEdit(Command&); + CommandResult handleCommandBanClient(Command&); + CommandResult handleCommandBanDel(Command&); + CommandResult handleCommandBanDelAll(Command&); + CommandResult handleCommandBanTriggerList(Command&); + + CommandResult handleCommandTokenList(Command&); + CommandResult handleCommandTokenAdd(Command&); + CommandResult handleCommandTokenUse(Command&); + CommandResult handleCommandTokenDelete(Command&); + + CommandResult handleCommandClientDbList(Command&); + CommandResult handleCommandClientDBEdit(Command&); + CommandResult handleCommandClientDbInfo(Command&); + CommandResult handleCommandClientDBDelete(Command&); + CommandResult handleCommandClientDBFind(Command&); + + CommandResult handleCommandPluginCmd(Command&); + + CommandResult handleCommandClientMute(Command&); + CommandResult handleCommandClientUnmute(Command&); + + CommandResult handleCommandComplainAdd(Command&); + CommandResult handleCommandComplainList(Command&); + CommandResult handleCommandComplainDel(Command&); + CommandResult handleCommandComplainDelAll(Command&); + + CommandResult handleCommandClientGetDBIDfromUID(Command&); + CommandResult handleCommandClientGetNameFromDBID(Command&); + CommandResult handleCommandClientGetNameFromUid(Command&); + CommandResult handleCommandClientGetUidFromClid(Command&); + + //Original from query but still reachable for all + CommandResult handleCommandClientList(Command&); + CommandResult handleCommandWhoAmI(Command&); + CommandResult handleCommandServerGroupsByClientId(Command &); //Maybe not query? + + CommandResult handleCommandClientFind(Command&); + CommandResult handleCommandClientInfo(Command&); + CommandResult handleCommandVersion(Command&); + + CommandResult handleCommandVerifyChannelPassword(Command&); + CommandResult handleCommandVerifyServerPassword(Command&); + + CommandResult handleCommandMessageList(Command&); + CommandResult handleCommandMessageAdd(Command&); + CommandResult handleCommandMessageGet(Command&); + CommandResult handleCommandMessageUpdateFlag(Command&); + CommandResult handleCommandMessageDel(Command&); + + CommandResult handleCommandPermGet(Command&); + CommandResult handleCommandPermIdGetByName(Command&); + CommandResult handleCommandPermFind(Command&); + CommandResult handleCommandPermOverview(Command&); + + CommandResult handleCommandChannelFind(Command&); //TODO: Use cached permission values + CommandResult handleCommandChannelInfo(Command&); //TODO: Use cached permission values + + CommandResult handleCommandMusicBotCreate(Command&); //TODO: Use cached permission values + CommandResult handleCommandMusicBotDelete(Command&); //TODO: Use cached permission values + CommandResult handleCommandMusicBotSetSubscription(Command&); //TODO: Use cached permission values + + CommandResult handleCommandMusicBotPlayerInfo(Command&); //TODO: Use cached permission values + CommandResult handleCommandMusicBotPlayerAction(Command&); //TODO: Use cached permission values + + CommandResult handleCommandMusicBotQueueList(Command&); //TODO: Use cached permission values + CommandResult handleCommandMusicBotQueueAdd(Command&); //TODO: Use cached permission values + CommandResult handleCommandMusicBotQueueRemove(Command&); //TODO: Use cached permission values + CommandResult handleCommandMusicBotQueueReorder(Command&); //TODO: Use cached permission values + + CommandResult handleCommandMusicBotPlaylistAssign(Command&); + + /* playlist management */ + CommandResult handleCommandPlaylistList(Command&); + CommandResult handleCommandPlaylistCreate(Command&); + CommandResult handleCommandPlaylistDelete(Command&); + CommandResult handleCommandPlaylistPermList(Command&); + CommandResult handleCommandPlaylistAddPerm(Command&); + CommandResult handleCommandPlaylistDelPerm(Command&); + + /* playlist properties */ + CommandResult handleCommandPlaylistInfo(Command&); + CommandResult handleCommandPlaylistEdit(Command&); + + CommandResult handleCommandPlaylistSongList(Command&); + CommandResult handleCommandPlaylistSongAdd(Command&); + CommandResult handleCommandPlaylistSongReorder(Command&); + CommandResult handleCommandPlaylistSongRemove(Command&); + + CommandResult handleCommandPermReset(Command&); //TODO: Use cached permission values + + CommandResult handleCommandHelp(Command&); //TODO: Use cached permission values + + CommandResult handleCommandUpdateMyTsId(Command&); + CommandResult handleCommandUpdateMyTsData(Command&); + /// + /// With a whisper list set a client can talk to the specified clients and channels bypassing the standard rule that voice is only transmitted to the current channel. Whisper lists can be defined for individual clients. + /// + /// + /// To control which client is allowed to whisper to own client, the Library implements an internal whisper whitelist mechanism. When a client receives a whisper while the whispering client has not yet been added to the whisper allow list, the receiving client gets the -Event. Note that whisper voice data is not received until the sending client is added to the receivers whisper allow list. + /// + /// array of channels to whisper to, set to null to disable + /// array of clients to whisper to, set to null to disable + //CMD_TODO handleCommandSetWhisperlist + + //CMD_TODO handleCommandServerTempPasswordList + //CMD_TODO handleCommandServerTempPasswordDel + //CMD_TODO handleCommandServerTempPasswordAdd -> servertemppasswordadd pw=PWD desc=DES duration=1200 (20 min) tcid=4 tcpw=asdasd (channel password) + + //Legacy, for TeamSpeak 3 + CommandResult handleCommandClientSetServerQueryLogin(Command&); + + CommandResult handleCommandQueryList(Command&); + CommandResult handleCommandQueryRename(Command&); + CommandResult handleCommandQueryCreate(Command&); + CommandResult handleCommandQueryDelete(Command&); + CommandResult handleCommandQueryChangePassword(Command&); + + CommandResult handleCommandLogView(Command&); + //CMD_TODO handleCommandLogAdd + + //handleCommandClientSiteReport() -> return findError(0x00) + //handleCommandChannelCreatePrivate() -> return findError(0x02) + //handleCommandCustome_Unknown_Command() -> return findError(0x100) + + CommandResult handleCommandDummy_IpChange(Command&); + //handleCommandDummy_NewIp + //handleCommandDummy_ConnectFailed + //handleCommandDummy_ConnectionLost + + //Not needed - completly useless + //CMD_TODO handleCommandCustomInfo + //CMD_TODO handleCommandCustomSearch + //CMD_TODO serverquerycmd + + void sendChannelList(bool lock_channel_tree); + void sendServerInit(); + void sendTSPermEditorWarning(); + void sendChannelDescription(const std::shared_ptr&, bool lock_tree); + + bool handleTextMessage(ChatMessageMode, std::string, const std::shared_ptr& /* sender target */); + + typedef std::function& /* sender */, const std::string& /* message */)> handle_text_command_fn_t; + bool handle_text_command( + ChatMessageMode, + const std::string& /* key */, + const std::deque& /* arguments */, + const handle_text_command_fn_t& /* send function */, + const std::shared_ptr& /* sender target */ + ); + + inline std::string notify_response_command(const std::string_view& notify) { + if(this->getExternalType() == ClientType::CLIENT_TEAMSPEAK) + return std::string(notify); + return ""; + } + }; + } +} \ No newline at end of file diff --git a/server/src/client/ConnectedClientCommandHandler.cpp b/server/src/client/ConnectedClientCommandHandler.cpp new file mode 100644 index 0000000..7ee1e55 --- /dev/null +++ b/server/src/client/ConnectedClientCommandHandler.cpp @@ -0,0 +1,7220 @@ +#include + +#include +#include +#include +#include +#include "../build.h" +#include "ConnectedClient.h" +#include "InternalClient.h" +#include "../server/file/FileServer.h" +#include "../server/VoiceServer.h" +#include "voice/VoiceClient.h" +#include "PermissionManager.h" +#include "../InstanceHandler.h" +#include "../server/QueryServer.h" +#include "file/FileClient.h" +#include "music/MusicClient.h" +#include "query/QueryClient.h" +#include "../weblist/WebListManager.h" +#include +#include +#include + +#include +#include +#include +#include +#include +#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::token; + +extern ts::server::InstanceHandler *serverInstance; + +#define QUERY_PASSWORD_LENGTH 12 + +#define PARSE_PERMISSION(cmd) \ +permission::PermissionType permType = permission::PermissionType::unknown; \ +bool grant = false; \ +if (cmd[index].has("permid")) { \ + permType = cmd[index]["permid"].as(); \ + if ((permType & PERM_ID_GRANT) != 0) { \ + grant = true; \ + permType &= ~PERM_ID_GRANT; \ + } \ +} else if (cmd[index].has("permsid")) { \ + auto resv = permission::resolvePermissionData(cmd[index]["permsid"].as()); \ + permType = resv->type; \ + if (resv->grantName() == cmd[index]["permsid"].as()) grant = true; \ +} \ +if (permission::resolvePermissionData(permType)->type == permission::PermissionType::unknown) { \ + if (conOnError) continue; \ + return {findError("parameter_invalid"), "could not resolve permission " + (cmd[index].has("permid") ? cmd[index]["permid"].as() : cmd[index]["permsid"].as())}; \ +} + +#define CHANNEL_PERM_TEST_INIT \ +auto current_channel = channel == this->getChannel() + +#define CHANNEL_PERM_TEST(permission_type, required, enfore_required) \ +do { \ + if(current_channel) \ + PERM_CHECK_CHANNELR(permission_type, required, channel, enfore_required); \ + else \ + PERM_CHECK_CHANNELR(permission_type, required, channel, enfore_required); \ +} while(0) + +#define RESOLVE_CHANNEL_R(command, force) \ +auto channel_tree = this->server ? this->server->channelTree : serverInstance->getChannelTree().get();\ +shared_lock channel_tree_read_lock(this->server ? this->server->channel_tree_lock : serverInstance->getChannelTreeLock());\ +auto channel_id = command.as(); \ +auto l_channel = channel_id ? channel_tree->findLinkedChannel(command.as()) : nullptr; \ +if (!l_channel && (channel_id != 0 || force)) return {findError("channel_invalid_id"), "Cant resolve channel"}; \ + +#define RESOLVE_CHANNEL_W(command, force) \ +auto channel_tree = this->server ? this->server->channelTree : serverInstance->getChannelTree().get();\ +unique_lock channel_tree_write_lock(this->server ? this->server->channel_tree_lock : serverInstance->getChannelTreeLock());\ +auto channel_id = command.as(); \ +auto l_channel = channel_id ? channel_tree->findLinkedChannel(command.as()) : nullptr; \ +if (!l_channel && (channel_id != 0 || force)) return {findError("channel_invalid_id"), "Cant resolve channel"}; \ + +/* the "newest" channel permission access test */ +#define CHANNEL_PERMISSION_TEST(permission_type, permission_needed_type, _channel, permission_required) \ +do { \ + auto permission_granted = this->calculate_permission_value(permission_type, _channel->channelId()); \ + if(!channel->permission_granted(permission_needed_type, permission_granted, permission_required)) \ + return CommandResultPermissionError{permission_type}; \ +} while(0) + +/* the "newest" group permission access test */ +#define GROUP_PERMISSION_TEST(permission_type, permission_needed_type, _group, permission_required) \ +do { \ + auto permission_granted = this->calculate_permission_value(permission_type, 0); \ + if(!_group->permission_granted(permission_needed_type, permission_granted, permission_required)) \ + return CommandResultPermissionError{permission_type}; \ +} while(0) + +inline bool permission_require_granted_value(permission::PermissionType type) { + switch (type) { + case permission::i_permission_modify_power: + case permission::i_channel_group_member_add_power: + case permission::i_channel_group_member_remove_power: + case permission::i_channel_group_modify_power: + + case permission::i_server_group_member_add_power: + case permission::i_server_group_member_remove_power: + case permission::i_server_group_modify_power: + + case permission::i_displayed_group_member_add_power: + case permission::i_displayed_group_member_remove_power: + case permission::i_displayed_group_modify_power: + + case permission::i_channel_permission_modify_power: + case permission::i_client_permission_modify_power: + + case permission::i_client_needed_kick_from_server_power: + case permission::i_client_needed_kick_from_channel_power: + case permission::i_client_kick_from_channel_power: + case permission::i_client_kick_from_server_power: + return true; + default: + return false; + } +} + +inline bool permission_is_group_property(permission::PermissionType type) { + switch (type) { + case permission::i_icon_id: + case permission::i_group_show_name_in_tree: + case permission::i_group_sort_id: + case permission::b_group_is_permanent: + case permission::i_displayed_group_needed_modify_power: + case permission::i_displayed_group_needed_member_add_power: + case permission::i_displayed_group_needed_member_remove_power: + return true; + default: + return false; + } +} + +inline bool permission_is_client_property(permission::PermissionType type) { + switch (type) { + case permission::i_icon_id: + case permission::i_client_talk_power: + case permission::i_client_max_idletime: + case permission::i_group_sort_id: + case permission::i_channel_view_power: + case permission::b_channel_ignore_view_power: + case permission::b_client_is_priority_speaker: + return true; + default: + return false; + } +} + +CommandResult 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") return this->handleCommandServerGroupAddClient(cmd); + else if (command == "servergroupdelclient") 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") return this->handleCommandFTInitUpload(cmd); + else if (command == "ftinitdownload") return this->handleCommandFTInitDownload(cmd); + else if (command == "ftgetfileinfo") return this->handleCommandFTGetFileInfo(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 == "tokenlist" || command == "privilegekeylist") return this->handleCommandTokenList(cmd); + else if (command == "tokenadd" || command == "privilegekeyadd") return this->handleCommandTokenAdd(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 == "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 == "playlistpermlist") return this->handleCommandPlaylistPermList(cmd); + else if (command == "playlistaddperm") return this->handleCommandPlaylistAddPerm(cmd); + else if (command == "playlistdelperm") return this->handleCommandPlaylistDelPerm(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 == "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); + + if (this->getType() == ClientType::CLIENT_QUERY) return CommandResult::NotImplemented; //Dont log query invalid commands + if (this->getType() == ClientType::CLIENT_TEAMSPEAK) + if (command.empty() || command.find_first_not_of(' ') == -1) { + if (!this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_client_allow_invalid_packet, 1, this->currentChannel)) + ((VoiceClient *) this)->disconnect(VREASON_SERVER_KICK, config::messages::kick_invalid_command, this->server ? this->server->serverAdmin : static_pointer_cast(serverInstance->getInitalServerAdmin()), true); + } + + logError(this->getServerId(), "Missing command '{}'", command); + return CommandResult::NotImplemented; +} + +CommandResult ConnectedClient::handleCommandServerGetVariables(Command &cmd) { + CMD_REQ_SERVER; + this->notifyServerUpdated(_this.lock()); + return CommandResult::Success; +} + +#define SERVEREDIT_CHK_PROP(name, perm, type)\ +else if(key == name) { \ + if(!permissionGranted(permission::PERMTEST_HIGHEST, perm, 1, nullptr, true, cache, target_server, true)) return CommandResultPermissionError(perm); \ + if(toApplay.count(key) == 0) toApplay[key] = cmd[key].as(); \ + if(!cmd[0][key].castable()) return {findError("parameter_invalid"), "Invalid type for " + key}; + +#define SERVEREDIT_CHK_PROP_CACHED(name, perm, type)\ +else if(key == name) { \ + if(!this->permission_granted(this->cached_permission_value(perm), 1)) return CommandResultPermissionError(perm); \ + if(toApplay.count(key) == 0) toApplay[key] = cmd[key].as(); \ + if(!cmd[0][key].castable()) return {findError("parameter_invalid"), "Invalid type for " + key}; + +#define SERVEREDIT_CHK_PROP2(name, perm, type_a, type_b)\ +else if(key == name) { \ + if(!permissionGranted(permission::PERMTEST_HIGHEST, perm, 1, nullptr, true, cache, target_server, true)) return CommandResultPermissionError(perm); \ + if(toApplay.count(key) == 0) toApplay[key] = cmd[key].as(); \ + if(!cmd[0][key].castable() && !!cmd[0][key].castable()) return {findError("parameter_invalid"), "Invalid type for " + key}; + +CommandResult ConnectedClient::handleCommandServerEdit(Command &cmd) { + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + if (cmd[0].has("sid") && this->getServerId() != cmd["sid"].as()) + return {findError("server_invalid_id"), "Invalid server id! You can just edit the server where your're bound on"}; + + auto target_server = this->server; + if(cmd[0].has("sid")) { + target_server = serverInstance->getVoiceServerManager()->findServerById(cmd["sid"]); + if(!target_server && cmd["sid"].as() != 0) return {findError("server_invalid_id")}; + } + + auto cache = make_shared(); + map toApplay; + for (auto &key : cmd[0].keys()) { + if (key == "sid") continue; + SERVEREDIT_CHK_PROP_CACHED("virtualserver_name", permission::b_virtualserver_modify_name, string) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_name_phonetic", permission::b_virtualserver_modify_name, string) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_maxclients", permission::b_virtualserver_modify_maxclients, size_t) + if (cmd["virtualserver_maxclients"].as() > 1024) + return {findError("accounting_slot_limit_reached"), "Do you really need more that 1024 slots?"}; + } SERVEREDIT_CHK_PROP_CACHED("virtualserver_reserved_slots", permission::b_virtualserver_modify_reserved_slots, size_t) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_icon_id", permission::b_virtualserver_modify_icon_id, int64_t) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_channel_temp_delete_delay_default", permission::b_virtualserver_modify_channel_temp_delete_delay_default, ChannelId) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_codec_encryption_mode", permission::b_virtualserver_modify_codec_encryption_mode, int) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_default_server_group", permission::b_virtualserver_modify_default_servergroup, GroupId) + if(target_server) { + auto group = target_server->groups->findGroup(cmd["virtualserver_default_server_group"].as()); + if (!group || group->target() != GROUPTARGET_SERVER) return {findError("parameter_invalid"), "Invalid default server group!"}; + } + } SERVEREDIT_CHK_PROP_CACHED("virtualserver_default_channel_group", permission::b_virtualserver_modify_default_channelgroup, GroupId) + if(target_server) { + auto group = target_server->groups->findGroup(cmd["virtualserver_default_channel_group"].as()); + if (!group || group->target() != GROUPTARGET_CHANNEL) return {findError("parameter_invalid"), "Invalid default channel group!"}; + } + } SERVEREDIT_CHK_PROP_CACHED("virtualserver_default_channel_admin_group", permission::b_virtualserver_modify_default_channeladmingroup, GroupId) + if(target_server) { + auto group = target_server->groups->findGroup(cmd["virtualserver_default_channel_admin_group"].as()); + if (!group || group->target() != GROUPTARGET_CHANNEL) return {findError("parameter_invalid"), "Invalid default channel admin group!"}; + } + } SERVEREDIT_CHK_PROP_CACHED("virtualserver_default_music_group", permission::b_virtualserver_modify_default_musicgroup, GroupId) + if(target_server) { + auto group = target_server->groups->findGroup(cmd["virtualserver_default_music_group"].as()); + if (!group || group->target() != GROUPTARGET_SERVER) return {findError("parameter_invalid"), "Invalid default music group!"}; + } + } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_priority_speaker_dimm_modificator", permission::b_virtualserver_modify_priority_speaker_dimm_modificator, float) } + + SERVEREDIT_CHK_PROP_CACHED("virtualserver_port", permission::b_virtualserver_modify_port, uint16_t) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_host", permission::b_virtualserver_modify_host, string) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_web_host", permission::b_virtualserver_modify_port, uint16_t) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_web_port", permission::b_virtualserver_modify_host, string) } + + SERVEREDIT_CHK_PROP_CACHED("virtualserver_hostbanner_url", permission::b_virtualserver_modify_hostbanner, string) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_hostbanner_gfx_url", permission::b_virtualserver_modify_hostbanner, string) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_hostbanner_gfx_interval", permission::b_virtualserver_modify_hostbanner, int) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_hostbanner_mode", permission::b_virtualserver_modify_hostbanner, int) } + + SERVEREDIT_CHK_PROP_CACHED("virtualserver_hostbutton_tooltip", permission::b_virtualserver_modify_hostbutton, string) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_hostbutton_url", permission::b_virtualserver_modify_hostbutton, string) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_hostbutton_gfx_url", permission::b_virtualserver_modify_hostbutton, string) } + + SERVEREDIT_CHK_PROP_CACHED("virtualserver_hostmessage", permission::b_virtualserver_modify_hostmessage, string) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_hostmessage_mode", permission::b_virtualserver_modify_hostmessage, int) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_welcomemessage", permission::b_virtualserver_modify_welcomemessage, string) } + + SERVEREDIT_CHK_PROP_CACHED("virtualserver_weblist_enabled", permission::b_virtualserver_modify_weblist, bool) + if (target_server && target_server->running()) { + if (cmd["virtualserver_weblist_enabled"].as()) + serverInstance->getWebList()->enable_report(target_server); + else + serverInstance->getWebList()->disable_report(target_server); + debugMessage(string() + "Changed weblist state to -> " + (cmd["virtualserver_weblist_enabled"].as() ? "activated" : "disabled")); + } + } SERVEREDIT_CHK_PROP_CACHED("virtualserver_needed_identity_security_level", permission::b_virtualserver_modify_needed_identity_security_level, int) } + + SERVEREDIT_CHK_PROP_CACHED("virtualserver_antiflood_points_tick_reduce", permission::b_virtualserver_modify_antiflood, uint64_t) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_antiflood_points_needed_command_block", permission::b_virtualserver_modify_antiflood, uint64_t) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_antiflood_points_needed_ip_block", permission::b_virtualserver_modify_antiflood, uint64_t) } + + SERVEREDIT_CHK_PROP_CACHED("virtualserver_complain_autoban_count", permission::b_virtualserver_modify_complain, uint64_t) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_complain_autoban_time", permission::b_virtualserver_modify_complain, uint64_t) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_complain_remove_time", permission::b_virtualserver_modify_complain, uint64_t) } + + SERVEREDIT_CHK_PROP_CACHED("virtualserver_autostart", permission::b_virtualserver_modify_autostart, bool) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_min_clients_in_channel_before_forced_silence", permission::b_virtualserver_modify_channel_forced_silence, int) } + + SERVEREDIT_CHK_PROP_CACHED("virtualserver_log_client", permission::b_virtualserver_modify_log_settings, bool) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_log_query", permission::b_virtualserver_modify_log_settings, bool) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_log_channel", permission::b_virtualserver_modify_log_settings, bool) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_log_permissions", permission::b_virtualserver_modify_log_settings, bool) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_log_server", permission::b_virtualserver_modify_log_settings, bool) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_log_filetransfer", permission::b_virtualserver_modify_log_settings, bool) } + + SERVEREDIT_CHK_PROP("virtualserver_min_client_version", permission::b_virtualserver_modify_min_client_version, uint64_t) } + SERVEREDIT_CHK_PROP("virtualserver_min_android_version", permission::b_virtualserver_modify_min_client_version, uint64_t) } + SERVEREDIT_CHK_PROP("virtualserver_min_ios_version", permission::b_virtualserver_modify_min_client_version, uint64_t) } + + + SERVEREDIT_CHK_PROP_CACHED("virtualserver_music_bot_limit", permission::b_virtualserver_modify_music_bot_limit, int) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_flag_password", permission::b_virtualserver_modify_password, bool) + if (cmd["virtualserver_flag_password"].as() && !cmd[0].has("virtualserver_password")) + return {findError("parameter_invalid"), "Invalid password flag"}; + } SERVEREDIT_CHK_PROP_CACHED("virtualserver_password", permission::b_virtualserver_modify_password, string) + if(cmd["virtualserver_password"].string().empty()) { + toApplay["virtualserver_flag_password"] = "0"; + } else { + toApplay["virtualserver_flag_password"] = "1"; + if(this->getType() == CLIENT_QUERY) + toApplay["virtualserver_password"] = base64::encode(digest::sha1(cmd["virtualserver_password"].string())); + } + } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_default_client_description", permission::b_virtualserver_modify_default_messages, string) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_default_channel_description", permission::b_virtualserver_modify_default_messages, string) } + SERVEREDIT_CHK_PROP_CACHED("virtualserver_default_channel_topic", permission::b_virtualserver_modify_default_messages, string) } + SERVEREDIT_CHK_PROP2("virtualserver_max_download_total_bandwidth", permission::b_virtualserver_modify_ft_settings, uint64_t, int64_t) + if(cmd["virtualserver_max_download_total_bandwidth"].string().find('-') == string::npos) + toApplay["virtualserver_max_download_total_bandwidth"] = to_string((int64_t) cmd["virtualserver_max_download_total_bandwidth"].as()); + else + toApplay["virtualserver_max_download_total_bandwidth"] = to_string(cmd["virtualserver_max_download_total_bandwidth"].as()); + } + SERVEREDIT_CHK_PROP2("virtualserver_max_upload_total_bandwidth", permission::b_virtualserver_modify_ft_settings, uint64_t, int64_t) + if(cmd["virtualserver_max_upload_total_bandwidth"].string().find('-') == string::npos) + toApplay["virtualserver_max_upload_total_bandwidth"] = to_string((int64_t) cmd["virtualserver_max_upload_total_bandwidth"].as()); + else + toApplay["virtualserver_max_upload_total_bandwidth"] = to_string(cmd["virtualserver_max_upload_total_bandwidth"].as()); + } + SERVEREDIT_CHK_PROP2("virtualserver_download_quota", permission::b_virtualserver_modify_ft_quotas, uint64_t, int64_t) + if(cmd["virtualserver_download_quota"].string().find('-') == string::npos) + toApplay["virtualserver_download_quota"] = to_string((int64_t) cmd["virtualserver_download_quota"].as()); + else + toApplay["virtualserver_download_quota"] = to_string(cmd["virtualserver_download_quota"].as()); + } + SERVEREDIT_CHK_PROP2("virtualserver_upload_quota", permission::b_virtualserver_modify_ft_quotas, uint64_t, int64_t) + if(cmd["virtualserver_upload_quota"].string().find('-') == string::npos) + toApplay["virtualserver_upload_quota"] = to_string((int64_t) cmd["virtualserver_upload_quota"].as()); + else + toApplay["virtualserver_upload_quota"] = to_string(cmd["virtualserver_upload_quota"].as()); + } + else { + logError(target_server ? target_server->getServerId() : 0, "Client " + this->getDisplayName() + " tried to change a not existing server properties. (" + key + ")"); + //return CommandResult::NotImplemented; + } + } + + std::deque keys; + bool group_update = false; + for (const auto& elm : toApplay) { + auto info = property::impl::info(elm.first); + if(*info == property::VIRTUALSERVER_UNDEFINED) { + logCritical(target_server ? target_server->getServerId() : 0, "Missing server property " + elm.first); + continue; + } + + if(!info->validate_input(elm.second)) { + logError(target_server ? target_server->getServerId() : 0, "Client " + this->getDisplayName() + " tried to change a property to an invalid value. (Value: '" + elm.second + "', Property: '" + info->name + "')"); + continue; + } + if(target_server) + target_server->properties()[info] = elm.second; + else + (*serverInstance->getDefaultServerProperties())[info] = elm.second; + keys.push_back(elm.first); + + group_update |= *info == property::VIRTUALSERVER_DEFAULT_SERVER_GROUP || *info == property::VIRTUALSERVER_DEFAULT_CHANNEL_GROUP || *info == property::VIRTUALSERVER_DEFAULT_MUSIC_GROUP; + } + + if(target_server) { + if (group_update) + target_server->forEachClient([&](const shared_ptr& client) { + if(target_server->notifyClientPropertyUpdates(client, target_server->groups->update_server_group_property(client, true))) { + if(client->update_cached_permissions()) /* update cached calculated permissions */ + client->sendNeededPermissions(false); /* cached permissions had changed, notify the client */ + } + }); + + if (!keys.empty()) + target_server->notifyServerEdited(_this.lock(), keys); + } + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientGetVariables(Command &cmd) { + CMD_REQ_SERVER; + auto client = this->server->findClient(cmd["clid"].as()); + shared_lock tree_lock(this->channel_lock); + + if (!client || (client != this && !this->isClientVisible(client, false))) + return {findError("client_invalid_id"), ""}; + + deque> props; + for (auto &prop : client->properties()->list_properties(property::FLAG_CLIENT_VARIABLE, this->getType() == CLIENT_TEAMSPEAK ? property::FLAG_NEW : (uint16_t) 0)) { + props.push_back(property::info((property::ClientProperties) prop.type().property_index)); + } + + this->notifyClientUpdated(client, props, false); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientKick(Command &cmd) { + CMD_REQ_SERVER; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + auto client = this->server->findClient(cmd["clid"].as()); + if (!client) return {findError("client_invalid_id"), "invalid client id"}; + if (client->getType() == CLIENT_MUSIC) return {findError("client_invalid_type"), "You cant kick a music bot!"}; + std::shared_ptr targetChannel = nullptr; + auto type = cmd["reasonid"].as(); + if (type == ViewReasonId::VREASON_CHANNEL_KICK) { + auto channel = client->currentChannel; + PERM_CHECK_CHANNELR(permission::i_client_kick_from_channel_power, client->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_needed_kick_from_channel_power, channel), channel, true); + targetChannel = this->server->channelTree->getDefaultChannel(); + } else if (type == ViewReasonId::VREASON_SERVER_KICK) { + auto channel = client->currentChannel; + PERM_CHECK_CHANNELR(permission::i_client_kick_from_server_power, client->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_needed_kick_from_server_power, channel), channel, true); + targetChannel = nullptr; + } else return CommandResult::NotImplemented; + + if (targetChannel) { + this->server->notify_client_kick(client, this->ref(), cmd["reasonmsg"].as(), targetChannel); + } else { + this->server->notify_client_kick(client, this->ref(), cmd["reasonmsg"].as(), nullptr); + client->closeConnection(system_clock::now() + seconds(1)); + } + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandChannelGetDescription(Command &cmd) { + CMD_CHK_AND_INC_FLOOD_POINTS(0); + RESOLVE_CHANNEL_R(cmd["cid"], true); + auto channel = dynamic_pointer_cast(l_channel->entry); + assert(channel); + + if(!this->permission_granted(this->permissionValue(permission::b_channel_ignore_description_view_power, channel), 1, true)) { + CHANNEL_PERMISSION_TEST(permission::i_channel_description_view_power, permission::i_channel_needed_description_view_power, channel, false); + } + + this->sendChannelDescription(channel, true); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandGetConnectionInfo(Command &cmd) { + CMD_REQ_SERVER; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + auto client = this->server->findClient(cmd["clid"].as()); + if (!client) return {findError("client_invalid_id"), "invalid client id"}; + + auto info = client->requestConnectionInfo(_this.lock()); + if (info) + this->notifyConnectionInfo(client, info); + + return CommandResult::Success; +} + +CommandResult 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.lock(), info); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandServerRequestConnectionInfo(Command &) { + CMD_REQ_SERVER; + CACHED_PERM_CHECK(permission::b_virtualserver_connectioninfo_view, 1, true); + + Command notify("notifyserverconnectioninfo"); + + auto statistics = this->server->getServerStatistics()->statistics(); + auto report = this->server->getServerStatistics()->dataReport(); + + notify[0]["connection_filetransfer_bandwidth_sent"] = report.file_send; + notify[0]["connection_filetransfer_bandwidth_received"] = report.file_recv; + notify[0]["connection_filetransfer_bytes_sent_total"] = (*statistics)[property::CONNECTION_FILETRANSFER_BYTES_SENT_TOTAL].as(); + notify[0]["connection_filetransfer_bytes_received_total"] = (*statistics)[property::CONNECTION_FILETRANSFER_BYTES_RECEIVED_TOTAL].as(); + + notify[0]["connection_packets_sent_total"] = (*statistics)[property::CONNECTION_PACKETS_SENT_TOTAL].as(); + notify[0]["connection_bytes_sent_total"] = (*statistics)[property::CONNECTION_BYTES_SENT_TOTAL].as(); + notify[0]["connection_packets_received_total"] = (*statistics)[property::CONNECTION_PACKETS_RECEIVED_TOTAL].as(); + notify[0]["connection_bytes_received_total"] = (*statistics)[property::CONNECTION_BYTES_RECEIVED_TOTAL].as(); + + notify[0]["connection_bandwidth_sent_last_second_total"] = report.send_second; + notify[0]["connection_bandwidth_sent_last_minute_total"] = report.send_minute; + notify[0]["connection_bandwidth_received_last_second_total"] = report.recv_second; + notify[0]["connection_bandwidth_received_last_minute_total"] = report.recv_minute; + + notify[0]["connection_connected_time"] = this->server->properties()[property::VIRTUALSERVER_UPTIME].as(); + notify[0]["connection_packetloss_total"] = this->server->averagePacketLoss(); + notify[0]["connection_ping"] = this->server->averagePing(); + + this->sendCommand(notify); + return CommandResult::Success; +} + +//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 +CommandResult ConnectedClient::handleCommandConnectionInfoAutoUpdate(Command &cmd) { + 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 CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientGetIds(Command &cmd) { + CMD_REQ_SERVER; + + bool error = false; + bool found = false; + auto client_list = this->server->getClients(); + + Command notify(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifyclientids" : ""); + int result_index = 0; + + for(int index = 0; index < cmd.bulkCount(); index++) { + auto unique_id = cmd[index]["cluid"].as(); + for(const auto& entry : client_list) { + if(entry->getUid() == unique_id) { + if(!config::server::show_invisible_clients_as_online && !this->channels->channel_visible(entry->currentChannel, nullptr)) + continue; + + notify[result_index]["name"] = entry->getDisplayName(); + notify[result_index]["clid"] = entry->getClientId(); + notify[result_index]["cluid"] = entry->getUid(); + result_index++; + found = true; + } + } + if(found) found = false; + else error = false; + } + string uid = cmd["cluid"]; + + if(result_index > 0) { + this->sendCommand(notify); + } + if(error) { + return {findError("database_empty_result"), "empty!"}; + } + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandChannelSubscribe(Command &cmd) { + CMD_REF_SERVER(ref_server); + CMD_RESET_IDLE; + + bool flood_points = false; + deque> channels; + + { + shared_lock server_channel_lock(this->server->channel_tree_lock); + unique_lock client_channel_lock(this->channel_lock); + + for (int index = 0; index < cmd.bulkCount(); index++) { + auto local_channel = this->channel_view()->find_channel(cmd[index]["cid"].as()); + if(!local_channel) + return {findError("channel_invalid_id"), "Cant resolve channel"}; + + auto channel = this->server->channelTree->findChannel(cmd[index]["cid"].as()); + if (!channel) + return {findError("channel_invalid_id"), "Cant resolve channel"}; + + channels.push_back(channel); + if(!flood_points && system_clock::now() - local_channel->view_timestamp > seconds(5)) { + flood_points = true; + CMD_CHK_AND_INC_FLOOD_POINTS(15); + } + } + + if(!channels.empty()) + this->subscribeChannel(channels, false, false); + } + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandChannelSubscribeAll(Command &cmd) { + CMD_REQ_SERVER; + CMD_CHK_AND_INC_FLOOD_POINTS(20); + + { + shared_lock server_channel_lock(this->server->channel_tree_lock); + unique_lock client_channel_lock(this->channel_lock); + this->subscribeChannel(this->server->channelTree->channels(), false, false); + this->subscribeToAll = true; + } + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandChannelUnsubscribe(Command &cmd) { + CMD_REQ_SERVER; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + { + shared_lock server_channel_lock(this->server->channel_tree_lock); + unique_lock client_channel_lock(this->channel_lock); + + deque> channels; + for(int index = 0; index < cmd.bulkCount(); index++) { + auto channel = this->server->channelTree->findChannel(cmd["cid"].as()); + if(!channel) continue; + channels.push_front(channel); + } + + this->unsubscribeChannel(channels, false); + } + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandChannelUnsubscribeAll(Command &cmd) { + CMD_REQ_SERVER; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + { + shared_lock server_channel_lock(this->server->channel_tree_lock); + unique_lock client_channel_lock(this->channel_lock); + this->unsubscribeChannel(this->server->channelTree->channels(), false); + this->subscribeToAll = false; + } + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandPermissionList(Command &cmd) { + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + static std::string permission_list_string; + static std::mutex permission_list_string_lock; + + static auto build_permission_list = [](const std::string& command) { + 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; + }); + for (const auto& permission : avPerms) { + if (!permission->clientSupported) continue; + + auto &blk = list_builder[index++]; + blk["permname"] = permission->name; + blk["permdesc"] = permission->description; + blk["permid"] = permission->type; + } + return list_builder; + }; + + if(this->getType() == CLIENT_TEASPEAK || this->getType() == CLIENT_TEAMSPEAK || this->getType() == CLIENT_QUERY) { + Command response(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifypermissionlist" : ""); + { + lock_guard lock(permission_list_string_lock); + if(permission_list_string.empty()) + permission_list_string = build_permission_list("").build(); + response[0][""] = permission_list_string; + } + this->sendCommand(response); + } else { + this->sendCommand(build_permission_list(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifypermissionlist" : "")); + } + return CommandResult::Success; +} + + +#define M(ptype) \ +do { \ + for(const auto& prop : property::impl::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]; \ + index++; \ + } \ +} while(0) + +CommandResult ConnectedClient::handleCommandPropertyList(ts::Command& cmd) { + Command response(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifypropertylist" : ""); + + { + string pattern; + for (auto flag_name : property::flag_names) + 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("group")) + M(property::GroupProperties); + if(cmd.hasParm("all") || cmd.hasParm("connection")) + M(property::ConnectionProperties); + + this->sendCommand(response); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandServerGroupList(Command &) { + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + CACHED_PERM_CHECK(permission::b_virtualserver_servergroup_list, 1, true); + + this->notifyServerGroupList(); + this->command_times.servergrouplist = system_clock::now(); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandChannelGroupAdd(Command &cmd) { + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + CACHED_PERM_CHECK(permission::b_virtualserver_channelgroup_create, 1, true); + + auto group_manager = this->server ? this->server->getGroupManager() : serverInstance->getGroupManager().get(); + if(cmd["type"].as() == GroupType::GROUP_TYPE_NORMAL && !this->server) return {findError("parameter_invalid"), "You cant create normal channel groups on the template server"}; + if(cmd["name"].string().empty()) return {findError("parameter_invalid"), "invalid group name"}; + for(const auto& gr : group_manager->availableServerGroups(true)) + if(gr->name() == cmd["name"].string() && gr->target() == GroupTarget::GROUPTARGET_CHANNEL) return {findError("parameter_invalid"), "Group already exists"}; + auto group = group_manager->createGroup(GroupTarget::GROUPTARGET_CHANNEL, cmd["type"].as(), cmd["name"].string()); + if (group) { + group->permissions()->set_permission(permission::b_group_is_permanent, {1, 0}, permission::v2::set_value, permission::v2::do_nothing); + if(this->server) + this->server->forEachClient([](shared_ptr cl) { + cl->notifyChannelGroupList(); + }); + } else return {findError("parameter_invalid"), "invalid server group id"}; + return CommandResult::Success; +} + +//name=Channel\sAdmin scgid=5 tcgid=4 type=1 +CommandResult ConnectedClient::handleCommandChannelGroupCopy(Command &cmd) { + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + CACHED_PERM_CHECK(permission::b_virtualserver_channelgroup_create, 1, true); + + auto group_manager = this->server ? this->server->getGroupManager() : serverInstance->getGroupManager().get(); + auto src = group_manager->findGroup(cmd["scgid"].as()); + if (!src || src->target() != GROUPTARGET_CHANNEL) return {findError("parameter_invalid"), "invalid channel group id"}; + + bool global = false; + if(cmd[0].has("tcgid")&& cmd["tcgid"].as() != 0) { + auto target = group_manager->findGroup(cmd["tcgid"].as()); + if (!target || target->target() != GROUPTARGET_CHANNEL) return {findError("parameter_invalid"), "invalid target channel group id"}; + auto result = group_manager->copyGroupPermissions(src, target); + if(!result) return {findError("vs_critical"), "could not copy group!"}; + global = !this->server || group_manager->isLocalGroup(target); + } else { + auto type = cmd["type"].as(); + if(type == GroupType::GROUP_TYPE_QUERY) + CACHED_PERM_CHECK(permission::b_serverinstance_modify_querygroup, 1, true); + if(type == GroupType::GROUP_TYPE_TEMPLATE) + CACHED_PERM_CHECK(permission::b_serverinstance_modify_templates, 1, true); + + if(type == GroupType::GROUP_TYPE_NORMAL && !this->server) return {findError("parameter_invalid"), "You cant create normal channel groups on the template server"}; + auto result = group_manager->copyGroup(src, cmd["type"], cmd["name"], type == GROUP_TYPE_NORMAL ? this->getServerId() : 0); //TODO maybe check by name? No duplicated groups? + if (!result) return {findError("vs_critical"), "could not copy group!"}; + global = !this->server || type != GroupType::GROUP_TYPE_NORMAL; + + if(this->getType() == ClientType::CLIENT_QUERY) { + Command notify(""); + notify["cgid"] = group_manager->availableChannelGroups(false).back()->groupId(); + this->sendCommand(notify); + } + } + + for(const auto& server : (global ? serverInstance->getVoiceServerManager()->serverInstances() : deque>{this->server})) + if(server) + server->forEachClient([](shared_ptr cl) { + cl->notifyChannelGroupList(); + }); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandChannelGroupRename(Command &cmd) { + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + auto group_manager = this->server ? this->server->getGroupManager() : serverInstance->getGroupManager().get(); + auto serverGroup = group_manager->findGroup(cmd["cgid"].as()); + if (!serverGroup || serverGroup->target() != GROUPTARGET_CHANNEL) return {findError("parameter_invalid"), "invalid channel group id"}; + GROUP_PERMISSION_TEST(permission::i_channel_group_modify_power, permission::i_channel_group_needed_modify_power, serverGroup, true); + + group_manager->renameGroup(serverGroup, cmd["name"].string()); + if(this->server) + this->server->forEachClient([](shared_ptr cl) { + cl->notifyChannelGroupList(); + }); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandChannelGroupDel(Command &cmd) { + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + auto group_manager = this->server ? this->server->getGroupManager() : serverInstance->getGroupManager().get(); + auto channel_group = group_manager->findGroup(cmd["cgid"].as()); + if (!channel_group || channel_group->target() != GROUPTARGET_CHANNEL) return {findError("parameter_invalid"), "invalid channel group id"}; + CACHED_PERM_CHECK(permission::b_virtualserver_channelgroup_delete, 1, true); + + if(this->server) { + if(this->server->properties()[property::VIRTUALSERVER_DEFAULT_CHANNEL_GROUP] == channel_group->groupId()) + return {findError("parameter_invalid"), "Could not delete default channel group!"}; + if(this->server->properties()[property::VIRTUALSERVER_DEFAULT_CHANNEL_ADMIN_GROUP] == channel_group->groupId()) + return {findError("parameter_invalid"), "Could not delete default channel admin group!"}; + } + if(serverInstance->properties()[property::SERVERINSTANCE_TEMPLATE_CHANNELDEFAULT_GROUP] == channel_group->groupId()) + return {findError("parameter_invalid"), "Could not delete instance default channel group!"}; + if(serverInstance->properties()[property::SERVERINSTANCE_TEMPLATE_CHANNELADMIN_GROUP] == channel_group->groupId()) + return {findError("parameter_invalid"), "Could not delete instance default channel admin group!"}; + + if (!cmd["force"].as()) + if (!group_manager->listGroupMembers(channel_group, false).empty()) + return {findError("database_empty_result"), "group not empty!"}; + + if (group_manager->deleteGroup(channel_group) && this->server) { + this->server->forEachClient([&](shared_ptr cl) { + if(this->server->notifyClientPropertyUpdates(cl, this->server->groups->update_server_group_property(cl, true))) { + if(cl->update_cached_permissions()) /* update cached calculated permissions */ + cl->sendNeededPermissions(false); /* cached permissions had changed, notify the client */ + } + cl->notifyChannelGroupList(); + }); + } + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandChannelGroupList(Command &) { + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + CACHED_PERM_CHECK(permission::b_virtualserver_channelgroup_list, 1, true); + + this->notifyChannelGroupList(); + this->command_times.servergrouplist = system_clock::now(); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandChannelGroupClientList(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + CACHED_PERM_CHECK(permission::b_virtualserver_channelgroup_client_list, 1, true); + Command result(this->getExternalType() == ClientType::CLIENT_TEAMSPEAK ? "notifychannelgroupclientlist" : ""); + + deque variables{variable{":sid", this->getServerId()}}; + string query = "SELECT `groupId`, `cldbid`, `until`, `channelId` FROM `assignedGroups` WHERE `serverId` = :sid"; + + if(cmd[0].has("cgid") && cmd["cgid"].as() > 0) { + auto group = this->server->getGroupManager()->findGroup(cmd["cgid"]); + if(!group || group->target() != GroupTarget::GROUPTARGET_CHANNEL) + return {findError("parameter_invalid"), "invalid channel group id"}; + query += " AND `groupId` = :groupId"; + variables.push_back({":groupId", cmd["cgid"].as()}); + } else { + query += " AND `groupId` IN (SELECT `groupId` FROM `groups` WHERE `serverId` = :sid AND `target` = :target)"; + variables.push_back({":target", GroupTarget::GROUPTARGET_CHANNEL}); + } + if(cmd[0].has("cldbid") && cmd["cldbid"].as() > 0) { + query += " AND `cldbid` = :cldbid"; + variables.push_back({":cldbid", cmd["cldbid"].as()}); + } + if(cmd[0].has("cid") && cmd["cid"].as() > 0) { + auto channel = this->server->getChannelTree()->findChannel(cmd["cid"]); + if(!channel) + return {findError("parameter_invalid"), "invalid channel id"}; + query += " AND `channelId` = :cid"; + variables.push_back({":cid", cmd["cid"].as()}); + } + debugMessage(this->getServerId(), "Command channelgroupclientlist sql: {}", query); + + auto command = sql::command(this->sql, query); + for(const auto& variable : variables) + command.value(variable); + + int index = 0; + command.query([&](Command& command, int& index, int length, string* values, string* names) { + GroupId group_id = 0; + ChannelId channel_id = 0; + ClientDbId cldbid = 0; + for(int i = 0; i < length; i++) { + try { + if(names[i] == "groupId") + group_id = stoll(values[i]); + else if(names[i] == "cldbid") + cldbid = stoll(values[i]); + else if(names[i] == "channelId") + channel_id = stoll(values[i]); + } catch(std::exception& ex) { + logError(this->getServerId(), "Failed to parse db field {}", names[i]); + } + } + result[index]["cid"] = channel_id; + result[index]["cgid"] = group_id; + result[index]["cldbid"] = cldbid; + index++; + }, result, index); + if (index == 0) return {findError("database_empty_result"), "empty"}; + this->sendCommand(result); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandChannelGroupPermList(Command &cmd) { + CMD_CHK_AND_INC_FLOOD_POINTS(5); + CACHED_PERM_CHECK(permission::b_virtualserver_channelgroup_permission_list, 1, true); + + auto channelGroup = (this->server ? this->server->groups : serverInstance->getGroupManager().get())->findGroup(cmd["cgid"].as()); + if (!channelGroup || channelGroup->target() != GROUPTARGET_CHANNEL) return {findError("parameter_invalid"), "invalid channel group id"}; + if (!this->notifyGroupPermList(channelGroup, cmd.hasParm("permsid"))) return {findError("database_empty_result"), "empty"}; + + if(this->getType() == ClientType::CLIENT_TEAMSPEAK && this->command_times.last_notify + this->command_times.notify_timeout < system_clock::now()) { + this->sendTSPermEditorWarning(); + } + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandChannelGroupAddPerm(Command &cmd) { + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + auto group_manager = this->server ? this->server->getGroupManager() : serverInstance->getGroupManager().get(); + auto channelGroup = group_manager->findGroup(cmd["cgid"].as()); + if (!channelGroup || channelGroup->target() != GROUPTARGET_CHANNEL) return {findError("parameter_invalid"), "invalid channel group id"}; + GROUP_PERMISSION_TEST(permission::i_channel_group_modify_power, permission::i_channel_group_needed_modify_power, channelGroup, true); + + auto maxValue = this->getPermissionGrantValue(permission::PERMTEST_ORDERED, permission::i_permission_modify_power, this->currentChannel); + bool ignoreGrant = this->permission_granted(this->cached_permission_value(permission::b_permission_modify_power_ignore), 1); + bool updateList = false; + bool conOnError = cmd[0].has("continueonerror"); + + auto permission_manager = channelGroup->permissions(); + for (int index = 0; index < cmd.bulkCount(); index++) { + PARSE_PERMISSION(cmd); + + auto val = cmd[index]["permvalue"].as(); + if(permission_require_granted_value(permType) && val > maxValue) + return CommandResultPermissionError{permission::i_permission_modify_power}; + + if(!ignoreGrant && !this->permissionGrantGranted(permission::PERMTEST_ORDERED, permType, 1, this->currentChannel)) + return CommandResultPermissionError{permission::i_permission_modify_power}; + + if (grant) { + permission_manager->set_permission(permType, {0, cmd[index]["permvalue"]}, permission::v2::PermissionUpdateType::do_nothing, permission::v2::PermissionUpdateType::set_value); + } else { + permission_manager->set_permission( + permType, + {cmd[index]["permvalue"], 0}, + permission::v2::PermissionUpdateType::set_value, + permission::v2::PermissionUpdateType::do_nothing, + + cmd[index]["permnegated"].as() ? 1 : 0, + cmd[index]["permskip"].as() ? 1 : 0 + ); + updateList |= permission_is_group_property(permType); + } + } + + + if(updateList) + channelGroup->apply_properties_from_permissions(); + + if(this->server) { + if(updateList) + this->server->forEachClient([](shared_ptr cl) { + cl->notifyChannelGroupList(); + }); + this->server->forEachClient([channelGroup](shared_ptr cl) { + unique_lock client_channel_lock(cl->channel_lock); /* while we're updating groups we dont want to change anything! */ + if (cl->channelGroupAssigned(channelGroup, cl->getChannel())) { + if(cl->update_cached_permissions()) + cl->sendNeededPermissions(false); /* update the needed permissions */ + cl->updateChannelClientProperties(true, true); + } + }); + } + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandChannelGroupDelPerm(Command &cmd) { + CMD_CHK_AND_INC_FLOOD_POINTS(5); + auto group_manager = this->server ? this->server->getGroupManager() : serverInstance->getGroupManager().get(); + auto channelGroup = group_manager->findGroup(cmd["cgid"].as()); + if (!channelGroup || channelGroup->target() != GROUPTARGET_CHANNEL) return {findError("parameter_invalid"), "invalid channel group id"}; + GROUP_PERMISSION_TEST(permission::i_channel_group_modify_power, permission::i_channel_group_needed_modify_power, channelGroup, true); + + bool ignoreGrant = this->permission_granted(this->cached_permission_value(permission::b_permission_modify_power_ignore), 1); + bool updateList = false; + bool conOnError = cmd[0].has("continueonerror"); + + auto permission_manager = channelGroup->permissions(); + for (int index = 0; index < cmd.bulkCount(); index++) { + PARSE_PERMISSION(cmd) + + if(!ignoreGrant && !this->permissionGrantGranted(permission::PERMTEST_ORDERED, permType, 1, this->currentChannel)) + return CommandResultPermissionError{permission::i_permission_modify_power}; + if (grant) { + permission_manager->set_permission(permType, permission::v2::empty_permission_values, permission::v2::PermissionUpdateType::do_nothing, permission::v2::PermissionUpdateType::delete_value); + } else { + permission_manager->set_permission( + permType, + permission::v2::empty_permission_values, + permission::v2::PermissionUpdateType::delete_value, + permission::v2::PermissionUpdateType::do_nothing + ); + updateList |= permission_is_group_property(permType); + } + } + + if(updateList) + channelGroup->apply_properties_from_permissions(); + + if(this->server) { + if(updateList) + this->server->forEachClient([](shared_ptr cl) { + cl->notifyChannelGroupList(); + }); + this->server->forEachClient([channelGroup](shared_ptr cl) { + unique_lock client_channel_lock(cl->channel_lock); /* while we're updating groups we dont want to change anything! */ + if (cl->channelGroupAssigned(channelGroup, cl->getChannel())) { + if(cl->update_cached_permissions()) /* update cached calculated permissions */ + cl->sendNeededPermissions(false); /* cached permissions had changed, notify the client */ + cl->updateChannelClientProperties(true, false); + } + }); + } + return CommandResult::Success; +} + +using namespace std::chrono; + +CommandResult ConnectedClient::handleCommandClientMove(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(10); + + shared_lock server_channel_r_lock(this->server->channel_tree_lock); + auto target_client_id = cmd["clid"].as(); + auto target_client = target_client_id == 0 ? this->ref() : this->server->findClient(target_client_id); + if(!target_client) { + return {findError("client_invalid_id"), "Invalid target clid"}; + } + + if(!target_client->currentChannel) { + if(target_client != this) + return {findError("client_invalid_id"), "Invalid target clid"}; + } + auto channel = this->server->channelTree->findChannel(cmd["cid"].as()); + if (!channel) { + return {findError("channel_invalid_id")}; + } + + auto permission_cache = make_shared(); + if(!cmd[0].has("cpw")) + cmd["cpw"] = ""; + if (!channel->passwordMatch(cmd["cpw"], true)) + if (!this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_channel_join_ignore_password, 1, channel, true, permission_cache)) + return {findError("channel_invalid_password"), "invalid password"}; + + switch(channel->channelType()) { + case ChannelType::permanent: + PERM_CHECK_CHANNEL_CR(permission::b_channel_join_permanent, 1, channel, true, permission_cache); + break; + case ChannelType::semipermanent: + PERM_CHECK_CHANNEL_CR(permission::b_channel_join_semi_permanent, 1, channel, true, permission_cache); + break; + case ChannelType::temporary: + PERM_CHECK_CHANNEL_CR(permission::b_channel_join_temporary, 1, channel, true, permission_cache); + break; + } + if (!channel->properties()[property::CHANNEL_FLAG_MAXCLIENTS_UNLIMITED].as() || !channel->properties()[property::CHANNEL_FLAG_MAXFAMILYCLIENTS_UNLIMITED].as()) { + if(!this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_channel_join_ignore_maxclients, 1, channel, true, permission_cache)) { + if(!channel->properties()[property::CHANNEL_FLAG_MAXCLIENTS_UNLIMITED].as()) { + auto maxClients = channel->properties()[property::CHANNEL_MAXCLIENTS].as(); + if (maxClients >= 0 && maxClients <= this->server->getClientsByChannel(channel).size()) + return {findError("channel_maxclients_reached"), ""}; + } + if(!channel->properties()[property::CHANNEL_FLAG_MAXFAMILYCLIENTS_UNLIMITED].as()) { + shared_ptr family_root; + + if(channel->properties()[property::CHANNEL_FLAG_MAXFAMILYCLIENTS_INHERITED].as()) { + family_root = channel; + while(family_root && family_root->properties()[property::CHANNEL_FLAG_MAXFAMILYCLIENTS_INHERITED].as()) family_root = family_root->parent(); + } + if(family_root && !family_root->properties()[property::CHANNEL_FLAG_MAXFAMILYCLIENTS_UNLIMITED]) { //Could not be CHANNEL_FLAG_MAXFAMILYCLIENTS_INHERITED + auto maxClients = family_root->properties()[property::CHANNEL_MAXFAMILYCLIENTS].as(); + auto clients = 0; + for(const auto& entry : this->server->getClientsByChannelRoot(channel, false)) if(entry.get() != this) clients++; //Dont count the client itself + if (maxClients >= 0 && maxClients <= clients) + return {findError("channel_maxfamily_reached"), ""}; + } + } + } + } + + if(!this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_channel_ignore_join_power, 1, channel, true, permission_cache)) { + CHANNEL_PERMISSION_TEST(permission::i_channel_join_power, permission::i_channel_needed_join_power, channel, false); + + if (target_client == this) { + auto permission_cache_current = make_shared(); + auto val = this->permissionValue(permission::PERMTEST_ORDERED, permission::b_client_is_sticky, this->currentChannel, permission_cache_current); + if (val != permNotGranted && val > 0) { + auto st = this->permissionValue(permission::PERMTEST_ORDERED, permission::b_client_ignore_sticky, this->currentChannel, permission_cache_current); + if (st != 1) + return CommandResultPermissionError{permission::b_client_is_sticky}; + } + } + } + if (target_client != this) { + PERM_CHECK_CHANNELR(permission::i_client_move_power, target_client->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_needed_move_power, target_client->getChannel()), target_client->getChannel(), true); + PERM_CHECK_CHANNEL_CR(permission::i_client_move_power, target_client->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_needed_move_power, channel), channel, true, permission_cache); + } + + server_channel_r_lock.unlock(); + unique_lock server_channel_w_lock(this->server->channel_tree_lock); + auto oldChannel = target_client->getChannel(); + this->server->client_move( + target_client, + channel, + target_client == this ? nullptr : _this.lock(), + "", + target_client == this ? ViewReasonId::VREASON_USER_ACTION : ViewReasonId::VREASON_MOVED, + true, + server_channel_w_lock + ); + + if(oldChannel) { + if(!server_channel_w_lock.owns_lock()) + server_channel_w_lock.lock(); + if(oldChannel->channelType() == ChannelType::temporary && oldChannel->properties()[property::CHANNEL_DELETE_DELAY].as() == 0) + if(this->server->getClientsByChannelRoot(oldChannel, false).empty()) + this->server->delete_channel(dynamic_pointer_cast(oldChannel), this->ref(), "temporary auto delete", server_channel_w_lock); + if(server_channel_w_lock.owns_lock()) + server_channel_w_lock.unlock(); + } + return CommandResult::Success; +} + +//TODO: Test if parent or previous is deleted! +CommandResult ConnectedClient::handleCommandChannelCreate(Command &cmd) { + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + CMD_CHK_PARM_COUNT(1); + + auto permission_cache = make_shared(); + if (cmd[0].has("cpid") && cmd["cpid"].as() != 0) CACHED_PERM_CHECK(permission::b_channel_create_child, 1); + if (cmd[0].has("channel_order")) CACHED_PERM_CHECK(permission::b_channel_create_with_sortorder, 1); + + if(!cmd[0].has("channel_flag_permanent")) cmd[0]["channel_flag_permanent"] = false; + if(!cmd[0].has("channel_flag_semi_permanent")) cmd[0]["channel_flag_semi_permanent"] = false; + if(!cmd[0].has("channel_flag_default")) cmd[0]["channel_flag_default"] = false; + if(!cmd[0].has("channel_flag_password")) cmd[0]["channel_flag_password"] = false; + + if (cmd[0]["channel_flag_permanent"].as()) CACHED_PERM_CHECK(permission::b_channel_create_permanent, 1); + else if (cmd[0]["channel_flag_semi_permanent"].as()) CACHED_PERM_CHECK(permission::b_channel_create_semi_permanent, 1); + else CACHED_PERM_CHECK(permission::b_channel_create_temporary, 1); + + if (!cmd[0]["channel_flag_permanent"].as() && !this->server) return {findError("parameter_invalid"), "You can only create a permanent channel"}; + + if (cmd[0]["channel_flag_default"].as()) CACHED_PERM_CHECK(permission::b_channel_create_with_default, 1); + if (cmd[0]["channel_flag_password"].as()) CACHED_PERM_CHECK(permission::b_channel_create_with_password, 1); + else if(this->permission_granted(this->cached_permission_value(permission::b_channel_create_modify_with_force_password), 1)) return CommandResultPermissionError{permission::b_channel_create_modify_with_force_password}; + + if(cmd[0].has("channel_password") && this->getType() == ClientType::CLIENT_QUERY) + cmd["channel_password"] = base64::decode(digest::sha1(cmd["channel_password"].string())); + if (cmd[0].has("channel_description")) CACHED_PERM_CHECK(permission::b_channel_create_with_description, 1); + if (cmd[0].has("channel_maxclients") || (cmd[0].has("channel_flag_maxclients_unlimited") && !cmd["channel_flag_maxclients_unlimited"].as())) { + CACHED_PERM_CHECK(permission::b_channel_create_with_maxclients, 1); + if(!cmd[0]["channel_flag_permanent"].as() && !cmd[0]["channel_flag_semi_permanent"].as()) { + cmd["channel_maxclients"] = -1; + cmd["channel_flag_maxclients_unlimited"] = 1; + } + } + if (cmd[0].has("channel_maxfamilyclients")) CACHED_PERM_CHECK(permission::b_channel_create_with_maxfamilyclients, 1); + if (cmd[0].has("channel_needed_talk_power")) CACHED_PERM_CHECK(permission::b_channel_create_with_needed_talk_power, 1); + if (cmd[0].has("channel_topic")) CACHED_PERM_CHECK(permission::b_channel_create_with_topic, 1); + + auto target_tree = this->server ? this->server->channelTree : serverInstance->getChannelTree().get(); + auto& tree_lock = this->server ? this->server->channel_tree_lock : serverInstance->getChannelTreeLock(); + unique_lock tree_channel_lock(tree_lock); + + { + auto delete_delay = cmd[0].has("channel_delete_delay") ? cmd["channel_delete_delay"].as() : 0UL; + if(delete_delay == 0) { + if(this->server) + cmd["channel_delete_delay"] = this->server->properties()[property::VIRTUALSERVER_CHANNEL_TEMP_DELETE_DELAY_DEFAULT].as(); + else + cmd["channel_delete_delay"] = 0; + } else { + CACHED_PERM_CHECK(permission::i_channel_create_modify_with_temp_delete_delay, cmd["channel_delete_delay"].as()); + } + } + + { + size_t created_tmp = 0, created_semi = 0, created_perm = 0; + auto own_cldbid = this->getClientDatabaseId(); + for(const auto& channel : target_tree->channels()) { + if(channel->properties()[property::CHANNEL_CREATED_BY] == own_cldbid) { + if(channel->properties()[property::CHANNEL_FLAG_PERMANENT].as()) + created_perm++; + else if(channel->properties()[property::CHANNEL_FLAG_SEMI_PERMANENT].as()) + created_semi++; + else + created_tmp++; + } + } + + + auto max_channels = this->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_max_channels, nullptr, permission_cache); + + if(max_channels >= 0) { + if(max_channels <= created_perm + created_semi + created_tmp) + return CommandResultPermissionError{permission::i_client_max_channels}; + } + + if (cmd[0]["channel_flag_permanent"].as()) { + max_channels = this->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_max_permanent_channels, nullptr, permission_cache); + if(max_channels >= 0) { + if(max_channels <= created_perm) + return CommandResultPermissionError{permission::i_client_max_permanent_channels}; + } + } + else if (cmd[0]["channel_flag_semi_permanent"].as()) { + max_channels = this->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_max_semi_channels, nullptr, permission_cache); + if(max_channels >= 0) { + if(max_channels <= created_semi) + return CommandResultPermissionError{permission::i_client_max_semi_channels}; + } + } + else { + max_channels = this->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_max_temporary_channels, nullptr, permission_cache); + if(max_channels >= 0) { + if(max_channels <= created_tmp) + return CommandResultPermissionError{permission::i_client_max_temporary_channels}; + } + } + } + + //TODO check voice (opus etc) + std::shared_ptr parent = nullptr; + std::shared_ptr created_channel = nullptr, old_default_channel; + + //bool enforce_permanent_parent = cmd[0]["channel_flag_default"].as(); //TODO check parents here + { //Checkout the parent(s) + if (cmd[0].has("cpid") && cmd["cpid"].as() != 0 && cmd["cpid"].as() != -1) { + parent = target_tree->findLinkedChannel(cmd["cpid"].as()); + if (!parent) return {findError("channel_invalid_id"), "Cant resolve parent channel"}; + } + + { + + auto min_channel_deep = this->permissionValue(permission::PERMTEST_ORDERED, permission::i_channel_min_depth, nullptr, permission_cache); + auto max_channel_deep = this->permissionValue(permission::PERMTEST_ORDERED, permission::i_channel_max_depth, nullptr, permission_cache); + + if(min_channel_deep >= 0 || max_channel_deep >= 0) { + auto channel_deep = 0; + auto local_parent = parent; + while(local_parent) { + channel_deep++; + { + const auto typed_parent = dynamic_pointer_cast(local_parent->entry); + if(typed_parent->deleted) return {findError("channel_is_deleted"), "Oneof parent is deleted"}; + } + local_parent = local_parent->parent.lock(); + } + + if(min_channel_deep >= 0 && channel_deep < min_channel_deep) return CommandResultPermissionError{permission::i_channel_min_depth}; + if(max_channel_deep >= 0 && channel_deep > max_channel_deep) return CommandResultPermissionError{permission::i_channel_max_depth}; + } + } + } + + if(!cmd[0].has("channel_order")) { + auto last = parent ? parent->child_head : target_tree->tree_head(); + while(last && last->next) + last = last->next; + if(last) + cmd["channel_order"] = last->entry->channelId(); + } else { + + } + + if (cmd["channel_name"].string().length() < 1) return {findError("channel_name_inuse"), "Invalid channel name"}; + + { + if (target_tree->findChannel(cmd["channel_name"].as(), parent ? dynamic_pointer_cast(parent->entry) : nullptr)) return {findError("channel_name_inuse"), "Name already in use"}; + + created_channel = target_tree->createChannel(parent ? parent->entry->channelId() : (ChannelId) 0, cmd[0].has("channel_order") ? cmd["channel_order"].as() : (ChannelId) 0, cmd["channel_name"].as()); + } + + if (!created_channel) return {findError("channel_invalid_flags"), "Could not create channel"}; + + auto created_linked_channel = target_tree->findLinkedChannel(created_channel->channelId()); + sassert(created_linked_channel); + + if (cmd[0].has("channel_flag_default") && cmd["channel_flag_default"].as()) { + old_default_channel = target_tree->getDefaultChannel(); + target_tree->setDefaultChannel(created_channel); + } + + created_channel->properties()[property::CHANNEL_CREATED_BY] = this->getClientDatabaseId(); + + { + auto permission_manager = created_channel->permissions(); + permission_manager->set_permission( + permission::i_channel_needed_modify_power, + {this->permissionValue(permission::PERMTEST_ORDERED, permission::i_channel_modify_power, this->currentChannel, permission_cache), 0}, + permission::v2::PermissionUpdateType::set_value, + permission::v2::PermissionUpdateType::do_nothing + ); + permission_manager->set_permission( + permission::i_channel_needed_delete_power, + {this->permissionValue(permission::PERMTEST_ORDERED, permission::i_channel_delete_power, this->currentChannel, permission_cache), 0}, + permission::v2::PermissionUpdateType::set_value, + permission::v2::PermissionUpdateType::do_nothing + ); + } + + for (auto &prop : cmd[0].keys()) { + if (prop == "channel_flag_default") continue; + if (prop == "channel_order") continue; + if (prop == "channel_name") continue; + if (prop == "cpid") continue; + if (prop == "cid") continue; + + const auto &property = property::info(prop); + if(*property == property::CHANNEL_UNDEFINED) { + logError(this->getServerId(), "Client " + this->getDisplayName() + " tried to change a not existing channel property " + prop); + continue; + } + + if(!property->validate_input(cmd[prop].as())) { + logError(this->getServerId(), "Client " + this->getDisplayName() + " tried to change a property to an invalid value. (Value: '" + cmd[prop].as() + "', Property: '" + property->name + "')"); + continue; + } + created_channel->properties()[property] = cmd[prop].as(); + } + if(created_channel->parent()) { + if(created_channel->parent()->channelType() > created_channel->channelType()) + created_channel->setChannelType(created_channel->parent()->channelType()); + } + + if(this->server) { + const auto self_lock = _this.lock(); + this->server->forEachClient([&, created_channel](const shared_ptr& client) { + unique_lock client_channel_lock(client->channel_lock); + auto result = client->channels->add_channel(created_linked_channel); + if(!result) return; + + client->notifyChannelCreate(created_channel, result->previous_channel, self_lock); + if(client == self_lock && this->getType() == ClientType::CLIENT_QUERY) { + Command notify(""); + notify["cid"] = created_channel->channelId(); + this->sendCommand(notify); + } + client->notifyChannelDescriptionChanged(created_channel); + if (client->subscribeToAll) + client->subscribeChannel({created_channel}, false, true); + + if(old_default_channel) { + //TODO: Reminder: client channel tree must be at least read locked here! + client->notifyChannelEdited(old_default_channel, {"channel_flag_default"}, this); + } + }); + + GroupId adminGroup = this->server->properties()[property::VIRTUALSERVER_DEFAULT_CHANNEL_ADMIN_GROUP]; + auto channel_admin_group = this->server->groups->findGroup(adminGroup); + if (!channel_admin_group) { + logError(this->getServerId(), "Missing server's default channel admin group! Using default channel group!"); + channel_admin_group = this->server->groups->defaultGroup(GroupTarget::GROUPTARGET_CHANNEL); + } + this->server->groups->setChannelGroup(this->getClientDatabaseId(), channel_admin_group, created_channel); + + if (created_channel->channelType() == ChannelType::temporary && (this->getType() == ClientType::CLIENT_TEAMSPEAK || this->getType() == ClientType::CLIENT_WEB)) + this->server->client_move( + this->ref(), + created_channel, + nullptr, + "channel created", + ViewReasonId::VREASON_USER_ACTION, + true, + tree_channel_lock + ); + } + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandChannelDelete(Command &cmd) { + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + RESOLVE_CHANNEL_W(cmd["cid"], true); + auto channel = dynamic_pointer_cast(l_channel->entry); + assert(channel); + if(channel->deleted) /* channel gets already removed */ + return CommandResult::Success; + + CHANNEL_PERMISSION_TEST(permission::i_channel_delete_power, permission::i_channel_needed_delete_power, channel, true); + for(const auto& ch : channel_tree->channels(channel)) { + if(ch->defaultChannel()) + return {findError("channel_can_not_delete_default"), "The channel tree you're going to delete contains the default channel!"}; + } + + if(this->server) { + auto clients = this->server->getClientsByChannelRoot(channel, false); + if(!clients.empty()) + PERM_CHECK_CHANNELR(permission::b_channel_delete_flag_force, 1, channel, true); + + this->server->delete_channel(channel, this->ref(), "channel deleted", channel_tree_write_lock); + } else { + auto channel_ids = channel_tree->deleteChannelRoot(channel); + this->notifyChannelDeleted(channel_ids, this->ref()); + } + return CommandResult::Success; +} + +inline ssize_t count_characters(const std::string& in) { + size_t index = 0; + size_t count = 0; + while(index < in.length()){ + count++; + + auto current = (uint8_t) in[index]; + if(current >= 128) { //UTF8 check + if(current >= 192 && (current <= 193 || current >= 245)) { + return -1; + } else if(current >= 194 && current <= 223) { + if(in.length() - index <= 1) return -1; + else if((uint8_t) in[index + 1] >= 128 && (uint8_t) in[index + 1] <= 191) index += 1; //Valid + else return -1; + } else if(current >= 224 && current <= 239) { + if(in.length() - index <= 2) return -1; + else if((uint8_t) in[index + 1] >= 128 && (uint8_t) in[index + 1] <= 191 && + (uint8_t) in[index + 2] >= 128 && (uint8_t) in[index + 2] <= 191) index += 2; //Valid + else return -1; + } else if(current >= 240 && current <= 244) { + if(in.length() - index <= 3) return -1; + else if((uint8_t) in[index + 1] >= 128 && (uint8_t) in[index + 1] <= 191 && + (uint8_t) in[index + 2] >= 128 && (uint8_t) in[index + 2] <= 191 && + (uint8_t) in[index + 3] >= 128 && (uint8_t) in[index + 3] <= 191) index += 3; //Valid + else return -1; + } else { + return -1; + } + } + index++; + } + return count; +} + +/* + * 1. check for permission and basic requirements + * 2. Lock the channel tree in write mode if required + * 3. Apply changed, test for advanced requirements like channel name etc + * 4. notify everyone + */ +CommandResult ConnectedClient::handleCommandChannelEdit(Command &cmd) { + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + RESOLVE_CHANNEL_R(cmd["cid"], true); + auto channel = dynamic_pointer_cast(l_channel->entry); + assert(channel); + if(channel->deleted) { + /* channel gets already removed */ + return CommandResult::Success; + } + + std::deque> keys; + bool require_write_lock = false; + bool update_max_clients = false; + bool update_max_family_clients = false; + bool update_clients_in_channel = false; + bool update_password = false; + bool update_name = false; + /* Step 1 */ + + bool target_channel_type_changed = false; + ChannelType::ChannelType target_channel_type = channel->channelType(); + CHANNEL_PERM_TEST_INIT; + for (const auto &key : cmd[0].keys()) { + if(key == "cid") + continue; + + const auto &property = property::info(key); + if(*property == property::CHANNEL_UNDEFINED) { + logError(this->getServerId(), R"({} Tried to edit a not existing channel property "{}" to "{}")", CLIENT_STR_LOG_PREFIX, key, cmd[key].string()); + continue; + } + + if((property->flags & property::FLAG_USER_EDITABLE) == 0) { + logError(this->getServerId(), "{} Tried to change a channel property which is not changeable. (Key: {}, Value: \"{}\")", CLIENT_STR_LOG_PREFIX, key, cmd[key].string()); + continue; + } + + if(!property->validate_input(cmd[key].as())) { + logError(this->getServerId(), "{} Tried to change a channel property to an invalid value. (Key: {}, Value: \"{}\")", CLIENT_STR_LOG_PREFIX, key, cmd[key].string()); + continue; + } + + if(key == "channel_icon_id") { + CHANNEL_PERMISSION_TEST(permission::i_channel_permission_modify_power, permission::i_channel_needed_permission_modify_power, channel, true); + } else if (key == "channel_order") { + CHANNEL_PERM_TEST(permission::b_channel_modify_sortorder, 1, true); + require_write_lock = true; + } else if (key == "channel_flag_default") { + if(!cmd["channel_flag_default"].as()) + return {findError("channel_invalid_flags")}; + + CHANNEL_PERM_TEST(permission::b_channel_modify_make_default, 1, true); + require_write_lock = true; + } else if (key == "channel_name") { + CHANNEL_PERM_TEST(permission::b_channel_modify_name, 1, true); + if (count_characters(cmd["channel_name"]) < 1) + return {findError("channel_name_inuse"), "Invalid channel name (too short)"}; + if (count_characters(cmd["channel_name"]) > 40) + return {findError("channel_name_inuse"), "Invalid channel name (too long)"}; + require_write_lock = true; + update_name = true; + } else if (key == "channel_name_phonetic") { + CHANNEL_PERM_TEST(permission::b_channel_modify_name, 1, true); + } else if (key == "channel_topic") { + CHANNEL_PERM_TEST(permission::b_channel_modify_topic, 1, true); + } else if (key == "channel_description") { + CHANNEL_PERM_TEST(permission::b_channel_modify_description, 1, true); + if(!this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_client_use_bbcode_any, 1, this->currentChannel)) { + auto bbcode_image = bbcode::sloppy::has_image(cmd[key]); + auto bbcode_url = bbcode::sloppy::has_url(cmd[key]); + debugMessage(this->getServerId(), "Channel description contains bb codes: Image: {} URL: {}", bbcode_image, bbcode_url); + if(bbcode_image && !this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_client_use_bbcode_image, 1, this->currentChannel)) + return CommandResultPermissionError{permission::b_client_use_bbcode_image}; + if(bbcode_url && !this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_client_use_bbcode_url, 1, this->currentChannel)) + return CommandResultPermissionError{permission::b_client_use_bbcode_url}; + } + } else if (key == "channel_codec") { + CHANNEL_PERM_TEST(permission::b_channel_modify_codec, 1, true); + } else if (key == "channel_codec_quality") { + CHANNEL_PERM_TEST(permission::b_channel_modify_codec_quality, 1, true); + } else if (key == "channel_codec_is_unencrypted") { + if (cmd["channel_codec_is_unencrypted"].as()) + CHANNEL_PERM_TEST(permission::b_channel_modify_make_codec_encrypted, 1, true); + } else if (key == "channel_needed_talk_power") { + CHANNEL_PERM_TEST(permission::b_channel_modify_needed_talk_power, 1, true); + update_clients_in_channel = true; + } else if (key == "channel_maxclients" || key == "channel_flag_maxclients_unlimited") { + CHANNEL_PERM_TEST(permission::b_channel_modify_maxclients, 1, true); + require_write_lock = true; + update_max_clients = true; + } else if(key == "channel_maxfamilyclients" || key == "channel_flag_maxfamilyclients_unlimited" || key == "channel_flag_maxfamilyclients_inherited") { + CHANNEL_PERM_TEST(permission::b_channel_modify_maxfamilyclients, 1, true); + require_write_lock = true; + update_max_family_clients = true; + } else if (key == "channel_flag_permanent" || key == "channel_flag_semi_permanent") { + if (cmd[0].has("channel_flag_permanent") && cmd["channel_flag_permanent"].as()) { + CHANNEL_PERM_TEST(permission::b_channel_modify_make_permanent, 1, true); + target_channel_type = ChannelType::permanent; + } else if (cmd[0].has("channel_flag_semi_permanent") && cmd["channel_flag_semi_permanent"].as()) { + CHANNEL_PERM_TEST(permission::b_channel_modify_make_semi_permanent, 1, true); + target_channel_type = ChannelType::semipermanent; + } else { + CHANNEL_PERM_TEST(permission::b_channel_modify_make_temporary, 1, true); + target_channel_type = ChannelType::temporary; + } + target_channel_type_changed = true; + require_write_lock = true; + } else if (key == "channel_delete_delay") { + CHANNEL_PERM_TEST(permission::b_channel_modify_temp_delete_delay, cmd["channel_delete_delay"].as(), true); + } else if (key == "channel_password" || key == "channel_flag_password") { + CHANNEL_PERM_TEST(permission::b_channel_modify_password, 1, true); + update_password = true; + } else { + logCritical( + this->getServerId(), + "The client " + this->getDisplayName() + " tried to change a editable channel property but we haven't found a permission. Please report this error. (Channel property: {})", + key + ); + continue; + } + keys.push_back(property); + } + + unique_lock server_channel_w_lock(this->server ? this->server->channel_tree_lock : serverInstance->getChannelTreeLock(), defer_lock); + if(require_write_lock) { + channel_tree_read_lock.unlock(); + server_channel_w_lock.lock(); + + /* not that while we're waiting to edit the server the channel got deleted... fuck my english */ + if(channel->deleted) + return CommandResult::Success; + } + + /* test the password parameters */ + if(update_password) { + if(!cmd[0].has("channel_password")) { + if(cmd[0].has("channel_flag_password") && cmd["channel_flag_password"].as()) + return {findError("parameter_missing"), ""}; + else + cmd["channel_password"] = ""; /* no password set */ + keys.push_back(property::info(property::CHANNEL_PASSWORD)); + } + if(!cmd[0].has("channel_flag_password")) { + cmd["channel_flag_password"] = !cmd["channel_password"].string().empty(); + keys.push_back(property::info(property::CHANNEL_FLAG_PASSWORD)); + } + + if(cmd["channel_flag_password"].as()) { + if(cmd["channel_password"].string().empty()) + return {findError("channel_invalid_flags")}; /* we cant enable a password without a given password */ + + /* we've to "encode" the password */ + if(this->getType() == ClientType ::CLIENT_QUERY) + cmd["channel_password"] = base64::encode(digest::sha1(cmd["channel_password"].string())); + } else { + cmd["channel_password"] = ""; /* flag password if false so we set the password to empty */ + } + } + + /* test the default channel update */ + if(cmd[0].has("channel_flag_default") || channel->defaultChannel()) { + if(target_channel_type != ChannelType::permanent) + return {findError("channel_default_require_permanent")}; /* default channel is not allowed to be non permanent */ + + if((cmd[0].has("channel_flag_password") && cmd["channel_flag_password"].as()) || channel->properties()[property::CHANNEL_FLAG_PASSWORD]) { + cmd["channel_flag_password"] = false; + cmd["channel_password"] = ""; + keys.push_back(property::info(property::CHANNEL_FLAG_PASSWORD)); + } + + if(cmd[0].has("channel_flag_default")) { + cmd["channel_maxclients"] = -1; + cmd["channel_flag_maxclients_unlimited"] = true; + keys.push_back(property::info(property::CHANNEL_MAXCLIENTS)); + keys.push_back(property::info(property::CHANNEL_FLAG_MAXCLIENTS_UNLIMITED)); + update_max_clients = true; + + cmd["channel_maxfamilyclients"] = -1; + cmd["channel_flag_maxfamilyclients_inherited"] = true; + keys.push_back(property::info(property::CHANNEL_MAXFAMILYCLIENTS)); + keys.push_back(property::info(property::CHANNEL_FLAG_MAXFAMILYCLIENTS_INHERITED)); + update_max_family_clients = true; + } + } + + /* "fix" max client for temporary channels */ + if(target_channel_type_changed) { + if(target_channel_type == ChannelType::temporary) { + if(channel->properties()[property::CHANNEL_MAXCLIENTS].as() != -1) { + cmd["channel_maxclients"] = -1; + cmd["channel_flag_maxclients_unlimited"] = true; + keys.push_back(property::info(property::CHANNEL_MAXCLIENTS)); + keys.push_back(property::info(property::CHANNEL_FLAG_MAXCLIENTS_UNLIMITED)); + update_max_clients = true; + } + if(channel->properties()[property::CHANNEL_MAXFAMILYCLIENTS].as() != -1) { + cmd["channel_maxfamilyclients"] = -1; + cmd["channel_flag_maxfamilyclients_inherited"] = true; + keys.push_back(property::info(property::CHANNEL_MAXFAMILYCLIENTS)); + keys.push_back(property::info(property::CHANNEL_FLAG_MAXFAMILYCLIENTS_INHERITED)); + update_max_family_clients = true; + } + } + + if(target_channel_type != ChannelType::permanent) { + /* test if any child is the default channel */ + for(const auto& child : channel_tree->channels(channel)) + if(child->defaultChannel()) + return {findError("channel_default_require_permanent")}; /* default channel is not allowed to be non permanent */ + } + auto parent = channel->parent(); + if(parent && parent->channelType() > target_channel_type) + return {findError("channel_parent_not_permanent")}; + } + + /* test the max clients parameters */ + if(update_max_clients) { + if(!cmd[0].has("channel_maxclients")) { + if(cmd[0].has("channel_flag_maxclients_unlimited") && cmd["channel_flag_maxclients_unlimited"].as()) + cmd["channel_maxclients"] = -1; + else + return {findError("parameter_missing"), "channel_maxclients"}; /* max clients must be specified */ + keys.push_back(property::info(property::CHANNEL_MAXCLIENTS)); + } + + if(!cmd[0].has("channel_flag_maxclients_unlimited")) { + cmd["channel_flag_maxclients_unlimited"] = cmd["channel_maxclients"].as() < 0; + keys.push_back(property::info(property::CHANNEL_FLAG_MAXCLIENTS_UNLIMITED)); + } + + if(cmd["channel_flag_maxclients_unlimited"].as() && cmd["channel_maxclients"].as() != -1) + return {findError("channel_invalid_flags")}; /* channel cant have a max client settings AND be unlimited as well */ + + if(!cmd["channel_flag_maxclients_unlimited"].as() && target_channel_type == ChannelType::temporary) + return {findError("channel_invalid_flags")}; /* temporary channels cant have a limited user count */ + } + + /* test the max family clients parameters */ + if(update_max_family_clients) { + if(!cmd[0].has("channel_maxfamilyclients")) { + if(cmd[0].has("channel_flag_maxfamilyclients_unlimited")) { + if(cmd["channel_flag_maxfamilyclients_unlimited"].as()) + cmd["channel_flag_maxfamilyclients_inherited"] = false; + else + cmd["channel_flag_maxfamilyclients_inherited"] = true; + keys.push_back(property::info(property::CHANNEL_FLAG_MAXFAMILYCLIENTS_INHERITED)); + } else if(cmd[0].has("channel_flag_maxfamilyclients_inherited")) { + if(cmd["channel_flag_maxfamilyclients_inherited"].as()) + cmd["channel_flag_maxfamilyclients_unlimited"] = false; + else + cmd["channel_flag_maxfamilyclients_unlimited"] = true; + keys.push_back(property::info(property::CHANNEL_FLAG_MAXFAMILYCLIENTS_UNLIMITED)); + } else /* not really possible */ + return {findError("parameter_missing"), "channel_maxfamilyclients"}; /* family max clients must be */ + cmd["channel_maxfamilyclients"] = -1; + keys.push_back(property::info(property::CHANNEL_MAXFAMILYCLIENTS)); + } + //keep this order because this command: "channeledit cid= channel_maxfamilyclients=-1" should set max family clients mode to inherited + if(!cmd[0].has("channel_flag_maxfamilyclients_inherited")) { + auto flag_unlimited = cmd[0].has("channel_flag_maxfamilyclients_unlimited") && cmd["channel_flag_maxfamilyclients_unlimited"].as(); + if(flag_unlimited) + cmd["channel_flag_maxfamilyclients_inherited"] = false; + else + cmd["channel_flag_maxfamilyclients_inherited"] = cmd["channel_maxfamilyclients"].as() < 0; + } + if(!cmd[0].has("channel_flag_maxfamilyclients_unlimited")) { + auto flag_inherited = cmd[0].has("channel_flag_maxfamilyclients_inherited") && cmd["channel_flag_maxfamilyclients_inherited"].as(); + if(flag_inherited) + cmd["channel_flag_maxfamilyclients_unlimited"] = false; + else + cmd["channel_flag_maxfamilyclients_unlimited"] = cmd["channel_maxfamilyclients"].as() < 0; + } + + if(cmd["channel_flag_maxfamilyclients_inherited"].as() && cmd["channel_flag_maxfamilyclients_unlimited"].as()) + return {findError("channel_invalid_flags")}; /* both at the same time are not possible */ + + if(cmd["channel_flag_maxfamilyclients_inherited"].as() && cmd["channel_maxfamilyclients"].as() != -1) + return {findError("channel_invalid_flags")}; /* flag inherited required max users to be -1 */ + + if(cmd["channel_flag_maxfamilyclients_unlimited"].as() && cmd["channel_maxfamilyclients"].as() != -1) + return {findError("channel_invalid_flags")}; /* flag unlimited required max users to be -1 */ + + if(cmd["channel_maxfamilyclients"].as() != -1 && target_channel_type == ChannelType::temporary) + return {findError("channel_invalid_flags")}; /* temporary channels cant have a limited user count */ + } + + /* test the channel name */ + if(update_name) { + auto named_channel = channel_tree->findChannel(cmd["channel_name"].string(), channel->parent()); + if (named_channel) + return {findError("channel_name_inuse"), to_string(named_channel->channelId())}; + } + + shared_ptr old_default_channel; + deque> child_channel_updated; + for(const std::shared_ptr& key : keys) { + if(*key == property::CHANNEL_ORDER) { + /* TODO: May move that up because if it fails may some other props have already be applied */ + if (!channel_tree->change_order(channel, cmd[key->name])) + return {findError("channel_invalid_order"), "Can't change order id"}; + + if(this->server) { + auto parent = channel->hasParent() ? channel_tree->findLinkedChannel(channel->parent()->channelId()) : nullptr; + auto previous = channel_tree->findLinkedChannel(channel->previousChannelId()); + + this->server->forEachClient([&](const shared_ptr& cl) { + unique_lock client_channel_lock(cl->channel_lock); + + auto actions = cl->channels->change_order(l_channel, parent, previous); + std::deque deletions; + for(const auto& action : actions) { + switch (action.first) { + case ClientChannelView::NOTHING: + continue; + case ClientChannelView::ENTER_VIEW: + cl->notifyChannelShow(action.second->channel(), action.second->previous_channel); + break; + case ClientChannelView::DELETE_VIEW: + deletions.push_back(action.second->channelId()); + break; + case ClientChannelView::MOVE: + cl->notifyChannelMoved(action.second->channel(), action.second->previous_channel, this->ref()); + break; + case ClientChannelView::REORDER: + cl->notifyChannelEdited(action.second->channel(), {"channel_order"}, this); + break; + } + } + if(!deletions.empty()) { + cl->notifyChannelHide(deletions, false); + return; //Channel got deleted so we dont have to send the updates + } + }); + } + } else if(*key == property::CHANNEL_FLAG_DEFAULT) { + old_default_channel = channel_tree->getDefaultChannel(); + if(old_default_channel == channel) { + old_default_channel = nullptr; + continue; + } + + if(!cmd[key->name].as()) { + old_default_channel = nullptr; + continue; + } + + channel_tree->setDefaultChannel(channel); + + deque> updated_channels; + shared_ptr current_channel = channel; + do { + if(current_channel->channelType() == ChannelType::permanent) + break; + + current_channel->setChannelType(ChannelType::permanent); + if(current_channel != channel) + updated_channels.push_back(channel); + } while ((current_channel = current_channel->parent())); + + if(this->server && !updated_channels.empty()) { + std::reverse(updated_channels.begin(), updated_channels.end()); + this->server->forEachClient([&](const shared_ptr& client) { + unique_lock client_channel_lock(client->channel_lock); + for(const auto& channel : updated_channels) { + client->notifyChannelEdited(channel, {"channel_flag_permanent", "channel_flag_semi_permanent"}, this); + } + }); + } + } else if(*key == property::CHANNEL_FLAG_PERMANENT || *key == property::CHANNEL_FLAG_SEMI_PERMANENT) { + if(target_channel_type_changed) { /* must be true else the key would not appere here */ + target_channel_type_changed = false; /* we only need to check all subchannels once! */ + + + { /* check channel children */ + deque> channel_to_test = {channel}; + + while(!channel_to_test.empty()) { + auto current_channel = channel_to_test.front(); + channel_to_test.pop_front(); + + for(const auto& child : channel_tree->channels(current_channel, 1)) { + if(child == current_channel) + continue; + + if(child->channelType() < target_channel_type) { + child->setChannelType(target_channel_type); + channel_to_test.push_back(child); + child_channel_updated.push_back(child); + } + } + } + } + } + } + + channel->properties()[key] = cmd[key->name].string(); + } + if(this->server) { + vector key_vector; + key_vector.reserve(keys.size()); + for(const auto& key : keys) + key_vector.push_back(key->name); + + this->server->forEachClient([&](const shared_ptr& client) { + unique_lock client_channel_lock(client->channel_lock); + + for(const auto& channel : child_channel_updated) + client->notifyChannelEdited(channel, {"channel_flag_permanent", "channel_flag_semi_permanent"}, this); + + client->notifyChannelEdited(channel, key_vector, this); + + if(old_default_channel) /* clients need to have one or more defualt channels... */ + client->notifyChannelEdited(old_default_channel, {"channel_flag_default"}, this); + }); + } + + if(server_channel_w_lock.owns_lock()) + server_channel_w_lock.unlock(); + + if(!channel_tree_read_lock.owns_lock()) + channel_tree_read_lock.lock(); + + if(update_clients_in_channel && this->server) { + for(const auto& client : this->server->getClientsByChannel(channel)) + client->updateChannelClientProperties(true, true); //TODO: May only update the talk power and not all? + } + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandChannelMove(Command &cmd) { + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + RESOLVE_CHANNEL_W(cmd["cid"], true); + auto channel = dynamic_pointer_cast(l_channel->entry); + assert(channel); + if(channel->deleted) + return {findError("channel_is_deleted"), "target channel has been deleted"}; + + if(!cmd[0].has("order")) + cmd["order"] = 0; + + auto l_parent = channel_tree->findLinkedChannel(cmd["cpid"]); + shared_ptr l_order; + if(cmd[0].has("order")) { + l_order = channel_tree->findLinkedChannel(cmd["order"]); + if (!l_order && cmd["order"].as() != 0) return {findError("channel_invalid_id"), "Cant resolve order channel"}; + } else { + l_order = l_parent ? l_parent->child_head : (this->server ? this->server->getChannelTree() : serverInstance->getChannelTree().get())->tree_head(); + while(l_order && l_order->next) + l_order = l_order->next; + } + + auto parent = l_parent ? dynamic_pointer_cast(l_parent->entry) : nullptr; + auto order = l_order ? dynamic_pointer_cast(l_order->entry) : nullptr; + + if((parent && parent->deleted) || (order && order->deleted)) + return {findError("channel_is_deleted"), "parent channel order previous channel has been deleted"}; + + if(channel->parent() == parent && channel->channelOrder() == (order ? order->channelId() : 0)) + return CommandResult::Success; + + if (channel->parent() != parent) + PERM_CHECK_CHANNELR(permission::b_channel_modify_parent, 1, channel, true); + if ((order ? order->channelId() : 0) != channel->channelOrder()) + PERM_CHECK_CHANNELR(permission::b_channel_modify_sortorder, 1, channel, true); + + { + + auto min_channel_deep = this->permissionValue(permission::PERMTEST_ORDERED, permission::i_channel_min_depth, nullptr, nullptr); + auto max_channel_deep = this->permissionValue(permission::PERMTEST_ORDERED, permission::i_channel_max_depth, nullptr, nullptr); + + if(min_channel_deep >= 0 || max_channel_deep >= 0) { + auto channel_deep = 0; + auto local_parent = l_parent; + while(local_parent) { + channel_deep++; + local_parent = local_parent->parent.lock(); + } + + if(min_channel_deep >= 0 && channel_deep < min_channel_deep) return CommandResultPermissionError{permission::i_channel_min_depth}; + if(max_channel_deep >= 0 && channel_deep > max_channel_deep) return CommandResultPermissionError{permission::i_channel_max_depth}; + } + } + { + auto name = channel_tree->findChannel(channel->name(), parent); + if(name && name != channel) return {findError("channel_invalid_name"), "Channel with this name already exists"}; + } + debugMessage(this->getServerId(), "Moving channel {} from old [{} | {}] to [{} | {}]", channel->name(), channel->channelOrder(), channel->parent() ? channel->parent()->channelId() : 0, order ? order->channelId() : 0, parent ? parent->channelId() : 0); + + if (!channel_tree->move_channel(channel, parent, order)) return {findError("channel_invalid_order"), "Cant change order id"}; + + deque> channel_type_updates; + { + auto flag_default = channel->defaultChannel(); + auto current_channel = channel; + do { + if(flag_default) { + if(current_channel->channelType() != ChannelType::permanent) { + current_channel->setChannelType(ChannelType::permanent); + channel_type_updates.push_front(current_channel); + } + } else if(current_channel->hasParent()) { + if(current_channel->channelType() < current_channel->parent()->channelType()) { + current_channel->setChannelType(current_channel->parent()->channelType()); + channel_type_updates.push_front(current_channel); + } + } + } while ((current_channel = dynamic_pointer_cast(current_channel->parent()))); + } + + if(this->server) { + this->server->forEachClient([&](const shared_ptr& client) { + unique_lock channel_lock(client->channel_lock); + for(const auto& type_update : channel_type_updates) + client->notifyChannelEdited(type_update, {"channel_flag_permanent", "channel_flag_semi_permanent"}, this); + + auto actions = client->channels->change_order(l_channel, l_parent, l_order); + std::deque deletions; + for(const auto& action : actions) { + switch (action.first) { + case ClientChannelView::NOTHING: + continue; + case ClientChannelView::ENTER_VIEW: + client->notifyChannelShow(action.second->channel(), action.second->previous_channel); + break; + case ClientChannelView::DELETE_VIEW: + deletions.push_back(action.second->channelId()); + break; + case ClientChannelView::MOVE: + client->notifyChannelMoved(action.second->channel(), action.second->previous_channel, _this.lock()); + break; + case ClientChannelView::REORDER: + client->notifyChannelEdited(action.second->channel(), vector{"channel_order"}, _this.lock().get()); + break; + } + } + if(!deletions.empty()) + client->notifyChannelHide(deletions, false); + }); + } + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientPoke(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + auto client = this->server->findClient(cmd["clid"].as()); + if (!client) return {findError("client_invalid_id"), "invalid client id"}; + if (client->getType() == CLIENT_MUSIC) return {findError("client_invalid_type"), "You cant poke a music bot!"}; + CACHED_PERM_CHECK(permission::i_client_poke_power, client->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_needed_poke_power, client->currentChannel)); + + client->notifyClientPoke(_this.lock(), cmd["msg"]); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandChannelPermList(Command &cmd) { + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + RESOLVE_CHANNEL_R(cmd["cid"], true); + auto channel = dynamic_pointer_cast(l_channel->entry); + assert(channel); + + CHANNEL_PERM_TEST_INIT; + CHANNEL_PERM_TEST(permission::b_virtualserver_channel_permission_list, 1, true); + + if(this->getType() == ClientType::CLIENT_TEAMSPEAK && this->command_times.last_notify + this->command_times.notify_timeout < system_clock::now()) { + this->sendTSPermEditorWarning(); + } + + auto sids = cmd.hasParm("permsid"); + + Command result(this->notify_response_command("notifychannelpermlist")); + int index = 0; + result["cid"] = channel->channelId(); + + 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) { + if(sids) { + result[index]["permsid"] = permission::resolvePermissionData(std::get<0>(permission_data))->name; + } else { + result[index]["permid"] = std::get<0>(permission_data); + } + + result[index]["permvalue"] = permission.values.value; + result[index]["permnegated"] = permission.flags.negate; + result[index]["permskip"] = permission.flags.skip; + index++; + } + if(permission.flags.grant_set) { + if(sids) { + result[index]["permsid"] = permission::resolvePermissionData(std::get<0>(permission_data))->grant_name; + } else { + result[index]["permid"] = (uint16_t) (std::get<0>(permission_data) | PERM_ID_GRANT); + } + result[index]["permvalue"] = permission.values.grant; + result[index]["permnegated"] = 0; + result[index]["permskip"] = 0; + index++; + } + } + if(index == 0) + return {ErrorType::DBEmpty}; + + this->sendCommand(result); + return CommandResult::Success; +} + +// +//channel_icon_id=18446744073297259750 +//channel_name +//channel_topic +//Desctiption has no extra parm +CommandResult ConnectedClient::handleCommandChannelAddPerm(Command &cmd) { + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + RESOLVE_CHANNEL_R(cmd["cid"], true); + auto channel = dynamic_pointer_cast(l_channel->entry); + assert(channel); + + CHANNEL_PERMISSION_TEST(permission::i_channel_permission_modify_power, permission::i_channel_needed_permission_modify_power, channel, true); + + auto maxValue = this->getPermissionGrantValue(permission::PERMTEST_ORDERED, permission::i_permission_modify_power, channel); + bool ignoreGrant = this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_permission_modify_power_ignore, 1, channel); + auto updateClients = false, update_view = false, update_channel_properties = false; + + CommandResult command_result = CommandResult::Success; + bool conOnError = cmd[0].has("continueonerror"); + + auto permission_manager = channel->permissions(); + for (int index = 0; index < cmd.bulkCount(); index++) { + PARSE_PERMISSION(cmd); + + auto val = cmd[index]["permvalue"].as(); + if(permission_require_granted_value(permType) && val > maxValue) { + command_result = CommandResultPermissionError{permission::i_permission_modify_power}; + break; + } + + if(!ignoreGrant && !this->permissionGrantGranted(permission::PERMTEST_ORDERED, permType, 1, channel)) { + command_result = CommandResultPermissionError{permission::i_permission_modify_power}; + break; + } + + if (grant) { + permission_manager->set_permission(permType, {0, cmd[index]["permvalue"]}, permission::v2::PermissionUpdateType::do_nothing, permission::v2::PermissionUpdateType::set_value); + } else { + permission_manager->set_permission( + permType, + {cmd[index]["permvalue"], 0}, + permission::v2::PermissionUpdateType::set_value, + permission::v2::PermissionUpdateType::do_nothing, + + cmd[index]["permnegated"].as() ? 1 : 0, + cmd[index]["permskip"].as() ? 1 : 0 + ); + updateClients |= permission_is_client_property(permType); + update_view |= permType == permission::i_channel_needed_view_power; + update_channel_properties |= channel->permission_require_property_update(permType); + + if (permType == permission::i_icon_id) { + if(this->server) + this->server->forEachClient([&](std::shared_ptr cl) { + shared_lock client_channel_lock(cl->channel_lock); + cl->notifyChannelEdited(channel, {"channel_icon_id"}, this); + }); + continue; + } + } + } + + /* broadcast the updated channel properties */ + if(update_channel_properties) { + auto updates = channel->update_properties_from_permissions(); + if(!updates.empty() && this->server){ + vector keys; + keys.reserve(updates.size()); + + for(auto& property : updates) + keys.push_back(property::info(property)->name); + + this->server->forEachClient([&](std::shared_ptr cl) { + shared_lock client_channel_lock(cl->channel_lock); + cl->notifyChannelEdited(channel, keys, this); + }); + } + } + + if(updateClients && this->server) + for(const auto& client : this->server->getClientsByChannel(channel)) { + /* let them lock the server channel tree as well (read lock so does not matter) */ + client->updateChannelClientProperties(true, true); + } + + if(update_view && this->server) + this->server->forEachClient([&](const shared_ptr& cl) { + /* server tree read lock still active */ + auto l_source = cl->server->channelTree->findLinkedChannel(channel->channelId()); + auto l_target = !cl->currentChannel ? nullptr : cl->server->channelTree->findLinkedChannel(cl->currentChannel->channelId()); + sassert(l_source); + if(cl->currentChannel) sassert(l_target); + + { + unique_lock client_channel_lock(cl->channel_lock); + + deque deleted; + for(const auto& update_entry : cl->channels->update_channel(l_source, l_target)) { + if(update_entry.first) + cl->notifyChannelShow(update_entry.second->channel(), update_entry.second->previous_channel); + else deleted.push_back(update_entry.second->channelId()); + } + if(!deleted.empty()) + cl->notifyChannelHide(deleted, false); + } + }); + return command_result; +} + +CommandResult ConnectedClient::handleCommandChannelDelPerm(Command &cmd) { + CMD_RESET_IDLE; + + RESOLVE_CHANNEL_R(cmd["cid"], true) + auto channel = dynamic_pointer_cast(l_channel->entry); + assert(channel); + CHANNEL_PERMISSION_TEST(permission::i_channel_permission_modify_power, permission::i_channel_needed_permission_modify_power, channel, true); + + bool ignoreGrant = this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_permission_modify_power_ignore, 1, channel); + bool conOnError = cmd[0].has("continueonerror"); + auto updateClients = false, update_view = false, update_channel_properties = false; + + auto permission_manager = channel->permissions(); + for (int index = 0; index < cmd.bulkCount(); index++) { + PARSE_PERMISSION(cmd); + + if(!ignoreGrant && !this->permissionGrantGranted(permission::PERMTEST_ORDERED, permType, 1, channel)) + return CommandResultPermissionError{permission::i_permission_modify_power}; + + if (grant) { + permission_manager->set_permission(permType, permission::v2::empty_permission_values, permission::v2::PermissionUpdateType::do_nothing, permission::v2::PermissionUpdateType::delete_value); + } else { + permission_manager->set_permission(permType, permission::v2::empty_permission_values, permission::v2::PermissionUpdateType::delete_value, permission::v2::PermissionUpdateType::do_nothing); + updateClients |= permission_is_client_property(permType); + update_view |= permType == permission::i_channel_needed_view_power; + update_channel_properties |= channel->permission_require_property_update(permType); + } + } + + /* broadcast the updated channel properties */ + if(update_channel_properties) { + auto updates = channel->update_properties_from_permissions(); + if(!updates.empty() && this->server){ + vector keys; + keys.reserve(updates.size()); + + for(auto& property : updates) + keys.push_back(property::info(property)->name); + + this->server->forEachClient([&](std::shared_ptr cl) { + shared_lock client_channel_lock(cl->channel_lock); + cl->notifyChannelEdited(channel, keys, this); + }); + } + } + + if(updateClients && this->server) + this->server->forEachClient([&](std::shared_ptr cl) { + if(cl->currentChannel == channel) + cl->updateChannelClientProperties(true, true); + }); + if(update_view && this->server) { + this->server->forEachClient([&](std::shared_ptr cl) { + /* server tree read lock still active */ + auto l_source = cl->server->channelTree->findLinkedChannel(channel->channelId()); + auto l_target = !cl->currentChannel ? nullptr : cl->server->channelTree->findLinkedChannel(cl->currentChannel->channelId()); + sassert(l_source); + if(cl->currentChannel) sassert(l_target); + + { + unique_lock client_channel_lock(cl->channel_lock); + + deque deleted; + for(const auto& update_entry : cl->channels->update_channel(l_source, l_target)) { + if(update_entry.first) + cl->notifyChannelShow(update_entry.second->channel(), update_entry.second->previous_channel); + else deleted.push_back(update_entry.second->channelId()); + } + if(!deleted.empty()) + cl->notifyChannelHide(deleted, false); + } + }); + } + return CommandResult::Success; +} + +//servergroupadd name=TestGroup type=1 +CommandResult ConnectedClient::handleCommandServerGroupAdd(Command &cmd) { + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + CACHED_PERM_CHECK(permission::b_virtualserver_servergroup_create, 1, true); + if(cmd["name"].string().empty()) return {findError("parameter_invalid"), "invalid group name"}; + + if(cmd["type"].as() == GroupType::GROUP_TYPE_QUERY) { + if(!this->permission_granted(this->cached_permission_value(permission::b_serverinstance_modify_querygroup), 1, true)) + return CommandResultPermissionError{permission::b_serverinstance_modify_querygroup}; + } else if(cmd["type"].as() == GroupType::GROUP_TYPE_TEMPLATE) { + if(!this->permission_granted(this->cached_permission_value(permission::b_serverinstance_modify_templates), 1, true)) + return CommandResultPermissionError{permission::b_serverinstance_modify_templates}; + } else if(!this->server) return {findError("parameter_invalid"), "you cant create normal groups on the template server!"}; + + auto group_manager = this->server ? this->server->getGroupManager() : serverInstance->getGroupManager().get(); + for(const auto& gr : group_manager->availableServerGroups(true)) + if(gr->name() == cmd["name"].string() && gr->target() == GroupTarget::GROUPTARGET_SERVER) return {findError("parameter_invalid"), "Group already exists"}; + auto group = group_manager->createGroup(GroupTarget::GROUPTARGET_SERVER, cmd["type"].as(), cmd["name"].string()); + if (group) { + group->permissions()->set_permission(permission::b_group_is_permanent, {1,0}, permission::v2::set_value, permission::v2::do_nothing); + if(this->server) + this->server->forEachClient([](shared_ptr cl) { + cl->notifyServerGroupList(); + }); + } else return {ErrorType::VSError, "Could not create group"}; + return CommandResult::Success; +} + +//name=Server\sAdmin\s(Copy) ssgid=1 tsgid=0 type=1 +CommandResult ConnectedClient::handleCommandServerGroupCopy(Command &cmd) { + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + CACHED_PERM_CHECK(permission::b_virtualserver_servergroup_create, 1, true); + + auto group_manager = this->server ? this->server->groups : serverInstance->getGroupManager().get(); + + bool global = false; + auto src = group_manager->findGroup(cmd["ssgid"].as()); + if (!src || src->target() != GROUPTARGET_SERVER) return {findError("parameter_invalid"), "invalid server group id"}; + if(cmd[0].has("tsgid") && cmd["tsgid"].as() != 0) { + auto target = group_manager->findGroup(cmd["tsgid"].as()); + if (!target || target->target() != GROUPTARGET_SERVER) return {findError("parameter_invalid"), "invalid target server group id"}; + auto result = group_manager->copyGroupPermissions(src, target); + if(!result) return {findError("vs_critical"), "could not copy group!"}; + global = !this->server || group_manager->isLocalGroup(target); + } else { + //GroupType + auto type = cmd["type"].as(); + if(type == GroupType::GROUP_TYPE_NORMAL && !this->server) return {findError("parameter_invalid"), "You cant create normal groups on the template server!"}; + if(type == GroupType::GROUP_TYPE_QUERY) { + if(!this->permission_granted(this->cached_permission_value(permission::b_serverinstance_modify_querygroup), 1, true)) + return CommandResultPermissionError{permission::b_serverinstance_modify_querygroup}; + } else if(type == GroupType::GROUP_TYPE_TEMPLATE) { + if(!this->permission_granted(this->cached_permission_value(permission::b_serverinstance_modify_templates), 1, true)) + return CommandResultPermissionError{permission::b_serverinstance_modify_templates}; + } + + auto result = group_manager->copyGroup(src, type, cmd["name"], type != GroupType::GROUP_TYPE_NORMAL ? 0 : this->getServerId()); //TODO maybe check by name? No duplicated groups? + if (!result) return {findError("vs_critical"), "could not copy group!"}; + global = !this->server || type != GroupType::GROUP_TYPE_NORMAL; + + if(this->getType() == ClientType::CLIENT_QUERY) { + Command notify(""); + notify["sgid"] = group_manager->availableServerGroups(false).back()->groupId(); + this->sendCommand(notify); + } + } + + for(const auto& server : (global ? serverInstance->getVoiceServerManager()->serverInstances() : deque>{this->server})) + if(server) + server->forEachClient([](shared_ptr cl) { + cl->notifyServerGroupList(); + }); + return CommandResult::Success; +} + +//servergrouprename sgid=2 name=Operators +CommandResult ConnectedClient::handleCommandServerGroupRename(Command &cmd) { + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + auto group_manager = this->server ? this->server->getGroupManager() : serverInstance->getGroupManager().get(); + + auto serverGroup = group_manager->findGroup(cmd["sgid"].as()); + if (!serverGroup || serverGroup->target() != GROUPTARGET_SERVER) return {findError("parameter_invalid"), "invalid server group id"}; + GROUP_PERMISSION_TEST(permission::i_server_group_modify_power, permission::i_server_group_needed_modify_power, serverGroup, true); + + auto type = serverGroup->type(); + if(type == GroupType::GROUP_TYPE_QUERY) { + if(!this->permission_granted(this->cached_permission_value(permission::b_serverinstance_modify_querygroup), 1, true)) + return CommandResultPermissionError{permission::b_serverinstance_modify_querygroup}; + } else if(type == GroupType::GROUP_TYPE_TEMPLATE) { + if(!this->permission_granted(this->cached_permission_value(permission::b_serverinstance_modify_templates), 1, true)) + return CommandResultPermissionError{permission::b_serverinstance_modify_templates}; + } + + group_manager->renameGroup(serverGroup, cmd["name"].string()); + if(this->server) + this->server->forEachClient([](shared_ptr cl) { + cl->notifyServerGroupList(); + }); + + return CommandResult::Success; +} + +//servergroupdel sgid=2 force=0 +CommandResult ConnectedClient::handleCommandServerGroupDel(Command &cmd) { + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + CACHED_PERM_CHECK(permission::b_virtualserver_servergroup_delete, 1, true); + auto group_manager = this->server ? this->server->getGroupManager() : serverInstance->getGroupManager().get(); + auto serverGroup = group_manager->findGroup(cmd["sgid"].as()); + if (!serverGroup || serverGroup->target() != GROUPTARGET_SERVER) return {findError("parameter_invalid"), "invalid server group id"}; + + if(this->server && this->server->properties()[property::VIRTUALSERVER_DEFAULT_SERVER_GROUP] == serverGroup->groupId()) + return {findError("parameter_invalid"), "Could not delete default server group!"}; + if(serverInstance->properties()[property::SERVERINSTANCE_TEMPLATE_SERVERADMIN_GROUP] == serverGroup->groupId()) + return {findError("parameter_invalid"), "Could not delete instance default server admin group!"}; + if(serverInstance->properties()[property::SERVERINSTANCE_TEMPLATE_SERVERDEFAULT_GROUP] == serverGroup->groupId()) + return {findError("parameter_invalid"), "Could not delete instance default server group!"}; + if(serverInstance->properties()[property::SERVERINSTANCE_GUEST_SERVERQUERY_GROUP] == serverGroup->groupId()) + return {findError("parameter_invalid"), "Could not delete instance default guest server query group!"}; + + auto type = serverGroup->type(); + if(type == GroupType::GROUP_TYPE_QUERY) { + if(!this->permission_granted(this->cached_permission_value(permission::b_serverinstance_modify_querygroup), 1, true)) + return CommandResultPermissionError{permission::b_serverinstance_modify_querygroup}; + } else if(type == GroupType::GROUP_TYPE_TEMPLATE) { + if(!this->permission_granted(this->cached_permission_value(permission::b_serverinstance_modify_templates), 1, true)) + return CommandResultPermissionError{permission::b_serverinstance_modify_templates}; + } + + if (!cmd["force"].as()) + if (!group_manager->listGroupMembers(serverGroup, false).empty()) + return {findError("database_empty_result"), "group not empty!"}; + + if (group_manager->deleteGroup(serverGroup)) { + if(this->server) + this->server->forEachClient([&](shared_ptr cl) { + if(this->server->notifyClientPropertyUpdates(cl, this->server->groups->update_server_group_property(cl, true))) { + if(cl->update_cached_permissions()) /* update cached calculated permissions */ + cl->sendNeededPermissions(false); /* cached permissions had changed, notify the client */ + } + cl->notifyServerGroupList(); + }); + } + + return CommandResult::Success; +} + +//servergroupclientlist sgid=2 +//notifyservergroupclientlist sgid=6 cldbid=2 client_nickname=WolverinDEV client_unique_identifier=xxjnc14LmvTk+Lyrm8OOeo4tOqw= +CommandResult ConnectedClient::handleCommandServerGroupClientList(Command &cmd) { + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + CACHED_PERM_CHECK(permission::b_virtualserver_servergroup_client_list, 1, true); + + auto server = cmd[0].has("sid") && cmd["sid"] == 0 ? nullptr : this->server; + auto groupManager = server ? this->server->groups : serverInstance->getGroupManager().get(); + + auto serverGroup = groupManager->findGroup(cmd["sgid"].as()); + if (!serverGroup || serverGroup->target() != GROUPTARGET_SERVER) return {findError("parameter_invalid"), "invalid server group id"}; + + Command notify(this->getExternalType() == ClientType::CLIENT_TEAMSPEAK ? "notifyservergroupclientlist" : ""); + notify["sgid"] = cmd["sgid"].as(); + int index = 0; + for (const auto &clientEntry : groupManager->listGroupMembers(serverGroup)) { + notify[index]["cldbid"] = clientEntry->cldbId; + notify[index]["client_nickname"] = clientEntry->displayName; + notify[index]["client_unique_identifier"] = clientEntry->uid; + index++; + } + this->sendCommand(notify); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandServerGroupAddClient(Command &cmd) { + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + auto server = cmd[0].has("sid") && cmd["sid"] == 0 ? nullptr : this->server; + auto groupManager = server ? this->server->groups : serverInstance->getGroupManager().get(); + auto serverGroup = groupManager->findGroup(cmd["sgid"].as()); + if (!serverGroup) return {findError("parameter_invalid"), "invalid server group id"}; + if((server != this->server || !this->server) && serverGroup->target() != GroupTarget::GROUPTARGET_SERVER && serverGroup->type() != GroupType::GROUP_TYPE_QUERY) return {findError("parameter_invalid"), "invalid group type"}; + + auto target_cldbid = cmd["cldbid"].as(); + { + if(!serverGroup->permission_granted(permission::i_server_group_needed_member_add_power, this->calculate_permission_value(permission::i_server_group_member_add_power, -1), true)) { + if(target_cldbid != this->getClientDatabaseId()) + return CommandResultPermissionError{permission::i_server_group_member_add_power}; + if(!serverGroup->permission_granted(permission::i_server_group_needed_member_add_power, this->calculate_permission_value(permission::i_server_group_self_add_power, -1), true)) + return CommandResultPermissionError{permission::i_server_group_self_add_power}; + } + + auto needed_client_permission = this->server->calculatePermission(permission::PERMTEST_ORDERED, target_cldbid, permission::i_client_needed_permission_modify_power, ClientType::CLIENT_TEAMSPEAK,nullptr); + if(needed_client_permission != permNotGranted) { + if(!this->permission_granted(this->permissionValue(permission::i_client_permission_modify_power), needed_client_permission)) + return CommandResultPermissionError{permission::i_client_needed_permission_modify_power}; + } + } + + if (!serverInstance->databaseHelper()->validClientDatabaseId(server, cmd["cldbid"])) return {findError("client_invalid_id"), "invalid cldbid"}; + if(groupManager->hasServerGroupAssigned(cmd["cldbid"], serverGroup)) return {findError("parameter_invalid"), "Client is already member of this group"}; + + groupManager->addServerGroup(target_cldbid, serverGroup); + + for(const auto& _server : server ? std::deque>{server} : serverInstance->getVoiceServerManager()->serverInstances()) { + for (const auto &targetClient : _server->findClientsByCldbId(target_cldbid)) { + if (_server->notifyClientPropertyUpdates(targetClient, _server->groups->update_server_group_property(targetClient,true))) { + for (const auto &client : _server->getClients()) { + if(client->isClientVisible(targetClient, true) || client == targetClient) + client->notifyServerGroupClientAdd(_this.lock(), targetClient, serverGroup); + } + if(targetClient->update_cached_permissions()) /* update cached calculated permissions */ + targetClient->sendNeededPermissions(false); /* cached permissions had changed, notify the client */ + targetClient->updateChannelClientProperties(true, true); + } + } + } + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandServerGroupDelClient(Command &cmd) { + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + auto server = cmd[0].has("sid") && cmd["sid"] == 0 ? nullptr : this->server; + auto groupManager = server ? this->server->groups : serverInstance->getGroupManager().get(); + auto serverGroup = groupManager->findGroup(cmd["sgid"].as()); + if (!serverGroup) return {findError("parameter_invalid"), "invalid server group id"}; + if((server != this->server || !this->server) && serverGroup->target() != GroupTarget::GROUPTARGET_SERVER && serverGroup->type() != GroupType::GROUP_TYPE_QUERY) return {findError("parameter_invalid"), "invalid group type"}; + + auto target_cldbid = cmd["cldbid"].as(); + { + if(!serverGroup->permission_granted(permission::i_server_group_needed_member_remove_power, this->calculate_permission_value(permission::i_server_group_member_remove_power, -1), true)) { + if(target_cldbid != this->getClientDatabaseId()) + return CommandResultPermissionError{permission::i_server_group_member_remove_power}; + if(!serverGroup->permission_granted(permission::i_server_group_needed_member_remove_power, this->calculate_permission_value(permission::i_server_group_self_remove_power, -1), true)) + return CommandResultPermissionError{permission::i_server_group_self_remove_power}; + } + + auto needed_client_permission = this->server->calculatePermission(permission::PERMTEST_ORDERED, target_cldbid, permission::i_client_needed_permission_modify_power, ClientType::CLIENT_TEAMSPEAK,nullptr); + if(needed_client_permission != permNotGranted) { + if(!this->permission_granted(this->permissionValue(permission::i_client_permission_modify_power), needed_client_permission)) + return CommandResultPermissionError{permission::i_client_needed_permission_modify_power}; + } + } + + if (!serverInstance->databaseHelper()->validClientDatabaseId(server, cmd["cldbid"])) return {findError("client_invalid_id"), "invalid cldbid"}; + if(!groupManager->hasServerGroupAssigned(cmd["cldbid"], serverGroup)) return {findError("parameter_invalid"), "Client isn't a member of this group"}; + for(const auto& assignment : groupManager->listGroupAssignments(cmd["cldbid"])) + if(assignment->group == serverGroup && assignment->server != this->getServerId()) return {findError("parameter_invalid"), "Group wasn't assigned over this server (Assigned server: " + to_string(assignment->server) + ")"}; + + groupManager->removeServerGroup(target_cldbid, serverGroup); + + for(const auto& _server : server ? std::deque>{server} : serverInstance->getVoiceServerManager()->serverInstances()) { + for (const auto &targetClient : _server->findClientsByCldbId(target_cldbid)) { + if (_server->notifyClientPropertyUpdates(targetClient, _server->groups->update_server_group_property(targetClient, true))) { + for (const auto &client : _server->getClients()) { + if (client->isClientVisible(targetClient, true) || client == targetClient) + client->notifyServerGroupClientRemove(_this.lock(), targetClient, serverGroup); + } + if(targetClient->update_cached_permissions()) /* update cached calculated permissions */ + targetClient->sendNeededPermissions(false); /* cached permissions had changed, notify the client */ + targetClient->updateChannelClientProperties(true, true); + } + } + } + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandServerGroupPermList(Command &cmd) { + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + CACHED_PERM_CHECK(permission::b_virtualserver_servergroup_permission_list, 1, true); + + auto serverGroup = (this->server ? this->server->groups : serverInstance->getGroupManager().get())->findGroup(cmd["sgid"].as()); + if (!serverGroup) return {findError("parameter_invalid"), "invalid server group id"}; + + if(this->getType() == ClientType::CLIENT_TEAMSPEAK && this->command_times.last_notify + this->command_times.notify_timeout < system_clock::now()) { + this->sendTSPermEditorWarning(); + } + if (!this->notifyGroupPermList(serverGroup, cmd.hasParm("permsid"))) return {findError("database_empty_result"), "empty"}; + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandServerGroupAddPerm(Command &cmd) { + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + auto serverGroup = (this->server ? this->server->groups : serverInstance->getGroupManager().get())->findGroup(cmd["sgid"].as()); + if (!serverGroup) return {findError("parameter_invalid"), "invalid server group id"}; + if (serverGroup->target() != GROUPTARGET_SERVER) return {findError("parameter_invalid"), "invalid group type"}; + GROUP_PERMISSION_TEST(permission::i_server_group_modify_power, permission::i_server_group_needed_modify_power, serverGroup, true); + + auto type = serverGroup->type(); + if(type == GroupType::GROUP_TYPE_QUERY) { + if(!this->permission_granted(this->cached_permission_value(permission::b_serverinstance_modify_querygroup), 1, true)) + return CommandResultPermissionError{permission::b_serverinstance_modify_querygroup}; + } else if(type == GroupType::GROUP_TYPE_TEMPLATE) { + if(!this->permission_granted(this->cached_permission_value(permission::b_serverinstance_modify_templates), 1, true)) + return CommandResultPermissionError{permission::b_serverinstance_modify_templates}; + } + + auto maxValue = this->getPermissionGrantValue(permission::PERMTEST_ORDERED, permission::i_permission_modify_power, this->currentChannel); + bool ignoreGrant = this->permission_granted(this->cached_permission_value(permission::b_permission_modify_power_ignore), 1); + bool conOnError = cmd[0].has("continueonerror"); + bool checkTp = false; + bool sgroupUpdate = false; + + auto permissions = serverGroup->permissions(); + for (int index = 0; index < cmd.bulkCount(); index++) { + PARSE_PERMISSION(cmd); + //permvalue='1' permnegated='0' permskip='0' + + auto val = cmd[index]["permvalue"].as(); + if(permission_require_granted_value(permType) && val > maxValue) { + if(conOnError) continue; + return CommandResultPermissionError{permission::i_permission_modify_power}; + } + + if(!ignoreGrant && !this->permissionGrantGranted(permission::PERMTEST_ORDERED, permType, 1, this->currentChannel)) { + if(conOnError) continue; + return CommandResultPermissionError{permission::i_permission_modify_power}; + } + + if (grant) { + permissions->set_permission(permType, {0, cmd[index]["permvalue"]}, permission::v2::PermissionUpdateType::do_nothing, permission::v2::PermissionUpdateType::set_value); + } else { + permissions->set_permission( + permType, + {cmd[index]["permvalue"], 0}, + permission::v2::PermissionUpdateType::set_value, + permission::v2::PermissionUpdateType::do_nothing, + + cmd[index]["permnegated"].as() ? 1 : 0, + cmd[index]["permskip"].as() ? 1 : 0 + ); + sgroupUpdate |= permission_is_group_property(permType); + checkTp |= permission_is_client_property(permType); + } + } + + if(sgroupUpdate) + serverGroup->apply_properties_from_permissions(); + + //TODO may update for every server? + if(this->server) { + auto lock = this->_this.lock(); + auto server = this->server; + threads::Thread([checkTp, sgroupUpdate, serverGroup, lock, server]() { + if(sgroupUpdate) + server->forEachClient([](shared_ptr cl) { + cl->notifyServerGroupList(); + }); + server->forEachClient([serverGroup, checkTp](shared_ptr cl) { + if (cl->serverGroupAssigned(serverGroup)) { + if(cl->update_cached_permissions()) /* update cached calculated permissions */ + cl->sendNeededPermissions(false); /* cached permissions had changed, notify the client */ + if (checkTp) + cl->updateChannelClientProperties(true, true); + } + }); + }).detach(); + } + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandServerGroupDelPerm(Command &cmd) { + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + auto serverGroup = (this->server ? this->server->groups : serverInstance->getGroupManager().get())->findGroup(cmd["sgid"].as()); + if (!serverGroup) return {findError("parameter_invalid"), "invalid server group id"}; + if (serverGroup->target() != GROUPTARGET_SERVER) return {findError("parameter_invalid"), "invalid group type"}; + GROUP_PERMISSION_TEST(permission::i_server_group_modify_power, permission::i_server_group_needed_modify_power, serverGroup, true); + + auto type = serverGroup->type(); + if(type == GroupType::GROUP_TYPE_QUERY) { + if(!this->permission_granted(this->cached_permission_value(permission::b_serverinstance_modify_querygroup), 1, true)) + return CommandResultPermissionError{permission::b_serverinstance_modify_querygroup}; + } else if(type == GroupType::GROUP_TYPE_TEMPLATE) { + if(!this->permission_granted(this->cached_permission_value(permission::b_serverinstance_modify_templates), 1, true)) + return CommandResultPermissionError{permission::b_serverinstance_modify_templates}; + } + + bool ignoreGrant = this->permission_granted(this->cached_permission_value(permission::b_permission_modify_power_ignore), 1); + bool conOnError = cmd[0].has("continueonerror"); + bool checkTp = false; + auto sgroupUpdate = false; + for (int index = 0; index < cmd.bulkCount(); index++) { + PARSE_PERMISSION(cmd); + + if(!ignoreGrant && !this->permissionGrantGranted(permission::PERMTEST_ORDERED, permType, 1, this->currentChannel)) { + if(conOnError) continue; + return CommandResultPermissionError{permission::i_permission_modify_power}; + } + + if (grant) { + serverGroup->permissions()->set_permission(permType, permission::v2::empty_permission_values, permission::v2::PermissionUpdateType::do_nothing, permission::v2::PermissionUpdateType::delete_value); + } else { + serverGroup->permissions()->set_permission( + permType, + permission::v2::empty_permission_values, + permission::v2::PermissionUpdateType::delete_value, + permission::v2::PermissionUpdateType::do_nothing + ); + sgroupUpdate |= permission_is_group_property(permType); + checkTp |= permission_is_client_property(permType); + } + } + + if(sgroupUpdate) + serverGroup->apply_properties_from_permissions(); + + if(this->server) { + auto lock = this->_this.lock(); + auto server = this->server; + threads::Thread([checkTp, sgroupUpdate, serverGroup, lock, server]() { + if(sgroupUpdate) + server->forEachClient([](shared_ptr cl) { + cl->notifyServerGroupList(); + }); + server->forEachClient([serverGroup, checkTp](shared_ptr cl) { + if (cl->serverGroupAssigned(serverGroup)) { + + if(cl->update_cached_permissions()) /* update cached calculated permissions */ + cl->sendNeededPermissions(false); /* cached permissions had changed, notify the client */ + if (checkTp) + cl->updateChannelClientProperties(true, true); + } + }); + }).detach(); + } + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandServerGroupAutoAddPerm(ts::Command& cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + deque> groups; + for(const auto& group : this->server->groups->availableGroups(false)) { + if(group->updateType() == cmd["sgtype"].as() && group->target() == GROUPTARGET_SERVER) { + if(group->permission_granted(permission::i_server_group_needed_modify_power, this->calculate_permission_value(permission::i_server_group_modify_power, 0), true)) { + auto type = group->type(); + if(type == GroupType::GROUP_TYPE_QUERY) { + if(!this->permission_granted(this->cached_permission_value(permission::b_serverinstance_modify_querygroup), 1, true)) + continue; + } else if(type == GroupType::GROUP_TYPE_TEMPLATE) { + if(!this->permission_granted(this->cached_permission_value(permission::b_serverinstance_modify_templates), 1, true)) + continue; + } + groups.push_back(group);//sgtype + } + } + } + + if(groups.empty()) + return CommandResult::Success; + + auto maxValue = this->getPermissionGrantValue(permission::PERMTEST_ORDERED, permission::i_permission_modify_power, this->currentChannel); + bool ignoreGrant = this->permission_granted(this->cached_permission_value(permission::b_permission_modify_power_ignore), 1); + bool conOnError = cmd[0].has("continueonerror"); + bool checkTp = false; + bool sgroupUpdate = false; + + for (int index = 0; index < cmd.bulkCount(); index++) { + PARSE_PERMISSION(cmd); + //permvalue='1' permnegated='0' permskip='0'end + + auto val = cmd[index]["permvalue"].as(); + if(permission_require_granted_value(permType) && val > maxValue) { + if(conOnError) continue; + return CommandResultPermissionError{permission::i_permission_modify_power}; + } + + if(!ignoreGrant && !this->permissionGrantGranted(permission::PERMTEST_ORDERED, permType, 1, this->currentChannel)) { + if(conOnError) continue; + return CommandResultPermissionError{permission::i_permission_modify_power}; + } + for(const auto& serverGroup : groups) { + if (grant) { + serverGroup->permissions()->set_permission(permType, {0, cmd[index]["permvalue"]}, permission::v2::PermissionUpdateType::do_nothing, permission::v2::PermissionUpdateType::set_value); + } else { + serverGroup->permissions()->set_permission( + permType, + {cmd[index]["permvalue"], 0}, + permission::v2::PermissionUpdateType::set_value, + permission::v2::PermissionUpdateType::do_nothing, + + cmd[index]["permnegated"].as() ? 1 : 0, + cmd[index]["permskip"].as() ? 1 : 0 + ); + } + } + sgroupUpdate |= permission_is_group_property(permType); + checkTp |= permission_is_client_property(permType); + } + + if(sgroupUpdate) + for(auto& group : groups) + group->apply_properties_from_permissions(); + + auto lock = this->_this.lock(); + auto server = this->server; + threads::Thread([checkTp, sgroupUpdate, groups, lock, server]() { + if(sgroupUpdate) + server->forEachClient([](shared_ptr cl) { + cl->notifyServerGroupList(); + }); + server->forEachClient([groups, checkTp](shared_ptr cl) { + for(const auto& serverGroup : groups) { + if (cl->serverGroupAssigned(serverGroup)) { + if(cl->update_cached_permissions()) /* update cached calculated permissions */ + cl->sendNeededPermissions(false); /* cached permissions had changed, notify the client */ + if (checkTp) + cl->updateChannelClientProperties(true, true); + break; + } + } + }); + }).detach(); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandServerGroupAutoDelPerm(ts::Command& cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + deque> groups; + for(const auto& group : this->server->groups->availableGroups(false)) { + if(group->updateType() == cmd["sgtype"].as() && group->target() == GROUPTARGET_SERVER) { + if(group->permission_granted(permission::i_server_group_needed_modify_power, this->calculate_permission_value(permission::i_server_group_modify_power, 0), true)) { + auto type = group->type(); + if(type == GroupType::GROUP_TYPE_QUERY) { + if(!this->permission_granted(this->cached_permission_value(permission::b_serverinstance_modify_querygroup), 1, true)) + continue; + } else if(type == GroupType::GROUP_TYPE_TEMPLATE) { + if(!this->permission_granted(this->cached_permission_value(permission::b_serverinstance_modify_templates), 1, true)) + continue; + } + groups.push_back(group);//sgtype + } + } + } + + if(groups.empty()) return CommandResult::Success; + + bool ignoreGrant = this->permission_granted(this->cached_permission_value(permission::b_permission_modify_power_ignore), 1); + bool conOnError = cmd[0].has("continueonerror"); + bool checkTp = false; + auto sgroupUpdate = false; + for (int index = 0; index < cmd.bulkCount(); index++) { + PARSE_PERMISSION(cmd); + + if(!ignoreGrant && !this->permissionGrantGranted(permission::PERMTEST_ORDERED, permType, 1, this->currentChannel)) { + if(conOnError) continue; + return CommandResultPermissionError{permission::i_permission_modify_power}; + } + + for(const auto& serverGroup : groups) { + if (grant) { + serverGroup->permissions()->set_permission(permType, permission::v2::empty_permission_values, permission::v2::PermissionUpdateType::do_nothing, permission::v2::PermissionUpdateType::delete_value); + } else { + serverGroup->permissions()->set_permission( + permType, + permission::v2::empty_permission_values, + permission::v2::PermissionUpdateType::delete_value, + permission::v2::PermissionUpdateType::do_nothing + ); + sgroupUpdate |= permission_is_group_property(permType); + } + } + checkTp |= permission_is_client_property(permType); + } + + + if(sgroupUpdate) + for(auto& group : groups) + group->apply_properties_from_permissions(); + + auto lock = this->_this.lock(); + auto server = this->server; + threads::Thread([checkTp, sgroupUpdate, groups, lock, server]() { + if(sgroupUpdate) + server->forEachClient([](shared_ptr cl) { + cl->notifyServerGroupList(); + }); + server->forEachClient([groups, checkTp](shared_ptr cl) { + for(const auto& serverGroup : groups) { + if (cl->serverGroupAssigned(serverGroup)) { + if(cl->update_cached_permissions()) /* update cached calculated permissions */ + cl->sendNeededPermissions(false); /* cached permissions had changed, notify the client */ + if (checkTp) + cl->updateChannelClientProperties(true, true); + break; + } + } + }); + }).detach(); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandSetClientChannelGroup(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + auto serverGroup = this->server->groups->findGroup(cmd["cgid"].as()); + if (!serverGroup && cmd["gcid"].as() == 0) + serverGroup = this->server->groups->defaultGroup(GroupTarget::GROUPTARGET_CHANNEL); + + if (!serverGroup || serverGroup->target() != GROUPTARGET_CHANNEL) + return {findError("parameter_invalid"), "invalid channel group id"}; + + shared_lock server_channel_lock(this->server->channel_tree_lock); /* ensure we dont get moved or somebody could move us */ + std::shared_ptr channel = this->server->channelTree->findChannel(cmd["cid"].as()); + if (!channel) return {findError("channel_invalid_id"), "Cant resolve channel"}; + + auto target_cldbid = cmd["cldbid"].as(); + { + + if(!serverGroup->permission_granted(permission::i_channel_group_member_add_power, this->calculate_permission_value(permission::i_channel_group_member_add_power, -1), true)) { + if(target_cldbid != this->getClientDatabaseId()) + return CommandResultPermissionError{permission::i_channel_group_member_add_power}; + if(!serverGroup->permission_granted(permission::i_channel_group_member_add_power, this->calculate_permission_value(permission::i_channel_group_self_add_power, -1), true)) + return CommandResultPermissionError{permission::i_channel_group_self_add_power}; + } + + + auto needed_client_permission = this->server->calculatePermission(permission::PERMTEST_ORDERED, target_cldbid, permission::i_client_needed_permission_modify_power, ClientType::CLIENT_TEAMSPEAK,nullptr); + if(needed_client_permission != permNotGranted) { + if(!this->permission_granted(this->permissionValue(permission::i_client_permission_modify_power), needed_client_permission)) + return CommandResultPermissionError{permission::i_client_needed_permission_modify_power}; + } + } + + auto oldGroup = this->server->groups->getChannelGroupExact(target_cldbid, channel, false); + if(oldGroup) { + if(!serverGroup->permission_granted(permission::i_channel_group_member_remove_power, this->calculate_permission_value(permission::i_channel_group_member_remove_power, -1), true)) { + if(target_cldbid != this->getClientDatabaseId()) + return CommandResultPermissionError{permission::i_channel_group_member_remove_power}; + if(!serverGroup->permission_granted(permission::i_channel_group_member_remove_power, this->calculate_permission_value(permission::i_channel_group_self_remove_power, -1), true)) + return CommandResultPermissionError{permission::i_channel_group_self_remove_power}; + } + } + + this->server->groups->setChannelGroup(target_cldbid, serverGroup, channel); + + for (const auto &targetClient : this->server->findClientsByCldbId(target_cldbid)) { + unique_lock client_channel_lock_w(targetClient->channel_lock); + auto updates = this->server->groups->update_server_group_property(targetClient, false); /* needs a write lock */ + client_channel_lock_w.unlock(); + shared_lock client_channel_lock_r(targetClient->channel_lock); + auto result = this->server->notifyClientPropertyUpdates(targetClient, updates); + if (result) { + if(targetClient->update_cached_permissions()) /* update cached calculated permissions */ + targetClient->sendNeededPermissions(false); /* cached permissions had changed, notify the client */ + + if(targetClient->properties()[property::CLIENT_CHANNEL_GROUP_INHERITED_CHANNEL_ID] == channel->channelId()) { //Only if group assigned over the channel + for (const auto &viewer : this->server->getClients()) { + /* if in view will be tested within that method */ + shared_lock viewer_channel_lock(viewer->channel_lock, defer_lock); + if(viewer != targetClient) + viewer_channel_lock.lock(); + viewer->notifyClientChannelGroupChanged(_this.lock(), targetClient, targetClient->getChannel(), channel, serverGroup, false); + } + } + } + targetClient->updateChannelClientProperties(false, true); + } + + return CommandResult::Success; +} + +//sendtextmessage targetmode=1 <1 = direct | 2 = channel | 3 = server> msg=asd target=1 +CommandResult ConnectedClient::handleCommandSendTextMessage(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + if (cmd["targetmode"].as() == ChatMessageMode::TEXTMODE_PRIVATE) { + auto target = this->server->findClient(cmd["target"].as()); + if (!target) return {findError("client_invalid_id"), "invalid target clid"}; + + bool chat_open = false; + { + + shared_lock channel_lock(this->channel_lock); + this->openChats.erase(remove_if(this->openChats.begin(), this->openChats.end(), [](const weak_ptr& weak) { return !weak.lock(); }), this->openChats.end()); + for(const auto& entry : this->openChats) { + if(entry.lock() == target) { + chat_open = true; + break; + } + } + } + + if(!chat_open) { + if (target == this) { + PERM_CHECK_CHANNELR(permission::b_client_even_textmessage_send, 1, this->currentChannel, true); + } + + PERM_CHECK_CHANNELR(permission::i_client_private_textmessage_power, target->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_needed_private_textmessage_power, target->currentChannel), this->currentChannel, false); + + + { + unique_lock channel_lock(target->channel_lock); + target->openChats.push_back(_this); + } + + { + unique_lock channel_lock(this->channel_lock); + this->openChats.push_back(target); + } + } + + if(this->handleTextMessage(ChatMessageMode::TEXTMODE_PRIVATE, cmd["msg"], target)) return CommandResult::Success; + target->notifyTextMessage(ChatMessageMode::TEXTMODE_PRIVATE, _this.lock(), target->getClientId(), cmd["msg"].string()); + this->notifyTextMessage(ChatMessageMode::TEXTMODE_PRIVATE, _this.lock(), target->getClientId(), cmd["msg"].string()); + } else if (cmd["targetmode"] == ChatMessageMode::TEXTMODE_CHANNEL) { + CMD_REQ_CHANNEL; + CACHED_PERM_CHECK(permission::b_client_channel_textmessage_send, 1, false); + + if(this->handleTextMessage(ChatMessageMode::TEXTMODE_CHANNEL, cmd["msg"], nullptr)) return CommandResult::Success; + for (auto &cl : this->server->getClientsByChannel(this->currentChannel)) + cl->notifyTextMessage(ChatMessageMode::TEXTMODE_CHANNEL, _this.lock(), this->getClientId(), cmd["msg"].string()); + } else if (cmd["targetmode"] == ChatMessageMode::TEXTMODE_SERVER) { + CACHED_PERM_CHECK(permission::b_client_server_textmessage_send, 1); + + if(this->handleTextMessage(ChatMessageMode::TEXTMODE_SERVER, cmd["msg"], nullptr)) return CommandResult::Success; + this->server->forEachClient([&](shared_ptr client) { + client->notifyTextMessage(ChatMessageMode::TEXTMODE_SERVER, _this.lock(), this->getClientId(), cmd["msg"].string()); + }); + } else return {findError("parameter_invalid"), "invalid target mode"}; + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientChatComposing(Command &cmd) { + CMD_REQ_SERVER; + CMD_CHK_AND_INC_FLOOD_POINTS(0); + + auto client = this->server->findClient(cmd["clid"].as()); + if (!client) return {findError("client_invalid_id"), "invalid client id"}; + + client->notifyClientChatComposing(_this.lock()); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientChatClosed(Command &cmd) { + CMD_REQ_SERVER; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + auto client = this->server->findClient(cmd["clid"].as()); + if (!client) return {findError("client_invalid_id"), "invalid client id"}; + { + unique_lock channel_lock(this->channel_lock); + this->openChats.erase(remove_if(this->openChats.begin(), this->openChats.end(), [client](const weak_ptr& weak) { + return weak.lock() == client; + }), this->openChats.end()); + } + { + unique_lock channel_lock(client->channel_lock); + client->openChats.erase(remove_if(client->openChats.begin(), client->openChats.end(), [&](const weak_ptr& weak) { + return weak.lock().get() == this; + }), client->openChats.end()); + } + client->notifyClientChatClosed(_this.lock()); + return CommandResult::Success; +} + +//ftgetfilelist cid=1 cpw path=\/ return_code=1:x +//Answer: +//1 .. n +// notifyfilelist cid=1 path=\/ return_code=1:x name=testFile size=35256 datetime=1509459767 type=1|name=testDir size=0 datetime=1509459741 type=0|name=testDir_2 size=0 datetime=1509459763 type=0 +//notifyfilelistfinished cid=1 path=\/ +inline void appendFileList(Command &fileList, vector> files) { + int index = 0; + for (const auto& fileEntry : files) { + debugMessage(lstream << "Having file " << fileEntry->path << "/" << fileEntry->name << " (Name: " << fileEntry->name << ")" << endl); + fileList[index]["name"] = fileEntry->name; + fileList[index]["datetime"] = std::chrono::duration_cast(fileEntry->lastChanged.time_since_epoch()).count(); + fileList[index]["type"] = fileEntry->type; + if (fileEntry->type == file::FileType::FILE) + fileList[index]["size"] = static_pointer_cast(fileEntry)->fileSize; + else + fileList[index]["size"] = 0; + index++; + } +} + +#define CMD_REQ_FSERVER if(!serverInstance->getFileServer()) return {findError("vs_critical"), "file server not started yet!"} + +CommandResult ConnectedClient::handleCommandFTGetFileList(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_REQ_FSERVER; + std::string code = cmd["return_code"].size() > 0 ? cmd["return_code"].string() : ""; + + Command fileList(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifyfilelist" : ""); + Command fileListFinished("notifyfilelistfinished"); + + fileList["path"] = cmd["path"].as(); + fileList["return_code"] = code; + fileListFinished["path"] = cmd["path"].as(); + fileList["cid"] = cmd["cid"].as(); + fileListFinished["cid"] = cmd["cid"].as(); + + if (cmd[0].has("cid") && cmd["cid"] != 0) { //Channel + auto channel = this->server->channelTree->findChannel(cmd["cid"].as()); + if (!channel) return {findError("channel_invalid_id"), "Cant resolve channel"}; + if (!channel->passwordMatch(cmd["cpw"]) && !this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_ft_ignore_password, 1, channel, true)) + return cmd["cpw"].string().empty() ? CommandResultPermissionError{permission::b_ft_ignore_password} : CommandResult{findError("channel_invalid_password")}; + CHANNEL_PERMISSION_TEST(permission::i_ft_file_browse_power, permission::i_ft_needed_file_browse_power, channel, true); + appendFileList(fileList, serverInstance->getFileServer()->listFiles(serverInstance->getFileServer()->resolveDirectory(this->server, channel, cmd["path"].as()))); + } else { + if (cmd["path"].as() == "/icons" || cmd["path"].as() == "/icons/") { + appendFileList(fileList, serverInstance->getFileServer()->listFiles(serverInstance->getFileServer()->iconDirectory(this->server))); + PERM_CHECKR(permission::b_icon_manage, 1, true); + } else if (cmd["path"].as() == "/") { + appendFileList(fileList, serverInstance->getFileServer()->listFiles(serverInstance->getFileServer()->avatarDirectory(this->server))); + PERM_CHECKR(permission::b_icon_manage, 1, true); + } else { + cerr << "Unknown requested directory: '" << cmd["path"].as() << "'" << endl; + return CommandResult::NotImplemented; + } + } + + if (fileList[0].has("name")) { + this->sendCommand(fileList); + if (this->getExternalType() == CLIENT_TEAMSPEAK) this->sendCommand(fileListFinished); + return CommandResult::Success; + } else { + return {findError("database_empty_result"), "empty"}; + } +} + +//ftcreatedir cid=4 cpw dirname=\/TestDir return_code=1:17 +CommandResult ConnectedClient::handleCommandFTCreateDir(Command &cmd) { + CMD_REQ_SERVER; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + CMD_REQ_FSERVER; + + auto channel = this->server->channelTree->findChannel(cmd["cid"].as()); + if (!channel) return {findError("channel_invalid_id"), "Cant resolve channel"}; + if (!channel->passwordMatch(cmd["cpw"]) && !this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_ft_ignore_password, 1, channel, true)) + return cmd["cpw"].string().empty() ? CommandResultPermissionError{permission::b_ft_ignore_password} : CommandResult{findError("channel_invalid_password")}; + CHANNEL_PERMISSION_TEST(permission::i_ft_directory_create_power, permission::i_ft_needed_directory_create_power, channel, true); + + auto dir = serverInstance->getFileServer()->createDirectory(cmd["dirname"], serverInstance->getFileServer()->resolveDirectory(this->server, channel, cmd["path"].as())); + if (!dir) return {findError("file_invalid_path"), "could not create dir"}; + + return CommandResult::Success; +} + + +CommandResult ConnectedClient::handleCommandFTDeleteFile(Command &cmd) { + CMD_REQ_SERVER; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + CMD_REQ_FSERVER; + + std::vector> files; + + if (cmd[0].has("cid") && cmd["cid"] != 0) { //Channel + auto channel = this->server->channelTree->findChannel(cmd["cid"].as()); + if (!channel) return {findError("channel_invalid_id"), "Cant resolve channel"}; + if (!channel->passwordMatch(cmd["cpw"]) && !this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_ft_ignore_password, 1, channel, true)) + return cmd["cpw"].string().empty() ? CommandResultPermissionError{permission::b_ft_ignore_password} : CommandResult{findError("channel_invalid_password")}; + CHANNEL_PERMISSION_TEST(permission::i_ft_file_delete_power, permission::i_ft_needed_file_delete_power, channel, true); + for (int index = 0; index < cmd.bulkCount(); index++) + files.push_back(serverInstance->getFileServer()->findFile(cmd[index]["name"].as(), serverInstance->getFileServer()->resolveDirectory(this->server, channel))); + } else { + if (cmd["name"].string().find("/icon_") == 0 && cmd["path"].string().empty()) { + PERM_CHECKR(permission::b_icon_manage, 1, true); + files.push_back(serverInstance->getFileServer()->findFile(cmd["name"].string(), serverInstance->getFileServer()->iconDirectory(this->server))); + } else if (cmd["name"].string().find("/avatar_") == 0 && cmd["path"].string().empty()) { + if (cmd["name"].string() != "/avatar_") { + PERM_CHECKR(permission::b_client_avatar_delete_other, 1, true); + + auto uid = cmd["name"].string().substr(strlen("/avatar_")); + auto avId = hex::hex(base64::decode(uid), 'a', 'q'); + + auto cls = this->server->findClientsByUid(uid); + for (const auto &cl : cls) { + cl->properties()[property::CLIENT_FLAG_AVATAR] = ""; + this->server->notifyClientPropertyUpdates(cl, deque{property::CLIENT_FLAG_AVATAR}); + } + + cmd["name"] = "/avatar_" + avId; + } else { + cmd["name"] = "/avatar_" + this->getAvatarId(); + this->properties()[property::CLIENT_FLAG_AVATAR] = ""; + this->server->notifyClientPropertyUpdates(_this.lock(), deque{property::CLIENT_FLAG_AVATAR}); + } + files.push_back(serverInstance->getFileServer()->findFile(cmd["name"].string(), serverInstance->getFileServer()->avatarDirectory(this->server))); + } else { + cerr << "Unknown requested file to delete: " << cmd["path"].as() << endl; + return CommandResult::NotImplemented; + } + } + + for (const auto &file : files) { + if (!file) continue; + if (!serverInstance->getFileServer()->deleteFile(file)) { + logCritical(this->getServerId(), lstream << "Cound not delete file " << file->path << "/" << file->name); + } + } + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandFTInitUpload(Command &cmd) { + CMD_REQ_SERVER; + CMD_REQ_FSERVER; + + std::shared_ptr directory = nullptr; + + if (cmd[0].has("cid") && cmd["cid"] != 0) { //Channel + auto channel = this->server->channelTree->findChannel(cmd["cid"].as()); + if (!channel) return {findError("channel_invalid_id"), "Cant resolve channel"}; + if (!channel->passwordMatch(cmd["cpw"]) && !this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_ft_ignore_password, 1, channel, true)) + return cmd["cpw"].string().empty() ? CommandResultPermissionError{permission::b_ft_ignore_password} : CommandResult{findError("channel_invalid_password")}; + CHANNEL_PERMISSION_TEST(permission::i_ft_file_upload_power, permission::i_ft_needed_file_upload_power, channel, true); + directory = serverInstance->getFileServer()->resolveDirectory(this->server, channel); + } else { + if (cmd["path"].as().empty() && cmd["name"].as().find("/icon_") == 0) { + auto max_size = this->permissionValue(permission::PERMTEST_ORDERED, permission::i_max_icon_filesize, this->currentChannel); + if(max_size != -1 && max_size < (ssize_t) cmd["size"].as()) return CommandResultPermissionError{permission::i_max_icon_filesize}; + directory = serverInstance->getFileServer()->iconDirectory(this->server); + } else if (cmd["path"].as().empty() && cmd["name"].string() == "/avatar") { + auto max_size = this->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_max_avatar_filesize, this->currentChannel); + if(max_size != -1 && max_size < (ssize_t) cmd["size"].as()) return CommandResultPermissionError{permission::i_client_max_avatar_filesize}; + + directory = serverInstance->getFileServer()->avatarDirectory(this->server); + cmd["name"] = "/avatar_" + this->getAvatarId(); + } else { + cerr << "Unknown requested directory: " << cmd["path"].as() << endl; + return CommandResult::NotImplemented; + } + } + + if (!directory || directory->type != file::FileType::DIRECTORY) { //Should not happen + cerr << "Invalid upload file path!" << endl; + return {findError("file_invalid_path"), "could not resolve directory"}; + } + + { + auto server_quota = this->server->properties()[property::VIRTUALSERVER_UPLOAD_QUOTA].as(); + auto server_used_quota = this->server->properties()[property::VIRTUALSERVER_MONTH_BYTES_UPLOADED].as(); + server_used_quota += cmd["size"].as(); + for(const auto& trans : serverInstance->getFileServer()->pending_file_transfers()) + server_used_quota += trans->size; + for(const auto& trans : serverInstance->getFileServer()->running_file_transfers()) + server_used_quota += trans->remaining_bytes(); + if(server_quota >= 0 && server_quota * 1024 * 1024 < (int64_t) server_used_quota) return {findError("file_transfer_server_quota_exceeded")}; + + auto client_quota = this->permissionValue(permission::PERMTEST_ORDERED, permission::i_ft_quota_mb_upload_per_client, this->currentChannel); + auto client_used_quota = this->properties()[property::CLIENT_MONTH_BYTES_UPLOADED].as(); + client_used_quota += cmd["size"].as(); + for(const auto& trans : serverInstance->getFileServer()->pending_file_transfers(_this.lock())) + client_used_quota += trans->size; + for(const auto& trans : serverInstance->getFileServer()->running_file_transfers(_this.lock())) + client_used_quota += trans->remaining_bytes(); + + if(client_quota >= 0 && client_quota * 1024 * 1024 < (int64_t) client_used_quota) return {findError("file_transfer_client_quota_exceeded")}; + } + + if (cmd["overwrite"].as() && cmd["resume"].as()) return {findError("file_overwrite_excludes_resume"), "funny"}; + if (serverInstance->getFileServer()->findFile(cmd["name"].as(), directory) && !(cmd["overwrite"].as() || cmd["resume"].as())) + return {findError("file_already_exists"), "file already exists"}; + + //Request: clientftfid=1 serverftfid=6 ftkey=itRNdsIOvcBiBg\/Xj4Ge51ZSrsShHuid port=30033 seekpos=0 + //Reqpose: notifystartupload clientftfid=4079 serverftfid=1 ftkey=aX9CFQbfaddHpOYxhQiSLu\/BumfVtPyP port=30033 seekpos=0 proto=1 + string error = "success"; + auto key = serverInstance->getFileServer()->generateUploadTransferKey(error, directory->path + "/" + directory->name + cmd["name"].string(), cmd["size"].as(), 0, _this.lock()); //TODO implement resume! + if (!key) return {findError(""), "cant generate key"}; //TODO insert error! + + Command result(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifystartupload" : ""); + result["clientftfid"] = cmd["clientftfid"].as(); + result["ftkey"] = key->key; + result["port"] = ntohs(serverInstance->getFileServer()->boundedAddress()->sin_port); + if(serverInstance->getFileServer()->boundedAddress()->sin_addr.s_addr != 0) + result["ip"] = inet_ntoa(serverInstance->getFileServer()->boundedAddress()->sin_addr) + string(","); + result["seekpos"] = 0; + result["proto"] = 1; + result["serverftfid"] = key->key_id; //TODO generate! + this->sendCommand(result); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandFTInitDownload(Command &cmd) { + CMD_REQ_SERVER; + CMD_REQ_FSERVER; + + std::shared_ptr directory = nullptr; + + if(!cmd[0].has("path")) cmd["path"] = ""; + + if (cmd[0].has("cid") && cmd["cid"] != (ChannelId) 0) { //Channel + auto channel = this->server->channelTree->findChannel(cmd["cid"].as()); + if (!channel) return {findError("channel_invalid_id"), "Cant resolve channel"}; + + CHANNEL_PERMISSION_TEST(permission::i_ft_file_download_power, permission::i_ft_needed_file_download_power, channel, true); + if (!channel->passwordMatch(cmd["cpw"]) && !this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_ft_ignore_password, 1, channel, true)) + return cmd["cpw"].string().empty() ? CommandResultPermissionError{permission::b_ft_ignore_password} : CommandResult{findError("channel_invalid_password")}; + + directory = serverInstance->getFileServer()->resolveDirectory(this->server, channel); + } else { + if (cmd["path"].as().empty() && cmd["name"].as().find("/icon_") == 0) { + directory = serverInstance->getFileServer()->iconDirectory(this->server); + } else if (cmd["path"].as().empty() && cmd["name"].as().find("/avatar_") == 0) { //TODO fix avatar download not working + directory = serverInstance->getFileServer()->avatarDirectory(this->server); + } else { + cerr << "Unknown requested directory: " << cmd["path"].as() << endl; + return CommandResult::NotImplemented; + } + } + + if (!directory || directory->type != file::FileType::DIRECTORY) { //Should not happen + cerr << "Invalid download file path!" << endl; + return {findError("file_invalid_path"), "could not resolve directory"}; + } + + if (!serverInstance->getFileServer()->findFile(cmd["name"].as(), directory)) + return {findError("file_not_found"), "file not exists"}; + + string error = "success"; + auto key = serverInstance->getFileServer()->generateDownloadTransferKey(error, directory->path + "/" + directory->name + cmd["name"].as(), 0, _this.lock()); //TODO implement resume! + if (!key) return {findError("vs_critical"), "cant generate key (" + error + ")"}; + + { + auto server_quota = this->server->properties()[property::VIRTUALSERVER_DOWNLOAD_QUOTA].as(); + auto server_used_quota = this->server->properties()[property::VIRTUALSERVER_MONTH_BYTES_DOWNLOADED].as(); + server_used_quota += key->size; + for(const auto& trans : serverInstance->getFileServer()->pending_file_transfers()) + server_used_quota += trans->size; + for(const auto& trans : serverInstance->getFileServer()->running_file_transfers()) + server_used_quota += trans->remaining_bytes(); + if(server_quota >= 0 && server_quota * 1024 * 1024 < (int64_t) server_used_quota) return {findError("file_transfer_server_quota_exceeded")}; + + + auto client_quota = this->permissionValue(permission::PERMTEST_ORDERED, permission::i_ft_quota_mb_download_per_client, this->currentChannel); + auto client_used_quota = this->properties()[property::CLIENT_MONTH_BYTES_DOWNLOADED].as(); + client_used_quota += key->size; + for(const auto& trans : serverInstance->getFileServer()->pending_file_transfers(_this.lock())) + client_used_quota += trans->size; + for(const auto& trans : serverInstance->getFileServer()->running_file_transfers(_this.lock())) + client_used_quota += trans->remaining_bytes(); + + if(client_quota >= 0 && client_quota * 1024 * 1024 < (int64_t) client_used_quota) return {findError("file_transfer_client_quota_exceeded")}; + } + + Command result(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifystartdownload" : ""); + result["clientftfid"] = cmd["clientftfid"].as(); + result["proto"] = 1; + result["serverftfid"] = key->key_id; + result["ftkey"] = key->key; + result["port"] = ntohs(serverInstance->getFileServer()->boundedAddress()->sin_port); + if(serverInstance->getFileServer()->boundedAddress()->sin_addr.s_addr != 0) + result["ip"] = inet_ntoa(serverInstance->getFileServer()->boundedAddress()->sin_addr) + string(","); + result["size"] = key->size; + this->sendCommand(result); + + return CommandResult::Success; +} + +/* + * Usage: ftgetfileinfo cid={channelID} cpw={channelPassword} name={filePath}... + +Permissions: + i_ft_file_browse_power + i_ft_needed_file_browse_power + +Description: + Displays detailed information about one or more specified files stored in a + channels file repository. + +Example: + ftgetfileinfo cid=2 cpw= path=\/Pic1.PNG|cid=2 cpw= path=\/Pic2.PNG + cid=2 path=\/ name=Stuff size=0 datetime=1259415210 type=0|name=Pic1.PNG size=563783 datetime=1259425462 type=1|name=Pic2.PNG ... + error id=0 msg=ok + + */ + +CommandResult ConnectedClient::handleCommandFTGetFileInfo(ts::Command &cmd) { + CMD_REQ_SERVER; + CMD_REQ_FSERVER; + CMD_RESET_IDLE; + + Command result(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifyfileinfo" : ""); + + int result_index = 0; + for(int index = 0; index < cmd.bulkCount(); index++) { + auto& request = cmd[index]; + std::shared_ptr file; + if (request.has("cid") && request["cid"].as() != 0) { //Channel + auto channel = this->server->channelTree->findChannel(request["cid"].as()); + if (!channel) return {findError("channel_invalid_id"), "Cant resolve channel"}; + if (!channel->passwordMatch(cmd["cpw"]) && !this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_ft_ignore_password, 1, channel, true)) + return cmd["cpw"].string().empty() ? CommandResultPermissionError{permission::b_ft_ignore_password} : CommandResult{findError("channel_invalid_password"), ""}; + + CHANNEL_PERMISSION_TEST(permission::i_ft_file_browse_power, permission::i_ft_needed_file_browse_power, channel, true); + file = serverInstance->getFileServer()->findFile(request["name"],serverInstance->getFileServer()->resolveDirectory(this->server, channel, request["path"])); + } else { + std::shared_ptr directory; + if (!request.has("path") || request["path"].as() == "/") { + directory = serverInstance->getFileServer()->avatarDirectory(this->server); + } else if (request["path"].as() == "/icons" || request["path"].as() == "/icons/") { + directory = serverInstance->getFileServer()->iconDirectory(this->server); + } else { + cerr << "Unknown requested directory: '" << request["path"].as() << "'" << endl; + return CommandResult::NotImplemented; + } + + if(!directory) continue; + file = serverInstance->getFileServer()->findFile(cmd["name"].as(), directory); + } + if(!file) continue; + + result[result_index]["cid"] = request["cid"].as(); + result[result_index]["name"] = request["name"]; + result[result_index]["path"] = request["path"]; + result[result_index]["type"] = file->type; + result[result_index]["datetime"] = duration_cast(file->lastChanged.time_since_epoch()).count(); + if (file->type == file::FileType::FILE) + result[result_index]["size"] = static_pointer_cast(file)->fileSize; + else + result[result_index]["size"] = 0; + result_index++; + } + + if(result_index == 0) + return {findError("database_empty_result")}; + this->sendCommand(result); + + return CommandResult::Success; +} +//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 +CommandResult 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) { + if(!this->permission_granted(this->cached_permission_value(permission::b_client_ban_list_global), 1)) + return CommandResultPermissionError{permission::b_client_ban_list_global}; + } else { + auto server = serverInstance->getVoiceServerManager()->findServerById(sid); + if (!server) return {findError("parameter_invalid"), ""}; + + if (server->calculatePermission(permission::PERMTEST_ORDERED, this->getClientDatabaseId(), permission::b_client_ban_list, this->getType(), nullptr) != 1) + return CommandResultPermissionError{permission::b_client_ban_list}; + } + + //When empty: return {findError("database_empty_result"), "empty"}; + auto banList = serverInstance->banManager()->listBans(sid); + if (banList.empty()) return {findError("database_empty_result")}; + + auto allow_ip = this->permission_granted(this->cached_permission_value(permission::b_client_remoteaddress_view), 1); + 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 CommandResult::Success; +} + +CommandResult 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) { + if(!this->permission_granted(this->cached_permission_value(permission::b_client_ban_create_global), 1)) + return CommandResultPermissionError{permission::b_client_ban_create_global}; + } else { + auto server = serverInstance->getVoiceServerManager()->findServerById(sid); + if (!server) return {findError("parameter_invalid"), ""}; + if (server->calculatePermission(permission::PERMTEST_ORDERED, this->getClientDatabaseId(), permission::b_client_ban_create, this->getType(), nullptr) != 1) + return CommandResultPermissionError{permission::b_client_ban_create_global}; + } + + auto max_ban_time = this->cached_permission_value(permission::i_client_ban_max_bantime); + if (max_ban_time != permNotGranted) { + if (max_ban_time != -1 && max_ban_time < time) + return CommandResultPermissionError(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 {findError("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.lock()); + return CommandResult::Success; +} + +CommandResult 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 {findError("database_empty_result"), "empty"}; + + if (sid == 0) { + if(!this->permission_granted(this->cached_permission_value(permission::b_client_ban_edit_global), 1)) + return CommandResultPermissionError{permission::b_client_ban_edit_global}; + } else { + auto server = serverInstance->getVoiceServerManager()->findServerById(sid); + if (!server) return {findError("parameter_invalid"), ""}; + + if (server->calculatePermission(permission::PERMTEST_ORDERED, this->getClientDatabaseId(), permission::b_client_ban_edit, this->getType(), nullptr) != 1) return CommandResultPermissionError{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 {findError("parameter_invalid"), ""}; + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandBanClient(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + PERM_CHECKR(permission::i_client_ban_power, 1, true); //Require a little ban power + + string uid; + 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"); + + deque> target_clients; + if (cmd[0].has("uid")) { + target_clients = this->server->findClientsByUid(uid = cmd["uid"].string()); + for(const auto& client : target_clients) + if(client->getType() == ClientType::CLIENT_MUSIC) + return {findError("client_invalid_id"), "You cant ban a music bot!"}; + } else { + target_clients = {this->server->findClient(cmd["clid"].as())}; + if(!target_clients[0]) { + return {findError("client_invalid_id"), "Could not find target client"}; + } + if(target_clients[0]->getType() == ClientType::CLIENT_MUSIC) { + return {findError("client_invalid_id"), "You cant ban a music bot!"}; + } + uid = target_clients[0]->getUid(); + } + + ClientDbId target_dbid = 0; + if (!target_clients.empty()) target_dbid = target_clients[0]->getClientDatabaseId(); + else { + auto info = serverInstance->databaseHelper()->queryDatabaseInfoByUid(this->getServer(), {uid}); + if (!info.empty()) + target_dbid = info[0]->cldbid; + else + return {findError("client_unknown")}; + } + if (target_dbid != 0) { + if (this->server->calculatePermission(permission::PERMTEST_ORDERED, target_dbid, permission::i_client_needed_ban_power, ClientType::CLIENT_TEAMSPEAK, nullptr) > this->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_ban_power)) + return CommandResultPermissionError(permission::i_client_ban_power); + if (this->server->calculatePermission(permission::PERMTEST_ORDERED, target_dbid, permission::b_client_ignore_bans, ClientType::CLIENT_TEAMSPEAK, nullptr) >= 1) return CommandResultPermissionError(permission::b_client_ignore_bans); + } + deque ban_ids; + auto _id = serverInstance->banManager()->registerBan(this->getServer()->getServerId(), this->getClientDatabaseId(), reason, uid, "", "", "", until); + ban_ids.push_back(_id); + + auto b_ban_name = this->cached_permission_value(permission::b_client_ban_name) == permNotGranted || this->permission_granted(this->cached_permission_value(permission::b_client_ban_name), 1, false); + auto b_ban_ip = this->cached_permission_value(permission::b_client_ban_ip) == permNotGranted || this->permission_granted(this->cached_permission_value(permission::b_client_ban_ip), 1, false); + auto b_ban_hwid = this->cached_permission_value(permission::b_client_ban_hwid) == permNotGranted || this->permission_granted(this->cached_permission_value(permission::b_client_ban_hwid), 1, false); + + auto max_value = this->cached_permission_value(permission::i_client_ban_max_bantime); + if(max_value != permNotGranted && max_value != -1) { + if(time > max_value) return CommandResultPermissionError{permission::i_client_ban_max_bantime}; + if(time == 0) return CommandResultPermissionError{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->closeConnection(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.lock()); + + 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 CommandResult::Success; +} + +CommandResult 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 {findError("database_empty_result"), "empty"}; + + if (sid == 0) { + const auto permission = ban->invokerDbId == this->getClientDatabaseId() ? permission::b_client_ban_delete_own_global : permission::b_client_ban_delete_global; + if(!this->permission_granted(this->cached_permission_value(permission), 1)) + return CommandResultPermissionError{permission}; + } else { + auto server = serverInstance->getVoiceServerManager()->findServerById(sid); + if (!server) return {findError("parameter_invalid"), ""}; + + auto perm = ban->invokerDbId == this->getClientDatabaseId() ? permission::b_client_ban_delete_own : permission::b_client_ban_delete; + if (server->calculatePermission(permission::PERMTEST_ORDERED, this->getClientDatabaseId(), perm, this->getType(), nullptr) != 1) return CommandResultPermissionError{perm}; + } + serverInstance->banManager()->unban(ban); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandBanDelAll(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + CACHED_PERM_CHECK(permission::b_client_ban_delete, 1, true); + + serverInstance->banManager()->deleteAllBans(server->getServerId()); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandBanTriggerList(ts::Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + CACHED_PERM_CHECK(permission::b_client_ban_trigger_list, 1, true); + CMD_REQ_PARM("banid"); + + auto record = serverInstance->banManager()->findBanById(this->getServerId(), cmd["banid"]); + if(!record) return {findError("parameter_invalid"), "Invalid ban id"}; + + Command notify(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifybantriggerlist" : ""); + notify["banid"] = record->banId; + notify["serverid"] = record->serverId; + + auto allow_ip = this->permission_granted(this->cached_permission_value(permission::b_client_remoteaddress_view), 1); + 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 {findError("database_empty_result")}; + + this->sendCommand(notify); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandTokenList(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + CACHED_PERM_CHECK(permission::b_virtualserver_token_list, 1, true); + + Command notify(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifytokenlist" : ""); + int index = 0; + for (auto &token : this->server->tokenManager->avariableTokes()) { + notify[index]["token"] = token->token; + notify[index]["token_type"] = token->type; + notify[index]["token_id1"] = token->groupId; + notify[index]["token_id2"] = token->channelId; + notify[index]["token_created"] = chrono::duration_cast(token->created.time_since_epoch()).count(); + notify[index]["token_description"] = token->description; + index++; + } + if (index == 0) return {findError("database_empty_result"), "empty!"}; + + this->sendCommand(notify); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandTokenAdd(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + CACHED_PERM_CHECK(permission::b_virtualserver_token_add, 1, true); + + TokenType ttype = static_cast(cmd["tokentype"].as()); + auto gId = cmd["tokenid1"].as(); + auto cId = cmd["tokenid2"].as(); + string description = cmd["tokendescription"].string(); + + { + auto group = this->server->groups->findGroup(gId); + if(!group) return {findError("parameter_invalid"), "invalid server group id"}; + if(group->type() == GroupType::GROUP_TYPE_TEMPLATE) return {findError("parameter_invalid"), "invalid server group type"}; + + if(group->target() == GroupTarget::GROUPTARGET_SERVER) + GROUP_PERMISSION_TEST(permission::i_server_group_member_add_power, permission::i_server_group_needed_member_add_power, group, true); + else + GROUP_PERMISSION_TEST(permission::i_channel_group_member_add_power, permission::i_channel_group_needed_member_add_power, group, true); + } + if (ttype == TokenType::TOKEN_CHANNEL) { + if (!this->server->channelTree->findChannel(cId)) return {findError("channel_invalid_id"), "Cant resolve channel"}; + } else if (ttype == TokenType::TOKEN_SERVER); + else return {findError("parameter_invalid"), "invalid token target type"}; + + + auto result = this->server->tokenManager->createToken(ttype, gId, description, cId, cmd["token"]); + if (!result) return {ErrorType::Success, "internal error"}; + + Command notify(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifytokenadd" : ""); + notify["token"] = result->token; + this->sendCommand(notify); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandTokenUse(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + CACHED_PERM_CHECK(permission::b_virtualserver_token_use, 1, true); + + auto strToken = cmd["token"].string(); + auto token = this->server->tokenManager->findToken(strToken); + if (!token) return {findError("token_invalid_id"), "Invalid token. (Token not registered)"}; + this->server->tokenManager->deleteToke(token->token); + + auto serverGroup = this->server->groups->findGroup(token->groupId); + if (!serverGroup) return {findError("token_invalid_id"), "Token invalid groupId"}; + this->server->properties()[property::VIRTUALSERVER_ASK_FOR_PRIVILEGEKEY] = false; //TODO test if its the default token + + std::shared_ptr channel = nullptr; + if (token->channelId != 0) { + channel = this->server->channelTree->findChannel(token->channelId); + if (!channel) return {findError("token_invalid_id"), "Token invalid channelId"}; + + this->server->groups->setChannelGroup(this->getClientDatabaseId(), serverGroup, channel); + } else { + if(!this->server->groups->hasServerGroupAssigned(this->getClientDatabaseId(), serverGroup)) + this->server->groups->addServerGroup(this->getClientDatabaseId(), serverGroup); + else { + return CommandResult::Success; + } + } + + if (this->server->notifyClientPropertyUpdates(_this.lock(), this->server->groups->update_server_group_property(_this.lock(), true))) { + if(this->update_cached_permissions()) /* update cached calculated permissions */ + this->sendNeededPermissions(false); /* cached permissions had changed, notify the client */ + + { + for (auto &viewer : this->server->getClients()) { + if(viewer->isClientVisible(_this.lock(), true)) + viewer->notifyServerGroupClientAdd(this->server->serverRoot, _this.lock(), serverGroup); + } + } + this->notifyServerGroupClientAdd(this->server->serverRoot, _this.lock(), serverGroup); + } + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandTokenDelete(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + CACHED_PERM_CHECK(permission::b_virtualserver_token_delete, 1, true); + + auto strToken = cmd["token"].string(); + auto token = this->server->tokenManager->findToken(strToken); + if (!token) return {findError("token_invalid_id"), "Invalid token. (Token not registered)"}; + this->server->tokenManager->deleteToke(token->token); + if(token->token == this->server->properties()[property::VIRTUALSERVER_AUTOGENERATED_PRIVILEGEKEY].as()) { + 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 CommandResult::Success; +} + +//start=0 duration=10 +//pattern=%asd% + +struct ClientDbArgs { + shared_ptr server; + int index = 0; + int offset = 0; + int resultIndex = 0; + bool showIp = false; + bool largeInfo = false; + Command *result = nullptr; +}; + +CommandResult ConnectedClient::handleCommandClientDbList(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + CACHED_PERM_CHECK(permission::b_virtualserver_client_dblist, 1, true); + + Command notify(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifyclientdblist" : ""); + + if (!cmd[0].has("start")) + cmd["start"] = 0; + if (!cmd[0].has("duration")) + cmd["duration"] = 20; + if (cmd[0]["duration"].as() > 2000) cmd["duration"] = 2000; + if (cmd[0]["duration"].as() < 1) cmd["duration"] = 1; + + auto maxIndex = cmd["start"].as() + cmd["duration"].as(); + ClientDbArgs args; + args.server = this->server; + args.offset = cmd["start"].as(); + args.result = ¬ify; + args.resultIndex = 0; + args.index = 0; + args.showIp = this->permission_granted(this->cached_permission_value(permission::b_client_remoteaddress_view), 1); + args.largeInfo = cmd.hasParm("details"); + + (LOG_SQL_CMD)(sql::command(this->server->getSql(), "SELECT * FROM `clients` WHERE `serverId` = :sid ORDER BY `cldbid` ASC" + (maxIndex > 0 ? " LIMIT " + to_string(maxIndex) : ""), variable{":sid", this->server->getServerId()}).query( + [](ClientDbArgs *pArgs, int length, char **values, char **column) { + pArgs->index++; + if (pArgs->offset < pArgs->index) { + ClientDbId id = 0; + string uid, name, ip; + string created = "0", lastConnected = "0", connections = "0"; + for (int index = 0; index < length; index++) { + string key = column[index]; + if (key == "cldbid") + id = stoll(values[index]); + else if (key == "clientUid") + uid = values[index]; + else if (key == "firstConnect") + created = values[index]; + else if (key == "lastConnect") + lastConnected = values[index]; + else if (key == "connections") + connections = values[index]; + else if (key == "lastName") + name = values[index]; + } + + pArgs->result->operator[](pArgs->resultIndex)["cldbid"] = id; + pArgs->result->operator[](pArgs->resultIndex)["client_unique_identifier"] = uid; + pArgs->result->operator[](pArgs->resultIndex)["client_nickname"] = name; + pArgs->result->operator[](pArgs->resultIndex)["client_created"] = created; + pArgs->result->operator[](pArgs->resultIndex)["client_lastconnected"] = lastConnected; + pArgs->result->operator[](pArgs->resultIndex)["client_totalconnections"] = connections; + pArgs->result->operator[](pArgs->resultIndex)["client_description"] = ""; + + auto props = serverInstance->databaseHelper()->loadClientProperties(pArgs->server, id, ClientType::CLIENT_TEAMSPEAK); + if (props) { + pArgs->result->operator[](pArgs->resultIndex)["client_lastip"] = (*props)[property::CONNECTION_CLIENT_IP].as(); + pArgs->result->operator[](pArgs->resultIndex)["client_description"] = (*props)[property::CLIENT_DESCRIPTION].as(); + + if (pArgs->largeInfo) { + pArgs->result->operator[](pArgs->resultIndex)["client_badges"] = (*props)[property::CLIENT_BADGES].as(); + pArgs->result->operator[](pArgs->resultIndex)["client_version"] = (*props)[property::CLIENT_VERSION].as(); + pArgs->result->operator[](pArgs->resultIndex)["client_platform"] = (*props)[property::CLIENT_PLATFORM].as(); + pArgs->result->operator[](pArgs->resultIndex)["client_hwid"] = (*props)[property::CLIENT_HARDWARE_ID].as(); + } + } + if (!pArgs->showIp) + pArgs->result->operator[](pArgs->resultIndex)["client_lastip"] = "hidden"; + pArgs->resultIndex++; + } + return 0; + }, &args)); + + if (args.resultIndex == 0) return {findError("database_empty_result"), "empty!"}; + if (cmd.hasParm("count")) { + size_t result = 0; + sql::command(this->server->getSql(), "SELECT COUNT(*) AS `count` FROM `clients` WHERE `serverId` = :sid", variable{":sid", this->server->getServerId()}).query([](size_t *ptr, int, char **v, char **) { + *ptr = static_cast(stoll(v[0])); + return 0; + }, &result); + notify[0]["count"] = result; + } + this->sendCommand(notify); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientDBEdit(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + PERM_CHECKR(permission::b_client_modify_dbproperties, 1, true); + + if (!serverInstance->databaseHelper()->validClientDatabaseId(this->server, cmd["cldbid"])) return {findError("database_empty_result"), "invalid cldbid"}; + auto props = serverInstance->databaseHelper()->loadClientProperties(this->server, cmd["cldbid"], ClientType::CLIENT_TEAMSPEAK); + + for (auto &elm : cmd[0].keys()) { + if (elm == "cldbid") continue; + + auto info = property::info(elm); + if(*info == property::CLIENT_UNDEFINED) { + logError(this->getServerId(), "Client " + this->getDisplayName() + " tried to change someone's db entry, but the entry in unknown: " + elm); + continue; + } + if(!info->validate_input(cmd[elm].as())) { + logError(this->getServerId(), "Client " + this->getDisplayName() + " tried to change a property to an invalid value. (Value: '" + cmd[elm].as() + "', Property: '" + info->name + "')"); + continue; + } + (*props)[info] = cmd[elm].string(); + } + + return CommandResult::Success; +} + +CommandResult 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.lock()); + } else if (mode == PluginTargetMode::PLUGINCMD_SUBSCRIBED_CLIENTS) { + for (auto &cl : this->server->getClients()) + if (cl->isClientVisible(_this.lock(), true)) + cl->notifyPluginCmd(cmd["name"], cmd["data"], _this.lock()); + } else if (mode == PluginTargetMode::PLUGINCMD_SERVER) { + for (auto &cl : this->server->getClients()) + cl->notifyPluginCmd(cmd["name"], cmd["data"], _this.lock()); + } else if (mode == PluginTargetMode::PLUGINCMD_CLIENT) { + for (int index = 0; index < cmd.bulkCount(); index++) { + auto target = cmd[index]["target"].as(); + auto cl = this->server->findClient(target); + if (!cl) return {findError("client_invalid_id"), "invalid target id"}; + cl->notifyPluginCmd(cmd["name"], cmd["data"], _this.lock()); + } + } + + /* + else if(mode == PluginTargetMode::PLUGINCMD_SEND_COMMAND) { + auto target = _this.lock(); + if(cmd[0].has("target")) + target = this->server->findClient(cmd["target"].as()); + if(!target) return {findError("client_invalid_id"), "invalid target id"}; + + target->sendCommand(Command(cmd["command"].string()), cmd[0].has("low") ? cmd["low"] : false); + } + */ + + else return CommandResult::NotImplemented; + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientEdit(ts::Command &cmd) { + CMD_REQ_SERVER; + + auto client = this->server->findClient(cmd["clid"].as()); + if (!client) return {findError("client_invalid_id"), "invalid client id"}; + return this->handleCommandClientEdit(cmd, client); +} + +CommandResult ConnectedClient::handleCommandClientEdit(Command &cmd, const std::shared_ptr& client) { + assert(client); + auto self = client == this; + CMD_CHK_AND_INC_FLOOD_POINTS(self ? 15 : 25); + CMD_RESET_IDLE; + + + bool update_talk_rights = false; + unique_ptr> nickname_lock; + deque> keys; + for(const auto& key : cmd[0].keys()) { + if(key == "return_code") continue; + if(key == "clid") continue; + + const auto &info = property::info(key); + if(*info == property::CLIENT_UNDEFINED) { + logError(this->getServerId(), R"([{}] Tried to change a not existing client property for {}. (Key: "{}", Value: "{}"))", CLIENT_STR_LOG_PREFIX, CLIENT_STR_LOG_PREFIX_(client), key, cmd[key].string()); + continue; + } + + if((info->flags & property::FLAG_USER_EDITABLE) == 0) { + logError(this->getServerId(), R"([{}] Tried to change a not user editable client property for {}. (Key: "{}", Value: "{}"))", CLIENT_STR_LOG_PREFIX, CLIENT_STR_LOG_PREFIX_(client), key, cmd[key].string()); + continue; + } + + if(!info->validate_input(cmd[key].as())) { + logError(this->getServerId(), R"([{}] Tried to change a client property to an invalid value for {}. (Key: "{}", Value: "{}"))", CLIENT_STR_LOG_PREFIX, CLIENT_STR_LOG_PREFIX_(client), key, cmd[key].string()); + continue; + } + if(client->properties()[info].as() == cmd[key].as()) continue; + + if (*info == property::CLIENT_DESCRIPTION) { + if (self) + PERM_CHECKR(permission::b_client_modify_own_description, 1, true); + else if(client->getType() == ClientType::CLIENT_MUSIC) { + if(client->properties()[property::CLIENT_OWNER] != this->getClientDatabaseId()) { + CACHED_PERM_CHECK(permission::i_client_music_modify_power, client->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_music_needed_modify_power, client->currentChannel), true); + } + } else { + PERM_CHECKR(permission::b_client_modify_description, 1, true); + } + + string value = cmd["client_description"].string(); + if (count_characters(value) > 200) return {findError("parameter_invalid"), "Invalid description length. A maximum of 200 characters is allowed!"}; + } else if (*info == property::CLIENT_IS_TALKER) { + PERM_CHECK_CHANNELR(permission::b_client_set_flag_talker, 1, client->getChannel(), true); + cmd["client_is_talker"] = cmd["client_is_talker"].as(); + cmd["client_talk_request"] = 0; + update_talk_rights = true; + + keys.emplace_back(property::CLIENT_IS_TALKER, "client_is_talker"); + keys.emplace_back(property::CLIENT_TALK_REQUEST, "client_talk_request"); + continue; + } else if(*info == property::CLIENT_NICKNAME) { + if(!self) { + if(client->getType() != ClientType::CLIENT_MUSIC) return {findError("client_invalid_type")}; + if(client->properties()[property::CLIENT_OWNER] != this->getClientDatabaseId()) { + CACHED_PERM_CHECK(permission::i_client_music_rename_power, client->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_music_needed_rename_power, client->currentChannel), true); + } + } + + string name = cmd["client_nickname"].string(); + if (count_characters(name) < 3) return {findError("parameter_invalid"), "Invalid name length. A minimum of 3 characters is required!"}; + if (count_characters(name) > 30) return {findError("parameter_invalid"), "Invalid name length. A maximum of 30 characters is allowed!"}; + + auto banIgnore = this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_client_ignore_bans, 1, this->currentChannel); + if (!banIgnore) { + auto banRecord = serverInstance->banManager()->findBanByName(this->getServerId(), name); + if (banRecord) return {findError("client_nickname_inuse"), string() + "This nickname is " + (banRecord->serverId == 0 ? "globally " : "") + "banned for the reason: " + banRecord->reason}; + } + if (this->server) { + nickname_lock = std::make_unique>(this->server->client_nickname_lock); + bool self = false; + for (const auto &cl : this->server->getClients()) { + if (cl->getDisplayName() == cmd["client_nickname"].string()) { + if(cl == this) + self = true; + else + return {findError("client_nickname_inuse"), "This nickname is already in use"}; + } + } + if(self) { + nickname_lock.reset(); + continue; + } + } + } else if(*info == property::CLIENT_PLAYER_VOLUME) { + if(client->getType() != ClientType::CLIENT_MUSIC) return {findError("client_invalid_type")}; + if(client->properties()[property::CLIENT_OWNER] != this->getClientDatabaseId()) { + CACHED_PERM_CHECK(permission::i_client_music_modify_power, client->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_music_needed_modify_power, client->currentChannel), true); + } + auto bot = dynamic_pointer_cast(client); + assert(bot); + + auto volume = cmd["player_volume"].as(); + auto max_volume = this->cached_permission_value(permission::i_client_music_create_modify_max_volume); + if(max_volume != permNotGranted && !this->permission_granted(max_volume, volume * 100)) return CommandResultPermissionError{permission::i_client_music_create_modify_max_volume}; + + bot->volume_modifier(cmd["player_volume"]); + } else if(*info == property::CLIENT_IS_CHANNEL_COMMANDER) { + if(!self) { + if(client->getType() != ClientType::CLIENT_MUSIC) return {findError("client_invalid_type")}; + if(client->properties()[property::CLIENT_OWNER] != this->getClientDatabaseId()) { + CACHED_PERM_CHECK(permission::i_client_music_modify_power, client->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_music_needed_modify_power, client->currentChannel), true); + } + } + + if(cmd["client_is_channel_commander"].as()) { + CACHED_PERM_CHECK(permission::b_client_use_channel_commander, 1); + } + } else if(*info == property::CLIENT_IS_PRIORITY_SPEAKER) { + //FIXME allow other to remove this thing + if(!self) { + if(client->getType() != ClientType::CLIENT_MUSIC) + return {findError("client_invalid_type")}; + if(client->properties()[property::CLIENT_OWNER] != this->getClientDatabaseId()) { + CACHED_PERM_CHECK(permission::i_client_music_modify_power, client->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_music_needed_modify_power, client->currentChannel), true); + } + } + + if(cmd["client_is_priority_speaker"].as()) { + PERM_CHECK_CHANNELR(permission::b_client_use_priority_speaker, 1, this->currentChannel, true); + } + } else if (self && key == "client_talk_request") { + CMD_CHK_AND_INC_FLOOD_POINTS(20); + CACHED_PERM_CHECK(permission::b_client_request_talker, 1, true); + + if (cmd["client_talk_request"].as()) + cmd["client_talk_request"] = duration_cast(system_clock::now().time_since_epoch()).count(); + else + cmd["client_talk_request"] = 0; + keys.emplace_back(property::CLIENT_TALK_REQUEST, "client_talk_request"); + continue; + } else if (self && key == "client_badges") { + std::string str = cmd[key]; + size_t index = 0; + int badgesTags = 0; + do { + index = str.find("badges", index); + if (index < str.length()) badgesTags++; + index++; + } while (index < str.length() && index != 0); + if (badgesTags >= 2) { + if (!this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_client_allow_invalid_badges, 1, this->currentChannel)) + ((VoiceClient *) this)->disconnect(VREASON_SERVER_KICK, config::messages::kick_invalid_badges, this->server ? this->server->serverAdmin : dynamic_pointer_cast(serverInstance->getInitalServerAdmin()), true); + return {findError("parameter_invalid"), "Invalid badges"}; + } + //FIXME stuff here + } else if(!self && key == "client_version") { + if(client->getType() != ClientType::CLIENT_MUSIC) return {findError("client_invalid_type")}; + if(client->properties()[property::CLIENT_OWNER] != this->getClientDatabaseId()) { + CACHED_PERM_CHECK(permission::i_client_music_modify_power, client->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_music_needed_modify_power, client->currentChannel), true); + } + } else if(!self && key == "client_platform") { + if(client->getType() != ClientType::CLIENT_MUSIC) return {findError("client_invalid_type")}; + if(client->properties()[property::CLIENT_OWNER] != this->getClientDatabaseId()) { + CACHED_PERM_CHECK(permission::i_client_music_modify_power, client->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_music_needed_modify_power, client->currentChannel), true); + } + } else if(!self && key == "client_country") { + if(client->getType() != ClientType::CLIENT_MUSIC) return {findError("client_invalid_type")}; + if(client->properties()[property::CLIENT_OWNER] != this->getClientDatabaseId()) { + CACHED_PERM_CHECK(permission::i_client_music_modify_power, client->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_music_needed_modify_power, client->currentChannel), true); + } + } else if(!self && (*info == property::CLIENT_FLAG_NOTIFY_SONG_CHANGE/* || *info == property::CLIENT_NOTIFY_SONG_MESSAGE*/)) { + if(client->getType() != ClientType::CLIENT_MUSIC) return {findError("client_invalid_type")}; + if(client->properties()[property::CLIENT_OWNER] != this->getClientDatabaseId()) { + CACHED_PERM_CHECK(permission::i_client_music_modify_power, client->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_music_needed_modify_power, client->currentChannel), true); + } + } else if(!self && key == "client_uptime_mode") { + if(client->getType() != ClientType::CLIENT_MUSIC) return {findError("client_invalid_type")}; + if(client->properties()[property::CLIENT_OWNER] != this->getClientDatabaseId()) { + CACHED_PERM_CHECK(permission::i_client_music_modify_power, client->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_music_needed_modify_power, client->currentChannel), true); + } + + if(cmd[key].as() == MusicClient::UptimeMode::TIME_SINCE_SERVER_START) { + cmd["client_lastconnected"] = duration_cast(this->server->startTimestamp.time_since_epoch()).count(); + } else { + string value = client->properties()[property::CLIENT_CREATED]; + if(value.empty()) + value = "0"; + cmd["client_lastconnected"] = value; + } + + keys.emplace_back(property::CLIENT_LASTCONNECTED, "client_lastconnected"); + } else if(!self && *info == property::CLIENT_BOT_TYPE) { + auto type = cmd["client_bot_type"].as(); + if(type == MusicClient::Type::TEMPORARY) { + CACHED_PERM_CHECK(permission::b_client_music_modify_temporary, 1, true); + } else if(type == MusicClient::Type::SEMI_PERMANENT) { + CACHED_PERM_CHECK(permission::b_client_music_modify_semi_permanent, 1, true); + } else if(type == MusicClient::Type::PERMANENT) { + CACHED_PERM_CHECK(permission::b_client_music_modify_permanent, 1, true); + } else + return {findError("parameter_invalid")}; + } else if(!self) { /* dont edit random properties of other clients. For us self its allowed to edit the rest without permissions */ + continue; + } + + keys.emplace_back((property::ClientProperties) info->property_index, key); + } + + deque updates; + for(const auto& key : keys) { + if(key.first == property::CLIENT_IS_PRIORITY_SPEAKER) { + client->clientPermissions->set_permission(permission::b_client_is_priority_speaker, {1, 0}, cmd["client_is_priority_speaker"].as() ? permission::v2::PermissionUpdateType::set_value : permission::v2::PermissionUpdateType::delete_value, permission::v2::PermissionUpdateType::do_nothing); + } + client->properties()[key.first] = cmd[0][key.second].value(); + updates.push_back(key.first); + } + if(update_talk_rights) + client->updateTalkRights(client->properties()[property::CLIENT_TALK_POWER]); + + if(this->server) + this->server->notifyClientPropertyUpdates(client, updates); + nickname_lock.reset(); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientUpdate(Command &cmd) { + return this->handleCommandClientEdit(cmd, _this.lock()); +} + +CommandResult ConnectedClient::handleCommandClientMute(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + + auto client = this->server->findClient(cmd["clid"].as()); + if (!client || client->getClientId() == this->getClientId()) return {findError("client_invalid_id"), "invalid client id"}; + + { + unique_lock channel_lock(this->channel_lock); + for(const auto& weak : this->mutedClients) + if(weak.lock() == client) return CommandResult::Success; + this->mutedClients.push_back(client); + } + + if (config::voice::notifyMuted) + client->notifyTextMessage(ChatMessageMode::TEXTMODE_PRIVATE, _this.lock(), client->getClientId(), config::messages::mute_notify_message); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientUnmute(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + + auto client = this->server->findClient(cmd["clid"].as()); + if (!client || client->getClientId() == this->getClientId()) return {findError("client_invalid_id"), "invalid client id"}; + + { + unique_lock channel_lock(this->channel_lock); + this->mutedClients.erase(std::remove_if(this->mutedClients.begin(), this->mutedClients.end(), [client](const weak_ptr& weak) { + auto c = weak.lock(); + return !c || c == client; + }), this->mutedClients.end()); + } + + if (config::voice::notifyMuted) + client->notifyTextMessage(ChatMessageMode::TEXTMODE_PRIVATE, _this.lock(), client->getClientId(), config::messages::unmute_notify_message); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientList(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + + bool allow_ip = false; + if (cmd.hasParm("ip")) + allow_ip = this->permission_granted(this->cached_permission_value(permission::b_client_remoteaddress_view), 1); + Command result(""); + + int index = 0; + this->server->forEachClient([&](shared_ptr client) { + if (client->getType() == ClientType::CLIENT_INTERNAL) return; + + result[index]["clid"] = client->getClientId(); + if (client->getChannel()) + result[index]["cid"] = client->getChannel()->channelId(); + else result[index]["cid"] = 0; + result[index]["client_database_id"] = client->getClientDatabaseId(); + result[index]["client_nickname"] = client->getDisplayName(); + result[index]["client_type"] = client->getType(); + + if (cmd.hasParm("uid")) + result[index]["client_unique_identifier"] = client->getUid(); + if (cmd.hasParm("away")) { + result[index]["client_away"] = client->properties()[property::CLIENT_AWAY].as(); + result[index]["client_away_message"] = client->properties()[property::CLIENT_AWAY_MESSAGE].as(); + } + if (cmd.hasParm("groups")) { + result[index]["client_channel_group_id"] = client->properties()[property::CLIENT_CHANNEL_GROUP_ID].as(); + result[index]["client_servergroups"] = client->properties()[property::CLIENT_SERVERGROUPS].as(); + result[index]["client_channel_group_inherited_channel_id"] = client->properties()[property::CLIENT_CHANNEL_GROUP_INHERITED_CHANNEL_ID].as(); + } + if (cmd.hasParm("times")) { + result[index]["client_idle_time"] = duration_cast(system_clock::now() - client->idleTimestamp).count(); + result[index]["client_total_online_time"] = client->properties()[property::CLIENT_TOTAL_ONLINE_TIME].as() + duration_cast(system_clock::now() - client->lastOnlineTimestamp).count(); + result[index]["client_month_online_time"] = client->properties()[property::CLIENT_MONTH_ONLINE_TIME].as() + duration_cast(system_clock::now() - client->lastOnlineTimestamp).count(); + result[index]["client_idle_time"] = duration_cast(system_clock::now() - client->idleTimestamp).count(); + result[index]["client_created"] = client->properties()[property::CLIENT_CREATED].as(); + result[index]["client_lastconnected"] = client->properties()[property::CLIENT_LASTCONNECTED].as(); + } + if (cmd.hasParm("info")) { + result[index]["client_version"] = client->properties()[property::CLIENT_VERSION].as(); + result[index]["client_platform"] = client->properties()[property::CLIENT_PLATFORM].as(); + } + + if (cmd.hasParm("badges")) + result[index]["client_badges"] = client->properties()[property::CLIENT_BADGES].as(); + if (cmd.hasParm("country")) + result[index]["client_country"] = client->properties()[property::CLIENT_COUNTRY].as(); + if (cmd.hasParm("ip")) + result[index]["connection_client_ip"] = allow_ip ? client->properties()[property::CONNECTION_CLIENT_IP].as() : "hidden"; + if (cmd.hasParm("icon")) + result[index]["client_icon_id"] = client->properties()[property::CLIENT_ICON_ID].as(); + + if (cmd.hasParm("voice")) { + result[index]["client_talk_power"] = client->properties()[property::CLIENT_TALK_POWER].as(); + result[index]["client_flag_talking"] = client->properties()[property::CLIENT_FLAG_TALKING].as(); + result[index]["client_input_muted"] = client->properties()[property::CLIENT_INPUT_MUTED].as(); + result[index]["client_output_muted"] = client->properties()[property::CLIENT_OUTPUT_MUTED].as(); + result[index]["client_input_hardware"] = client->properties()[property::CLIENT_INPUT_HARDWARE].as(); + result[index]["client_output_hardware"] = client->properties()[property::CLIENT_OUTPUT_HARDWARE].as(); + result[index]["client_is_talker"] = client->properties()[property::CLIENT_IS_TALKER].as(); + result[index]["client_is_priority_speaker"] = client->properties()[property::CLIENT_IS_PRIORITY_SPEAKER].as(); + result[index]["client_is_recording"] = client->properties()[property::CLIENT_IS_RECORDING].as(); + result[index]["client_is_channel_commander"] = client->properties()[property::CLIENT_IS_CHANNEL_COMMANDER].as(); + + } + index++; + }); + this->sendCommand(result); + return CommandResult::Success; +} + +CommandResult 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(); + result["virtualserver_port"] = 0; + if (this->server->udpVoiceServer) { + result["virtualserver_port"] = this->server->properties()[property::VIRTUALSERVER_PORT].as_save(); + } + } 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(); + 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 CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandServerGroupsByClientId(Command &cmd) { + CMD_RESET_IDLE; + + ClientDbId cldbid = cmd["cldbid"]; + if(!serverInstance->databaseHelper()->validClientDatabaseId(this->server, cldbid)) return {findError("client_invalid_id"), "invalid client id"}; + + Command result(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifyservergroupsbyclientid" : ""); + + int index = 0; + if (this->server) { + for (const auto &group : this->server->groups->getAssignedServerGroups(cldbid)) { + result[index]["name"] = group->group->name(); + result[index]["sgid"] = group->group->groupId(); + result[index]["cldbid"] = cldbid; + index++; + } + } else { + for (const auto &group : serverInstance->getGroupManager()->getAssignedServerGroups(cldbid)) { + result[index]["name"] = group->group->name(); + result[index]["sgid"] = group->group->groupId(); + result[index]["cldbid"] = cldbid; + index++; + } + } + + if (index == 0) return {findError("database_empty_result"), "empty!"}; + this->sendCommand(result); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientGetDBIDfromUID(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + + deque unique_ids; + for(int index = 0; index < cmd.bulkCount(); index++) + unique_ids.push_back(cmd[index]["cluid"].as()); + + auto res = serverInstance->databaseHelper()->queryDatabaseInfoByUid(this->server, unique_ids); + if (res.empty()) return {findError("database_empty_result"), "empty!"}; + + Command result(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifyclientdbidfromuid" : ""); + int result_index = 0; + for(auto& info : res) { + result[result_index]["cluid"] = info->uniqueId; + result[result_index]["cldbid"] = info->cldbid; + result_index++; + } + this->sendCommand(result); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientGetNameFromDBID(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + + deque dbids; + for(int index = 0; index < cmd.bulkCount(); index++) + dbids.push_back(cmd[index]["cldbid"].as()); + + auto res = serverInstance->databaseHelper()->queryDatabaseInfo(this->server, dbids); + if (res.empty()) return {findError("database_empty_result"), "empty!"}; + + Command result(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifyclientgetnamefromdbid" : ""); + int result_index = 0; + for(auto& info : res) { + result[result_index]["cluid"] = info->uniqueId; + result[result_index]["cldbid"] = info->cldbid; + result[result_index]["name"] = info->lastName; + result[result_index]["clname"] = info->lastName; + result_index++; + } + this->sendCommand(result); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientGetNameFromUid(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + + deque unique_ids; + for(int index = 0; index < cmd.bulkCount(); index++) + unique_ids.push_back(cmd[index]["cluid"].as()); + + auto res = serverInstance->databaseHelper()->queryDatabaseInfoByUid(this->server, unique_ids); + if (res.empty()) return {findError("database_empty_result"), "empty!"}; + + Command result(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifyclientnamefromuid" : ""); + int result_index = 0; + for(auto& info : res) { + result[result_index]["cluid"] = info->uniqueId; + result[result_index]["cldbid"] = info->cldbid; + result[result_index]["name"] = info->lastName; + result[result_index]["clname"] = info->lastName; + result_index++; + } + this->sendCommand(result); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientGetUidFromClid(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + + bool error = false; + bool found = false; + auto client_list = this->server->getClients(); + + Command notify(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifyclientgetuidfromclid" : ""); + int result_index = 0; + + for(int index = 0; index < cmd.bulkCount(); index++) { + auto client_id = cmd[index]["clid"].as(); + for(const auto& entry : client_list) { + if(entry->getClientId() == client_id) { + notify[result_index]["clname"] = entry->getDisplayName(); + notify[result_index]["clid"] = entry->getClientId(); + notify[result_index]["cluid"] = entry->getUid(); + notify[result_index]["cldbid"] = entry->getClientDatabaseId(); + result_index++; + found = true; + } + } + if(found) found = false; + else error = false; + } + + if(result_index > 0) + this->sendCommand(notify); + if(error) + return {findError("database_empty_result"), "empty!"}; + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientAddPerm(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + if(!serverInstance->databaseHelper()->validClientDatabaseId(this->server, cmd["cldbid"])) return {findError("client_invalid_id"), "invalid client id"}; + auto mgr = serverInstance->databaseHelper()->loadClientPermissionManager(this->server, cmd["cldbid"]); + + PERM_CHECKR(permission::i_client_permission_modify_power, this->server->calculatePermission(permission::PERMTEST_ORDERED, cmd["cldbid"], permission::i_client_needed_permission_modify_power, ClientType::CLIENT_TEAMSPEAK, nullptr), true); + + auto maxValue = this->getPermissionGrantValue(permission::PERMTEST_ORDERED, permission::i_permission_modify_power, this->currentChannel); + bool ignoreGrant = this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_permission_modify_power_ignore, 1, this->currentChannel); + bool conOnError = cmd[0].has("continueonerror"); + auto update_channels = false; + for (int index = 0; index < cmd.bulkCount(); index++) { + PARSE_PERMISSION(cmd); + + auto val = cmd[index]["permvalue"].as(); + if(permission_require_granted_value(permType) && val > maxValue) + return CommandResultPermissionError{permission::i_permission_modify_power}; + + if(!ignoreGrant && !this->permissionGrantGranted(permission::PERMTEST_ORDERED, permType, 1, this->currentChannel)) + return CommandResultPermissionError{permission::i_permission_modify_power}; + + if (grant) { + mgr->set_permission(permType, {0, cmd[index]["permvalue"]}, permission::v2::do_nothing, permission::v2::set_value); + } else { + mgr->set_permission(permType, {cmd[index]["permvalue"], 0}, permission::v2::set_value, permission::v2::do_nothing, cmd[index]["permskip"] ? 1 : 0, cmd[index]["permnegated"] ? 1 : 0); + update_channels |= permission_is_client_property(permType); + } + } + auto onlineClients = this->server->findClientsByCldbId(cmd["cldbid"]); + if (!onlineClients.empty()) + for (const auto &elm : onlineClients) { + if(elm->update_cached_permissions()) /* update cached calculated permissions */ + elm->sendNeededPermissions(false); /* cached permissions had changed, notify the client */ + if(update_channels) + elm->updateChannelClientProperties(true, true); + } + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientDelPerm(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + if(!serverInstance->databaseHelper()->validClientDatabaseId(this->server, cmd["cldbid"])) return {findError("client_invalid_id"), "invalid client id"}; + auto mgr = serverInstance->databaseHelper()->loadClientPermissionManager(this->server, cmd["cldbid"]); + PERM_CHECKR(permission::i_client_permission_modify_power, this->server->calculatePermission(permission::PERMTEST_ORDERED, cmd["cldbid"], permission::i_client_needed_permission_modify_power, ClientType::CLIENT_TEAMSPEAK, nullptr), true); + + bool ignoreGrant = this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_permission_modify_power_ignore, 1, this->currentChannel); + bool conOnError = cmd[0].has("continueonerror"); + auto onlineClients = this->server->findClientsByCldbId(cmd["cldbid"]); + auto update_channel = false; + for (int index = 0; index < cmd.bulkCount(); index++) { + PARSE_PERMISSION(cmd) + + if(!ignoreGrant && !this->permissionGrantGranted(permission::PERMTEST_ORDERED, permType, 1, this->currentChannel)) + return CommandResultPermissionError{permission::i_permission_modify_power}; + + + if (grant) { + mgr->set_permission(permType, permission::v2::empty_permission_values, permission::v2::do_nothing, permission::v2::delete_value); + } else { + mgr->set_permission(permType, permission::v2::empty_permission_values, permission::v2::delete_value, permission::v2::do_nothing); + update_channel |= permission_is_client_property(permType); + } + } + if (!onlineClients.empty()) + for (const auto &elm : onlineClients) { + if(elm->update_cached_permissions()) /* update cached calculated permissions */ + elm->sendNeededPermissions(false); /* cached permissions had changed, notify the client */ + if(update_channel) + elm->updateChannelClientProperties(true, true); + } + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientPermList(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + PERM_CHECKR(permission::b_virtualserver_client_permission_list, 1, true); + if(!serverInstance->databaseHelper()->validClientDatabaseId(this->server, cmd["cldbid"])) return {findError("client_invalid_id"), "invalid client id"}; + auto mgr = serverInstance->databaseHelper()->loadClientPermissionManager(this->server, cmd["cldbid"]); + if (!this->notifyClientPermList(cmd["cldbid"], mgr, cmd.hasParm("permsid"))) return {findError("database_empty_result"), "empty!"}; + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandChannelClientPermList(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + PERM_CHECKR(permission::b_virtualserver_channelclient_permission_list, 1, true); + + std::shared_ptr channel = this->server->channelTree->findChannel(cmd["cid"].as()); + if (!channel) return {findError("channel_invalid_id"), "Cant resolve channel"}; + + if(!serverInstance->databaseHelper()->validClientDatabaseId(this->server, cmd["cldbid"])) return {findError("client_invalid_id"), "invalid client id"}; + auto mgr = serverInstance->databaseHelper()->loadClientPermissionManager(this->server, cmd["cldbid"].as()); + + Command res(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifychannelclientpermlist" : ""); + + auto permissions = mgr->channel_permissions(channel->channelId()); + if(permissions.empty()) + return {ErrorType::DBEmpty}; + + int index = 0; + res[index]["cid"] = channel->channelId(); + res[index]["cldbid"] = cmd["cldbid"].as(); + + auto sids = cmd.hasParm("permsid"); + for (const auto &permission_data : permissions) { + auto& permission = std::get<1>(permission_data); + if(permission.flags.value_set) { + if (sids) + res[index]["permsid"] = permission::resolvePermissionData(get<0>(permission_data))->name; + else + res[index]["permid"] = get<0>(permission_data); + res[index]["permvalue"] = permission.values.value; + + res[index]["permnegated"] = permission.flags.negate; + res[index]["permskip"] = permission.flags.skip; + index++; + } + + + if(permission.flags.grant_set) { + if (sids) + res[index]["permsid"] = permission::resolvePermissionData(get<0>(permission_data))->grant_name; + else + res[index]["permid"] = (get<0>(permission_data) | PERM_ID_GRANT); + res[index]["permvalue"] = permission.values.grant; + res[index]["permnegated"] = 0; + res[index]["permskip"] = 0; + index++; + } + } + + this->sendCommand(res); + return CommandResult::Success; +} + +//TODO: Update this specific channel visibility? +CommandResult ConnectedClient::handleCommandChannelClientDelPerm(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + if (!serverInstance->databaseHelper()->validClientDatabaseId(this->server, cmd["cldbid"])) return {findError("parameter_invalid"), "Invalid manager db id"}; + + std::shared_ptr channel = this->server->channelTree->findChannel(cmd["cid"].as()); + if (!channel) return {findError("channel_invalid_id"), "Cant resolve channel"}; + + auto mgr = serverInstance->databaseHelper()->loadClientPermissionManager(this->server, cmd["cldbid"]); + PERM_CHECKR(permission::i_client_permission_modify_power, this->server->calculatePermission(permission::PERMTEST_ORDERED, cmd["cldbid"], permission::i_client_needed_permission_modify_power, ClientType::CLIENT_TEAMSPEAK, nullptr), true); + + bool ignoreGrant = this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_permission_modify_power_ignore, 1, this->currentChannel); + bool conOnError = cmd[0].has("continueonerror"); + auto cll = this->server->findClientsByCldbId(cmd["cldbid"]); + for (int index = 0; index < cmd.bulkCount(); index++) { + PARSE_PERMISSION(cmd); + + if(!ignoreGrant && !this->permissionGrantGranted(permission::PERMTEST_ORDERED, permType, 1, this->currentChannel)) + return CommandResultPermissionError{permission::i_permission_modify_power}; + + if (grant) { + mgr->set_channel_permission(permType, channel->channelId(), permission::v2::empty_permission_values, permission::v2::do_nothing, permission::v2::delete_value); + } else { + mgr->set_channel_permission(permType, channel->channelId(), permission::v2::empty_permission_values, permission::v2::delete_value, permission::v2::do_nothing); + } + } + + if (!cll.empty()) { + for (const auto &cl : cll) { + if(cl->update_cached_permissions()) /* update cached calculated permissions */ + cl->sendNeededPermissions(false); /* cached permissions had changed, notify the client */ + } + + for (const auto &elm : cll) + if (elm->currentChannel == channel) { + elm->updateChannelClientProperties(true, true); + } + } + + return CommandResult::Success; +} + +//TODO: Update this specific channel visibility? +CommandResult ConnectedClient::handleCommandChannelClientAddPerm(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + if (!serverInstance->databaseHelper()->validClientDatabaseId(this->server, cmd["cldbid"])) return {findError("parameter_invalid"), "Invalid manager db id"}; + + auto mgr = serverInstance->databaseHelper()->loadClientPermissionManager(this->server, cmd["cldbid"]); + PERM_CHECKR(permission::i_client_permission_modify_power, this->server->calculatePermission(permission::PERMTEST_ORDERED, cmd["cldbid"], permission::i_client_needed_permission_modify_power, ClientType::CLIENT_TEAMSPEAK, nullptr), true); + + std::shared_ptr channel = this->server->channelTree->findChannel(cmd["cid"].as()); + if (!channel) return {findError("channel_invalid_id"), "Cant resolve channel"}; + + auto maxValue = this->getPermissionGrantValue(permission::PERMTEST_ORDERED, permission::i_permission_modify_power, this->currentChannel); + bool ignoreGrant = this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_permission_modify_power_ignore, 1, this->currentChannel); + bool conOnError = cmd[0].has("continueonerror"); + auto onlineClientInstances = this->server->findClientsByCldbId(cmd["cldbid"]); + for (int index = 0; index < cmd.bulkCount(); index++) { + PARSE_PERMISSION(cmd); + + auto val = cmd[index]["permvalue"].as(); + if(permission_require_granted_value(permType) && val > maxValue) + return CommandResultPermissionError{permission::i_permission_modify_power}; + + if(!ignoreGrant && !this->permissionGrantGranted(permission::PERMTEST_ORDERED, permType, 1, this->currentChannel)) + return CommandResultPermissionError{permission::i_permission_modify_power}; + + + if (grant) { + mgr->set_channel_permission(permType, channel->channelId(), {0, cmd[index]["permvalue"]}, permission::v2::do_nothing, permission::v2::set_value); + } else { + mgr->set_channel_permission(permType, channel->channelId(), {cmd[index]["permvalue"], 0}, permission::v2::set_value, permission::v2::do_nothing, cmd[index]["permskip"] ? 1 : 0, cmd[index]["permnegated"] ? 1 : 0); + } + } + if (!onlineClientInstances.empty()) + for (const auto &elm : onlineClientInstances) { + if (elm->update_cached_permissions()) /* update cached calculated permissions */ + elm->sendNeededPermissions(false); /* cached permissions had changed, notify the client */ + + if (elm->currentChannel == channel) { + elm->updateChannelClientProperties(true, true); + } + } + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientDbInfo(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + PERM_CHECKR(permission::b_virtualserver_client_dbinfo, 1, true); + + deque cldbids; + for(int index = 0; index < cmd.bulkCount(); index++) + cldbids.push_back(cmd[index]["cldbid"]); + + auto basic = serverInstance->databaseHelper()->queryDatabaseInfo(this->server, cldbids); + if (basic.empty()) return {findError("database_empty_result"), "empty!"}; + + auto allow_ip = this->permission_granted(this->cached_permission_value(permission::b_client_remoteaddress_view), 1); + Command res(this->getExternalType() == ClientType::CLIENT_TEAMSPEAK ? "notifyclientdbinfo" : ""); + + size_t index = 0; + for(const auto& info : basic) { + res[index]["client_base64HashClientUID"] = hex::hex(base64::validate(info->uniqueId) ? base64::decode(info->uniqueId) : info->uniqueId, 'a', 'q'); + res[index]["client_unique_identifier"] = info->uniqueId; + res[index]["client_nickname"] = info->lastName; + res[index]["client_database_id"] = info->cldbid; + res[index]["client_created"] = chrono::duration_cast(info->created.time_since_epoch()).count(); + res[index]["client_lastconnected"] = chrono::duration_cast(info->lastjoin.time_since_epoch()).count(); + res[index]["client_totalconnections"] = info->connections; + res[index]["client_database_id"] = info->cldbid; + + auto props = serverInstance->databaseHelper()->loadClientProperties(this->server, info->cldbid, ClientType::CLIENT_TEAMSPEAK); + if (allow_ip) + res[index]["client_lastip"] = (*props)[property::CONNECTION_CLIENT_IP].as(); + else + res[index]["client_lastip"] = "hidden"; + + res[index]["client_icon_id"] = (*props)[property::CLIENT_ICON_ID].as(); + res[index]["client_badges"] = (*props)[property::CLIENT_BADGES].as(); + res[index]["client_version"] = (*props)[property::CLIENT_VERSION].as(); + res[index]["client_platform"] = (*props)[property::CLIENT_PLATFORM].as(); + res[index]["client_hwid"] = (*props)[property::CLIENT_HARDWARE_ID].as(); + res[index]["client_total_bytes_downloaded"] = (*props)[property::CLIENT_TOTAL_BYTES_DOWNLOADED].as(); + res[index]["client_total_bytes_uploaded"] = (*props)[property::CLIENT_TOTAL_BYTES_UPLOADED].as(); + res[index]["client_month_bytes_downloaded"] = (*props)[property::CLIENT_MONTH_BYTES_DOWNLOADED].as(); + res[index]["client_month_bytes_uploaded"] = (*props)[property::CLIENT_MONTH_BYTES_DOWNLOADED].as(); + res[index]["client_description"] = (*props)[property::CLIENT_DESCRIPTION].as(); + res[index]["client_flag_avatar"] = (*props)[property::CLIENT_FLAG_AVATAR].as(); + + res[index]["client_month_online_time"] = (*props)[property::CLIENT_MONTH_ONLINE_TIME].as(); + res[index]["client_total_online_time"] = (*props)[property::CLIENT_TOTAL_ONLINE_TIME].as(); + index++; + } + + this->sendCommand(res); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientDBDelete(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + PERM_CHECKR(permission::b_client_delete_dbproperties, 1, true); + + ClientDbId id = cmd["cldbid"]; + if (!serverInstance->databaseHelper()->validClientDatabaseId(this->server, id)) return {findError("database_empty_result"), ""}; + serverInstance->databaseHelper()->deleteClient(this->server, id); + + return CommandResult::Success; +} + +struct DBFindArgs { + int index = 0; + bool full = false; + bool ip = false; + Command cmd{""}; +}; + +CommandResult ConnectedClient::handleCommandClientDBFind(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + PERM_CHECKR(permission::b_virtualserver_client_dbsearch, 1, true); + + bool uid = cmd.hasParm("uid"); + string pattern = cmd["pattern"]; + + DBFindArgs args{}; + args.cmd = Command(this->getType() == CLIENT_QUERY ? "" : "notifyclientdbfind"); + args.full = cmd.hasParm("details"); + args.ip = this->permission_granted(this->cached_permission_value(permission::b_client_remoteaddress_view), 1); + auto res = sql::command(this->sql, string() + "SELECT * FROM `clients` WHERE `serverId` = :sid AND `" + (uid ? "clientUid" : "lastName") + "` LIKE '" + pattern + "' LIMIT 50", variable{":sid", this->server->getServerId()}).query( + [&](DBFindArgs *ptr, int len, char **values, char **names) { + for (int index = 0; index < len; index++) + if (strcmp(names[index], "cldbid") == 0) + ptr->cmd[ptr->index]["cldbid"] = values[index]; + else if (strcmp(names[index], "clientUid") == 0 && ptr->full) + ptr->cmd[ptr->index]["client_unique_identifier"] = values[index]; + else if (strcmp(names[index], "lastConnect") == 0 && ptr->full) + ptr->cmd[ptr->index]["client_lastconnected"] = values[index]; + else if (strcmp(names[index], "connections") == 0 && ptr->full) + ptr->cmd[ptr->index]["client_totalconnections"] = values[index]; + else if (strcmp(names[index], "lastName") == 0 && ptr->full) + ptr->cmd[ptr->index]["client_nickname"] = values[index]; + if (ptr->full) { + auto props = serverInstance->databaseHelper()->loadClientProperties(this->server, ptr->cmd[ptr->index]["cldbid"], ClientType::CLIENT_TEAMSPEAK); + if (props) { + if (ptr->ip) { + ptr->cmd[ptr->index]["client_lastip"] = (*props)[property::CONNECTION_CLIENT_IP].as(); + } else { + ptr->cmd[ptr->index]["client_lastip"] = "hidden"; + } + ptr->cmd[ptr->index]["client_badges"] = (*props)[property::CLIENT_BADGES].as(); + ptr->cmd[ptr->index]["client_version"] = (*props)[property::CLIENT_VERSION].as(); + ptr->cmd[ptr->index]["client_platform"] = (*props)[property::CLIENT_PLATFORM].as(); + ptr->cmd[ptr->index]["client_hwid"] = (*props)[property::CLIENT_HARDWARE_ID].as(); + } + } + ptr->index++; + return 0; + }, &args); + auto pf = LOG_SQL_CMD; + pf(res); + if (args.index == 0) return {findError("database_empty_result"), ""}; + + this->sendCommand(args.cmd); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientInfo(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + + Command res(this->getExternalType() == ClientType::CLIENT_TEAMSPEAK ? "notifyclientinfo" : ""); + bool trigger_error = false; + bool view_remote = this->permission_granted(this->cached_permission_value(permission::b_client_remoteaddress_view), 1); + + int result_index = 0; + for(int index = 0; index < cmd.bulkCount(); index++) { + auto client_id = cmd[index]["clid"].as(); + if(client_id == 0) continue; + + auto client = this->server->findClient(client_id); + if(!client) { + trigger_error = true; + continue; + } + + for (const auto &key : client->properties()->list_properties(property::FLAG_CLIENT_VIEW | property::FLAG_CLIENT_VARIABLE | property::FLAG_CLIENT_INFO, this->getType() == CLIENT_TEAMSPEAK ? property::FLAG_NEW : (uint16_t) 0)) + res[result_index][key.type().name] = key.value(); + if(view_remote) + res[result_index]["connection_client_ip"] = client->properties()[property::CONNECTION_CLIENT_IP].as(); + else + res[result_index]["connection_client_ip"] = "hidden"; + res[result_index]["client_idle_time"] = duration_cast(system_clock::now() - client->idleTimestamp).count(); + res[result_index]["connection_connected_time"] = duration_cast(system_clock::now() - client->connectTimestamp).count(); + { + auto channel = client->currentChannel; + if(channel) + res[result_index]["cid"] = channel->channelId(); + else + res[result_index]["cid"] = 0; + } + + result_index++; + } + + + if(result_index > 0) { + this->sendCommand(res); + } + + if(trigger_error || result_index == 0) + return {findError("client_invalid_id"), "invalid client id"}; + else + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientFind(Command &cmd) { + CMD_REQ_SERVER; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + string pattern = cmd["pattern"]; + std::transform(pattern.begin(), pattern.end(), pattern.begin(), ::tolower); + + Command res(""); + int index = 0; + for (const auto &cl : this->server->getClients()) { + string name = cl->getDisplayName(); + std::transform(name.begin(), name.end(), name.begin(), ::tolower); + if (name.find(pattern) != std::string::npos) { + res[index]["clid"] = cl->getClientId(); + res[index]["client_nickname"] = cl->getDisplayName(); + index++; + } + } + if (index == 0) return {findError("database_empty_result"), ""}; + this->sendCommand(res); + + return CommandResult::Success; +} + +CommandResult 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 WINDOWS + res["platform"] = "Windows"; +#else + res["platform"] = "Linux"; +#endif + this->sendCommand(res); + return CommandResult::Success; +} + +//cid=%d password=%s +CommandResult 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 {findError("channel_invalid_id"), "Cant resolve channel"}; + + std::string password = cmd["password"]; + if (!channel->passwordMatch(password, false)) return {findError("server_invalid_password"), ""}; + return CommandResult::Success; +} + +CommandResult 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 {findError("server_invalid_password"), ""}; + return CommandResult::Success; +} + +//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 +CommandResult 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 {findError("database_empty_result"), "no letters avaraible"}; + + 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 CommandResult::Success; +} + +//messageadd cluid=ePHuXhcai9nk\/4Fd\/xkxrokvnNk= subject=Test message=Message +CommandResult ConnectedClient::handleCommandMessageAdd(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + CACHED_PERM_CHECK(permission::b_client_offline_textmessage_send, 1, true); + + this->server->letters->createLetter(this->getUid(), cmd["cluid"], cmd["subject"], cmd["message"]); + return CommandResult::Success; +} + +CommandResult 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 CommandResult::Success; +} + +CommandResult 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 CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandMessageDel(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + this->server->letters->deleteLetter(cmd["msgid"]); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandPermGet(Command &cmd) { + CMD_RESET_IDLE; + CACHED_PERM_CHECK(permission::b_client_permissionoverview_own, 1, true); + + Command res(""); + + deque requrested; + 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; + if (permission::resolvePermissionData(permType)->type == permission::PermissionType::unknown) return {findError("parameter_invalid"), "could not resolve permission"}; + + requrested.push_back(permType); + } + + int index = 0; + for(const auto& entry : this->permissionValues(permission::PERMTEST_ORDERED, requrested, this->currentChannel)) { + res[index]["permsid"] = permission::resolvePermissionData(entry.first)->name; + res[index]["permid"] = entry.first; + res[index++]["permvalue"] = entry.second; + } + this->sendCommand(res); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandPermIdGetByName(Command &cmd) { + auto found = permission::resolvePermissionData(cmd["permsid"].as()); + Command res(""); + res["permid"] = found->type; + this->sendCommand(res); + return CommandResult::Success; +} + +CommandResult 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); + PERM_CHECKR(permission::b_virtualserver_permission_find, 1, true); + + deque, bool>> permissions; + 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 {findError("parameter_invalid"), "could not resolve permission (id=" + cmd[index]["permid"].string() + ")"}; + } else if (cmd[index].has("permsid")) { + permission = permission::resolvePermissionData(cmd[index]["permsid"].as()); + granted = permission->grant_name == cmd[index]["permsid"].as(); + + if(permission->type == permission::PermissionType::unknown) + return {findError("parameter_invalid"), "could not resolve permission (id=" + cmd[index]["permid"].string() + ")"}; + } else { + continue; + } + + permissions.emplace_back(pair, bool>{{permission->name, permission->type}, granted}); + } + + if(permissions.empty()) + return {findError("database_empty_result")}; + + map flags; + map quick_mapping; + string query_string; + for(const auto& entry : permissions) { + if(flags[entry.first.first] == 0) { + quick_mapping[entry.first.first] = entry.first.second; + query_string += string(query_string.empty() ? "" : " OR ") + "`permId` = '" + entry.first.first + "'"; + } + + flags[entry.first.first] |= entry.second ? 2 : 1; + } + + 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; + 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; + } + } + + /* value */ + if((flags[permission_name] & 0x1) > 0 && value > 0) { + auto result = make_unique(); + result->permission_type = quick_mapping[permission_name]; + result->permission_value = value; + result->type = type; + result->channel_id = channel_id; + result->negate = negate; + result->skip = skip; + if (type == permission::SQL_PERM_GROUP) { + auto gr = this->server->groups->findGroup(id); + if (!gr) return 0; + + result->group_id = id; + if(gr->target() == GROUPTARGET_CHANNEL) + result->channel_id = 1; + } else if(type == permission::SQL_PERM_USER) { + result->client_id = id; + } + + if(result) + entries.push_back(std::move(result)); + } + + /* granted */ + if((flags[permission_name] & 0x2) > 0 && granted_value > 0) { + auto result = make_unique(); + result->permission_type = (permission::PermissionType) (quick_mapping[permission_name] | 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) { + auto gr = this->server->groups->findGroup(id); + if (!gr) return 0; + + result->group_id = id; + if(gr->target() == GROUPTARGET_CHANNEL) + result->channel_id = 1; + } else if(type == permission::SQL_PERM_USER) { + result->client_id = id; + } + + if(result) + entries.push_back(std::move(result)); + } + return 0; + }); + + struct CommandPerm { + permission::PermissionType p; + permission::PermissionValue v; + int64_t id1; + int64_t id2; + uint8_t t; + }; + + std::deque perms; + perms.resize(entries.size()); + size_t index = 0; + for(const auto& entry : entries) { + auto& perm = perms[index++]; + + perm.p = entry->permission_type; + perm.v = entry->permission_value; + + 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 */ + } + } + } + + + 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 result(""); + index = 0; + + // http://yat.qa/ressourcen/server-query-kommentare/#permfind + for(const auto& e : perms) { + result[index]["p"] = e.p; + result[index]["v"] = e.v; + result[index]["id1"] = e.id1; + result[index]["id2"] = e.id2; + result[index]["t"] = e.t; + index++; + } + + if(index == 0) return {findError("database_empty_result")}; + this->sendCommand(result); + return CommandResult::Success; +} + +/* + * - Alle rechte der aktuellen server gruppen vom client + * - Alle client rechte | channel cleint rechte + * - Alle rechte des channels + */ +CommandResult 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 {findError("client_invalid_id")}; + + if(client_dbid == this->getClientDatabaseId()) { + CACHED_PERM_CHECK(permission::b_client_permissionoverview_own, 1, true); + } else { + CACHED_PERM_CHECK(permission::b_client_permissionoverview_view, 1, true); + } + + string channel_query, perm_query; + + auto channel = this->server ? this->server->channelTree->findChannel(cmd["cid"]) : serverInstance->getChannelTree()->findChannel(cmd["cid"]); + if(!channel) return {findError("channel_invalid_id")}; + + auto server_groups = this->server->getGroupManager()->getServerGroups(client_dbid, ClientType::CLIENT_TEAMSPEAK); + auto channel_group = this->server->getGroupManager()->getChannelGroup(client_dbid, channel, true); + auto permission_manager = serverInstance->databaseHelper()->loadClientPermissionManager(this->getServer(), 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 : server_groups) { + auto permission_manager = server_group->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->groupId(); + 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.skip) { + result[index]["t"] = 0; /* server group */ + result[index]["id1"] = server_group->group->groupId(); + 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.skip) { + 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.skip) { + 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 permission_manager = channel_group->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"] = channel_group->channelId; + result[index]["id2"] = channel_group->group->groupId(); + + 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.skip) { + result[index]["t"] = 3; /* channel group */ + result[index]["id1"] = channel_group->channelId; + result[index]["id2"] = channel_group->group->groupId(); + + 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.skip) { + 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 {findError("database_empty_result"), ""}; + this->sendCommand(result); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandChannelFind(Command &cmd) { + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + string pattern = cmd["pattern"]; + std::transform(pattern.begin(), pattern.end(), pattern.begin(), ::tolower); + + Command res(""); + int index = 0; + for (const auto &cl : (this->server ? this->server->channelTree : serverInstance->getChannelTree().get())->channels()) { + string name = cl->name(); + std::transform(name.begin(), name.end(), name.begin(), ::tolower); + if (name.find(pattern) != std::string::npos) { + res[index]["cid"] = cl->channelId(); + res[index]["channel_name"] = cl->name(); + index++; + } + } + if (index == 0) return {findError("database_empty_result"), ""}; + this->sendCommand(res); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandChannelInfo(Command &cmd) { + std::shared_ptr channel = (this->server ? this->server->channelTree : serverInstance->getChannelTree().get())->findChannel(cmd["cid"].as()); + if (!channel) return {findError("channel_invalid_id"), "Cant resolve channel"}; + + Command res(""); + + for (const auto &prop : channel->properties().list_properties(property::FLAG_CHANNEL_VIEW | property::FLAG_CHANNEL_VARIABLE, this->getType() == CLIENT_TEAMSPEAK ? property::FLAG_NEW : (uint16_t) 0)) + res[prop.type().name] = prop.value(); + + res["seconds_empty"] = channel->emptySince(); + res["pid"] = res["cpid"].string(); + this->sendCommand(res); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandClientSetServerQueryLogin(Command &cmd) { + PERM_CHECKR(permission::b_client_create_modify_serverquery_login, 1, true); + + if(!cmd[0].has("client_login_password")) cmd["client_login_password"] = ""; + + std::string password = cmd["client_login_password"]; + if(password.empty()) + password = rnd_string(QUERY_PASSWORD_LENGTH); + + auto old = serverInstance->getQueryServer()->find_query_account_by_name(cmd["client_login_name"]); + if (old) { + if(old->unique_id == this->getUid()) { + serverInstance->getQueryServer()->change_query_password(old, password); + } else { + return {findError("client_not_logged_in")}; + } + } else { + serverInstance->getQueryServer()->create_query_account(cmd["client_login_name"], this->getServerId(), this->getUid(), password); + } + + Command res(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifyclientserverqueryloginpassword" : ""); + res["client_login_password"] = password; + this->sendCommand(res); + + return CommandResult::Success; +} + +CommandResult 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 {findError("client_invalid_id"), "invalid client id"}; + PERM_CHECKR(permission::i_client_complain_power, cl[0]->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_needed_complain_power), true); + + /* + if(!serverInstance->databaseHelper()->validClientDatabaseId(target)) + return {findError("client_invalid_id"), "invalid database id"}; + */ + + for (const auto &elm : this->server->complains->findComplainsFromTarget(target)) + if (elm->invoker == this->getClientDatabaseId()) + return {findError("database_duplicate_entry"), "you already send a complain"}; + + if (!this->server->complains->createComplain(target, this->getClientDatabaseId(), msg)) return {findError("vs_critical"), "could not create complains"}; + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandComplainList(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + CACHED_PERM_CHECK(permission::b_client_complain_list, 1, true); + + 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 {findError("database_empty_result"), "empty!"}; + + 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->cldbid == elm->target) + result[index]["tname"] = e->lastName; + if (e->cldbid == elm->invoker) + result[index]["fname"] = e->lastName; + } + index++; + } + + this->sendCommand(result); + return CommandResult::Success; +} + +CommandResult 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 {findError("database_empty_result"), "empty!"}; + + if (entry->invoker == this->getClientDatabaseId()) + CACHED_PERM_CHECK(permission::b_client_complain_delete_own, 1, true); + else + CACHED_PERM_CHECK(permission::b_client_complain_delete, 1, true); + + this->server->complains->deleteComplain(entry); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandComplainDelAll(Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + CACHED_PERM_CHECK(permission::b_client_complain_delete, 1, true); + ClientDbId tid = cmd["tcldbid"]; + if (!this->server->complains->deleteComplainsFromTarget(tid)) return {findError("database_empty_result"), "empty!"}; + return CommandResult::Success; +} + + +CommandResult ConnectedClient::handleCommandMusicBotCreate(Command& cmd) { + if(!config::music::enabled) return {findError("music_disabled")}; + + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + if(this->server->musicManager->max_bots() != -1 && this->server->musicManager->max_bots() <= this->server->musicManager->current_bot_count()){ + if(config::license->isPremium()) + return {findError("music_limit_reached"), ""}; + else + return {findError("music_limit_reached"), "You reached the server music bot limit. You could increase this limit by extend your server with a premium license."}; + } + + + auto permissions_list = this->permissionValues(permission::PERMTEST_ORDERED, { + permission::i_client_music_limit, + permission::b_client_music_create_permanent, + permission::b_client_music_create_semi_permanent, + permission::b_client_music_create_temporary, + permission::i_channel_join_power, + permission::i_client_music_delete_power, + permission::i_client_music_create_modify_max_volume + }, this->currentChannel); + + auto permissions = map(permissions_list.begin(), permissions_list.end()); + + auto max_bots = permissions[permission::i_client_music_limit]; + if(max_bots >= 0) { + auto ownBots = this->server->musicManager->listBots(this->getClientDatabaseId()); + if(ownBots.size() > max_bots) + return {findError("music_client_limit_reached"), ""}; + } + + MusicClient::Type::value create_type; + if(cmd[0].has("type")) { + create_type = cmd["type"].as(); + switch(create_type) { + case MusicClient::Type::PERMANENT: + if(permissions[permission::b_client_music_create_permanent] != 1) + return CommandResultPermissionError{permission::b_client_music_create_permanent}; + break; + case MusicClient::Type::SEMI_PERMANENT: + if(permissions[permission::b_client_music_create_semi_permanent] != 1) + return CommandResultPermissionError{permission::b_client_music_create_semi_permanent}; + break; + case MusicClient::Type::TEMPORARY: + if(permissions[permission::b_client_music_create_temporary] != 1) + return CommandResultPermissionError{permission::b_client_music_create_temporary}; + break; + default: + return {ErrorType::VSError}; + } + } else { + if(permissions[permission::b_client_music_create_permanent] == 1) + create_type = MusicClient::Type::PERMANENT; + else if(permissions[permission::b_client_music_create_semi_permanent] == 1) + create_type = MusicClient::Type::SEMI_PERMANENT; + else if(permissions[permission::b_client_music_create_temporary] == 1) + create_type = MusicClient::Type::TEMPORARY; + else + return CommandResultPermissionError{permission::b_client_music_create_temporary}; + } + + shared_lock server_channel_lock(this->server->channel_tree_lock); + auto channel = cmd[0].has("cid") ? this->server->channelTree->findChannel(cmd["cid"]) : this->currentChannel; + if(!channel) { + if(cmd[0].has("cid")) return {findError("client_invalid_id")}; + } else { + CHANNEL_PERMISSION_TEST(permission::i_channel_description_view_power, permission::i_channel_needed_description_view_power, channel, false); + auto permission_granted = this->calculate_permission_value(permission::i_channel_join_power, channel->channelId()); + if(!channel->permission_granted(permission::i_channel_needed_join_power, permission_granted, false)) + channel = nullptr; + } + if(!channel) { + channel = this->server->channelTree->getDefaultChannel(); + } + + auto bot = this->server->musicManager->createBot(this->getClientDatabaseId()); + if(!bot) return {ErrorType::VSError, ""}; + bot->set_bot_type(create_type); + { + if(permissions[permission::i_client_music_create_modify_max_volume] > 0) { + auto max_volume = min(100, max(0, permissions[permission::i_client_music_create_modify_max_volume])); + if(max_volume >= 0) + bot->volume_modifier(max_volume / 100.f); + } + } + this->selectedBot = bot; + + { + server_channel_lock.unlock(); + unique_lock server_channel_w_lock(this->server->channel_tree_lock); + this->server->client_move( + bot, + channel, + nullptr, + "music bot created", + ViewReasonId::VREASON_USER_ACTION, + false, + server_channel_w_lock + ); + } + + if(permissions[permission::i_client_music_delete_power] > 0) { + bot->clientPermissions->set_permission(permission::i_client_music_needed_delete_power, {permissions[permission::i_client_music_delete_power],0}, permission::v2::set_value, permission::v2::do_nothing); + } + + Command notify(this->getExternalType() == ClientType::CLIENT_TEAMSPEAK ? "notifymusiccreated" : ""); + notify["bot_id"] = bot->getClientDatabaseId(); + this->sendCommand(notify); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandMusicBotDelete(Command& cmd) { + if(!config::music::enabled) return {findError("music_disabled")}; + + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + auto bot = this->server->musicManager->findBotById(cmd["bot_id"]); + if(!bot) return {findError("music_invalid_id")}; + + bool permPower = this->permissionGranted(permission::PERMTEST_ORDERED, permission::i_client_music_delete_power, bot->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_music_needed_delete_power)); + if(bot->getOwner() != this->getClientDatabaseId()) { + if(!permPower) return CommandResultPermissionError{permission::i_client_music_delete_power}; + } + + this->server->musicManager->deleteBot(bot); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandMusicBotSetSubscription(ts::Command &cmd) { + if(!config::music::enabled) return {findError("music_disabled")}; + + auto bot = this->server->musicManager->findBotById(cmd["bot_id"]); + if(!bot && cmd["bot_id"].as() != 0) return {findError("music_invalid_id")}; + + { + auto old_bot = this->subscribed_bot.lock(); + if(old_bot) + old_bot->remove_subscriber(_this.lock()); + } + + if(bot) { + bot->add_subscriber(_this.lock()); + this->subscribed_bot = bot; + } + + return CommandResult::Success; +} + +void apply_song(Command& command, const std::shared_ptr& element, int index = 0) { + if(!element) return; + + command[index]["song_id"] = element ? element->getSongId() : 0; + command[index]["song_url"] = element ? element->getUrl() : ""; + command[index]["song_invoker"] = element ? element->getInvoker() : 0; + command[index]["song_loaded"] = false; + + auto entry = dynamic_pointer_cast(element); + if(entry) { + auto data = entry->song_loaded_data(); + command[index]["song_loaded"] = entry->song_loaded() && data; + + if(entry->song_loaded() && data) { + command[index]["song_title"] = data->title; + command[index]["song_description"] = data->description; + command[index]["song_thumbnail"] = data->thumbnail; + command[index]["song_length"] = data->length.count(); + } + } +} + +CommandResult ConnectedClient::handleCommandMusicBotPlayerInfo(Command& cmd) { + if(!config::music::enabled) return {findError("music_disabled")}; + + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + auto bot = this->server->musicManager->findBotById(cmd["bot_id"]); + if(!bot) return {findError("music_invalid_id")}; + + Command result(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifymusicplayerinfo" : ""); + result["bot_id"] = bot->getClientDatabaseId(); + + result["player_state"] =(int) bot->player_state(); + if(bot->current_player()) { + result["player_buffered_index"] = bot->current_player()->bufferedUntil().count(); + result["player_replay_index"] = bot->current_player()->currentIndex().count(); + result["player_max_index"] = bot->current_player()->length().count(); + result["player_seekable"] = bot->current_player()->seek_supported(); + + result["player_title"] = bot->current_player()->songTitle(); + result["player_description"] = bot->current_player()->songDescription(); + } else { + result["player_buffered_index"] = 0; + result["player_replay_index"] = 0; + result["player_max_index"] = 0; + result["player_seekable"] = 0; + result["player_title"] = ""; + result["player_description"] = ""; + } + + apply_song(result, bot->current_song()); + this->sendCommand(result); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandMusicBotPlayerAction(Command& cmd) { + if(!config::music::enabled) return {findError("music_disabled")}; + + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + auto bot = this->server->musicManager->findBotById(cmd["bot_id"]); + if(!bot) return {findError("music_invalid_id")}; + PERM_CHECK_CHANNELR(permission::i_client_music_play_power, bot->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_music_needed_play_power, bot->currentChannel), this->currentChannel, true); + + if(cmd["action"] == 0) { + bot->stopMusic(); + } else if(cmd["action"] == 1) { + bot->playMusic(); + } else if(cmd["action"] == 2) { + bot->player_pause(); + } else if(cmd["action"] == 3) { + bot->forwardSong(); + } else if(cmd["action"] == 4) { + bot->rewindSong(); + } else if(cmd["action"] == 5) { + if(!bot->current_player()) return {findError("music_no_player")}; + bot->current_player()->forward(::music::PlayerUnits(cmd["units"].as())); + } else if(cmd["action"] == 6) { + if(!bot->current_player()) return {findError("music_no_player")}; + bot->current_player()->rewind(::music::PlayerUnits(cmd["units"].as())); + } else return {findError("music_invalid_action")}; + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandPlaylistList(ts::Command &cmd) { + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + auto self_dbid = this->getClientDatabaseId(); + auto playlist_view_power = this->cached_permission_value(permission::i_playlist_view_power); + auto playlists = this->server->musicManager->playlists(); + + playlists.erase(find_if(playlists.begin(), playlists.end(), [&](const shared_ptr& playlist) { + if(playlist->properties()[property::PLAYLIST_OWNER_DBID] == self_dbid) + return false; + + auto needed_view_power = playlist->permissions()->getPermissionValue(permission::i_playlist_needed_view_power); + return !this->permission_granted(playlist_view_power, needed_view_power, false); + }), playlists.end()); + + if(playlists.empty()) + return {ErrorType::DBEmpty}; + + Command notify(this->notify_response_command("notifyplaylistlist")); + + size_t index = 0; + for(const auto& entry : playlists) { + notify[index]["playlist_id"] = entry->playlist_id(); + auto bot = entry->current_bot(); + notify[index]["playlist_bot_id"] = bot ? bot->getClientDatabaseId() : 0; + notify[index]["playlist_title"] = entry->properties()[property::PLAYLIST_TITLE].value(); + notify[index]["playlist_type"] = entry->properties()[property::PLAYLIST_TYPE].value(); + notify[index]["playlist_owner_dbid"] = entry->properties()[property::PLAYLIST_OWNER_DBID].value(); + notify[index]["playlist_owner_name"] = entry->properties()[property::PLAYLIST_OWNER_NAME].value(); + notify[index]["needed_power_modify"] = entry->permissions()->getPermissionValue(permission::i_playlist_needed_modify_power); + notify[index]["needed_power_permission_modify"] = entry->permissions()->getPermissionValue(permission::i_playlist_needed_permission_modify_power); + notify[index]["needed_power_delete"] = entry->permissions()->getPermissionValue(permission::i_playlist_needed_delete_power); + notify[index]["needed_power_song_add"] = entry->permissions()->getPermissionValue(permission::i_playlist_song_needed_add_power); + notify[index]["needed_power_song_move"] = entry->permissions()->getPermissionValue(permission::i_playlist_song_needed_move_power); + notify[index]["needed_power_song_remove"] = entry->permissions()->getPermissionValue(permission::i_playlist_song_needed_remove_power); + index++; + } + + this->sendCommand(notify); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandPlaylistCreate(ts::Command &cmd) { + CMD_REF_SERVER(ref_server); + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + CACHED_PERM_CHECK(permission::b_playlist_create, 1, true); + + { + auto max_playlists = this->cached_permission_value(permission::i_max_playlists); + if(max_playlists != permNotGranted) { + auto playlists = ref_server->musicManager->find_playlists_by_client(this->getClientDatabaseId()); + if(!this->permission_granted(this->cached_permission_value(permission::i_max_playlists), playlists.size(), false)) + return CommandResultPermissionError{permission::i_max_playlists}; + } + } + + auto playlist = ref_server->musicManager->create_playlist(this->getClientDatabaseId(), this->getDisplayName()); + if(!playlist) return {ErrorType::VSError}; + + playlist->properties()[property::PLAYLIST_TYPE] = music::Playlist::Type::GENERAL; + + { + auto max_songs = this->cached_permission_value(permission::i_max_playlist_size); + if(max_songs != permNotGranted) + playlist->properties()[property::PLAYLIST_MAX_SONGS] = max_songs; + } + + playlist->permissions()->setPermission(permission::i_playlist_song_needed_remove_power, this->cached_permission_value(permission::i_playlist_song_remove_power), nullptr); + playlist->permissions()->setPermission(permission::i_playlist_needed_delete_power, this->cached_permission_value(permission::i_playlist_delete_power), nullptr); + playlist->permissions()->setPermission(permission::i_playlist_needed_modify_power, this->cached_permission_value(permission::i_playlist_modify_power), nullptr); + playlist->permissions()->setPermission(permission::i_playlist_needed_permission_modify_power, this->cached_permission_value(permission::i_playlist_permission_modify_power), nullptr); + + Command notify(this->notify_response_command("notifyplaylistcreated")); + notify["playlist_id"] = playlist->playlist_id(); + this->sendCommand(notify); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandPlaylistDelete(ts::Command &cmd) { + CMD_REF_SERVER(ref_server); + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + auto playlist = ref_server->musicManager->find_playlist(cmd["playlist_id"]); + if(!playlist) return {findError("playlist_invalid_id")}; + + if(playlist->properties()[property::PLAYLIST_OWNER_DBID] != this->getClientDatabaseId()) + CACHED_PERM_CHECK(permission::i_playlist_delete_power, playlist->permissions()->getPermissionValue(permission::i_playlist_needed_delete_power)); + + string error; + if(!ref_server->musicManager->delete_playlist(playlist->playlist_id(), error)) { + logError(this->getServerId(), "Failed to delete playlist with id {}. Error: {}", playlist->playlist_id(), error); + return {ErrorType::VSError, error}; + } + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandPlaylistInfo(ts::Command &cmd) { + CMD_REF_SERVER(ref_server); + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + auto playlist = ref_server->musicManager->find_playlist(cmd["playlist_id"]); + if(!playlist) return {findError("playlist_invalid_id")}; + + if(playlist->properties()[property::PLAYLIST_OWNER_DBID] != this->getClientDatabaseId()) + CACHED_PERM_CHECK(permission::i_playlist_view_power, playlist->permissions()->getPermissionValue(permission::i_playlist_needed_view_power)); + + + Command notify(this->notify_response_command("notifyplaylistinfo")); + for(const auto& property : playlist->properties().list_properties(property::FLAG_PLAYLIST_VARIABLE)) { + notify[property.type().name] = property.value(); + } + this->sendCommand(notify); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandPlaylistEdit(ts::Command &cmd) { + CMD_REF_SERVER(ref_server); + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + auto playlist = ref_server->musicManager->find_playlist(cmd["playlist_id"]); + if(!playlist) return {findError("playlist_invalid_id")}; + + if(playlist->properties()[property::PLAYLIST_OWNER_DBID] != this->getClientDatabaseId()) + CACHED_PERM_CHECK(permission::i_playlist_modify_power, playlist->permissions()->getPermissionValue(permission::i_playlist_needed_modify_power)); + + deque, string>> properties; + + for(const auto& key : cmd[0].keys()) { + if(key == "playlist_id") continue; + if(key == "return_code") continue; + + auto property = property::info(key); + if(*property == property::PLAYLIST_UNDEFINED) { + logError(this->getServerId(), R"([{}] Tried to edit a not existing playlist property "{}" to "{}")", CLIENT_STR_LOG_PREFIX, key, cmd[key].string()); + continue; + } + + if((property->flags & property::FLAG_USER_EDITABLE) == 0) { + logError(this->getServerId(), "[{}] Tried to change a playlist property which is not changeable. (Key: {}, Value: \"{}\")", CLIENT_STR_LOG_PREFIX, key, cmd[key].string()); + continue; + } + + if(!property->validate_input(cmd[key].as())) { + logError(this->getServerId(), "[{}] Tried to change a playlist property to an invalid value. (Key: {}, Value: \"{}\")", CLIENT_STR_LOG_PREFIX, key, cmd[key].string()); + continue; + } + + if(*property == property::PLAYLIST_CURRENT_SONG_ID) { + auto song_id = cmd[key].as(); + auto song = song_id > 0 ? playlist->find_song(song_id) : nullptr; + if(song_id != 0 && !song) + return {findError("playlist_invalid_song_id")}; + } else if(*property == property::PLAYLIST_MAX_SONGS) { + auto value = cmd[key].as(); + auto max_value = this->cached_permission_value(permission::i_max_playlist_size); + if(!this->permission_granted(max_value, value, false)) + return CommandResultPermissionError{permission::i_max_playlist_size}; + } + + properties.emplace_back(property, key); + } + for(const auto& property : properties) { + if(*property.first == property::PLAYLIST_CURRENT_SONG_ID) { + playlist->set_current_song(cmd[property.second]); + continue; + } + + playlist->properties()[property.first] = cmd[property.second].string(); + } + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandPlaylistPermList(ts::Command &cmd) { + CMD_REF_SERVER(ref_server); + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + auto playlist = ref_server->musicManager->find_playlist(cmd["playlist_id"]); + if(!playlist) return {findError("playlist_invalid_id")}; + + if(playlist->properties()[property::PLAYLIST_OWNER_DBID] != this->getClientDatabaseId()) + CACHED_PERM_CHECK(permission::b_virtualserver_playlist_permission_list, 1, true); + + auto permissions = playlist->permissions()->listPermissions(PERM_FLAG_PUBLIC); + if(permissions.empty()) + return {ErrorType::VSError}; + + Command result(this->notify_response_command("notifyplaylistpermlist")); + int index = 0; + result["playlist_id"] = playlist->playlist_id(); + for (const auto &elm : permissions) { + if(elm->hasValue()) { + result[index]["permid"] = elm->type->type; + + result[index]["permvalue"] = elm->value; + result[index]["permnegated"] = elm->flag_negate; + result[index]["permskip"] = elm->flag_skip; + index++; + } + if(elm->hasGrant()) { + result[index]["permid"] = (uint16_t) (elm->type->type | PERM_ID_GRANT); + result[index]["permvalue"] = elm->granted; + result[index]["permnegated"] = 0; + result[index]["permskip"] = 0; + index++; + } + } + this->sendCommand(result); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandPlaylistAddPerm(ts::Command &cmd) { + CMD_REF_SERVER(ref_server); + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + auto playlist = ref_server->musicManager->find_playlist(cmd["playlist_id"]); + if(!playlist) return {findError("playlist_invalid_id")}; + + if(playlist->properties()[property::PLAYLIST_OWNER_DBID] != this->getClientDatabaseId()) + CACHED_PERM_CHECK(permission::i_playlist_permission_modify_power, playlist->permissions()->getPermissionValue(permission::i_playlist_needed_permission_modify_power), true); + + auto maxValue = this->getPermissionGrantValue(permission::i_permission_modify_power, this->currentChannel); + bool ignoreGrant = this->permission_granted(this->cached_permission_value(permission::b_permission_modify_power_ignore), 1, true); + + bool conOnError = cmd[0].has("continueonerror"); + for (int index = 0; index < cmd.bulkCount(); index++) { + PARSE_PERMISSION(cmd); + + auto val = cmd[index]["permvalue"].as(); + if(permission_require_granted_value(permType) && val > maxValue) + return CommandResultPermissionError{permission::i_permission_modify_power}; + + if(!ignoreGrant && !this->permissionGrantGranted(permission::PERMTEST_ORDERED, permType, 1, this->currentChannel)) + return CommandResultPermissionError{permission::i_permission_modify_power}; + + if (grant) { + playlist->permissions()->setPermissionGranted(permType, cmd[index]["permvalue"], nullptr); + } else { + + playlist->permissions()->setPermission(permType, cmd[index]["permvalue"], nullptr, cmd[index]["permnegated"], cmd[index]["permskip"]); + } + } + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandPlaylistDelPerm(ts::Command &cmd) { + CMD_REF_SERVER(ref_server); + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + + auto playlist = ref_server->musicManager->find_playlist(cmd["playlist_id"]); + if(!playlist) return {findError("playlist_invalid_id")}; + + if(playlist->properties()[property::PLAYLIST_OWNER_DBID] != this->getClientDatabaseId()) + CACHED_PERM_CHECK(permission::i_playlist_permission_modify_power, playlist->permissions()->getPermissionValue(permission::i_playlist_needed_permission_modify_power), true); + + bool ignoreGrant = this->permission_granted(this->cached_permission_value(permission::b_permission_modify_power_ignore), 1, true); + bool conOnError = cmd[0].has("continueonerror"); + + for (int index = 0; index < cmd.bulkCount(); index++) { + PARSE_PERMISSION(cmd); + + if(!ignoreGrant && !this->permissionGrantGranted(permission::PERMTEST_ORDERED, permType, 1, this->currentChannel)) + return CommandResultPermissionError{permission::i_permission_modify_power}; + + if (grant) { + playlist->permissions()->setPermissionGranted(permType, permNotGranted, nullptr); + } else { + playlist->permissions()->deletePermission(permType, nullptr); + } + } + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandPlaylistSongList(ts::Command &cmd) { + CMD_REF_SERVER(ref_server); + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + auto playlist = ref_server->musicManager->find_playlist(cmd["playlist_id"]); + if(!playlist) return {findError("playlist_invalid_id")}; + + if(playlist->properties()[property::PLAYLIST_OWNER_DBID] != this->getClientDatabaseId()) + CACHED_PERM_CHECK(permission::i_playlist_view_power, playlist->permissions()->getPermissionValue(permission::i_playlist_needed_view_power)); + + auto songs = playlist->list_songs(); + if(songs.empty()) + return {ErrorType::DBEmpty}; + + Command notify(this->notify_response_command("notifyplaylistsonglist")); + notify["playlist_id"] = playlist->playlist_id(); + + size_t index = 0; + for(const auto& song : songs) { + notify[index]["song_id"] = song->id; + notify[index]["song_invoker"] = song->invoker; + notify[index]["song_previous_song_id"] = song->previous_song_id; + notify[index]["song_url"] = song->url; + notify[index]["song_url_loader"] = song->url_loader; + notify[index]["song_loaded"] = song->loaded; + notify[index]["song_metadata"] = song->metadata; + index++; + } + this->sendCommand(notify); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandPlaylistSongAdd(ts::Command &cmd) { + CMD_REF_SERVER(ref_server); + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + auto playlist = ref_server->musicManager->find_playlist(cmd["playlist_id"]); + if(!playlist) return {findError("playlist_invalid_id")}; + + if(playlist->properties()[property::PLAYLIST_OWNER_DBID] != this->getClientDatabaseId()) + CACHED_PERM_CHECK(permission::i_playlist_song_add_power, playlist->permissions()->getPermissionValue(permission::i_playlist_song_needed_add_power)); + + if(!cmd[0].has("invoker")) + cmd["invoker"] = ""; + if(!cmd[0].has("previous")) { + auto songs = playlist->list_songs(); + if(songs.empty()) + cmd["previous"] = "0"; + else + cmd["previous"] = songs.back()->id; + } + + + auto song = playlist->add_song(_this.lock(), cmd["url"], cmd["invoker"], cmd["previous"]); + if(!song) return {ErrorType::VSError}; + + Command notify(this->notify_response_command("notifyplaylistsongadd")); + notify["song_id"] = song->id; + this->sendCommand(notify); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandPlaylistSongReorder(ts::Command &cmd) { + CMD_REF_SERVER(ref_server); + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + auto playlist = ref_server->musicManager->find_playlist(cmd["playlist_id"]); + if(!playlist) return {findError("playlist_invalid_id")}; + + if(playlist->properties()[property::PLAYLIST_OWNER_DBID] != this->getClientDatabaseId()) + CACHED_PERM_CHECK(permission::i_playlist_song_move_power, playlist->permissions()->getPermissionValue(permission::i_playlist_song_needed_move_power)); + + SongId song_id = cmd["song_id"]; + SongId previous_id = cmd["song_previous_song_id"]; + + auto song = playlist->find_song(song_id); + if(!song) return {findError("playlist_invalid_song_id")}; + + if(!playlist->reorder_song(song_id, previous_id)) + return {ErrorType::VSError}; + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandPlaylistSongRemove(ts::Command &cmd) { + CMD_REF_SERVER(ref_server); + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + auto playlist = ref_server->musicManager->find_playlist(cmd["playlist_id"]); + if(!playlist) return {findError("playlist_invalid_id")}; + + if(playlist->properties()[property::PLAYLIST_OWNER_DBID] != this->getClientDatabaseId()) + CACHED_PERM_CHECK(permission::i_playlist_song_remove_power, playlist->permissions()->getPermissionValue(permission::i_playlist_song_needed_remove_power)); + + SongId song_id = cmd["song_id"]; + + auto song = playlist->find_song(song_id); + if(!song) return {findError("playlist_invalid_song_id")}; + + if(!playlist->delete_song(song_id)) + return {ErrorType::VSError}; + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandMusicBotQueueList(Command& cmd) { + return CommandResult::NotImplemented; //FIXME + + /* + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + auto bot = this->server->musicManager->findBotById(cmd["bot_id"]); + if(!bot) return {findError("music_invalid_id")}; + PERM_CHECK_CHANNELR(permission::i_client_music_info, bot->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_music_needed_info, bot->currentChannel), this->currentChannel, true); + + + bool bulked = cmd.hasParm("bulk") || cmd.hasParm("balk") || cmd.hasParm("pipe") || cmd.hasParm("bar") || cmd.hasParm("paypal"); + int command_index = 0; + + Command notify(this->getExternalType() == CLIENT_VOICE ? "notifymusicqueueentry" : ""); + { + auto history = bot->queue()->history(); + for(int index = history.size(); index > 0; index--) { + if(!bulked) + notify = Command(this->getExternalType() == CLIENT_VOICE ? "notifymusicqueueentry" : ""); + + apply_song(notify, history[index - 1], command_index); + notify[command_index]["queue_index"] = -index; + + if(!bulked) + this->sendCommand(notify); + else + command_index++; + } + } + + { + if(!bulked) + notify = Command(this->getExternalType() == CLIENT_VOICE ? "notifymusicqueueentry" : ""); + + auto song = bot->queue()->currentSong(); + apply_song(notify, song, command_index); + notify[command_index]["queue_index"] = 0; + + if(!bulked) + this->sendCommand(notify); + else if(song) + command_index++; + } + + + { + auto queue = bot->queue()->queueEntries(); + for(int index = 0; index < queue.size(); index++) { + if(!bulked) + notify = Command(this->getExternalType() == CLIENT_VOICE ? "notifymusicqueueentry" : ""); + + apply_song(notify, queue[index], command_index); + notify[command_index]["queue_index"] = index + 1; + + if(!bulked) + this->sendCommand(notify); + else + command_index++; + } + } + + debugMessage(this->getServerId(),"Send: {}",notify.build()); + if(bulked) { + if(command_index > 0) { + this->sendCommand(notify); + } else return { ErrorType::DBEmpty }; + } + + if(this->getExternalType() == CLIENT_VOICE) { + Command notify("notifymusicqueuefinish"); + notify["bot_id"] = bot->getClientDatabaseId(); + this->sendCommand(notify); + } + + return CommandResult::Success; + */ +} + +CommandResult ConnectedClient::handleCommandMusicBotQueueAdd(Command& cmd) { + return CommandResult::NotImplemented; //FIXME + + /* + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + auto bot = this->server->musicManager->findBotById(cmd["bot_id"]); + if(!bot) return {findError("music_invalid_id")}; + PERM_CHECK_CHANNELR(permission::i_client_music_play_power, bot->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_music_needed_play_power, bot->currentChannel), this->currentChannel, true); + + MusicClient::loader_t loader; + auto& type = cmd[0]["type"]; + if((type.castable() && type.as() == 0) || type.as() == "yt") { + loader = bot->ytLoader(this->getServer()); + } else if((type.castable() && type.as() == 1) || type.as() == "ffmpeg") { + loader = bot->ffmpegLoader(this->getServer()); + } else if((type.castable() && type.as() == 2) || type.as() == "channel") { + loader = bot->channelLoader(this->getServer()); + } else if((type.castable() && type.as() == -1) || type.as() == "any") { + loader = bot->providerLoader(this->getServer(), ""); + } + if(!loader) return {findError("music_invalid_action")}; + + auto entry = bot->queue()->insertEntry(cmd["url"], _this.lock(), loader); + if(!entry) return {ErrorType::VSError}; + + this->server->forEachClient([&](shared_ptr client) { + client->notifyMusicQueueAdd(bot, entry, bot->queue()->queueEntries().size() - 1, _this.lock()); + }); + + return CommandResult::Success; + */ +} + +CommandResult ConnectedClient::handleCommandMusicBotQueueRemove(Command& cmd) { + return CommandResult::NotImplemented; //FIXME + + /* + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + auto bot = this->server->musicManager->findBotById(cmd["bot_id"]); + if(!bot) return {findError("music_invalid_id")}; + PERM_CHECK_CHANNELR(permission::i_client_music_play_power, bot->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_music_needed_play_power, bot->currentChannel), this->currentChannel, true); + + std::deque> songs; + for(int index = 0; index < cmd.bulkCount(); index++) { + auto entry = bot->queue()->find_queue(cmd["song_id"]); + if(!entry) { + if(cmd.hasParm("skip_error")) continue; + return {ErrorType::DBEmpty}; + } + + songs.push_back(move(entry)); + } + + for(const auto& entry : songs) + bot->queue()->deleteEntry(dynamic_pointer_cast(entry)); + this->server->forEachClient([&](shared_ptr client) { + client->notifyMusicQueueRemove(bot, songs, _this.lock()); + }); + return CommandResult::Success; + */ +} + +CommandResult ConnectedClient::handleCommandMusicBotQueueReorder(Command& cmd) { + return CommandResult::NotImplemented; //FIXME + + /* + CMD_REQ_SERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + auto bot = this->server->musicManager->findBotById(cmd["bot_id"]); + if(!bot) return {findError("music_invalid_id")}; + PERM_CHECK_CHANNELR(permission::i_client_music_play_power, bot->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_music_needed_play_power, bot->currentChannel), this->currentChannel, true); + + auto entry = bot->queue()->find_queue(cmd["song_id"]); + if(!entry) return {ErrorType::DBEmpty}; + + auto order = bot->queue()->changeOrder(entry, cmd["index"]); + if(order < 0) return {ErrorType::VSError}; + this->server->forEachClient([&](shared_ptr client) { + client->notifyMusicQueueOrderChange(bot, entry, order, _this.lock()); + }); + return CommandResult::Success; + */ +} + +CommandResult ConnectedClient::handleCommandMusicBotPlaylistAssign(ts::Command &cmd) { + CMD_REF_SERVER(ref_server); + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(25); + + auto bot = ref_server->musicManager->findBotById(cmd["bot_id"]); + if(!bot) return {findError("music_invalid_id")}; + if(bot->getOwner() != this->getClientDatabaseId()) + PERM_CHECK_CHANNELR(permission::i_client_music_play_power, bot->permissionValue(permission::PERMTEST_ORDERED, permission::i_client_music_needed_play_power, bot->currentChannel), this->currentChannel, true); + + auto playlist = ref_server->musicManager->find_playlist(cmd["playlist_id"]); + if(!playlist && cmd["playlist_id"] != 0) return {findError("playlist_invalid_id")}; + + if(ref_server->musicManager->find_bot_by_playlist(playlist)) + return {findError("playlist_already_in_use")}; + + if(playlist && playlist->properties()[property::PLAYLIST_OWNER_DBID] != this->getClientDatabaseId()) + CACHED_PERM_CHECK(permission::i_playlist_view_power, playlist->permissions()->getPermissionValue(permission::i_playlist_needed_view_power)); + + if(!ref_server->musicManager->assign_playlist(bot, playlist)) + return {ErrorType::VSError}; + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandHelp(Command& cmd) { + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(5); + PERM_CHECKR(permission::b_serverinstance_help_view, 1, false); + + 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 {findError("file_not_found"), "Could not resolve file " + file.string()}; + string line; + ifstream stream(file); + if(!stream) return {findError("file_io_error"), "Could not read documentation file " + file.string()}; + while(getline(stream, line)) + this->sendCommand(Command(line)); + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandPermReset(ts::Command& cmd) { + CMD_REQ_FSERVER; + CMD_RESET_IDLE; + CMD_CHK_AND_INC_FLOOD_POINTS(50); + PERM_CHECKR(permission::b_virtualserver_permission_reset, 1, true); + + string token; + if(!this->server->resetPermissions(token)) + return {ErrorType::VSError, "Could not reset permissions!"}; + + Command result(""); + result["token"] = token; + this->sendCommand(result); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandLogView(ts::Command& cmd) { + CMD_CHK_AND_INC_FLOOD_POINTS(50); + + auto lagacy = this->getType() == CLIENT_TEAMSPEAK || cmd.hasParm("lagacy") || cmd.hasParm("legacy"); + string log_path; + ServerId target_server = cmd[0].has("instance") && cmd["instance"].as() ? (ServerId) 0 : this->getServerId(); + string server_identifier; + if(target_server > 0) + server_identifier = to_string(target_server); + else server_identifier = "[A-Z]{0,7}"; + + if(target_server == 0) + PERM_CHECKR(permission::b_serverinstance_log_view, 1, true); + else + PERM_CHECKR(permission::b_virtualserver_log_view, 1, true); + + for(const auto& sink : logger::logger(target_server)->sinks()) { + if(dynamic_pointer_cast(sink)) { + log_path = dynamic_pointer_cast(sink)->_file_helper.filename(); + } + } + if(log_path.empty()) + return {findError("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 += "\"\\] \\[.*\\]( ){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 {findError("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.push_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.push_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 {findError("file_io_error"), "fread(...) returned " + to_string(read) + " (" + to_string(errno) + ")"}; + } + + if(!line_buffer.empty()) { + lines.push_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 {ErrorType::DBEmpty}; + 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); + } + result[index++]["l"] = ts; + } else { + result[index++]["l"] = line; + } + } + this->sendCommand(result); + + return CommandResult::Success; +} + +CommandResult ConnectedClient::handleCommandUpdateMyTsId(ts::Command &) { + if(config::voice::suppress_myts_warnings) return CommandResult::Success; + return CommandResult::NotImplemented; +} + +CommandResult ConnectedClient::handleCommandUpdateMyTsData(ts::Command &) { + if(config::voice::suppress_myts_warnings) return CommandResult::Success; + return CommandResult::NotImplemented; +} + + +CommandResult 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 {findError("server_invalid_id")}; + + auto global_list = this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_client_query_list, 1, nullptr, true, nullptr, server, true); + auto own_list = global_list || this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_client_query_list_own, 1, nullptr, true, nullptr, server, true); + + if(!own_list && !global_list) + return CommandResultPermissionError{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 {ErrorType::DBEmpty}; + + 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 CommandResult::Success; +} + +CommandResult 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 {findError("server_invalid_id")}; + + if(!this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_client_query_create, 1, nullptr, true, nullptr, server, true)) + return CommandResultPermissionError{permission::b_client_query_create}; + + auto username = cmd["client_login_name"].as(); + auto password = cmd[0].has("client_login_password") ? cmd["client_login_password"].as() : ""; + + if(password.empty()) + password = rnd_string(QUERY_PASSWORD_LENGTH); + + auto account = serverInstance->getQueryServer()->find_query_account_by_name(username); + if(account) return {findError("query_already_exists")}; + + account = serverInstance->getQueryServer()->create_query_account(username, server_id, this->getUid(), password); + if(!account) + return {ErrorType::VSError}; + + 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 CommandResult::Success; +} + +CommandResult 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 {findError("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 {findError("server_invalid_id")}; + */ + + auto delete_all = this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_client_query_delete, 1, nullptr, true, nullptr, server, true); + auto delete_own = delete_all || this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_client_query_delete_own, 1, nullptr, true, nullptr, server, true); + + if(account->unique_id == this->getUid()) { + if(!delete_own) + return CommandResultPermissionError{permission::b_client_query_delete_own}; + } else { + if(!delete_all) + return CommandResultPermissionError{permission::b_client_query_delete}; + } + if(account->unique_id == "serveradmin" && account->username == "serveradmin") + return ErrorType::VSError; + + serverInstance->getQueryServer()->delete_query_account(account); + + return CommandResult::Success; +} + +CommandResult 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 {findError("query_not_exists")}; + + auto server = serverInstance->getVoiceServerManager()->findServerById(account->bound_server); + if(!server && account->bound_server != 0) + return {findError("server_invalid_id")}; + + auto rename_all = this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_client_query_rename, 1, nullptr, true, nullptr, server, true); + auto rename_own = rename_all || this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_client_query_rename_own, 1, nullptr, true, nullptr, server, true); + + if(account->unique_id == this->getUid()) { + if(!rename_own) + return CommandResultPermissionError{permission::b_client_query_rename_own}; + } else { + if(!rename_all) + return CommandResultPermissionError{permission::b_client_query_rename}; + } + + auto target_account = serverInstance->getQueryServer()->find_query_account_by_name(new_username); + if(target_account) return {findError("query_already_exists")}; + + if(account->unique_id == "serveradmin" && account->username == "serveradmin") + return ErrorType::VSError; + + if(!serverInstance->getQueryServer()->rename_query_account(account, new_username)) + return {ErrorType::VSError}; + + return CommandResult::Success; +} + +CommandResult 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 {findError("query_not_exists")}; + + auto server = serverInstance->getVoiceServerManager()->findServerById(account->bound_server); + if(!server && account->bound_server != 0) + return {findError("server_invalid_id")}; + + auto change_all = this->permissionGranted(permission::PERMTEST_ORDERED, server ? permission::b_client_query_change_password : permission::b_client_query_change_password_global, 1, nullptr, true, nullptr, server, true); + auto change_own = change_all || this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_client_query_change_own_password, 1, nullptr, true, nullptr, server, true); + + 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 CommandResultPermissionError{permission::b_client_query_change_own_password}; + } else { + if(!change_all) + return CommandResultPermissionError{server ? permission::b_client_query_change_password : permission::b_client_query_change_password_global}; + } + + if(!serverInstance->getQueryServer()->change_query_password(account, password)) + return {ErrorType::VSError}; + + Command result(this->getExternalType() == ClientType::CLIENT_TEAMSPEAK ? "notifyquerypasswordchanges" : ""); + result["client_login_name"] = account->username; + result["client_login_password"] = password; + this->sendCommand(result); + + return CommandResult::Success; +} + +CommandResult 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 && !this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_client_ignore_vpn, 1)) { + 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 CommandResult::Success; + } + } + + 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) { + logError(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.lock(), 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()); + } + } + + return CommandResult::Success; +} + + + + + + + + + + + + + + + + + + + diff --git a/server/src/client/ConnectedClientNotifyHandler.cpp b/server/src/client/ConnectedClientNotifyHandler.cpp new file mode 100644 index 0000000..6bc7708 --- /dev/null +++ b/server/src/client/ConnectedClientNotifyHandler.cpp @@ -0,0 +1,711 @@ +#include +#include +#include +#include "ConnectedClient.h" +#include "voice/VoiceClient.h" +#include "../server/file/FileServer.h" +#include "../server/VoiceServer.h" +#include "../InstanceHandler.h" +#include "../server/QueryServer.h" +#include "music/MusicClient.h" +#include +#include +#include + +using namespace std::chrono; +using namespace std; +using namespace ts; +using namespace ts::server; +using namespace ts::token; + +extern ts::server::InstanceHandler* serverInstance; + +#define INVOKER(command, invoker) \ +do { \ + if(invoker) { \ + command["invokerid"] = invoker->getClientId(); \ + command["invokername"] = invoker->getDisplayName(); \ + command["invokeruid"] = invoker->getUid(); \ + } else { \ + command["invokerid"] = 0; \ + command["invokername"] = "undefined"; \ + command["invokeruid"] = "undefined"; \ + } \ +} while(0) + + +bool ConnectedClient::notifyServerGroupList() { + Command cmd(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifyservergrouplist" : ""); + int index = 0; + + for (const auto& group : (this->server ? this->server->groups : serverInstance->getGroupManager().get())->availableServerGroups(true)) { + if(group->target() == GroupTarget::GROUPTARGET_CHANNEL) { + cmd[index]["cgid"] = group->groupId(); + } else { + cmd[index]["sgid"] = group->groupId(); + } + for (const auto &prop : group->properties().list_properties(property::FLAG_GROUP_VIEW, this->getType() == CLIENT_TEAMSPEAK ? property::FLAG_NEW : (uint16_t) 0)) + cmd[index][prop.type().name] = prop.value(); + + + auto modify_power = group->permissions()->permission_value_flagged(permission::i_displayed_group_needed_modify_power); + auto add_power = group->permissions()->permission_value_flagged(permission::i_displayed_group_needed_member_add_power); + auto remove_power = group->permissions()->permission_value_flagged(permission::i_displayed_group_needed_member_remove_power); + cmd[index]["n_modifyp"] = modify_power.has_value ? modify_power.value : 0; + cmd[index]["n_member_addp"] = add_power.has_value ? add_power.value : 0; + cmd[index]["n_member_removep"] = remove_power.has_value ? remove_power.value : 0; + index++; + } + + this->sendCommand(cmd); + return true; +} + +bool ConnectedClient::notifyGroupPermList(const std::shared_ptr& group, bool as_sid) { + Command cmd(this->getExternalType() == CLIENT_TEAMSPEAK ? group->target() == GROUPTARGET_SERVER ? "notifyservergrouppermlist" : "notifychannelgrouppermlist" : ""); + if (group->target() == GROUPTARGET_SERVER) + cmd["sgid"] = group->groupId(); + else + cmd["cgid"] = group->groupId(); + + int index = 0; + + auto permissions = group->permissions()->permissions(); + for (const auto &permission_data : permissions) { + auto& permission = get<1>(permission_data); + if(!permission.flags.value_set) + continue; + + auto type = permission::resolvePermissionData(get<0>(permission_data)); + if(as_sid) { + cmd[index]["permsid"] = type->name; + } else { + cmd[index]["permid"] = (uint16_t) type->type; + } + cmd[index]["permvalue"] = permission.values.value; + cmd[index]["permnegated"] = permission.flags.negate; + cmd[index]["permskip"] = permission.flags.skip; + index++; + } + for (const auto &permission_data : permissions) { + auto& permission = get<1>(permission_data); + if(!permission.flags.grant_set) + continue; + + auto type = permission::resolvePermissionData(get<0>(permission_data)); + if(as_sid) { + cmd[index]["permsid"] = type->name; + } else { + cmd[index]["permid"] = (uint16_t) type->type; + } + cmd[index]["permvalue"] = permission.values.value; + cmd[index]["permnegated"] = permission.flags.negate; + cmd[index]["permskip"] = permission.flags.skip; + index++; + } + + if (index == 0) + return false; + + this->sendCommand(cmd); + return true; +} + +bool ConnectedClient::notifyClientPermList(ClientDbId cldbid, const std::shared_ptr& mgr, bool perm_sids) { + Command res(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifyclientpermlist" : ""); + + auto permissions = mgr->permissions(); + if(permissions.empty()) + return false; + + int index = 0; + res[index]["cldbid"] = cldbid; + + for (const auto &permission_data : permissions) { + auto& permission = std::get<1>(permission_data); + if(permission.flags.value_set) { + if (perm_sids) + res[index]["permsid"] = permission::resolvePermissionData(get<0>(permission_data))->name; + else + res[index]["permid"] = get<0>(permission_data); + res[index]["permvalue"] = permission.values.value; + + res[index]["permnegated"] = permission.flags.negate; + res[index]["permskip"] = permission.flags.skip; + index++; + } + + + if(permission.flags.grant_set) { + if (perm_sids) + res[index]["permsid"] = permission::resolvePermissionData(get<0>(permission_data))->grant_name; + else + res[index]["permid"] = (get<0>(permission_data) | PERM_ID_GRANT); + res[index]["permvalue"] = permission.values.grant; + res[index]["permnegated"] = 0; + res[index]["permskip"] = 0; + index++; + } + } + + this->sendCommand(res); + return true; +} + +bool ConnectedClient::notifyChannelGroupList() { + Command cmd(this->getExternalType() == CLIENT_TEAMSPEAK ? "notifychannelgrouplist" : ""); + int index = 0; + for (const auto &group : (this->server ? this->server->groups : serverInstance->getGroupManager().get())->availableChannelGroups(false)) { + if(group->target() == GroupTarget::GROUPTARGET_CHANNEL) { + cmd[index]["cgid"] = group->groupId(); + } else { + cmd[index]["sgid"] = group->groupId(); + } + for (auto &prop : group->properties().list_properties(property::FLAG_GROUP_VIEW, this->getType() == CLIENT_TEAMSPEAK ? property::FLAG_NEW : (uint16_t) 0)) + cmd[index][prop.type().name] = prop.value(); + + + auto modify_power = group->permissions()->permission_value_flagged(permission::i_displayed_group_needed_modify_power); + auto add_power = group->permissions()->permission_value_flagged(permission::i_displayed_group_needed_member_add_power); + auto remove_power = group->permissions()->permission_value_flagged(permission::i_displayed_group_needed_member_remove_power); + cmd[index]["n_modifyp"] = modify_power.has_value ? modify_power.value : 0; + cmd[index]["n_member_addp"] = add_power.has_value ? add_power.value : 0; + cmd[index]["n_member_removep"] = remove_power.has_value ? remove_power.value : 0; + index++; + } + if(index == 0) return false; + this->sendCommand(cmd); + return true; +} + +bool ConnectedClient::notifyTextMessage(ChatMessageMode mode, const shared_ptr &invoker, uint64_t targetId, const string &textMessage) { + //notifytextmessage targetmode=1 msg=asdasd target=2 invokerid=1 invokername=WolverinDEV invokeruid=xxjnc14LmvTk+Lyrm8OOeo4tOqw= + Command cmd("notifytextmessage"); + INVOKER(cmd, invoker); + cmd["targetmode"] = mode; + cmd["target"] = targetId; + cmd["msg"] = textMessage; + this->sendCommand(cmd); + return true; +} + +bool ConnectedClient::notifyServerGroupClientAdd(const shared_ptr &invoker, const shared_ptr &client, const shared_ptr &group) { + Command cmd("notifyservergroupclientadded"); + INVOKER(cmd, invoker); + + cmd["sgid"] = group->groupId(); + cmd["clid"] = client->getClientId(); + cmd["name"] = client->getDisplayName(); + cmd["cluid"] = client->getUid(); + + this->sendCommand(cmd); + return true; +} + +bool ConnectedClient::notifyServerGroupClientRemove(std::shared_ptr invoker, std::shared_ptr client, std::shared_ptr group) { + Command cmd("notifyservergroupclientdeleted"); + INVOKER(cmd, invoker); + + cmd["sgid"] = group->groupId(); + cmd["clid"] = client->getClientId(); + cmd["name"] = client->getDisplayName(); + cmd["cluid"] = client->getUid(); + + this->sendCommand(cmd); + return true; +} + +bool ConnectedClient::notifyClientChannelGroupChanged( + const std::shared_ptr &invoker, + const std::shared_ptr &client, + const std::shared_ptr& channel, + const std::shared_ptr& inherited, + const std::shared_ptr& group, + bool lock_channel_tree) { + assert(!lock_channel_tree); /* not supported */ + + if(!this->isClientVisible(client, false) && client != this) return false; + if(client->getChannel() != channel) return false; + + assert(client); + assert(channel); + assert(inherited); + assert(group); + Command cmd("notifyclientchannelgroupchanged"); + INVOKER(cmd, invoker); + + cmd["cgid"] = group->groupId(); + cmd["clid"] = client->getClientId(); + cmd["cid"] = channel->channelId(); + cmd["cgi"] = inherited ? inherited->channelId() : channel->channelId(); //The inherited channel + + this->sendCommand(cmd); + return true; +} + +bool ConnectedClient::notifyConnectionInfo(std::shared_ptr target, std::shared_ptr info) { + Command notify("notifyconnectioninfo"); + notify["clid"] = target->getClientId(); + + if(target->getClientId() != this->getClientId()){ + for(const auto& elm : info->properties) + notify[elm.first] = elm.second; + notify["connection_connected_time"] = chrono::duration_cast(chrono::system_clock::now() - target->connectTimestamp).count(); + notify["connection_filetransfer_bandwidth_sent"] = 0; + notify["connection_filetransfer_bandwidth_received"] = 0; + } + + if(target->getClientId() == this->getClientId() || this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_client_remoteaddress_view, 1, target->currentChannel, true)) { + notify["connection_client_ip"] = target->getLoggingPeerIp(); + notify["connection_client_port"] = target->getPeerPort(); + } + + notify["connection_client2server_packetloss_speech"] = 0.f; + notify["connection_client2server_packetloss_keepalive"] = 0.f; + notify["connection_client2server_packetloss_control"] = 0.f; + notify["connection_client2server_packetloss_total"] = 0.f; + + notify["connection_idle_time"] = chrono::duration_cast(chrono::system_clock::now() - target->idleTimestamp).count(); + this->sendCommand(notify); + return true; +} + +bool ConnectedClient::notifyClientMoved(const shared_ptr &client, + const std::shared_ptr &target_channel, + ViewReasonId reason, + std::string msg, + std::shared_ptr invoker, + bool lock_channel_tree) { + assert(!lock_channel_tree); + assert(client->getClientId() > 0); + assert(client->currentChannel); + assert(target_channel); + assert(this->isClientVisible(client, false) || &*client == this); + + Command mv("notifyclientmoved"); + + mv["clid"] = client->getClientId(); + mv["cfid"] = client->currentChannel->channelId(); + mv["ctid"] = target_channel->channelId(); + mv["reasonid"] = reason; + if (invoker) + INVOKER(mv, invoker); + if (!msg.empty()) mv["reasonmsg"] = msg; + else mv["reasonmsg"] = ""; + + this->sendCommand(mv); + return true; +} + +bool ConnectedClient::notifyClientUpdated(const std::shared_ptr &client, const deque> &props, bool lock) { + shared_lock channel_lock(this->channel_lock, defer_lock); + if(lock) + channel_lock.lock(); + + if(!this->isClientVisible(client, false) && client != this) + return false; + + auto client_id = client->getClientId(); + if(client_id == 0) { + logError(this->getServerId(), "{} Attempted to send a clientupdate for client id 0. Updated client: {}", CLIENT_STR_LOG_PREFIX, CLIENT_STR_LOG_PREFIX_(client)); + return false; + } + Command response(this->getExternalType() == ClientType::CLIENT_TEAMSPEAK ? "notifyclientupdated" : ""); + response["clid"] = client_id; + for (const auto &prop : props) { + if(lastOnlineTimestamp.time_since_epoch().count() > 0 && (prop->property_index == property::CLIENT_TOTAL_ONLINE_TIME || prop->property_index == property::CLIENT_MONTH_ONLINE_TIME)) + response[prop->name] = client->properties()[prop].as() + duration_cast(system_clock::now() - client->lastOnlineTimestamp).count(); + else + response[prop->name] = client->properties()[prop].value(); + } + + this->sendCommand(response); + return true; +} + +bool ConnectedClient::notifyPluginCmd(std::string name, std::string msg, std::shared_ptr sender) { + Command notify("notifyplugincmd"); + notify["name"] = name; + notify["data"] = msg; + INVOKER(notify, sender); + this->sendCommand(notify); + return true; +} + +bool ConnectedClient::notifyClientChatComposing(const shared_ptr &client) { + Command notify("notifyclientchatcomposing"); + notify["clid"] = client->getClientId(); + notify["cluid"] = client->getUid(); + this->sendCommand(notify); + return true; +} + +bool ConnectedClient::notifyClientChatClosed(const shared_ptr &client) { + Command notify("notifyclientchatclosed"); + notify["clid"] = client->getClientId(); + notify["cluid"] = client->getUid(); + this->sendCommand(notify); + return true; +} + +bool ConnectedClient::notifyChannelMoved(const std::shared_ptr &channel, ChannelId order, const std::shared_ptr &invoker) { + if(!channel || !this->channels->channel_visible(channel)) return false; + + Command notify("notifychannelmoved"); + INVOKER(notify, invoker); + notify["reasonid"] = ViewReasonId::VREASON_MOVED; + notify["cid"] = channel->channelId(); + notify["cpid"] = channel->parent() ? channel->parent()->channelId() : 0; + notify["order"] = order; + this->sendCommand(notify); + return true; +} + +bool ConnectedClient::notifyChannelCreate(const std::shared_ptr &channel, ChannelId orderId, const std::shared_ptr &invoker) { + Command notify("notifychannelcreated"); + for (auto &prop : channel->properties().list_properties(property::FLAG_CHANNEL_VARIABLE | property::FLAG_CHANNEL_VIEW, this->getType() == CLIENT_TEAMSPEAK ? property::FLAG_NEW : (uint16_t) 0)) { + if(prop.type() == property::CHANNEL_ORDER) + notify[prop.type().name] = orderId; + else + notify[prop.type().name] = prop.value(); + } + INVOKER(notify, invoker); + this->sendCommand(notify); + return true; +} + +bool ConnectedClient::notifyChannelHide(const std::deque &channel_ids, bool lock_channel_tree) { + if(channel_ids.empty()) return true; + + if(this->getType() == ClientType::CLIENT_TEAMSPEAK) { //Voice hasnt that event + shared_lock server_channel_lock(this->server->channel_tree_lock, defer_lock); + unique_lock client_channel_lock(this->channel_lock, defer_lock); + if(lock_channel_tree) { + server_channel_lock.lock(); + client_channel_lock.lock(); + } else { + assert(mutex_locked(this->channel_lock)); + } + + deque> clients_to_remove; + { + for(const auto& w_client : this->visibleClients) { + auto client = w_client.lock(); + if(!client) continue; + if(client->currentChannel) { + auto id = client->currentChannel->channelId(); + for(const auto cid : channel_ids) { + if(cid == id) { + clients_to_remove.push_back(client); + break; + } + } + } + } + } + + //TODO: May send a unsubscribe and remove the clients like that? + this->notifyClientLeftView(clients_to_remove, "Channel gone out of view", false, ViewReasonServerLeft); + return this->notifyChannelDeleted(channel_ids, this->server->serverRoot); + } else { + Command notify("notifychannelhide"); + int index = 0; + for(const auto& channel : channel_ids) + notify[index++]["cid"] = channel; + this->sendCommand(notify); + } + return true; +} + +bool ConnectedClient::notifyChannelShow(const std::shared_ptr &channel, ts::ChannelId orderId) { + if(!channel) + return false; + + auto result = false; + if(this->getType() == ClientType::CLIENT_TEAMSPEAK) { //Voice hasn't that event + result = this->notifyChannelCreate(channel, orderId, this->server->serverRoot); + } else { + Command notify("notifychannelshow"); + for (auto &prop : channel->properties().list_properties(property::FLAG_CHANNEL_VARIABLE | property::FLAG_CHANNEL_VIEW, this->getType() == CLIENT_TEAMSPEAK ? property::FLAG_NEW : (uint16_t) 0)) { + if(prop.type() == property::CHANNEL_ORDER) + notify[prop.type().name] = orderId; + else + notify[prop.type().name] = prop.value(); + } + this->sendCommand(notify); + } + if(result && this->subscribeToAll) + this->subscribeChannel({channel}, false, true); + return true; +} + +bool ConnectedClient::notifyChannelDescriptionChanged(std::shared_ptr channel) { + if(!this->channels->channel_visible(channel)) return false; + Command notifyDesChanges("notifychanneldescriptionchanged"); + notifyDesChanges["cid"] = channel->channelId(); + this->sendCommand(notifyDesChanges); + return true; +} + +bool ConnectedClient::notifyChannelPasswordChanged(std::shared_ptr channel) { + if(!this->channels->channel_visible(channel)) return false; + Command notifyDesChanges("notifychannelpasswordchanged"); + notifyDesChanges["cid"] = channel->channelId(); + this->sendCommand(notifyDesChanges); + return true; +} + +bool ConnectedClient::notifyClientEnterView(const std::shared_ptr &client, const std::shared_ptr &invoker, const std::string& reason, const std::shared_ptr &to, ViewReasonId reasonId, const std::shared_ptr &from, bool lock_channel_tree) { + sassert(client && client->getClientId() > 0); + sassert(to); + sassert(!lock_channel_tree); /* we don't support locking */ + sassert(!this->isClientVisible(client, false) || &*client == this); + + switch (reasonId) { + case ViewReasonId::VREASON_MOVED: + case ViewReasonId::VREASON_BAN: + case ViewReasonId::VREASON_CHANNEL_KICK: + case ViewReasonId::VREASON_SERVER_KICK: + if(!invoker) { + logCritical(this->getServerId(), "{} ConnectedClient::notifyClientEnterView() => missing invoker for reason id {}", CLIENT_STR_LOG_PREFIX, reasonId); + if(this->server) + ;//invoker = this->server->serverRoot.get(); + } + break; + default: + break; + } + + Command cmd("notifycliententerview"); + + cmd["cfid"] = from ? from->channelId() : 0; + cmd["ctid"] = to ? to->channelId() : 0; + cmd["reasonid"] = reasonId; + INVOKER(cmd, invoker); + switch (reasonId) { + case ViewReasonId::VREASON_MOVED: + case ViewReasonId::VREASON_BAN: + case ViewReasonId::VREASON_CHANNEL_KICK: + case ViewReasonId::VREASON_SERVER_KICK: + cmd["reasonmsg"] = reason; + break; + default: + break; + } + + for (const auto &elm : client->properties()->list_properties(property::FLAG_CLIENT_VIEW, this->getType() == CLIENT_TEAMSPEAK ? property::FLAG_NEW : (uint16_t) 0)) { + cmd[elm.type().name] = elm.value(); + } + + visibleClients.emplace_back(client); + this->sendCommand(cmd); + + return true; +} + +bool ConnectedClient::notifyClientEnterView(const std::deque> &clients, const ts::ViewReasonSystemT &_vrs) { + if(clients.empty()) + return true; + assert(mutex_locked(this->channel_lock)); + + Command cmd("notifycliententerview"); + + cmd["cfid"] = 0; + cmd["reasonid"] = ViewReasonId::VREASON_SYSTEM; + ChannelId current_channel = 0; + + size_t index = 0; + auto it = clients.begin(); + while(it != clients.end()) { + auto client = *(it++); + + if(this->isClientVisible(client, false)) + continue; + + auto channel = client->getChannel(); + sassert(!channel || channel->channelId() != 0); + if(!channel) /* hmm suspecious */ + continue; + + if(current_channel != channel->channelId()) { + if(current_channel == 0) + cmd[index]["ctid"] = (current_channel = channel->channelId()); + else { + it--; /* we still have to send him */ + break; + } + } + + this->visibleClients.push_back(client); + for (const auto &elm : client->properties()->list_properties(property::FLAG_CLIENT_VIEW, this->getType() == CLIENT_TEAMSPEAK ? property::FLAG_NEW : (uint16_t) 0)) { + cmd[index][elm.type().name] = elm.value(); + } + + index++; + if(index > 16) /* max 16 clients per packet */ + break; + } + + if(index > 0) + this->sendCommand(cmd); + + if(it != clients.end()) + return this->notifyClientEnterView({it, clients.end()}, _vrs); + return true; +} + +bool ConnectedClient::notifyChannelEdited(std::shared_ptr channel, std::vector keys, ConnectedClient *invoker) { + auto v_channel = this->channels->find_channel(channel->channelId()); + if(!v_channel) return false; //Not visible? Important do not remove! + + Command notify("notifychanneledited"); + + auto counter = 0; + for (const auto& key : keys) { + auto info = property::impl::info(key); + if(*info == property::CHANNEL_UNDEFINED) { + logError(this->getServerId(), "Tried to edit a non existing channel property: " + key); + continue; + } + counter++; + if(*info == property::CHANNEL_ORDER) { + notify[key] = v_channel->previous_channel; + } else { + notify[key] = channel->properties()[info].as(); + } + } + if(counter == 0) return true; + + notify["cid"] = channel->channelId(); + INVOKER(notify, invoker); + notify["reasonid"] = ViewReasonId::VREASON_EDITED; + + this->sendCommand(notify); + return true; +} + +bool ConnectedClient::notifyChannelDeleted(const deque& channel_ids, const std::shared_ptr& invoker) { + if(channel_ids.empty()) + return true; + + Command notify("notifychanneldeleted"); + + int index = 0; + for (const auto& channel_id : channel_ids) + notify[index++]["cid"] = channel_id; + + INVOKER(notify, invoker); + notify["reasonid"] = ViewReasonId::VREASON_EDITED; + + this->sendCommand(notify); + return true; +} + +bool ConnectedClient::notifyServerUpdated(std::shared_ptr invoker) { + Command response("notifyserverupdated"); + + for (const auto& elm : this->server->properties().list_properties(property::FLAG_SERVER_VARIABLE, this->getType() == CLIENT_TEAMSPEAK ? property::FLAG_NEW : (uint16_t) 0)) { + if(elm.type() == property::VIRTUALSERVER_MIN_WINPHONE_VERSION) + continue; + + //if(elm->type() == property::VIRTUALSERVER_RESERVED_SLOTS) + response[elm.type().name] = elm.value(); + } + + if(getType() == CLIENT_QUERY) + INVOKER(response, invoker); + this->sendCommand(response); + return true; +} + +bool ConnectedClient::notifyClientPoke(std::shared_ptr invoker, std::string msg) { + Command cmd("notifyclientpoke"); + INVOKER(cmd, invoker); + cmd["msg"] = msg; + this->sendCommand(cmd); + return true; +} + +bool ConnectedClient::notifyChannelSubscribed(const deque> &channels) { + Command notify("notifychannelsubscribed"); + int index = 0; + for (const auto& ch : channels) { + notify[index]["es"] = this->server->getClientsByChannel(ch).empty() ? ch->emptySince() : 0; + notify[index++]["cid"] = ch->channelId(); + } + this->sendCommand(notify); + return true; +} + +bool ConnectedClient::notifyChannelUnsubscribed(const deque> &channels) { + Command notify("notifychannelunsubscribed"); + int index = 0; + for (const auto& ch : channels) { + notify[index]["es"] = this->server->getClientsByChannel(ch).empty() ? ch->emptySince() : 0; + notify[index++]["cid"] = ch->channelId(); + } + this->sendCommand(notify); + return true; +} + +bool ConnectedClient::notifyMusicQueueAdd(const shared_ptr& bot, const shared_ptr& entry, int index, const std::shared_ptr& invoker) { + Command notify("notifymusicqueueadd"); + notify["bot_id"] = bot->getClientDatabaseId(); + notify["song_id"] = entry->getSongId(); + notify["url"] = entry->getUrl(); + notify["index"] = index; + INVOKER(notify, invoker); + this->sendCommand(notify); + return true; +} + +bool ConnectedClient::notifyMusicQueueRemove(const std::shared_ptr &bot, const std::deque> &entry, const std::shared_ptr& invoker) { + Command notify("notifymusicqueueremove"); + notify["bot_id"] = bot->getClientDatabaseId(); + int index = 0; + for(const auto& song : entry) + notify[index++]["song_id"] = song->getSongId(); + INVOKER(notify, invoker); + this->sendCommand(notify); + return true; +} + +bool ConnectedClient::notifyMusicQueueOrderChange(const std::shared_ptr &bot, const std::shared_ptr &entry, int order, const std::shared_ptr& invoker) { + Command notify("notifymusicqueueorderchange"); + notify["bot_id"] = bot->getClientDatabaseId(); + notify["song_id"] = entry->getSongId(); + notify["index"] = order; + INVOKER(notify, invoker); + this->sendCommand(notify); + return true; +} + +bool ConnectedClient::notifyMusicPlayerStatusUpdate(const std::shared_ptr &bot) { + Command notify("notifymusicstatusupdate"); + notify["bot_id"] = bot->getClientDatabaseId(); + + auto player = bot->current_player(); + if(player) { + notify["player_buffered_index"] = player->bufferedUntil().count(); + notify["player_replay_index"] = player->currentIndex().count(); + } else { + notify["player_buffered_index"] = 0; + notify["player_replay_index"] = 0; + } + this->sendCommand(notify); + return true; +} + +extern void apply_song(Command& command, const std::shared_ptr& element, int index = 0); +bool ConnectedClient::notifyMusicPlayerSongChange(const std::shared_ptr &bot, const shared_ptr &newEntry) { + Command notify("notifymusicplayersongchange"); + notify["bot_id"] = bot->getClientDatabaseId(); + + if(newEntry) { + apply_song(notify, newEntry); + } else { + notify["song_id"] = 0; + } + this->sendCommand(notify); + return true; +} \ No newline at end of file diff --git a/server/src/client/ConnectedClientTextCommandHandler.cpp b/server/src/client/ConnectedClientTextCommandHandler.cpp new file mode 100644 index 0000000..ca68ef2 --- /dev/null +++ b/server/src/client/ConnectedClientTextCommandHandler.cpp @@ -0,0 +1,678 @@ +#include +#include +#include +#include +#include +#include +#include "ConnectedClient.h" +#include "src/client/voice/VoiceClient.h" + +using namespace ts; +using namespace ts::server; +using namespace std; +using namespace std::chrono; + +extern InstanceHandler *serverInstance; + +std::deque split(std::string str, std::string sep) { + char *cstr = const_cast(str.c_str()); + char *current; + std::deque arr; + current = strtok(cstr, sep.c_str()); + while (current != NULL) { + arr.push_back(current); + current = strtok(NULL, sep.c_str()); + } + return arr; +} + +#define ERR(sender, msg) \ +do { \ + send_message(sender, msg); \ + return true; \ +} while(false) + +#define TLEN(index) if(arguments.size() < index) ERR(serverInstance->musicRoot(), "Invalid argument count"); +#define TARG(index, type) (arguments.size() > index && arguments[index] == type) +#define TMUSIC(bot) if(!bot->current_player()) ERR(serverInstance->musicRoot(), "Im not playing a song!"); +#define GBOT(var, ignore_disabled) \ +auto var = this->selectedBot.lock(); \ +if(!var) var = dynamic_pointer_cast(target); \ +if(!var) { \ + send_message(serverInstance->musicRoot(), "Please select a music bot! (" + ts::config::music::command_prefix + "mbot select )"); \ + return true; \ +} \ +if(!ignore_disabled && var->properties()[property::CLIENT_DISABLED].as()) { \ + send_message(serverInstance->musicRoot(), "This bot has been disabled. Upgrade your TeaSpeak license to use more bots."); \ + return true; \ +} + +inline string filterUrl(string in){ + size_t index = 0; + while ((index = in.find("[URL")) != std::string::npos && index < in.length()) { + auto end = in.find(']', index); + in.replace(index, end - index + 1, "", 0); + } + + while ((index = in.find("[/URL")) != std::string::npos && index < in.length()) { + auto end = in.find(']', index); + in.replace(index, end - index + 1, "", 0); + } + return in; +} + +inline void permissionableCommand(ConnectedClient* client, stringstream& ss, const std::string& cmd, permission::PermissionType perm, permission::PermissionType sec = permission::unknown) { + auto permA = perm == permission::unknown || client->permissionGranted(permission::PERMTEST_ORDERED, perm, 1, client->getChannel(), true); + auto permB = permA || (sec != permission::unknown && client->permissionGranted(permission::PERMTEST_ORDERED, sec, 1, client->getChannel(), true)); + if(!(permA || permB)) { + ss << "[color=red]" << cmd << "[/color]" << endl; + } else { + ss << "[color=green]" << cmd << "[/color]" << endl; + } +} + +inline std::string bot_volume(float vol) { + auto volume = to_string(vol * 100); + auto idx = volume.find('.'); + return volume.substr(0, idx + 2); +} + +//FIXME add chat command for channel commander (within bot settings) +//Return true if blocked +bool ConnectedClient::handleTextMessage(ChatMessageMode mode, std::string text, const std::shared_ptr& target) { + if (text.length() < ts::config::music::command_prefix.length()) return false; + if (text.find(ts::config::music::command_prefix) != 0) return false; + + std::string command = text.substr(ts::config::music::command_prefix.length()); + auto arguments = command.find(' ') != -1 ? split(command.substr(command.find(' ') + 1), " ") : deque{}; + command = command.substr(0, command.find(' ')); + debugMessage(this->getServerId(), "Having command \"" + command + "\"."); + for (const auto &arg : arguments) + debugMessage(this->getServerId(), " Argument: '" + arg + "'"); + + + #define PERM_CHECK_BOT(perm, reqperm, err) \ + if(bot->properties()[property::CLIENT_OWNER] != this->getClientDatabaseId() && !this->permissionGranted(permission::PERMTEST_ORDERED, permission::perm, this->permissionValue(permission::PERMTEST_ORDERED, permission::reqperm, this->currentChannel), this->currentChannel, true)) { \ + send_message(serverInstance->musicRoot(), err); \ + return true; \ + } + +#define HANDLE_CMD_ERROR(_message) \ + if(result.extraProperties.count("extra_msg") > 0) \ + send_message(serverInstance->musicRoot(), string(_message) + ": " + result.extraProperties["extra_msg"]); \ + else if(result.extraProperties.count("failed_permid") > 0) \ + send_message(serverInstance->musicRoot(), string(_message) + ". (Missing permission " + permission::resolvePermissionData((permission::PermissionType) stoull(result.extraProperties["failed_permid"]))->name + ")"); \ + else \ + send_message(serverInstance->musicRoot(), string(_message) + ": " + result.error.message); + +#define JOIN_ARGS(variable, index) \ + string variable; \ + { \ + stringstream ss; \ + for (auto it = arguments.begin() + index; it != arguments.end(); it++) \ + ss << *it << (it + 1 == arguments.end() ? "" : " "); \ + variable = ss.str(); \ + } + + handle_text_command_fn_t function; + if(mode == ChatMessageMode::TEXTMODE_SERVER) { + function = [&](const shared_ptr& sender, const string& message) { + this->notifyTextMessage(ChatMessageMode::TEXTMODE_SERVER, sender, 0, message); + }; + } else if(mode == ChatMessageMode::TEXTMODE_CHANNEL) { + function = [&](const shared_ptr& sender, const string& message) { + this->notifyTextMessage(ChatMessageMode::TEXTMODE_CHANNEL, sender, 0, message); + }; + } else if(mode == ChatMessageMode::TEXTMODE_PRIVATE) { + function = [&, target](const shared_ptr& sender, const string& message) { + this->notifyTextMessage(ChatMessageMode::TEXTMODE_PRIVATE, target, this->getClientId(), message); + }; + + } + return handle_text_command(mode, command, arguments, function, target); +} + +bool ConnectedClient::handle_text_command( + ChatMessageMode mode, + const string &command, + const deque &arguments, + const function &, const string &)> &send_message, + const shared_ptr& target) { + + if (command == "mbot") { + if(!config::music::enabled) { + send_message(serverInstance->musicRoot(), "Music bots are not enabled! Enable them via the server config!"); + return true; + } + if (TARG(0, "create")) { + Command cmd(""); + auto result = this->handleCommandMusicBotCreate(cmd); + if(!result) { + HANDLE_CMD_ERROR("Failed to create music bot"); + return true; + } + send_message(serverInstance->musicRoot(), "Bot created"); + return true; + } else if (TARG(0, "list")) { + bool server = false; + if(TARG(1, "server")) + server = true; + + string locationStr = server ? "on this server" : "in this channel"; + if(!this->permissionGranted(permission::PERMTEST_ORDERED, server ? permission::b_client_music_server_list : permission::b_client_music_channel_list, 1, this->currentChannel, true)) { + send_message(serverInstance->musicRoot(), "You don't have the permission to list all music bots " + locationStr); + return true; + } + auto mbots = this->server->getClientsByChannel(server ? nullptr : this->currentChannel); + if (mbots.empty()) + send_message(serverInstance->musicRoot(), "There are no music bots " + locationStr); + else { + send_message(serverInstance->musicRoot(), "There are " + to_string(mbots.size()) + " music bots " + locationStr + ":"); + for (const auto &mbot : mbots) { + if(mbot->properties()[property::CLIENT_DISABLED].as()) { + send_message(serverInstance->musicRoot(), " - [color=red]" + to_string(mbot->getClientDatabaseId()) + " | " + mbot->getDisplayName() + " [DISABLED][/color]"); + } else { + send_message(serverInstance->musicRoot(), " - [color=green]" + to_string(mbot->getClientDatabaseId()) + " | " + mbot->getDisplayName() + "[/color]"); + } + } + } + return true; + } else if (TARG(0, "select")) { + TLEN(2); + if(arguments[1].find_first_not_of("0123456789") != std::string::npos) { + send_message(serverInstance->musicRoot(), "Invalid bot id"); + return true; + } + auto botId = static_cast(stoll(arguments[1])); + auto bot = this->server->musicManager->findBotById(botId); + if (!bot) ERR(serverInstance->musicRoot(), "Could not find target bot"); + if(bot->properties()[property::CLIENT_OWNER] != this->getClientDatabaseId() && !this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_client_music_channel_list, 1, this->currentChannel, true) && !this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_client_music_server_list, 1, this->currentChannel, true)) { //No perms for listing + send_message(serverInstance->musicRoot(), "You don't have the permission to select a music bot"); + return true; + } + + this->selectedBot = bot; + send_message(serverInstance->musicRoot(), "You successfully select the bot " + to_string(botId) + " | " + bot->getDisplayName()); + return true; + } else if (TARG(0, "rename")) { + TLEN(2); + GBOT(bot, true); + + stringstream ss; + for (auto it = arguments.begin() + 1; it != arguments.end(); it++) + ss << *it << (it + 1 == arguments.end() ? "" : " "); + string name = ss.str(); + + Command cmd(""); + cmd["client_nickname"] = ss.str(); + auto result = this->handleCommandClientEdit(cmd, bot); + if(!result) { + HANDLE_CMD_ERROR("Failed to rename bot"); + return true; + } + send_message(serverInstance->musicRoot(), "Name successfully changed!"); + return true; + } else if (TARG(0, "delete")) { + GBOT(bot, true); + PERM_CHECK_BOT(i_client_music_delete_power, i_client_music_needed_delete_power, "You don't have the permission to rename this music bot"); + this->server->musicManager->deleteBot(bot); + send_message(bot, "You successfully deleted this music bot!"); + return true; + } else if(TARG(0, "yt") || TARG(0, "soundcloud") || TARG(0, "sc")){ + TLEN(2); + GBOT(bot, false); + PERM_CHECK_BOT(i_client_music_play_power, i_client_music_needed_play_power, "You don't have the permission to play something this music bot"); + + auto playlist = bot->playlist(); + if(!playlist) { + send_message(bot, "bot hasnt a playlist!"); + return true; + } + + JOIN_ARGS(url, 1); + send_message(bot, "Queueing video " + url); + playlist->add_song(this->ref(), filterUrl(url), "YouTube"); + return true; + } else if(TARG(0, "stream")){ + TLEN(2); + GBOT(bot, false); + PERM_CHECK_BOT(i_client_music_play_power, i_client_music_needed_play_power, "You don't have the permission to play something this music bot"); + + auto playlist = bot->playlist(); + if(!playlist) { + send_message(bot, "bot hasnt a playlist!"); + return true; + } + + JOIN_ARGS(url, 1); + send_message(bot, "Queueing video " + url); + playlist->add_song(this->ref(), filterUrl(url), "FFMpeg"); + return true; + } else if(TARG(0, "player")) { + TLEN(2); + GBOT(bot, false); + PERM_CHECK_BOT(i_client_music_play_power, i_client_music_needed_play_power, "You don't have the permission to play something this music bot"); + + auto playlist = bot->playlist(); + if(!playlist) { + send_message(bot, "bot hasnt a playlist!"); + return true; + } + + JOIN_ARGS(url, 2); + send_message(bot, "Queueing video " + url); + playlist->add_song(this->ref(), filterUrl(url), arguments[1]); + return true; + } else if(TARG(0, "forward")){ + TLEN(2); + GBOT(bot, false); + PERM_CHECK_BOT(i_client_music_play_power, i_client_music_needed_play_power, "You don't have the permission to play something this music bot"); + TMUSIC(bot); + + if(arguments[1].find_first_not_of("0123456789") != std::string::npos) { + send_message(bot, "Invalid number of seconds!"); + return true; + } + threads::Thread([bot, arguments]() { + bot->current_player()->forward(seconds(stoll(arguments[1]))); + }).detach(); + send_message(bot, "Skipped " + to_string(stoll(arguments[1])) + " seconds!"); + return true; + } else if(TARG(0, "rewind")){ + TLEN(2); + GBOT(bot, false); + PERM_CHECK_BOT(i_client_music_play_power, i_client_music_needed_play_power, "You don't have the permission to play something this music bot"); + TMUSIC(bot); + + if(arguments[1].find_first_not_of("0123456789") != std::string::npos) { + send_message(bot, "Invalid number of seconds!"); + return true; + } + threads::Thread([bot, arguments]() { + bot->current_player()->rewind(seconds(stoll(arguments[1]))); + }).detach(); + send_message(bot, "Rewind " + to_string(stoll(arguments[1])) + " seconds!"); + return true; + } else if(TARG(0, "stop")){ + GBOT(bot, true); + PERM_CHECK_BOT(i_client_music_play_power, i_client_music_needed_play_power, "You don't have the permission to play something this music bot"); + TMUSIC(bot); + bot->current_player()->stop(); + send_message(bot, "Music stopped!"); + return true; + } else if(TARG(0, "pause")){ + GBOT(bot, false); + PERM_CHECK_BOT(i_client_music_play_power, i_client_music_needed_play_power, "You don't have the permission to play something this music bot"); + TMUSIC(bot); + bot->current_player()->pause(); + send_message(bot, "Music paused!"); + return true; + } else if(TARG(0, "play")) { + if(arguments.size() >= 2) { + GBOT(bot, false); + PERM_CHECK_BOT(i_client_music_play_power, i_client_music_needed_play_power, "You don't have the permission to play something this music bot"); + + auto playlist = bot->playlist(); + if(!playlist) { + send_message(bot, "bot hasnt a playlist!"); + return true; + } + + send_message(bot, "Queueing video " + arguments[1]); + playlist->add_song(this->ref(), filterUrl(arguments[1]), ""); + return true; + } else { + GBOT(bot, false); + PERM_CHECK_BOT(i_client_music_play_power, i_client_music_needed_play_power, "You don't have the permission to play something this music bot"); + TMUSIC(bot); + bot->current_player()->play(); + send_message(bot, "Music started!"); + return true; + } + } else if(TARG(0, "info")){ + GBOT(bot, true); + PERM_CHECK_BOT(i_client_music_info, i_client_music_needed_info, "You don't have the permission to display the music bot information"); + send_message(bot, "Music bot info:"); + send_message(bot, " Bot id : " + to_string(bot->getClientDatabaseId())); + send_message(bot, " Bot name: " + bot->getDisplayName()); + send_message(bot, " Bot volume: " + bot_volume(bot->volumeModifier())); + send_message(bot, " State: " + to_string(bot->player_state())); + if(bot->current_player()){ + auto player = bot->current_player(); + send_message(bot, " Play state: player open"); + send_message(bot, " State : " + string(::music::stateNames[player->state()])); + send_message(bot, " Title : " + player->songTitle()); + send_message(bot, " Description : " + player->songDescription()); + send_message(bot, " Timeline : " + to_string(duration_cast(player->currentIndex()).count()) + "/" + to_string(duration_cast(player->length()).count())); + send_message(bot, " Buffered : " + to_string(duration_cast(player->bufferedUntil() - player->currentIndex()).count()) + " seconds"); + } else { + send_message(bot, " Play state: not playing"); + } + + auto bot_playlist = bot->playlist(); + if(bot_playlist) { + send_message(bot, " Playlist ID : " + to_string(bot_playlist->playlist_id())); + send_message(bot, " Playlist size : " + to_string(bot_playlist->list_songs().size())); + } else { + send_message(bot, " Playlist ID : No playlist assigned"); + } + return true; + } else if(TARG(0, "queue") || TARG(0, "playlist") || TARG(0, "pl")) { + GBOT(bot, false); + PERM_CHECK_BOT(i_client_music_info, i_client_music_needed_info, "You don't have the permission to display the music bot information"); + + auto bot_playlist = bot->playlist(); + if(bot_playlist) { + send_message(bot, "Playlist ID : " + to_string(bot_playlist->playlist_id())); + send_message(bot, "Playlist entries (" + to_string(bot_playlist->list_songs().size()) + "): [color=orange]orange[/color] = currently index"); + + set dbids; + for(const auto& song : bot_playlist->list_songs()) + dbids.insert(song->invoker); + + auto dbinfo = serverInstance->databaseHelper()->queryDatabaseInfo(this->getServer(), deque(dbids.begin(), dbids.end())); + + auto current_song = bot_playlist->currently_playing(); + for(const auto& song : bot_playlist->list_songs()) { + string invoker = "unknown"; + for(const auto& e : dbinfo) { + if(e->cldbid == song->invoker) { + invoker = "[URL=client://0/" + e->uniqueId + "~" + e->lastName + "]" + e->lastName + "[/URL]"; + break; + } + } + + string data = "\"" + song->url + "\" added by " + invoker; + if(song->id == current_song) + data = "[color=orange]" + data + "[/color]"; + send_message(bot, " - " + data); + } + + } else { + send_message(bot, "The bot hasn't a playlist"); + } + return true; + } else if(TARG(0, "next")) { + GBOT(bot, false); + PERM_CHECK_BOT(i_client_music_play_power, i_client_music_needed_play_power, "You don't have the permission to play something this music bot"); + bot->forwardSong(); + auto song = bot->current_song(); + if(song) + send_message(bot, "Replaying next song (" + song->getUrl() + ")"); + else send_message(bot, "Queue is empty! Could not forward!"); + return true; + } else if(TARG(0, "volume")) { + GBOT(bot, false); + PERM_CHECK_BOT(i_client_music_play_power, i_client_music_needed_play_power, "You don't have the permission to play something this music bot"); + if(arguments.size() < 2) { + send_message(bot, "Current volume: " + bot_volume(bot->volumeModifier())); + return true; + } + if(arguments[1].find_first_not_of(".-0123456789") != std::string::npos) { + send_message(bot, "Invalid volume!"); + return true; + } + auto volume = stof(arguments[1]); + if(volume < 0 || volume > 100) { + send_message(bot, "Invalid volume! Volume must be greater or equal to zero and less or equal then one!"); + return true; + } + + auto max_volume = this->cached_permission_value(permission::i_client_music_create_modify_max_volume); + if(max_volume != permNotGranted && !this->permission_granted(max_volume, volume, true)) { + send_message(bot, "You don't have the permission to use higher volumes that " + to_string(max_volume) + "%"); + return true; + } + + bot->volume_modifier(volume / 100); + send_message(bot, "Volume successfully changed to " + bot_volume(bot->volumeModifier())); + return true; + } else if(TARG(0, "formats")) { + auto providers = ::music::manager::registeredTypes(); + stringstream ss; + ss << "Available providers:" << endl; + for(const auto& prov : providers) { + ss << " " << prov->providerName << ":" << endl; + ss << " Description: " << prov->providerDescription << endl; + + auto fmts = prov->availableFormats(); + ss << " Supported formats: (" << fmts.size() << ")" << endl; + for(const auto& fmt : fmts) + ss << " - " << fmt << endl; + + auto prots = prov->availableProtocols(); + ss << " Supported protocols: (" << prots.size() << ")" << endl; + for(const auto& fmt : prots) + ss << " - " << fmt << endl; + } + send_message(serverInstance->musicRoot(), ss.str()); + return true; + } else if(TARG(0, "settings")) { + GBOT(bot, false); + PERM_CHECK_BOT(i_client_music_play_power, i_client_music_needed_play_power, "You don't have the permission to change anything on the bot"); //TODO FIXME! + + if(TARG(1, "bot")) { + + const static vector editable_properties = { + "client_nickname", + "client_player_volume", + "client_is_channel_commander", + "client_version", + "client_country", + "client_platform", + "client_bot_type", + "client_uptime_mode", + "client_is_priority_speaker", + "client_flag_notify_song_change" + }; + + if(arguments.size() < 3) { + send_message(bot, "Bot properties:"); + for(const auto& property : bot->properties()->list_properties(~0)) { + if(find(editable_properties.begin(), editable_properties.end(), property.type().name) == editable_properties.end()) continue; + + send_message(bot, " - " + property.type().name + " = " + property.value() + " " + (property.default_value() == property.value() ? "(default)" : "")); + } + } else if(arguments.size() < 4) { + if(find(editable_properties.begin(), editable_properties.end(), arguments[2]) == editable_properties.end()) { + send_message(bot, "Unknown property or property is not editable."); + return true; + } + + const std::shared_ptr &property_info = property::info(arguments[2]); + if(!property_info || property_info->type_property == property::PROP_TYPE_UNKNOWN || property_info->property_index == property::CLIENT_UNDEFINED) { + send_message(bot, "Unknown property " + arguments[2] + "."); + return true; + } + + auto prop = bot->properties()[(property::ClientProperties) property_info->property_index]; + send_message(bot, "Bot property " + property_info->name + " = " + prop.value() + " " + (property_info->default_value == prop.value() ? "(default)" : "")); + return true; + } else { + Command cmd(""); + JOIN_ARGS(value, 3); + cmd[arguments[2]] = value; + auto result = this->handleCommandClientEdit(cmd, bot); + if(!result) { + HANDLE_CMD_ERROR("Failed to change bot property"); + return true; + } + send_message(serverInstance->musicRoot(), "Property successfully changed!"); + return true; + } + + return true; + } else if(TARG(1, "playlist")) { + auto playlist = bot->playlist(); + if(!playlist) { + send_message(bot, "Bot hasn't a playlist!"); + return true; + } + if(arguments.size() < 3) { + send_message(bot, "Playlist properties:"); + for(const auto& property : playlist->properties().list_properties(property::FLAG_PLAYLIST_VARIABLE)) { + send_message(bot, " - " + property.type().name + " = " + property.value() + " " + (property.default_value() == property.value() ? "(default)" : "")); + } + } else if(arguments.size() < 4) { + const std::shared_ptr &property_info = property::info(arguments[2]); + if(!property_info || property_info->type_property == property::PROP_TYPE_UNKNOWN || property_info->property_index == property::PLAYLIST_UNDEFINED) { + send_message(bot, "Unknown property " + arguments[2] + "."); + return true; + } + + auto prop = playlist->properties()[(property::PlaylistProperties) property_info->property_index]; + send_message(bot, "Bot property " + property_info->name + " = " + prop.value() + " " + (property_info->default_value == prop.value() ? "(default)" : "")); + } else { + const std::shared_ptr &property_info = property::info(arguments[2]); + if(!property_info || property_info->type_property == property::PROP_TYPE_UNKNOWN || property_info->property_index == property::PLAYLIST_UNDEFINED) { + send_message(bot, "Unknown property " + arguments[2] + "."); + return true; + } + + JOIN_ARGS(value, 3); + if(!property_info->validate_input(value)) { + send_message(bot, "Please enter a valid value!"); + return true; + } + + if((property_info->flags & property::FLAG_USER_EDITABLE) == 0) { + send_message(bot, "This property isnt changeable!"); + return true; + } + + playlist->properties()[(property::PlaylistProperties) property_info->property_index] = value; + send_message(bot, "Property successfully changed"); + return true; + } + } else { + send_message(bot, "Please enter a valid setting mode"); + } + return true; + } + } else if (command == "help") { + //send_message(serverInstance->musicRoot(), " ̶.̶̶m̶̶b̶̶o̶̶t̶̶ ̶̶f̶̶o̶̶r̶̶m̶̶a̶̶t̶̶s (Not supported yet)"); + stringstream ss; + ss << "Available music bot commands: ([color=green]green[/color] = permission granted | [color=red]red[/color] = insufficient permissions)" << endl; + + bool has_list_server = this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_client_music_server_list, 1, this->currentChannel); + bool has_list_channel = this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_client_music_channel_list, 1, this->currentChannel); + permissionableCommand(this, ss, string() + " .mbot list [<[color=" + (has_list_server ? "green" : "red") + "]server[/color]|[color=" + (has_list_channel ? "green" : "red") + "]channel[/color]>]", permission::b_client_music_channel_list, permission::b_client_music_server_list); + permissionableCommand(this, ss, " " + ts::config::music::command_prefix + "mbot select ", permission::b_client_music_channel_list, permission::b_client_music_server_list); + permissionableCommand(this, ss, " " + ts::config::music::command_prefix + "mbot formats", permission::unknown); + + permissionableCommand(this, ss, " " + ts::config::music::command_prefix + "mbot create", permission::b_client_music_create_temporary); // [] + permissionableCommand(this, ss, " " + ts::config::music::command_prefix + "mbot info", permission::i_client_music_info); + permissionableCommand(this, ss, " " + ts::config::music::command_prefix + "mbot rename", permission::i_client_music_rename_power); + permissionableCommand(this, ss, " " + ts::config::music::command_prefix + "mbot delete", permission::i_client_music_delete_power); + + permissionableCommand(this, ss, " " + ts::config::music::command_prefix + "mbot yt