#include #include #include #include #include #include #include #include #include #include "src/channel/ClientChannelView.h" #include "SpeakingClient.h" #include "src/InstanceHandler.h" #include "StringVariable.h" #include "misc/timer.h" #include "../manager/ActionLogger.h" #include "./voice/VoiceClient.h" #include "../rtc/imports.h" using namespace std::chrono; using namespace ts; using namespace ts::server; using namespace ts::protocol; //#define PKT_LOG_VOICE //#define PKT_LOG_WHISPER SpeakingClient::SpeakingClient(sql::SqlManager *a, const std::shared_ptr &b) : ConnectedClient(a, b), whisper_handler_{this} { speak_begin = std::chrono::system_clock::now(); speak_last_packet = std::chrono::system_clock::now(); }; SpeakingClient::~SpeakingClient() { if(auto server{this->server}; this->rtc_client_id > 0 && server) { server->rtc_server().destroy_client(this->rtc_client_id); } } bool SpeakingClient::shouldReceiveVoice(const std::shared_ptr &sender) { //if(this->properties()[property::CLIENT_AWAY].as()) return false; if(!this->properties()[property::CLIENT_OUTPUT_HARDWARE].as()) return false; if(this->properties()[property::CLIENT_OUTPUT_MUTED].as()) return false; { shared_lock client_lock(this->channel_lock); for(const auto& entry : this->mutedClients) if(entry.lock() == sender) return false; } return true; } bool SpeakingClient::shouldReceiveVoiceWhisper(const std::shared_ptr &sender) { if(!this->shouldReceiveVoice(sender)) return false; return permission::v2::permission_granted(this->cpmerission_needed_whisper_power, sender->cpmerission_whisper_power, false); } bool SpeakingClient::should_handle_voice_packet(size_t) { auto current_channel = this->currentChannel; if(!current_channel) { return false; } if(!this->allowedToTalk) { return false; } this->updateSpeak(false, system_clock::now()); this->resetIdleTime(); return true; } inline bool update_whisper_error(std::chrono::system_clock::time_point& last) { auto now = std::chrono::system_clock::now(); if(last + std::chrono::milliseconds{500} < now) { last = now; return true; } return false; } auto regex_wildcard = std::regex(".*"); #define S(x) #x #define HWID_REGEX(name, pattern) \ auto regex_hwid_ ##name = []() noexcept { \ try { \ return std::regex(pattern); \ } catch (std::exception& ex) { \ logError(0, "Failed to parse regex for " S(name)); \ } \ return regex_wildcard; \ }(); HWID_REGEX(windows, "^[a-z0-9]{32},[a-z0-9]{32}$"); HWID_REGEX(unix, "^[a-z0-9]{32}$"); HWID_REGEX(android, "^[a-z0-9]{16}$"); HWID_REGEX(ios, "^[A-Z0-9]{8}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{12}$"); command_result SpeakingClient::handleCommandClientInit(Command& cmd) { TIMING_START(timings); { lock_guard lock(this->server->join_attempts_lock); auto client_address = this->getPeerIp(); auto& client_join_attempts = this->server->join_attempts[client_address]; auto& general_join_attempts = this->server->join_attempts["_"]; if(config::voice::clientConnectLimit > 0 && client_join_attempts + 1 > config::voice::clientConnectLimit) { return command_result{error::client_join_rate_limit_reached}; } if(config::voice::connectLimit > 0 && general_join_attempts + 1 > config::voice::connectLimit) { return command_result{error::server_join_rate_limit_reached}; } client_join_attempts++; general_join_attempts++; } TIMING_STEP(timings, "join atmp c"); if(!DatabaseHelper::assignDatabaseId(this->server->getSql(), this->server->getServerId(), this->ref())) { return command_result{error::vs_critical, "Could not assign database id!"}; } TIMING_STEP(timings, "db assign "); this->server->getGroupManager()->enableCache(this->getClientDatabaseId()); TIMING_STEP(timings, "gr cache "); const static vector available_parameters = { "client_nickname", "client_version", "client_platform", "client_input_muted", "client_input_hardware", "client_output_hardware", "client_output_muted", "client_default_channel", "client_default_channel_password", "client_server_password", "client_meta_data", "client_version_sign", "client_key_offset", "client_nickname_phonetic", "client_default_token", "client_badges=badges", "client_badges", "client_myteamspeak_id", "client_integrations", "client_active_integrations_info", "client_browser_engine", "client_away", "client_away_message", "hwid", "myTeamspeakId", "acTime", "userPubKey", "authSign", "pubSign", "pubSignCert" }; for(const auto& key : cmd[0].keys()) { if(key == "return_code") continue; bool parm_allowed = false; for(const auto& _allowed_key : available_parameters) { if(_allowed_key == key) { parm_allowed = true; break; } } if(!parm_allowed) { debugMessage(this->getServerId(), "{} Tried to insert a not allowed parameter within clientinit (Key: {}, Value: {})", CLIENT_STR_LOG_PREFIX, key,cmd[key].string()); continue; } if(key == "myTeamspeakId") { this->properties()[property::CLIENT_MYTEAMSPEAK_ID] = cmd[key].string(); continue; } else if(key == "acTime") continue; else if(key == "userPubKey") continue; else if(key == "authSign") continue; else if(key == "pubSign") continue; else if(key == "pubSignCert") continue; else if(key == "client_version" || key == "client_platform") { for(auto& character : cmd[key].string()) if(!isascii(character)) { logWarning(this->getServerId(), "{} Tried to join within an invalid supplied '{}' ({})", CLIENT_STR_LOG_PREFIX, key,cmd[key].string()); return command_result{error::client_hacked}; } } else if(key == "client_talk_request_msg") { if(cmd["client_talk_request_msg"].string().length() > ts::config::server::limits::talk_power_request_message_length) return command_result{error::parameter_invalid_size, "client_talk_request_msg"}; } else if(key == "client_away_message") { if(cmd["client_away_message"].string().length() > ts::config::server::limits::afk_message_length) return command_result{error::parameter_invalid_size, "client_away_message"}; } else if(key == "client_nickname_phonetic") { auto name = cmd["client_nickname_phonetic"].string(); if (count_characters(name) > 30) return command_result{error::parameter_invalid, "client_nickname_phonetic"}; } else if(key == "client_nickname") { auto name = cmd["client_nickname"].string(); if (count_characters(name) < 3) return command_result{error::parameter_invalid, "client_nickname"}; if (count_characters(name) > 30) return command_result{error::parameter_invalid, "client_nickname"}; } const auto &info = property::find(key); if(info.is_undefined()) { logError(this->getServerId(), "{} Tried to pass a unknown value {}. Please report this, if you're sure that this key should be known!", CLIENT_STR_LOG_PREFIX, key); continue; //return {findError("parameter_invalid"), "Unknown property " + key}; } if(!info.validate_input(cmd[key].as())) return command_result{error::parameter_invalid}; this->properties()[info] = cmd[key].as(); } debugMessage(this->getServerId(), "{} Got client init. (HWID: {})", CLIENT_STR_LOG_PREFIX, this->getHardwareId()); TIMING_STEP(timings, "props apply"); auto permissions_list = this->calculate_permissions({ permission::b_virtualserver_join_ignore_password, permission::b_client_ignore_bans, permission::b_client_ignore_vpn, permission::i_client_max_clones_uid, permission::i_client_max_clones_ip, permission::i_client_max_clones_hwid, permission::b_client_enforce_valid_hwid, permission::b_client_use_reserved_slot }, 0); auto permissions = map(permissions_list.begin(), permissions_list.end()); TIMING_STEP(timings, "perm calc 1"); if(geoloc::provider_vpn && !permission::v2::permission_granted(1, permissions[permission::b_client_ignore_vpn])) { auto provider = this->isAddressV4() ? geoloc::provider_vpn->resolveInfoV4(this->getPeerIp(), true) : geoloc::provider_vpn->resolveInfoV6(this->getPeerIp(), true); if(provider) return command_result{error::server_connect_banned, strvar::transform(ts::config::messages::kick_vpn, strvar::StringValue{"provider.name", provider->name}, strvar::StringValue{"provider.website", provider->side})}; } if(this->getType() == ClientType::CLIENT_TEAMSPEAK && permission::v2::permission_granted(1, permissions[permission::b_client_enforce_valid_hwid])) { auto hwid = this->properties()[property::CLIENT_HARDWARE_ID].as(); if( !std::regex_match(hwid, regex_hwid_windows) && !std::regex_match(hwid, regex_hwid_unix) && !std::regex_match(hwid, regex_hwid_android) && !std::regex_match(hwid, regex_hwid_ios) ) { return command_result{error::parameter_invalid, config::messages::kick_invalid_hardware_id}; } } TIMING_STEP(timings, "valid hw ip"); if(!permission::v2::permission_granted(1, permissions[permission::b_virtualserver_join_ignore_password])) if(!this->server->verifyServerPassword(cmd["client_server_password"].string(), true)) return command_result{error::server_invalid_password}; if(!config::server::clients::ignore_max_clone_permissions) { size_t clones_uid = 0; size_t clones_ip = 0; size_t clones_hwid = 0; auto _own_hwid = this->getHardwareId(); this->server->forEachClient([&](const shared_ptr& client) { if(client->getExternalType() != CLIENT_TEAMSPEAK) return; if(client->getUid() == this->getUid()) clones_uid++; if(client->getPeerIp() == this->getPeerIp()) clones_ip++; if(!_own_hwid.empty() && client->getHardwareId() == _own_hwid) clones_hwid++; }); if(clones_uid > 0 && permissions[permission::i_client_max_clones_uid].has_value && !permission::v2::permission_granted(clones_uid, permissions[permission::i_client_max_clones_uid])) { logMessage(this->getServerId(), "{} Disconnecting because there are already {} uid clones connected. (Allowed: {})", CLIENT_STR_LOG_PREFIX, clones_uid, permissions[permission::i_client_max_clones_uid].value); return command_result{error:: client_too_many_clones_connected, "too many clones connected (uid)"}; } if(clones_ip > 0 && permissions[permission::i_client_max_clones_ip].has_value && !permission::v2::permission_granted(clones_ip, permissions[permission::i_client_max_clones_ip])) { logMessage(this->getServerId(), "{} Disconnecting because there are already {} ip clones connected. (Allowed: {})", CLIENT_STR_LOG_PREFIX, clones_ip, permissions[permission::i_client_max_clones_ip].value); return command_result{error:: client_too_many_clones_connected, "too many clones connected (ip)"}; } if(clones_hwid > 0 && permissions[permission::i_client_max_clones_hwid].has_value && !permission::v2::permission_granted(clones_hwid, permissions[permission::i_client_max_clones_hwid])) { logMessage(this->getServerId(), "{} Disconnecting because there are already {} hwid clones connected. (Allowed: {})", CLIENT_STR_LOG_PREFIX, clones_hwid, permissions[permission::i_client_max_clones_hwid].value); return command_result{error:: client_too_many_clones_connected, "too many clones connected (hwid)"}; } TIMING_STEP(timings, "max clones "); } auto banEntry = this->resolveActiveBan(this->getPeerIp()); if(banEntry) { logMessage(this->getServerId(), "{} Disconnecting while init because of ban record. Record id {} at server {}", CLIENT_STR_LOG_PREFIX, banEntry->banId, banEntry->serverId); serverInstance->banManager()->trigger_ban(banEntry, this->getServerId(), this->getUid(), this->getHardwareId(), this->getDisplayName(), this->getPeerIp()); string fullReason = string() + "You are banned " + (banEntry->serverId == 0 ? "globally" : "from this server") + ". Reason: \"" + banEntry->reason + "\". Ban expires "; string time; if(banEntry->until.time_since_epoch().count() != 0){ time += "in "; auto seconds = chrono::ceil(banEntry->until - chrono::system_clock::now()).count(); tm p{}; memset(&p, 0, sizeof(p)); while(seconds >= 365 * 24 * 60 * 60){ p.tm_year++; seconds -= 365 * 24 * 60 * 60; } while(seconds >= 24 * 60 * 60){ p.tm_yday++; seconds -= 24 * 60 * 60; } while(seconds >= 60 * 60){ p.tm_hour++; seconds -= 60 * 60; } while(seconds >= 60){ p.tm_min++; seconds -= 60; } p.tm_sec = (int) seconds; if(p.tm_year > 0) time += to_string(p.tm_year) + " years, "; if(p.tm_yday > 0) time += to_string(p.tm_yday) + " days, "; if(p.tm_hour > 0) time += to_string(p.tm_hour) + " hours, "; if(p.tm_min > 0) time += to_string(p.tm_min) + " minutes, "; if(p.tm_sec > 0) time += to_string(p.tm_sec) + " seconds, "; if(time.empty()) time = "now, "; time = time.substr(0, time.length() - 2); } else time = "never"; fullReason += time + "!"; return command_result{error::server_connect_banned, fullReason}; } TIMING_STEP(timings, "ban resolve"); size_t count = 0; { for(const auto &cl : this->server->getClients()) if((cl->getType() == CLIENT_TEAMSPEAK || cl->getType() == CLIENT_WEB || cl->getType() == CLIENT_TEASPEAK || cl->getType() == CLIENT_MUSIC)) if(cl->connectionState() <= ConnectionState::CONNECTED && cl->connectionState() >= ConnectionState::INIT_HIGH) count++; } auto maxClients = this->server->properties()[property::VIRTUALSERVER_MAXCLIENTS].as(); auto reserved = this->server->properties()[property::VIRTUALSERVER_RESERVED_SLOTS].as(); bool allowReserved = permission::v2::permission_granted(1, permissions[permission::b_client_use_reserved_slot]); if(reserved > maxClients){ if(!allowReserved) return command_result{error::server_maxclients_reached}; } else if(maxClients - (allowReserved ? 0 : reserved) <= count) return command_result{error::server_maxclients_reached}; TIMING_STEP(timings, "max clients"); auto old_last_connected = this->properties()[property::CLIENT_LASTCONNECTED].as(); this->properties()[property::CONNECTION_CLIENT_IP] = this->getLoggingPeerIp(); this->properties()[property::CLIENT_LASTCONNECTED] = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); this->properties()[property::CLIENT_TOTALCONNECTIONS]++; { auto time_point = system_clock::time_point() + seconds(old_last_connected); if(time_point < build::version()->timestamp) { logMessage(this->getServerId(), "{} Client may cached a old permission list (Server is newer than the client's last join)", CLIENT_STR_LOG_PREFIX); TIMING_STEP(timings, "pre dummy p"); Command _dummy("dummy_permissionslist"); this->handleCommandPermissionList(_dummy); TIMING_STEP(timings, "pst dummy p"); } } this->postCommandHandler.emplace_back([&](){ auto self = dynamic_pointer_cast(this->ref()); std::thread([self](){ if(self->state != ConnectionState::INIT_HIGH) return; try { self->processJoin(); } catch (std::exception& ex) { logError(self->getServerId(), "Failed to proceed client join for {}. Got exception with message {}", CLIENT_STR_LOG_PREFIX_(self), ex.what()); self->close_connection(chrono::system_clock::now() + chrono::seconds{5}); } }).detach(); }); debugMessage(this->getServerId(), "{} Client init timings: {}", CLIENT_STR_LOG_PREFIX, TIMING_FINISH(timings)); return command_result{error::ok}; } /* must be triggered while helding an execute lock */ //Note: Client permissions are may not really void SpeakingClient::processJoin() { TIMING_START(timings); auto ref_server = this->server; this->resetIdleTime(); threads::MutexLock lock(this->command_lock); //Don't process any commands! if(this->state != ConnectionState::INIT_HIGH) { logError(this->getServerId(), "{} Invalid processJoin() connection state!", CLIENT_STR_LOG_PREFIX); return; } TIMING_STEP(timings, "setup "); ref_server->registerClient(this->ref()); { if(this->rtc_client_id) { /* in case of client reconnect */ this->server->rtc_server().destroy_client(this->rtc_client_id); } std::string error{}; this->rtc_client_id = this->server->rtc_server().create_client(dynamic_pointer_cast(this->ref())); if(auto voice_client{dynamic_cast(this)}; voice_client) { if(!this->server->rtc_server().initialize_native_connection(error, this->rtc_client_id)) { logCritical(this->getServerId(), "{} Native connection setup failed: {}", CLIENT_STR_LOG_PREFIX, error); } } if(this->getType() == ClientType::CLIENT_WEB || this->getType() == ClientType::CLIENT_TEASPEAK) { if(!this->server->rtc_server().initialize_rtc_connection(error, this->rtc_client_id)) { logCritical(this->getServerId(), "{} RTC connection setup failed: {}", CLIENT_STR_LOG_PREFIX, error); } else { this->rtc_session_pending_describe = true; } } } TIMING_STEP(timings, "server reg "); ref_server->getGroupManager()->cleanupAssignments(this->getClientDatabaseId()); TIMING_STEP(timings, "grp cleanup"); ref_server->getGroupManager()->update_server_group_property(this->ref(), true, nullptr); TIMING_STEP(timings, "grp apply "); this->properties()[property::CLIENT_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) { debugMessage(this->getServerId(), "Client " + this->getDisplayName() + "|" + this->getLoggingPeerIp() + " comes from " + loc->name + "|" + loc->identifier); this->properties()[property::CLIENT_COUNTRY] = loc->identifier; } else { logError(ref_server ? ref_server->getServerId() : 0, "Could not resolve country for ip " + this->getLoggingPeerIp() + "|" + this->getDisplayName()); } } //this->updateChannelClientProperties(false); /* will be already updated via assignChannel */ if(ref_server->properties()[property::VIRTUALSERVER_HOSTMESSAGE_MODE].as() == 3 && !ref_server->properties()[property::VIRTUALSERVER_HOSTMESSAGE].as().empty()) { auto weak = this->_this; threads::Thread([weak](){ threads::self::sleep_for(milliseconds(2000)); auto client = weak.lock(); if(!client || !client->server) return; client->disconnect(client->server->properties()[property::VIRTUALSERVER_HOSTMESSAGE].as()); }).detach(); } TIMING_STEP(timings, "ip 2 loc as"); //IP to location assignment this->sendServerInit(); debugMessage(this->getServerId(), "Client id: " + to_string(this->getClientId())); TIMING_STEP(timings, "notify sini"); if(!ref_server->assignDefaultChannel(this->ref(), false)) { auto result = command_result{error::vs_critical, "Could not assign default channel!"}; this->notifyError(result); result.release_data(); this->close_connection(system_clock::now() + seconds(1)); return; } TIMING_STEP(timings, "assign chan"); this->sendChannelList(true); this->state = ConnectionState::CONNECTED; TIMING_STEP(timings, "send chan t"); /* trick the join method */ auto channel = this->currentChannel; this->currentChannel = nullptr; { /* enforce an update of these properties */ this->properties()[property::CLIENT_CHANNEL_GROUP_INHERITED_CHANNEL_ID] = "0"; this->properties()[property::CLIENT_CHANNEL_GROUP_ID] = "0"; this->properties()[property::CLIENT_TALK_POWER] = "0"; unique_lock server_channel_lock(this->server->channel_tree_lock); this->server->client_move(this->ref(), channel, nullptr, "", ViewReasonId::VREASON_USER_ACTION, false, server_channel_lock); if(this->getType() != ClientType::CLIENT_TEAMSPEAK) this->subscribeChannel({this->currentChannel}, false, true); /* su "improve" the TS3 clients join speed we send the channel clients a bit later, when the TS3 client gets his own client variables */ } TIMING_STEP(timings, "join move "); this->properties()->triggerAllModified(); this->notifyServerGroupList(); this->notifyChannelGroupList(); TIMING_STEP(timings, "notify grou"); logMessage(this->getServerId(), "Voice client {}/{} ({}) from {} joined.", this->getClientDatabaseId(), this->getUid(), this->getDisplayName(), this->getLoggingPeerIp() + ":" + to_string(this->getPeerPort()) ); this->connectTimestamp = chrono::system_clock::now(); this->idleTimestamp = chrono::system_clock::now(); TIMING_STEP(timings, "welcome msg"); { std::string message{}; config::server::clients::WelcomeMessageType type{config::server::clients::WELCOME_MESSAGE_TYPE_NONE}; if(this->getType() == ClientType::CLIENT_TEASPEAK) { message = config::server::clients::extra_welcome_message_teaspeak; type = config::server::clients::extra_welcome_message_type_teaspeak; } else if(this->getType() == ClientType::CLIENT_TEAMSPEAK) { message = config::server::clients::extra_welcome_message_teamspeak; type = config::server::clients::extra_welcome_message_type_teamspeak; } else if(this->getType() == ClientType::CLIENT_WEB) { message = config::server::clients::extra_welcome_message_teaweb; type = config::server::clients::extra_welcome_message_type_teaweb; } if(type == config::server::clients::WELCOME_MESSAGE_TYPE_POKE) { this->notifyClientPoke(this->server->serverRoot, message); } else if(type == config::server::clients::WELCOME_MESSAGE_TYPE_CHAT) { this->notifyTextMessage(ChatMessageMode::TEXTMODE_SERVER, this->server->serverRoot, 0, 0, std::chrono::system_clock::now(), message); } } debugMessage(this->getServerId(), "{} Client join timings: {}", CLIENT_STR_LOG_PREFIX, TIMING_FINISH(timings)); serverInstance->action_logger()->client_channel_logger.log_client_join(this->getServerId(), this->ref(), this->getChannelId(), this->currentChannel->name()); } void SpeakingClient::processLeave() { auto ownLock = this->ref(); auto server = this->getServer(); auto channel = this->currentChannel; if(server){ logMessage(this->getServerId(), "Voice client {}/{} ({}) from {} left.", this->getClientDatabaseId(), this->getUid(), this->getDisplayName(), this->getLoggingPeerIp() + ":" + to_string(this->getPeerPort())); { unique_lock server_channel_lock(this->server->channel_tree_lock); server->unregisterClient(ownLock, "disconnected", server_channel_lock); /* already moves client to void if needed */ } server->groups->disableCache(ownLock->getClientDatabaseId()); server->music_manager_->cleanup_client_bots(this->getClientDatabaseId()); //ref_server = nullptr; Removed caused nullptr exceptions } } void SpeakingClient::triggerVoiceEnd() { this->properties()[property::CLIENT_FLAG_TALKING] = false; } void SpeakingClient::updateSpeak(bool only_update, const std::chrono::system_clock::time_point &now) { std::lock_guard speak_lock{this->speak_mutex}; if(this->speak_last_packet + this->speak_accuracy < now) { if(this->speak_last_packet > this->speak_begin) { if(!this->properties()[property::CLIENT_FLAG_TALKING].as()) { this->properties()[property::CLIENT_FLAG_TALKING] = true; } this->speak_time += duration_cast(this->speak_last_packet - this->speak_begin); } else { if(this->properties()[property::CLIENT_FLAG_TALKING].as()) { this->properties()[property::CLIENT_FLAG_TALKING] = false; } } this->speak_begin = now; this->speak_last_packet = now; } if(!only_update) { this->speak_last_packet = now; } } void SpeakingClient::tick_server(const std::chrono::system_clock::time_point &time) { ConnectedClient::tick_server(time); ALARM_TIMER(A1, "SpeakingClient::tick", milliseconds(2)); this->updateSpeak(true, time); if(this->state == ConnectionState::CONNECTED) { if(this->max_idle_time.has_value) { auto max_idle = this->max_idle_time.value; if(max_idle > 0 && this->idleTimestamp.time_since_epoch().count() > 0 && duration_cast(time - this->idleTimestamp).count() > max_idle) { this->server->notify_client_kick(this->ref(), this->server->getServerRoot(), ts::config::messages::idle_time_exceeded, nullptr); this->close_connection(system_clock::now() + seconds(1)); } } } } void SpeakingClient::updateChannelClientProperties(bool channel_lock, bool notify) { ConnectedClient::updateChannelClientProperties(channel_lock, notify); this->max_idle_time = this->calculate_permission(permission::i_client_max_idletime, this->currentChannel ? this->currentChannel->channelId() : 0); } command_result SpeakingClient::handleCommand(Command &command) { if(this->connectionState() == ConnectionState::INIT_HIGH) { if(this->handshake.state == HandshakeState::BEGIN || this->handshake.state == HandshakeState::IDENTITY_PROOF) { command_result result; if(command.command() == "handshakebegin") { result.reset(this->handleCommandHandshakeBegin(command)); } else if(command.command() == "handshakeindentityproof") { result.reset(this->handleCommandHandshakeIdentityProof(command)); } else { result.reset(command_result{error::client_not_logged_in}); } if(result.has_error()) { this->postCommandHandler.push_back([&]{ this->close_connection(system_clock::now() + seconds(1)); }); } return result; } } else if(this->connectionState() == ConnectionState::CONNECTED) { if(command.command() == "rtcsessiondescribe") { return this->handleCommandRtcSessionDescribe(command); } else if(command.command() == "rtcicecandidate") { return this->handleCommandRtcIceCandidate(command); } else if(command.command() == "rtcbroadcast") { /* TODO: Remove this command once the first 1.5.0 stable is out */ return this->handleCommandRtcBroadcast(command); } else if(command.command() == "rtcsessionreset") { return this->handleCommandRtcSessionReset(command); } else if(command.command() == "broadcastaudio") { return this->handleCommandBroadcastAudio(command); } else if(command.command() == "broadcastvideo") { return this->handleCommandBroadcastVideo(command); } else if(command.command() == "broadcastvideojoin") { return this->handleCommandBroadcastVideoJoin(command); } else if(command.command() == "broadcastvideoleave") { return this->handleCommandBroadcastVideoLeave(command); } else if(command.command() == "broadcastvideoconfig") { return this->handleCommandBroadcastVideoConfig(command); } else if(command.command() == "broadcastvideoconfigure") { return this->handleCommandBroadcastVideoConfigure(command); } } return ConnectedClient::handleCommand(command); } command_result SpeakingClient::handleCommandRtcSessionDescribe(Command &command) { CMD_REQ_SERVER; if(this->rtc_session_pending_describe) { this->rtc_session_pending_describe = false; } else { CMD_CHK_AND_INC_FLOOD_POINTS(15); } uint32_t mode; if(command["mode"].string() == "offer") { mode = 1; } else if(command["mode"].string() == "answer") { mode = 2; } else { return command_result{error::parameter_invalid, "mode"}; } std::string error{}; if(!this->server->rtc_server().apply_remote_description(error, this->rtc_client_id, mode, command["sdp"])) { return command_result{error::vs_critical, error}; } if(mode == 1) { std::string result{}; if(!this->server->rtc_server().generate_local_description(this->rtc_client_id, result)) { return command_result{error::vs_critical, result}; } else { ts::command_builder notify{"notifyrtcsessiondescription"}; notify.put_unchecked(0, "mode", "answer"); notify.put_unchecked(0, "sdp", result); this->sendCommand(notify); } } return command_result{error::ok}; } command_result SpeakingClient::handleCommandRtcSessionReset(Command &command) { CMD_REQ_SERVER; CMD_CHK_AND_INC_FLOOD_POINTS(15); this->server->rtc_server().reset_rtp_session(this->rtc_client_id); if(this->getType() == ClientType::CLIENT_TEASPEAK) { /* registering the broadcast again since rtp session reset resets the broadcasts as well */ this->server->rtc_server().start_broadcast_audio(this->rtc_client_id, 1); } return command_result{error::ok}; } command_result SpeakingClient::handleCommandRtcIceCandidate(Command &command) { CMD_REQ_SERVER; std::string error; if(command[0].has("candidate")) { auto candidate = command["candidate"].string(); if(!this->server->rtc_server().add_ice_candidate(error, this->rtc_client_id, command["media_line"], candidate)) { return command_result{error::vs_critical, error}; } } else { this->server->rtc_server().ice_candidates_finished(this->rtc_client_id); } return command_result{error::ok}; } ts::command_result parse_broadcast_options(ParameterBulk &cmd, VideoBroadcastOptions& options, bool requires_all) { if(cmd.has("broadcast_bitrate_max")) { options.update_mask |= VideoBroadcastOptions::kOptionBitrate; options.bitrate = cmd["broadcast_bitrate_max"]; } else if(requires_all) { return ts::command_result{error::parameter_missing, "broadcast_bitrate_max"}; } if(cmd.has("broadcast_keyframe_interval")) { options.update_mask |= VideoBroadcastOptions::kOptionKeyframeInterval; options.keyframe_interval = cmd["broadcast_keyframe_interval"]; } else if(requires_all) { return ts::command_result{error::parameter_missing, "broadcast_keyframe_interval"}; } return ts::command_result{error::ok}; } void simplify_broadcast_options(const VideoBroadcastOptions& current_options, VideoBroadcastOptions& target_options) { if(target_options.bitrate == current_options.bitrate) { target_options.update_mask &= ~VideoBroadcastOptions::kOptionBitrate; } if(target_options.keyframe_interval == current_options.keyframe_interval) { target_options.update_mask &= ~VideoBroadcastOptions::kOptionKeyframeInterval; } } /** * Test if the client has permissions to use the target broadcast options * @param client * @param channel_id * @param options * @return */ ts::command_result test_broadcast_options(SpeakingClient& client, ChannelId channel_id, const VideoBroadcastOptions& options) { if(options.update_mask & VideoBroadcastOptions::kOptionBitrate) { auto required_value = options.bitrate == 0 ? -1 : (permission::PermissionValue) (options.bitrate / 1000); if(!permission::v2::permission_granted(required_value, client.calculate_permission(permission::i_video_max_kbps, channel_id))) { return ts::command_result{permission::i_video_max_kbps}; } } return ts::command_result{error::ok}; } inline command_result broadcast_start_result_to_command_result(rtc::BroadcastStartResult broadcast_result) { switch(broadcast_result) { case rtc::BroadcastStartResult::Success: return ts::command_result{error::ok}; case rtc::BroadcastStartResult::InvalidBroadcastType: return ts::command_result{error::parameter_invalid, "type"}; case rtc::BroadcastStartResult::InvalidStreamId: return ts::command_result{error::rtc_missing_target_channel}; case rtc::BroadcastStartResult::ClientHasNoChannel: return ts::command_result{error::vs_critical, "no channel"}; case rtc::BroadcastStartResult::InvalidClient: return ts::command_result{error::vs_critical, "invalid client"}; case rtc::BroadcastStartResult::UnknownError: default: return ts::command_result{error::vs_critical, "unknown error"}; } } command_result SpeakingClient::handleCommandBroadcastAudio(Command &command) { CMD_REQ_SERVER; CMD_CHK_AND_INC_FLOOD_POINTS(5); auto ssrc = command[0].has("ssrc") ? command["ssrc"].as() : (uint32_t) 0; auto broadcast_result = this->server->rtc_server().start_broadcast_audio(this->rtc_client_id, ssrc); return broadcast_start_result_to_command_result(broadcast_result); } command_result SpeakingClient::handleCommandBroadcastVideo(Command &command) { CMD_REQ_SERVER; CMD_CHK_AND_INC_FLOOD_POINTS(15); auto ssrc = command[0].has("ssrc") ? command["ssrc"].as() : (uint32_t) 0; auto type = (rtc::VideoBroadcastType) command["type"].as(); switch(type) { case rtc::VideoBroadcastType::Screen: if(!permission::v2::permission_granted(1, this->calculate_permission(permission::b_video_screen, this->getChannelId()), false)) { return ts::command_result{permission::b_video_screen}; } break; case rtc::VideoBroadcastType::Camera: if(!permission::v2::permission_granted(1, this->calculate_permission(permission::b_video_camera, this->getChannelId()), false)) { return ts::command_result{permission::b_video_camera}; } break; default: return ts::command_result{error::parameter_invalid, "type"}; } VideoBroadcastOptions options; memset(&options, 0, sizeof(options)); if(ssrc != 0) { ts::command_result result; result.reset(parse_broadcast_options(command[0], options, true)); if(result.has_error()) { return result; } result.reset(test_broadcast_options(*this, this->getChannelId(), options)); if(result.has_error()) { return result; } result.release_data(); } auto broadcast_result = this->server->rtc_server().start_broadcast_video(this->rtc_client_id, type, ssrc, &options); return broadcast_start_result_to_command_result(broadcast_result); } command_result SpeakingClient::handleCommandRtcBroadcast(Command &command) { CMD_REQ_SERVER; CMD_CHK_AND_INC_FLOOD_POINTS(15); std::vector> broadcasts{}; broadcasts.reserve(command.bulkCount()); ts::command_result_bulk result{}; for(size_t index{0}; index < command.bulkCount(); index++) { auto& bulk = command[index]; auto ssrc = bulk.has("ssrc") ? bulk["ssrc"].as() : (uint32_t) 0; auto type = bulk["type"].as(); for(const auto& entry : broadcasts) { if(std::get<0>(entry) == type) { return ts::command_result{error::parameter_constraint_violation}; } } broadcasts.push_back(std::make_tuple(type, ssrc)); switch(type) { case 1: { ts::Command cmd{""}; cmd["ssrc"] = ssrc; result.insert_result(this->handleCommandBroadcastAudio(cmd)); break; } case 2: { ts::Command cmd{""}; cmd["broadcast_bitrate_max"] = 1500000; cmd["broadcast_keyframe_interval"] = 0; cmd["ssrc"] = ssrc; cmd["type"] = (uint8_t) rtc::VideoBroadcastType::Camera; result.insert_result(this->handleCommandBroadcastVideo(cmd)); break; } case 3: { ts::Command cmd{""}; cmd["broadcast_bitrate_max"] = 1500000; cmd["broadcast_keyframe_interval"] = 0; cmd["ssrc"] = ssrc; cmd["type"] = (uint8_t) rtc::VideoBroadcastType::Screen; result.insert_result(this->handleCommandBroadcastVideo(cmd)); break; } default: result.emplace_result(error::parameter_invalid, "type"); break; } } return ts::command_result{std::move(result)}; } command_result SpeakingClient::handleCommandBroadcastVideoJoin(Command &cmd) { CMD_REQ_SERVER; auto broadcast_type = (rtc::VideoBroadcastType) cmd["bt"].as(); auto broadcast_id = cmd["bid"].as(); if(broadcast_id != this->rtc_client_id) { CMD_CHK_AND_INC_FLOOD_POINTS(25); /* the broadcast is is actually the rtc client id of the broadcaster */ uint32_t camera_streams, screen_streams; if(!this->server->rtc_server().client_video_stream_count(this->rtc_client_id, &camera_streams, &screen_streams)) { return ts::command_result{error::vs_critical, "failed to count client streams"}; } auto permission_max_streams = this->calculate_permission(permission::i_video_max_streams, this->getChannelId()); if(permission_max_streams.has_value) { if(!permission::v2::permission_granted(camera_streams + screen_streams, permission_max_streams, false)) { return ts::command_result{permission::i_video_max_streams}; } } switch(broadcast_type) { case rtc::VideoBroadcastType::Camera: { const auto permission_max_camera_streams = this->calculate_permission(permission::i_video_max_camera_streams, this->getChannelId()); if(permission_max_camera_streams.has_value) { if(!permission::v2::permission_granted(camera_streams, permission_max_camera_streams, false)) { return ts::command_result{permission::i_video_max_camera_streams}; } } break; } case rtc::VideoBroadcastType::Screen: { const auto permission_max_screen_streams = this->calculate_permission(permission::i_video_max_camera_streams, this->getChannelId()); if(permission_max_screen_streams.has_value) { if(!permission::v2::permission_granted(screen_streams, permission_max_screen_streams, false)) { return ts::command_result{permission::i_video_max_screen_streams}; } } break; } default: return ts::command_result{error::broadcast_invalid_type}; } } else { CMD_CHK_AND_INC_FLOOD_POINTS(5); /* The client is free to join his own broadcast */ } using VideoBroadcastJoinResult = rtc::VideoBroadcastJoinResult; switch(this->server->rtc_server().join_video_broadcast(this->rtc_client_id, broadcast_id, broadcast_type)) { case VideoBroadcastJoinResult::Success: return ts::command_result{error::ok}; case VideoBroadcastJoinResult::InvalidBroadcast: return ts::command_result{error::broadcast_invalid_id}; case VideoBroadcastJoinResult::InvalidBroadcastType: return ts::command_result{error::broadcast_invalid_type}; case VideoBroadcastJoinResult::InvalidClient: return ts::command_result{error::client_invalid_id}; case VideoBroadcastJoinResult::UnknownError: default: return ts::command_result{error::vs_critical}; } } command_result SpeakingClient::handleCommandBroadcastVideoLeave(Command &cmd) { CMD_REQ_SERVER; auto broadcast_type = (rtc::VideoBroadcastType) cmd["bt"].as(); auto broadcast_id = cmd["bid"].as(); this->server->rtc_server().leave_video_broadcast(this->rtc_client_id, broadcast_id, broadcast_type); return ts::command_result{error::ok}; } command_result SpeakingClient::handleCommandBroadcastVideoConfig(Command &cmd) { CMD_REQ_SERVER; auto broadcast_type = (rtc::VideoBroadcastType) cmd["bt"].as(); VideoBroadcastOptions options; auto result = this->server->rtc_server().client_broadcast_video_config(this->rtc_client_id, broadcast_type, &options); switch(result) { case rtc::VideoBroadcastConfigureResult::Success: break; case rtc::VideoBroadcastConfigureResult::InvalidBroadcast: return ts::command_result{error::broadcast_invalid_id}; case rtc::VideoBroadcastConfigureResult::InvalidBroadcastType: return ts::command_result{error::broadcast_invalid_type}; case rtc::VideoBroadcastConfigureResult::InvalidClient: return ts::command_result{error::client_invalid_id}; case rtc::VideoBroadcastConfigureResult::UnknownError: default: return ts::command_result{error::vs_critical}; } ts::command_builder notify{this->notify_response_command("notifybroadcastvideoconfig")}; notify.put_unchecked(0, "bt", (uint8_t) broadcast_type); notify.put_unchecked(0, "broadcast_keyframe_interval", options.keyframe_interval); notify.put_unchecked(0, "broadcast_bitrate_max", options.bitrate); this->sendCommand(notify); return ts::command_result{error::ok}; } inline command_result broadcast_config_result_to_command_result(rtc::VideoBroadcastConfigureResult result) { switch(result) { case rtc::VideoBroadcastConfigureResult::Success: return ts::command_result{error::ok}; case rtc::VideoBroadcastConfigureResult::InvalidBroadcast: return ts::command_result{error::broadcast_invalid_id}; case rtc::VideoBroadcastConfigureResult::InvalidBroadcastType: return ts::command_result{error::broadcast_invalid_type}; case rtc::VideoBroadcastConfigureResult::InvalidClient: return ts::command_result{error::client_invalid_id}; case rtc::VideoBroadcastConfigureResult::UnknownError: default: return ts::command_result{error::vs_critical}; } } command_result SpeakingClient::handleCommandBroadcastVideoConfigure(Command &cmd) { CMD_REQ_SERVER; auto broadcast_type = (rtc::VideoBroadcastType) cmd["bt"].as(); VideoBroadcastOptions current_options; auto query_result = this->server->rtc_server().client_broadcast_video_config(this->rtc_client_id, broadcast_type, ¤t_options); if(query_result != rtc::VideoBroadcastConfigureResult::Success) { return broadcast_config_result_to_command_result(query_result); } VideoBroadcastOptions options; memset(&options, 0, sizeof(options)); { ts::command_result result; result.reset(parse_broadcast_options(cmd[0], options, false)); if(result.has_error()) { return result; } simplify_broadcast_options(current_options, options); result.reset(test_broadcast_options(*this, this->getChannelId(), options)); if(result.has_error()) { return result; } result.release_data(); } { auto result = test_broadcast_options(*this, this->getChannelId(), options); if(result.has_error()) { return result; } result.release_data(); } auto result = this->server->rtc_server().client_broadcast_video_configure(this->rtc_client_id, broadcast_type, &options); return broadcast_config_result_to_command_result(result); }