951 lines
39 KiB
C++
951 lines
39 KiB
C++
#define XMALLOC undefined_malloc /* fix jemalloc and tomcrypt */
|
|
#define XCALLOC undefined_calloc
|
|
#define XFREE undefined_free
|
|
#define XREALLOC undefined_realloc
|
|
|
|
#include <log/LogUtils.h>
|
|
#include "InstanceHandler.h"
|
|
#include "src/client/InternalClient.h"
|
|
#include "src/server/QueryServer.h"
|
|
#include "src/manager/PermissionNameMapper.h"
|
|
#include "./FileServerHandler.h"
|
|
#include <ThreadPool/Timer.h>
|
|
#include "ShutdownHelper.h"
|
|
#include <sys/utsname.h>
|
|
#include <misc/digest.h>
|
|
#include <misc/base64.h>
|
|
#include <misc/hex.h>
|
|
#include <misc/rnd.h>
|
|
#include <protocol/buffers.h>
|
|
#include "./groups/GroupManager.h"
|
|
|
|
#ifndef _POSIX_SOURCE
|
|
#define _POSIX_SOURCE
|
|
#endif
|
|
#include <unistd.h>
|
|
#include "./manager/ActionLogger.h"
|
|
#include "./client/shared/ServerCommandExecutor.h"
|
|
|
|
#undef _POSIX_SOURCE
|
|
|
|
using namespace std;
|
|
using namespace std::chrono;
|
|
using namespace ts;
|
|
using namespace ts::server;
|
|
|
|
extern bool mainThreadActive;
|
|
InstanceHandler::InstanceHandler(SqlDataManager *sql) : sql(sql), permission_helper_{this} {
|
|
serverInstance = this;
|
|
|
|
this->general_task_executor_ = std::make_shared<task_executor>(ts::config::threads::ticking, "instance tick ");
|
|
this->general_task_executor_->set_exception_handler([](const std::string& task_name, const std::exception_ptr& exception) {
|
|
std::string message{};
|
|
try {
|
|
std::rethrow_exception(exception);
|
|
} catch (const std::exception& ex) {
|
|
message = "std::exception::what() -> " + std::string{ex.what()};
|
|
} catch(...) {
|
|
message = "unknown exception";
|
|
}
|
|
|
|
logCritical(LOG_INSTANCE, "Instance task executor received exception: {}", message);
|
|
});
|
|
|
|
this->statistics = make_shared<stats::ConnectionStatistics>(nullptr);
|
|
|
|
std::string error_message{};
|
|
this->license_service_ = std::make_shared<license::LicenseService>();
|
|
if(!this->license_service_->initialize(error_message)) {
|
|
logCritical(LOG_INSTANCE, strobf("Failed to the license service: {}").string(), error_message);
|
|
}
|
|
this->dbHelper = new DatabaseHelper(this->getSql());
|
|
|
|
this->action_logger_ = std::make_unique<log::ActionLogger>();
|
|
if(!this->action_logger_->initialize(error_message)) {
|
|
logCritical(LOG_INSTANCE, "Failed to initialize instance action logs: {}", error_message);
|
|
logCritical(LOG_INSTANCE, "Action log has been disabled.");
|
|
this->action_logger_->finalize();
|
|
}
|
|
|
|
this->_properties = std::make_shared<PropertyManager>();
|
|
this->_properties->register_property_type<property::InstanceProperties>();
|
|
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::find<property::InstanceProperties>(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->get_database_version();
|
|
this->properties()[property::SERVERINSTANCE_PERMISSIONS_VERSION] = this->sql->get_permissions_version();
|
|
|
|
|
|
this->globalServerAdmin = std::make_shared<ts::server::InternalClient>(this->getSql(), nullptr, "serveradmin", true);
|
|
this->globalServerAdmin->initialize_weak_reference(this->globalServerAdmin);
|
|
ts::server::DatabaseHelper::assignDatabaseId(this->getSql(), 0, globalServerAdmin);
|
|
|
|
this->_musicRoot = std::make_shared<InternalClient>(this->getSql(), nullptr, "Music Manager", false);
|
|
dynamic_pointer_cast<InternalClient>(this->_musicRoot)->initialize_weak_reference(this->_musicRoot);
|
|
|
|
{
|
|
using GroupLoadResult = groups::GroupLoadResult;
|
|
|
|
this->group_manager_ = std::make_shared<groups::GroupManager>(this->getSql(), 0, nullptr);
|
|
if(!this->group_manager_->initialize(this->group_manager_, error_message)) {
|
|
logCritical(LOG_INSTANCE, "Failed to initialize instance group manager: {}", error_message);
|
|
mainThreadActive = false;
|
|
return;
|
|
}
|
|
|
|
bool initialize_groups{false};
|
|
switch(this->group_manager_->server_groups()->load_data(true)) {
|
|
case GroupLoadResult::SUCCESS:
|
|
break;
|
|
case GroupLoadResult::NO_GROUPS:
|
|
initialize_groups = true;
|
|
break;
|
|
|
|
case GroupLoadResult::DATABASE_ERROR:
|
|
logCritical(LOG_INSTANCE, "Failed to load instance server groups (Database error)");
|
|
mainThreadActive = false;
|
|
return;
|
|
}
|
|
|
|
switch(this->group_manager_->channel_groups()->load_data(true)) {
|
|
case GroupLoadResult::SUCCESS:
|
|
break;
|
|
case GroupLoadResult::NO_GROUPS:
|
|
initialize_groups = true;
|
|
break;
|
|
|
|
case GroupLoadResult::DATABASE_ERROR:
|
|
logCritical(LOG_INSTANCE, "Failed to load instance channel groups (Database error)");
|
|
mainThreadActive = false;
|
|
return;
|
|
}
|
|
|
|
if(!this->group_manager_->assignments().load_data(error_message)) {
|
|
logCritical(LOG_INSTANCE, "Failed to load instance group assignments: {}", error_message);
|
|
mainThreadActive = false;
|
|
return;
|
|
}
|
|
|
|
if (initialize_groups) {
|
|
if(!this->setupDefaultGroups()){
|
|
logCritical(LOG_INSTANCE, "Could not setup server instance! Stopping...");
|
|
mainThreadActive = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
this->validate_default_groups();
|
|
}
|
|
|
|
{
|
|
this->default_tree = make_shared<ServerChannelTree>(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<BasicChannel> 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->channel_count() == 4) {
|
|
auto default_channel = this->default_tree->findChannel("[cspacer04]Default Channel", nullptr);
|
|
if(default_channel) {
|
|
auto ch = this->default_tree->createChannel(0, default_channel->channelId(), "[cspacer05]Administrator Room");
|
|
ch->permissions()->set_permission(permission::i_channel_needed_view_power, {75, 0}, permission::v2::set_value, permission::v2::do_nothing, false, false);
|
|
this->save_channel_permissions();
|
|
}
|
|
}
|
|
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_or<int64_t>(0) == 0) {
|
|
debugMessage(LOG_INSTANCE, "Setting up monthly reset timestamp!");
|
|
this->properties()[property::SERVERINSTANCE_MONTHLY_TIMESTAMP] = duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
|
|
}
|
|
|
|
this->banMgr = new BanManager(this->getSql());
|
|
this->banMgr->loadBans();
|
|
}
|
|
|
|
InstanceHandler::~InstanceHandler() {
|
|
delete this->banMgr;
|
|
delete this->dbHelper;
|
|
|
|
group_manager_ = nullptr;
|
|
globalServerAdmin = nullptr;
|
|
_musicRoot = nullptr;
|
|
|
|
statistics = nullptr;
|
|
}
|
|
|
|
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<string> split_hosts(const std::string& message, char delimiter) {
|
|
vector<string> 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 InstanceHandler::startInstance() {
|
|
if (this->active) {
|
|
return false;
|
|
}
|
|
|
|
active = true;
|
|
string errorMessage;
|
|
|
|
this->server_command_executor_ = std::make_shared<ServerCommandExecutor>(ts::config::threads::command_execute);
|
|
|
|
this->permission_mapper = make_shared<permission::PermissionNameMapper>();
|
|
if(!this->permission_mapper->initialize(config::permission_mapping_file, errorMessage)) {
|
|
logCritical(LOG_INSTANCE, "Failed to initialize permission name mapping from file {}: {}", config::permission_mapping_file, errorMessage);
|
|
return false;
|
|
}
|
|
this->sslMgr = new ssl::SSLManager();
|
|
if(!this->sslMgr->initialize()) {
|
|
logCritical(LOG_GENERAL, "Failed to initialize ssl manager.");
|
|
return false;
|
|
}
|
|
|
|
this->conversation_io = make_shared<event::EventExecutor>("conv io #");
|
|
if(!this->conversation_io->initialize(1)) { //TODO: Make the conversation IO loop thread size configurable
|
|
logCritical(LOG_GENERAL, "Failed to initialize conversation io write loop");
|
|
return false;
|
|
}
|
|
|
|
{
|
|
vector<string> errors;
|
|
if(!this->reloadConfig(errors, false)) {
|
|
logCritical(LOG_GENERAL, "Failed to initialize config:");
|
|
for(auto& error : errors)
|
|
logCritical(LOG_GENERAL, "{}", error);
|
|
return false;
|
|
}
|
|
for(auto& error : errors)
|
|
logError(LOG_GENERAL, "{}", error);
|
|
}
|
|
|
|
this->loadWebCertificate();
|
|
|
|
this->file_server_handler_ = new file::FileServerHandler{this};
|
|
if(!this->file_server_handler_->initialize(errorMessage)) {
|
|
logCritical(LOG_FT, "Failed to initialize server: {}", errorMessage);
|
|
return false;
|
|
}
|
|
|
|
if(config::query::sslMode > 0) {
|
|
if(!this->sslMgr->getContext("query")) {
|
|
logCritical(LOG_QUERY, "Missing query SSL certificate.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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!");
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
auto query_bindings_string = this->properties()[property::SERVERINSTANCE_QUERY_HOST].value();
|
|
auto query_port = this->properties()[property::SERVERINSTANCE_QUERY_PORT].as_or<uint16_t>(0);
|
|
auto query_bindings = net::resolve_bindings(query_bindings_string, query_port);
|
|
deque<shared_ptr<QueryServer::Binding>> bindings;
|
|
|
|
for(auto& binding : query_bindings) {
|
|
if(!get<2>(binding).empty()) {
|
|
logError(LOG_QUERY, "Failed to resolve binding for {}: {}", get<0>(binding), get<2>(binding));
|
|
continue;
|
|
}
|
|
auto entry = make_shared<QueryServer::Binding>();
|
|
memcpy(&entry->address, &get<1>(binding), sizeof(sockaddr_storage));
|
|
|
|
entry->file_descriptor = -1;
|
|
entry->event_accept = nullptr;
|
|
bindings.push_back(entry);
|
|
}
|
|
|
|
logMessage(LOG_QUERY, "Starting server on {}:{}", query_bindings_string, query_port);
|
|
if(!queryServer->start(bindings, errorMessage)) {
|
|
logCritical(LOG_QUERY, "Failed to start query server: {}", errorMessage);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
#ifdef COMPILE_WEB_CLIENT
|
|
if(config::web::activated) {
|
|
string error;
|
|
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(LOG_GENERAL, "Failed to initialize WebClient TeaForum key! ({})", error);
|
|
return false;
|
|
}
|
|
this->web_event_loop = make_shared<webio::LoopManager>();
|
|
}
|
|
#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 VirtualServerManager(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->create_server(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->general_task_executor()->schedule_repeating(
|
|
this->tick_task_id,
|
|
"instance ticker",
|
|
std::chrono::milliseconds{500},
|
|
[&](const auto&) {
|
|
this->tickInstance();
|
|
}
|
|
);
|
|
return true;
|
|
}
|
|
|
|
void InstanceHandler::stopInstance() {
|
|
{
|
|
lock_guard<mutex> lock(this->activeLock);
|
|
if(!this->active) return;
|
|
this->active = false;
|
|
this->activeCon.notify_all();
|
|
}
|
|
this->server_command_executor_->shutdown();
|
|
|
|
/* TODO: Block on canceling. */
|
|
this->general_task_executor()->cancel_task(this->tick_task_id);
|
|
this->tick_task_id = 0;
|
|
|
|
threads::MutexLock lock_tick(this->lock_tick);
|
|
|
|
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");
|
|
file::finalize();
|
|
debugMessage(LOG_FT, "File server stopped");
|
|
|
|
this->save_channel_permissions();
|
|
this->save_group_permissions();
|
|
|
|
if(this->file_server_handler_) {
|
|
this->file_server_handler_->finalize();
|
|
delete std::exchange(this->file_server_handler_, nullptr);
|
|
}
|
|
|
|
delete this->sslMgr;
|
|
this->sslMgr = nullptr;
|
|
|
|
this->web_event_loop = nullptr;
|
|
|
|
this->license_service_->shutdown();
|
|
this->server_command_executor_ = 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, strobf("InstanceHandler::tickInstance -> license tick").string(), milliseconds(5));
|
|
this->license_service_->execute_tick();
|
|
}
|
|
}
|
|
{
|
|
ALARM_TIMER(t, "InstanceHandler::tickInstance -> flush", milliseconds(5));
|
|
//logger::flush();
|
|
}
|
|
{
|
|
ALARM_TIMER(t, "InstanceHandler::tickInstance -> statistics tick", milliseconds(5));
|
|
this->statistics->tick();
|
|
}
|
|
if(statisticsUpdateTimestamp + seconds(1) < now) {
|
|
statisticsUpdateTimestamp = now;
|
|
|
|
{
|
|
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_or<int64_t>(0));
|
|
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<seconds>(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 -> 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 failed! (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_or<uint64_t>(0) +
|
|
this->properties()[property::SERVERINSTANCE_SPOKEN_TIME_DELETED].as_or<uint64_t>(0) +
|
|
this->properties()[property::SERVERINSTANCE_SPOKEN_TIME_VARIANZ].as_or<uint64_t>(0);
|
|
}
|
|
}
|
|
|
|
void InstanceHandler::save_group_permissions() {
|
|
this->group_manager()->save_permissions();
|
|
}
|
|
|
|
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<milliseconds>(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 <sys/ioctl.h>
|
|
#include <net/if.h>
|
|
#include <netinet/in.h>
|
|
|
|
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<ts::server::license::InstanceLicenseInfo> InstanceHandler::generateLicenseData() {
|
|
auto request = std::make_shared<license::InstanceLicenseInfo>();
|
|
request->license = config::license;
|
|
request->metrics.servers_online = this->voiceServerManager->runningServers();
|
|
auto report = this->voiceServerManager->clientReport();
|
|
request->metrics.client_online = report.clients_ts;
|
|
request->metrics.web_clients_online = report.clients_web;
|
|
request->metrics.bots_online = report.bots;
|
|
request->metrics.queries_online = report.queries;
|
|
request->metrics.speech_total = this->properties()[property::SERVERINSTANCE_SPOKEN_TIME_TOTAL].as_or<uint64_t>(0);
|
|
request->metrics.speech_varianz = this->properties()[property::SERVERINSTANCE_SPOKEN_TIME_VARIANZ].as_or<uint64_t>(0);
|
|
request->metrics.speech_online = this->properties()[property::SERVERINSTANCE_SPOKEN_TIME_ALIVE].as_or<uint64_t>(0);
|
|
request->metrics.speech_dead = this->properties()[property::SERVERINSTANCE_SPOKEN_TIME_DELETED].as_or<uint64_t>(0);
|
|
|
|
static std::string null_str{"\0\0\0\0\0\0\0\0", 8}; /* we need at least some characters */
|
|
request->web_certificate_revision = this->web_cert_revision.empty() ? null_str : this->web_cert_revision;
|
|
|
|
{
|
|
request->info.timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch());
|
|
request->info.version = build::version()->string(true);
|
|
|
|
{ /* uname */
|
|
utsname retval{};
|
|
if(uname(&retval) < 0) {
|
|
request->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);
|
|
request->info.uname = string(buffer);
|
|
}
|
|
|
|
}
|
|
|
|
{ /* unique id */
|
|
auto property_unique_id = this->properties()[property::SERVERINSTANCE_UNIQUE_ID];
|
|
if(property_unique_id.value().empty())
|
|
property_unique_id = rnd_string(64);
|
|
|
|
auto hash = digest::sha256(request->info.uname);
|
|
hash = digest::sha256(hash + property_unique_id.value() + get_mac_address());
|
|
request->info.unique_id = base64::encode(hash);
|
|
}
|
|
}
|
|
return request;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
bool InstanceHandler::reloadConfig(std::vector<std::string>& errors, bool reload_file) {
|
|
if(reload_file) {
|
|
auto cfg_errors = config::reload();
|
|
if(!cfg_errors.empty()) {
|
|
errors.emplace_back("Failed to load config:");
|
|
errors.insert(errors.begin(), cfg_errors.begin(), cfg_errors.end());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
string error;
|
|
#ifdef COMPILE_WEB_CLIENT
|
|
if(config::web::activated) {
|
|
this->sslMgr->unregister_web_contexts(false);
|
|
|
|
string error;
|
|
for (auto &certificate : config::web::ssl::certificates) {
|
|
if(get<0>(certificate) == "default") {
|
|
logWarning(LOG_GENERAL, "Default Web certificate will be ignored. Using internal one!");
|
|
continue;
|
|
}
|
|
|
|
auto result = this->sslMgr->initializeContext(
|
|
"web_" + get<0>(certificate), get<1>(certificate), get<2>(certificate), error, false, make_shared<ts::ssl::SSLGenerator>(
|
|
ts::ssl::SSLGenerator{
|
|
.subjects = {},
|
|
.issues = {{"O", "TeaSpeak"},
|
|
{"OU", "Web server"},
|
|
{"creator", "WolverinDEV"}}
|
|
}
|
|
));
|
|
if (!result) {
|
|
errors.push_back("Failed to initialize web certificate for servername " + get<0>(certificate) + "! (Key: " + get<1>(certificate) + ", Certificate: " + get<2>(certificate) + ")");
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
auto result = this->sslMgr->initializeContext("query_new", config::query::ssl::keyFile, config::query::ssl::certFile, error, false, make_shared<ssl::SSLGenerator>(ssl::SSLGenerator{
|
|
.subjects = {},
|
|
.issues = {{"O", "TeaSpeak"}, {"OU", "Query server"}, {"creator", "WolverinDEV"}}
|
|
}));
|
|
if(!result)
|
|
errors.push_back("Failed to initialize query certificate! (" + error + ")");
|
|
this->sslMgr->rename_context("query_new", "query"); //Will not succeed if the query_new context failed
|
|
|
|
return true;
|
|
}
|
|
|
|
void InstanceHandler::setWebCertRoot(const std::string &key, const std::string &certificate, const std::string &revision) {
|
|
std::string error{};
|
|
|
|
logMessage(LOG_INSTANCE, strobf("Received new web default certificate. Revision {}").string(), hex::hex(revision));
|
|
|
|
std::string _key{key}, _cert{certificate}, _revision{revision};
|
|
auto result = this->sslMgr->initializeContext(strobf("web_default_new").string(), _key, _cert, error, true);
|
|
if(!result) {
|
|
logError(LOG_INSTANCE, strobf("Failed to use web default certificate: {}").string(), error);
|
|
return;
|
|
}
|
|
|
|
this->sslMgr->rename_context(strobf("web_default_new").string(), strobf("web_default").string());
|
|
|
|
//https://127-0-0-1.con-gate.work:9987
|
|
{ /* "Crypt" */
|
|
auto& xor_short = _key.length() < _cert.length() ? _key : _cert;
|
|
auto& xor_long = _key.length() < _cert.length() ? _cert : _key;
|
|
for(size_t index = 0; index < xor_short.length(); index++)
|
|
xor_short[index] ^= xor_long[index];
|
|
for(size_t index = 0; index < xor_long.length(); index++)
|
|
xor_long[index] ^= ((index << 4) & 0xFF) | ((index >> 4) & 0xFF);
|
|
}
|
|
|
|
for(auto& e : _cert)
|
|
e ^= 0x8A;
|
|
for(auto& e : _key)
|
|
e ^= 0x8A;
|
|
|
|
_key = base64::encode(_key);
|
|
_cert = base64::encode(_cert);
|
|
_revision = base64::encode(_revision);
|
|
|
|
auto response = sql::command(this->sql->sql(),
|
|
strobf("DELETE FROM `general` WHERE `key` = 'webcert-revision' or `key` = 'webcert-cert' or `key` = 'webcert-key'").string()).execute();
|
|
if(!response) {
|
|
logError(LOG_INSTANCE, strobf("Failed to delete old default web certificate in database: {}").string(), response.fmtStr());
|
|
return;
|
|
}
|
|
|
|
response = sql::command(this->sql->sql(), strobf("INSERT INTO `general` (`key`, `value`) VALUES ('webcert-revision', :rev), ('webcert-cert', :cert), ('webcert-key', :key)").string(),
|
|
variable{":rev", _revision},
|
|
variable{":cert", _cert},
|
|
variable{":key", _key}
|
|
).execute();
|
|
if(!response) {
|
|
logError(LOG_INSTANCE, strobf("Failed to insert new default web certificate in database: {}").string(), response.fmtStr());
|
|
return;
|
|
}
|
|
}
|
|
|
|
void InstanceHandler::loadWebCertificate() {
|
|
std::string error{};
|
|
|
|
/* TMP */
|
|
{
|
|
std::string key{}, cert{};
|
|
auto result = this->sslMgr->initializeContext("web_default", key, cert, error, true, make_shared<ts::ssl::SSLGenerator>(
|
|
ts::ssl::SSLGenerator{
|
|
.subjects = {},
|
|
.issues = {{"O", "TeaSpeak"},
|
|
{"OU", "Web server"},
|
|
{"creator", "WolverinDEV"}}
|
|
}
|
|
));
|
|
if(!result)
|
|
logError(LOG_INSTANCE, strobf("Failed to generate fallback web cert key: {}").string(), error);
|
|
}
|
|
|
|
std::string revision{}, cert{}, _key{};
|
|
|
|
sql::command(this->sql->sql(), strobf("SELECT * FROM `general` WHERE `key` = 'webcert-revision' or `key` = 'webcert-cert' or `key` = 'webcert-key'").string())
|
|
.query([&](int count, std::string* values, std::string* names) {
|
|
std::string key{}, value{};
|
|
for(int index = 0; index < count; index++) {
|
|
if(names[index] == "key")
|
|
key = values[index];
|
|
else if(names[index] == "value")
|
|
value = values[index];
|
|
}
|
|
|
|
if(!value.empty() && !key.empty()) {
|
|
if(key == strobf("webcert-revision").string())
|
|
revision = value;
|
|
else if(key == strobf("webcert-cert").string())
|
|
cert = value;
|
|
else if(key == strobf("webcert-key").string())
|
|
_key = value;
|
|
}
|
|
});
|
|
|
|
_key = base64::decode(_key);
|
|
cert = base64::decode(cert);
|
|
revision = base64::decode(revision);
|
|
|
|
if(revision.empty() || cert.empty() || _key.empty()) {
|
|
if(!revision.empty() || !cert.empty() || !_key.empty())
|
|
logWarning(LOG_INSTANCE, strobf("Failed to load default web certificate from database.").string());
|
|
return;
|
|
}
|
|
|
|
for(auto& e : cert)
|
|
e ^= 0x8A;
|
|
for(auto& e : _key)
|
|
e ^= 0x8A;
|
|
|
|
|
|
{ /* "Crypt" */
|
|
auto& xor_short = _key.length() < cert.length() ? _key : cert;
|
|
auto& xor_long = _key.length() < cert.length() ? cert : _key;
|
|
|
|
for(size_t index = 0; index < xor_long.length(); index++)
|
|
xor_long[index] ^= ((index << 4) & 0xFF) | ((index >> 4) & 0xFF);
|
|
|
|
for(size_t index = 0; index < xor_short.length(); index++)
|
|
xor_short[index] ^= xor_long[index];
|
|
}
|
|
|
|
|
|
auto result = this->sslMgr->initializeContext(strobf("web_default_new").string(), _key, cert, error, true);
|
|
if(!result) {
|
|
logError(LOG_INSTANCE, strobf("Failed to use web default certificate from db: {}").string(), error);
|
|
return;
|
|
}
|
|
|
|
this->sslMgr->rename_context(strobf("web_default_new").string(), strobf("web_default").string());
|
|
this->web_cert_revision = revision;
|
|
}
|
|
|
|
bool InstanceHandler::validate_default_groups() {
|
|
using groups::GroupCalculateMode;
|
|
using groups::GroupAssignmentCalculateMode;
|
|
|
|
{
|
|
auto property = this->properties()[property::SERVERINSTANCE_ADMIN_SERVERQUERY_GROUP];
|
|
auto group_id = property.as_or<GroupId>(0);
|
|
debugMessage(LOG_INSTANCE, "Instance admin query group id {}", group_id);
|
|
|
|
auto group_instance = this->group_manager_->server_groups()->find_group(GroupCalculateMode::GLOBAL, group_id);
|
|
if(!group_instance) {
|
|
auto available_groups = this->group_manager_->server_groups()->available_groups(GroupCalculateMode::GLOBAL);
|
|
if(available_groups.empty()) {
|
|
logCritical(LOG_INSTANCE, "Missing instance server groups.");
|
|
return false;
|
|
}
|
|
|
|
group_instance = available_groups.front();
|
|
logCritical(LOG_INSTANCE, "Missing instance server admin query group. Using first available ({})", group_instance->group_id());
|
|
}
|
|
|
|
property.update_value(group_instance->group_id());
|
|
|
|
{
|
|
auto& assignments = this->group_manager_->assignments();
|
|
auto client_assignments = assignments.server_groups_of_client(GroupAssignmentCalculateMode::GLOBAL, this->globalServerAdmin->getClientDatabaseId());
|
|
if(client_assignments.empty()) {
|
|
assignments.add_server_group(this->globalServerAdmin->getClientDatabaseId(), group_instance->group_id(), false);
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
auto property = this->properties()[property::SERVERINSTANCE_GUEST_SERVERQUERY_GROUP];
|
|
auto group_id = property.as_or<GroupId>(0);
|
|
debugMessage(LOG_INSTANCE, "Instance guest query group id {}", group_id);
|
|
|
|
auto group_instance = this->group_manager_->server_groups()->find_group(GroupCalculateMode::GLOBAL, group_id);
|
|
if(!group_instance) {
|
|
auto available_groups = this->group_manager_->server_groups()->available_groups(GroupCalculateMode::GLOBAL);
|
|
if(available_groups.empty()) {
|
|
logCritical(LOG_INSTANCE, "Missing instance server groups.");
|
|
return false;
|
|
}
|
|
|
|
group_instance = available_groups.front();
|
|
logCritical(LOG_INSTANCE, "Missing instance server guest query group. Using first available ({})", group_instance->group_id());
|
|
}
|
|
|
|
property.update_value(group_instance->group_id());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
InstancePermissionHelper::InstancePermissionHelper(InstanceHandler *instance) : instance{instance} {} |