Teaspeak-Server/license/server/LicenseManager.cpp

463 lines
21 KiB
C++

#include "LicenseManager.h"
#include <log/LogUtils.h>
using namespace std;
using namespace std::chrono;
using namespace license;
using namespace license::server;
using namespace sql;
LicenseManager::LicenseManager(sql::SqlManager* database) : database(database){
this->id_cache = make_shared<KeyIdCache>(this);
}
LicenseManager::~LicenseManager() { }
/*
LicenseType type;
std::string username;
std::string first_name;
std::string last_name;
std::string email;
std::chrono::system_clock::time_point start;
std::chrono::system_clock::time_point end;
std::chrono::system_clock::time_point creation;
*/
#define CTBL(cmd) \
res = command(this->database, cmd).execute(); \
if(!res) { \
error = "Could not setup tables! (" + res.fmtStr() + ")"; \
return false; \
}
#define CIDX(cmd) \
res = command(this->database, cmd).execute(); \
if(!res && res.msg().find("Duplicate") == string::npos && res.msg().find("exist") == string::npos) { \
error = "Could not setup indexes! (" + res.fmtStr() + ")"; \
return false; \
}
#define SET_VERSION(ver) \
version = ver; \
sql::command(this->database, "UPDATE `general` SET `value` = :version WHERE `key` = 'version'", variable{":version", version}).execute();
bool LicenseManager::setup(std::string& error) {
result res;
int version = -1;
sql::command(this->database, "SELECT `value` FROM `general` WHERE `key` = 'version'").query([](int* version, int lnegth, string* values, string* names) {
*version = stoll(values[0]);
return 0;
}, &version);
switch (version) {
case -1:
CTBL("CREATE TABLE IF NOT EXISTS `license` (`key` BINARY(64) NOT NULL PRIMARY KEY, `type` INT, `deleted` BOOL, `issuer` TEXT)");
CTBL("CREATE TABLE IF NOT EXISTS `license_info` (`key` BINARY(64) NOT NULL PRIMARY KEY, `username` VARCHAR(128), `first_name` TEXT, `last_name` TEXT, `email` TEXT, `begin` BIGINT, `end` BIGINT, `generated` BIGINT)");
CTBL("CREATE TABLE IF NOT EXISTS `license_request` (`key` BINARY(64) NOT NULL, `ip` TEXT, `timestamp` BIGINT, `result` INT)");
CTBL("CREATE TABLE IF NOT EXISTS `general` (`key` VARCHAR(64), `value` TEXT)");
CTBL("INSERT INTO `general` (`key`, `value`) VALUES ('version', '0');");
SET_VERSION(0);
case 0:
logMessage("Updating database! To version 1");
CTBL("CREATE TABLE IF NOT EXISTS `history_speach` (`keyId` INT, `timestamp` BIGINT, `total` BIGINT, `dead` BIGINT, `online` BIGINT, `varianz` BIGINT)");
CTBL("CREATE TABLE IF NOT EXISTS `history_online` (`keyId` INT, `timestamp` BIGINT, `server` INT, `clients` INT, `web` INT, `music` INT, `queries` INT)");
CIDX("CREATE INDEX `license_key` ON `license` (`key`)");
CIDX("CREATE INDEX `license_info_key` ON `license_info` (`key`)");
CTBL("ALTER TABLE `license` DROP PRIMARY KEY, ADD `keyId` INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST;");
CTBL("ALTER TABLE `license_info` DROP PRIMARY KEY, ADD `keyId` INT NOT NULL PRIMARY KEY AUTO_INCREMENT FIRST;");
//Fixing key id's
CTBL("UPDATE `license_info` LEFT JOIN `license` ON `license`.`key` = `license_info`.`key` SET `license_info`.`keyId` = `license`.`keyId`");
//Deleting the key blob and removing auto
CTBL("ALTER TABLE `license_info` CHANGE `keyId` `keyId` INT NOT NULL UNIQUE, DROP COLUMN `key`");
//Fixing request table
CTBL("ALTER TABLE license_request ADD COLUMN `keyId` INT(0) NOT NULL;");
CTBL("UPDATE license_request LEFT JOIN license ON license.key = license_request.key SET license_request.keyId = license.keyId;");
CTBL("ALTER TABLE license_request DROP COLUMN `key`;");
//IDK why but i failed stuff :D
CTBL("ALTER TABLE license DROP PRIMARY KEY, CHANGE `keyId` `keyId` INT NOT NULL AUTO_INCREMENT PRIMARY KEY");
SET_VERSION(1);
case 1:
CTBL("ALTER TABLE history_online ADD COLUMN `ip` TEXT AFTER `keyId`;");
CTBL("ALTER TABLE history_speach ADD COLUMN `ip` TEXT AFTER `keyId`;");
SET_VERSION(2);
case 2:
CTBL("CREATE TABLE `history_version` (`keyId` INT, `ip` VARCHAR(64), `timestamp` BIGINT, `version` VARCHAR(126));");
SET_VERSION(3);
case 3:
/*
CTBL("CREATE TABLE `save_history_online` AS SELECT * FROM `history_online`;");
CTBL("CREATE TABLE `save_history_speach` AS SELECT * FROM `history_speach`;");
CTBL("CREATE TABLE `save_history_version` AS SELECT * FROM `history_version`;");
*/
CTBL("ALTER TABLE `history_online` CHANGE `ip` `unique_id` VARCHAR(64) NOT NULL;");
CTBL("ALTER TABLE `history_speach` CHANGE `ip` `unique_id` VARCHAR(64) NOT NULL;");
CTBL("ALTER TABLE `history_version` CHANGE `ip` `unique_id` VARCHAR(64) NOT NULL;");
CTBL("ALTER TABLE `license_request` ADD `unique_id` VARCHAR(64) NOT NULL AFTER `ip`;");
SET_VERSION(4);
case 4:
CTBL("CREATE TABLE IF NOT EXISTS `users` (`username` VARCHAR(64) NOT NULL PRIMARY KEY, `password_hash` VARCHAR(128), `status` INT, `owner` VARCHAR(64))");
default:;
}
return true;
}
bool LicenseManager::registerLicense(const std::string& key, const shared_ptr<LicenseInfo>& info, const std::string& issuer) {
result res;
res = command(this->database, "INSERT INTO `license` (`key`, `type`, `deleted`, `issuer`) VALUES (:key, :type, :deleted, :issuer)", variable{":key", key}, variable{":type", (uint32_t) info->type}, variable{":deleted", false}, variable{":issuer", issuer}).execute();
if(!res) {
logError("Could not register new license (" + res.fmtStr() + ")");
return false;
}
auto keyId = this->id_cache->getKeyId(key);
if(keyId == 0) return false;
res = command(this->database, "INSERT INTO `license_info` (`keyId`, `username`, `first_name`, `last_name`, `email`, `begin`, `end`, `generated`) VALUES (:key, :username, :first_name, :last_name, :email, :begin, :end, :generated)",
variable{":key", keyId},
variable{":username", info->username},
variable{":first_name", info->first_name},
variable{":last_name", info->last_name},
variable{":email", info->email},
variable{":generated", duration_cast<milliseconds>(info->creation.time_since_epoch()).count()},
variable{":begin", duration_cast<milliseconds>(info->start.time_since_epoch()).count()},
variable{":end", duration_cast<milliseconds>(info->end.time_since_epoch()).count()}
).execute();
if(!res) {
logError("Could not register new license info (" + res.fmtStr() + ")");
return false;
}
return true;
}
bool LicenseManager::deleteLicense(const std::string& key, bool full) {
if(full) {
auto keyId = this->id_cache->getKeyId(key);
if(keyId == 0) return false; //Never exists
auto res = command(this->database, "DELETE FROM `license` WHERE `key` = :key", variable{":key", key}).execute();
if(!res) logError("Could not delete license (" + res.fmtStr() + ")");
res = command(this->database, "DELETE FROM `license_info` WHERE `keyId` = :key", variable{":keyId", keyId}).execute();
if(!res) logError("Could not delete license (" + res.fmtStr() + ")");
return !!res;
} else {
auto res = command(this->database, "UPDATE `license` SET `deleted` = :true WHERE `key` = :key", variable{":true", true}, variable{":key", key}).execute();
if(!res) logError("Could not delete license (" + res.fmtStr() + ")");
return !!res;
}
}
bool LicenseManager::validLicenseKey(const std::string& key) {
bool valid = false;
auto res = command(this->database, "SELECT * FROM `license` WHERE `key` = :key AND `deleted` = :false LIMIT 1", variable{":false", false}, variable{":key", key}).query([](bool* flag, int, char**, char**) {
*flag = true;
return 0;
}, &valid);
if(!res) logError("Could not validate license (" + res.fmtStr() + ")");
return !!res && valid;
}
inline std::map<std::string, std::shared_ptr<LicenseInfo>> query_license(SqlManager* mgr, std::string key, int offset, int length) {
std::map<std::string, std::shared_ptr<LicenseInfo>> result;
auto query = string() + "SELECT `key`, `username`, `first_name`, `last_name`, `email`, `begin`, `end`, `generated`, `deleted` FROM `license_info` INNER JOIN `license` ON `license_info`.`keyId` = `license`.`keyId`";
if(!key.empty())
query += "WHERE `key` = :key";
else
query += "WHERE `deleted` = :false";
if(offset > 0 || length > 0)
query += " LIMIT " + to_string(offset) + ", " + to_string(length);
logTrace(LOG_GENERAL, "Executing query {}", query);
auto res = command(mgr, query, variable{":key", key}, variable{":false", false}).query([](std::map<std::string, std::shared_ptr<LicenseInfo>>* list, int length, std::string* values, std::string* names) {
auto info = make_shared<LicenseInfo>();
info->deleted = false;
string k;
for(int index = 0; index < length; index++) {
try {
if(names[index] == "username")
info->username = values[index];
else if(names[index] == "first_name")
info->first_name = values[index];
else if(names[index] == "key")
k = values[index];
else if(names[index] == "last_name")
info->last_name = values[index];
else if(names[index] == "email")
info->email = values[index];
else if(names[index] == "begin")
info->start = system_clock::time_point() + milliseconds(stoll(values[index]));
else if(names[index] == "end")
info->end = system_clock::time_point() + milliseconds(stoll(values[index]));
else if(names[index] == "generated")
info->creation = system_clock::time_point() + milliseconds(stoll(values[index]));
else if(names[index] == "deleted")
info->deleted = values[index] == "1" || values[index] == "true";
else
logError(LOG_GENERAL, "Unknown field {}", names[index]);
} catch (std::exception& ex) {
logError(LOG_GENERAL, "Failed to parse field {} ({}). Message: {}", names[index], values[index], ex.what());
return 1;
}
}
(*list)[k] = info;
return 0;
}, &result);
logTrace(LOG_GENERAL, "Query returned {} results", result.size());
if(!res) logError("Could not query license (" + res.fmtStr() + ")");
return result;
}
std::shared_ptr<LicenseInfo> LicenseManager::licenseInfo(const std::string& key) {
auto result = query_license(this->database, key, 0, 0);
if(result.empty()) return nullptr;
return result.begin()->second;
}
std::map<std::string, std::shared_ptr<LicenseInfo>> LicenseManager::listLicenses(int offset, int limit) {
return query_license(this->database, "", offset, limit);
}
bool LicenseManager::logRequest(const std::string& key, const std::string& unique_id, const std::string& ip, const std::string& version, int state) {
result res;
auto keyId = this->id_cache->getKeyId(key);
if(keyId == 0) {
logError(LOG_GENERAL, "Failed to log license request (could not resolve key id)");
return false;
}
auto timestamp = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
//SELECT * FROM `license_info` WHERE `keyId` IN (SELECT `keyId` FROM `license` WHERE `key` = '000')
res = command(this->database, "INSERT INTO `license_request` (`keyId`, `ip`, `unique_id`, `timestamp`, `result`) VALUES (:key, :ip, :unique_id, :timestamp, :result)",
variable{":key", keyId},
variable{":ip", ip},
variable{":timestamp", timestamp},
variable{":unique_id", unique_id},
variable{":result", state}).execute();
if(!res) {
logError("Could not log license validation (" + res.fmtStr() + ")");
return false;
}
{ //Log version
res = command(this->database, "INSERT INTO `history_version`(`keyId`, `unique_id`, `timestamp`, `version`) VALUES (:key, :unique_id, :time, :version)",
variable{":key", keyId},
variable{":time", timestamp},
variable{":unique_id", unique_id},
variable{":version", version}
).execute();
if(!res)
logError("Could not log license version statistic (" + res.fmtStr() + ")");
res = {};
}
return true;
}
bool LicenseManager::logStatistic(const std::string &key, const std::string& unique_id, const std::string &ip,
const ts::proto::license::PropertyUpdateRequest &data) {
result res;
auto keyId = this->id_cache->getKeyId(key);
if(keyId == 0) return false;
auto time = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
{ //Log online
res = command(this->database, "INSERT INTO `history_online`(`keyId`, `unique_id`, `timestamp`, `server`, `clients`, `web`, `music`, `queries`) VALUES (:key, :unique_id, :time, :server, :client, :web, :music, :query)",
variable{":key", keyId},
variable{":unique_id", unique_id},
variable{":time", time},
variable{":server", data.servers_online()},
variable{":client", data.clients_online()},
variable{":web", data.web_clients_online()},
variable{":music", data.bots_online()},
variable{":query", data.queries_online()}
).execute();
if(!res)
logError("Could not log license statistics (" + res.fmtStr() + ")");
res = {};
}
//SELECT * FROM `license_info` WHERE `keyId` IN (SELECT `keyId` FROM `license` WHERE `key` = '000')
{
res = command(this->database, "INSERT INTO `history_speach`(`keyId`, `unique_id`, `timestamp`, `total`, `dead`, `online`, `varianz`) VALUES (:key, :unique_id, :time, :total, :dead, :online, :varianz)",
variable{":key", keyId},
variable{":unique_id", unique_id},
variable{":time", time},
variable{":total", data.speach_total()},
variable{":dead", data.speach_dead()},
variable{":online", data.speach_online()},
variable{":varianz", data.speach_varianz()}
).execute();
if(!res)
logError("Could not log license statistics (" + res.fmtStr() + ")");
res = {};
}
return true;
}
std::deque<std::unique_ptr<LicenseManager::GlobalUserStatistics>> LicenseManager::list_statistics_user(const system_clock::time_point &begin, const system_clock::time_point &end, const milliseconds &interval) {
map<std::string, deque<unique_ptr<LicenseManager::UserStatistics>>> server_statistics;
auto timeout = hours(2) + minutes(10); //TODO timeout configurable?
auto state = command(this->database,"SELECT * FROM `history_online` WHERE `timestamp` >= :timestamp_start AND `timestamp` <= :timestamp_end ORDER BY `timestamp` ASC",
variable{":timestamp_start", duration_cast<milliseconds>(begin.time_since_epoch() - timeout).count()},
variable{":timestamp_end", duration_cast<milliseconds>(end.time_since_epoch()).count()}
).query([&server_statistics](int columns, std::string* values, std::string* names){
size_t key_id = 0;
std::string unique_id;
auto info = make_unique<LicenseManager::UserStatistics>();
for(int index = 0; index < columns; index++) {
try {
if(names[index] == "keyId")
key_id = stoull(values[index]);
else if(names[index] == "unique_id")
unique_id = values[index];
else if(names[index] == "timestamp")
info->timestamp = system_clock::time_point() + milliseconds(stoll(values[index]));
else if(names[index] == "server")
info->servers_online = stoull(values[index]);
else if(names[index] == "clients")
info->clients_online = stoull(values[index]);
else if(names[index] == "web")
info->web_clients_online = stoull(values[index]);
else if(names[index] == "music")
info->bots_online = stoull(values[index]);
else if(names[index] == "queries")
info->queries_online = stoull(values[index]);
} catch (std::exception& ex) {
logError("Failed to parse column " + names[index] + " => " + ex.what() + " (Value: " + values[index] + ")");
return 0;
}
}
if(key_id == 0) return 0;
server_statistics[to_string(key_id) + "_" + unique_id].push_back(std::move(info));
return 0;
});
if(!state) {
logError("Could not read license statistics (" + state.fmtStr() + ")");
return {};
}
std::deque<std::unique_ptr<LicenseManager::GlobalUserStatistics>> result;
system_clock::time_point current_timestamp = begin;
while(current_timestamp <= end) {
auto info = make_unique<LicenseManager::GlobalUserStatistics>();
info->timestamp = current_timestamp;
for(auto& server : server_statistics) {
while(!server.second.empty()) {
auto& first = *server.second.begin();
if(first->timestamp > current_timestamp) break; //Entry within the future
if(first->timestamp + timeout < current_timestamp) { //Entry within the past
server.second.pop_front();
continue;
}
if(server.second.size() > 1) {
auto& second = *(server.second.begin() + 1);
if(second->timestamp <= current_timestamp) {
server.second.pop_front(); //The next entry is more up 2 date
continue;
}
}
info->instance_online++;
info->queries_online += first->queries_online;
info->bots_online += first->bots_online;
info->web_clients_online += first->web_clients_online;
info->clients_online += first->clients_online;
info->servers_online += first->servers_online;
break;
}
}
result.push_back(std::move(info));
current_timestamp += interval;
}
return result;
}
std::deque<std::unique_ptr<LicenseManager::GlobalVersionsStatistic>> LicenseManager::list_statistics_version(const std::chrono::system_clock::time_point &begin, const std::chrono::system_clock::time_point &end, const std::chrono::milliseconds &interval) {
map<std::string, deque<unique_ptr<LicenseManager::GlobalVersionsStatistic>>> server_statistics;
auto timeout = hours(2) + minutes(10); //TODO timeout configurable?
auto state = command(this->database,"SELECT * FROM `history_version` WHERE `timestamp` >= :timestamp_start AND `timestamp` <= :timestamp_end ORDER BY `timestamp` ASC",
variable{":timestamp_start", duration_cast<milliseconds>(begin.time_since_epoch() - timeout).count()},
variable{":timestamp_end", duration_cast<milliseconds>(end.time_since_epoch()).count()}
).query([&server_statistics](int columns, std::string* values, std::string* names){
size_t key_id = 0;
std::string unique_id;
auto info = make_unique<LicenseManager::GlobalVersionsStatistic>();
for(int index = 0; index < columns; index++) {
try {
if(names[index] == "keyId")
key_id = stoull(values[index]);
else if(names[index] == "unique_id")
unique_id = values[index];
else if(names[index] == "timestamp")
info->timestamp = system_clock::time_point() + milliseconds(stoll(values[index]));
else if(names[index] == "version")
info->versions[values[index]] = 1;
} catch (std::exception& ex) {
logError("Failed to parse column " + names[index] + " => " + ex.what() + " (Value: " + values[index] + ")");
return 0;
}
}
if(key_id == 0) return 0;
server_statistics[to_string(key_id) + "_" + unique_id].push_back(std::move(info));
return 0;
});
if(!state) {
logError("Could not read license statistics (" + state.fmtStr() + ")");
return {};
}
std::deque<std::unique_ptr<LicenseManager::GlobalVersionsStatistic>> result;
system_clock::time_point current_timestamp = begin;
while(current_timestamp <= end) {
auto info = make_unique<LicenseManager::GlobalVersionsStatistic>();
info->timestamp = current_timestamp;
for(auto& server : server_statistics) {
while(!server.second.empty()) {
auto& first = *server.second.begin();
if(first->timestamp > current_timestamp) break; //Entry within the future
if(first->timestamp + timeout < current_timestamp) { //Entry within the past
server.second.pop_front();
continue;
}
if(server.second.size() > 1) {
auto& second = *(server.second.begin() + 1);
if(second->timestamp <= current_timestamp) {
server.second.pop_front(); //The next entry is more up 2 date
continue;
}
}
for(const auto& entry : first->versions)
info->versions[entry.first] += entry.second;
break;
}
}
result.push_back(std::move(info));
current_timestamp += interval;
}
return result;
}