#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 supported here! Host delimiter is \",\""); } } { 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 supported here! Host delimiter is \",\""); } } } { 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(); }