775 lines
32 KiB
C++
775 lines
32 KiB
C++
//
|
|
// Created by wolverindev on 22.10.17.
|
|
//
|
|
|
|
#include "QueryServer.h"
|
|
#include <algorithm>
|
|
#include <netinet/tcp.h>
|
|
#include <src/VirtualServer.h>
|
|
#include <src/client/query/QueryClient.h>
|
|
#include <src/client/InternalClient.h>
|
|
#include <misc/rnd.h>
|
|
#include <src/InstanceHandler.h>
|
|
#include <ThreadPool/ThreadHelper.h>
|
|
#include <log/LogUtils.h>
|
|
|
|
using namespace std;
|
|
using namespace std::chrono;
|
|
using namespace ts;
|
|
using namespace ts::server;
|
|
|
|
#if defined(TCP_CORK) && !defined(TCP_NOPUSH)
|
|
#define TCP_NOPUSH TCP_CORK
|
|
#endif
|
|
|
|
QueryServer::QueryServer(sql::SqlManager* db) : sql(db) {
|
|
}
|
|
|
|
QueryServer::~QueryServer() {
|
|
stop();
|
|
}
|
|
|
|
|
|
void QueryServer::unregisterConnection(const shared_ptr<QueryClient> &client) {
|
|
{
|
|
lock_guard lock(this->connected_clients_mutex);
|
|
auto found = std::find(this->connected_clients.begin(), this->connected_clients.end(), client);
|
|
if(found != this->connected_clients.end()) {
|
|
this->connected_clients.erase(found);
|
|
} else {
|
|
logError(LOG_QUERY, "Attempted to unregister an invalid connection!");
|
|
}
|
|
}
|
|
|
|
/* client->handle = nullptr; */
|
|
}
|
|
|
|
bool QueryServer::start(const deque<shared_ptr<QueryServer::Binding>> &bindings_, std::string &error) {
|
|
if(this->active) {
|
|
error = "already started";
|
|
return false;
|
|
}
|
|
this->active = true;
|
|
|
|
/* load ip black/whitelist */
|
|
{
|
|
ip_blacklist = std::make_unique<IpListManager>("query_ip_blacklist.txt", std::deque<std::string>{"#A new line separated address blacklist", "#", "#For example if we dont want google:", "8.8.8.8"});
|
|
ip_whitelist = std::make_unique<IpListManager>("query_ip_whitelist.txt", std::deque<std::string>{"#A new line separated address whitelist", "#Every ip have no flood and login attempt limit!", "127.0.0.1/8", "::1"});
|
|
|
|
if(!this->ip_blacklist->reload(error)) {
|
|
logError(LOG_QUERY, "Failed to load query blacklist: {}", error);
|
|
}
|
|
|
|
if(!this->ip_whitelist->reload(error)) {
|
|
logError(LOG_QUERY, "Failed to load query whitelist: {}", error);
|
|
}
|
|
|
|
error.clear();
|
|
}
|
|
|
|
/* reserve backup file descriptor in case that the max file descriptors have been reached */
|
|
{
|
|
this->server_reserve_fd = dup(1);
|
|
if(this->server_reserve_fd < 0) {
|
|
logWarning(LOG_QUERY, "Failed to reserve a backup accept file descriptor. ({} | {})", errno, strerror(errno));
|
|
}
|
|
}
|
|
|
|
/* setup event bases */
|
|
{
|
|
this->event_io_loop = event_base_new();
|
|
this->event_io_thread = std::thread{[&]{
|
|
while(this->active) {
|
|
debugMessage(LOG_QUERY, "Entering event loop ({})", (void*) this->event_io_loop);
|
|
event_base_loop(this->event_io_loop, EVLOOP_NO_EXIT_ON_EMPTY);
|
|
if(this->active) {
|
|
debugMessage(LOG_QUERY, "Event loop exited ({}). No active events. Sleeping 1 seconds", (void*) this->event_io_loop);
|
|
this_thread::sleep_for(seconds(1));
|
|
} else {
|
|
debugMessage(LOG_QUERY, "Event loop exited ({})", (void*) this->event_io_loop);
|
|
}
|
|
}
|
|
}};
|
|
|
|
threads::name(this->event_io_thread, "query io");
|
|
}
|
|
|
|
for(auto& binding : bindings_) {
|
|
binding->file_descriptor = socket(binding->address.ss_family, (unsigned) SOCK_STREAM | (unsigned) SOCK_NONBLOCK, 0);
|
|
if(binding->file_descriptor < 0) {
|
|
logError(LOG_QUERY, "Failed to bind server to {}. (Failed to create socket: {} | {})", binding->as_string(), errno, strerror(errno));
|
|
continue;
|
|
}
|
|
|
|
int enable = 1, disabled = 0;
|
|
|
|
if (setsockopt(binding->file_descriptor, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)) < 0) {
|
|
logWarning(LOG_QUERY, "Failed to activate SO_REUSEADDR for binding {} ({} | {})", binding->as_string(), errno, strerror(errno));
|
|
}
|
|
|
|
if(setsockopt(binding->file_descriptor, IPPROTO_TCP, TCP_NOPUSH, &disabled, sizeof disabled) < 0) {
|
|
logWarning(LOG_QUERY, "Failed to deactivate TCP_NOPUSH for binding {} ({} | {})", binding->as_string(), errno, strerror(errno));
|
|
}
|
|
|
|
if(binding->address.ss_family == AF_INET6) {
|
|
if(setsockopt(binding->file_descriptor, IPPROTO_IPV6, IPV6_V6ONLY, &enable, sizeof(int)) < 0) {
|
|
logWarning(LOG_QUERY, "Failed to activate IPV6_V6ONLY for IPv6 binding {} ({} | {})", binding->as_string(), errno, strerror(errno));
|
|
}
|
|
}
|
|
|
|
if(fcntl(binding->file_descriptor, F_SETFD, FD_CLOEXEC) < 0) {
|
|
logWarning(LOG_QUERY, "Failed to set flag FD_CLOEXEC for binding {} ({} | {})", binding->as_string(), errno, strerror(errno));
|
|
}
|
|
|
|
|
|
if (bind(binding->file_descriptor, (struct sockaddr *) &binding->address, sizeof(binding->address)) < 0) {
|
|
logError(LOG_QUERY, "Failed to bind server to {}. (Failed to bind socket: {} | {})", binding->as_string(), errno, strerror(errno));
|
|
close(binding->file_descriptor);
|
|
continue;
|
|
}
|
|
|
|
if (listen(binding->file_descriptor, SOMAXCONN) < 0) {
|
|
logError(LOG_QUERY, "Failed to bind server to {}. (Failed to listen: {} | {})", binding->as_string(), errno, strerror(errno));
|
|
close(binding->file_descriptor);
|
|
continue;
|
|
}
|
|
|
|
binding->event_accept = event_new(this->event_io_loop, binding->file_descriptor, EV_READ | EV_PERSIST, [](int a, short b, void* c){ ((QueryServer *) c)->on_client_receive(a, b, c); }, this);
|
|
event_add(binding->event_accept, nullptr);
|
|
this->bindings.push_back(binding);
|
|
}
|
|
|
|
if(this->bindings.empty()) {
|
|
this->stop();
|
|
error = "failed to bind to any address";
|
|
return false;
|
|
}
|
|
|
|
this->tick_active = true;
|
|
this->tick_thread = std::thread{[&]{ this->tick_executor(); }};
|
|
threads::name(this->tick_thread, "query tick");
|
|
return true;
|
|
}
|
|
void QueryServer::stop() {
|
|
if(!this->active) {
|
|
return;
|
|
}
|
|
|
|
this->active = false;
|
|
|
|
/* 1. Shutdown all bindings so we don't get any new queries */
|
|
for(auto& binding : this->bindings) {
|
|
if(binding->event_accept) {
|
|
event_del_block(binding->event_accept);
|
|
event_free(binding->event_accept);
|
|
binding->event_accept = nullptr;
|
|
}
|
|
|
|
if(binding->file_descriptor > 0) {
|
|
/* Shutdown not needed since we're not connected. A shutdown would result in "Transport endpoint is not connected". */
|
|
if(close(binding->file_descriptor) < 0) {
|
|
logError(LOG_QUERY, "Failed to close socket for binding {} ({} | {}).", binding->as_string(), errno, strerror(errno));
|
|
}
|
|
|
|
binding->file_descriptor = -1;
|
|
}
|
|
}
|
|
this->bindings.clear();
|
|
|
|
/* 2. Disconnect all connected query clients */
|
|
{
|
|
ts::command_builder notify{"serverstop"};
|
|
notify.put_unchecked(0, "stopped", "1");
|
|
|
|
std::lock_guard client_lock{this->connected_clients_mutex};
|
|
for(const auto &client : this->connected_clients) {
|
|
client->sendCommand(notify, false);
|
|
client->disconnect("server stopped");
|
|
|
|
/*
|
|
* Shortcircuiting the disconnect since we don't want the full "server leave" disconnect.
|
|
* We only wan't to prevent the client form receiving any more notifications.
|
|
*/
|
|
this->execute_query_disconnect(client, true);
|
|
}
|
|
}
|
|
|
|
/* Await all clients to disconnect within 5 seconds. */
|
|
{
|
|
std::unique_lock client_lock{this->connected_clients_mutex};
|
|
this->connected_client_disconnected_notify.wait_for(client_lock, std::chrono::seconds{5}, [&]{
|
|
return this->connected_clients.empty();
|
|
});
|
|
}
|
|
|
|
/* 3. Shutdown the query event loop (to finish of client disconnects as well) */
|
|
{
|
|
std::lock_guard tick_lock{this->tick_mutex};
|
|
this->tick_active = false;
|
|
this->tick_notify.notify_all();
|
|
}
|
|
threads::save_join(this->tick_thread, true);
|
|
|
|
/*
|
|
* 4. Force disconnect pending clients.
|
|
*/
|
|
{
|
|
std::unique_lock client_lock{this->connected_clients_mutex};
|
|
auto connected_clients_ = std::move(this->connected_clients);
|
|
client_lock.unlock();
|
|
|
|
if(!connected_clients_.empty()) {
|
|
logWarning(LOG_QUERY, "Failed to normally disconnect {} query clients. Closing connection.", connected_clients_.size());
|
|
|
|
for(const auto& client : this->connected_clients) {
|
|
this->execute_query_connection_close(client, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 5. Shutdown the io event loop */
|
|
if(this->event_io_loop) {
|
|
event_base_loopexit(this->event_io_loop, nullptr);
|
|
}
|
|
threads::save_join(this->event_io_thread, false);
|
|
|
|
if(this->event_io_loop) {
|
|
event_base_free(this->event_io_loop);
|
|
this->event_io_loop = nullptr;
|
|
}
|
|
|
|
/* 6. Cleanup the servers reserve file descriptor */
|
|
if(this->server_reserve_fd > 0) {
|
|
if(close(this->server_reserve_fd) < 0) {
|
|
logError(LOG_QUERY, "Failed to close backup file descriptor ({} | {})", errno, strerror(errno));
|
|
}
|
|
}
|
|
this->server_reserve_fd = -1;
|
|
}
|
|
|
|
inline std::string logging_address(const sockaddr_storage& address) {
|
|
if(config::server::disable_ip_saving)
|
|
return "X.X.X.X" + to_string(net::port(address));
|
|
return net::to_string(address, true);
|
|
}
|
|
|
|
inline void send_direct_disconnect(const sockaddr_storage& address, int file_descriptor, const char* message, size_t message_length) {
|
|
auto enable_non_block = [&]{
|
|
int flags = fcntl(file_descriptor, F_GETFL, 0);
|
|
if (flags == -1) {
|
|
debugMessage(LOG_QUERY, "[{}] Failed to set socket to nonblock. Flag query failed ({} | {})", logging_address(address), errno, strerror(errno));
|
|
return;
|
|
}
|
|
|
|
flags &= ~O_NONBLOCK;
|
|
if(fcntl(file_descriptor, F_SETFL, flags) == -1) {
|
|
debugMessage(LOG_QUERY, "[{}] Failed to set socket to nonblock. Flag apply failed ({} | {})", logging_address(address), errno, strerror(errno));
|
|
return;
|
|
}
|
|
};
|
|
enable_non_block();
|
|
|
|
{
|
|
struct timeval timeout{};
|
|
timeout.tv_sec = 5;
|
|
timeout.tv_usec = 0;
|
|
if (setsockopt (file_descriptor, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, sizeof(timeout)) < 0) {
|
|
debugMessage(LOG_QUERY, "[{}] Failed to set the send timeout on socket", logging_address(address));
|
|
}
|
|
}
|
|
|
|
bool broken_pipe = false;
|
|
auto send_ = [&](const char* data, size_t length) {
|
|
if(broken_pipe) {
|
|
return;
|
|
}
|
|
|
|
size_t written_bytes = 0;
|
|
while(written_bytes < length) {
|
|
auto result = send(file_descriptor, data + written_bytes, length - written_bytes, MSG_NOSIGNAL);
|
|
if(result <= 0) {
|
|
broken_pipe |= errno == EPIPE;
|
|
debugMessage(LOG_QUERY, "[{}] Failed to send a message of length {}. Bytes written: {}, error: {} | {}", logging_address(address), length, written_bytes, errno, strerror(errno));
|
|
return;
|
|
} else {
|
|
written_bytes += result;
|
|
}
|
|
}
|
|
};
|
|
|
|
/* we could ignore errors here */
|
|
send_(config::query::motd.data(), config::query::motd.size());
|
|
send_(message, message_length);
|
|
|
|
/* "flush" with the last new line and then close */
|
|
int flag = 1;
|
|
if(setsockopt(file_descriptor, IPPROTO_TCP, TCP_NODELAY, (char *) &flag, sizeof(int)) < 0) {
|
|
debugMessage(LOG_QUERY, "[{}] Failed to enabled TCP no delay to flush the direct query disconnect socket ({} | {}).", logging_address(address), errno, strerror(errno));
|
|
}
|
|
send_(config::query::newlineCharacter.data(), config::query::newlineCharacter.size());
|
|
|
|
if(shutdown(file_descriptor, SHUT_RDWR) < 0) {
|
|
debugMessage(LOG_QUERY, "[{}] Failed to shutdown socket ({} | {}).", logging_address(address), errno, strerror(errno));
|
|
}
|
|
if(close(file_descriptor) < 0) {
|
|
debugMessage(LOG_QUERY, "[{}] Failed to close socket ({} | {}).", logging_address(address), errno, strerror(errno));
|
|
}
|
|
}
|
|
|
|
//dummyfdflood
|
|
//dummyfdflood clear
|
|
|
|
void QueryServer::on_client_receive(int server_file_descriptor, short, void *) {
|
|
sockaddr_storage remote_address{};
|
|
memset(&remote_address, 0, sizeof(sockaddr_in));
|
|
socklen_t address_length = sizeof(remote_address);
|
|
|
|
int client_file_descriptor = accept(server_file_descriptor, (struct sockaddr *) &remote_address, &address_length);
|
|
if (client_file_descriptor < 0) {
|
|
if(errno == EAGAIN) {
|
|
return;
|
|
}
|
|
|
|
if(errno == EMFILE || errno == ENFILE) {
|
|
if(errno == EMFILE) {
|
|
logError(LOG_QUERY, "Server ran out file descriptors. Please increase the process file descriptor limit or decrease the instance variable 'serverinstance_query_max_connections'");
|
|
} else {
|
|
logError(LOG_QUERY, "Server ran out file descriptors. Please increase the process and system-wide file descriptor limit or decrease the instance variable 'serverinstance_query_max_connections'");
|
|
}
|
|
|
|
bool tmp_close_success{false};
|
|
{
|
|
lock_guard reserve_fd_lock(server_reserve_fd_lock);
|
|
if(this->server_reserve_fd > 0) {
|
|
debugMessage(LOG_QUERY, "Trying to accept client with the reserved file descriptor to send him a protocol limit reached exception.");
|
|
auto _ = [&]{
|
|
if(close(this->server_reserve_fd) < 0) {
|
|
debugMessage(LOG_QUERY, "Failed to close reserved file descriptor");
|
|
tmp_close_success = false;
|
|
return;
|
|
}
|
|
this->server_reserve_fd = 0;
|
|
|
|
errno = 0;
|
|
client_file_descriptor = accept(server_file_descriptor, (struct sockaddr *) &remote_address, &address_length);
|
|
if(client_file_descriptor < 0) {
|
|
if(errno == EMFILE || errno == ENFILE) {
|
|
debugMessage(LOG_QUERY, "[{}] Even with freeing the reserved descriptor accept failed. Attempting to reclaim reserved file descriptor", logging_address(remote_address));
|
|
} else if(errno == EAGAIN) {
|
|
/* Nothing to do */
|
|
} else {
|
|
debugMessage(LOG_QUERY, "[{}] Failed to accept client with reserved file descriptor. ({} | {})", logging_address(remote_address), errno, strerror(errno));
|
|
}
|
|
this->server_reserve_fd = dup(1);
|
|
if(this->server_reserve_fd < 0) {
|
|
debugMessage(LOG_QUERY, "[{}] Failed to reclaim reserved file descriptor. Future clients cant be accepted!", logging_address(remote_address));
|
|
} else {
|
|
tmp_close_success = true;
|
|
}
|
|
return;
|
|
}
|
|
debugMessage(LOG_QUERY, "[{}] Successfully accepted client via reserved descriptor (fd: {}). Initializing socket and sending MOTD and disconnect.", logging_address(remote_address), client_file_descriptor);
|
|
|
|
static auto resource_limit_error = R"(error id=57344 msg=query\sserver\sresource\slimit\sreached extra_msg=file\sdescriptor\slimit\sexceeded)";
|
|
send_direct_disconnect(remote_address, client_file_descriptor, resource_limit_error, strlen(resource_limit_error));
|
|
|
|
this->server_reserve_fd = dup(1);
|
|
if(this->server_reserve_fd < 0) {
|
|
debugMessage(LOG_QUERY, "Failed to reclaim reserved file descriptor. Future clients cant be accepted!");
|
|
} else {
|
|
tmp_close_success = true;
|
|
}
|
|
logMessage(LOG_QUERY, "[{}] Dropping new query connection attempt because of too many open file descriptors.", logging_address(remote_address));
|
|
};
|
|
_();
|
|
}
|
|
}
|
|
|
|
if(!tmp_close_success) {
|
|
debugMessage(LOG_QUERY, "Sleeping two seconds because we're currently having no resources for this user. (Removing the accept event)");
|
|
for(auto& binding : this->bindings) {
|
|
event_del_noblock(binding->event_accept);
|
|
}
|
|
accept_event_deleted = system_clock::now();
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
logMessage(LOG_QUERY, "Got an error while accepting a new client. (errno: {}, message: {})", errno, strerror(errno));
|
|
return;
|
|
}
|
|
|
|
{
|
|
unique_lock lock{this->connected_clients_mutex};
|
|
auto max_connections = serverInstance->properties()[property::SERVERINSTANCE_QUERY_MAX_CONNECTIONS].as_unchecked<size_t>();
|
|
if(max_connections > 0 && max_connections <= this->connected_clients.size()) {
|
|
lock.unlock();
|
|
logMessage(LOG_QUERY, "[{}] Dropping new query connection attempt because of too many connected query clients.", logging_address(remote_address));
|
|
static auto query_server_full = R"(error id=4611 msg=max\sclients\sreached)";
|
|
send_direct_disconnect(remote_address, client_file_descriptor, query_server_full, strlen(query_server_full));
|
|
return;
|
|
}
|
|
|
|
auto max_ip_connections = serverInstance->properties()[property::SERVERINSTANCE_QUERY_MAX_CONNECTIONS_PER_IP].as_unchecked<size_t>();
|
|
if(max_ip_connections > 0) {
|
|
size_t connection_count = 0;
|
|
for(auto& client : this->connected_clients) {
|
|
if(net::address_equal(client->remote_address, remote_address))
|
|
connection_count++;
|
|
}
|
|
|
|
if(connection_count >= max_ip_connections) {
|
|
lock.unlock();
|
|
logMessage(LOG_QUERY, "[{}] Dropping new query connection attempt because of too many simultaneously connected session from this ip.", logging_address(remote_address));
|
|
static auto query_server_full = R"(error id=4610 msg=too\smany\ssimultaneously\sconnected\ssessions)";//
|
|
send_direct_disconnect(remote_address, client_file_descriptor, query_server_full, strlen(query_server_full));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
auto client = std::make_shared<QueryClient>(this, client_file_descriptor);
|
|
memcpy(&client->remote_address, &remote_address, sizeof(remote_address));
|
|
client->initialize_weak_reference(client);
|
|
|
|
{
|
|
lock_guard lock(this->connected_clients_mutex);
|
|
this->connected_clients.push_back(client);
|
|
}
|
|
|
|
client->preInitialize();
|
|
if(client->event_read) {
|
|
event_add(client->event_read, nullptr);
|
|
}
|
|
logMessage(LOG_QUERY, "Got new client from {}", client->getLoggingPeerIp() + ":" + to_string(client->getPeerPort()));
|
|
}
|
|
|
|
/*
|
|
#define QUERY_PASSWORD_LENGTH 7
|
|
std::string QueryServer::resetQueryPassword(const std::shared_ptr<QueryLoginCredentials>& credits, std::string password) {
|
|
if(password.empty()) password = rnd_string(QUERY_PASSWORD_LENGTH);
|
|
auto password_copy = password;
|
|
auto res = sql::command(this->sql, "UPDATE `queries` SET `password` = :password WHERE `uniqueId` = :uid AND `username` = :name", variable{":name", credits->username}, variable{":uid", credits->uniqueId}, variable{":password", password}).execute();
|
|
LOG_SQL_CMD(res);
|
|
return password_copy; //Analize why I have to copy that shit here?
|
|
}
|
|
|
|
std::string QueryServer::createQueryLogin(const string &name, ClientUid uid, string password) {
|
|
bool exists = false;
|
|
sql::command(this->sql, "SELECT * FROM `queries` WHERE `username` = :name", variable{":name", name}).query([](bool* flag, int, char**, char**){
|
|
*flag = true;
|
|
return 0;
|
|
}, &exists);
|
|
if(exists) return "";
|
|
|
|
if(password.empty())
|
|
password = rnd_string(QUERY_PASSWORD_LENGTH);
|
|
sql::command(this->sql, "INSERT INTO `queries` (`username`, `password`, `uniqueId`) VALUES (:name, :password, :uid)", variable{":name", name}, variable{":uid", uid}, variable{":password", password}).execute();
|
|
|
|
return password;
|
|
}
|
|
|
|
bool QueryServer::renameQueryLogin(ClientUid uid, const string &name) {
|
|
bool exists = false;
|
|
sql::command(this->sql, "SELECT * FROM `queries` WHERE `username` = :name", variable{":name", name}).query([](bool* flag, int, char**, char**){
|
|
*flag = true;
|
|
return 0;
|
|
}, &exists);
|
|
if(!exists) return false;
|
|
|
|
auto res = sql::command(this->sql, "UPDATE `queries` SET `username` = :name WHERE `uniqueId` = :uid", variable{":name", name}, variable{":uid", uid}).execute();
|
|
LOG_SQL_CMD(res);
|
|
|
|
return res;
|
|
}
|
|
|
|
std::shared_ptr<QueryLoginCredentials> QueryServer::findQueryLoginByName(const string &name) {
|
|
std::shared_ptr<QueryLoginCredentials> result = std::make_shared<QueryLoginCredentials>();
|
|
sql::command(this->sql, "SELECT * FROM `queries` WHERE `username` = :name", variable{":name", name}).query([](QueryLoginCredentials* res, int length, char** value, char** columns){
|
|
for(int index = 0; index < length; index++)
|
|
if(strcmp(columns[index], "username") == 0)
|
|
res->username = value[index];
|
|
else if(strcmp(columns[index], "password") == 0)
|
|
res->password = value[index];
|
|
else if(strcmp(columns[index], "uniqueId") == 0)
|
|
res->uniqueId = value[index];
|
|
return 0;
|
|
}, result.get());
|
|
if(result->username.empty()) return nullptr;
|
|
return result;
|
|
}
|
|
|
|
std::shared_ptr<QueryLoginCredentials> QueryServer::findQueryLoginByUid(const string &uid) {
|
|
std::shared_ptr<QueryLoginCredentials> result = std::make_shared<QueryLoginCredentials>();
|
|
sql::command(this->sql, "SELECT * FROM `queries` WHERE `uniqueId` = :name", variable{":name", uid}).query([](QueryLoginCredentials *res, int length, char **value, char **columns) {
|
|
for (int index = 0; index < length; index++)
|
|
if (strcmp(columns[index], "username") == 0)
|
|
res->username = value[index];
|
|
else if (strcmp(columns[index], "password") == 0)
|
|
res->password = value[index];
|
|
else if (strcmp(columns[index], "uniqueId") == 0)
|
|
res->uniqueId = value[index];
|
|
return 0;
|
|
}, result.get());
|
|
if (result->username.empty()) return nullptr;
|
|
|
|
return result;
|
|
}
|
|
*/
|
|
|
|
/* api */
|
|
inline deque<shared_ptr<QueryAccount>> query_accounts(sql::command& command) {
|
|
deque<shared_ptr<QueryAccount>> result;
|
|
command.query([&](int length, std::string* value, std::string* columns){
|
|
auto entry = std::make_shared<PasswortedQueryAccount>();
|
|
for(int index = 0; index < length; index++){
|
|
try {
|
|
if(columns[index] == "username")
|
|
entry->username = value[index];
|
|
else if(columns[index] == "password")
|
|
entry->password = value[index];
|
|
else if(columns[index] == "uniqueId")
|
|
entry->unique_id = value[index];
|
|
else if(columns[index] == "server") {
|
|
entry->bound_server = value[index].empty() ? 0 : stoll(value[index]);
|
|
}
|
|
} catch (std::exception& ex) {
|
|
logError(LOG_QUERY, "Failed to parse query account data for row {} ({})", columns[index], value[index]);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
result.push_back(entry);
|
|
return 0;
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
std::shared_ptr<QueryAccount> QueryServer::create_query_account(const std::string &username, ts::ServerId server, const std::string &owner, const std::string &password) {
|
|
LOG_SQL_CMD(sql::command(this->sql, "INSERT INTO `queries` (`username`, `password`, `uniqueId`, `server`) VALUES (:name, :password, :uid, :server)", variable{":name", username}, variable{":uid", owner}, variable{":password", password}, variable{":server", server}).execute());
|
|
|
|
return this->find_query_account_by_name(username);
|
|
}
|
|
|
|
std::shared_ptr<PasswortedQueryAccount> QueryServer::load_password(const std::shared_ptr<QueryAccount> &account) {
|
|
return dynamic_pointer_cast<PasswortedQueryAccount>(account);
|
|
}
|
|
|
|
bool QueryServer::delete_query_account(const std::shared_ptr<ts::server::QueryAccount> &account) {
|
|
LOG_SQL_CMD(sql::command(this->sql, "DELETE FROM `queries` WHERE `username` = :username AND `server` = :server", variable{":username", account->username}, variable{":server", account->bound_server}).execute());
|
|
|
|
return true;
|
|
}
|
|
|
|
std::deque<std::shared_ptr<QueryAccount>> QueryServer::list_query_accounts(OptionalServerId server_id) {
|
|
auto command = (
|
|
server_id != EmptyServerId ?
|
|
sql::command(this->sql, "SELECT * FROM `queries` WHERE `server` = :server", variable{":server", server_id}) :
|
|
sql::command(this->sql, "SELECT * FROM `queries`")
|
|
);
|
|
auto accounts = query_accounts(command);
|
|
return accounts;
|
|
}
|
|
|
|
std::shared_ptr<QueryAccount> QueryServer::find_query_account_by_name(const std::string &name) {
|
|
auto command = sql::command(this->sql, "SELECT * FROM `queries` WHERE `username` = :name", variable{":name", name});
|
|
auto accounts = query_accounts(command);
|
|
if(accounts.empty()) return nullptr;
|
|
return accounts.back();
|
|
}
|
|
|
|
deque<shared_ptr<QueryAccount>> QueryServer::find_query_accounts_by_unique_id(const std::string &unique_id) {
|
|
auto command = sql::command(this->sql, "SELECT * FROM `queries` WHERE `uniqueId` = :unique_id", variable{":name", unique_id});
|
|
return query_accounts(command);
|
|
}
|
|
|
|
bool QueryServer::rename_query_account(const std::shared_ptr<ts::server::QueryAccount> &account, const std::string &new_name) {
|
|
LOG_SQL_CMD(sql::command(this->sql, "UPDATE `queries` SET `username` = :new_name WHERE `username` = :old_name AND `server` = :server", variable{":new_name",new_name}, variable{":old_name", account->username}, variable{":server", account->bound_server}).execute());
|
|
return true;
|
|
}
|
|
|
|
bool QueryServer::change_query_password(const std::shared_ptr<ts::server::QueryAccount> &account, const std::string &password) {
|
|
LOG_SQL_CMD(sql::command(this->sql, "UPDATE `queries` SET `password` = :password WHERE `username` = :name AND `server` = :server", variable{":password", password}, variable{":name", account->username}, variable{":server", account->bound_server}).execute());
|
|
return true;
|
|
}
|
|
|
|
void QueryServer::tick_clients() {
|
|
decltype(this->connected_clients) connected_clients_;
|
|
{
|
|
lock_guard lock(this->connected_clients_mutex);
|
|
connected_clients_ = this->connected_clients;
|
|
}
|
|
|
|
for(const auto& cl : connected_clients_) {
|
|
cl->tick_query();
|
|
}
|
|
|
|
{
|
|
std::lock_guard connect_lock{this->client_connect_mutex};
|
|
|
|
std::vector<std::string> erase_bans{};
|
|
erase_bans.reserve(32);
|
|
|
|
for(auto& elm : this->client_connect_bans) {
|
|
if(elm.second < system_clock::now()) {
|
|
erase_bans.push_back(elm.first);
|
|
}
|
|
}
|
|
|
|
for(const auto& ip : erase_bans) {
|
|
this->client_connect_bans.erase(ip);
|
|
}
|
|
|
|
if(system_clock::now() - seconds(5) < client_connect_last_decrease) {
|
|
this->client_connect_last_decrease = system_clock::now();
|
|
|
|
std::vector<std::string> erase_attempts{};
|
|
for(auto& elm : this->client_connect_count) {
|
|
if(elm.second == 0) {
|
|
erase_attempts.push_back(elm.first);
|
|
} else {
|
|
elm.second--;
|
|
}
|
|
}
|
|
|
|
for(const auto& ip : erase_bans) {
|
|
this->client_connect_count.erase(ip);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(this->accept_event_deleted.time_since_epoch().count() != 0 && accept_event_deleted + seconds(5) < system_clock::now()) {
|
|
debugMessage(LOG_QUERY, "Readding accept event and try again if we have enough resources again.");
|
|
for(auto& binding : this->bindings) {
|
|
event_add(binding->event_accept, nullptr);
|
|
}
|
|
accept_event_deleted = system_clock::time_point{};
|
|
}
|
|
}
|
|
|
|
void QueryServer::tick_executor() {
|
|
bool tick_clients;
|
|
|
|
while(this->tick_active) {
|
|
std::unique_lock tick_lock{this->tick_mutex};
|
|
this->tick_notify.wait_until(tick_lock, this->tick_next_client_timestamp, [&]{
|
|
return !this->tick_active || !this->tick_pending_disconnects.empty() || !this->tick_pending_connection_close.empty();
|
|
});
|
|
|
|
auto current_timestamp = std::chrono::system_clock::now();
|
|
if(current_timestamp > this->tick_next_client_timestamp) {
|
|
this->tick_next_client_timestamp = current_timestamp + std::chrono::milliseconds{500};
|
|
tick_clients = true;
|
|
} else {
|
|
tick_clients = false;
|
|
}
|
|
|
|
auto pending_disconnects = std::move(this->tick_pending_disconnects);
|
|
auto pending_closes = std::move(this->tick_pending_connection_close);
|
|
|
|
if(!this->tick_active) {
|
|
if(this->tick_pending_connection_close.empty() && this->tick_pending_connection_close.empty()) {
|
|
/* We're done with our work */
|
|
break;
|
|
}
|
|
}
|
|
tick_lock.unlock();
|
|
|
|
if(tick_clients) {
|
|
this->tick_clients();
|
|
}
|
|
|
|
for(const auto& pending_disconnect : pending_disconnects) {
|
|
auto client = pending_disconnect.lock();
|
|
if(!client) {
|
|
continue;
|
|
}
|
|
|
|
this->execute_query_disconnect(client, false);
|
|
}
|
|
|
|
for(const auto& pending_close : pending_closes) {
|
|
auto client = pending_close.lock();
|
|
if(!client) {
|
|
continue;
|
|
}
|
|
|
|
this->execute_query_connection_close(client, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void QueryServer::enqueue_query_disconnect(const std::shared_ptr<QueryClient> &client) {
|
|
std::lock_guard lock{this->tick_mutex};
|
|
if(!this->tick_active) {
|
|
logCritical(LOG_GENERAL, "Tried to close a query connection without an active query event loop.");
|
|
return;
|
|
}
|
|
|
|
this->tick_pending_disconnects.push_back(client);
|
|
this->tick_notify.notify_one();
|
|
}
|
|
|
|
void QueryServer::enqueue_query_connection_close(const std::shared_ptr<QueryClient> &client) {
|
|
std::lock_guard lock{this->tick_mutex};
|
|
if(!this->tick_active) {
|
|
logCritical(LOG_GENERAL, "Tried to close a query connection without an active query event loop.");
|
|
return;
|
|
}
|
|
|
|
this->tick_pending_connection_close.push_back(client);
|
|
this->tick_notify.notify_one();
|
|
}
|
|
|
|
void QueryServer::execute_query_disconnect(const std::shared_ptr<QueryClient> &client, bool shutdown_disconnect) {
|
|
{
|
|
std::lock_guard state_lock{client->state_lock};
|
|
if(client->state >= ConnectionState::DISCONNECTING) {
|
|
/* client will already be disconnected */
|
|
return;
|
|
}
|
|
|
|
client->state = ConnectionState::DISCONNECTING;
|
|
}
|
|
|
|
if(!shutdown_disconnect) {
|
|
client->disconnect_from_virtual_server("");
|
|
}
|
|
|
|
{
|
|
std::lock_guard network_lock{client->network_mutex};
|
|
if(client->event_write) {
|
|
event_add(client->event_write, nullptr);
|
|
}
|
|
}
|
|
}
|
|
|
|
void QueryServer::execute_query_connection_close(const std::shared_ptr<QueryClient> &client, bool warn_unknown_client) {
|
|
{
|
|
std::lock_guard state_lock{client->state_lock};
|
|
if(client->state == ConnectionState::DISCONNECTED) {
|
|
/* client has already been disconnected */
|
|
return;
|
|
}
|
|
|
|
client->state = ConnectionState::DISCONNECTED;
|
|
}
|
|
|
|
client->disconnect_from_virtual_server("");
|
|
client->execute_final_disconnect();
|
|
|
|
{
|
|
std::lock_guard client_lock{this->connected_clients_mutex};
|
|
auto index = std::find(this->connected_clients.begin(), this->connected_clients.end(), client);
|
|
if(index == this->connected_clients.end()) {
|
|
if(warn_unknown_client) {
|
|
logWarning(LOG_QUERY, "Closed the connection of an unknown/unregistered query.");
|
|
}
|
|
return;
|
|
}
|
|
|
|
this->connected_clients.erase(index);
|
|
this->connected_client_disconnected_notify.notify_all();
|
|
}
|
|
} |