This commit is contained in:
2023-05-26 16:09:29 +02:00
commit 32db868ae6
83 changed files with 5753 additions and 0 deletions

View File

@ -0,0 +1,60 @@
#include <std_include.hpp>
#include "elimination_handler.hpp"
constexpr auto T7_PROTOCOL = 7;
constexpr size_t MAX_SERVERS_PER_GAME = 15;
void elimination_handler::run_frame()
{
std::unordered_map<game_type, std::unordered_map<network::address, size_t>> server_count;
auto now = std::chrono::high_resolution_clock::now();
this->get_server().get_server_list().iterate([&](server_list::iteration_context& context)
{
auto& server = context.get();
const auto diff = now - server.heartbeat;
if ((server.state == game_server::state::pinged && diff > 2min) ||
(server.state == game_server::state::can_ping && diff > 15min))
{
context.remove();
}
if (server.game == game_type::unknown)
{
return;
}
if (server.game == game_type::t7 && server.protocol < T7_PROTOCOL)
{
#ifdef _DEBUG
console::info("Removing T7 server '%s' because protocol %i is less than %i\n",
context.get_address().to_string().data(), server.protocol, T7_PROTOCOL);
#endif
context.remove();
}
++server_count[server.game][context.get_address()];
if (server_count[server.game][context.get_address()] >= MAX_SERVERS_PER_GAME)
{
#ifdef _DEBUG
console::info("Removing server '%s' because it exceeds MAX_SERVERS_PER_GAME\n",
context.get_address().to_string().data());
#endif
context.remove();
}
});
now = std::chrono::high_resolution_clock::now();
this->get_server().get_client_list().iterate([&](client_list::iteration_context& context)
{
auto& client = context.get();
const auto diff = now - client.heartbeat;
if (diff > 5min || (!client.registered && diff > 20s))
{
context.remove();
}
});
}

View File

@ -0,0 +1,11 @@
#pragma once
#include "../service.hpp"
class elimination_handler : public service
{
public:
using service::service;
void run_frame() override;
};

View File

@ -0,0 +1,56 @@
#include <std_include.hpp>
#include "getbots_command.hpp"
#include "../console.hpp"
const char* getbots_command::get_command() const
{
return "getbots";
}
void getbots_command::handle_command(const network::address& target, const std::string_view&)
{
static const std::vector<std::string> bot_names
{
"aerosoul",
"Eldor",
"FutureRave",
"Girl",
"INeedBots",
"INeedGames",
"Infamous",
"Jebus3211",
"Joel",
"JTAG",
"Laupetin",
"Louvenarde",
"OneFourOne",
"PeterG",
"quaK",
"RezTech",
"sass",
"Slykuiper",
"st0rm",
"xensik",
"xoxor4d",
"Diamante",
"Dsso",
"Evan",
"FragsAreUs",
"FryTechTip",
"H3X1C",
"homura",
"Jimbo",
"RektInator",
"Squirrel",
};
std::stringstream stream{};
for (const auto& bot : bot_names)
{
stream << bot << std::endl;
}
this->get_server().send(target, "getbotsResponse", stream.str());
console::log("Sent bot names: %s", target.to_string().data());
}

View File

@ -0,0 +1,12 @@
#pragma once
#include "../service.hpp"
class getbots_command : public service
{
public:
using service::service;
const char* get_command() const override;
void handle_command(const network::address& target, const std::string_view& data) override;
};

View File

@ -0,0 +1,96 @@
#include <std_include.hpp>
#include "getservers_command.hpp"
#include "../console.hpp"
#include <utils/parameters.hpp>
namespace
{
struct prepared_server
{
uint32_t address;
uint16_t port;
};
}
constexpr auto MTU = 1400; // Real UDP MTU is more like 1500 bytes, but we keep a little wiggle room just in case
constexpr auto DPM_PROTOCOL_ADDRESS_LENGTH = sizeof prepared_server::address;
constexpr auto DPM_PROTOCOL_PORT_LENGTH = sizeof prepared_server::port;
static_assert(DPM_PROTOCOL_ADDRESS_LENGTH == 4);
static_assert(DPM_PROTOCOL_PORT_LENGTH == 2);
const char* getservers_command::get_command() const
{
return "getservers";
}
void getservers_command::handle_command(const network::address& target, const std::string_view& data)
{
const utils::parameters params(data);
if (params.size() < 2)
{
throw execution_exception("Invalid parameter count");
}
const auto& game = params[0];
const auto* p = params[1].data();
char* end;
const auto protocol = strtol(params[1].data(), &end, 10);
if (p == end)
{
throw execution_exception("Invalid protocol");
}
const auto game_type = resolve_game_type(game);
if (game_type == game_type::unknown)
{
throw execution_exception("Invalid game type: " + game);
}
std::queue<prepared_server> prepared_servers{};
this->get_server().get_server_list() //
.find_registered_servers(game_type, protocol,
[&prepared_servers](const game_server&, const network::address& address)
{
const auto addr = address.get_in_addr().sin_addr.s_addr;
const auto port = htons(address.get_port());
prepared_servers.push({ addr, port });
});
size_t packet_count = 0;
std::string response{};
while (!prepared_servers.empty())
{
const auto& server = prepared_servers.front();
response.push_back('\\');
response.append(reinterpret_cast<const char*>(&server.address), DPM_PROTOCOL_ADDRESS_LENGTH);
response.append(reinterpret_cast<const char*>(&server.port), DPM_PROTOCOL_PORT_LENGTH);
prepared_servers.pop();
if (response.size() >= MTU || prepared_servers.empty())
{
// Only send EOT if the queue is empty (last packet)
if (prepared_servers.empty())
{
response.push_back('\\');
response.append("EOT");
response.push_back('\0');
response.push_back('\0');
response.push_back('\0');
}
this->get_server().send(target, "getserversResponse", response);
packet_count++;
response.clear();
}
}
console::log("Sent %zu servers in %zu parts for game %s:\t%s", prepared_servers.size(), packet_count, game.data(), target.to_string().data());
}

View File

@ -0,0 +1,12 @@
#pragma once
#include "../service.hpp"
class getservers_command : public service
{
public:
using service::service;
const char* get_command() const override;
void handle_command(const network::address& target, const std::string_view& data) override;
};

View File

@ -0,0 +1,12 @@
#include <std_include.hpp>
#include "heartbeat_command.hpp"
const char* heartbeat_command::get_command() const
{
return "heartbeat";
}
void heartbeat_command::handle_command(const network::address& target, [[maybe_unused]] const std::string_view& data)
{
this->get_server().get_server_list().heartbeat(target);
}

View File

@ -0,0 +1,12 @@
#pragma once
#include "../service.hpp"
class heartbeat_command : public service
{
public:
using service::service;
const char* get_command() const override;
void handle_command(const network::address& target, const std::string_view& data) override;
};

View File

@ -0,0 +1,60 @@
#include <std_include.hpp>
#include "info_response_command.hpp"
#include "../console.hpp"
const char* info_response_command::get_command() const
{
return "infoResponse";
}
void info_response_command::handle_command(const network::address& target, const std::string_view& data)
{
const auto found = this->get_server().get_server_list().find(
target, [&data](game_server& server, const network::address& address)
{
utils::info_string info{data};
const auto game = info.get("gamename");
const auto challenge = info.get("challenge");
const auto game_type = resolve_game_type(game);
if (game_type == game_type::unknown)
{
server.state = game_server::state::dead;
throw execution_exception{"Invalid game type: " + game};
}
if (server.state != game_server::state::pinged)
{
throw execution_exception{"Stray info response"};
}
if (challenge != server.challenge)
{
throw execution_exception{"Invalid challenge"};
}
const auto player_count = atoi(info.get("clients").data());
const auto bot_count = atoi(info.get("bots").data());
auto real_player_count = player_count - bot_count;
real_player_count = std::clamp(real_player_count, 0, 18);
server.registered = true;
server.game = game_type;
server.state = game_server::state::can_ping;
server.protocol = atoi(info.get("protocol").data());
server.clients = static_cast<unsigned int>(real_player_count);
server.name = info.get("hostname");
server.heartbeat = std::chrono::high_resolution_clock::now();
server.info_string = std::move(info);
console::log("Server registered for game %s (%i):\t%s\t- %s", game.data(), server.protocol,
address.to_string().data(), server.name.data());
});
if (!found)
{
throw execution_exception{"infoResponse without server!"};
}
}

View File

@ -0,0 +1,12 @@
#pragma once
#include "../service.hpp"
class info_response_command : public service
{
public:
using service::service;
const char* get_command() const override;
void handle_command(const network::address& target, const std::string_view& data) override;
};

153
src/services/kill_list.cpp Normal file
View File

@ -0,0 +1,153 @@
#include "std_include.hpp"
#include "kill_list.hpp"
#include <utils/io.hpp>
constexpr auto* kill_file = "./kill.txt";
kill_list::kill_list_entry::kill_list_entry(std::string ip_address, std::string reason)
: ip_address_(std::move(ip_address)), reason_(std::move(reason))
{
}
bool kill_list::contains(const network::address& address, std::string& reason)
{
auto str_address = address.to_string(false);
return this->entries_container_.access<bool>([&str_address, &reason](const kill_list_entries& entries)
{
if (const auto itr = entries.find(str_address); itr != entries.end())
{
reason = itr->second.reason_;
return true;
}
return false;
});
}
void kill_list::add_to_kill_list(const kill_list_entry& add)
{
const auto any_change = this->entries_container_.access<bool>([&add](kill_list_entries& entries)
{
const auto existing_entry = entries.find(add.ip_address_);
if (existing_entry == entries.end() || existing_entry->second.reason_ != add.reason_)
{
console::info("Added %s to kill list (reason: %s)", add.ip_address_.data(), add.reason_.data());
entries[add.ip_address_] = add;
return true;
}
return false;
});
if (!any_change)
{
console::info("%s already in kill list, doing nothing", add.ip_address_.data());
return;
}
this->write_to_disk();
}
void kill_list::remove_from_kill_list(const network::address& remove)
{
this->remove_from_kill_list(remove.to_string());
}
void kill_list::remove_from_kill_list(const std::string& remove)
{
const auto any_change = this->entries_container_.access<bool>([&remove](kill_list_entries& entries)
{
if (entries.erase(remove))
{
console::info("Removed %s from kill list", remove.data());
return true;
}
return false;
});
if (!any_change)
{
console::info("%s not in kill list, doing nothing", remove.data());
return;
}
this->write_to_disk();
}
void kill_list::reload_from_disk()
{
std::string contents;
if (!utils::io::read_file(kill_file, &contents))
{
console::info("Could not find %s, kill list will not be loaded.", kill_file);
return;
}
std::istringstream string_stream(contents);
std::string line;
this->entries_container_.access([&string_stream, &line](kill_list_entries& entries)
{
entries.clear();
while (std::getline(string_stream, line))
{
if (line[0] == '#')
{
// comments or ignored line
continue;
}
std::string ip;
std::string comment;
const auto index = line.find(' ');
if (index != std::string::npos)
{
ip = line.substr(0, index);
comment = line.substr(index + 1);
}
else
{
ip = line;
}
if (ip.empty())
{
continue;
}
// Double line breaks from windows' \r\n
if (ip[ip.size() - 1] == '\r')
{
ip.pop_back();
}
entries.emplace(ip, kill_list_entry(ip, comment));
}
console::info("Loaded %zu kill list entries from %s", entries.size(), kill_file);
});
}
void kill_list::write_to_disk()
{
std::ostringstream stream;
this->entries_container_.access([&stream](const kill_list_entries& entries)
{
for (const auto& [ip, entry] : entries)
{
stream << entry.ip_address_ << " " << entry.reason_ << "\n";
}
utils::io::write_file(kill_file, stream.str(), false);
console::info("Wrote %s to disk (%zu entries)", kill_file, entries.size());
});
}
kill_list::kill_list(server& server) : service(server)
{
this->reload_from_disk();
}

View File

@ -0,0 +1,32 @@
#pragma once
#include <network/address.hpp>
#include "../service.hpp"
class kill_list : public service
{
public:
class kill_list_entry
{
public:
kill_list_entry() = default;
kill_list_entry(std::string ip_address, std::string reason);
std::string ip_address_;
std::string reason_;
};
kill_list(server& server);
bool contains(const network::address& address, std::string& reason);
void add_to_kill_list(const kill_list_entry& add);
void remove_from_kill_list(const network::address& remove);
void remove_from_kill_list(const std::string& remove);
private:
using kill_list_entries = std::unordered_map<std::string, kill_list_entry>;
utils::concurrency::container<kill_list_entries> entries_container_;
void reload_from_disk();
void write_to_disk();
};

View File

@ -0,0 +1,65 @@
#include <std_include.hpp>
#include "patch_kill_list_command.hpp"
#include "crypto_key.hpp"
#include "services/kill_list.hpp"
#include <utils/parameters.hpp>
#include <utils/io.hpp>
#include <utils/string.hpp>
const char* patch_kill_list_command::get_command() const
{
return "patchkill";
}
// patchkill timestamp signature add/remove target_ip (ban_reason)
void patch_kill_list_command::handle_command([[maybe_unused]] const network::address& target, const std::string_view& data)
{
const utils::parameters params(data);
if (params.size() < 3)
{
throw execution_exception("Invalid parameter count");
}
const auto supplied_timestamp = std::chrono::seconds(std::stoul(params[0]));
const auto current_timestamp = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch());
// Abs the duration so that the client can be ahead or behind
const auto time_stretch = std::chrono::abs(current_timestamp - supplied_timestamp);
// not offset by more than 5 minutes in either direction
if (time_stretch > 5min)
{
throw execution_exception(utils::string::va("Invalid timestamp supplied - expected %llu, got %llu, which is more than 5 minutes apart", current_timestamp.count(), supplied_timestamp.count()));
}
const auto& signature = utils::cryptography::base64::decode(params[1]);
const auto should_remove = params[2] == "remove"s;
if (!should_remove && params[2] != "add"s)
{
throw execution_exception("Invalid parameter #2: should be 'add' or 'remove'");
}
const auto supplied_reason = params.join(4);
const auto& crypto_key = crypto_key::get();
const auto signature_candidate = std::to_string(supplied_timestamp.count());
if (!utils::cryptography::ecc::verify_message(crypto_key, signature_candidate, signature))
{
throw execution_exception("Signature verification of the kill list patch key failed");
}
const auto kill_list_service = this->get_server().get_service<kill_list>();
const auto& supplied_address = params[3];
if (should_remove)
{
kill_list_service->remove_from_kill_list(supplied_address);
}
else
{
kill_list_service->add_to_kill_list(kill_list::kill_list_entry(supplied_address, supplied_reason));
}
}

View File

@ -0,0 +1,12 @@
#pragma once
#include "../service.hpp"
class patch_kill_list_command : public service
{
public:
using service::service;
const char* get_command() const override;
void handle_command(const network::address& target, const std::string_view& data) override;
};

View File

@ -0,0 +1,26 @@
#include <std_include.hpp>
#include "ping_handler.hpp"
#include <utils/cryptography.hpp>
void ping_handler::run_frame()
{
auto count = 0;
this->get_server().get_server_list().iterate([&](server_list::iteration_context& context)
{
auto& server = context.get();
if (server.state == game_server::state::needs_ping)
{
server.state = game_server::state::pinged;
server.challenge = utils::cryptography::random::get_challenge();
server.heartbeat = std::chrono::high_resolution_clock::now();
this->get_server().send(context.get_address(), "getinfo", server.challenge);
if (20 >= ++count)
{
context.stop_iterating();
}
}
});
}

View File

@ -0,0 +1,11 @@
#pragma once
#include "../service.hpp"
class ping_handler : public service
{
public:
using service::service;
void run_frame() override;
};

View File

@ -0,0 +1,86 @@
#include <std_include.hpp>
#include "statistics_handler.hpp"
#include "../console.hpp"
#include <utils/io.hpp>
#include <utils/string.hpp>
namespace
{
void write_stats(const std::map<game_type, std::vector<std::pair<std::string, network::address>>>& servers,
std::map<game_type, uint32_t>& players)
{
rapidjson::Document root{};
root.SetObject();
for (const auto& game_servers : servers)
{
const auto server_count = static_cast<uint32_t>(game_servers.second.size());
const auto player_count = players[game_servers.first];
rapidjson::Value game{};
game.SetObject();
game.AddMember("servers", server_count, root.GetAllocator());
game.AddMember("players", player_count, root.GetAllocator());
auto game_name = resolve_game_type_name(game_servers.first);
game_name = utils::string::to_lower(game_name);
rapidjson::Value game_name_object(game_name, root.GetAllocator());
root.AddMember(game_name_object, game, root.GetAllocator());
}
rapidjson::StringBuffer root_buffer{};
rapidjson::Writer<rapidjson::StringBuffer, rapidjson::Document::EncodingType, rapidjson::ASCII<>>
root_writer(root_buffer);
root.Accept(root_writer);
std::string root_data(root_buffer.GetString(), root_buffer.GetLength());
utils::io::write_file("/var/www/master.xlabs.dev/stats.json", root_data);
}
}
statistics_handler::statistics_handler(server& server)
: service(server)
, last_print(std::chrono::high_resolution_clock::now())
{
}
void statistics_handler::run_frame()
{
const auto now = std::chrono::high_resolution_clock::now();
if (now - this->last_print < 5min)
{
return;
}
std::map<game_type, uint32_t> players{};
std::map<game_type, std::vector<std::pair<std::string, network::address>>> servers;
this->last_print = std::chrono::high_resolution_clock::now();
this->get_server().get_server_list().iterate([&servers, &players](const server_list::iteration_context& context)
{
const auto& server = context.get();
if (server.registered)
{
servers[server.game].emplace_back(server.name, context.get_address());
players[server.game] += server.clients;
}
});
console::lock _{};
for (const auto& game_servers : servers)
{
console::log("%s (%d):", resolve_game_type_name(game_servers.first).data(),
static_cast<uint32_t>(game_servers.second.size()));
for (const auto& server : game_servers.second)
{
console::log("\t%s\t%s", server.second.to_string().data(), server.first.data());
}
}
write_stats(servers, players);
}

View File

@ -0,0 +1,14 @@
#pragma once
#include "../service.hpp"
class statistics_handler : public service
{
public:
statistics_handler(server& server);
void run_frame() override;
private:
std::chrono::high_resolution_clock::time_point last_print;
};